Audit fixes: party money, reconciliations, require policy, K-1 property

An independent recompute of the real books found no numeric
disagreements; review found 15 defects, all fixed with spec-first
regression tests:

- k1: a prepaid credited straight to capital is property; a member
  paying a vendor through 2100 is cash. Docs and B-13 match.
- parties: a reversal of a reversal counts on its root's side;
  liability-to-liability payments (member pays the card) count as paid.
- viewer: party money uses the server's cash accounts and full calendar
  year; per-party "What for" scales multi-party entries.
- reconciliations: refuse a withdrawn statement before journaling; new
  books.void_statement; reconciled lines can't be un-cleared; one shared
  reconcilable-accounts rule for digest and viewer.
- CPA package includes receipts linked to entries; a file can't be both
  filed on and linked to one entry.
- parties = "require" covers payroll, tax remittance (agency) and
  disposal (buyer); no-party marks follow reversal chains and clear when
  a vendor is tagged.
- derived member shares are frozen to exact amounts when posted, so a
  settings change never re-splits history.

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 20:16:03 -06:00
1 parent e1b2509c22
commit 8498e6cfab
22 files changed
+1802 -343

No files matched your search

+46
View File
@@ -603,3 +603,49 @@ fn books_show_opens_a_reconciliation() {
h.state_mut().show(ViewerTarget::Reconciliation { id: 999 });
assert_eq!(h.state_mut().route(), &Route::Reconciliation(r));
}
/// ledger_store::reconcilable INVARIANT 1, the digest's rule: a liability
/// not named for a card (an Amex) joins the accounts to reconcile once it
/// has a recorded statement, so Needs attention asks for it as the digest
/// does (it used to be listed by the digest only).
#[test]
fn a_liability_with_a_statement_needs_reconciling_whatever_its_name() {
let mut f = fixture("amex");
let amex = f
.store
.add_account("2060", "Amex", AccountType::Liability, None, false)
.unwrap();
let software = f.store.ledger().chart().find_by_code("6300").unwrap().id;
let e = f.store.create_entry(d(2026, 9, 3), "Amex charge").unwrap();
f.store
.add_line(e, software, Side::Debit, usd(1_000))
.unwrap();
f.store.add_line(e, amex, Side::Credit, usd(1_000)).unwrap();
f.store.post_entry(e).unwrap();
let b = load(&f.dir.0);
assert!(!b.reconcilable_accounts().contains(&amex));
let (att, _, _) = f
.store
.file_document(
"statement",
"amex-aug.pdf",
"application/pdf",
b"amex aug",
None,
)
.unwrap();
f.store
.record_statement(Statement {
attachment_id: att,
account_id: amex,
period_start: d(2026, 8, 1),
period_end: d(2026, 8, 31),
opening_balance: Decimal::ZERO,
closing_balance: Decimal::ZERO,
})
.unwrap();
let b = load(&f.dir.0);
assert!(b.reconcilable_accounts().contains(&amex));
assert_eq!(b.reconciliation_due(amex), Some(d(2026, 9, 30)));
assert!(reconcile_items(&b).contains(&"Reconcile Amex through Sep 30, 2026".to_string()));
}