feat(arap): record an invoice's sends as invoice.sent (P4b S5)

The books never send invoices (A-4), but they must remember that one
went out: who sent it, how, to whom, when, whether it was the original
or a reminder, and the channel's own reference. That record is a fact
of the document's life, not a figure: a send moves no money, so it is
one journal line (`invoice.sent`) and no entry, and every derived
figure (outstanding, aging, DocState) is exactly as it was.

`Ledger::mark_sent` refuses a send on a void invoice (it should never
have reached the customer), a send dated before the invoice, no
recipients, and any recipient, reference or note that would put a full
TIN or control characters into the journal. Open, partial, paid and
written-off invoices can be sent (a copy for the customer's records, a
reminder).

The event is `documents_v2`'s (D-52): an older version refuses the
type rather than dropping the record. Replay restores it and the
projection keeps it in schema 16's `invoice_sends` table, so live,
replay and the viewer agree. The new old-books fixture `p4_s5_before`
was written by eeee9658 and opens to the same figures; a hand-written
send in `books_written_under_older_rules_still_open` proves an older
journal with the new line still opens.

Tests: mark_sent_waits_for_documents_v2, sends_replay_and_project,
a_send_changes_no_figure, mark_sent_on_void_refused, Op::MarkSent in
status_is_a_function_of_journal_and_date,
an_older_version_stops_at_invoice_sent.
This commit is contained in:
swaits committed 2026-10-27 13:24:10 -06:00
1 parent ed637f0e27
commit d1fb512e4b
16 files changed
+979 -5

No files matched your search

+17
View File
@@ -1946,6 +1946,15 @@ fn books_written_under_older_rules_still_open() {
"memo": "A4 paper", "duplicate_reason": "the vendor sent it twice"}),
)
.unwrap();
// P4b S5's new shape: the record that the invoice went out
// (`invoice.sent`, feature documents_v2). A send moves no figure, so
// the document's figures are the same before and after it.
w.append(
"invoice.sent",
json!({"invoice_id": 1, "on": "2026-04-05", "by": "Avery Quill", "via": "email",
"to": ["billing@fernhill.example"], "kind": "original", "reference": "msg-1"}),
)
.unwrap();
drop(w);
// Opening rebuilds from the journal; it must accept all of it.
let _ = std::fs::remove_file(dir.join(ledger_store::db_file()));
@@ -2007,6 +2016,14 @@ fn books_written_under_older_rules_still_open() {
);
assert_eq!(bill.memo.as_deref(), Some("A4 paper"));
assert_eq!(bill.lines[0].account, Some(6));
// P4b S5's send replays as written, and moves no figure.
let sends = &store.ledger().get_invoice(1).expect("the invoice").sends;
assert_eq!(sends.len(), 1);
assert_eq!(sends[0].on, NaiveDate::from_ymd_opt(2026, 4, 5).unwrap());
assert_eq!(sends[0].by, "Avery Quill");
assert_eq!(sends[0].to, ["billing@fernhill.example"]);
assert_eq!(sends[0].kind, ledger_core::arap::SendKind::Original);
assert_eq!(sends[0].reference.as_deref(), Some("msg-1"));
}
/// The books an earlier wanderbooks wrote, one directory each under