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
Open
Finance deferred round 2 — real payment runs, direct-payment tracking, rev-rec guards, as-of reports#129domsteil wants to merge 9 commits into
domsteil wants to merge 9 commits into
Conversation
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
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #127 (v1.28.2). Fixes the four highest-severity deferred findings from the finance audit:
Summary
ap_payments+ allocations per bill in one transaction, skipping (and recording) bills paid since run creation. Previouslyprocess_payment_runflipped the status and paid nobody.invoices.direct_amount_paidcolumn (migrations 074 sqlite / 081 postgres); recalc computesamount_paid = direct + applications + credit applicationsinstead of erasingrecord_paymentamounts.get_account_balancederive from posted/reversed journal lines dated ≤ the requested date instead of echoing the live running balance.Testing
🤖 Generated with Claude Code
https://claude.ai/code/session_01PCC33Xf4iAmBnkHFDDPBLu