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

+60
View File
@@ -112,6 +112,9 @@ pub struct BooksSettings {
/// ("FL" for "FL-0001"). Unset: [`BooksSettings::asset_tag_prefix`]
/// derives it from the company name.
pub asset_tag_prefix: Option<String>,
/// `[books] parties`: how hard the agent asks for each entry's
/// counterparty. Unset: [`PartyPolicy::Suggest`].
pub parties: PartyPolicy,
/// `[vault] key_file`: the TIN vault's key file. Unset: the vault's
/// default (`~/.config/wanderbooks/vault.key`).
pub vault_key_file: Option<std::path::PathBuf>,
@@ -212,6 +215,7 @@ struct BooksSection {
#[serde(default)]
investing_accounts: Vec<String>,
asset_tag_prefix: Option<String>,
parties: Option<String>,
}
/// The return a federal tax classification files (docs/spec/sources/
@@ -532,9 +536,46 @@ pub fn parse(text: &str) -> Result<BooksSettings, String> {
}
None => None,
},
parties: match raw.books.parties.as_deref() {
None | Some("suggest") => PartyPolicy::Suggest,
Some("off") => PartyPolicy::Off,
Some("require") | Some("required") => PartyPolicy::Require,
Some(other) => {
return Err(format!(
"[books] parties {other:?}: \"off\", \"suggest\", or \"require\""
));
}
},
})
}
/// `[books] parties`: whether entries name their counterparty
/// (docs/parties-and-attachments.md "Counterparties"; DECISION, owner,
/// 2026-10-15: the default suggests, books may require or turn it off).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum PartyPolicy {
/// Never mention counterparties.
Off,
/// A draft or preview without a party names the likely one; nothing
/// is refused.
#[default]
Suggest,
/// Posting is refused until the entry names a party or is marked as
/// having none.
Require,
}
impl PartyPolicy {
/// Its wire name.
pub fn key(self) -> &'static str {
match self {
PartyPolicy::Off => "off",
PartyPolicy::Suggest => "suggest",
PartyPolicy::Require => "require",
}
}
}
/// Load `wanderbooks.toml` from a path.
pub fn load(path: &Path) -> Result<BooksSettings, String> {
let text = std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?;
@@ -545,6 +586,25 @@ pub fn load(path: &Path) -> Result<BooksSettings, String> {
mod tests {
use super::*;
/// `[books] parties`: suggest by default; off, suggest, require (and
/// "required") parse; anything else is refused, naming the choices.
#[test]
fn parties_policy() {
let base = "[company]\nname = \"Acme LLC\"\n[books]\n";
assert_eq!(parse(base).unwrap().parties, PartyPolicy::Suggest);
for (v, want) in [
("off", PartyPolicy::Off),
("suggest", PartyPolicy::Suggest),
("require", PartyPolicy::Require),
("required", PartyPolicy::Require),
] {
let s = parse(&format!("{base}parties = \"{v}\"\n")).unwrap();
assert_eq!(s.parties, want, "{v}");
}
let e = parse(&format!("{base}parties = \"always\"\n")).unwrap_err();
assert!(e.contains("require"), "{e}");
}
fn d(y: i32, m: u32, day: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(y, m, day).unwrap()
}