check / check (push) Canceled after 0s
Max-testing pass, step 8. The final mutation run left 112 survivors (89% caught). Three agents triaged them by file, by the playbook's table: spec the boundary then test it, a law rather than one more pinned value, assert the real value, never weaken a test. Every expected value is hand-computed. - reports.rs (35): 28 killed, chiefly cash flow (inclusive start date, each line's sign and label, a loss on disposal moving to investing, financing designation), general ledger and members' equity date boundaries, and the balanced() checks (a real report nudged by a cent must stop balancing). 7 equivalent: four >/>= after an early return on zero; two in dead code (removed in this commit); one sign of a zero cleaned on output. - ledger.rs, kernel.rs, journal.rs, money.rs (52, plus one new): 49 killed, including every replay_* mutant at the core level (ids, accumulation, status, vendor tags, out-of-order refusal), FX rates, 0%/100% deductible boundaries, a dividend sweep with no income, the legacy member rounding at close, a bill paid on its own date, a liability-account reconciliation, mixed-currency entries (via serde), ISO 4217 minor units, Display/Debug. 3 equivalent: zero refused before a </<= check; a period whose end is in a closed period refused earlier; columns(0). - schedules, tax, analysis, payouts, assets, form1099 (25): 23 killed (schedule length 1-600, recurring end on its start, frequency parsing and stepping, cost of goods in tax net, a zero payout, disposal on the purchase date, quarter starts, labels); 2 equivalent (zero adds zero; days_held is provably >= 1). Two small production cleanups the triage surfaced: kernel::columns(0) returned a negative-zero credit that would print "-0" (regression a_zero_movement_has_no_negative_zero_column, shown to fail before); and cash_flow computed a book value it never used. Spec lines added where a boundary wasn't specified (cash flow's inclusive start and line signs, members' equity start date, payout with no deposit, disposal on the purchase date, bill paid on its own date). `just mutants` now runs core and store tests (the store's replay tests), not the whole workspace, which rebuilt the viewer for every mutant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
186 lines
5.5 KiB
Rust
186 lines
5.5 KiB
Rust
//! Payout gross-up. Numbers computed by hand.
|
||
//!
|
||
//! An app-store month: App A sold 1,000.00, App B 500.00; the store kept
|
||
//! 15% (225.00) and clawed back a 9.99 refund. Deposit: 1,500.00 − 225.00
|
||
//! − 9.99 = 1,265.01.
|
||
|
||
use chrono::NaiveDate;
|
||
use ledger_core::reports::profit_and_loss_for_period;
|
||
use ledger_core::{
|
||
AccountType, Currency, Ledger, LedgerError, Money, Payout, PayoutAdjustment, PayoutLine, Side,
|
||
payout_plan,
|
||
};
|
||
use rust_decimal::Decimal;
|
||
use std::str::FromStr;
|
||
|
||
fn dec(s: &str) -> Decimal {
|
||
Decimal::from_str(s).unwrap()
|
||
}
|
||
|
||
fn d(day: u32) -> NaiveDate {
|
||
NaiveDate::from_ymd_opt(2026, 11, day).unwrap()
|
||
}
|
||
|
||
struct Books {
|
||
l: Ledger,
|
||
bank: u64,
|
||
sales: u64,
|
||
fees: u64,
|
||
refunds: u64,
|
||
}
|
||
|
||
fn books() -> Books {
|
||
let mut l = Ledger::new(Currency::USD);
|
||
let mut a = |code: &str, name: &str, t| l.add_account(code, name, t, None, false).unwrap();
|
||
let bank = a("1000", "Bank", AccountType::Asset);
|
||
let sales = a("4000", "App Sales", AccountType::Revenue);
|
||
let fees = a("6000", "Platform Fees", AccountType::Expense);
|
||
let refunds = a("4100", "Refunds", AccountType::Expense);
|
||
Books {
|
||
l,
|
||
bank,
|
||
sales,
|
||
fees,
|
||
refunds,
|
||
}
|
||
}
|
||
|
||
fn line(account: u64, amount: &str) -> PayoutLine {
|
||
PayoutLine {
|
||
account,
|
||
amount: dec(amount),
|
||
class: None,
|
||
}
|
||
}
|
||
|
||
fn month(b: &Books, deposit: &str) -> Payout {
|
||
Payout {
|
||
deposit_account: b.bank,
|
||
deposit: dec(deposit),
|
||
revenue: vec![line(b.sales, "1000.00"), line(b.sales, "500.00")],
|
||
fees: vec![line(b.fees, "225.00")],
|
||
adjustments: vec![PayoutAdjustment {
|
||
account: b.refunds,
|
||
side: Side::Debit,
|
||
amount: dec("9.99"),
|
||
class: None,
|
||
}],
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn a_payout_is_grossed_up_to_revenue_and_fees() {
|
||
let mut b = books();
|
||
let (plan, totals) = payout_plan(&month(&b, "1265.01"), b.l.chart()).unwrap();
|
||
assert_eq!(totals.gross, dec("1500.00"));
|
||
assert_eq!(totals.fees, dec("225.00"));
|
||
assert_eq!(totals.adjustments, dec("-9.99"));
|
||
assert_eq!(
|
||
plan.iter()
|
||
.map(|(a, s, m, _)| (*a, *s, *m))
|
||
.collect::<Vec<_>>(),
|
||
[
|
||
(b.bank, Side::Debit, dec("1265.01")),
|
||
(b.fees, Side::Debit, dec("225.00")),
|
||
(b.refunds, Side::Debit, dec("9.99")),
|
||
(b.sales, Side::Credit, dec("1000.00")),
|
||
(b.sales, Side::Credit, dec("500.00")),
|
||
]
|
||
);
|
||
let e = b.l.create_entry(d(30), "App store payout");
|
||
for (a, s, m, _) in plan {
|
||
b.l.add_line(e, a, s, Money::new(m, Currency::USD)).unwrap();
|
||
}
|
||
b.l.post_entry(e).unwrap();
|
||
let pl = profit_and_loss_for_period(&b.l, d(1), d(30), None).unwrap();
|
||
assert_eq!(pl.total_revenue, dec("1500.00"));
|
||
assert_eq!(pl.total_expenses, dec("234.99"));
|
||
}
|
||
|
||
#[test]
|
||
fn a_payout_must_reconcile_to_the_cent() {
|
||
let b = books();
|
||
let err = payout_plan(&month(&b, "1265.00"), b.l.chart()).unwrap_err();
|
||
let LedgerError::InvalidPayout(msg) = err else {
|
||
panic!("{err:?}")
|
||
};
|
||
assert!(msg.contains("= 1265.01"), "{msg}");
|
||
assert!(msg.contains("off by -0.01"), "{msg}");
|
||
}
|
||
|
||
#[test]
|
||
fn a_negative_payout_credits_the_bank() {
|
||
let b = books();
|
||
// 10.00 of sales, 3.00 of fees, a 50.00 refund: the store debits 43.00.
|
||
let p = Payout {
|
||
deposit_account: b.bank,
|
||
deposit: dec("-43.00"),
|
||
revenue: vec![line(b.sales, "10.00")],
|
||
fees: vec![line(b.fees, "3.00")],
|
||
adjustments: vec![PayoutAdjustment {
|
||
account: b.refunds,
|
||
side: Side::Debit,
|
||
amount: dec("50.00"),
|
||
class: None,
|
||
}],
|
||
};
|
||
let (plan, _) = payout_plan(&p, b.l.chart()).unwrap();
|
||
assert_eq!(plan[0], (b.bank, Side::Credit, dec("43.00"), None));
|
||
}
|
||
|
||
#[test]
|
||
fn accounts_must_fit_their_roles() {
|
||
let b = books();
|
||
let invalid = |p: Payout| {
|
||
matches!(
|
||
payout_plan(&p, b.l.chart()),
|
||
Err(LedgerError::InvalidPayout(_))
|
||
)
|
||
};
|
||
let mut p = month(&b, "1265.01");
|
||
p.fees[0].account = b.sales;
|
||
assert!(invalid(p), "fees must be expenses");
|
||
let mut p = month(&b, "1265.01");
|
||
p.revenue[0].account = b.fees;
|
||
assert!(invalid(p), "revenue must be revenue or a liability");
|
||
let mut p = month(&b, "1265.01");
|
||
p.deposit_account = b.sales;
|
||
assert!(invalid(p), "deposits land in an asset");
|
||
let mut p = month(&b, "1265.01");
|
||
p.fees[0].amount = dec("-225.00");
|
||
assert!(invalid(p), "amounts are positive");
|
||
let mut p = month(&b, "1265.01");
|
||
p.revenue.clear();
|
||
assert!(invalid(p), "needs revenue");
|
||
}
|
||
|
||
#[test]
|
||
fn a_zero_payout_has_no_bank_line() {
|
||
let b = books();
|
||
// 10.00 of sales, 3.00 of fees, a 7.00 refund: nothing deposited.
|
||
let p = Payout {
|
||
deposit_account: b.bank,
|
||
deposit: dec("0.00"),
|
||
revenue: vec![line(b.sales, "10.00")],
|
||
fees: vec![line(b.fees, "3.00")],
|
||
adjustments: vec![PayoutAdjustment {
|
||
account: b.refunds,
|
||
side: Side::Debit,
|
||
amount: dec("7.00"),
|
||
class: None,
|
||
}],
|
||
};
|
||
let (plan, totals) = payout_plan(&p, b.l.chart()).unwrap();
|
||
assert_eq!(totals.deposit, Decimal::ZERO);
|
||
assert_eq!(
|
||
plan.iter()
|
||
.map(|(a, s, m, _)| (*a, *s, *m))
|
||
.collect::<Vec<_>>(),
|
||
[
|
||
(b.fees, Side::Debit, dec("3.00")),
|
||
(b.refunds, Side::Debit, dec("7.00")),
|
||
(b.sales, Side::Credit, dec("10.00")),
|
||
]
|
||
);
|
||
}
|