P3: party totals by what moved the money, once (defects 14, 11, 24)

Each party's part of an entry is signed as its own account would show it,
with a direction and its cash and in-kind amounts; a reversal is its
original negated. Totals on a cash or all basis: for a member, all equals
the members' equity statement and cash the K-1's cash. Money is cash,
cards and the financing accounts, and a liability credit never pays its
own creditor (creditor_of), so accruals and declared distributions count
once. tin_last4 from the TIN or W-9 facts.

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 12:12:26 -06:00
1 parent 9ab003d0d1
commit c2ceabbae9
52 files changed
+7535 -560

No files matched your search

+222 -1
View File
@@ -36,7 +36,10 @@
//! to zero, and a reversal's parts are the original's negated (B-14);
//! * what a member or lender paid or received for the company, with no
//! cash account moving, is split among the kinds it settled, netting to
//! zero, and mirrored by a reversal (B-14 rule 11).
//! zero, and mirrored by a reversal (B-14 rule 11);
//! * a party's part of an entry is signed as a line on the party's own
//! account would be, a reversal's is its original's negated, and what of
//! it counts as money or as property never exceeds it (B-35).
use crate::{AccountType, Side};
use rust_decimal::Decimal;
@@ -642,6 +645,224 @@ pub fn exchanged<T: Amount>(debits: T, credits: T, inflow: T, outflow: T) -> Opt
Some(d.min(c).max(T::ZERO))
}
/// Which way an entry moved value with its parties (rules.md B-35).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Direction {
/// The company paid the party, or gave it value: a line on the
/// party's own account would be a debit.
Paid,
/// The company received from the party: a line on its own account
/// would be a credit.
Received,
/// Neither: nothing moved with the party yet (a bill, an invoice, a
/// fee charged on its own card, a distribution still owed), or the
/// entry moved nothing with anyone (a close, a reclassification).
/// Counted in no total.
Neither,
}
impl Direction {
/// The other way: paid and received swap; neither stays neither.
pub fn flip(self) -> Self {
match self {
Direction::Paid => Direction::Received,
Direction::Received => Direction::Paid,
Direction::Neither => Direction::Neither,
}
}
/// The direction of a signed amount: paid above zero, received below,
/// neither at zero.
pub fn of<T: Amount>(amount: T) -> Self {
if amount > T::ZERO {
Direction::Paid
} else if amount < T::ZERO {
Direction::Received
} else {
Direction::Neither
}
}
/// Its wire name: `paid`, `received`, or `neither`.
pub fn key(self) -> &'static str {
match self {
Direction::Paid => "paid",
Direction::Received => "received",
Direction::Neither => "neither",
}
}
}
/// What an entry's parties split, and what of it counts in their totals
/// (rules.md B-35).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PartyTarget<T> {
/// The whole, signed as a line on the parties' own account would be
/// (debit-positive): positive, the company paid or gave; negative, it
/// received.
pub amount: T,
/// Which way value moved.
pub direction: Direction,
/// The part of `amount` that moved as money (of a member's
/// contribution, what the K-1 worksheet counts as cash; of a
/// distribution, the money it paid): counted on both bases.
pub cash: T,
/// The part of `amount` that moved as property: counted on basis
/// "all" only.
pub in_kind: T,
}
/// The magnitude of `a`, or `None` past the type's range.
fn magnitude<T: Amount>(a: T) -> Option<T> {
if a < T::ZERO {
a.checked_neg()
} else {
Some(a)
}
}
/// The magnitude `m` with the sign of `like`, never a negative zero (a
/// zero `m` is the plain zero, whatever sign arithmetic left on it).
fn with_sign<T: Amount>(like: T, m: T) -> Option<T> {
if m == T::ZERO {
Some(T::ZERO)
} else if like < T::ZERO {
m.checked_neg()
} else {
Some(m)
}
}
/// The smaller of `a` and `b`.
fn least<T: Amount>(a: T, b: T) -> T {
if a < b { a } else { b }
}
/// What an entry's parties split and what of it counts (rules.md B-35
/// item 3, every case but a close and a reversal, which the caller
/// handles: a close is its parties' own-account movement with direction
/// neither, a reversal is [`reverse_target`] of its original's).
///
/// ```text
/// GOAL: one signed whole for an entry's parties, debit-positive as a line
/// on their own account, with its direction and what of it moved as
/// money or as property, so a property contribution and its
/// reversal never read alike, and a promise is never counted as if
/// it were paid.
/// SOURCE: rules.md B-35 (B-2 signs; DECISION, owner, 2026-10-20: a
/// member's entry counts what moved their capital, money is cash,
/// cards, the members' financing and loans, and a liability credit
/// never counts as paying that liability's own creditor; Helvering
/// v. Price: a promise to pay is not payment).
/// INPUTS (each signed debit-positive over the entry's lines):
/// capital: Some((e, property)) when every party named is a member: e
/// the movement of equity other than retained earnings; property what
/// property accounts took in (their debits, when e < 0) or gave up
/// (their credits, when e > 0).
/// net: the money paid (+) or received (−) (parties::entry_flow, a
/// named party's own liabilities not counted as money).
/// own: the movement of the parties' own accounts (payables,
/// receivables, liabilities owed to a party named).
/// debits: the entry's total debits.
/// INVARIANTS:
/// 1. capital with e < 0 (a contribution): amount = e; |in_kind| =
/// min(|e|, property⁺); cash the rest (what the K-1 worksheet counts
/// as cash: money, or what the member paid for the company).
/// 2. capital with e > 0 (a distribution): amount = e; cash = min(e,
/// net⁺), the money it paid; in_kind = min(e − cash, property⁺), the
/// property it gave; the rest (credited to a liability that isn't
/// money for the entry: a distribution declared, still owed) counts
/// as nothing until it is paid.
/// 3. In 1 and 2, direction is the sign of e, or neither when nothing
/// counts.
/// 4. Else net ≠ 0: amount = cash = net, in_kind = 0, direction its
/// sign.
/// 5. Else amount = own when it isn't zero, else debits; neither.
/// 6. cash and in_kind each have amount's sign or are zero (never a
/// negative zero), |cash| + |in_kind| ≤ |amount|, and direction is
/// neither exactly when both are zero.
/// 7. Odd, but for the debits fallback and a member's entry whose cash
/// isn't exactly its money (an expense booked straight to capital, a
/// debt converted, a distribution still owed): negating e, net and
/// own (property comes from the other side of the lines, so it keeps
/// its size) gives reverse_target of the result.
/// ```
///
/// `None` past the type's range.
pub fn party_target<T: Amount>(
capital: Option<(T, T)>,
net: T,
own: T,
debits: T,
) -> Option<PartyTarget<T>> {
if let Some((e, property)) = capital
&& e != T::ZERO
{
let whole = magnitude(e)?;
let property = if property > T::ZERO {
property
} else {
T::ZERO
};
let (cash, in_kind) = if e < T::ZERO {
let in_kind = least(property, whole);
(whole.checked_sub(in_kind)?, in_kind)
} else {
let paid = if net > T::ZERO {
least(net, whole)
} else {
T::ZERO
};
(paid, least(property, whole.checked_sub(paid)?))
};
let counted = cash.checked_add(in_kind)?;
return Some(PartyTarget {
amount: e,
direction: if counted == T::ZERO {
Direction::Neither
} else {
Direction::of(e)
},
cash: with_sign(e, cash)?,
in_kind: with_sign(e, in_kind)?,
});
}
if net != T::ZERO {
return Some(PartyTarget {
amount: net,
direction: Direction::of(net),
cash: net,
in_kind: T::ZERO,
});
}
Some(PartyTarget {
amount: if own != T::ZERO { own } else { debits },
direction: Direction::Neither,
cash: T::ZERO,
in_kind: T::ZERO,
})
}
/// A reversal's target from its original's (rules.md B-35 item 3; B-9: a
/// correction is the opposite entry): every amount negated (never to a
/// negative zero), paid and received swapped. An involution. `None` past
/// the type's range.
pub fn reverse_target<T: Amount>(t: PartyTarget<T>) -> Option<PartyTarget<T>> {
let neg = |a: T| {
if a == T::ZERO {
Some(T::ZERO)
} else {
a.checked_neg()
}
};
Some(PartyTarget {
amount: neg(t.amount)?,
direction: t.direction.flip(),
cash: neg(t.cash)?,
in_kind: neg(t.in_kind)?,
})
}
#[cfg(test)]
mod tests {
use super::*;