store: refusal property reaches every success path; two engine fixes
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
The refusal property now starts from real books and adds compound commands with valid arguments (book an entry, register an asset or schedule, raise an invoice or bill, close a month to members), so every command's success path is exercised. Compound commands checkpoint after each step, so the law is checked against the one command refused. STORE_CASES sets the case count (256 by default). It found two bugs: - run_recurring created drafts dated inside a closed period (B-12). generate_recurring now refuses them, atomically; the store leaves them ungenerated and books.run_recurring reports them in skipped_closed_period for a human to decide. Replay uses replay_generate_recurring so older journals still open. The store also journals before mutating the ledger here, as everywhere else. - Disposing an asset for no proceeds (scrapping it) journaled a catch-up depreciation entry and a half-built draft with a 0.00 line, then refused. Zero lines are now left out (Store::disposal_lines, shared with the MCP preview), and the whole disposal is rehearsed on a clone before anything is written. 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
1212367138
commit
22d1a3d449
6 files changed
+633
-91
No files matched your search
@@ -1887,7 +1887,7 @@ impl BooksServer {
|
||||
|
||||
#[tool(
|
||||
name = "books.run_recurring",
|
||||
description = "Turn every recurring entry's occurrences on or before `through` (default today) into drafts. Idempotent: an occurrence generates once. Each draft then goes through the normal flow: attach the receipt, preview, post."
|
||||
description = "Turn every recurring entry's occurrences on or before `through` (default today) into drafts. Idempotent: an occurrence generates once. Occurrences dated in a closed period are not generated; they come back in `skipped_closed_period` for the human to decide. Each draft then goes through the normal flow: attach the receipt, preview, post."
|
||||
)]
|
||||
async fn run_recurring(
|
||||
&self,
|
||||
@@ -1908,7 +1908,19 @@ impl BooksServer {
|
||||
entry_id: entry,
|
||||
})
|
||||
.collect();
|
||||
Ok(Json(RunRecurringOut { drafts }))
|
||||
let skipped_closed_period = store
|
||||
.ledger()
|
||||
.recurring_in_closed_periods(through)
|
||||
.into_iter()
|
||||
.map(|(id, date)| RecurringSkippedOut {
|
||||
recurring_id: id,
|
||||
date: date.to_string(),
|
||||
})
|
||||
.collect();
|
||||
Ok(Json(RunRecurringOut {
|
||||
drafts,
|
||||
skipped_closed_period,
|
||||
}))
|
||||
}
|
||||
|
||||
#[tool(
|
||||
@@ -2725,28 +2737,16 @@ impl BooksServer {
|
||||
|
||||
// The would-be disposal entry's lines, for the preview and the
|
||||
// receipt-policy check.
|
||||
let mut raw: Vec<(AccountId, Side, Money)> = vec![
|
||||
(
|
||||
asset.accum_depr_account,
|
||||
Side::Debit,
|
||||
Money::new(preview.accumulated, base),
|
||||
),
|
||||
(proceeds_account, Side::Debit, proceeds),
|
||||
(asset.cost_account, Side::Credit, asset.cost),
|
||||
];
|
||||
if preview.gain_loss > Decimal::ZERO {
|
||||
raw.push((
|
||||
gain_loss_account,
|
||||
Side::Credit,
|
||||
Money::new(preview.gain_loss, base),
|
||||
));
|
||||
} else if preview.gain_loss < Decimal::ZERO {
|
||||
raw.push((
|
||||
gain_loss_account,
|
||||
Side::Debit,
|
||||
Money::new(-preview.gain_loss, base),
|
||||
));
|
||||
}
|
||||
let raw: Vec<(AccountId, Side, Money)> = Store::disposal_lines(
|
||||
&asset,
|
||||
&preview,
|
||||
proceeds.amount,
|
||||
proceeds_account,
|
||||
gain_loss_account,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|(a, side, amount)| (a, side, Money::new(amount, base)))
|
||||
.collect();
|
||||
// Disposals are money movement: the bill of sale is the receipt.
|
||||
let chart = store.ledger().chart();
|
||||
let code_of = |id: AccountId| {
|
||||
|
||||
Reference in new issue
Block a user