feat(store): draft states and discards are journaled, posted_seq is kept (P4)
Accounting reason: a draft that waits on its receipt, and one thrown
away, are facts about the books' working papers that must survive a
restart and replay identically, or a discarded draft would come back and
hold its period open. entry.draft_state_set and entry.draft_discarded are
drafts_v2's (an older version stops at the declaration instead of
misreading them); no figure moves. Schema 19 (taken at merge as the next
after the current last): entries.posted_seq, and a draft_states table
(drafts are not rows of entries, so the plan's entries.draft_state column
has nowhere to live). posted_seq is the journal line that posted each
entry (entry.posted, entry.reversed, the line that closed a period or
posted a schedule month), identical live and on replay.
8.0.6: a hand-written line of each new shape in
books_written_under_older_rules_still_open; fixture old-books/p4_before
written by 25b52580 (hand entries posted late, a reversal, a document, a
schedule month, a recurring draft, open drafts, a close) opens to the
same figures (serialized entries omit a building state, so no golden
moved); drafts_v2_is_its_own_feature; the fuzzer and GUI journal know the
types.
Tests: draft_states_replay_and_project, drafts_v2_waits_for_the_upgrade,
posted_seq_is_the_posting_lines_seq, an_older_version_stops_at_the_draft_events.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
1 parent
be8bcf340e
commit
28d46332e4
18 files changed
+1455
-24
No files matched your search
@@ -11,6 +11,9 @@
|
||||
# the revision before P4b S5)
|
||||
# cargo run --bin p2_1099 -- OUT (p2_vendor_1099: P2's 1099 marks, run
|
||||
# against 9c99a0cf, the revision before P3)
|
||||
# cargo run --bin p4_before -- OUT (p4_before: run against 25b52580, the
|
||||
# revision before P4: hand entries posted
|
||||
# late, a reversal, open drafts, a close)
|
||||
# cargo run --bin expect -- OUT (expected.json beside each journal)
|
||||
# and copy each journal.jsonl, blobs/ and expected.json into old-books/.
|
||||
# x_member_closes was written later, the same way, by main 00226a4c
|
||||
|
||||
@@ -6,7 +6,7 @@ fn main() {
|
||||
names.sort();
|
||||
for dir in names {
|
||||
let n = dir.file_name().unwrap().to_string_lossy().to_string();
|
||||
let _ = std::fs::remove_file(dir.join(ledger_store::DB_FILE));
|
||||
let _ = std::fs::remove_file(dir.join(ledger_store::db_file()));
|
||||
let s = match ledger_store::Store::open(&dir) { Ok(s) => s, Err(e) => { println!("FAILS {n}: {e}"); continue } };
|
||||
let l = s.ledger();
|
||||
let entries: Vec<_> = (1..l.next_entry_id()).filter_map(|id| l.get_entry(id)).collect();
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
//! Writes `p4_before`: books the version before P4 (main 25b52580) wrote
|
||||
//! through its Store API: hand entries (one posted late with an early
|
||||
//! date), a reversal, an invoice and its payment, a schedule month, a
|
||||
//! recurring entry with a generated draft, two open drafts (one with
|
||||
//! lines, one empty), and a closed January. No draft state, discard or
|
||||
//! posting sequence exists in that version; this version opens the same
|
||||
//! journal and must report the same figures.
|
||||
use chrono::NaiveDate;
|
||||
use ledger_core::schedules::{Frequency, NewRecurring, NewSchedule, ScheduleConvention, ScheduleKind};
|
||||
use ledger_core::{AccountType, Currency, DocumentLine, Money, Side};
|
||||
use ledger_store::Store;
|
||||
use rust_decimal::Decimal;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn d(y: i32, m: u32, day: u32) -> NaiveDate {
|
||||
NaiveDate::from_ymd_opt(y, m, day).unwrap()
|
||||
}
|
||||
fn cents(c: i64) -> Decimal {
|
||||
Decimal::new(c, 2)
|
||||
}
|
||||
fn usd(c: i64) -> Money {
|
||||
Money::new(cents(c), Currency::USD)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::args().nth(1).unwrap());
|
||||
let dir = root.join("p4_before");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let mut s = Store::create(&dir, Currency::USD).unwrap();
|
||||
let bank = s.add_account("1000", "Bank", AccountType::Asset, None, false).unwrap();
|
||||
let prepaid = s.add_account("1300", "Prepaid expenses", AccountType::Asset, None, false).unwrap();
|
||||
let capital = s.add_account("3000", "Capital", AccountType::Equity, None, false).unwrap();
|
||||
let sales = s.add_account("4000", "Sales", AccountType::Revenue, None, false).unwrap();
|
||||
let rent = s.add_account("6000", "Rent", AccountType::Expense, None, false).unwrap();
|
||||
let software = s.add_account("6100", "Software", AccountType::Expense, None, false).unwrap();
|
||||
let post = |s: &mut Store, date: NaiveDate, what: &str, dr: u64, cr: u64, c: i64| {
|
||||
let e = s.create_entry(date, what).unwrap();
|
||||
s.add_line(e, dr, Side::Debit, usd(c)).unwrap();
|
||||
s.add_line(e, cr, Side::Credit, usd(c)).unwrap();
|
||||
e
|
||||
};
|
||||
let seed = post(&mut s, d(2026, 1, 1), "Opening capital", bank, capital, 100_000);
|
||||
s.post_entry(seed).unwrap();
|
||||
// Drafted first, posted last, dated first: posting order is not date order.
|
||||
let late = post(&mut s, d(2026, 1, 3), "Domain renewal", software, bank, 1_800);
|
||||
let rent_jan = post(&mut s, d(2026, 1, 5), "January rent", rent, bank, 50_000);
|
||||
s.post_entry(rent_jan).unwrap();
|
||||
s.post_entry(late).unwrap();
|
||||
let wrong = post(&mut s, d(2026, 1, 12), "Rent paid twice", rent, bank, 50_000);
|
||||
s.post_entry(wrong).unwrap();
|
||||
s.reverse_entry(wrong, d(2026, 1, 14), "Entered twice").unwrap();
|
||||
// A document and its payment.
|
||||
let customer = s.add_customer("Fernhill Labs").unwrap();
|
||||
let inv = s
|
||||
.create_invoice(
|
||||
customer,
|
||||
d(2026, 1, 10),
|
||||
d(2026, 2, 9),
|
||||
vec![DocumentLine {
|
||||
description: "Design".into(),
|
||||
amount: cents(120_000),
|
||||
class_id: None,
|
||||
account: None,
|
||||
quantity: None,
|
||||
unit_price: None,
|
||||
}],
|
||||
)
|
||||
.unwrap();
|
||||
s.receive_payment(inv, d(2026, 1, 20), cents(40_000), bank).unwrap();
|
||||
// A schedule month.
|
||||
let sched = s
|
||||
.register_schedule(NewSchedule {
|
||||
description: "Annual insurance".into(),
|
||||
kind: ScheduleKind::PrepaidExpense,
|
||||
total: usd(120_000),
|
||||
start: d(2026, 1, 1),
|
||||
months: 12,
|
||||
balance_account: prepaid,
|
||||
pl_account: rent,
|
||||
convention: ScheduleConvention::MonthlyEven,
|
||||
})
|
||||
.unwrap();
|
||||
let paid = post(&mut s, d(2026, 1, 2), "Insurance premium", prepaid, bank, 120_000);
|
||||
s.post_entry(paid).unwrap();
|
||||
s.post_schedule_month(sched, d(2026, 1, 31)).unwrap();
|
||||
// A recurring entry with one generated draft (February).
|
||||
s.register_recurring(NewRecurring {
|
||||
description: "Figma".into(),
|
||||
lines: vec![(software, Side::Debit, usd(1_500)), (bank, Side::Credit, usd(1_500))],
|
||||
frequency: Frequency::Monthly,
|
||||
start: d(2026, 2, 10),
|
||||
end: None,
|
||||
})
|
||||
.unwrap();
|
||||
s.run_recurring(d(2026, 2, 28)).unwrap();
|
||||
// Two open drafts dated in February; January closes around them.
|
||||
let open = post(&mut s, d(2026, 2, 12), "Coworking day pass", rent, bank, 3_500);
|
||||
let _ = open;
|
||||
let _empty = s.create_entry(d(2026, 2, 14), "Placeholder").unwrap();
|
||||
let jan = s.create_period("2026-01", d(2026, 1, 1), d(2026, 1, 31)).unwrap();
|
||||
s.close_period(jan).unwrap();
|
||||
let _ = sales;
|
||||
s.verify().unwrap();
|
||||
println!("WROTE {:?}", dir);
|
||||
}
|
||||
Reference in new issue
Block a user