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]>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user