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