fix(core,store): refuse control accounts of the wrong type, with a reason
Invoices, bills, payments, payroll, and closes post to accounts the engine finds by code (1200, 2000, 4000, 6999, 3999, 6100-2150). If an account with that code existed it was used whatever its type: a 1200 "Customer Deposits" liability would have taken receivables. Now (Stephen, 2026-10-13: never force an imported chart into our numbering) the engine refuses with ControlAccountMismatch, naming the code, the account, its type, the role, and the type needed, so the agent can work it out with the human. A placeholder is refused too. Every command checks all its control accounts before changing anything (Ledger::check_controls, spec + SOURCE in its doc comment), so a refusal never leaves a half-made account behind. One table of control accounts (ledger_core::RECEIVABLES ... PAYROLL) replaces the codes duplicated across the engine and the store. Tests (spec-derived, all assert the books are untouched after the refusal): an_invoice_refuses_a_1200_that_is_not_receivables, an_invoice_refuses_a_placeholder_1200, an_invoice_creates_nothing_when_its_second_account_is_wrong, a_bill_refuses_a_2000_that_is_not_payables, a_close_refuses_a_3999_that_is_not_equity. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
1 parent
ad572ae89d
commit
e2bebce051
5 files changed
+370
-122
No files matched your search
@@ -75,6 +75,33 @@ pub enum LedgerError {
|
||||
#[error("entry {0} closes a period and cannot be reversed")]
|
||||
ClosingEntryIrreversible(u64),
|
||||
|
||||
/// The engine posts invoices, bills, payroll, and closes to accounts it
|
||||
/// finds by code (1200 receivables, 2000 payables, 4000 revenue, ...).
|
||||
/// An account with that code exists but is the wrong type, or a
|
||||
/// placeholder, so the engine refuses rather than post there or force
|
||||
/// the chart into its numbering (Stephen, 2026-10-13). The bookkeeper
|
||||
/// decides with the human how to handle it.
|
||||
#[error(
|
||||
"account {code} \"{name}\" is {}, but {role} needs account {code} to be a postable {expected:?} account; \
|
||||
wanderbooks won't post {role} there. Decide with the books' owner how to handle it \
|
||||
(for instance, renumber that account so {code} is free, if they agree)",
|
||||
if *placeholder { format!("a placeholder {found:?} account") } else { format!("a {found:?} account") }
|
||||
)]
|
||||
ControlAccountMismatch {
|
||||
/// The code the engine looks for.
|
||||
code: String,
|
||||
/// The existing account's name.
|
||||
name: String,
|
||||
/// What the engine wanted it for, e.g. "accounts receivable".
|
||||
role: String,
|
||||
/// The type that role needs.
|
||||
expected: crate::AccountType,
|
||||
/// The existing account's type.
|
||||
found: crate::AccountType,
|
||||
/// Whether the existing account is a placeholder.
|
||||
placeholder: bool,
|
||||
},
|
||||
|
||||
/// The referenced journal entry does not exist.
|
||||
#[error("entry {0} does not exist")]
|
||||
UnknownEntry(u64),
|
||||
|
||||
Reference in new issue
Block a user