feat(store): journal a write-off with its evidence (P4b S7)

A bad debt is deducted on evidence it is worthless (Treas. Reg.
§1.166-2), so the store will not journal one without it:
Store::write_off_invoice takes the evidence as bytes to file or as an
attachment already filed, and files it in the same call — a
receipt.attached on the write-off's entry and an attachment.linked to
the invoice (rules.md B-41). With no evidence it refuses, journaling
nothing.

The entry (Dr 6990 Bad Debt Expense / Cr 1200) and its evidence are
journaled after the engine's whole check and after the feature gate: the
new event type invoice.written_off needs documents_v2, so an older
version stops at the declaration rather than reading a written-off
invoice as still owed (B-27, D-52). Its payload is the invoice, the
entry, the day, the amount and the reason; replay tags the receivable
line WriteOff, and the projection keeps no written-off figure — it is
the tagged lines' sum (B-38).

`meta::invoice_written_off_of` reads and writes the payload; the replay
fuzzer draws the type.

8.0.6: (1) `books_written_under_older_rules_still_open` hand-writes the
new event and its entry through JournalWriter and still opens, with the
figures tailing; (2) a new fixture old-books/p4_s7_before was written by
main 4cad2cfc (the version before this unit) through the old-books-writer
harness — a numbered invoice with terms and snapshots, a partial payment,
a voided invoice and a paid bill — with its expected.json; this version
opens it and reports the same figures, the viewer included; (3) the
cut-over entry is in format_upgrade.rs's books_p2_served_cut_over_whole;
(4) documents_v2_is_its_own_feature is green and the event gate's arms
pass; (5) no figure moves: no golden changed (the previous version's
expected.json is the comparison); (6) merge does not need the owner's
go-ahead; the live books refuse the new write until he gives it.

Tests: write_off_waits_for_documents_v2, write_off_replays_and_projects,
write_off_without_evidence_is_refused; arap_props.rs gains WriteOff.
This commit is contained in:
swaits committed 2026-10-27 12:44:45 -06:00
1 parent 62f2fb058e
commit 4e71cf6320
13 files changed
+1005 -7

No files matched your search

+24
View File
@@ -64,6 +64,9 @@ enum Op {
Reverse(usize, u8),
/// Void document `k` on day `n`.
Void(usize, u8),
/// Write off an invoice `k` (mod the documents) on day `n` for `c`
/// cents, with its evidence filed in the same call.
WriteOff(usize, u8, i64),
/// Close month `m` (January to March), in order.
Close(u8),
/// A hand line on receivables (true) or payables: always refused.
@@ -77,6 +80,8 @@ fn arb_op() -> impl Strategy<Value = Op> {
.prop_map(|(k, n, c, card, r)| Op::Pay(k, n, c, card, r)),
3 => (0usize..40, 0u8..90).prop_map(|(k, n)| Op::Reverse(k, n)),
1 => (0usize..8, 0u8..90).prop_map(|(k, n)| Op::Void(k, n)),
2 => (0usize..8, 0u8..90, 1i64..40_000)
.prop_map(|(k, n, c)| Op::WriteOff(k, n, c)),
1 => (1u8..4).prop_map(Op::Close),
1 => (any::<bool>(), 1i64..10_000).prop_map(|(r, c)| Op::Hand(r, c)),
]
@@ -173,6 +178,25 @@ fn run(s: &mut Store, k: &Setup, ops: &[Op], docs: &mut Vec<DocRef>) -> Result<(
Op::Void(i, n) => s
.void_document(docs[i % docs.len()], day(n), "duplicate")
.map(drop),
Op::WriteOff(..) if docs.is_empty() => Ok(()),
Op::WriteOff(i, n, c) => match docs[i % docs.len()] {
// A bill is never written off: the API takes an invoice.
DocRef::Bill(_) => Ok(()),
DocRef::Invoice(id) => s
.write_off_invoice(
id,
day(n),
cents(c),
"uncollectible",
Some(ledger_store::WriteOffEvidence::File {
file_name: "collection-letter.pdf",
mime_type: "application/pdf",
bytes: b"%PDF-1.4\nFinal demand\n%%EOF\n",
description: Some("Collection attempts"),
}),
)
.map(drop),
},
Op::Close(m) => {
let start = NaiveDate::from_ymd_opt(2026, u32::from(m), 1).unwrap();
let end = start + chrono::Months::new(1) - chrono::Days::new(1);