merge: P4b S5, sent tracking (branch S5, ad649be3)
The second of the paused branches. invoice.sent, Ledger::mark_sent (with its Lean step), books.mark_sent in its own group, the two attention items, schema 17. Conflicts, resolved as plan 8.0.2 says: the shared over-the-wire test file crates/ledger-cli/tests/documents.rs (S4.4's file kept whole, S5's test appended, its create call given the confirmed: true the S4.4 contract now needs), types.rs (both appends kept), docs/parity.md (the review's oracle row and S5's new row). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
commit
3a3f03d884
44 files changed
+1847
-30
No files matched your search
@@ -169,6 +169,7 @@ groups are:
|
||||
|
||||
- `documents` — invoices, bills, payments, voids, `books.documents`,
|
||||
`books.document`, the control check and the agings;
|
||||
- `sent` — `books.mark_sent`, the record that an invoice went out (P4b S5);
|
||||
- `entries` — `books.draft_entry`, `books.add_line`, `books.post_entry`,
|
||||
`books.reverse_entry`, `books.get_entry`, `books.search_entries`, the
|
||||
bank CSV import;
|
||||
|
||||
+21
-3
@@ -2107,9 +2107,24 @@ snapshots; see plan 8.2 S4.
|
||||
number and reads `void`. Refused while a payment stands on it (the
|
||||
refusal names the payment entry to reverse first), when void already,
|
||||
before its date or in a closed period (P4b S1, B-38).
|
||||
- `books.mark_sent { invoice, on?, by?, via?, recipients?: [string],
|
||||
kind?, reference?, note? }` → `{ invoice, sends: [{ on, by, via, to, kind,
|
||||
reference?, note? }] }`. Records that an invoice went out (P4b S5,
|
||||
A-4): the books never send, so the owner or a delegate sends and this
|
||||
records who (`by`: the token's name unless given), how (`via`: email
|
||||
the default, or mail, portal, hand, another channel's name), to whom
|
||||
(`recipients`: the invoice's bill-to email, else the customer's, else
|
||||
required), when (`on`: today in the company's time zone), original or
|
||||
reminder (`kind`: original on the first send, reminder after), and the
|
||||
channel's own id (`reference`: a message id). `invoice` is
|
||||
`invoice:N` or `N`. Several sends are allowed; none moves a figure or
|
||||
the due date. Refused on a void invoice, dated before it, with no
|
||||
recipients, or while the books wait for `documents_v2`
|
||||
(`format_upgrade_pending`). `sends` is every send on the invoice, in
|
||||
order, the new one last.
|
||||
- `books.documents { kind, status?, party?, due_by?, as_of? }` → `{ as_of,
|
||||
kind, documents: [{ ref, party, date, due_date, total, outstanding,
|
||||
status, overdue_days }], outstanding_total }`. Invoices (`kind`
|
||||
status, overdue_days, last_sent? }], outstanding_total }`. Invoices (`kind`
|
||||
`invoices`) or bills (`bills`), due date first, every figure derived
|
||||
from the document's lines that stand (B-38). `status` filters: `open`
|
||||
(default: anything still owed, partly paid and overdue included),
|
||||
@@ -2117,11 +2132,14 @@ snapshots; see plan 8.2 S4.
|
||||
own `status` is one of `open`, `partial`, `paid`, `written_off`,
|
||||
`void`, `overpaid` or `unattached` (`DocStatus::key()`). `party` (a
|
||||
customer's or vendor's name, ignoring case) and `due_by` (due on or
|
||||
before that day) narrow the list (P4b S2).
|
||||
before that day) narrow the list (P4b S2). `last_sent` is the day the
|
||||
invoice last went out, when a send is recorded (P4b S5); absent for an
|
||||
invoice never sent and for every bill.
|
||||
- `books.document { ref, as_of?, pdf? }` → `{ ref, party, date, due_date,
|
||||
entry_id, lines: [{ description, amount, project? }], total, paid,
|
||||
discounted, written_off, outstanding, status, overdue_days, timeline:
|
||||
[{ date, entry_id, kind, amount, reversal, stands }], pdf? }`. `ref` is
|
||||
[{ date, entry_id, kind, amount, reversal, stands }], sends: [{ on, by,
|
||||
via, to, kind, reference?, note? }], pdf? }`. `ref` is
|
||||
`invoice:N` or `bill:N`; `status` as `books.documents`' rows; the
|
||||
timeline is every ledger line tagged with the document, dated on or
|
||||
before the day, in posting order, each saying whether its entry still
|
||||
|
||||
@@ -45,6 +45,7 @@ No other statuses.
|
||||
| 1099 vendor tracking + year-end worksheet | Vendors → Track payments for 1099; 1099 report | partial | hand-verified fixture (bill payments, card exclusion, reversal, year boundary); no golden file yet |
|
||||
| Prepaid expense + deferred revenue amortization | Amortization schedules (Xero/QBO add-ons) | partial | hand-verified fixture, to the cent; no golden file yet |
|
||||
| Automated cross-implementation diff harness (GnuCash first) | — (`just oracle`) | partial | live: `tests/oracle_gnucash.rs` runs 200 random books through GnuCash 5.10 in a pinned container, 15,800 exact comparisons, weekly in CI (docs/oracle.md Layer 5); open: fixture books diffed as `snapshot()` output, and the machine-readable catalog with CI enforcing this floor |
|
||||
| Invoice sent tracking (who, how, to whom, when, original or reminder, the channel's reference) | Business → Invoices (Xero's "Sent"; QBO's email) | done | P4b S5; records, never sends (A-4); a send changes no figure; `invoice.sent` under `documents_v2` (D-52) |
|
||||
|
||||
Workflow/UI parity (QuickBooks/Xero reference) gets its own catalog when the
|
||||
egui phase starts; semantics come first.
|
||||
@@ -135,3 +136,18 @@ door on loopback for Caddy or a tunnel. `crates/ledger-cli/tests/two_user.rs`
|
||||
proves the account boundary and the move's head in CI
|
||||
(`.github/workflows/two-user.yml`); the recipe is `deploy/README.md`.
|
||||
The cutover rule in `docs/capability-parity.md` is unchanged.
|
||||
|
||||
### P4b S5: sent tracking (2026-10-27)
|
||||
|
||||
The catalog gains one row: an invoice's recorded sends. The books never
|
||||
send (A-4): `books.mark_sent` records who sent an invoice, how, to whom,
|
||||
when, whether it was the original or a reminder, and the channel's own
|
||||
reference, for an owner or a delegate such as an agent. The record is
|
||||
`invoice.sent`, under `documents_v2` (D-52), and it moves no figure, so
|
||||
the A/R and A/P rows' figures are unchanged. The floor never decreases:
|
||||
the row is `done`, tested by `sends_replay_and_project`,
|
||||
`a_send_changes_no_figure` and `mark_sent_records_who_and_how`. Sent
|
||||
tracking also feeds Needs attention: an issued invoice nobody marked sent
|
||||
(past `invoicing.unsent_days`) and an overdue invoice not reminded within
|
||||
`invoicing.reminder_days` are one list in the viewer and in `books.start`,
|
||||
each offering `books.mark_sent` (step 4; U0b's one engine).
|
||||
@@ -58,8 +58,10 @@ apply is safe — on startup the applier replays every journal line with
|
||||
- `ts`: UTC, RFC 3339. Recorded, never trusted for ordering (`seq` orders).
|
||||
- `type`: `account.added`, `entry.posted`, `entry.reversed`, `invoice.created`,
|
||||
`bill.created`, `receipt.attached`, `receipt.linked`, `receipt.unlinked`,
|
||||
`document.filed`, `attachment.withdrawn` (plus the A/R, A/P, asset,
|
||||
period, payroll, and class events listed on `JournalLine`).
|
||||
`document.filed`, `attachment.withdrawn`, `invoice.sent` (a recorded
|
||||
send: a document's record, never a ledger line or a figure; P4b S5)
|
||||
(plus the A/R, A/P, asset, period, payroll, and class events listed on
|
||||
`JournalLine`).
|
||||
- `payload`: the full domain facts (account fields, entry with lines, blob
|
||||
sha, ...). Everything the applier needs; nothing it must look up elsewhere.
|
||||
An `invoice.created` or `bill.created` written after the `documents_v2`
|
||||
|
||||
+5
-2
@@ -1746,8 +1746,11 @@ person, a billing or shipping address, a billing email (D-55); an
|
||||
`invoice.created` or `bill.created`
|
||||
carrying a number, terms, a bill-to or seller snapshot, a customer
|
||||
reference or memo, a vendor reference, a receipt date or a duplicate's
|
||||
reason, or a line naming its account, quantity or unit price (S4.3); and
|
||||
S5 to S7's sends, settlements and write-offs. An older version would
|
||||
reason, or a line naming its account, quantity or unit price (S4.3);
|
||||
S5's `invoice.sent` (who sent an invoice, how, to whom and when: a record
|
||||
that moves no figure); S6's payments with `line`, `discount`,
|
||||
`discount_line`, `discount_reason`, `fee` or `fee_line`; and S7's
|
||||
`invoice.written_off`. An older version would
|
||||
restore the document without them silently: every invoice line would post
|
||||
to the revenue role and every bill line to general expenses, and every new
|
||||
event type would be refused. A command asks for
|
||||
|
||||
@@ -3050,3 +3050,67 @@ 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 S5: sent tracking (2026-10-27)
|
||||
|
||||
The books record that an invoice went out; they never send it (A-4). A
|
||||
send is `invoice.sent`, under `documents_v2` (D-52), and moves no figure:
|
||||
`Ledger::mark_sent` pushes a `Sent` onto the invoice, the store journals
|
||||
one line and no entry, replay restores it and the projection keeps it in
|
||||
`schema` v17's `invoice_sends` (S4.3 took 16).
|
||||
|
||||
- **`ledger-store` `tests/arap_standing.rs`:** `mark_sent_waits_for_documents_v2`
|
||||
(books with no features: refused, nothing journaled; after `upgrade`,
|
||||
taken, and the reopen equals what was live), `sends_replay_and_project`
|
||||
(two sends: the journal's `invoice.sent` carries the whole record in
|
||||
order, `verify` passes, the reopened invoice holds both),
|
||||
`a_send_changes_no_figure` (on the `p4-arap-before` fixture: the report
|
||||
snapshot, the invoice's `DocState` and every derived cache are equal
|
||||
before and after), `mark_sent_on_void_refused` (`DocumentVoid`,
|
||||
nothing journaled; a send dated before the void is taken).
|
||||
- **`ledger-store` `tests/arap_props.rs`:** `status_is_a_function_of_journal_and_date`
|
||||
gains `Op::MarkSent` (random sends on invoices, skipped on bills): every
|
||||
figure, the control check and both agings still agree across live,
|
||||
replay and snapshot.
|
||||
- **`ledger-store` `tests/format.rs`:** `an_older_version_stops_at_invoice_sent`
|
||||
(`invoice.sent` is this version's, a reader without `documents_v2` waits,
|
||||
and the books declare the feature before the first send line).
|
||||
- **8.0.6:** `books_written_under_older_rules_still_open` hand-writes an
|
||||
`invoice.sent` and still opens; the fixture `old-books/p4_s5_before`
|
||||
was written by eeee9658 (the revision before this unit) and opens to
|
||||
the same figures; `books_p2_served_cut_over_whole` refuses a send before
|
||||
consent and takes it after.
|
||||
- **`ledger-cli` `tests/documents.rs`:** `mark_sent_records_who_and_how`
|
||||
(two sends over the wire: the token's name, email and original by
|
||||
default, then a reminder with a message id; `books.documents` shows
|
||||
`last_sent`, `books.document` both sends, and the invoice still owes its
|
||||
total).
|
||||
- **`ledger-mcp/src/handbook.rs`:** the new `invoices-and-bills` section
|
||||
renders, and its id is unique (the existing handbook tests).
|
||||
- **Step 4, Attention (R-F):** `ledger-gui/tests/invoices.rs`:
|
||||
`an_unsent_invoice_is_listed_until_a_send_clears_it` (an issued invoice
|
||||
past `invoicing.unsent_days` is one to-do opening the invoice; the
|
||||
overview draws it, `attention-unsent-invoice.png`; a send clears it),
|
||||
`a_legacy_invoice_with_no_number_is_never_listed_as_unsent` (exempt),
|
||||
`an_overdue_invoice_with_an_old_send_is_asked_to_remind` (due Mar 1,
|
||||
last sent Feb 20: 14 days overdue, target the invoice, prompt names
|
||||
`books.mark_sent`). `ledger-cli/tests/attention_parity.rs`:
|
||||
`an_unsent_invoice_is_on_both_lists` (the viewer's list and `books.start`
|
||||
both carry the item, and the step is `books.mark_sent` on `invoice:1`).
|
||||
`the_viewer_and_books_start_see_the_same_attention` stays green.
|
||||
|
||||
**For P12 (mutation targets):** `Ledger::mark_sent`'s refusals (unknown
|
||||
invoice, a send dated before it, `DocumentVoid`, empty `to`, and each of
|
||||
the three `check_text` calls; the note's check is stricter than the plan's
|
||||
list, to keep a full TIN out of the journal); `SentVia::parse`'s arms and
|
||||
its `other:` prefix; `SendKind::parse`'s two arms; `sent_via`'s arms and
|
||||
its empty fallback; `send_kind`'s error arm; the handler's `kind` default
|
||||
(`original` when `sends` is empty, `reminder` after), its `to` fallback
|
||||
(bill-to, then party, then refused) and its `by` fallback; `invoice_id`'s
|
||||
`invoice:` strip; `last_sent`'s `max`; the replay reader's
|
||||
`newer_content` arms for an unknown channel or kind; and the two attention
|
||||
predicates in `ledger-package::attention`: `invoice.number.is_some()`
|
||||
(the legacy exemption), `Open | Partial`, `sends.is_empty()`, the
|
||||
`checked_add_days` `unsent_days` comparison (`>`), `overdue_days > 0`, the
|
||||
`sends.max()` last send, and the `reminder_days` comparison (`>`), plus
|
||||
each item's target (`Target::Invoice`) and `books.mark_sent` step args.
|
||||
@@ -542,6 +542,24 @@ come from the kernel:
|
||||
No Lean model: these are two arithmetic laws, not a new `Ledger`
|
||||
command (S4.1 adds fields to the existing document commands).
|
||||
|
||||
### Sent tracking (P4b S5)
|
||||
|
||||
34. **A send changes no figure.** `Ledger::mark_sent` is a command, so it
|
||||
is a `Step` in the model (AGENTS.md's formal-methods bar; the unit's
|
||||
"no Lean change" phrase was overruled by it, and says so in its As
|
||||
built block). `Documents.lean`'s `Books` gains `sends`, and
|
||||
`Step.markSent` records one on a document the lines carry, touching
|
||||
no line. Every control balance, every document's sum and what is
|
||||
unattached are therefore the same before and after, through any day,
|
||||
and `Reachable.tagged` (the control lines are all documents') covers
|
||||
the new step; the engine's side is the same statement, checked by
|
||||
`a_send_changes_no_figure` on the fixture book.
|
||||
- Lean `Documents.Step.markSent_changes_no_figure` (no axioms) ·
|
||||
`Documents.Reachable.tagged`, `Documents.Reachable.control_is_documents`
|
||||
cover the new step · tests `sends_replay_and_project`,
|
||||
`a_send_changes_no_figure`, `mark_sent_on_void_refused`,
|
||||
`mark_sent_waits_for_documents_v2`
|
||||
|
||||
## What the proofs found
|
||||
|
||||
The engine already had 190+ tests, property tests, mutation testing,
|
||||
|
||||
Reference in new issue
Block a user