package service import ( "context" "errors" "strings" "time" "financeiro-carvalho/internal/model" "financeiro-carvalho/internal/repository" ) var ( ErrRecurringEmptyName = errors.New("name is required") ErrRecurringInvalidAmount = errors.New("expected_amount must be greater than zero") ErrRecurringInvalidDayOfMonth = errors.New("day_of_month must be between 1 and 31") ErrRecurringNotIncome = errors.New("recurring item is not of type income") ) type RecurringService struct { repo repository.RecurringRepository txRepo repository.ManualTransactionRepository } func NewRecurringService(repo repository.RecurringRepository, txRepo repository.ManualTransactionRepository) *RecurringService { return &RecurringService{repo: repo, txRepo: txRepo} } func (s *RecurringService) List(ctx context.Context) ([]model.RecurringExpense, error) { items, err := s.repo.List(ctx) if items == nil { return []model.RecurringExpense{}, err } return items, err } func (s *RecurringService) Create(ctx context.Context, in model.RecurringInput) (*model.RecurringExpense, error) { if err := validateRecurringInput(in); err != nil { return nil, err } in.Name = strings.TrimSpace(in.Name) return s.repo.Create(ctx, in) } func (s *RecurringService) Update(ctx context.Context, id int, in model.RecurringInput) (*model.RecurringExpense, error) { if err := validateRecurringInput(in); err != nil { return nil, err } in.Name = strings.TrimSpace(in.Name) return s.repo.Update(ctx, id, in) } func (s *RecurringService) Delete(ctx context.Context, id int) error { return s.repo.Delete(ctx, id) } // MonthlyStatus checks which active recurring items are covered/ignored/late for the given month (YYYY-MM). func (s *RecurringService) MonthlyStatus(ctx context.Context, month string) ([]model.RecurringStatus, error) { items, err := s.repo.List(ctx) if err != nil { return nil, err } var result []model.RecurringStatus for _, re := range items { if !re.Active { continue } txType := re.Type if txType == "" { txType = "expense" } if txType == "expense" { ignored, reason, err := s.repo.IsIgnored(ctx, re.ID, month) if err != nil { return nil, err } covered := false if !ignored { covered, err = s.txRepo.HasMatchingTransaction(ctx, re.CategoryID, month, re.ExpectedAmount, "expense") if err != nil { return nil, err } } result = append(result, model.RecurringStatus{ RecurringExpense: re, Covered: covered || ignored, Ignored: ignored, Reason: reason, }) } else { // income type late, err := s.repo.IsLate(ctx, re.ID, month) if err != nil { return nil, err } covered, err := s.txRepo.HasMatchingTransaction(ctx, re.CategoryID, month, re.ExpectedAmount, "income") if err != nil { return nil, err } result = append(result, model.RecurringStatus{ RecurringExpense: re, Covered: covered, Late: late, }) } } return result, nil } // ConfirmIncome creates an income transaction confirming receipt of this recurring income. func (s *RecurringService) ConfirmIncome(ctx context.Context, id int, month string) error { re, err := s.repo.GetByID(ctx, id) if err != nil { return err } if re.Type != "income" { return ErrRecurringNotIncome } today := time.Now().Format("2006-01-02") tx := model.Transaction{ Date: today, Amount: re.ExpectedAmount, Description: re.Name, Type: "income", CategoryID: re.CategoryID, } _, err = s.txRepo.Create(ctx, tx) return err } func (s *RecurringService) MarkLate(ctx context.Context, id int, month string) error { re, err := s.repo.GetByID(ctx, id) if err != nil { return err } if re.Type != "income" { return ErrRecurringNotIncome } return s.repo.MarkLate(ctx, id, month) } func (s *RecurringService) Ignore(ctx context.Context, id int, month, reason string) error { return s.repo.Ignore(ctx, id, month, reason) } func (s *RecurringService) Unignore(ctx context.Context, id int, month string) error { return s.repo.Unignore(ctx, id, month) } func validateRecurringInput(in model.RecurringInput) error { if strings.TrimSpace(in.Name) == "" { return ErrRecurringEmptyName } if in.ExpectedAmount <= 0 { return ErrRecurringInvalidAmount } if in.DayOfMonth < 1 || in.DayOfMonth > 31 { return ErrRecurringInvalidDayOfMonth } return nil }