Parties, attachment links, statements, reconciliations (store), TIN vault crate
Parties: one directory for vendors, contractors, customers, employees, members, platforms, agencies, banks, landlords (ledger-core parties.rs). W-9/W-4 facts without the number (last four only; any nine-digit run in a party field is refused, Treas. Reg. 301.6109-4). Vendor and customer records link to parties on replay; core tags every bill, invoice, payment, and reversal with its counterparty. Journal events party.*, entry.party_set; payroll lines name the employee party. MCP: books.add_party, update_party, set_party_roles, set_w9, set_w4, parties, party, set_entry_party; draft_entry party; run_payroll employee; show party. Viewer: Parties directory and party pages, search, journal text, W-9 attention, demo data. One definition of a party's money (ledger_core::parties::activity) for MCP and viewer. 1099 amounts count payments from cash and financing accounts (a member paying for the company counts; a card doesn't; a prepayment counts when made): vendor_1099_report_with, B-19. Store: attachment links to any subject, statements, reconciliations (schema v11), with replay and projection. ledger-vault: encrypted TIN and W-9 records (ChaCha20-Poly1305, key file outside the books). Spec-blind tests for parties (core, store, MCP, viewer) and attachments. 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
2a188bd039
commit
a299077991
54 files changed
+10961
-178
No files matched your search
@@ -21,25 +21,30 @@ descriptions get records.
|
||||
|
||||
```text
|
||||
Party {
|
||||
id: PartyId, // vendors and customers keep their ids (see Migration)
|
||||
id: PartyId, // its own id space (vendor and customer records keep theirs)
|
||||
name: String, // legal or trade name, as on documents
|
||||
display_name: Option<String>,
|
||||
roles: BTreeSet<Role>, // vendor, contractor, customer, employee, member,
|
||||
// payout_platform, tax_agency, financial_institution, landlord
|
||||
roles: BTreeSet<Role>, // set by hand: contractor, employee, member, payout_platform,
|
||||
// tax_agency, financial_institution, landlord (vendor and
|
||||
// customer follow the records; see all_roles)
|
||||
website: Option<String>,
|
||||
address: Option<String>, // business address; no personal PII beyond it
|
||||
w9: Option<W9Facts>, // contractor/landlord: the TIN itself is in the vault
|
||||
w4: Option<W4Facts>, // employee: never the SSN
|
||||
form_1099: Option<Form1099>, // carried over from Vendor
|
||||
member: Option<String>, // a member's name in wanderbooks.toml, when role member
|
||||
notes: Option<String>,
|
||||
archived: bool, // no longer dealt with; history stays
|
||||
w9: Option<W9Facts>, // contractor/landlord: the TIN itself is in the vault
|
||||
w4: Option<W4Facts>, // employee: never the SSN
|
||||
vendor: Option<VendorId>, // its vendor record (bills, payments, 1099 form)
|
||||
customer: Option<CustomerId>,// its customer record (invoices)
|
||||
}
|
||||
W9Facts { received: NaiveDate, entity_type: String, exempt_payee_code: Option<String>,
|
||||
fatca_code: Option<String>, tin_last4: String, kept_at: String }
|
||||
fatca_code: Option<String>, tin_last4: String, kept_at: String,
|
||||
vault_record: Option<String> /* SHA-256 of the vault record */ }
|
||||
W4Facts { received: NaiveDate, state: String, kept_at: String }
|
||||
```
|
||||
|
||||
The 1099 form stays on the vendor record (`vendor.1099_set`); a party
|
||||
reaches it through `vendor`.
|
||||
|
||||
Rules (to vendor as B-26 with sources):
|
||||
|
||||
- A full TIN or SSN is never in the journal: `tin_last4` is exactly four
|
||||
@@ -65,29 +70,32 @@ Rules (to vendor as B-26 with sources):
|
||||
|
||||
### Journal events
|
||||
|
||||
`party.added {id, name, roles}`, `party.updated {id, field, value}` (one
|
||||
field per event: name, display_name, website, address, notes, archived),
|
||||
`party.role_added {id, role}`, `party.role_removed {id, role}`,
|
||||
`party.added {id, name, roles}`, `party.updated {id, change}` (one
|
||||
`PartyField` per event, `{field, value}`: name, display_name, website,
|
||||
address, notes, archived), `party.role_set {id, role, on}`,
|
||||
`party.w9_set {id, facts|null}`, `party.w4_set {id, facts|null}`,
|
||||
`entry.party_set`. Replayed and projected like the rest; every event gets
|
||||
viewer journal text (the GUI test enforces it).
|
||||
`entry.party_set {entry_id, party_id|null}`. Replayed and projected like
|
||||
the rest; every event gets viewer journal text (the GUI test enforces it).
|
||||
|
||||
### Migration
|
||||
|
||||
Old journals replay unchanged. `vendor.added` and `customer.added` keep
|
||||
their meaning and create parties with the vendor or customer role; the
|
||||
party id space starts above the highest vendor and customer ids, and a
|
||||
vendor and a customer with the same name stay two parties until the
|
||||
human merges them (`books.merge_parties`, journaled, re-pointing links).
|
||||
`entry.vendor_set` replays as `entry.party_set`. `vendor.1099_set` sets
|
||||
the party's `form_1099` and the contractor role.
|
||||
their meaning and, on replay as live, link their record to a party: the
|
||||
live party of that name (ignoring case and surrounding space) without
|
||||
such a record yet, else a new party. So a vendor and a customer of the
|
||||
same name share one party; a second vendor of a taken name gets a new
|
||||
party named "Name (2)" (names stay unique). Vendor and customer names
|
||||
obey the party text rules (no full TIN). `entry.vendor_set` also sets the
|
||||
entry's party, and `entry.party_set` to a party with a vendor record sets
|
||||
the vendor tag: one counterparty.
|
||||
|
||||
### Tools
|
||||
|
||||
`books.add_party`, `books.update_party`, `books.party_roles`,
|
||||
`books.set_w9`, `books.set_w4`, `books.parties` (list, filter by role),
|
||||
`books.party` (one: facts, roles, totals this year, documents, links),
|
||||
`books.merge_parties`, `books.set_entry_party`. `books.add_vendor` and
|
||||
`books.set_entry_party`. (Merging two parties, `books.merge_parties`, is
|
||||
not built yet: records of one name already share a party.) `books.add_vendor` and
|
||||
`books.add_customer` stay as shortcuts.
|
||||
|
||||
### Viewer
|
||||
@@ -111,10 +119,13 @@ worksheet it hands the CPA is complete, but never in the journal.
|
||||
W-9 file itself, each encrypted at rest (XChaCha20-Poly1305, one random
|
||||
nonce per record; the file format carries a version and the party id as
|
||||
associated data, so a record can't be moved to another party).
|
||||
- **Key:** a random 256-bit key in the OS keyring (Secret Service on Linux,
|
||||
Keychain on macOS), created on first use; never on disk in the books dir.
|
||||
A recovery copy is the human's job: `just vault-export` writes the key
|
||||
and records as one passphrase-encrypted file for a password manager.
|
||||
- **Key:** a random 256-bit key in a key file outside the books dir
|
||||
(default `~/.config/wanderbooks/vault.key`, mode 0600; `[vault] key_file`
|
||||
in wanderbooks.toml overrides it), created on first use. No OS keyring
|
||||
or other system service (the owner, 2026-10-15: no external dependencies),
|
||||
so the vault works anywhere wanderbooks runs. Recovery is a copy of that
|
||||
file in the human's password manager; without it the vault can't be
|
||||
read (the books still open and verify).
|
||||
- **Journal:** `party.w9_set` records that a W-9 was received, its date,
|
||||
entity type, codes, and last four, plus a hash of the vault record, so
|
||||
the books prove a W-9 existed without holding it.
|
||||
|
||||
Reference in new issue
Block a user