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:
swaitsandClaude Opus 5.5 committed 2026-10-15 14:43:40 -06:00
1 parent 2a188bd039
commit a299077991
54 files changed
+10961 -178

No files matched your search

+93 -2
View File
@@ -250,6 +250,94 @@ pub fn build(dir: &Path, today: NaiveDate) -> Result<(), StoreError> {
.map(|n| s.add_customer(n))
.collect::<Result<_, _>>()?;
// The party directory: who they are, beyond the vendor and customer
// records (roles, W-9 facts with only the TIN's last four, sites).
{
use ledger_core::{PartyField, Role, W9Facts};
let party_of = |s: &Store, v: u64| s.ledger().party_of_vendor(v).expect("linked");
let sam_p = party_of(&s, sam);
s.set_party_role(sam_p, Role::Contractor, true)?;
s.set_party_w9(
sam_p,
Some(W9Facts {
received: start + Days::new(20),
entity_type: "Individual/sole proprietor".into(),
exempt_payee_code: None,
fatca_code: None,
tin_last4: "4821".into(),
kept_at: "wanderbooks vault".into(),
vault_record: None,
}),
)?;
s.update_party(
sam_p,
PartyField::Notes(Some(
"Illustrator for client projects; invoices after each delivery.".into(),
)),
)?;
let riley_p = party_of(&s, riley);
s.set_party_role(riley_p, Role::Contractor, true)?;
s.update_party(riley_p, PartyField::Website(Some("rileychen.dev".into())))?;
let lofts = party_of(&s, landlord);
s.set_party_role(lofts, Role::Landlord, true)?;
s.set_party_w9(
lofts,
Some(W9Facts {
received: start + Days::new(3),
entity_type: "LLC taxed as a partnership".into(),
exempt_payee_code: None,
fatca_code: None,
tin_last4: "3307".into(),
kept_at: "CPA portal".into(),
vault_record: None,
}),
)?;
s.update_party(
lofts,
PartyField::Address(Some("1200 NW Everett St, Portland, OR 97209".into())),
)?;
s.update_party(lofts, PartyField::DisplayName(Some("Everett Lofts".into())))?;
if let Some(fern) = s.ledger().party_of_customer(customers[0]) {
s.update_party(
fern,
PartyField::Website(Some("fernandfield.coffee".into())),
)?;
s.update_party(
fern,
PartyField::Address(Some("88 SE Division St, Portland, OR 97202".into())),
)?;
}
let irs = s.add_party("Internal Revenue Service", &[Role::TaxAgency])?;
s.update_party(irs, PartyField::DisplayName(Some("IRS".into())))?;
s.update_party(irs, PartyField::Website(Some("irs.gov".into())))?;
let odr = s.add_party("Oregon Department of Revenue", &[Role::TaxAgency])?;
s.update_party(odr, PartyField::Website(Some("oregon.gov/dor".into())))?;
let old = s.add_party("Etsy, Inc.", &[Role::PayoutPlatform])?;
s.update_party(
old,
PartyField::Notes(Some("Sold templates here before moving to Gumroad.".into())),
)?;
s.update_party(old, PartyField::Archived(true))?;
}
let gumroad = s.add_party("Gumroad, Inc.", &[ledger_core::Role::PayoutPlatform])?;
s.update_party(
gumroad,
ledger_core::PartyField::DisplayName(Some("Gumroad".into())),
)?;
s.update_party(
gumroad,
ledger_core::PartyField::Website(Some("gumroad.com".into())),
)?;
let chase = s.add_party(
"JPMorgan Chase Bank, N.A.",
&[ledger_core::Role::FinancialInstitution],
)?;
s.update_party(
chase,
ledger_core::PartyField::DisplayName(Some("Chase".into())),
)?;
let alex = s.add_party("Alex Rivera", &[ledger_core::Role::Member])?;
// Formation documents.
s.file_document(
"formation",
@@ -562,7 +650,7 @@ pub fn build(dir: &Path, today: NaiveDate) -> Result<(), StoreError> {
// Pay the card in full on the 25th.
let date = day(m, 25);
if in_range(date) && card_balance > Decimal::ZERO {
post(
let paid = post(
&mut s,
date,
"Credit card payment",
@@ -582,6 +670,7 @@ pub fn build(dir: &Path, today: NaiveDate) -> Result<(), StoreError> {
None,
None,
)?;
s.set_entry_party(paid, Some(chase))?;
card_balance = Decimal::ZERO;
}
// Client invoices: two or three a month, net 30.
@@ -657,6 +746,7 @@ pub fn build(dir: &Path, today: NaiveDate) -> Result<(), StoreError> {
None,
)?;
s.post_entry(e)?;
s.set_entry_party(e, Some(gumroad))?;
}
}
// Contractors.
@@ -715,7 +805,7 @@ pub fn build(dir: &Path, today: NaiveDate) -> Result<(), StoreError> {
if month_index >= 3 {
let date = day(m, 28);
if in_range(date) {
post(
let draw = post(
&mut s,
date,
"Owner draw",
@@ -726,6 +816,7 @@ pub fn build(dir: &Path, today: NaiveDate) -> Result<(), StoreError> {
None,
None,
)?;
s.set_entry_party(draw, Some(alex))?;
}
}
if month_index % 3 == 2 {