feat(arap): settle documents with discounts and a fee (P4b S6)
One payment had to name one document and could take no discount or processor fee, so paying several invoices from one deposit meant several entries and the fee had nowhere to go. This adds the kernel's exact settlement arithmetic (applications total, net deposit), a Ledger::settle that posts one entry for a customer's or vendor's documents, and the gross method and gross fee A-8/A-4 chose. - kernel::settle: sum(cash)+sum(discounts)=applications, sum(cash)-fee= deposit, None on overflow or a negative deposit. Kani settle_is_exact plus the Decimal bridge. - Ledger::settle / settlement_lines: one party and one kind per settlement, one application per document, each cash+discount within payment_room; a discount within the terms (discount_max and discount_by) needs no reason, a larger or later one does; a bill with an asset line is refused with D-49's remedy; a fee is a customer payment's only and posts gross. - Rows are payment lines in application order, then discount lines, the fee line and the cash line, so a journaled event's line indices are stable; replay tags exactly the line an event names and leaves an unmatched line unattached (never refuses). - receive_payment_with and pay_bill_with become one-application calls. - Roles: sales_allowances (4100), purchase_discounts (6995) and payment_processing_fees (6060), constants used on demand but not added to control_accounts() (existing charts may use those codes for other things). - Lean: settle posts through Step.document, so settle_posts_a_document_step and settle_unattached, no new constructor; both in Audit.lean. - Core tests: bill_on_terms_with_discount, late_discount_needs_a_reason, discount_on_an_asset_bill_is_refused_with_the_remedy, a_fee_posts_gross_to_receivables_and_net_to_the_bank, a_settlement_names_its_lines_and_refuses_a_document_twice, and equipment_bill_payment_is_investing_cash in the cash-flow suite. - Bookspec B-42 is stated, with its id BS-arap-6; the catalogue and index now record it as CONVENTION. just check and just prove green.
This commit is contained in:
1 parent
ed637f0e27
commit
10ee0681c1
15 files changed
+1511
-146
No files matched your search
@@ -269,6 +269,63 @@ pub fn discount_max<T: Amount>(total: T, percent_bp: u32) -> Option<T> {
|
||||
total.checked_mul(bp)?.checked_div_exact(ten_thousand)
|
||||
}
|
||||
|
||||
/// What a settlement of one or more documents comes to: the applications'
|
||||
/// total (cash plus discounts taken) and what reaches the deposit account
|
||||
/// (the cash less the processor's fee).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Settlement<T> {
|
||||
/// Σ cash + Σ discounts: what the settlement applies to the documents.
|
||||
pub applications_total: T,
|
||||
/// Σ cash − fee: what the deposit account receives (never negative).
|
||||
pub deposit: T,
|
||||
}
|
||||
|
||||
/// The arithmetic of a settlement (rules.md B-42; plan P4b S6): each
|
||||
/// document's cash and discount, and the processor's fee withheld from the
|
||||
/// cash that reaches the bank.
|
||||
///
|
||||
/// ```text
|
||||
/// GOAL: a settlement is exact: what the documents are settled by is the
|
||||
/// cash plus the discounts, and the deposit is the cash less the fee.
|
||||
/// SOURCE: docs/spec/rules.md B-42; plan/served.md 8.3.6, 8.3.13 (B-42);
|
||||
/// CONVENTION; DECISION: the gross method (A-8) and the fee's
|
||||
/// gross presentation (A-4).
|
||||
/// INVARIANTS:
|
||||
/// 1. Some(s): s.applications_total = Σ cash + Σ discounts and
|
||||
/// s.deposit = Σ cash − fee, exactly, with s.deposit >= 0.
|
||||
/// 2. None on overflow, a negative amount, a negative fee, a fee above
|
||||
/// the cash, or arrays of different lengths: never a wrapped or
|
||||
/// rounded figure.
|
||||
/// ```
|
||||
pub fn settle<T: Amount>(cash: &[T], discounts: &[T], fee: T) -> Option<Settlement<T>> {
|
||||
if cash.len() != discounts.len() || fee < T::ZERO {
|
||||
return None;
|
||||
}
|
||||
let mut total_cash = T::ZERO;
|
||||
for &amount in cash {
|
||||
if amount < T::ZERO {
|
||||
return None;
|
||||
}
|
||||
total_cash = total_cash.checked_add(amount)?;
|
||||
}
|
||||
let mut total_discounts = T::ZERO;
|
||||
for &amount in discounts {
|
||||
if amount < T::ZERO {
|
||||
return None;
|
||||
}
|
||||
total_discounts = total_discounts.checked_add(amount)?;
|
||||
}
|
||||
let applications_total = total_cash.checked_add(total_discounts)?;
|
||||
let deposit = total_cash.checked_sub(fee)?;
|
||||
if deposit < T::ZERO {
|
||||
return None;
|
||||
}
|
||||
Some(Settlement {
|
||||
applications_total,
|
||||
deposit,
|
||||
})
|
||||
}
|
||||
|
||||
/// A document line's amount from its quantity and unit price: the exact
|
||||
/// product, never rounded.
|
||||
///
|
||||
|
||||
Reference in new issue
Block a user