docs(testing): the P4b S1-B, S2 and S3 sections (U0c)
The 8.0.4 bar asks every unit to record what P12 must mutate (D-33), and
P4b S1 part A has its paragraph while part B, S2 and S3 shipped without
one. S1-B's targets are the payment details (a reference is one line of
at most 200 characters, carried in the entry's description; the
instrument is tagged in the same call) and the two handlers that put
B-38 on the wire (void, control check). S2's are the status filters and
the timeline labels that make a reversed payment read as reversed
instead of disappearing. S3's are the stamp arms and the re-created-copy
branch, where an invoice that has no kept copy says so.
The S2 section records what shipped against the plan's As-built words:
the timeline labels reversed items ("Payment reversed", "Payment
(reversed later)"); the viewer draws no strikethrough.
This commit is contained in:
1 parent
1bfce9157a
commit
e9febc5694
1 file changed
+160
+160
@@ -2647,3 +2647,163 @@ amount match and one-line rule; `refresh_doc`'s settled sum;
|
||||
`doc_status`'s Overpaid and Unattached arms; the aging's `<= 0`;
|
||||
`ensure_payment_features`' comparison; `check_document_reversal`'s date
|
||||
check; `control_check`'s chain netting.
|
||||
|
||||
## P4b S1, part B: void, control check, and each payment's details (2026-10-24)
|
||||
|
||||
The MCP layer of rules.md B-38 (plan P4b S1, part B). Part A (above) is
|
||||
the engine: the tags, the derived figures, the guards. Part B puts it on
|
||||
the wire: `books.void_document` reverses a document's own entry with the
|
||||
reason, `books.control_check` ties each control account to its documents,
|
||||
and `books.receive_payment` and `books.pay_bill` take `instrument` and
|
||||
`reference` and answer the document's derived `remaining` and `status`.
|
||||
It adds no engine rule, kernel function, Kani harness or Lean theorem:
|
||||
it calls what part A proved and tested.
|
||||
|
||||
- **Over the wire, `crates/ledger-cli/tests/spec_mcp.rs`:**
|
||||
`documents_void_and_tie_out_over_the_wire`. A payment of 200.00 against
|
||||
a 500.00 invoice, recorded with instrument "Harbor checking" and
|
||||
reference "tr_8812", answers `remaining` 300.00 and `status` partial,
|
||||
and its entry carries both (the instrument, and a description ending
|
||||
"(ref tr_8812)"). Voiding that invoice is refused and the refusal names
|
||||
the payment entry; an invoice entered twice is voided and reads
|
||||
"void"; a hand line to 1200 is refused. `books.control_check` at
|
||||
2026-06-30 agrees: receivables 1200 has balance 300.00,
|
||||
`documents_total` 300.00 and `difference` 0.00; `books.digest` has no
|
||||
`control_problems` and `books.verify` names the control check it made.
|
||||
The happy-path journey (`happy_path`) voids an invoice and runs the
|
||||
check, so both output schemas are exercised on every run.
|
||||
- **The tool surface, `crates/ledger-mcp/src/server.rs`'s tests:**
|
||||
`annotations_complete` pins `books.void_document` destructive and
|
||||
posting, `books.control_check` read-only, and counts the 29
|
||||
destructive tools the doc names; `period_tools_describe_the_one_default`
|
||||
holds `books.control_check` to the one `as_of` sentence. The generic
|
||||
lints cover the rest without a change of their own:
|
||||
`every_tool_is_documented_and_every_documented_tool_exists`,
|
||||
`tool_text_lint`, `tool_descriptions_fit_700_characters`. `scopes.rs`'s
|
||||
table and `crates/ledger-cli/tests/mcp_journeys.rs::mcp_end_to_end`
|
||||
both count every tool (120 when part B landed, 122 with S2's two).
|
||||
- **Handbook:** `revenue-and-payouts` says what a payment carries, that a
|
||||
document that should never have existed is voided and never deleted,
|
||||
and that `books.control_check`'s findings go to the human.
|
||||
|
||||
**For P12 (mutation targets):** `PaymentDetails::check` (the newline test,
|
||||
the 200-character bound, a blank reference as none);
|
||||
`PaymentDetails::description` (the " (ref R)" suffix, the trim, a blank
|
||||
reference leaving the base); the server's `payment_details` (id or label resolution, an
|
||||
unknown instrument refused); `payment_out` (as of the end of the books,
|
||||
`remaining` from outstanding, `status` from `key()`); `doc_ref` (trim and
|
||||
case, the two kinds, anything else refused); the `void_document` handler
|
||||
(the status it answers, `map_or("void", ..)` after the day); `control_check`
|
||||
and `control_check_out` (`agrees` as zero difference and no unattached
|
||||
line and nothing needing attention, each field's mapping, the
|
||||
invoice/bill kind); `control_problems` (one message per unattached line
|
||||
and per document, both loops); the `digest` that includes it and the
|
||||
`verify` check that refuses a nonzero difference.
|
||||
|
||||
## P4b S2: reading documents, in the tools and in the viewer (2026-10-24)
|
||||
|
||||
`books.documents` and `books.document` (`7d2bf0b7`) and the viewer's
|
||||
Money in and Money out lists and a document's page (`b7b7f171`). No
|
||||
format change and no engine rule: both read the figures part A derives
|
||||
(B-38), and the viewer reads the same ledger directly (I-8).
|
||||
|
||||
The plan calls the timeline's reversed items "struck through"; as built
|
||||
they are labelled: a payment reversed later reads "Payment (reversed
|
||||
later)" and its reversal "Payment reversed". The viewer draws no
|
||||
strikethrough, and the test asserts the labels.
|
||||
|
||||
- **Over the wire, `crates/ledger-cli/tests/spec_mcp.rs`:**
|
||||
`documents_list_and_timeline_over_the_wire`. Two invoices (500.00 due
|
||||
Mar 1, 80.00 due Apr 30), a 200.00 payment against the first, and that
|
||||
payment reversed. `books.documents` at 2026-03-15 lists both as open,
|
||||
due date first, `outstanding_total` 580.00, the first 14 days overdue;
|
||||
`status` overdue keeps one, `due_by` 2026-03-31 keeps one, and the
|
||||
bills list is empty. `books.document` as of that day owes 500.00, paid
|
||||
0.00, status open, and its timeline is three moves in posting order:
|
||||
the origin standing, the payment not standing, and its reversal
|
||||
standing. A bad ref ("invoice") and a bad status ("late") are refused.
|
||||
The happy-path journey lists and reads one, so both output schemas are
|
||||
exercised on every run.
|
||||
- **The tool surface:** `annotations_complete` pins both read-only
|
||||
(table, now 122 tools); `period_tools_describe_the_one_default` holds
|
||||
both to the `as_of` sentence;
|
||||
`every_tool_is_documented_and_every_documented_tool_exists` and the
|
||||
text lints cover the doc; `scopes.rs`'s table and
|
||||
`mcp_journeys.rs::mcp_end_to_end` count 122.
|
||||
- **The viewer, `crates/ledger-gui/tests/invoices.rs`:**
|
||||
`the_list_shows_the_ledgers_figures` (the reversed payment no longer
|
||||
counts: $580 owed, $500 of it late; "Invoice 1, Harbor Client, 14 days
|
||||
late, owes 500.00"; the paid invoice under All; the list ties to 1200;
|
||||
the payables list); `a_documents_page_shows_its_timeline` (the payment
|
||||
"(reversed later)", its reversal "Payment reversed", received $0.00,
|
||||
and a row opens its entry); `the_overview_shows_what_is_owed_both_ways`
|
||||
("Owed to you", "You owe on bills", "1 invoice overdue");
|
||||
`books_without_documents_show_no_overview_tiles_for_them` (no tiles, no
|
||||
list). Each page is drawn for `SHOT_DIR`.
|
||||
|
||||
**For P12 (mutation targets):** the `documents` handler's status arms
|
||||
(`"all"`; `"open"` as `!st.outstanding.is_zero() && key != "void"`;
|
||||
`"overdue"` as `st.overdue_days > 0`; `key == other`), the party compare,
|
||||
`due_by`'s `>` (due on or before the day), the `(due, id)` sort and the
|
||||
`outstanding_total` sum; the `document` handler's
|
||||
`l.date <= as_of` timeline filter,
|
||||
`stands`/`reversal` from `stands_on`, `movement_key`'s arms, and
|
||||
`parse_doc_ref`'s split, parse and kind; `DocStatus::key`'s arms, which
|
||||
four of the filters compare against; the viewer's
|
||||
`Filter::keeps` arms (Open's `!outstanding.is_zero() && != Void`,
|
||||
Overdue's `overdue_days > 0`, Paid, Void, All), a document after the day
|
||||
shown only under All (`Ok(None)`), the list's `(due, date, name)` sort
|
||||
and the timeline's movement×reversal labels with their `(reversed later)`
|
||||
condition (`!t.reversal && !stands_on(t.entry, b.today)`).
|
||||
|
||||
## P4b S3: the invoice PDF (2026-10-24)
|
||||
|
||||
An invoice as a PDF: `ledger_import::invoice_pdf::invoice_pdf` typesets
|
||||
`templates/invoice.typ` with the bundled fonts, and
|
||||
`ledger_package::invoice::invoice_document` builds what it prints from the
|
||||
books once, so the server's download and the viewer's Save give the same
|
||||
bytes for the same books. `books.document { pdf: true }` hands the file
|
||||
over; a bill's PDF is the vendor's, so it is refused. No format change
|
||||
and no engine rule: the figures are part A's (B-38) and the template does
|
||||
no arithmetic.
|
||||
|
||||
- **`crates/ledger-import/src/invoice_pdf.rs`:**
|
||||
`an_invoice_renders_deterministically` (the same document renders to
|
||||
the same `%PDF` bytes twice; saved under `SHOT_DIR` to look at);
|
||||
`text_is_data_and_money_is_formatted_here` (the total, the balance and
|
||||
a settled line as printed, and a description that is Typst markup
|
||||
printed literally); `stamps_render_and_no_lines_is_refused` (every
|
||||
`InvoiceStamp` renders, and an invoice with no lines is refused).
|
||||
- **`crates/ledger-package/src/invoice.rs`:**
|
||||
`an_invoice_prints_its_figures_as_of_a_day` (before and after the
|
||||
payment: the balance, the one settled line, PAID with the day, the
|
||||
seller's address lines, the bill-to, the printed issue date, and the
|
||||
re-created note's day);
|
||||
`the_sample_invoice_in_each_state` (through the books: part paid and
|
||||
past due, its settled line reading "Payment Oct 6, 2026 · ACH · ref
|
||||
tr_8812"; paid in full with PAID and its day; void; each saved under
|
||||
`SHOT_DIR`).
|
||||
- **Over the wire:** `spec_mcp.rs::documents_list_and_timeline_over_the_wire`
|
||||
grew the PDF block: `pdf: true` answers a handed file whose download
|
||||
starts `%PDF` and is named `invoice-<id>.pdf`, and `pdf: true` on a
|
||||
bill is refused.
|
||||
- **The viewer:**
|
||||
`crates/ledger-gui/tests/invoices.rs::a_documents_page_shows_its_timeline`
|
||||
also asserts an invoice's page offers "Save invoice PDF" (a bill's page
|
||||
offers none: the file filed with it is the vendor's copy).
|
||||
|
||||
**For P12 (mutation targets):** `InvoiceStamp::text`'s five arms and
|
||||
`template_data`'s `stamp_draft` test (`== Draft`); `invoice_document`'s
|
||||
stamp match (Void, then Paid with `last_paid`, then `overdue_days > 0`
|
||||
before PartiallyPaid, then none); the settled loop (`movement ==
|
||||
Payment`, `!reversal`, `l.date > as_of`, `!stands_on`, the negated
|
||||
amount, the method and the " · ref " suffix parsed back out of the
|
||||
description, and `last_paid`); the re-created-copy (legacy) branch:
|
||||
`number: format!("Invoice {id}")`, `terms: None`, and the `note` that
|
||||
names `today`, which S4 replaces when the issued copy is kept; `dollars`'
|
||||
sign handling and `format_amount`'s parenthesized negatives, which the
|
||||
settled lines print; `template_data`'s no-lines refusal. Two arms no test
|
||||
asserts from `invoice_document` today, so expect survivors there until
|
||||
one does: `PartiallyPaid` (no sample is part paid and not yet due) and
|
||||
`Draft` (only S4's preview reaches it; the renderer's own test renders
|
||||
it but asserts only that it succeeds).
|
||||
Reference in new issue
Block a user