diff --git a/src/main/java/com/financebot/alert/domain/ExcessiveInstallmentAlert.java b/src/main/java/com/financebot/alert/domain/ExcessiveInstallmentAlert.java new file mode 100644 index 0000000..abe48dc --- /dev/null +++ b/src/main/java/com/financebot/alert/domain/ExcessiveInstallmentAlert.java @@ -0,0 +1,8 @@ +package com.financebot.alert.domain; + +public record ExcessiveInstallmentAlert( + long activeInstallmentGroups, + int threshold, + String explanation +) { +} diff --git a/src/main/java/com/financebot/alert/port/InstallmentRiskPort.java b/src/main/java/com/financebot/alert/port/InstallmentRiskPort.java new file mode 100644 index 0000000..253a065 --- /dev/null +++ b/src/main/java/com/financebot/alert/port/InstallmentRiskPort.java @@ -0,0 +1,8 @@ +package com.financebot.alert.port; + +import java.time.LocalDate; + +public interface InstallmentRiskPort { + + Long countDistinctActiveInstallmentGroupsByUser(Long userId, LocalDate today); +} diff --git a/src/main/java/com/financebot/alert/service/ExcessiveInstallmentDetector.java b/src/main/java/com/financebot/alert/service/ExcessiveInstallmentDetector.java new file mode 100644 index 0000000..c48c490 --- /dev/null +++ b/src/main/java/com/financebot/alert/service/ExcessiveInstallmentDetector.java @@ -0,0 +1,44 @@ +package com.financebot.alert.service; + +import com.financebot.alert.domain.ExcessiveInstallmentAlert; +import com.financebot.alert.port.InstallmentRiskPort; +import com.financebot.user.domain.User; +import org.springframework.stereotype.Service; + +import java.time.Clock; +import java.time.LocalDate; + +@Service +public class ExcessiveInstallmentDetector { + + public static final int DEFAULT_THRESHOLD = 5; + + private final InstallmentRiskPort installmentRiskPort; + private final Clock clock; + + public ExcessiveInstallmentDetector(InstallmentRiskPort installmentRiskPort, Clock clock) { + this.installmentRiskPort = installmentRiskPort; + this.clock = clock; + } + + public ExcessiveInstallmentAlert detect(User user) { + if (user == null || user.getId() == null) { + throw new IllegalArgumentException("User with id is required"); + } + long activeGroups = valueOrZero(installmentRiskPort + .countDistinctActiveInstallmentGroupsByUser(user.getId(), LocalDate.now(clock))); + if (activeGroups < DEFAULT_THRESHOLD) { + return null; + } + return new ExcessiveInstallmentAlert( + activeGroups, + DEFAULT_THRESHOLD, + "Você tem " + activeGroups + " grupos de parcelas ativos; a partir de " + + DEFAULT_THRESHOLD + " o comprometimento merece revisão." + ); + } + + private long valueOrZero(Long value) { + return value == null ? 0 : value; + } +} diff --git a/src/main/java/com/financebot/transaction/repository/TransactionRepository.java b/src/main/java/com/financebot/transaction/repository/TransactionRepository.java index 8e0989f..0b2fd6c 100644 --- a/src/main/java/com/financebot/transaction/repository/TransactionRepository.java +++ b/src/main/java/com/financebot/transaction/repository/TransactionRepository.java @@ -1,6 +1,7 @@ package com.financebot.transaction.repository; import com.financebot.alert.port.ExpenseHistoryPort; +import com.financebot.alert.port.InstallmentRiskPort; import com.financebot.transaction.domain.Transaction; import com.financebot.transaction.domain.TransactionType; import org.springframework.data.jpa.repository.JpaRepository; @@ -13,7 +14,8 @@ import java.util.List; import java.util.Optional; -public interface TransactionRepository extends JpaRepository, JpaSpecificationExecutor, ExpenseHistoryPort { +public interface TransactionRepository extends JpaRepository, JpaSpecificationExecutor, + ExpenseHistoryPort, InstallmentRiskPort { Optional findByIdAndUserId(Long id, Long userId); diff --git a/src/test/java/com/financebot/alert/service/ExcessiveInstallmentDetectorTest.java b/src/test/java/com/financebot/alert/service/ExcessiveInstallmentDetectorTest.java new file mode 100644 index 0000000..030ef0c --- /dev/null +++ b/src/test/java/com/financebot/alert/service/ExcessiveInstallmentDetectorTest.java @@ -0,0 +1,65 @@ +package com.financebot.alert.service; + +import com.financebot.alert.port.InstallmentRiskPort; +import com.financebot.user.domain.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneOffset; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ExcessiveInstallmentDetectorTest { + + private final FakeInstallmentRiskPort port = new FakeInstallmentRiskPort(); + private ExcessiveInstallmentDetector detector; + private User user; + + @BeforeEach + void setUp() { + detector = new ExcessiveInstallmentDetector( + port, + Clock.fixed(Instant.parse("2026-09-15T12:00:00Z"), ZoneOffset.UTC) + ); + user = new User(); + user.setId(1L); + } + + @Test + void shouldDetectFiveOrMoreActiveInstallmentGroups() { + port.count = 5L; + + var alert = detector.detect(user); + + assertThat(alert).isNotNull(); + assertThat(alert.activeInstallmentGroups()).isEqualTo(5); + assertThat(alert.explanation()).contains("5 grupos"); + } + + @Test + void shouldIgnoreCountBelowThreshold() { + port.count = 4L; + + assertThat(detector.detect(user)).isNull(); + } + + @Test + void shouldRejectUserWithoutId() { + assertThatThrownBy(() -> detector.detect(new User())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("User with id is required"); + } + + private static final class FakeInstallmentRiskPort implements InstallmentRiskPort { + private Long count; + + @Override + public Long countDistinctActiveInstallmentGroupsByUser(Long userId, LocalDate today) { + return count; + } + } +}