feat(mcp): payments on terms, bills due, and the agings' tie-out (P4b S6)

books.receive_payment and books.pay_bill take applications, each
document's cash with an optional discount and reason, and a customer
payment may carry the processor's fee; the result names every document it
settled and what reached the account. books.bills_due lists what to pay
and the running total by due date. The agings take detail and return the
control account's tie-out, and books.export writes ar_aging/ap_aging.

The handlers live in tools/settlements.rs (the EM's split, so S4.4's
documents.rs stays free); documents.rs keeps the documents, the void and
the control check. docs/mcp-interface.md documents every changed tool and
the new one; docs/architecture.md names the group; docs/testing.md
records the tests and P12's mutation targets.

Old names (document_id, cash_account) still work; the output keeps
remaining/status for one document. A fee's processor is named in the
description, not tagged as the entry's vendor: ledger tags are entry-wide
and tagging would move the customer's party totals and 1099-NEC.

The handbook's invoices-and-bills section gains the payment, discount,
fee and bounce-replacement procedures (S4.5 can extend it). Over the wire:
one_payment_settles_two_invoices, late_discount_needs_a_reason,
a_fee_posts_gross_to_receivables_and_net_to_the_bank,
card_paid_bill_is_not_1099_nec,
multi_document_payment_counts_once_for_1099_and_party_totals,
overpayment_refused_with_the_excess,
bills_due_orders_by_due_and_discount and the_aging_export_ties_out.

Terms cannot yet be journaled on a document (S4.3) and a bill line's
account is not an MCP field (S4.4), so bill_on_terms_with_discount,
equipment_bill_payment_is_investing_cash and the discount half of
bills_due are core tests until those land; the unit's As-built block
records this.
This commit is contained in:
swaits committed 2026-10-27 12:59:40 -06:00
1 parent 78a2385440
commit 57f04dbb0c
17 files changed
+1467 -231

No files matched your search

+5 -2
View File
@@ -147,8 +147,11 @@ each declared with `#[path]`, opening with `use super::*`, and holding a
BooksServer`; `server.rs`'s `tool_router` adds every group's router. The
groups are:
- `documents` — invoices, bills, payments, voids, `books.documents`,
`books.document`, the control check and the agings;
- `documents` — invoices, bills, voids, `books.documents`,
`books.document` and the control check;
- `settlements` — payments that settle one or several documents
(`books.receive_payment`, `books.pay_bill`), `books.bills_due`, and the
agings (`books.ar_aging`, `books.ap_aging`) with their tie-out (P4b S6);
- `entries` — `books.draft_entry`, `books.add_line`, `books.post_entry`,
`books.reverse_entry`, `books.get_entry`, `books.search_entries`, the
bank CSV import;
+52 -19
View File
@@ -1668,7 +1668,11 @@ posting.
`journal`: each entry posted in the period, line by line; `iif`:
every posted entry from the first through `as_of`, never a period),
`tax_summary` (see "Tools — tax categories"), `fixed_asset_register`
(as of a day; see "Tools — fixed assets").
(as of a day; see "Tools — fixed assets"), `ar_aging`, `ap_aging`
(as of a day: columns `number,party,date,due_date,terms,total,paid,
discounted,outstanding,bucket,days_overdue`, one row per open
document, then a tie-out line `TOTAL,,,,,,,,<Σ outstanding>,GL
<account> <balance>,difference 0.00`; B-23, 8.3.10).
- Formats: `pdf` and `xlsx` for `profit_and_loss`,
`balance_sheet`, `cash_flow`, `trial_balance`, `members_equity`,
`tax_summary` and `fixed_asset_register`; `xlsx`, `csv`, `json` for
@@ -2008,21 +2012,37 @@ S4 changes `books.create_invoice` and `books.create_bill`; see plan 8.2 S4.
never tagged; `notes` as for invoices, but none for an on-spec project
(a cost of on-spec work is an expense like any other); the feature as
for invoices.
- `books.receive_payment { document_id, date, amount, cash_account,
instrument?, reference? }` → `{ entry_id, remaining, status }`. Posts
Dr Cash / Cr A/R, the receivable line tagged with the invoice (B-38),
neither line tagged with a project (balance-sheet only: the invoice's
lines carry its projects). `instrument` (id or label) is tagged on the
entry in the same call; `reference` (one line, at most 200 characters:
a charge id, a trace number) goes into its description. `remaining`
and `status` (open, partial, paid, written_off, void, overpaid,
unattached) are the invoice's derived figures after it. The books
record payments; they never collect them (D-45).
- `books.pay_bill { document_id, date, amount, cash_account, instrument?,
reference? }` → `{ entry_id, remaining, status }`. Posts Dr A/P / Cr
Cash, the payable line tagged with the bill, neither line tagged with a
project; `instrument` and `reference` as for `receive_payment` (a card
payment is left out of 1099-NEC, D-40).
- `books.receive_payment { date, deposit_to | cash_account, instrument?,
reference?, invoice | document_id, amount? | applications: [{ invoice,
amount, discount?, discount_reason? }], fee?: { amount, account?,
vendor? }, paid_in_full_marking? }` → `{ entry_id, documents: [{ ref,
number?, outstanding, status }], deposited, remaining?, status?,
warnings? }`. Settles one or several invoices of one customer in one
entry (B-42): Dr the deposit account the cash less the fee, Dr the
payment-processing-fees account the fee, Dr sales allowances the
discounts, Cr A/R each invoice's cash and discount, every receivable
line tagged with its invoice (payment lines in application order, then
the discount lines). The `amount`s are cash only; a discount goes beside
one. A discount at most the invoice's terms allow and dated by their
discount date needs no reason; a larger or later one needs
`discount_reason`. `fee.vendor` (a processor the books know as a vendor)
is named in the entry's description; only cash movements count toward a
1099, so the fee is not a payment to the processor. `deposited` is the
cash less the fee (A-4's gross presentation); `remaining` and `status`
repeat the single document's figures. An overpayment is refused with the
excess and the remedy (a plain entry to 2300 Customer Deposits).
`paid_in_full_marking: true` adds a UCC §3-311 warning and records
nothing else. `document_id` and `cash_account` are the older names of
`invoice`/`bill` and `deposit_to`/`paid_from`. The books record
payments; they never collect them (D-45).
- `books.pay_bill { date, paid_from | cash_account, instrument?,
reference?, bill | document_id, amount? | applications: [{ bill, amount,
discount?, discount_reason? }] }` → as `receive_payment`, per bill and
one vendor (B-42): Dr A/P each bill's cash and discount, Cr the payer
account the cash, Cr purchase discounts the discounts. A discount on a
bill with an asset or prepaid line is refused with its remedy (record
the bill net, or reclassify Dr 6995 / Cr the asset, D-49); a processor's
fee is refused on a bill.
- `books.void_document { kind, id, date, reason }` → `{ entry_id, status }`.
Voids an invoice or bill (`kind` `invoice` or `bill`) that should never
have existed by reversing its own entry with the reason; it keeps its
@@ -2060,9 +2080,22 @@ S4 changes `books.create_invoice` and `books.create_bill`; see plan 8.2 S4.
there is no unattached line and nothing needing attention.
`books.verify` checks the same identity and `books.digest` lists its
problems (`control_problems`).
- `books.ar_aging { as_of? }` → `{ as_of, rows: [{ name, current, days_30,
days_60, days_90, days_90_plus, total }] }` (`as_of` omitted: today).
- `books.ap_aging { as_of? }` → same, per vendor (`as_of` omitted: today).
- `books.ar_aging { as_of?, detail? }` → `{ as_of, rows: [{ name,
current, days_30, days_60, days_90, days_90_plus, total }], documents?:
[{ ref, number?, party, date, due_date, terms?, total, paid,
discounted, outstanding, bucket, days_overdue }], control: { account,
balance, documents_total, unattached, difference } }` (`as_of` omitted:
today). `detail: true` adds one row per open invoice; `control` is the
receivables account's tie-out (B-23, B-38 INVARIANT 1), `difference`
always `0.00`.
- `books.ap_aging { as_of?, detail? }` → same, per vendor and bill.
- `books.bills_due { through?, as_of? }` → `{ as_of, through, bills:
[{ ref, vendor, vendor_ref?, due_date, outstanding, discount_by?,
max_discount?, pay_by_discount_date_amount? }], by_date: [{ date,
amount }] }`. Every open bill due on or before `through` (omitted:
today), due date first, with its terms' early-payment discount when it
is still open (`discount_by`, the most off, what to pay to take it);
`by_date` is the running total of what falls due through each date.
## Tools — foreign exchange
+77
View File
@@ -3050,3 +3050,80 @@ and `BooksServer::new`'s copy (a dropped field must fail
`isolation_label_is_enforced_under_the_unit`); the host's `server_flags`
extension and `parse_args`' passthrough of `--isolation` and its optional
`--http` value.
## P4b S6: paying on terms (2026-10-27)
rules.md B-42; plan 8.2 P4b S6. One payment settles several documents of
one party, with the early-payment discounts their terms allow, the
processor's fee posted gross, a bills-due list, the agings' detail and
tie-out, and the aging exports.
- **Core (`crates/ledger-core/tests/spec_documents.rs`):**
`bill_on_terms_with_discount` (the gross-method entry for a bill on
"2/10 net 30", and a 3% discount refused without a reason, taken with
one), `late_discount_needs_a_reason` (after the discount date, and the
asset-line refusal's unchanged books),
`discount_on_an_asset_bill_is_refused_with_the_remedy` (D-49: the code
and both remedies), `a_fee_posts_gross_to_receivables_and_net_to_the_bank`
(Dr bank 97, Dr 6060 3, Cr A/R 100),
`a_settlement_names_its_lines_and_refuses_a_document_twice` (control
lines in application order; one document twice refused).
`spec_cash_flow.rs::equipment_bill_payment_is_investing_cash` keeps
D-35 through a settlement.
- **Kernel:** `kernel_bridge.rs::settle_agrees`; Kani
`settle_is_exact` (a nonzero discount and a nonzero fee covered).
- **Store (`crates/ledger-store/tests/arap_standing.rs`):**
`a_settlement_names_its_lines_and_replays` (one entry, two invoices, a
discount and a fee; each event's `line`, `discount_line` and `fee_line`
point at the rows the plan posts; replay equals live);
`settlement_waits_for_documents_v2` (refused with the feature remedy and
no journal line, then accepted after the upgrade);
`legacy_payment_events_keep_the_positional_rule` (the old fixture's
events carry no `line`, and replay still tags in order).
`arap_props.rs`'s history gains `Settle` (two documents, a discount and,
on invoices, a fee), so `subledger_equals_control`, the bounds and
replay-equals-live run over settlements.
- **Store's older shapes:** `tests/store.rs::books_written_under_older_rules_still_open`
writes a payment event of S6's shape by hand through `JournalWriter`
and opens the books, the invoice reading paid and discounted.
- **Over the wire (`crates/ledger-cli/tests/documents.rs`):**
`one_payment_settles_two_invoices`, `late_discount_needs_a_reason`,
`a_fee_posts_gross_to_receivables_and_net_to_the_bank`,
`card_paid_bill_is_not_1099_nec`,
`multi_document_payment_counts_once_for_1099_and_party_totals`,
`overpayment_refused_with_the_excess`,
`bills_due_orders_by_due_and_discount` (the list and its running total;
no terms, so no discount fields), `the_aging_export_ties_out` (the CSV
header, row and `TOTAL` tie-out).
- **CPA package (`crates/ledger-package/tests/package.rs`):**
`the_aging_csvs_tie_to_the_control_check`.
- **Viewer (`crates/ledger-gui/tests/invoices.rs`):**
`the_list_shows_the_ledgers_figures` re-renders the Money out list's
screenshot with the new "Discount by" column (blank until S4.3
journals terms).
- **Not yet possible through the MCP (depends on S4.3/S4.4):**
`bill_on_terms_with_discount` and `bills_due_orders_by_due_and_discount`'s
discount half need a journaled `terms` field (S4.3) and
`equipment_bill_payment_is_investing_cash` needs a bill line's account
in `books.create_bill` (S4.4). The core tests named for them stand in
until those land; see the unit's report.
- **Older books (8.0.6):** the pre-S1 fixture
`tests/fixtures/p4-arap-before` is also pre-S6 and holds payments; its
golden figures still hold (`legacy_reversal_now_counts`), and S6 moves
no figure. Its journal has no declared `documents_v2`, so the store test
above proves the refusal and the upgrade.
**For P12 (mutation targets):** `kernel::settle`'s length check, each
negative check, the fee-above-cash test and both checked adds;
`Ledger::settlement_lines`'s kind/party checks, the duplicate-document
check, the per-application `cash + discount` bound, the discount window
(`<= max`, `<= by`, "has a reason"), the asset-bill loop, the fee-on-bill
refusal and the row order (payment lines, discounts, fee, cash);
`check_discount`'s `within` conjunction and its `because` arms;
`replay_*_payment_at`'s line/amount/account/side match and the discount
line's tag; the store's `documents_v2` condition (more than one
application, any discount, a fee) and the `line`/`discount_line`/`fee_line`
arithmetic; `bills_due`'s `through` filter, the discount-window test and
the running-total grouping; `aging_documents`'s outstanding filter and
bucket; `aging_csv`'s tie-out columns and `aging_statement`'s totals; the
viewer's `discount_by` and `days_overdue` flags.