Files
carvalho-finances/apps/api/internal/service/credit_bill.go
T
Mlcavalho1andClaude Sonnet 4.6 e3b3433703 feat(#37): módulo de cartão de crédito com faturas mensais
- Migration 011: closing_day/due_day em accounts + tabela credit_bills
- CreditBillService: cria/atualiza fatura corrente on-demand no GET /accounts
- Widget FATURA ATUAL no dashboard com total e botão PAGAR
- AccountsView: campos closing_day/due_day para contas credit + painel fatura
- Dashboard: current_bills lista faturas não pagas de todos os cartões

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-27 15:12:53 -03:00

96 lines
3.0 KiB
Go

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
}