From 962b52c99173a439d28761e59c89244d55fdcafa Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Mon, 26 Oct 2026 16:25:36 -0600 Subject: [PATCH] refactor(settings): the slice's contracts and notes (P4b S4.2) invoicing.first_number's refusal (LedgerError::FirstNumberLocked) lands as the contract the store's check raises once invoices carry numbers (S4.3): a series that has begun is never renumbered (B-40, A-2). Also: the settings invariants name the number prefix's bound, update_party's doc says which facts wait for documents_v2, and the email check no longer reads as a single-arm match. Terms is S4.1's as it landed; this slice no longer carries a copy. --- crates/ledger-core/src/error.rs | 9 +++++++++ crates/ledger-core/src/ledger.rs | 9 ++++----- crates/ledger-core/src/settings.rs | 2 ++ crates/ledger-store/src/store.rs | 4 ++++ 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/crates/ledger-core/src/error.rs b/crates/ledger-core/src/error.rs index d6eb3be..28212b3 100644 --- a/crates/ledger-core/src/error.rs +++ b/crates/ledger-core/src/error.rs @@ -655,6 +655,15 @@ pub enum LedgerError { )] TermsDiscountNeedsNetDays, + /// `invoicing.first_number` asked to move after a numbered invoice + /// exists: the series has begun, and a number is never reused or + /// renumbered (rules.md B-40, A-2). The store's check lands with the + /// invoice numbers it reads (P4b S4.3). + #[error( + "invoicing.first_number is locked: a numbered invoice already exists, so the series has begun and its numbers are never reused" + )] + FirstNumberLocked, + /// A date computed from another (payment terms' due or discount date) /// falls past the calendar the books keep. #[error( diff --git a/crates/ledger-core/src/ledger.rs b/crates/ledger-core/src/ledger.rs index 2b3b019..4fd384f 100644 --- a/crates/ledger-core/src/ledger.rs +++ b/crates/ledger-core/src/ledger.rs @@ -4702,12 +4702,11 @@ impl Ledger { } } Archived(true) => {} - Email(v) => match v { - Some(e) => { - crate::parties::check_email("email", e).map_err(LedgerError::InvalidParty)? + Email(v) => { + if let Some(e) = v { + crate::parties::check_email("email", e).map_err(LedgerError::InvalidParty)?; } - None => {} - }, + } // Terms are typed and already canonical (`crate::Terms`), so // there is nothing here a string could get wrong. DefaultTermsAsCustomer(_) | DefaultTermsAsVendor(_) => {} diff --git a/crates/ledger-core/src/settings.rs b/crates/ledger-core/src/settings.rs index 0c8e9c0..7387878 100644 --- a/crates/ledger-core/src/settings.rs +++ b/crates/ledger-core/src/settings.rs @@ -40,6 +40,8 @@ //! `lines`) are the one text the full-TIN check doesn't scan, because //! a routing number is nine digits (A-3); every other text in the //! list is scanned. +//! 8. An invoice number's prefix is 1 to 12 visible ASCII characters: +//! longer, blank or holding a space would not read as one number. //! ``` use crate::TaxForm; diff --git a/crates/ledger-store/src/store.rs b/crates/ledger-store/src/store.rs index 9c6753c..0e01fd8 100644 --- a/crates/ledger-store/src/store.rs +++ b/crates/ledger-store/src/store.rs @@ -1597,6 +1597,10 @@ impl Store { } /// Change one of a party's facts. Journaled as `party.updated`. + /// + /// An email or a default terms fact needs the `documents_v2` feature + /// (plan P4b S4.2): books that haven't declared it refuse the write + /// with the upgrade remedy, journaling nothing. pub fn update_party( &mut self, id: ledger_core::PartyId,