package repository import ( "context" "errors" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "financeiro-carvalho/internal/model" ) type RecurringRepository interface { List(ctx context.Context) ([]model.RecurringExpense, error) GetByID(ctx context.Context, id int) (*model.RecurringExpense, error) Create(ctx context.Context, in model.RecurringInput) (*model.RecurringExpense, error) Update(ctx context.Context, id int, in model.RecurringInput) (*model.RecurringExpense, error) Delete(ctx context.Context, id int) error IsIgnored(ctx context.Context, id int, month string) (bool, string, error) Ignore(ctx context.Context, id int, month, reason string) error Unignore(ctx context.Context, id int, month string) error IsLate(ctx context.Context, id int, month string) (bool, error) MarkLate(ctx context.Context, id int, month string) error UnmarkLate(ctx context.Context, id int, month string) error } type recurringRepo struct{ db *pgxpool.Pool } func NewRecurringRepository(db *pgxpool.Pool) RecurringRepository { return &recurringRepo{db: db} } func (r *recurringRepo) List(ctx context.Context) ([]model.RecurringExpense, error) { rows, err := r.db.Query(ctx, ` SELECT id, name, expected_amount, day_of_month, category_id, type, active, created_at, updated_at FROM recurring_expenses ORDER BY name ASC`) if err != nil { return nil, err } defer rows.Close() var out []model.RecurringExpense for rows.Next() { var re model.RecurringExpense if err := rows.Scan(&re.ID, &re.Name, &re.ExpectedAmount, &re.DayOfMonth, &re.CategoryID, &re.Type, &re.Active, &re.CreatedAt, &re.UpdatedAt); err != nil { return nil, err } out = append(out, re) } return out, rows.Err() } func (r *recurringRepo) GetByID(ctx context.Context, id int) (*model.RecurringExpense, error) { var re model.RecurringExpense err := r.db.QueryRow(ctx, ` SELECT id, name, expected_amount, day_of_month, category_id, type, active, created_at, updated_at FROM recurring_expenses WHERE id = $1`, id). Scan(&re.ID, &re.Name, &re.ExpectedAmount, &re.DayOfMonth, &re.CategoryID, &re.Type, &re.Active, &re.CreatedAt, &re.UpdatedAt) if errors.Is(err, pgx.ErrNoRows) { return nil, ErrNotFound } return &re, err } func (r *recurringRepo) Create(ctx context.Context, in model.RecurringInput) (*model.RecurringExpense, error) { t := in.Type if t == "" { t = "expense" } var re model.RecurringExpense err := r.db.QueryRow(ctx, ` INSERT INTO recurring_expenses (name, expected_amount, day_of_month, category_id, type) VALUES ($1, $2, $3, $4, $5) RETURNING id, name, expected_amount, day_of_month, category_id, type, active, created_at, updated_at`, in.Name, in.ExpectedAmount, in.DayOfMonth, in.CategoryID, t). Scan(&re.ID, &re.Name, &re.ExpectedAmount, &re.DayOfMonth, &re.CategoryID, &re.Type, &re.Active, &re.CreatedAt, &re.UpdatedAt) return &re, err } func (r *recurringRepo) Update(ctx context.Context, id int, in model.RecurringInput) (*model.RecurringExpense, error) { t := in.Type if t == "" { t = "expense" } var re model.RecurringExpense err := r.db.QueryRow(ctx, ` UPDATE recurring_expenses SET name = $1, expected_amount = $2, day_of_month = $3, category_id = $4, type = $5, updated_at = NOW() WHERE id = $6 RETURNING id, name, expected_amount, day_of_month, category_id, type, active, created_at, updated_at`, in.Name, in.ExpectedAmount, in.DayOfMonth, in.CategoryID, t, id). Scan(&re.ID, &re.Name, &re.ExpectedAmount, &re.DayOfMonth, &re.CategoryID, &re.Type, &re.Active, &re.CreatedAt, &re.UpdatedAt) if errors.Is(err, pgx.ErrNoRows) { return nil, ErrNotFound } return &re, err } func (r *recurringRepo) Delete(ctx context.Context, id int) error { tag, err := r.db.Exec(ctx, `DELETE FROM recurring_expenses WHERE id = $1`, id) if err != nil { return err } if tag.RowsAffected() == 0 { return ErrNotFound } return nil } func (r *recurringRepo) IsIgnored(ctx context.Context, id int, month string) (bool, string, error) { var reason string err := r.db.QueryRow(ctx, `SELECT COALESCE(reason, '') FROM recurring_ignores WHERE recurring_id = $1 AND month = $2`, id, month).Scan(&reason) if errors.Is(err, pgx.ErrNoRows) { return false, "", nil } return err == nil, reason, err } func (r *recurringRepo) Ignore(ctx context.Context, id int, month, reason string) error { _, err := r.db.Exec(ctx, ` INSERT INTO recurring_ignores (recurring_id, month, reason) VALUES ($1, $2, $3) ON CONFLICT (recurring_id, month) DO UPDATE SET reason = $3`, id, month, reason) return err } func (r *recurringRepo) Unignore(ctx context.Context, id int, month string) error { _, err := r.db.Exec(ctx, `DELETE FROM recurring_ignores WHERE recurring_id = $1 AND month = $2`, id, month) return err } func (r *recurringRepo) IsLate(ctx context.Context, id int, month string) (bool, error) { var count int err := r.db.QueryRow(ctx, `SELECT COUNT(*) FROM recurring_late WHERE recurring_id = $1 AND month = $2`, id, month).Scan(&count) if err != nil { return false, err } return count > 0, nil } func (r *recurringRepo) MarkLate(ctx context.Context, id int, month string) error { _, err := r.db.Exec(ctx, ` INSERT INTO recurring_late (recurring_id, month) VALUES ($1, $2) ON CONFLICT (recurring_id, month) DO NOTHING`, id, month) return err } func (r *recurringRepo) UnmarkLate(ctx context.Context, id int, month string) error { _, err := r.db.Exec(ctx, `DELETE FROM recurring_late WHERE recurring_id = $1 AND month = $2`, id, month) return err }