diff --git a/docker-compose.yml b/docker-compose.yml
index 82642df..deae354 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -16,6 +16,11 @@ services:
interval: 5s
timeout: 5s
retries: 5
+ redis:
+ image: redis:7-alpine
+ container_name: redis
+ ports:
+ - "6379:6379"
app:
build: .
diff --git a/pom.xml b/pom.xml
index bea7224..34b9702 100644
--- a/pom.xml
+++ b/pom.xml
@@ -49,6 +49,10 @@
org.springframework.boot
spring-boot-starter-amqp
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
io.jsonwebtoken
jjwt-api
diff --git a/src/main/java/com/guilherme/finance_api/config/RedisConfig.java b/src/main/java/com/guilherme/finance_api/config/RedisConfig.java
new file mode 100644
index 0000000..4a530ed
--- /dev/null
+++ b/src/main/java/com/guilherme/finance_api/config/RedisConfig.java
@@ -0,0 +1,34 @@
+package com.guilherme.finance_api.config;
+
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.RedisSerializationContext;
+
+import java.time.Duration;
+
+@Configuration
+@EnableCaching
+public class RedisConfig {
+
+ @Bean
+ @Primary
+ public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
+ RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
+ .entryTtl(Duration.ofMinutes(5))
+ .serializeValuesWith(
+ RedisSerializationContext.SerializationPair.fromSerializer(
+ new GenericJackson2JsonRedisSerializer()
+ )
+ );
+
+ return RedisCacheManager.builder(connectionFactory)
+ .cacheDefaults(config)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/guilherme/finance_api/controller/TransactionController.java b/src/main/java/com/guilherme/finance_api/controller/TransactionController.java
index 1940fda..b219161 100644
--- a/src/main/java/com/guilherme/finance_api/controller/TransactionController.java
+++ b/src/main/java/com/guilherme/finance_api/controller/TransactionController.java
@@ -3,6 +3,7 @@
import com.guilherme.finance_api.dto.TransactionFilter;
import com.guilherme.finance_api.dto.TransactionRequest;
import com.guilherme.finance_api.dto.TransactionResponse;
+import com.guilherme.finance_api.dto.TransactionSummary;
import com.guilherme.finance_api.entity.TransactionType;
import com.guilherme.finance_api.service.TransactionService;
import jakarta.validation.Valid;
@@ -13,6 +14,7 @@
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import org.springframework.data.domain.Pageable;
@@ -60,6 +62,15 @@ public ResponseEntity save(@Valid @RequestBody TransactionR
return ResponseEntity.status(HttpStatus.CREATED).body(savedTransaction);
}
+ @GetMapping("/summary")
+ public ResponseEntity getSummary(
+ @RequestParam(required = false) String month) {
+ String email = SecurityContextHolder.getContext()
+ .getAuthentication()
+ .getName();
+ return ResponseEntity.ok(transactionService.getSummary(month, email));
+ }
+
@DeleteMapping("/{id}")
public ResponseEntity delete(@PathVariable Long id) {
transactionService.delete(id);
diff --git a/src/main/java/com/guilherme/finance_api/dto/TransactionSummary.java b/src/main/java/com/guilherme/finance_api/dto/TransactionSummary.java
new file mode 100644
index 0000000..75db384
--- /dev/null
+++ b/src/main/java/com/guilherme/finance_api/dto/TransactionSummary.java
@@ -0,0 +1,17 @@
+package com.guilherme.finance_api.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class TransactionSummary {
+ private BigDecimal totalIncome;
+ private BigDecimal totalExpense;
+ private BigDecimal balance;
+ private String month;
+}
\ No newline at end of file
diff --git a/src/main/java/com/guilherme/finance_api/service/TransactionService.java b/src/main/java/com/guilherme/finance_api/service/TransactionService.java
index 144fdf5..07beaee 100644
--- a/src/main/java/com/guilherme/finance_api/service/TransactionService.java
+++ b/src/main/java/com/guilherme/finance_api/service/TransactionService.java
@@ -3,8 +3,10 @@
import com.guilherme.finance_api.dto.TransactionFilter;
import com.guilherme.finance_api.dto.TransactionRequest;
import com.guilherme.finance_api.dto.TransactionResponse;
+import com.guilherme.finance_api.dto.TransactionSummary;
import com.guilherme.finance_api.entity.Category;
import com.guilherme.finance_api.entity.Transaction;
+import com.guilherme.finance_api.entity.TransactionType;
import com.guilherme.finance_api.entity.User;
import com.guilherme.finance_api.event.TransactionEvent;
import com.guilherme.finance_api.event.TransactionPublisher;
@@ -13,8 +15,10 @@
import com.guilherme.finance_api.repository.TransactionRepository;
import com.guilherme.finance_api.repository.UserRepository;
import com.guilherme.finance_api.specification.TransactionSpecification;
+import org.springframework.cache.annotation.Cacheable;
import lombok.Data;
import lombok.RequiredArgsConstructor;
+import org.springframework.cache.annotation.CacheEvict;
import org.springframework.data.domain.Page;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.security.core.context.SecurityContextHolder;
@@ -22,6 +26,12 @@
import org.springframework.data.domain.Pageable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.YearMonth;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
@Data
@Service
@@ -80,6 +90,7 @@ public TransactionResponse update(Long id, TransactionRequest request) {
return toResponse(savedTransaction);
}
+ @CacheEvict(value = "transaction-summary", allEntries = true)
public TransactionResponse save(TransactionRequest request) {
Transaction transaction = new Transaction();
transaction.setDescription(request.getDescription());
@@ -114,6 +125,42 @@ public TransactionResponse save(TransactionRequest request) {
return toResponse(savedTransaction);
}
+ @Cacheable(value = "transaction-summary", key = "#month ?: 'current' + '::' + #email")
+ public TransactionSummary getSummary(String month, String email) {
+ User user = userRepository.findByEmail(email)
+ .orElseThrow(() -> new ResourceNotFoundException("User not found"));
+
+ YearMonth yearMonth = month != null
+ ? YearMonth.parse(month)
+ : YearMonth.now();
+
+ LocalDate start = yearMonth.atDay(1);
+ LocalDate end = yearMonth.atEndOfMonth();
+
+ Specification spec = Specification.unrestricted()
+ .and(TransactionSpecification.hasUser(user.getId()))
+ .and(TransactionSpecification.dateBetween(start, end));
+
+ List transactions = transactionRepository.findAll(spec);
+
+ BigDecimal totalIncome = transactions.stream()
+ .filter(t -> t.getType() == TransactionType.INCOME)
+ .map(Transaction::getAmount)
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+ BigDecimal totalExpense = transactions.stream()
+ .filter(t -> t.getType() == TransactionType.EXPENSE)
+ .map(Transaction::getAmount)
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+ return new TransactionSummary(
+ totalIncome,
+ totalExpense,
+ totalIncome.subtract(totalExpense),
+ yearMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"))
+ );
+ }
+
public void delete(Long id) {
if (!transactionRepository.existsById(id)) {
throw new ResourceNotFoundException("Transaction not found");
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 0a45536..07977e2 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -13,3 +13,5 @@ spring.rabbitmq.username=${RABBITMQ_USER:guest}
spring.rabbitmq.password=${RABBITMQ_PASSWORD:guest}
rabbitmq.exchange=transactions.exchange
rabbitmq.routing-key=transactions.created
+spring.data.redis.host=${REDIS_HOST:localhost}
+spring.data.redis.port=${REDIS_PORT:6379}
\ No newline at end of file