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

+69 -12
View File
@@ -3,11 +3,17 @@
//! A vendor is marked with the information return it gets (1099-NEC for
//! nonemployee compensation, 1099-MISC for rents and the like) and whether
//! its W-9 is on file. Entries are tagged with the vendor they paid
//! (`pay_bill` tags automatically). The amount paid is the cash that left
//! asset accounts on the vendor's tagged entries: credits minus debits to
//! asset accounts. A card payment (Dr expense, Cr credit card) moves no
//! asset, so it is excluded, as it should be: card payments are reported by
//! the card processor on 1099-K, not by the payer.
//! (`pay_bill` tags automatically). The amount paid is what left the
//! company's payment sources on the vendor's tagged entries: credits minus
//! debits to them ([`PaymentAccounts`]). The sources are the cash accounts
//! (bank, cash on hand) and the financing liabilities (what owners or
//! lenders paid on the company's behalf, e.g. Due to Shareholders): the
//! company is the payer either way. A card payment (Dr expense, Cr credit
//! card) leaves neither, so it is excluded, as it should be: card payments
//! are reported by the card processor on 1099-K, not by the payer. A bill
//! (Cr payables) is not a payment until it is paid, and a prepayment
//! (Dr prepaid, Cr cash) counts in full when made: §6041 reports payments
//! made in the year.
//!
//! Thresholds are set by law and change; [`default_threshold`] encodes
//! them and the report takes an override. Confirm with the CPA.
@@ -117,6 +123,57 @@ pub fn vendor_1099_report(
ledger: &Ledger,
year: i32,
threshold: Option<Decimal>,
) -> Result<Vendor1099Report, LedgerError> {
vendor_1099_report_with(ledger, year, threshold, &PaymentAccounts::default())
}
/// The accounts payments to vendors come out of, for
/// [`vendor_1099_report_with`]: the books' `[books] cash_accounts` and
/// `[books] financing_accounts`.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PaymentAccounts {
/// Cash accounts: bank, savings, cash on hand.
pub cash: Vec<crate::AccountId>,
/// Financing liabilities: owner advances, loans (paid on the company's
/// behalf). Never a payment card.
pub financing: Vec<crate::AccountId>,
}
impl PaymentAccounts {
/// Whether `account` is a payment source. With no cash accounts given,
/// every asset account is one (books that haven't named their cash).
fn includes(&self, ledger: &Ledger, account: crate::AccountId) -> bool {
if self.cash.is_empty() {
ledger
.chart()
.get(account)
.is_some_and(|a| a.account_type == AccountType::Asset)
|| self.financing.contains(&account)
} else {
self.cash.contains(&account) || self.financing.contains(&account)
}
}
}
/// [`vendor_1099_report`], counting payments out of `sources`.
///
/// ```text
/// SOURCE: docs/spec/rules.md B-19 (26 U.S.C. §6041(a): payments made in
/// the year; card payments go on 1099-K).
/// INVARIANTS:
/// 1. A vendor's amount is credits minus debits to `sources` on its
/// tagged posted entries dated in `year`.
/// SCENARIOS:
/// 1. Paid from cash, or by a member through a financing liability:
/// counts. Charged to a card, or billed and unpaid: doesn't.
/// 2. A prepayment (Dr prepaid, Cr cash) counts in full; a refund to
/// cash and a reversal subtract.
/// ```
pub fn vendor_1099_report_with(
ledger: &Ledger,
year: i32,
threshold: Option<Decimal>,
sources: &PaymentAccounts,
) -> Result<Vendor1099Report, LedgerError> {
let threshold = match threshold {
Some(t) => t,
@@ -133,12 +190,7 @@ pub fn vendor_1099_report(
let cash: Decimal = entry
.lines()
.iter()
.filter(|l| {
ledger
.chart()
.get(l.account)
.is_some_and(|a| a.account_type == AccountType::Asset)
})
.filter(|l| sources.includes(ledger, l.account))
.map(|l| match l.side {
crate::Side::Credit => l.amount.amount,
crate::Side::Debit => -l.amount.amount,
@@ -156,7 +208,12 @@ pub fn vendor_1099_report(
vendor_id: v.id,
name: v.name.clone(),
form: v.form_1099,
tin_on_file: v.tin_on_file,
// On file by the vendor's flag or its party's W-9 facts.
tin_on_file: v.tin_on_file
|| ledger
.party_of_vendor(v.id)
.and_then(|p| ledger.get_party(p))
.is_some_and(|p| p.w9.is_some()),
paid: amount,
reportable: v.form_1099.is_some() && amount >= threshold,
}