- 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]>
96 lines
3.0 KiB
Go
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
|
|
}
|