Commit Graph
3 Commits
Author SHA1 Message Date
swaits 6ce7920d72 feat(arap): settle documents with discounts and a fee (P4b S6)
One payment had to name one document and could take no discount or
processor fee, so paying several invoices from one deposit meant several
entries and the fee had nowhere to go. This adds the kernel's exact
settlement arithmetic (applications total, net deposit), a Ledger::settle
that posts one entry for a customer's or vendor's documents, and the gross
method and gross fee A-8/A-4 chose.

- kernel::settle: sum(cash)+sum(discounts)=applications, sum(cash)-fee=
  deposit, None on overflow or a negative deposit. Kani settle_is_exact
  plus the Decimal bridge.
- Ledger::settle / settlement_lines: one party and one kind per
  settlement, one application per document, each cash+discount within
  payment_room; a discount within the terms (discount_max and
  discount_by) needs no reason, a larger or later one does; a bill with
  an asset line is refused with D-49's remedy; a fee is a customer
  payment's only and posts gross.
- Rows are payment lines in application order, then discount lines, the
  fee line and the cash line, so a journaled event's line indices are
  stable; replay tags exactly the line an event names and leaves an
  unmatched line unattached (never refuses).
- receive_payment_with and pay_bill_with become one-application calls.
- Roles: sales_allowances (4100), purchase_discounts (6995) and
  payment_processing_fees (6060), constants used on demand but not added
  to control_accounts() (existing charts may use those codes for other
  things).
- Lean: settle posts through Step.document, so settle_posts_a_document_step
  and settle_unattached, no new constructor; both in Audit.lean.
- Core tests: 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, and
  equipment_bill_payment_is_investing_cash in the cash-flow suite.
- Bookspec B-42 is stated, with its id BS-arap-6; the catalogue and index
  now record it as CONVENTION.

just check and just prove green.
2026-10-27 12:59:05 -06:00
swaits a4c70dd3b1 fix(bookspec): the rebased P7 sentence speaks CHOICE too (P13) 2026-10-26 21:30:47 -06:00
swaits 7350adb07a feat(bookspec): the checker crate, the Engine trait and the runner (P13)
bookspec is a specification first: every rule normative and sourced,
every rule demonstrated by concrete examples, and any accounting engine
checked against them through one interface (D-34). This moves the
examples' schema out of the test that happened to parse them and makes
the interface explicit.

- `bookspec/checker` (its own workspace, inside this repository until
  the harness is stable): `Example`, `Setup`, `Event`, `Expect`, `Query`,
  `Refusal` and the TOML parser, every structure `deny_unknown_fields`,
  with `Example::from_toml` checking the cross-field shape (closed lists,
  exact decimal amounts, dates, one event kind, `reverses` counting from
  1, the shares summing to 1, commands accepted or refused and not both).
- `Engine` (`load`, `apply`, `query`, `claims`) and `Refusal { rule,
  reason }`; the refusal's rule is the bookspec identifier, so a refusal
  names the rule it belongs to. `Claims` also carries the option an
  engine picks for a CHOICE rule.
- `bookspec/runner` drives any `Engine` through the examples and reports
  per rule: pass, fail, not claimed. It compares the answer itself; an
  engine is never asked whether it agrees with itself.
- `crates/ledger-core/tests/bookspec.rs` is the wanderbooks adapter over
  `Store`: the loader parses through the checker, its own checks are
  unchanged, and the new `the_runner_reports_every_claimed_rule_green`
  runs all 56 examples through the trait. The IIF appendix is answered
  through `ledger_package::export_iif`, the code its own test checks.
- `just check` runs the bookspec workspace's fmt, clippy, rustdoc and
  tests too, so an example cannot rot (D-34, Z-17).

The plan's trait has four methods and no path for an example's
`[[changes]]` (a rename, the policy, an asset correction), which post no
entry: `change` is the fifth, required so an engine cannot skip those
examples silently. This is reported to the EM as the one departure.

One money rule holds here too: an answer's figures are exact decimal
strings, never JSON numbers, so no engine can round a cent through a
float.
2026-10-26 21:22:46 -06:00