Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.financebot.alert.domain;

public record ExcessiveInstallmentAlert(
long activeInstallmentGroups,
int threshold,
String explanation
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.financebot.alert.port;

import java.time.LocalDate;

public interface InstallmentRiskPort {

Long countDistinctActiveInstallmentGroupsByUser(Long userId, LocalDate today);
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -13,7 +14,8 @@
import java.util.List;
import java.util.Optional;

public interface TransactionRepository extends JpaRepository<Transaction, Long>, JpaSpecificationExecutor<Transaction>, ExpenseHistoryPort {
public interface TransactionRepository extends JpaRepository<Transaction, Long>, JpaSpecificationExecutor<Transaction>,
ExpenseHistoryPort, InstallmentRiskPort {

Optional<Transaction> findByIdAndUserId(Long id, Long userId);

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Loading