Skip to content

Finance deferred round 2 — real payment runs, direct-payment tracking, rev-rec guards, as-of reports - #129

Open
domsteil wants to merge 9 commits into
masterfrom
fix/finance-deferred-round2
Open

Finance deferred round 2 — real payment runs, direct-payment tracking, rev-rec guards, as-of reports#129
domsteil wants to merge 9 commits into
masterfrom
fix/finance-deferred-round2

Conversation

@domsteil

@domsteil domsteil commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Stacked on #127 (v1.28.2). Fixes the four highest-severity deferred findings from the finance audit:

Summary

  • AP payment runs are real: create validates bills atomically (payable status, positive balance, no duplicates, exclusivity vs other active runs); approve/process/cancel form a guarded state machine; process creates actual ap_payments + allocations per bill in one transaction, skipping (and recording) bills paid since run creation. Previously process_payment_run flipped the status and paid nobody.
  • AR direct payments survive recalculation: new invoices.direct_amount_paid column (migrations 074 sqlite / 081 postgres); recalc computes amount_paid = direct + applications + credit applications instead of erasing record_payment amounts.
  • Rev-rec contract-status guards: no recognition on draft/cancelled contracts, no schedule generation on cancelled; completed-contract retries stay idempotent no-ops.
  • Subledger/GL divergence closed: a failed GL auto-post after the subledger commit now compensates (recognition entries back to deferred; depreciation entries back to scheduled, asset state restored) so a retry recognizes and posts the full amount.
  • GL as-of-date reports: trial balance, balance sheet, and dated get_account_balance derive from posted/reversed journal lines dated ≤ the requested date instead of echoing the live running balance.

Testing

  • +21 new tests across ap/ar/gl_money_guards_test, rev-rec, and fixed-assets suites
  • stateset-db suite 900 passed / 0 failed; embedded finance suites 142 passed / 0 failed
  • clippy --all-targets --all-features clean; postgres compiles (no live postgres in env for the postgres-only fixtures)

🤖 Generated with Claude Code

https://claude.ai/code/session_01PCC33Xf4iAmBnkHFDDPBLu

domsteil and others added 4 commits August 31, 2026 15:13
General ledger (sqlite + postgres + kernel executor):
- auto_post_* is idempotent: a source document that already has a
  non-voided journal entry returns that entry instead of posting twice
- post/void of a journal entry requires its period to be open (the
  kernel ledger.post path rejects with commerce.ledger.period_not_open)
- income statement excludes closing entries, so a closed period's P&L
  no longer reads as zero
- run_period_close refuses while a posted closing entry stands for the
  period; re-closing a reopened period requires voiding it first

Accounts receivable (sqlite + postgres):
- apply_payment_to_invoices is bounded by the payment's own amount
  (existing applications counted), rejecting over-application
- payments/credit memos cannot be applied to voided/written-off invoices
- write-offs validated: positive and no more than the balance due;
  credit memos must be positive

Accounts payable (sqlite + postgres + core model):
- three-way match aggregates billed quantity per PO line, so duplicating
  a bill line can no longer slip over-billing past the match
- clear_payment, cancel_bill, dispute_bill, approve_bill are
  status-guarded transitions (0 rows -> Conflict, not silent success)
- count_payments applies the same date filters as list_payments

29 new tests (gl/ar/ap_money_guards_test, kernel closed-period
rejection, three-way match aggregation); ledger_simulation updated to
model void-then-re-close.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCC33Xf4iAmBnkHFDDPBLu
Mechanical bump 1.28.1 -> 1.28.2 across all release surfaces
(scripts/release-bump.sh), CHANGELOG entry, and README "What's New"
section for the finance money-integrity guards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCC33Xf4iAmBnkHFDDPBLu
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCC33Xf4iAmBnkHFDDPBLu
…ec guards, as-of reports

Accounts payable (sqlite + postgres):
- payment runs are real: create validates bills (payable status,
  positive balance, no duplicates, exclusivity against other active
  runs) atomically; approve/cancel are status-guarded transitions;
  process claims approved -> completed in one transaction and creates
  actual ap_payments + allocations per bill, skipping (and recording)
  bills paid since run creation — previously it only flipped the status
  and paid nobody

Accounts receivable (sqlite + postgres, migration 074/081):
- invoices track direct_amount_paid, so AR recalculation no longer
  erases payments recorded via invoices.record_payment when a credit
  memo or payment application lands (amount_paid = direct +
  applications + credit applications)

Revenue recognition + fixed assets (sqlite + postgres):
- recognize_period rejects draft/cancelled contracts;
  generate_schedule rejects cancelled contracts (completed retries
  remain idempotent no-ops)
- a failed GL auto-post after the subledger commit now compensates:
  recognition entries revert to deferred (and depreciation entries to
  scheduled, asset state restored) so a retry recognizes and posts the
  full amount — closing the permanent subledger/GL divergence

General ledger (sqlite + postgres):
- trial balance, balance sheet, and get_account_balance derive from
  posted/reversed journal lines dated on or before the requested date
  instead of echoing the live running balance; dateless account-balance
  reads keep the live balance

Tests: +21 (payment-run lifecycle ×9, direct-payment survival ×3,
rev-rec status guards ×2, GL-post compensation ×2, as-of reports ×2,
plus adjusted fixtures); db suite 900 passed, embedded finance suites
142 passed, clippy clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCC33Xf4iAmBnkHFDDPBLu
Comment thread crates/stateset-embedded/tests/gl_money_guards_test.rs Fixed
Comment thread crates/stateset-embedded/tests/gl_money_guards_test.rs Fixed
Comment thread crates/stateset-embedded/tests/gl_money_guards_test.rs Fixed
domsteil and others added 5 commits August 31, 2026 18:36
…versal resume

Accounts payable (sqlite + postgres):
- bill line items can only be added/removed on draft/pending bills
  (payments cannot exist before approval, so amount_due can no longer
  be driven negative and paid bills can no longer grow invisible debt)
- create_bill commits header, lines, and totals in one transaction —
  a failing line insert no longer leaves a partial draft bill
- sqlite date-comparison parity with postgres: get_bills_due_soon
  includes the boundary day (was excluded by a lexical RFC3339-vs-
  datetime('now') compare); aging buckets and supplier summaries
  truncate to midnight UTC so a bill due today is not overdue

Accounts receivable (sqlite + postgres):
- unapply_payment runs in one transaction (the application row and the
  invoice recalculation can no longer diverge mid-operation)
- reverse_write_off derives the restored invoice status from
  recalculation plus the standard overdue rule instead of
  unconditionally stamping 'overdue' on invoices due in the future

General ledger (sqlite + postgres):
- reverse_journal_entry resumes a stranded claim (crash between the
  posted->reversed claim and the reversing entry's creation was
  previously unrecoverable), repairs lost cross-links, and still
  rejects a genuine second reversal

Verification: sqlite finance suites 152 passed; stateset-db lib 720
passed; full postgres parity suite run against a live Postgres 16 (28
tests, all green — first live-postgres validation of both rounds'
mirrors; two rev-rec parity tests updated to activate contracts per
the new draft/cancelled guard); clippy --all-features clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCC33Xf4iAmBnkHFDDPBLu
…real opening balance

generate_statement (sqlite + postgres) previously listed only invoices
and payment applications, hardcoded opening_balance to zero, and left
total_credits at zero — a customer statement whose running balance
contradicted its closing balance whenever a credit memo existed.

Statements now gather all statement-visible activity for the window —
invoices (debits), payment applications, credit-memo applications, and
non-reversed write-offs (credits) — sorted by date into one running
balance that starts from a derived opening balance (the same activity
before the period start). total_credits reports applied credit memos.
closing_balance stays the live aging total.

2 new tests: a credit memo appears as a line item with consistent
running/closing balances; pre-period activity carries into the opening
balance of a future-window statement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCC33Xf4iAmBnkHFDDPBLu
…nel period guard

- docs/src/commerce/accounting.md gains a "Money-Integrity Guarantees"
  section covering the enforced invariants across GL, AP, and AR/rev-rec
  on both backends (idempotent auto-posting, period lifecycle, as-of
  reports, payment-run state machine, application bounds, statement
  semantics, subledger/GL compensation)
- postgres kernel executor gains the closed-period rejection parity test
  (commerce.ledger.period_not_open, draft preserved, no balance
  movement), verified against live Postgres 16 along with the full
  15-test postgres kernel suite

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCC33Xf4iAmBnkHFDDPBLu
spentThisMonth is calendar-bucketed, so the test's daysAgo(1)/(2)
entries fall into the previous month on the 1st and 2nd of every month
and the assertion failed (coverage went red on Sep 1). getBudgetForecast
accepts an optional injectable `now` (defaulting to the current time,
fully backwards-compatible) and the test pins it mid-month.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCC33Xf4iAmBnkHFDDPBLu
CodeQL (rust/cleartext-logging) traces account-derived data through the
error values into the panic messages. The assertions only need the
Result variant; the messages stay descriptive without the payload.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCC33Xf4iAmBnkHFDDPBLu
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants