Files
carvalho-finances/apps/api/internal/service/dashboard.go
T
Mlcavalho1andClaude Sonnet 4.6 2b8524dcde feat: #44 fatura de cartão com data futura fica pendente até confirmação
- Nova tabela pending_bill_imports (migration 016)
- ImportHandler.Confirm: quando is_credit_card=true e payment_date > hoje,
  salva como pending em vez de inserir transações
- Novos endpoints: GET /pending-bills, POST /pending-bills/:id/confirm,
  DELETE /pending-bills/:id
- Dashboard inclui pending_bill_imports no payload
- Frontend: resultado "fatura salva como pendente" no ImportView
- AccountsView exibe widget de faturas pendentes com ações de confirmar/descartar
- dashboard_test: mock de PendingBillRepo + TransactionRepository

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-28 22:26:42 -03:00

133 lines
3.5 KiB
Go

package service
import (
"context"
"financeiro-carvalho/internal/model"
)
type DashboardRepo interface {
MonthlySummary(ctx context.Context, month string) (income, expenses float64, err error)
TotalTaxes(ctx context.Context, month string) (float64, error)
ByCategory(ctx context.Context, month string) ([]model.CategoryTotal, error)
MonthlyEvolution(ctx context.Context, month string) ([]model.MonthEvolution, error)
RecentTransactions(ctx context.Context, month string) ([]model.RecentTransaction, error)
}
type PatrimonySource interface {
TotalPatrimony(ctx context.Context) (float64, error)
}
type DashboardService struct {
repo DashboardRepo
recurrSvc *RecurringService
patrimony PatrimonySource
billSvc *CreditBillService
pendingBillSvc *PendingBillService
}
func NewDashboardService(repo DashboardRepo, recurrSvc *RecurringService, patrimony PatrimonySource, billSvc *CreditBillService, pendingBillSvc *PendingBillService) *DashboardService {
return &DashboardService{repo: repo, recurrSvc: recurrSvc, patrimony: patrimony, billSvc: billSvc, pendingBillSvc: pendingBillSvc}
}
func (s *DashboardService) Get(ctx context.Context, month string) (*model.DashboardData, error) {
income, expenses, err := s.repo.MonthlySummary(ctx, month)
if err != nil {
return nil, err
}
taxes, err := s.repo.TotalTaxes(ctx, month)
if err != nil {
return nil, err
}
byCategory, err := s.repo.ByCategory(ctx, month)
if err != nil {
return nil, err
}
evolution, err := s.repo.MonthlyEvolution(ctx, month)
if err != nil {
return nil, err
}
recent, err := s.repo.RecentTransactions(ctx, month)
if err != nil {
return nil, err
}
// savings_pct uses net income (gross - taxes) as denominator (Option B)
var savingsPct float64
netIncome := income - taxes
if netIncome > 0 {
savingsPct = (income - expenses) / netIncome * 100
}
statuses, err := s.recurrSvc.MonthlyStatus(ctx, month)
if err != nil {
return nil, err
}
pending := 0
var pendingIncome []model.PendingIncome
for _, st := range statuses {
if st.Type == "income" {
if !st.Covered {
pendingIncome = append(pendingIncome, model.PendingIncome{
ID: st.ID,
Name: st.Name,
ExpectedAmount: st.ExpectedAmount,
DayOfMonth: st.DayOfMonth,
Late: st.Late,
})
}
} else {
if !st.Covered {
pending++
}
}
}
if pendingIncome == nil {
pendingIncome = []model.PendingIncome{}
}
if byCategory == nil {
byCategory = []model.CategoryTotal{}
}
if evolution == nil {
evolution = []model.MonthEvolution{}
}
if recent == nil {
recent = []model.RecentTransaction{}
}
patrimony, err := s.patrimony.TotalPatrimony(ctx)
if err != nil {
return nil, err
}
currentBills, _ := s.billSvc.currentBillsForDashboard(ctx)
if currentBills == nil {
currentBills = []model.CreditBill{}
}
pendingBillImports, _ := s.pendingBillSvc.List(ctx)
if pendingBillImports == nil {
pendingBillImports = []model.PendingBillImport{}
}
return &model.DashboardData{
Month: month,
TotalIncome: income,
TotalExpenses: expenses,
SavingsPct: savingsPct,
TotalPatrimony: patrimony,
ByCategory: byCategory,
MonthlyEvolution: evolution,
RecentTransactions: recent,
PendingRecurring: pending,
PendingIncomeRecurrings: pendingIncome,
CurrentBills: currentBills,
PendingBillImports: pendingBillImports,
}, nil
}