feat(#36): receitas recorrentes com confirmação de salário até dia 5

- Migration 009: coluna `type` em recurring_expenses + tabela recurring_late
- API Go: tipo income/expense em CRUD; endpoints POST /confirm e /late
- Dashboard: campo pending_income_recurrings na resposta
- Frontend: RecurringView com seções RECEITAS e DESPESAS separadas
- HomeView: widget de confirmação (dia 1-5) e badge SALÁRIO PENDENTE (pós dia 5)
- Testes unitários: 4 novos casos (income covered, late, confirm, reject expense)

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
2026-05-27 15:00:28 -03:00
co-authored by Claude Sonnet 4.6
parent 1931271b76
commit 79f4a62a1b
16 changed files with 725 additions and 77 deletions
@@ -16,8 +16,8 @@ type ManualTransactionRepository interface {
Create(ctx context.Context, t model.Transaction) (*model.Transaction, error)
Update(ctx context.Context, t model.Transaction) (*model.Transaction, error)
Delete(ctx context.Context, id int) error
// HasMatchingTransaction checks if a category has an expense in the given month.
HasMatchingTransaction(ctx context.Context, categoryID *int, month string, amount float64) (bool, error)
// HasMatchingTransaction checks if a category has a transaction of the given type in the given month.
HasMatchingTransaction(ctx context.Context, categoryID *int, month string, amount float64, txType string) (bool, error)
}
type manualTxRepo struct{ db *pgxpool.Pool }
@@ -103,20 +103,18 @@ func (r *manualTxRepo) Delete(ctx context.Context, id int) error {
return nil
}
func (r *manualTxRepo) HasMatchingTransaction(ctx context.Context, categoryID *int, month string, amount float64) (bool, error) {
var count int
var err error
if categoryID != nil {
err = r.db.QueryRow(ctx, `
SELECT COUNT(*) FROM transactions
WHERE category_id = $1
AND TO_CHAR(date, 'YYYY-MM') = $2
AND type = 'expense'
AND amount BETWEEN $3 * 0.9 AND $3 * 1.1`,
*categoryID, month, amount).Scan(&count)
} else {
func (r *manualTxRepo) HasMatchingTransaction(ctx context.Context, categoryID *int, month string, amount float64, txType string) (bool, error) {
if categoryID == nil {
// No category set — cannot auto-match, always report as uncovered
return false, nil
}
var count int
err := r.db.QueryRow(ctx, `
SELECT COUNT(*) FROM transactions
WHERE category_id = $1
AND TO_CHAR(date, 'YYYY-MM') = $2
AND type = $3
AND amount BETWEEN $4 * 0.9 AND $4 * 1.1`,
*categoryID, month, txType, amount).Scan(&count)
return count > 0, err
}