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:
1 parent
62f2fb058e
commit
4e71cf6320
13 files changed
+1005
-7
No files matched your search
@@ -1864,6 +1864,7 @@ fn books_written_under_older_rules_still_open() {
|
||||
(4, "4000", "Revenue", "Revenue"),
|
||||
(5, "2000", "Accounts Payable", "Liability"),
|
||||
(6, "6500", "Supplies", "Expense"),
|
||||
(7, "6990", "Bad Debt Expense", "Expense"),
|
||||
] {
|
||||
w.append(
|
||||
"account.added",
|
||||
@@ -1946,13 +1947,44 @@ fn books_written_under_older_rules_still_open() {
|
||||
"memo": "A4 paper", "duplicate_reason": "the vendor sent it twice"}),
|
||||
)
|
||||
.unwrap();
|
||||
// P4b S7's new shape, hand-written as this version writes it: a
|
||||
// bad-debt write-off of invoice 1 (its entry, then the event). This
|
||||
// reader tags the receivable line WriteOff and derives the figures;
|
||||
// the version before it stopped at the declaration.
|
||||
w.append(
|
||||
"entry.created",
|
||||
json!({"id": 4, "date": "2026-04-10", "description": "Write off invoice 1: uncollectible"}),
|
||||
)
|
||||
.unwrap();
|
||||
for (account, side) in [(7, "debit"), (3, "credit")] {
|
||||
w.append(
|
||||
"entry.line_added",
|
||||
json!({"id": 4, "account": account, "side": side, "amount": "30.00", "currency": "USD"}),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
w.append(
|
||||
"entry.posted",
|
||||
json!({"id": 4, "date": "2026-04-10",
|
||||
"description": "Write off invoice 1: uncollectible", "reversal_of": null,
|
||||
"lines": [
|
||||
{"account": 7, "side": "debit", "amount": "30.00", "currency": "USD"},
|
||||
{"account": 3, "side": "credit", "amount": "30.00", "currency": "USD"}]}),
|
||||
)
|
||||
.unwrap();
|
||||
w.append(
|
||||
"invoice.written_off",
|
||||
json!({"invoice_id": 1, "entry_id": 4, "date": "2026-04-10",
|
||||
"amount": "30.00", "reason": "uncollectible"}),
|
||||
)
|
||||
.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()));
|
||||
let store = Store::open(&dir).expect("older books still open");
|
||||
store.verify().unwrap();
|
||||
assert_eq!(store.journal_format().format, 1);
|
||||
assert_eq!(store.ledger().chart().len(), 6);
|
||||
assert_eq!(store.ledger().chart().len(), 7);
|
||||
assert_eq!(store.ledger().list_periods().len(), 2);
|
||||
let lines = store.ledger().get_entry(1).unwrap().lines();
|
||||
assert_eq!(lines.len(), 2);
|
||||
@@ -2007,6 +2039,20 @@ 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));
|
||||
// The write-off replays as written: its line is the invoice's
|
||||
// (WriteOff), so what it owes fell by 30.00 and what was written off
|
||||
// rose by it (B-41, B-38).
|
||||
let st = store
|
||||
.ledger()
|
||||
.invoice_state(1, NaiveDate::from_ymd_opt(2026, 6, 30).unwrap())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(st.written_off, "30.00".parse::<Decimal>().unwrap());
|
||||
assert_eq!(st.outstanding, "69.99".parse::<Decimal>().unwrap());
|
||||
assert_eq!(
|
||||
store.ledger().line_doc(4, 1).map(|t| t.movement),
|
||||
Some(ledger_core::Movement::WriteOff)
|
||||
);
|
||||
}
|
||||
|
||||
/// The books an earlier wanderbooks wrote, one directory each under
|
||||
@@ -2022,7 +2068,11 @@ fn books_written_under_older_rules_still_open() {
|
||||
/// 10,000 years, schedules and assets running into chrono's last month,
|
||||
/// depreciation posted for 10000-01, one recurring run of 1,044 drafts.
|
||||
/// tests/fixtures/old-books-writer is the harness that wrote them; each
|
||||
/// later phase adds the books its version writes.
|
||||
/// later phase adds the books its version writes. `p4_s7_before` is P4b
|
||||
/// S7's: the books main 4cad2cfc (the version before it) wrote — a
|
||||
/// numbered invoice with terms and snapshots, a partial payment, a voided
|
||||
/// invoice and a paid bill, no write-off — with that version's
|
||||
/// expected.json.
|
||||
fn old_books() -> Vec<PathBuf> {
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/old-books");
|
||||
let mut dirs: Vec<PathBuf> = std::fs::read_dir(&root)
|
||||
|
||||
Reference in new issue
Block a user