fix: books with an asset linked to a since-reversed entry failed to open (the projection read-back re-checked the link against today's books); links restore as recorded, Ledger::acquisition_reversed reports ones to relink; regression test
check / check (push) Waiting to run
check / check (push) Waiting to run
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
27e78a0ed5
commit
4826a39a88
3 files changed
+63
-1
No files matched your search
@@ -1585,6 +1585,36 @@ impl Ledger {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Restore a link recorded earlier (a projection read-back): it was
|
||||||
|
/// checked when made, and a later correction (reversing that entry)
|
||||||
|
/// doesn't undo it; it only becomes a link to fix, by linking the
|
||||||
|
/// replacement. Checks only that the asset and entry exist. (Re-checking
|
||||||
|
/// against today's books refused to open them once a linked entry was
|
||||||
|
/// reversed, 2026-10-15.)
|
||||||
|
pub fn restore_asset_acquisition(
|
||||||
|
&mut self,
|
||||||
|
id: AssetId,
|
||||||
|
entry: EntryId,
|
||||||
|
) -> Result<(), LedgerError> {
|
||||||
|
if !self.entries.contains_key(&entry) {
|
||||||
|
return Err(LedgerError::UnknownEntry(entry));
|
||||||
|
}
|
||||||
|
self.assets
|
||||||
|
.get_mut(&id)
|
||||||
|
.ok_or(LedgerError::UnknownAsset(id))?
|
||||||
|
.acquired_by = Some(entry);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether an asset's acquisition link points at an entry that has
|
||||||
|
/// since been reversed (to relink the replacement).
|
||||||
|
pub fn acquisition_reversed(&self, id: AssetId) -> bool {
|
||||||
|
self.assets
|
||||||
|
.get(&id)
|
||||||
|
.and_then(|a| a.acquired_by)
|
||||||
|
.is_some_and(|e| self.entries.values().any(|x| x.reversal_of() == Some(e)))
|
||||||
|
}
|
||||||
|
|
||||||
/// The validation behind [`Self::set_asset_acquisition`].
|
/// The validation behind [`Self::set_asset_acquisition`].
|
||||||
pub fn check_asset_acquisition(&self, id: AssetId, entry: EntryId) -> Result<(), LedgerError> {
|
pub fn check_asset_acquisition(&self, id: AssetId, entry: EntryId) -> Result<(), LedgerError> {
|
||||||
let asset = self.assets.get(&id).ok_or(LedgerError::UnknownAsset(id))?;
|
let asset = self.assets.get(&id).ok_or(LedgerError::UnknownAsset(id))?;
|
||||||
|
|||||||
@@ -2981,7 +2981,7 @@ impl Store {
|
|||||||
let links = qstmt.query_map([], |r| Ok((r.get::<_, i64>(0)?, r.get::<_, i64>(1)?)))?;
|
let links = qstmt.query_map([], |r| Ok((r.get::<_, i64>(0)?, r.get::<_, i64>(1)?)))?;
|
||||||
for row in links {
|
for row in links {
|
||||||
let (id, entry) = row?;
|
let (id, entry) = row?;
|
||||||
ledger.set_asset_acquisition(id as u64, entry as u64)?;
|
ledger.restore_asset_acquisition(id as u64, entry as u64)?;
|
||||||
}
|
}
|
||||||
// Payroll runs: id order; lines in insertion order. Amounts are
|
// Payroll runs: id order; lines in insertion order. Amounts are
|
||||||
// TEXT in SQLite, so they parse here with exact Decimal math.
|
// TEXT in SQLite, so they parse here with exact Decimal math.
|
||||||
|
|||||||
@@ -798,3 +798,35 @@ fn an_asset_is_corrected_until_depreciation_is_posted() {
|
|||||||
Err(StoreError::AssetDepreciationPosted(_))
|
Err(StoreError::AssetDepreciationPosted(_))
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Regression (2026-10-15): reversing an asset's linked acquisition entry
|
||||||
|
/// (a correction) must not stop the books from opening. The link was valid
|
||||||
|
/// when made; the books open and verify, and the reversed link is reported
|
||||||
|
/// for relinking.
|
||||||
|
#[test]
|
||||||
|
fn reversing_a_linked_acquisition_still_opens_and_verifies() {
|
||||||
|
let (dir, mut s, a, x, _) = fixture("relink");
|
||||||
|
let post = |s: &mut Store| {
|
||||||
|
let e = s.create_entry(date(2026, 1, 1), "contributed").unwrap();
|
||||||
|
s.add_line(e, a.cost, ledger_core::Side::Debit, usd("1200.00"))
|
||||||
|
.unwrap();
|
||||||
|
s.add_line(e, a.bank, ledger_core::Side::Credit, usd("1200.00"))
|
||||||
|
.unwrap();
|
||||||
|
s.post_entry(e).unwrap();
|
||||||
|
e
|
||||||
|
};
|
||||||
|
let first = post(&mut s);
|
||||||
|
s.set_asset_acquisition(x, first).unwrap();
|
||||||
|
s.reverse_entry(first, date(2026, 1, 2), "corrected")
|
||||||
|
.unwrap();
|
||||||
|
assert!(s.ledger().acquisition_reversed(x));
|
||||||
|
s.verify().unwrap();
|
||||||
|
drop(s);
|
||||||
|
let mut s = Store::open(&dir).expect("books with a reversed link open");
|
||||||
|
s.verify().unwrap();
|
||||||
|
let second = post(&mut s);
|
||||||
|
s.set_asset_acquisition(x, second).unwrap();
|
||||||
|
assert!(!s.ledger().acquisition_reversed(x));
|
||||||
|
drop(s);
|
||||||
|
Store::open(&dir).unwrap().verify().unwrap();
|
||||||
|
}
|
||||||
Reference in new issue
Block a user