diff --git a/crates/ledger-core/src/assets.rs b/crates/ledger-core/src/assets.rs index 2af270a..8c164c2 100644 --- a/crates/ledger-core/src/assets.rs +++ b/crates/ledger-core/src/assets.rs @@ -128,8 +128,26 @@ fn depreciation_end(asset: &Asset) -> NaiveDate { } } -/// Straight-line depreciation schedule from the purchase month through the -/// month containing `through` (clamped to the depreciation end). +/// Straight-line depreciation schedule from the purchase month through +/// `through` (clamped to the depreciation end): one line per month, the +/// last prorated by days when `through` falls mid-month. +/// +/// ```text +/// GOAL: how much of an asset's cost has been used up, month by month. +/// SOURCE: docs/spec/rules.md B-15 (straight line; salvage never +/// depreciated; partial months by days); DECISION (Stephen, +/// 2026-10-13): a schedule through a mid-month date prorates that +/// month by days, as partial first and last months do. +/// INVARIANTS: +/// 1. A full schedule MUST sum to exactly cost - salvage, to the cent. +/// 2. Accumulated depreciation NEVER falls; book value NEVER rises and +/// NEVER drops below salvage. +/// SCENARIOS: +/// 1. IF $12,000 over 12 months from Jan 1, through Jun 15: +/// - SHOULD end with June at 500.00 (15 of 30 days), accumulated +/// 5,500.00 +/// BECAUSE depreciation accrues by the day within a month. +/// ``` /// /// Partial first and last months are prorated by days held. Each month is /// rounded to cents via the running-total method, and the final month diff --git a/crates/ledger-core/src/error.rs b/crates/ledger-core/src/error.rs index 55cdb61..5481119 100644 --- a/crates/ledger-core/src/error.rs +++ b/crates/ledger-core/src/error.rs @@ -159,6 +159,16 @@ pub enum LedgerError { other_end: chrono::NaiveDate, }, + /// A payment can't apply to an invoice or bill dated after it (Stephen, + /// 2026-10-13); a real prepayment is a deposit, recorded differently. + #[error("a payment dated {date} can't apply to a document dated {document_date}")] + PaymentBeforeDocument { + /// The payment's date. + date: chrono::NaiveDate, + /// The invoice's or bill's date. + document_date: chrono::NaiveDate, + }, + /// The referenced journal entry does not exist. #[error("entry {0} does not exist")] UnknownEntry(u64), diff --git a/crates/ledger-core/src/ledger.rs b/crates/ledger-core/src/ledger.rs index 1ac8502..01be19f 100644 --- a/crates/ledger-core/src/ledger.rs +++ b/crates/ledger-core/src/ledger.rs @@ -212,6 +212,10 @@ pub struct Ledger { next_recurring_id: RecurringId, /// Who each entry paid or was billed by, for 1099 reporting. entry_vendors: HashMap, + /// Each invoice's payments (entry, amount), for aging as of a date. + invoice_payments: HashMap>, + /// Each bill's payments (entry, amount), for aging as of a date. + bill_payments: HashMap>, } impl Ledger { @@ -247,6 +251,8 @@ impl Ledger { recurring: std::collections::BTreeMap::new(), next_recurring_id: 1, entry_vendors: HashMap::new(), + invoice_payments: HashMap::new(), + bill_payments: HashMap::new(), } } @@ -1848,7 +1854,15 @@ impl Ledger { if let Some(pid) = self.closed_period_containing(date) { return Err(LedgerError::ClosedPeriod(pid, date.to_string())); } + if date < invoice.date { + return Err(LedgerError::PaymentBeforeDocument { + date, + document_date: invoice.date, + }); + } self.check_controls(&[RECEIVABLES])?; + // Every check before the entry exists: a refusal leaves no draft. + self.check_line_account(cash_account)?; let ar = self.ensure_control(&RECEIVABLES); let entry = self.create_entry(date, &format!("Payment on invoice {}", invoice_id)); let money = Money::new(amount, self.base_currency); @@ -1858,6 +1872,10 @@ impl Ledger { let inv = self.invoices.get_mut(&invoice_id).expect("invoice exists"); inv.amount_paid += amount; inv.status = arap::derive_status(inv.amount_paid, inv.total()); + self.invoice_payments + .entry(invoice_id) + .or_default() + .push((entry, amount)); Ok(entry) } @@ -1897,7 +1915,15 @@ impl Ledger { if let Some(pid) = self.closed_period_containing(date) { return Err(LedgerError::ClosedPeriod(pid, date.to_string())); } + if date < bill.date { + return Err(LedgerError::PaymentBeforeDocument { + date, + document_date: bill.date, + }); + } self.check_controls(&[PAYABLES])?; + // Every check before the entry exists: a refusal leaves no draft. + self.check_line_account(cash_account)?; let ap = self.ensure_control(&PAYABLES); let entry = self.create_entry(date, &format!("Payment on bill {}", bill_id)); let money = Money::new(amount, self.base_currency); @@ -1907,6 +1933,10 @@ impl Ledger { let b = self.bills.get_mut(&bill_id).expect("bill exists"); b.amount_paid += amount; b.status = arap::derive_status(b.amount_paid, b.total()); + self.bill_payments + .entry(bill_id) + .or_default() + .push((entry, amount)); self.entry_vendors.insert(entry, bill.vendor_id); Ok(entry) } @@ -1991,12 +2021,15 @@ impl Ledger { amount: Decimal, entry_id: EntryId, ) -> Result<(), LedgerError> { - let _ = entry_id; let inv = self .invoices .get_mut(&invoice_id) .ok_or(LedgerError::UnknownInvoice(invoice_id))?; inv.amount_paid += amount; + self.invoice_payments + .entry(invoice_id) + .or_default() + .push((entry_id, amount)); inv.status = arap::derive_status(inv.amount_paid, inv.total()); Ok(()) } @@ -2016,6 +2049,10 @@ impl Ledger { .ok_or(LedgerError::UnknownBill(bill_id))?; bill.amount_paid += amount; bill.status = arap::derive_status(bill.amount_paid, bill.total()); + self.bill_payments + .entry(bill_id) + .or_default() + .push((entry_id, amount)); let vendor = bill.vendor_id; self.entry_vendors.insert(entry_id, vendor); Ok(()) @@ -2026,7 +2063,12 @@ impl Ledger { pub fn ar_aging(&self, as_of: NaiveDate) -> Vec { let mut by_customer: HashMap = HashMap::new(); for inv in self.invoices.values() { - let balance = inv.balance(); + // Only what existed by `as_of`: issued by then, less payments + // made by then (Stephen, 2026-10-13). + if inv.date > as_of { + continue; + } + let balance = inv.total() - self.paid_by(self.invoice_payments.get(&inv.id), as_of); if balance.is_zero() { continue; } @@ -2056,12 +2098,27 @@ impl Ledger { rows } + /// What a document's payments add up to through `as_of`: payments are + /// posted entries, dated. + fn paid_by(&self, payments: Option<&Vec<(EntryId, Decimal)>>, as_of: NaiveDate) -> Decimal { + payments + .into_iter() + .flatten() + .filter(|(e, _)| self.get_entry(*e).is_some_and(|e| e.date() <= as_of)) + .map(|(_, amount)| *amount) + .sum() + } + /// A/P aging as of a date: one row per vendor with an outstanding /// balance, bucketed by days past due. pub fn ap_aging(&self, as_of: NaiveDate) -> Vec { let mut by_vendor: HashMap = HashMap::new(); for bill in self.bills.values() { - let balance = bill.balance(); + // Only what existed by `as_of` (see `ar_aging`). + if bill.date > as_of { + continue; + } + let balance = bill.total() - self.paid_by(self.bill_payments.get(&bill.id), as_of); if balance.is_zero() { continue; } diff --git a/crates/ledger-core/tests/assets.rs b/crates/ledger-core/tests/assets.rs index ccaddb3..28e0609 100644 --- a/crates/ledger-core/tests/assets.rs +++ b/crates/ledger-core/tests/assets.rs @@ -261,18 +261,39 @@ fn schedule_through_purchase_date_covers_one_day() { } #[test] -fn golden_van_depreciation_schedule() { - // The canonical van's month-by-month schedule, pinned byte-for-value. - // Hand-verified: 61 months, first 548.39 = 1000 x 17/31, last 451.61 = - // 1000 x 14/31, lifetime total exactly 60000.00. +fn van_schedule_is_the_hand_computed_one() { + // Rank 1, computed by hand (rules.md B-15), replacing an unreviewed + // golden file (2026-10-13; every row of the old file was checked + // against this computation first, and matched). + // $60,000 over 60 months is $1,000 a month. Bought 2026-01-15: January + // holds 17 of 31 days = 548.39; February 2026 through December 2030 are + // 59 full months; life ends 2031-01-15, so January 2031 holds 14 of 31 + // days = 451.61. 548.39 + 59,000 + 451.61 = 60,000.00 exactly. let (mut ledger, cost, accum, expense) = test_ledger(); let id = van_asset(&mut ledger, 60_000, cost, accum, expense); let asset = ledger.get_asset(id).unwrap(); let sched = depreciation_schedule(asset, date(2032, 1, 1)); - let got = serde_json::to_value(&sched).unwrap(); - let golden: serde_json::Value = - serde_json::from_str(include_str!("golden/van_schedule.json")).unwrap(); - assert_eq!(got, golden); + assert_eq!(sched.len(), 61); + let mut accumulated = Decimal::ZERO; + for (k, line) in sched.iter().enumerate() { + let (year, month) = (2026 + (k as i32) / 12, (k as u32) % 12 + 1); + let amount = match k { + 0 => Decimal::new(54839, 2), + 60 => Decimal::new(45161, 2), + _ => Decimal::new(100000, 2), + }; + accumulated += amount; + assert_eq!(line.period, format!("{year}-{month:02}")); + assert_eq!(line.amount, amount, "{}", line.period); + assert_eq!(line.accumulated, accumulated, "{}", line.period); + assert_eq!( + line.book_value, + Decimal::from(60_000) - accumulated, + "{}", + line.period + ); + } + assert_eq!(accumulated, Decimal::from(60_000)); } #[test] diff --git a/crates/ledger-core/tests/golden/van_schedule.json b/crates/ledger-core/tests/golden/van_schedule.json deleted file mode 100644 index 40cd910..0000000 --- a/crates/ledger-core/tests/golden/van_schedule.json +++ /dev/null @@ -1,368 +0,0 @@ -[ - { - "accumulated": "548.39", - "amount": "548.39", - "book_value": "59451.61", - "period": "2026-01" - }, - { - "accumulated": "1548.39", - "amount": "1000.00", - "book_value": "58451.61", - "period": "2026-02" - }, - { - "accumulated": "2548.39", - "amount": "1000.00", - "book_value": "57451.61", - "period": "2026-03" - }, - { - "accumulated": "3548.39", - "amount": "1000.00", - "book_value": "56451.61", - "period": "2026-04" - }, - { - "accumulated": "4548.39", - "amount": "1000.00", - "book_value": "55451.61", - "period": "2026-05" - }, - { - "accumulated": "5548.39", - "amount": "1000.00", - "book_value": "54451.61", - "period": "2026-06" - }, - { - "accumulated": "6548.39", - "amount": "1000.00", - "book_value": "53451.61", - "period": "2026-07" - }, - { - "accumulated": "7548.39", - "amount": "1000.00", - "book_value": "52451.61", - "period": "2026-08" - }, - { - "accumulated": "8548.39", - "amount": "1000.00", - "book_value": "51451.61", - "period": "2026-09" - }, - { - "accumulated": "9548.39", - "amount": "1000.00", - "book_value": "50451.61", - "period": "2026-10" - }, - { - "accumulated": "10548.39", - "amount": "1000.00", - "book_value": "49451.61", - "period": "2026-11" - }, - { - "accumulated": "11548.39", - "amount": "1000.00", - "book_value": "48451.61", - "period": "2026-12" - }, - { - "accumulated": "12548.39", - "amount": "1000.00", - "book_value": "47451.61", - "period": "2027-01" - }, - { - "accumulated": "13548.39", - "amount": "1000.00", - "book_value": "46451.61", - "period": "2027-02" - }, - { - "accumulated": "14548.39", - "amount": "1000.00", - "book_value": "45451.61", - "period": "2027-03" - }, - { - "accumulated": "15548.39", - "amount": "1000.00", - "book_value": "44451.61", - "period": "2027-04" - }, - { - "accumulated": "16548.39", - "amount": "1000.00", - "book_value": "43451.61", - "period": "2027-05" - }, - { - "accumulated": "17548.39", - "amount": "1000.00", - "book_value": "42451.61", - "period": "2027-06" - }, - { - "accumulated": "18548.39", - "amount": "1000.00", - "book_value": "41451.61", - "period": "2027-07" - }, - { - "accumulated": "19548.39", - "amount": "1000.00", - "book_value": "40451.61", - "period": "2027-08" - }, - { - "accumulated": "20548.39", - "amount": "1000.00", - "book_value": "39451.61", - "period": "2027-09" - }, - { - "accumulated": "21548.39", - "amount": "1000.00", - "book_value": "38451.61", - "period": "2027-10" - }, - { - "accumulated": "22548.39", - "amount": "1000.00", - "book_value": "37451.61", - "period": "2027-11" - }, - { - "accumulated": "23548.39", - "amount": "1000.00", - "book_value": "36451.61", - "period": "2027-12" - }, - { - "accumulated": "24548.39", - "amount": "1000.00", - "book_value": "35451.61", - "period": "2028-01" - }, - { - "accumulated": "25548.39", - "amount": "1000.00", - "book_value": "34451.61", - "period": "2028-02" - }, - { - "accumulated": "26548.39", - "amount": "1000.00", - "book_value": "33451.61", - "period": "2028-03" - }, - { - "accumulated": "27548.39", - "amount": "1000.00", - "book_value": "32451.61", - "period": "2028-04" - }, - { - "accumulated": "28548.39", - "amount": "1000.00", - "book_value": "31451.61", - "period": "2028-05" - }, - { - "accumulated": "29548.39", - "amount": "1000.00", - "book_value": "30451.61", - "period": "2028-06" - }, - { - "accumulated": "30548.39", - "amount": "1000.00", - "book_value": "29451.61", - "period": "2028-07" - }, - { - "accumulated": "31548.39", - "amount": "1000.00", - "book_value": "28451.61", - "period": "2028-08" - }, - { - "accumulated": "32548.39", - "amount": "1000.00", - "book_value": "27451.61", - "period": "2028-09" - }, - { - "accumulated": "33548.39", - "amount": "1000.00", - "book_value": "26451.61", - "period": "2028-10" - }, - { - "accumulated": "34548.39", - "amount": "1000.00", - "book_value": "25451.61", - "period": "2028-11" - }, - { - "accumulated": "35548.39", - "amount": "1000.00", - "book_value": "24451.61", - "period": "2028-12" - }, - { - "accumulated": "36548.39", - "amount": "1000.00", - "book_value": "23451.61", - "period": "2029-01" - }, - { - "accumulated": "37548.39", - "amount": "1000.00", - "book_value": "22451.61", - "period": "2029-02" - }, - { - "accumulated": "38548.39", - "amount": "1000.00", - "book_value": "21451.61", - "period": "2029-03" - }, - { - "accumulated": "39548.39", - "amount": "1000.00", - "book_value": "20451.61", - "period": "2029-04" - }, - { - "accumulated": "40548.39", - "amount": "1000.00", - "book_value": "19451.61", - "period": "2029-05" - }, - { - "accumulated": "41548.39", - "amount": "1000.00", - "book_value": "18451.61", - "period": "2029-06" - }, - { - "accumulated": "42548.39", - "amount": "1000.00", - "book_value": "17451.61", - "period": "2029-07" - }, - { - "accumulated": "43548.39", - "amount": "1000.00", - "book_value": "16451.61", - "period": "2029-08" - }, - { - "accumulated": "44548.39", - "amount": "1000.00", - "book_value": "15451.61", - "period": "2029-09" - }, - { - "accumulated": "45548.39", - "amount": "1000.00", - "book_value": "14451.61", - "period": "2029-10" - }, - { - "accumulated": "46548.39", - "amount": "1000.00", - "book_value": "13451.61", - "period": "2029-11" - }, - { - "accumulated": "47548.39", - "amount": "1000.00", - "book_value": "12451.61", - "period": "2029-12" - }, - { - "accumulated": "48548.39", - "amount": "1000.00", - "book_value": "11451.61", - "period": "2030-01" - }, - { - "accumulated": "49548.39", - "amount": "1000.00", - "book_value": "10451.61", - "period": "2030-02" - }, - { - "accumulated": "50548.39", - "amount": "1000.00", - "book_value": "9451.61", - "period": "2030-03" - }, - { - "accumulated": "51548.39", - "amount": "1000.00", - "book_value": "8451.61", - "period": "2030-04" - }, - { - "accumulated": "52548.39", - "amount": "1000.00", - "book_value": "7451.61", - "period": "2030-05" - }, - { - "accumulated": "53548.39", - "amount": "1000.00", - "book_value": "6451.61", - "period": "2030-06" - }, - { - "accumulated": "54548.39", - "amount": "1000.00", - "book_value": "5451.61", - "period": "2030-07" - }, - { - "accumulated": "55548.39", - "amount": "1000.00", - "book_value": "4451.61", - "period": "2030-08" - }, - { - "accumulated": "56548.39", - "amount": "1000.00", - "book_value": "3451.61", - "period": "2030-09" - }, - { - "accumulated": "57548.39", - "amount": "1000.00", - "book_value": "2451.61", - "period": "2030-10" - }, - { - "accumulated": "58548.39", - "amount": "1000.00", - "book_value": "1451.61", - "period": "2030-11" - }, - { - "accumulated": "59548.39", - "amount": "1000.00", - "book_value": "451.61", - "period": "2030-12" - }, - { - "accumulated": "60000", - "amount": "451.61", - "book_value": "0", - "period": "2031-01" - } -] diff --git a/crates/ledger-core/tests/spec_arap.rs b/crates/ledger-core/tests/spec_arap.rs index 7ae3e07..5dbbde5 100644 --- a/crates/ledger-core/tests/spec_arap.rs +++ b/crates/ledger-core/tests/spec_arap.rs @@ -1162,7 +1162,10 @@ proptest! { let rows = l.ar_aging(as_of); let mut want: std::collections::BTreeMap = Default::default(); for (i, doc) in docs.iter().enumerate() { - let open = totals[i] - expect_paid[i]; + // Payments (dated base + 5) count only once made: aging + // as of a date sees what existed then (Stephen, 2026-10-13). + let paid = if as_of >= base + Duration::days(5) { expect_paid[i] } else { Decimal::ZERO }; + let open = totals[i] - paid; if open.is_zero() { continue; } @@ -1175,7 +1178,8 @@ proptest! { prop_assert_eq!(rows.len(), got.len(), "one row per customer"); prop_assert_eq!(&got, &want); let row_sum: Decimal = rows.iter().map(|r| r.total()).sum(); - prop_assert_eq!(row_sum, open_sum); + let want_sum: Decimal = want.values().flatten().copied().sum(); + prop_assert_eq!(row_sum, want_sum); } } @@ -1247,7 +1251,9 @@ proptest! { let rows = l.ap_aging(as_of); let mut want: std::collections::BTreeMap = Default::default(); for (i, doc) in docs.iter().enumerate() { - let open = totals[i] - expect_paid[i]; + // Payments (dated base + 3) count only once made. + let paid = if as_of >= base + Duration::days(3) { expect_paid[i] } else { Decimal::ZERO }; + let open = totals[i] - paid; if open.is_zero() { continue; } @@ -1262,3 +1268,69 @@ proptest! { } } } + +// --------------------------------------------------------------------------- +// Decisions (Stephen, 2026-10-13): no payment before its document; aging +// as of a date sees only what existed by then. +// --------------------------------------------------------------------------- + +/// A payment can't apply to an invoice or bill that didn't exist yet (a +/// real prepayment is a deposit, recorded differently). Refused, books +/// unchanged. +#[test] +fn a_payment_never_predates_its_document() { + let (mut l, cash) = books(); + let c = l.add_customer("Acme").unwrap(); + let v = l.add_vendor("Vend").unwrap(); + let inv = l + .create_invoice(c, d(2026, 3, 10), d(2026, 4, 10), vec![line("w", "100.00")]) + .unwrap(); + let bill = l + .create_bill(v, d(2026, 3, 10), d(2026, 4, 10), vec![line("w", "100.00")]) + .unwrap(); + let before = fingerprint(&l); + assert_eq!( + l.receive_payment(inv, d(2026, 3, 9), dec("10.00"), cash), + Err(LedgerError::PaymentBeforeDocument { + date: d(2026, 3, 9), + document_date: d(2026, 3, 10), + }) + ); + assert!(matches!( + l.pay_bill(bill, d(2026, 3, 1), dec("10.00"), cash), + Err(LedgerError::PaymentBeforeDocument { .. }) + )); + assert_eq!(fingerprint(&l), before); + // The same day is fine. + l.receive_payment(inv, d(2026, 3, 10), dec("10.00"), cash) + .unwrap(); +} + +/// Aging as of a date shows only documents issued by then, less only the +/// payments made by then. +#[test] +fn aging_as_of_a_date_sees_only_what_existed_then() { + let (mut l, cash) = books(); + let c = l.add_customer("Acme").unwrap(); + let early = l + .create_invoice(c, d(2026, 1, 5), d(2026, 2, 4), vec![line("a", "300.00")]) + .unwrap(); + l.create_invoice(c, d(2026, 3, 1), d(2026, 3, 31), vec![line("b", "500.00")]) + .unwrap(); + l.receive_payment(early, d(2026, 2, 20), dec("100.00"), cash) + .unwrap(); + let total = |as_of| -> Decimal { + l.ar_aging(as_of) + .iter() + .map(|r| buckets(r).iter().copied().sum::()) + .sum() + }; + // Jan 31: only the first invoice exists, nothing paid yet. + assert_eq!(total(d(2026, 1, 31)), dec("300.00")); + // Feb 28: paid 100 of it; the March invoice doesn't exist yet. + assert_eq!(total(d(2026, 2, 28)), dec("200.00")); + // Mar 15: both. + assert_eq!(total(d(2026, 3, 15)), dec("700.00")); + // Before anything was issued: nothing. + assert!(l.ar_aging(d(2025, 12, 31)).is_empty()); +} diff --git a/crates/ledger-core/tests/spec_assets_payroll.rs b/crates/ledger-core/tests/spec_assets_payroll.rs index 20ff7db..de1ebfd 100644 --- a/crates/ledger-core/tests/spec_assets_payroll.rs +++ b/crates/ledger-core/tests/spec_assets_payroll.rs @@ -285,16 +285,17 @@ fn full_months_each_take_one_twelfth_b15() { } } -/// Oracle: interface doc "through the month containing `through`": through -/// 2026-06-15 covers Jan..Jun. +/// Oracle: depreciation_schedule SCENARIO 1 (DECISION, Stephen 2026-10-13, +/// resolving this test's original disagreement in favor of the code): a +/// schedule through 2026-06-15 prorates June by days: 15/30 of 1,000. #[test] -#[ignore = "SPEC-DISAGREEMENT: schedule through 2026-06-15 prorates June to 500.00 (acc 5500.00), doc says through the month containing it (acc 6000.00)"] -fn schedule_runs_through_month_containing_through_date() { +fn schedule_through_a_mid_month_date_prorates_that_month_by_days() { let a = asset("12000.00", "0.00", 12, date(2026, 1, 1)); let s = depreciation_schedule(&a, date(2026, 6, 15)); assert_eq!(s.len(), 6); assert_eq!(s.last().unwrap().period, "2026-06"); - assert_eq!(s.last().unwrap().accumulated, d("6000.00")); + assert_eq!(s.last().unwrap().amount, d("500.00")); + assert_eq!(s.last().unwrap().accumulated, d("5500.00")); } /// Oracle: interface doc "clamped to the depreciation end": a schedule asked diff --git a/docs/spec/rules.md b/docs/spec/rules.md index 8d9016d..6b6bbdb 100644 --- a/docs/spec/rules.md +++ b/docs/spec/rules.md @@ -245,7 +245,8 @@ depreciation that evenly splits the depreciable amount across the useful life of the asset," with "(Cost – salvage value)/Useful life"; partial periods: "A common method is to allocate depreciation expense based on the number of months the asset is owned in a year." wanderbooks prorates -partial months by days. ASC 360-10-35-4 not yet verified (see README). +partial months by days, including a schedule through a mid-month date +(DECISION, Stephen 2026-10-13). ASC 360-10-35-4 not yet verified (see README). Tax conventions (half-year, mid-month, mid-quarter): IRS Pub. 946 (`sources/irs-depreciation.md`), for the CPA. @@ -291,10 +292,16 @@ more than 90 days past due"; it is illustrative, not a rule. The FFIEC Call Report instructions (bank regulators, not binding here): "The past due status … should be determined in accordance with its contractual repayment terms." A document due today is not yet past due: DECISION -(Stephen, 2026-10-13). +(Stephen, 2026-10-13). Aging as of a date shows only documents issued by +then, less only payments made by then; and a payment never predates its +invoice or bill (a real prepayment is a deposit): DECISIONS (Stephen, +2026-10-13). -**Enforced:** `arap::aging_bucket`. **Checked:** `aging_boundaries`, -`aging_is_exclusive_and_only_ages`. +**Enforced:** `arap::aging_bucket`, `Ledger::ar_aging`/`ap_aging`, +`receive_payment`/`pay_bill` (`PaymentBeforeDocument`). **Checked:** +`aging_boundaries`, `aging_is_exclusive_and_only_ages`, +`aging_as_of_a_date_sees_only_what_existed_then`, +`a_payment_never_predates_its_document`. ## B-19 1099 reporting thresholds