Skip to main content

22. Late-payment interest + invoice lifecycle

Date: 2026-06-06

Status

Accepted

Context

Invoices were created draft with no way to advance them, so "overdue" and interest had nothing to act on. Step 15 adds the manual invoice lifecycle (Gmail auto-send is deferred) and corrected late-payment interest. The build plan §4 flagged three bugs in the spec's naive accrual.

Decision

  • Lifecycle (owner-gated, manual): markInvoiceSent (draft → sent), recordInvoicePayment (adds to amount_paid_cents, flips to partial/paid, sets paid_manually + paid_at). Payment recording uses optimistic concurrency (where amount_paid_cents = <read value>) so two concurrent payments can't lose an update. Overdue is computed at read time (status sent/partial past due_date) — no stored flip needed.
  • Interest engine (src/lib/interest, pure, tested), §4 corrections:
    • Charged on the outstanding balance (total − paid), never on a figure that already includes a prior interest line — so recomputation can't compound.
    • Real elapsed days past (due_date + grace), not a fixed ÷30. A monthly rate is converted to a true daily rate (× 12 ÷ 365); a daily rate is used as-is.
    • Idempotent storage: accrueInvoiceInterest writes interest_accrued_cents (SET, not increment) plus interest_basis_cents and interest_calculated_at, so re-running is safe.
    • Rate/grace/type come from the facility (interest_enabled, interest_rate, interest_rate_type, interest_grace_period_days). Disabled facility → accrued 0.
  • Owner waiver → audit: waiveInterest (owner only) sets interest_waived + interest_waived_by
    • interest_waived_at + note; the invoices audit trigger records it, and interest_waived_by captures who. A waived invoice's amount-due excludes interest.
  • UI: a per-invoice Manage panel on /admin/invoices shows amount-due-with-interest and the contextual controls (mark sent, record payment, refresh interest, waive). Money is integer USD cents throughout; no funding/email call is introduced.

Consequences

  • The invoice now has a real lifecycle (draft → sent → partial/paid, with overdue computed and interest accruable), which makes the dashboard's overdue/outstanding figures meaningful.
  • Interest is recompute-on-demand here; the scheduled nightly accrual is wired in Step 23 (cron), reusing the same computeInterest + accrueInvoiceInterest.
  • Deferred: automatic "mark sent" via Gmail (Step 12); check-reconciliation payments (Step 13) will call the same payment path. Net/withholding still out of scope (gross only).