merge: P4b S6, paying on terms (branch S6, 6c4480cd)

The third of the paused branches: Ledger::settle with discounts and the
processor's fee (kernel::settle, Kani settle_is_exact, Lean
settle_posts_a_document_step), books.receive_payment and books.pay_bill
with applications, books.bills_due, the agings' detail and tie-out, the
aging exports and the CPA package's aging CSVs, the handlers in
tools/settlements.rs. Twelve files conflicted with the S4.4 and S5
merges, resolved as plan 8.0.2 says: both routers; both test appends
(the shared match arm in arap_standing.rs spliced into both tests; the
shared `.unwrap()` in store.rs); the scope table at 124; one
invoices-and-bills handbook (S5's send step 1 with S6's steps 2, 3, 5
and procedures) registered once; docs/mcp-interface.md with S4.4's
create docs and S6's payment docs; S5's law renumbered 35 and S6's 36
(S1's is 34); the parity rows and sections of both. S6's wire tests
create their bills as S4.4's contract now requires (confirmed, the
vendor's number, a line account, the vendor's invoice uploaded) and live
in their own module beside S4.4's tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
swaitsandClaude Fable 5.1 committed 2026-10-28 21:26:40 -06:00
commit be58059830
45 files changed
+3949 -520

No files matched your search

+95
View File
@@ -3119,3 +3119,98 @@ starts from empty books (legacy lines are covered by the identity
coding of `DocRef`), and an account becoming a control account by its
first document, holding lines from before, is outside it (those lines are
listed as unattached).
## B-42 A settlement is exact and names its lines
**bookspec:** `BS-arap-6`
**Status:** CONVENTION (a payment that settles several documents at once is
still one balanced entry, and each control line carries its document; the
subledger tie-out is B-38's); CHOICE (the gross method for an
early-payment discount, A-8; the processor's fee posted gross, A-4; D-49's
refusal to apportion a discount across a bill's asset lines). ASC
606-10-32-5 to 32-9 are the strict treatment of an expected discount;
this company's size makes the gross method immaterial (A-8). Sources are
cited and not yet vendored, so this rule keeps the CONVENTION label (the
Open list in `README.md`).
1. **One party, one kind.** A settlement applies to one or more invoices
of one customer, or one or more bills of one vendor, never a mix, and
never the same document twice. Deposits: Σ cash + Σ discounts is what
it applies to the documents; Σ cash − fee is what reaches the deposit
account (`kernel::settle`). The arithmetic is exact: no epsilon, no
rounding, and a fee above the cash is refused.
2. **Bounds.** Each application's cash + discount is at most what its
document owes on every day from the settlement's date on
(`payment_room`), so no day is overpaid. A payment is never dated
before its document, and never in a closed period.
3. **Discounts.** A discount is at most `kernel::discount_max(total,
percent_bp)` for the document's terms and is dated on or before
`terms.discount_by(date)`; outside that it needs a reason, journaled
with the payment (a vendor honouring a late discount). Terms that
state no discount are the same: any discount needs a reason. A discount
on a bill whose lines include an asset or prepaid account is refused
with D-49's remedy: record the bill net of the discount, or reclassify
Dr 6995 Purchase Discounts / Cr the asset.
4. **The fee.** A processor's fee is a customer payment's only. It posts
gross: Dr the processing-fee expense, Dr the deposit account the net,
Cr accounts receivable the gross. Only cash movements count toward a
1099, so the fee is not a payment to the processor.
5. **Lines and journal.** The entry's rows are the payment lines in
application order, then the discount lines, then the fee line, then
the cash line; a new payment event names the line it tags (`line`) and,
when it took one, its discount and discount line (`discount`,
`discount_line`, `discount_reason`), and the fee's on the first
application's event (`fee`, `fee_line`). Replay tags exactly those
lines; a legacy event with no `line` takes the first untagged control
line for its amount (B-38 item 1). A line replay cannot match stays
untagged and the control check lists it: replay never refuses (B-28).
6. **Exports.** An aging as of a day lists one row per open document with
its number, party, dates, terms, total, paid, discounted, outstanding,
bucket and days overdue, then a tie-out line against its control
account (B-23, B-38 INVARIANT 1). The CPA package and `books.export`
write the same CSV.
```text
GOAL: a settlement is exact and names its lines.
SOURCE: CONVENTION; ASC 606-10-32-5 to 32-9 noted; CHOICE: the gross
method (A-8), the fee's gross presentation (A-4).
INVARIANTS:
1. Σ cash + Σ discounts = Σ applications; deposit = Σ cash − fee
(kernel::settle); one party per settlement; one application per
document.
2. Each application's cash + discount ≤ what the document owes on every
day from the payment's date on.
3. Each new payment event names the line it tags; replay tags that line;
a legacy event without one takes the first untagged control line for
its amount.
4. A discount on a bill with an asset or prepaid line is refused.
SCENARIOS:
1. Two invoices of one customer, 300 and 80, paid 380 by one deposit:
two payment lines, each tagged, one cash line.
2. "2/10 net 30" bill 100, paid 98 within ten days: Dr A/P 98 (payment)
and 2 (discount), Cr purchase discounts 2, Cr bank 98.
3. The same discount after the tenth day without a reason: refused,
naming discount_reason; with one: taken and journaled.
4. An invoice 100 paid through a processor keeping 3: Dr bank 97, Dr
processing fees 3, Cr A/R 100.
5. A discount on a bill whose line is equipment: refused with the
record-net or reclassify remedy (D-49).
```
**Enforced:** `kernel::settle`, `Ledger::{settle, settlement_lines,
check_discount}`, `arap::{Application, Fee, SettleParams, SettlementRow}`,
`Ledger::{replay_invoice_payment_at, replay_bill_payment_at}`,
`Store::settle`, the aging builders (`Ledger::aging_documents`,
`ledger_package::{aging_csv, aging_statement}`). **Checked:** `ledger-core` `tests/spec_documents.rs`
(`bill_on_terms_with_discount`, `late_discount_needs_a_reason`,
`discount_on_an_asset_bill_is_refused_with_the_remedy`,
`a_fee_posts_gross_to_receivables_and_net_to_the_bank`,
`a_settlement_names_its_lines_and_refuses_a_document_twice`),
`tests/kernel_bridge.rs::settle_agrees`; Kani `settle_is_exact`;
`ledger-store` `tests/arap_standing.rs`
(`a_settlement_names_its_lines_and_replays`, `settlement_waits_for_documents_v2`,
`legacy_payment_events_keep_the_positional_rule`), `tests/arap_props.rs`;
`ledger-cli` `tests/documents.rs`; `ledger-package`'s aging tie-out test.
The Lean step is `Documents.Step.document` with every control line tagged
(`Documents.settle_unattached`); no new `Step` constructor.