Files
carvalho-finances/apps/api/internal/service/recurring_test.go
T
Mlcavalho1andClaude Sonnet 4.6 6292858970 feat: CDI % configurável por conta + fix mocks de teste
- Adiciona campo cdi_percentage (default 100%) na tabela accounts
- Cálculo CDI aplica proporção: balance × (compound-1) × (pct/100)
- Frontend exibe input de % quando yield_type=cdi e badge mostra valor
- Corrige mocks de teste desatualizados (DeleteByMonth, isTax)
- Corrige ConfirmIncome para rejeitar tipo expense (ErrRecurringNotIncome)

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-28 22:20:32 -03:00

221 lines
8.4 KiB
Go

package service_test
import (
"context"
"testing"
"financeiro-carvalho/internal/model"
"financeiro-carvalho/internal/repository"
"financeiro-carvalho/internal/service"
)
// ── mock recurring repo ──────────────────────────────────────────────────────
type mockRecurringRepo struct {
items []model.RecurringExpense
ignores map[string]string // "id:month" → reason
lates map[string]bool // "id:month" → true
}
func newMockRecurring(items []model.RecurringExpense) *mockRecurringRepo {
return &mockRecurringRepo{items: items, ignores: map[string]string{}, lates: map[string]bool{}}
}
func (m *mockRecurringRepo) List(_ context.Context) ([]model.RecurringExpense, error) {
return m.items, nil
}
func (m *mockRecurringRepo) GetByID(_ context.Context, id int) (*model.RecurringExpense, error) {
for _, r := range m.items {
if r.ID == id {
cp := r
return &cp, nil
}
}
return nil, repository.ErrNotFound
}
func (m *mockRecurringRepo) Create(_ context.Context, in model.RecurringInput) (*model.RecurringExpense, error) {
r := model.RecurringExpense{ID: len(m.items) + 1, Name: in.Name, ExpectedAmount: in.ExpectedAmount, DayOfMonth: in.DayOfMonth, Type: in.Type, Active: true}
m.items = append(m.items, r)
return &r, nil
}
func (m *mockRecurringRepo) Update(_ context.Context, id int, in model.RecurringInput) (*model.RecurringExpense, error) {
for i, r := range m.items {
if r.ID == id {
m.items[i].Name = in.Name
m.items[i].ExpectedAmount = in.ExpectedAmount
cp := m.items[i]
return &cp, nil
}
}
return nil, repository.ErrNotFound
}
func (m *mockRecurringRepo) Delete(_ context.Context, id int) error { return nil }
func (m *mockRecurringRepo) IsIgnored(_ context.Context, id int, month string) (bool, string, error) {
key := string(rune(id)) + ":" + month
r, ok := m.ignores[key]
return ok, r, nil
}
func (m *mockRecurringRepo) Ignore(_ context.Context, id int, month, reason string) error {
m.ignores[string(rune(id))+":"+month] = reason
return nil
}
func (m *mockRecurringRepo) Unignore(_ context.Context, id int, month string) error {
delete(m.ignores, string(rune(id))+":"+month)
return nil
}
func (m *mockRecurringRepo) IsLate(_ context.Context, id int, month string) (bool, error) {
return m.lates[string(rune(id))+":"+month], nil
}
func (m *mockRecurringRepo) MarkLate(_ context.Context, id int, month string) error {
m.lates[string(rune(id))+":"+month] = true
return nil
}
func (m *mockRecurringRepo) UnmarkLate(_ context.Context, id int, month string) error {
delete(m.lates, string(rune(id))+":"+month)
return nil
}
// ── mock tx repo ─────────────────────────────────────────────────────────────
type mockTxRepo struct {
matchResult bool
}
func (m *mockTxRepo) List(_ context.Context, _ string) ([]model.Transaction, error) { return nil, nil }
func (m *mockTxRepo) GetByID(_ context.Context, _ int) (*model.Transaction, error) { return nil, nil }
func (m *mockTxRepo) Create(_ context.Context, t model.Transaction) (*model.Transaction, error) {
return &t, nil
}
func (m *mockTxRepo) Update(_ context.Context, t model.Transaction) (*model.Transaction, error) {
return &t, nil
}
func (m *mockTxRepo) Delete(_ context.Context, _ int) error { return nil }
func (m *mockTxRepo) DeleteByMonth(_ context.Context, _ string) (int, error) { return 0, nil }
func (m *mockTxRepo) HasMatchingTransaction(_ context.Context, _ *int, _ string, _ float64, _ string) (bool, error) {
return m.matchResult, nil
}
// ── tests ─────────────────────────────────────────────────────────────────────
var catID = 1
func TestMonthlyStatus_Covered(t *testing.T) {
re := model.RecurringExpense{ID: 1, Name: "Plano Saúde", ExpectedAmount: 300, DayOfMonth: 5, CategoryID: &catID, Type: "expense", Active: true}
svc := service.NewRecurringService(newMockRecurring([]model.RecurringExpense{re}), &mockTxRepo{matchResult: true})
statuses, err := svc.MonthlyStatus(context.Background(), "2024-03")
if err != nil {
t.Fatal(err)
}
if len(statuses) != 1 {
t.Fatalf("expected 1, got %d", len(statuses))
}
if !statuses[0].Covered {
t.Error("expected Covered=true")
}
}
func TestMonthlyStatus_Uncovered(t *testing.T) {
re := model.RecurringExpense{ID: 2, Name: "Netflix", ExpectedAmount: 55, DayOfMonth: 10, CategoryID: &catID, Type: "expense", Active: true}
svc := service.NewRecurringService(newMockRecurring([]model.RecurringExpense{re}), &mockTxRepo{matchResult: false})
statuses, err := svc.MonthlyStatus(context.Background(), "2024-03")
if err != nil {
t.Fatal(err)
}
if statuses[0].Covered {
t.Error("expected Covered=false")
}
}
func TestMonthlyStatus_Ignored_CountsAsCovered(t *testing.T) {
re := model.RecurringExpense{ID: 3, Name: "Seguro", ExpectedAmount: 200, DayOfMonth: 1, CategoryID: &catID, Type: "expense", Active: true}
repo := newMockRecurring([]model.RecurringExpense{re})
_ = repo.Ignore(context.Background(), 3, "2024-03", "viagem")
svc := service.NewRecurringService(repo, &mockTxRepo{matchResult: false})
statuses, err := svc.MonthlyStatus(context.Background(), "2024-03")
if err != nil {
t.Fatal(err)
}
if !statuses[0].Covered {
t.Error("ignored should count as covered")
}
if !statuses[0].Ignored {
t.Error("expected Ignored=true")
}
}
func TestMonthlyStatus_IncomeConfirmed(t *testing.T) {
re := model.RecurringExpense{ID: 4, Name: "Salário", ExpectedAmount: 3200, DayOfMonth: 5, CategoryID: &catID, Type: "income", Active: true}
svc := service.NewRecurringService(newMockRecurring([]model.RecurringExpense{re}), &mockTxRepo{matchResult: true})
statuses, err := svc.MonthlyStatus(context.Background(), "2024-03")
if err != nil {
t.Fatal(err)
}
if len(statuses) != 1 {
t.Fatalf("expected 1, got %d", len(statuses))
}
if !statuses[0].Covered {
t.Error("income with matching transaction should be covered")
}
}
func TestMonthlyStatus_IncomeLate(t *testing.T) {
re := model.RecurringExpense{ID: 5, Name: "Salário", ExpectedAmount: 3200, DayOfMonth: 5, CategoryID: &catID, Type: "income", Active: true}
repo := newMockRecurring([]model.RecurringExpense{re})
_ = repo.MarkLate(context.Background(), 5, "2024-03")
svc := service.NewRecurringService(repo, &mockTxRepo{matchResult: false})
statuses, err := svc.MonthlyStatus(context.Background(), "2024-03")
if err != nil {
t.Fatal(err)
}
if statuses[0].Covered {
t.Error("late income should NOT be covered")
}
if !statuses[0].Late {
t.Error("expected Late=true")
}
}
func TestConfirmIncome_CreatesTransaction(t *testing.T) {
re := model.RecurringExpense{ID: 6, Name: "Salário", ExpectedAmount: 3200, DayOfMonth: 5, CategoryID: &catID, Type: "income", Active: true}
svc := service.NewRecurringService(newMockRecurring([]model.RecurringExpense{re}), &mockTxRepo{})
err := svc.ConfirmIncome(context.Background(), 6, "2024-03")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestConfirmIncome_RejectsExpenseType(t *testing.T) {
re := model.RecurringExpense{ID: 7, Name: "Netflix", ExpectedAmount: 55, DayOfMonth: 10, CategoryID: &catID, Type: "expense", Active: true}
svc := service.NewRecurringService(newMockRecurring([]model.RecurringExpense{re}), &mockTxRepo{})
err := svc.ConfirmIncome(context.Background(), 7, "2024-03")
if err != service.ErrRecurringNotIncome {
t.Fatalf("expected ErrRecurringNotIncome, got %v", err)
}
}
func TestCreateRecurring_Validation(t *testing.T) {
svc := service.NewRecurringService(newMockRecurring(nil), &mockTxRepo{})
_, err := svc.Create(context.Background(), model.RecurringInput{Name: "", ExpectedAmount: 100, DayOfMonth: 5})
if err != service.ErrRecurringEmptyName {
t.Fatalf("expected ErrRecurringEmptyName, got %v", err)
}
_, err = svc.Create(context.Background(), model.RecurringInput{Name: "X", ExpectedAmount: -1, DayOfMonth: 5})
if err != service.ErrRecurringInvalidAmount {
t.Fatalf("expected ErrRecurringInvalidAmount, got %v", err)
}
_, err = svc.Create(context.Background(), model.RecurringInput{Name: "X", ExpectedAmount: 100, DayOfMonth: 0})
if err != service.ErrRecurringInvalidDayOfMonth {
t.Fatalf("expected ErrRecurringInvalidDayOfMonth, got %v", err)
}
}