test(store): S4.2's shapes open old books and wait for consent (P4b S4.2)
The 8.0.6 checklist for this slice, which adds fields on party.updated
(an email and two default terms) and the invoicing settings it guards:
1. books_written_under_older_rules_still_open now hand-writes a party's
email and terms and an invoicing setting through JournalWriter, and the
books still open, verify and replay them.
2. A new old-books fixture, p4_s42_settings_parties, was written by the
version before this slice (1bfce915, through the old-books-writer
harness) with a party, a vendor, two posted entries and three settings,
and its expected.json from that version's code. This version opens it,
verifies it and reports its figures identically.
3. books_p2_served_cut_over_whole gains a refused invoicing setting before
the go-ahead; the same call succeeds after it.
4. documents_v2_is_its_own_feature: books that declared every earlier
feature stop at its declaration as newer, wait for the upgrade, and
name exactly it in the upgrade.
No figure moves (item 5): settings and party facts enter no report.
Merge does not need the owner's go-ahead; the live books refuse the new
party and setting writes with the upgrade remedy until he gives it.
This commit is contained in:
1 parent
962b52c991
commit
98320b3018
5 files changed
+232
No files matched your search
@@ -722,6 +722,13 @@ fn books_p2_served_cut_over_whole() {
|
||||
json!({"decided_by": "Avery Quill",
|
||||
"changes": [{"key": "books.schedule_convention", "value": "monthly_even"}]}),
|
||||
),
|
||||
// An invoicing setting (documents_v2, P4b S4.2): an older version
|
||||
// refuses the key.
|
||||
(
|
||||
"books.set_setting",
|
||||
json!({"decided_by": "Avery Quill",
|
||||
"changes": [{"key": "invoicing.default_terms", "value": "Net 30"}]}),
|
||||
),
|
||||
(
|
||||
"books.add_account",
|
||||
json!({"code": "4900", "name": "Gain on sale", "account_type": "revenue",
|
||||
|
||||
@@ -858,6 +858,41 @@ mod tests {
|
||||
assert_eq!(up.adds(), vec!["the journal feature document_standing"]);
|
||||
}
|
||||
|
||||
/// The version before P4b S4.2 (every feature through
|
||||
/// `document_standing`) stops at a declaration of `documents_v2` as
|
||||
/// newer, never restoring a party's email or default terms as absent
|
||||
/// (or a document without its numbers, once S4.3 writes them); books
|
||||
/// that declared every earlier feature still wait for it (B-27, D-39).
|
||||
#[test]
|
||||
fn documents_v2_is_its_own_feature() {
|
||||
assert!(KNOWN_FEATURES.contains(&DOCUMENTS_V2));
|
||||
let before: Vec<&str> = KNOWN_FEATURES
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|f| *f != DOCUMENTS_V2)
|
||||
.collect();
|
||||
let l = line(
|
||||
FORMAT_SET,
|
||||
json!({"format": 2, "features": [DOCUMENT_STANDING, DOCUMENTS_V2]}),
|
||||
);
|
||||
match declared_knowing(&l, &before) {
|
||||
Err(StoreError::NewerJournal { need, .. }) => {
|
||||
assert_eq!(need, "journal features documents_v2")
|
||||
}
|
||||
other => panic!("expected NewerJournal, got {other:?}"),
|
||||
}
|
||||
let older = JournalFormat {
|
||||
format: 2,
|
||||
features: before.iter().map(|f| f.to_string()).collect(),
|
||||
};
|
||||
assert!(matches!(
|
||||
older.require_feature(DOCUMENTS_V2),
|
||||
Err(StoreError::FormatUpgradePending { .. })
|
||||
));
|
||||
let up = older.upgrade().expect("waits for it");
|
||||
assert_eq!(up.adds(), vec!["the journal feature documents_v2"]);
|
||||
}
|
||||
|
||||
/// This version's format: the newest it reads, every feature it
|
||||
/// knows, each once, sorted (what new books and an upgrade declare).
|
||||
#[test]
|
||||
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"assets": [],
|
||||
"bills": [],
|
||||
"entries": [
|
||||
{
|
||||
"date": "2026-03-01",
|
||||
"description": "Rent",
|
||||
"id": 1,
|
||||
"lines": [
|
||||
{
|
||||
"account": 3,
|
||||
"amount": {
|
||||
"amount": "1000.00",
|
||||
"currency": [
|
||||
85,
|
||||
83,
|
||||
68
|
||||
]
|
||||
},
|
||||
"side": "Debit"
|
||||
},
|
||||
{
|
||||
"account": 1,
|
||||
"amount": {
|
||||
"amount": "1000.00",
|
||||
"currency": [
|
||||
85,
|
||||
83,
|
||||
68
|
||||
]
|
||||
},
|
||||
"side": "Credit"
|
||||
}
|
||||
],
|
||||
"reversal_of": null,
|
||||
"status": "Posted"
|
||||
},
|
||||
{
|
||||
"date": "2026-01-01",
|
||||
"description": "Opening capital",
|
||||
"id": 2,
|
||||
"lines": [
|
||||
{
|
||||
"account": 1,
|
||||
"amount": {
|
||||
"amount": "500.00",
|
||||
"currency": [
|
||||
85,
|
||||
83,
|
||||
68
|
||||
]
|
||||
},
|
||||
"side": "Debit"
|
||||
},
|
||||
{
|
||||
"account": 2,
|
||||
"amount": {
|
||||
"amount": "500.00",
|
||||
"currency": [
|
||||
85,
|
||||
83,
|
||||
68
|
||||
]
|
||||
},
|
||||
"side": "Credit"
|
||||
}
|
||||
],
|
||||
"reversal_of": null,
|
||||
"status": "Posted"
|
||||
}
|
||||
],
|
||||
"entry_parties": [
|
||||
[
|
||||
1,
|
||||
[
|
||||
{
|
||||
"party_id": 1,
|
||||
"share": "derived"
|
||||
}
|
||||
]
|
||||
]
|
||||
],
|
||||
"invoices": [],
|
||||
"payroll_runs": [],
|
||||
"periods": [],
|
||||
"recurring": [],
|
||||
"schedules": [],
|
||||
"snapshot": {
|
||||
"Ok": {
|
||||
"balance_sheet": {
|
||||
"assets": "-500",
|
||||
"balanced": true,
|
||||
"current_earnings": "-1000",
|
||||
"equity": "-500",
|
||||
"liabilities": "0"
|
||||
},
|
||||
"profit_and_loss": {
|
||||
"expenses": "1000",
|
||||
"net_income": "-1000",
|
||||
"revenue": "0"
|
||||
},
|
||||
"trial_balance": {
|
||||
"lines": [
|
||||
{
|
||||
"code": "1000",
|
||||
"credit": "500",
|
||||
"debit": "0",
|
||||
"name": "Cash"
|
||||
},
|
||||
{
|
||||
"code": "3000",
|
||||
"credit": "500",
|
||||
"debit": "0",
|
||||
"name": "Capital"
|
||||
},
|
||||
{
|
||||
"code": "6000",
|
||||
"credit": "0",
|
||||
"debit": "1000",
|
||||
"name": "Rent"
|
||||
}
|
||||
],
|
||||
"total_credits": "1000",
|
||||
"total_debits": "1000"
|
||||
}
|
||||
}
|
||||
},
|
||||
"verify": {
|
||||
"Ok": null
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{"payload":{"base_currency":"USD"},"prev":"0000000000000000000000000000000000000000000000000000000000000000","seq":1,"ts":"2026-10-26T22:10:51Z","type":"store.created"}
|
||||
{"payload":{"features":["account_renamed","asset_basis","depreciation_catch_up","disposal_before_service","document_line_projects","document_standing","members_cumulative","month_reposting","party_creditors","payment_instruments","projects","require_tax_category","schedule_convention","tax_categories_gain_exempt","vendor_1099_decisions"],"format":2},"prev":"835d71063b78e376b5b909b42db729ed67d69b8523dd478c405be61044cbcb78","seq":2,"ts":"2026-10-26T22:10:51Z","type":"journal.format_set"}
|
||||
{"payload":{"account_type":"Asset","code":"1000","currency":"USD","id":1,"name":"Cash","parent":null,"placeholder":false},"prev":"a2c2087a01f0933c3a3b1d523a54e983217af0502aee9196d0f3c88147013c86","seq":3,"ts":"2026-10-26T22:10:52Z","type":"account.added"}
|
||||
{"payload":{"account_type":"Equity","code":"3000","currency":"USD","id":2,"name":"Capital","parent":null,"placeholder":false},"prev":"96c579e3e187ae847f574de3aad4a5f2e5c73563deeb7542dd177a84f18fe482","seq":4,"ts":"2026-10-26T22:10:52Z","type":"account.added"}
|
||||
{"payload":{"account_type":"Expense","code":"6000","currency":"USD","id":3,"name":"Rent","parent":null,"placeholder":false},"prev":"ccc5ea3d4404ac84d3c64e2859e94b7506fd98a6fc64d1e029c6f8303f67dba1","seq":5,"ts":"2026-10-26T22:10:52Z","type":"account.added"}
|
||||
{"payload":{"id":1,"name":"Acme Supply","roles":["contractor"]},"prev":"24f80e9ae9bb75da5af8114a30eff149eee30005f27bca7e10db3245d6894bfb","seq":6,"ts":"2026-10-26T22:10:52Z","type":"party.added"}
|
||||
{"payload":{"change":{"field":"website","value":"https://acme.example"},"id":1},"prev":"36b849235b1ec050c67df4ee00f6f89f11b5ecd630e07a47f1c17ee4df696115","seq":7,"ts":"2026-10-26T22:10:53Z","type":"party.updated"}
|
||||
{"payload":{"change":{"field":"address","value":"1 Main St"},"id":1},"prev":"04972a2b0b04ea96725b9eabc31afdda68f2b052787754a8ac2f5e1b94a3a35a","seq":8,"ts":"2026-10-26T22:10:54Z","type":"party.updated"}
|
||||
{"payload":{"change":{"field":"notes","value":"written before S4.2"},"id":1},"prev":"8e7c455384d4c442cb1ea6bbe89cb3aa257416fd288ef85b42218455fdb9002c","seq":9,"ts":"2026-10-26T22:10:54Z","type":"party.updated"}
|
||||
{"payload":{"id":1,"name":"Acme Supply"},"prev":"d3c708bf5e9c72cda200cfc4ec89b995db3a71798aa3de24dbe1d7588ca06dd5","seq":10,"ts":"2026-10-26T22:10:54Z","type":"vendor.added"}
|
||||
{"payload":{"date":"2026-03-01","description":"Rent","id":1},"prev":"ce9965f2c90588ca98f18aaa3bc687553414ab887cc3643b2c7c142309f6d6fa","seq":11,"ts":"2026-10-26T22:10:54Z","type":"entry.created"}
|
||||
{"payload":{"account":3,"amount":"1000.00","currency":"USD","id":1,"side":"debit"},"prev":"6a7c114116dbc7c6bc0a8e16ce0a6a47258989d17eb18a9a8f98e5b89633de2f","seq":12,"ts":"2026-10-26T22:10:54Z","type":"entry.line_added"}
|
||||
{"payload":{"account":1,"amount":"1000.00","currency":"USD","id":1,"side":"credit"},"prev":"554ebe7a6aa744a8207b9cd2281ed3ebec4dd248ff166cb639b0cb4e08d63a79","seq":13,"ts":"2026-10-26T22:10:54Z","type":"entry.line_added"}
|
||||
{"payload":{"date":"2026-03-01","description":"Rent","id":1,"lines":[{"account":3,"amount":"1000.00","currency":"USD","side":"debit"},{"account":1,"amount":"1000.00","currency":"USD","side":"credit"}],"reversal_of":null},"prev":"26b7f13ceac949b9ef63a25bc0b73c1a8d7639131c9ef6b25e0f85cbddf63a2d","seq":14,"ts":"2026-10-26T22:10:54Z","type":"entry.posted"}
|
||||
{"payload":{"entry_id":1,"vendor_id":1},"prev":"ef623789bd2695347c6a0bb20dd24132d686d6f01186cebf38b919ca6e89ca96","seq":15,"ts":"2026-10-26T22:10:55Z","type":"entry.vendor_set"}
|
||||
{"payload":{"date":"2026-01-01","description":"Opening capital","id":2},"prev":"c8f77ceb948f8d043d57d00c536e170fbda4bde7bb6b0c0492af2c976ecb2a08","seq":16,"ts":"2026-10-26T22:10:55Z","type":"entry.created"}
|
||||
{"payload":{"account":1,"amount":"500.00","currency":"USD","id":2,"side":"debit"},"prev":"068107796481cb2c83f49e0a69c0fd430c430fcea59d16664e19e1a009f63aeb","seq":17,"ts":"2026-10-26T22:10:55Z","type":"entry.line_added"}
|
||||
{"payload":{"account":2,"amount":"500.00","currency":"USD","id":2,"side":"credit"},"prev":"49febc3652a39e16a3920a1b505e5760f566954fb4273ca5db1e962c5b76999b","seq":18,"ts":"2026-10-26T22:10:55Z","type":"entry.line_added"}
|
||||
{"payload":{"date":"2026-01-01","description":"Opening capital","id":2,"lines":[{"account":1,"amount":"500.00","currency":"USD","side":"debit"},{"account":2,"amount":"500.00","currency":"USD","side":"credit"}],"reversal_of":null},"prev":"4eb247976658774447b34326ba512e2b697582b71dc46109aa59577dc706acd7","seq":19,"ts":"2026-10-26T22:10:55Z","type":"entry.posted"}
|
||||
{"payload":{"changes":[{"key":"company.name","value":"Fixture Labs LLC"},{"key":"policy.parties","value":"require"},{"key":"company.fiscal_year_start","value":"01-01"}],"source":{"decided_by":"Owner","decided_on":"2026-01-01"}},"prev":"0e2c7500174265af8f10abc5fa0671478c0cc9274ca85bae383e2733e4bbfe3e","seq":20,"ts":"2026-10-26T22:10:55Z","type":"settings.set"}
|
||||
@@ -1833,6 +1833,28 @@ fn books_written_under_older_rules_still_open() {
|
||||
"foreign_currency": "EUR", "foreign_amount": "100.00", "fx_rate": "1.0834567"}),
|
||||
)
|
||||
.unwrap();
|
||||
// P4b S4.2's new shapes, hand-written as a later version writes them:
|
||||
// a party's email and default terms, and an invoicing setting. This
|
||||
// reader takes them; the version before it stopped at the declaration.
|
||||
w.append("party.added", json!({"id": 1, "name": "Acme", "roles": []}))
|
||||
.unwrap();
|
||||
w.append(
|
||||
"party.updated",
|
||||
json!({"id": 1, "change": {"field": "email", "value": "billing@acme.example"}}),
|
||||
)
|
||||
.unwrap();
|
||||
w.append(
|
||||
"party.updated",
|
||||
json!({"id": 1, "change": {"field": "default_terms_as_customer",
|
||||
"value": {"kind": {"net": 30}, "text": "Net 30"}}}),
|
||||
)
|
||||
.unwrap();
|
||||
w.append(
|
||||
"settings.set",
|
||||
json!({"changes": [{"key": "invoicing.default_terms", "value": "2/10 net 30"}],
|
||||
"source": {"decided_by": "Owner", "decided_on": "2026-10-26"}}),
|
||||
)
|
||||
.unwrap();
|
||||
drop(w);
|
||||
// Opening rebuilds from the journal; it must accept all of it.
|
||||
let _ = std::fs::remove_file(dir.join(ledger_store::db_file()));
|
||||
@@ -1848,6 +1870,23 @@ fn books_written_under_older_rules_still_open() {
|
||||
lines[1].amount.amount,
|
||||
"108.34567".parse::<Decimal>().unwrap()
|
||||
);
|
||||
// The new shapes replay as written: the party's facts and the setting.
|
||||
let acme = store.ledger().get_party(1).expect("the party");
|
||||
assert_eq!(acme.email.as_deref(), Some("billing@acme.example"));
|
||||
assert_eq!(
|
||||
acme.default_terms_as_customer
|
||||
.as_ref()
|
||||
.map(|t| t.describe()),
|
||||
Some("Net 30".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
store
|
||||
.ledger()
|
||||
.settings()
|
||||
.get("invoicing.default_terms")
|
||||
.and_then(|r| r.value.as_str()),
|
||||
Some("2/10 net 30")
|
||||
);
|
||||
}
|
||||
|
||||
/// The books an earlier wanderbooks wrote, one directory each under
|
||||
|
||||
Reference in new issue
Block a user