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 } func newMockRecurring(items []model.RecurringExpense) *mockRecurringRepo { return &mockRecurringRepo{items: items, ignores: map[string]string{}} } 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, 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 } // ── 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) HasMatchingTransaction(_ context.Context, _ *int, _ string, _ float64) (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, 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, 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, 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 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) } }