test(store): a write-off whose notice is another entry's receipt is refused before posting
The defect found merging S7 and P6: under P6's dedup the notice's bytes resolve to the record already on file, and the one-receipt-one- entry rule refused it only after the write-off had journaled its entry. The store now checks the deduplicated record before the journal line (`6c26160e`'s companion fix in `write_off_invoice`); this test pins it: the refusal names the record and its entry, the head and the journal are unchanged, no entry posts, the invoice stays open, and the books verify. Also formats the B-27 test `ci-fixes` brought in. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
1 parent
95a54d69ab
commit
3a1f4fd4eb
2 files changed
+58
-1
No files matched your search
@@ -1468,6 +1468,62 @@ fn collection_letter() -> &'static [u8] {
|
|||||||
b"%PDF-1.4\nFinal demand, unpaid\n%%EOF\n"
|
b"%PDF-1.4\nFinal demand, unpaid\n%%EOF\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A notice whose bytes are already another entry's receipt is that one
|
||||||
|
/// record (plan P6 step 4), and the one-receipt-one-entry rule refuses it
|
||||||
|
/// before the write-off is journaled: no entry, no link, no journal line
|
||||||
|
/// (found merging S7 and P6, 2026-10-28: the refusal came after the entry
|
||||||
|
/// had posted).
|
||||||
|
#[test]
|
||||||
|
fn a_write_off_whose_notice_is_anothers_receipt_is_refused_before_posting() {
|
||||||
|
let dir = fixture("write-off-dedup");
|
||||||
|
let mut s = Store::open(&dir.0).unwrap();
|
||||||
|
s.upgrade_format().unwrap();
|
||||||
|
// The letter's bytes, filed first as a receipt on an unrelated entry.
|
||||||
|
let other = s.ledger().posted_entries().next().unwrap().id();
|
||||||
|
let (letter, _, dedup) = s
|
||||||
|
.attach_receipt(
|
||||||
|
Some(other),
|
||||||
|
"letter.pdf",
|
||||||
|
"application/pdf",
|
||||||
|
collection_letter(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(!dedup);
|
||||||
|
let head = s.head();
|
||||||
|
let len = journal_len(&dir.0);
|
||||||
|
let next_entry = s.ledger().next_entry_id();
|
||||||
|
match s.write_off_invoice(
|
||||||
|
1,
|
||||||
|
d(2026, 2, 1),
|
||||||
|
cents(10_000),
|
||||||
|
"uncollectible",
|
||||||
|
Some(WriteOffEvidence::File {
|
||||||
|
file_name: "collection-letter.pdf",
|
||||||
|
mime_type: "application/pdf",
|
||||||
|
bytes: collection_letter(),
|
||||||
|
description: Some("Collection attempts"),
|
||||||
|
}),
|
||||||
|
) {
|
||||||
|
Err(StoreError::AttachmentLinked(a, e)) => {
|
||||||
|
assert_eq!(a, letter);
|
||||||
|
assert_eq!(e, other);
|
||||||
|
}
|
||||||
|
other => panic!("expected AttachmentLinked, got {other:?}"),
|
||||||
|
}
|
||||||
|
assert_eq!(s.head(), head, "nothing journaled");
|
||||||
|
assert_eq!(journal_len(&dir.0), len);
|
||||||
|
assert_eq!(s.ledger().next_entry_id(), next_entry, "no entry posted");
|
||||||
|
let st = s
|
||||||
|
.ledger()
|
||||||
|
.invoice_state(1, d(2026, 12, 31))
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(st.status, ledger_core::DocStatus::Open);
|
||||||
|
assert!(s.attachments_for(Subject::Invoice(1)).unwrap().is_empty());
|
||||||
|
s.verify().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
/// A write-off needs journal feature documents_v2 (its event type is new;
|
/// A write-off needs journal feature documents_v2 (its event type is new;
|
||||||
/// an older version refuses the unknown type): refused, journaling
|
/// an older version refuses the unknown type): refused, journaling
|
||||||
/// nothing, until the owner's upgrade; then it is taken and replays to the
|
/// nothing, until the owner's upgrade; then it is taken and replays to the
|
||||||
|
|||||||
@@ -1574,7 +1574,8 @@ fn an_unknown_convention_kind_is_newer_for_the_writer_and_the_viewer_alike() {
|
|||||||
}
|
}
|
||||||
let store = ledger_store::Store::open(dir);
|
let store = ledger_store::Store::open(dir);
|
||||||
let snap = ledger_store::Snapshot::load(dir);
|
let snap = ledger_store::Snapshot::load(dir);
|
||||||
let newer = |e: &ledger_store::StoreError| matches!(e, ledger_store::StoreError::NewerJournal { .. });
|
let newer =
|
||||||
|
|e: &ledger_store::StoreError| matches!(e, ledger_store::StoreError::NewerJournal { .. });
|
||||||
match (store, snap) {
|
match (store, snap) {
|
||||||
(Err(a), Err(b)) => assert!(
|
(Err(a), Err(b)) => assert!(
|
||||||
newer(&a) && newer(&b),
|
newer(&a) && newer(&b),
|
||||||
|
|||||||
Reference in new issue
Block a user