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).