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:
1 parent
a299077991
commit
08ce558ed3
41 files changed
+8463
-465
No files matched your search
@@ -260,7 +260,7 @@ impl Vault {
|
||||
}
|
||||
|
||||
fn path(&self, party: u64, kind: Kind) -> PathBuf {
|
||||
self.dir.join(format!("{}-{party}.bin", kind.name()))
|
||||
record_path(&self.dir, party, kind)
|
||||
}
|
||||
|
||||
fn aad(party: u64, kind: Kind) -> Vec<u8> {
|
||||
@@ -361,18 +361,41 @@ impl Vault {
|
||||
/// Delete everything the vault holds for a party (INVARIANT 6). Returns
|
||||
/// whether anything was there.
|
||||
pub fn purge(&self, party: u64) -> Result<bool, Error> {
|
||||
let mut any = false;
|
||||
for kind in [Kind::Tin, Kind::W9] {
|
||||
match std::fs::remove_file(self.path(party, kind)) {
|
||||
Ok(()) => any = true,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(e) => return Err(Error::Io(e)),
|
||||
}
|
||||
}
|
||||
Ok(any)
|
||||
purge_records(&self.dir, party)
|
||||
}
|
||||
}
|
||||
|
||||
fn record_path(dir: &Path, party: u64, kind: Kind) -> PathBuf {
|
||||
dir.join(format!("{}-{party}.bin", kind.name()))
|
||||
}
|
||||
|
||||
/// What the vault in `dir` holds for a party, `(TIN, W-9 file)`, without
|
||||
/// the key: whether each record's file exists (nothing is decrypted).
|
||||
pub fn records_held(dir: &Path, party: u64) -> Result<(bool, bool), Error> {
|
||||
let exists = |kind| match std::fs::metadata(record_path(dir, party, kind)) {
|
||||
Ok(_) => Ok(true),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
|
||||
Err(e) => Err(Error::Io(e)),
|
||||
};
|
||||
Ok((exists(Kind::Tin)?, exists(Kind::W9)?))
|
||||
}
|
||||
|
||||
/// Delete everything the vault in `dir` holds for a party (INVARIANT 6),
|
||||
/// without the key: purging must work even where the vault can't be read
|
||||
/// (the key lost, or books restored on another machine). Returns whether
|
||||
/// anything was there.
|
||||
pub fn purge_records(dir: &Path, party: u64) -> Result<bool, Error> {
|
||||
let mut any = false;
|
||||
for kind in [Kind::Tin, Kind::W9] {
|
||||
match std::fs::remove_file(record_path(dir, party, kind)) {
|
||||
Ok(()) => any = true,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(e) => return Err(Error::Io(e)),
|
||||
}
|
||||
}
|
||||
Ok(any)
|
||||
}
|
||||
|
||||
/// Write a file readable by its owner only (0600 on Unix), creating its
|
||||
/// directory.
|
||||
fn write_private(path: &Path, bytes: &[u8]) -> Result<(), Error> {
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user