Attachments, statements, reconciliations, TIN vault, counterparty policy

MCP: books.link_attachment, unlink_attachment, attachments; link_to on
attach_receipt and file_document; record_statement, statements;
books.reconcile previews and, confirmed against a recorded statement,
records the reconciliation; void_reconciliation, reconciliations;
import_bank_csv links its source file; digest lists unreconciled
accounts.

TIN vault: books.set_w9 takes the TIN and the form into the encrypted
vault (last four in the books); books.vendor_1099 include_tins and
save_csv_to for the CPA's copy; books.get_w9; books.purge_tin. A leak
test scans every tool output, the journal, the database, and the blobs.

[books] parties = off | suggest | require (default suggest): every
preview names the entry's party or suggests likely ones; require refuses
to post until it names one or is marked as having none (entry.no_party).

Viewer: documents on every subject page; statements and reconciliation
history per account; reconciliation pages; reconcile and
no-counterparty attention items; party money by period (paid out,
received in, net, what for, by month); Needs attention gets its own
full-width section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-15 15:42:37 -06:00
1 parent a299077991
commit 08ce558ed3
41 files changed
+8463 -465

No files matched your search

+41
View File
@@ -180,3 +180,44 @@ fn purge_removes_a_party() {
assert!(v.tin(2).unwrap().is_some());
assert!(!v.purge(1).unwrap(), "nothing left to purge");
}
/// INVARIANT 6, without the key: `records_held` says what a party has on
/// disk and `purge_records` deletes it, so a vault whose key is lost can
/// still be purged.
#[test]
fn purge_and_holdings_need_no_key() {
let d = dir("keyless");
let v = vault(&d);
let tin = Tin::new("123456789", TinType::Ssn).unwrap();
v.put_tin(1, &tin).unwrap();
v.put_w9(
1,
&W9File {
file_name: "w9.pdf".into(),
mime_type: "application/pdf".into(),
bytes: b"%PDF".to_vec(),
},
)
.unwrap();
v.put_tin(2, &tin).unwrap();
std::fs::remove_file(d.join("keys/vault.key")).unwrap();
let vdir = d.join("vault");
assert_eq!(ledger_vault::records_held(&vdir, 1).unwrap(), (true, true));
assert_eq!(ledger_vault::records_held(&vdir, 2).unwrap(), (true, false));
assert_eq!(
ledger_vault::records_held(&vdir, 3).unwrap(),
(false, false)
);
assert!(ledger_vault::purge_records(&vdir, 1).unwrap());
assert_eq!(
ledger_vault::records_held(&vdir, 1).unwrap(),
(false, false)
);
assert_eq!(ledger_vault::records_held(&vdir, 2).unwrap(), (true, false));
assert!(!ledger_vault::purge_records(&vdir, 1).unwrap());
// A vault directory that doesn't exist holds nothing.
assert_eq!(
ledger_vault::records_held(&d.join("nowhere"), 1).unwrap(),
(false, false)
);
}