package service import ( "context" "errors" "time" "financeiro-carvalho/internal/model" "financeiro-carvalho/internal/repository" ) var ErrBillNotCreditAccount = errors.New("account is not a credit card account") type CreditBillService struct { repo repository.CreditBillRepository accountRepo repository.AccountRepoWithYield } func NewCreditBillService(repo repository.CreditBillRepository, accountRepo repository.AccountRepoWithYield) *CreditBillService { return &CreditBillService{repo: repo, accountRepo: accountRepo} } func (s *CreditBillService) ListByAccount(ctx context.Context, accountID int) ([]model.CreditBill, error) { bills, err := s.repo.ListByAccount(ctx, accountID) if bills == nil { return []model.CreditBill{}, err } return bills, err } func (s *CreditBillService) GetCurrent(ctx context.Context, accountID int) (*model.CreditBill, error) { return s.repo.GetCurrent(ctx, accountID) } // EnsureCurrentBill creates (or refreshes) the current open bill for a credit account // using the account's closing_day and due_day config (defaults: closing=1, due=10). func (s *CreditBillService) EnsureCurrentBill(ctx context.Context, accountID int, closingDay, dueDay int) error { if closingDay == 0 { closingDay = 1 } if dueDay == 0 { dueDay = 10 } now := time.Now() // Determine the current billing period based on closing day var periodStart, periodEnd time.Time if now.Day() > closingDay { // We're after the closing day: current period is from closingDay+1 this month to closingDay next month y, m, _ := now.Date() periodStart = time.Date(y, m, closingDay+1, 0, 0, 0, 0, time.UTC) next := time.Date(y, m+1, closingDay, 0, 0, 0, 0, time.UTC) periodEnd = next } else { // We're before the closing day: current period is from closingDay+1 last month to closingDay this month y, m, _ := now.Date() periodStart = time.Date(y, m-1, closingDay+1, 0, 0, 0, 0, time.UTC) periodEnd = time.Date(y, m, closingDay, 0, 0, 0, 0, time.UTC) } // Due date: dueDay of the month after periodEnd dueDate := time.Date(periodEnd.Year(), periodEnd.Month()+1, dueDay, 0, 0, 0, 0, time.UTC) in := model.CreditBillInput{ AccountID: accountID, PeriodStart: periodStart.Format("2006-01-02"), PeriodEnd: periodEnd.Format("2006-01-02"), DueDate: dueDate.Format("2006-01-02"), } _, err := s.repo.Upsert(ctx, in) return err } func (s *CreditBillService) MarkPaid(ctx context.Context, id int, paymentAccountID *int) error { return s.repo.MarkPaid(ctx, id, paymentAccountID) } // currentBillsForDashboard returns unpaid current bills across all credit accounts. func (s *CreditBillService) currentBillsForDashboard(ctx context.Context) ([]model.CreditBill, error) { accounts, err := s.accountRepo.List(ctx) if err != nil { return nil, err } var bills []model.CreditBill for _, a := range accounts { if a.Type != "credit" { continue } bill, err := s.repo.GetCurrent(ctx, a.ID) if err != nil || bill == nil { continue } bills = append(bills, *bill) } return bills, nil }