From f97ab11eaf930065d0cb26ff8e33be0a7059d151 Mon Sep 17 00:00:00 2001 From: Bryan Pinheiro Date: Tue, 15 Sep 2026 11:35:57 -0300 Subject: [PATCH] feat(alerts): detect tight budget risk --- .../alert/domain/TightBudgetAlert.java | 10 ++++ .../alert/service/TightBudgetDetector.java | 36 +++++++++++++ .../service/TightBudgetDetectorTest.java | 52 +++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 src/main/java/com/financebot/alert/domain/TightBudgetAlert.java create mode 100644 src/main/java/com/financebot/alert/service/TightBudgetDetector.java create mode 100644 src/test/java/com/financebot/alert/service/TightBudgetDetectorTest.java diff --git a/src/main/java/com/financebot/alert/domain/TightBudgetAlert.java b/src/main/java/com/financebot/alert/domain/TightBudgetAlert.java new file mode 100644 index 0000000..3d90b36 --- /dev/null +++ b/src/main/java/com/financebot/alert/domain/TightBudgetAlert.java @@ -0,0 +1,10 @@ +package com.financebot.alert.domain; + +import java.math.BigDecimal; + +public record TightBudgetAlert( + BigDecimal commitmentPercentage, + BigDecimal projectedNet, + String explanation +) { +} diff --git a/src/main/java/com/financebot/alert/service/TightBudgetDetector.java b/src/main/java/com/financebot/alert/service/TightBudgetDetector.java new file mode 100644 index 0000000..1de87b2 --- /dev/null +++ b/src/main/java/com/financebot/alert/service/TightBudgetDetector.java @@ -0,0 +1,36 @@ +package com.financebot.alert.service; + +import com.financebot.alert.domain.TightBudgetAlert; +import com.financebot.analysis.dto.response.FinancialCommitmentResponse; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; + +@Service +public class TightBudgetDetector { + + private static final BigDecimal ALERT_THRESHOLD = BigDecimal.valueOf(60); + + public TightBudgetAlert detect(FinancialCommitmentResponse analysis) { + if (analysis == null) { + throw new IllegalArgumentException("Financial analysis is required"); + } + BigDecimal percentage = valueOrZero(analysis.commitmentPercentage()); + BigDecimal projectedNet = valueOrZero(analysis.projectedNetNextMonth()); + if (percentage.compareTo(ALERT_THRESHOLD) < 0 && projectedNet.signum() >= 0) { + return null; + } + String reason = projectedNet.signum() < 0 + ? "a projeção do próximo mês ficou negativa" + : "o comprometimento projetado atingiu " + percentage + "%"; + return new TightBudgetAlert( + percentage, + projectedNet, + "Seu orçamento está apertado: " + reason + "." + ); + } + + private BigDecimal valueOrZero(BigDecimal value) { + return value == null ? BigDecimal.ZERO : value; + } +} diff --git a/src/test/java/com/financebot/alert/service/TightBudgetDetectorTest.java b/src/test/java/com/financebot/alert/service/TightBudgetDetectorTest.java new file mode 100644 index 0000000..6b3baf7 --- /dev/null +++ b/src/test/java/com/financebot/alert/service/TightBudgetDetectorTest.java @@ -0,0 +1,52 @@ +package com.financebot.alert.service; + +import com.financebot.analysis.dto.response.FinancialCommitmentResponse; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class TightBudgetDetectorTest { + + private final TightBudgetDetector detector = new TightBudgetDetector(); + + @Test + void shouldDetectCommitmentAtSixtyPercent() { + var alert = detector.detect(analysis("60", "100")); + + assertThat(alert).isNotNull(); + assertThat(alert.commitmentPercentage()).isEqualByComparingTo("60"); + assertThat(alert.explanation()).contains("60"); + } + + @Test + void shouldDetectNegativeProjectedNetEvenBelowPercentageThreshold() { + var alert = detector.detect(analysis("20", "-1")); + + assertThat(alert).isNotNull(); + assertThat(alert.explanation()).contains("negativa"); + } + + @Test + void shouldIgnoreHealthyBudget() { + assertThat(detector.detect(analysis("59.99", "100"))).isNull(); + } + + @Test + void shouldRejectNullAnalysis() { + assertThatThrownBy(() -> detector.detect(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Financial analysis is required"); + } + + private FinancialCommitmentResponse analysis(String percentage, String projectedNet) { + return new FinancialCommitmentResponse( + BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, + BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, + new BigDecimal(projectedNet), new BigDecimal(percentage), 0L, + false, false, false, "LOW", "" + ); + } +}