merge: P4b S6, paying on terms (branch S6, 6c4480cd)

The third of the paused branches: Ledger::settle with discounts and the
processor's fee (kernel::settle, Kani settle_is_exact, Lean
settle_posts_a_document_step), books.receive_payment and books.pay_bill
with applications, books.bills_due, the agings' detail and tie-out, the
aging exports and the CPA package's aging CSVs, the handlers in
tools/settlements.rs. Twelve files conflicted with the S4.4 and S5
merges, resolved as plan 8.0.2 says: both routers; both test appends
(the shared match arm in arap_standing.rs spliced into both tests; the
shared `.unwrap()` in store.rs); the scope table at 124; one
invoices-and-bills handbook (S5's send step 1 with S6's steps 2, 3, 5
and procedures) registered once; docs/mcp-interface.md with S4.4's
create docs and S6's payment docs; S5's law renumbered 35 and S6's 36
(S1's is 34); the parity rows and sections of both. S6's wire tests
create their bills as S4.4's contract now requires (confirmed, the
vendor's number, a line account, the vendor's invoice uploaded) and live
in their own module beside S4.4's tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
swaitsandClaude Fable 5.1 committed 2026-10-28 21:26:40 -06:00
commit be58059830
45 files changed
+3949 -520

No files matched your search

+80
View File
@@ -71,6 +71,16 @@ enum Op {
Close(u8),
/// A hand line on receivables (true) or payables: always refused.
Hand(bool, i64),
/// One payment settling two documents of one party, the first with a
/// discount and (on invoices) a processor's fee.
Settle {
a: usize,
b: usize,
n: u8,
cash: i64,
discount: i64,
fee: i64,
},
}
fn arb_op() -> impl Strategy<Value = Op> {
@@ -83,6 +93,15 @@ fn arb_op() -> impl Strategy<Value = Op> {
2 => (0usize..8, 0u8..90).prop_map(|(k, n)| Op::MarkSent(k, n)),
1 => (1u8..4).prop_map(Op::Close),
1 => (any::<bool>(), 1i64..10_000).prop_map(|(r, c)| Op::Hand(r, c)),
2 => (0usize..8, 0usize..8, 0u8..90, 1i64..20_000, 0i64..2_000, 0i64..500)
.prop_map(|(a, b, n, cash, discount, fee)| Op::Settle {
a,
b,
n,
cash,
discount,
fee,
}),
]
}
@@ -220,6 +239,67 @@ fn run(s: &mut Store, k: &Setup, ops: &[Op], docs: &mut Vec<DocRef>) -> Result<(
})
.map(drop)
}
Op::Settle { .. } if docs.is_empty() => Ok(()),
Op::Settle {
a,
b,
n,
cash,
discount,
fee,
} => {
let first = docs[a % docs.len()];
let mut second = docs[b % docs.len()];
if second == first && docs.len() > 1 {
second = docs[(b + 1) % docs.len()];
}
let same_kind = matches!(
(first, second),
(DocRef::Invoice(_), DocRef::Invoice(_)) | (DocRef::Bill(_), DocRef::Bill(_))
);
if !same_kind {
// One party and one kind per settlement: not this pair.
Ok(())
} else {
let party = match first {
DocRef::Invoice(id) => s.ledger().get_invoice(id).map(|i| i.customer_id),
DocRef::Bill(id) => s.ledger().get_bill(id).map(|v| v.vendor_id),
}
.unwrap();
let invoices = matches!(first, DocRef::Invoice(_));
let money = cents(cash);
let apps = vec![
ledger_core::Application {
doc: first,
cash: money,
discount: cents(discount),
discount_reason: (discount > 0)
.then(|| format!("a late discount, n {n}")),
},
ledger_core::Application {
doc: second,
cash: money,
discount: Decimal::ZERO,
discount_reason: None,
},
];
s.settle(ledger_core::SettleParams {
date: day(n),
description: "two documents, one payment".into(),
cash_account: k.bank,
party,
applications: apps,
fee: (invoices && fee > 0).then_some(ledger_core::Fee {
amount: cents(fee),
account: None,
vendor: None,
}),
instrument: None,
reference: None,
})
.map(drop)
}
}
Op::Hand(receivables, c) => {
let code = if receivables { "1200" } else { "2000" };
match s.ledger().chart().find_by_code(code).map(|a| a.id) {