P3 review: the same figures agree across the pieces

A distribution declared to a payable and then paid is financing when paid,
the declaration disclosed (ASC 230-10-45-15(a)); a disposal's depreciation
lines count as depreciation in the cash flow and the tax-basis K-1; an
as-of asset list says the status of that day. Seam tests tie the members'
statement, K-1, party totals, cash flow, 1099s, depreciation and the CPA
package together on three dates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-21 16:37:47 -06:00
1 parent 2f999c6050
commit 2ae93fb2d6
13 files changed
+2258 -24

No files matched your search

+105
View File
@@ -3124,3 +3124,108 @@ proptest! {
prop_assert!(cf.noncash_disclosures.is_empty());
}
}
/// Rule 2 (P3 seam review): a distribution declared to a liability that
/// holds only distributions (Dr capital / Cr 2400 Distributions Payable)
/// and paid later is a distribution paid, financing (ASC
/// 230-10-45-15(a)), under the payable's kind; the declaration is an
/// exchange inside financing, disclosed (50-3, 50-4). Before the fix the
/// payment was "Cash paid to suppliers and employees" and the declaration
/// an "Other noncash items" line beside an increase in the payable.
#[test]
fn a_distribution_declared_then_paid_is_a_distribution_paid() {
let mut b = funded();
b.acct("2400", "Distributions payable", AccountType::Liability);
let declared = b.post(
d(2026, 1, 31),
"Distribution declared",
&[
("3000", DR, "600.00"),
("3001", DR, "400.00"),
("2400", CR, "1000.00"),
],
);
let paid = b.post(
d(2026, 2, 10),
"Distribution paid",
&[("2400", DR, "1000.00"), ("1000", CR, "1000.00")],
);
let cf = feb(&b, &[], &[]);
assert!(cf.operating.is_empty(), "{:?}", cf.operating);
assert_eq!(
kinds(&cf, CashFlowSection::Financing),
[(
s("Distributions to members — 2400 Distributions payable"),
x("-1000.00")
)]
);
assert_eq!(em(&cf.financing), [(paid, x("-1000.00"))]);
assert!(recon(&cf).is_empty(), "{:?}", recon(&cf));
// The declaration's month: no cash, the exchange disclosed, and no
// operating item.
let jan = b.cf(d(2026, 1, 1), d(2026, 1, 31), &["1000", "1010"], &[], &[]);
foots(&jan);
assert_eq!(disclosed(&jan), [(declared, x("1000.00"))]);
assert!(
recon(&jan).is_empty(),
"{:?}",
jan.operating_reconciliation.lines
);
// A liability credited for anything else as well is no distributions
// payable: an expense accrued to it keeps it operating.
b.post(
d(2026, 2, 20),
"Accrued supplies",
&[("6000", DR, "50.00"), ("2400", CR, "50.00")],
);
let cf = feb(&b, &[], &[]);
assert_eq!(
cf.operating.iter().map(|l| l.entry).collect::<Vec<_>>(),
[paid]
);
}
/// Rule 8 (P3 seam review): a disposal's lines on the asset's
/// depreciation expense account (the days a month's run posted past the
/// disposal, credited back) are depreciation on the reconciliation, not
/// gain or loss, so its depreciation line is the P&L's depreciation
/// expense.
#[test]
fn a_disposals_depreciation_credit_back_is_depreciation() {
let mut b = funded();
b.post(
d(2026, 1, 10),
"Buy equipment",
&[("1500", DR, "1000.00"), ("1000", CR, "1000.00")],
);
b.post(
d(2026, 2, 28),
"Depreciation",
&[("6100", DR, "30.00"), ("1510", CR, "30.00")],
);
// Sold Feb 14: 15 of the month's 30.00 taken back, a 10.00 loss on
// the 985.00 book value.
b.post(
d(2026, 2, 14),
"Sell equipment",
&[
("1000", DR, "975.00"),
("1510", DR, "30.00"),
("6900", DR, "10.00"),
("1500", CR, "1000.00"),
("6100", CR, "15.00"),
],
);
let cf = feb(&b, &[], &[]);
assert_eq!(
recon(&cf),
[
(s("Depreciation"), x("15.00")),
(
s("(Gain) loss and other noncash changes in investing assets"),
x("10.00")
),
]
);
assert_eq!(cf.cash_from_investing, x("975.00"));
}
@@ -9,3 +9,4 @@ cc 9f5e63191433240d6df895c196fdd89b56ce1a2a3d8d5225740c2f10dc720756 # shrinks to
cc 5397d840688ed53dc033aceaf98b8db5df3a7a2616c4afe7f3f5fc2e0c8775f2 # shrinks to gens = [Gen { day: 140, mode: 2, picks: [0, 0, 2], x: 1, y: 1, classes: [0, 0, 0] }], (a, _m, e) = (0, 0, 140), designate = false
cc abd84709578e7889c275a0019ae0e71366d77526ae93072777be614d7ffb0dd5 # shrinks to gens = [Gen { day: 108, mode: 1, picks: [1, 5, 3], x: 1, y: 1, classes: [0, 0, 0] }], (a, _m, e) = (0, 0, 108), k = 2
cc 7481a0fb806d08439357ce4be66224389a7f76a7d91bf93ac320bb2b3c4d4891 # shrinks to gens = [Gen { day: 3, mode: 1, picks: [4, 5, 1], x: 1, y: 1, classes: [0, 0, 0] }], drafts = [Gen { day: 0, mode: 0, picks: [0, 0, 0], x: 1, y: 1, classes: [0, 0, 0] }], (a, _m, e) = (0, 0, 3)
cc b594a44c6ad193ee922103d535d0a8e7b41ff808b37166c296491f27cd73d5a4 # shrinks to gens = [Gen { day: 0, mode: 2, picks: [3, 0, 2], x: 1, y: 1, classes: [0, 0, 0] }, Gen { day: 135, mode: 2, picks: [0, 3, 2], x: 1, y: 1, classes: [0, 0, 0] }], (a, _m, e) = (0, 0, 135), designate = false
+29 -3
View File
@@ -2517,12 +2517,33 @@ proptest! {
let credits = |qs: &[i64]| qs.iter().map(|q| (-q).max(0)).sum::<i64>();
let debits = |qs: &[i64]| qs.iter().map(|q| (*q).max(0)).sum::<i64>();
let mut fin = 0i64;
// Rule 2: a liability left operating that only entries debiting
// nothing but equity credit, through the end, holds distributions
// declared: financing, after the loan.
let declared = |code: &str| {
let mut by_declaration = false;
for g in gens.iter().filter(|g| g.date() <= day(e)) {
let legs = g.legs(1);
if !legs.iter().any(|&(i, s, _)| s == CR && RCHART[i].0 == code) {
continue;
}
if legs.iter().filter(|l| l.1 == DR).all(|&(i, _, _)| RCHART[i].2 == AccountType::Equity) {
by_declaration = true;
} else {
return false;
}
}
by_declaration
};
let (payables_declared, loan_declared) = (declared("2000"), !designate && declared("2500"));
for g in gens.iter().filter(|g| g.date() >= day(a) && g.date() <= day(e)) {
// Kinds: cash; operating balance sheet, revenue and expense;
// capital A, capital B, the loan (financing when designated).
// capital A, capital B, the loan (financing when designated),
// then the liabilities holding distributions declared.
let mut cash = 0i64;
let mut op = [0i64; 2];
let mut fi = [0i64; 3];
let mut declared_fi = [0i64; 2];
for (i, side, cents) in g.legs(1) {
let m = if side == DR { cents } else { -cents };
match RCHART[i].0 {
@@ -2530,10 +2551,13 @@ proptest! {
"3000" => fi[0] += m,
"3001" => fi[1] += m,
"2500" if designate => fi[2] += m,
"2000" if payables_declared => declared_fi[0] += m,
"2500" if loan_declared => declared_fi[1] += m,
"4000" | "4100" | "6000" | "6100" => op[1] += m,
_ => op[0] += m,
}
}
let fi_all = fi.iter().sum::<i64>() + declared_fi.iter().sum::<i64>();
// Rule 3: operating, then financing (no investing here).
let take = |left: i64, m: i64| {
if left > 0 && m < 0 {
@@ -2545,10 +2569,12 @@ proptest! {
}
};
let to_op = take(cash, op[0] + op[1]);
let to_fin = take(cash - to_op, fi.iter().sum());
let to_fin = take(cash - to_op, fi_all);
fin += to_fin;
if to_op > 0 { settle(&mut op, to_op, false) } else { settle(&mut op, -to_op, true) }
if to_fin > 0 { settle(&mut fi, to_fin, false) } else { settle(&mut fi, -to_fin, true) }
let mut kinds: Vec<i64> = fi.iter().chain(&declared_fi).copied().collect();
if to_fin > 0 { settle(&mut kinds, to_fin, false) } else { settle(&mut kinds, -to_fin, true) }
fi.copy_from_slice(&kinds[..3]);
// Rule 11: the loan pays, then receives; then capital.
if designate {
let paid = credits(&fi[2..]).min(debits(&op));