diff --git a/scripts/scenario/scenarios/ledger/a-rule-and-the-category-it-sets.scenario b/scripts/scenario/scenarios/ledger/a-rule-and-the-category-it-sets.scenario index 600dab56..91f43f28 100644 --- a/scripts/scenario/scenarios/ledger/a-rule-and-the-category-it-sets.scenario +++ b/scripts/scenario/scenarios/ledger/a-rule-and-the-category-it-sets.scenario @@ -28,8 +28,10 @@ # The server records what it is told here -- it is an audit stamp, not an # optimistic-concurrency check (that is `UpdateRule`'s `expectedVersion`). # -# Wire shapes: RuleTrigger::DescriptionContains is 0 and RuleAction:: -# SetCategory is 0. CreateRule, CreateCategory, CreateBudget and SetBudgetLimit +# Wire shapes: enums travel as their enumerator names, so RuleTrigger is +# "DescriptionContains" and RuleAction is "SetCategory" -- each has one +# enumerator today, and neither is an integer on the wire. +# CreateRule, CreateCategory, CreateBudget and SetBudgetLimit # each answer with a bare id, so their captures read `$`. # # ── How this file stays re-runnable ───────────────────────────────────── @@ -53,10 +55,10 @@ client books model=LedgerModel session principal=$who token=$token # The nonce: the first account's own id, which no earlier run had. -do OpenAccount ledgerId=1 name="Ruled cash" kind=0 currency=1 +do OpenAccount ledgerId=1 name="Ruled cash" kind="Asset" currency="EUR" expect ok capture cash=$.id -do OpenAccount ledgerId=1 name="Ruled spend" kind=1 currency=1 +do OpenAccount ledgerId=1 name="Ruled spend" kind="Expense" currency="EUR" expect ok capture spend=$.id client budgets model=BudgetModel @@ -71,7 +73,7 @@ expect ok capture household=$ client rules model=RuleModel session principal=$who token=$token -do CreateRule ledgerId=1 trigger=0 matchText="SUPERMARKET-$cash" action=0 actionValue="Ruled household $cash" +do CreateRule ledgerId=1 trigger="DescriptionContains" matchText="SUPERMARKET-$cash" action="SetCategory" actionValue="Ruled household $cash" expect ok capture rule=$ # ── Two budgets, so both sides of the move are observable ──────────────── @@ -83,10 +85,10 @@ expect ok capture groceryBudget=$ do CreateBudget ledgerId=1 name="Ruled household budget $cash" categoryId=$household expect ok capture householdBudget=$ -do SetBudgetLimit budgetId=$groceryBudget month="2026-08" limit={"num":100000,"den":1,"dp":2} currency=1 +do SetBudgetLimit budgetId=$groceryBudget month="2026-08" limit={"num":100000,"den":1,"dp":2} currency="EUR" expect ok -do SetBudgetLimit budgetId=$householdBudget month="2026-08" limit={"num":100000,"den":1,"dp":2} currency=1 +do SetBudgetLimit budgetId=$householdBudget month="2026-08" limit={"num":100000,"den":1,"dp":2} currency="EUR" expect ok # ── The account starts on groceries, stamped with the rule that put it there @@ -97,7 +99,7 @@ expect ok # ── A transaction the rule does *not* match ───────────────────────────── do StoreTransaction ledgerId=1 description="corner bakery" date="2026-08-01T00:00:00.000Z" legs=[{"accountId":$cash,"amount":{"num":-1000,"den":1,"dp":2}},{"accountId":$spend,"amount":{"num":1000,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$spend,\"name\":\"Ruled spend\",\"kind\":1,\"currency\":1,\"balance\":.\"num\":1000" +expect ok field accounts ~ "\"id\":$spend,\"name\":\"Ruled spend\",\"kind\":\"Expense\",\"currency\":\"EUR\",\"balance\":.\"num\":1000" use budgets @@ -111,7 +113,7 @@ expect ok field spent.num == 0 use books do StoreTransaction ledgerId=1 description="SUPERMARKET-$cash run" date="2026-08-02T00:00:00.000Z" legs=[{"accountId":$cash,"amount":{"num":-2000,"den":1,"dp":2}},{"accountId":$spend,"amount":{"num":2000,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$spend,\"name\":\"Ruled spend\",\"kind\":1,\"currency\":1,\"balance\":.\"num\":3000" +expect ok field accounts ~ "\"id\":$spend,\"name\":\"Ruled spend\",\"kind\":\"Expense\",\"currency\":\"EUR\",\"balance\":.\"num\":3000" use budgets diff --git a/scripts/scenario/scenarios/ledger/a-second-editor-on-the-same-book.scenario b/scripts/scenario/scenarios/ledger/a-second-editor-on-the-same-book.scenario index 321ea285..552f2226 100644 --- a/scripts/scenario/scenarios/ledger/a-second-editor-on-the-same-book.scenario +++ b/scripts/scenario/scenarios/ledger/a-second-editor-on-the-same-book.scenario @@ -32,11 +32,11 @@ session principal=$bob token=$bobToken # ── She opens the accounts ───────────────────────────────────────────── use hers -do OpenAccount ledgerId=1 name="Shared book cash" kind=0 currency=1 +do OpenAccount ledgerId=1 name="Shared book cash" kind="Asset" currency="EUR" expect ok capture cash=$.id expect ok field balance.num == 0 -do OpenAccount ledgerId=1 name="Shared book spend" kind=1 currency=1 +do OpenAccount ledgerId=1 name="Shared book spend" kind="Expense" currency="EUR" expect ok capture spend=$.id # ── He sees them, on his own connection ──────────────────────────────── @@ -45,50 +45,50 @@ expect ok capture spend=$.id use his do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Shared book cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":0" -expect ok field accounts ~ "\"id\":$spend,\"name\":\"Shared book spend\",\"kind\":1,\"currency\":1,\"balance\":.\"num\":0" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Shared book cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":0" +expect ok field accounts ~ "\"id\":$spend,\"name\":\"Shared book spend\",\"kind\":\"Expense\",\"currency\":\"EUR\",\"balance\":.\"num\":0" # ── He posts ─────────────────────────────────────────────────────────── do StoreTransaction ledgerId=1 description="his entry" date="2026-08-01T00:00:00.000Z" legs=[{"accountId":$cash,"amount":{"num":-2000,"den":1,"dp":2}},{"accountId":$spend,"amount":{"num":2000,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Shared book cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-2000" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Shared book cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-2000" # ── ...and she sees it without reopening anything ───────────────────── use hers do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Shared book cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-2000" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Shared book cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-2000" # ── She posts on top of his ─────────────────────────────────────────── do StoreTransaction ledgerId=1 description="her entry" date="2026-08-02T00:00:00.000Z" legs=[{"accountId":$cash,"amount":{"num":-500,"den":1,"dp":2}},{"accountId":$spend,"amount":{"num":500,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Shared book cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-2500" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Shared book cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-2500" use his do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Shared book cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-2500" -expect ok field accounts ~ "\"id\":$spend,\"name\":\"Shared book spend\",\"kind\":1,\"currency\":1,\"balance\":.\"num\":2500" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Shared book cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-2500" +expect ok field accounts ~ "\"id\":$spend,\"name\":\"Shared book spend\",\"kind\":\"Expense\",\"currency\":\"EUR\",\"balance\":.\"num\":2500" # ── An account he opens is hers to post against ─────────────────────── -do OpenAccount ledgerId=1 name="Shared book third" kind=1 currency=1 +do OpenAccount ledgerId=1 name="Shared book third" kind="Expense" currency="EUR" expect ok capture third=$.id use hers do StoreTransaction ledgerId=1 description="against his account" date="2026-08-03T00:00:00.000Z" legs=[{"accountId":$cash,"amount":{"num":-750,"den":1,"dp":2}},{"accountId":$third,"amount":{"num":750,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$third,\"name\":\"Shared book third\",\"kind\":1,\"currency\":1,\"balance\":.\"num\":750" -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Shared book cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-3250" +expect ok field accounts ~ "\"id\":$third,\"name\":\"Shared book third\",\"kind\":\"Expense\",\"currency\":\"EUR\",\"balance\":.\"num\":750" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Shared book cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-3250" # ── An exactly-once key is scoped to the ledger, not to the connection ─ # She spends the key; his identical send is the retry, and it must not post. do StoreTransaction ledgerId=1 description="keyed by her" date="2026-08-04T00:00:00.000Z" opId="shared-$cash" legs=[{"accountId":$cash,"amount":{"num":-100,"den":1,"dp":2}},{"accountId":$spend,"amount":{"num":100,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Shared book cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-3350" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Shared book cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-3350" use his do StoreTransaction ledgerId=1 description="keyed by her" date="2026-08-04T00:00:00.000Z" opId="shared-$cash" legs=[{"accountId":$cash,"amount":{"num":-100,"den":1,"dp":2}},{"accountId":$spend,"amount":{"num":100,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Shared book cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-3350" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Shared book cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-3350" use hers do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Shared book cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-3350" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Shared book cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-3350" diff --git a/scripts/scenario/scenarios/ledger/a-signed-in-session-is-what-authorises-work.scenario b/scripts/scenario/scenarios/ledger/a-signed-in-session-is-what-authorises-work.scenario index 7b091175..6e20926e 100644 --- a/scripts/scenario/scenarios/ledger/a-signed-in-session-is-what-authorises-work.scenario +++ b/scripts/scenario/scenarios/ledger/a-signed-in-session-is-what-authorises-work.scenario @@ -37,7 +37,7 @@ expect ok field principal == accountant # ── No session at all: every model refuses ───────────────────────────── client books model=LedgerModel -do OpenAccount ledgerId=1 name="Authorised cash" kind=0 currency=1 +do OpenAccount ledgerId=1 name="Authorised cash" kind="Asset" currency="EUR" expect err message == "unauthorized" client budgets model=BudgetModel @@ -47,27 +47,27 @@ expect err message == "unauthorized" client rules model=RuleModel -do CreateRule ledgerId=1 trigger=0 matchText="anything" action=0 actionValue="Authorised category" +do CreateRule ledgerId=1 trigger="DescriptionContains" matchText="anything" action="SetCategory" actionValue="Authorised category" expect err message == "unauthorized" # ── A token shaped like one, signed by nobody ────────────────────────── use books session principal=accountant token="eyJwcmluY2lwYWwiOiJhY2NvdW50YW50In0.not-a-real-signature" -do OpenAccount ledgerId=1 name="Authorised cash" kind=0 currency=1 +do OpenAccount ledgerId=1 name="Authorised cash" kind="Asset" currency="EUR" expect err message == "unauthorized" # ── The real one ─────────────────────────────────────────────────────── session principal=$who token=$token -do OpenAccount ledgerId=1 name="Authorised cash" kind=0 currency=1 +do OpenAccount ledgerId=1 name="Authorised cash" kind="Asset" currency="EUR" expect ok capture cash=$.id -do OpenAccount ledgerId=1 name="Authorised spend" kind=1 currency=1 +do OpenAccount ledgerId=1 name="Authorised spend" kind="Expense" currency="EUR" expect ok capture spend=$.id do StoreTransaction ledgerId=1 description="authorised work" date="2026-08-01T00:00:00.000Z" legs=[{"accountId":$cash,"amount":{"num":-800,"den":1,"dp":2}},{"accountId":$spend,"amount":{"num":800,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Authorised cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-800" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Authorised cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-800" # ── The same token authorises the other models ──────────────────────── use budgets @@ -82,7 +82,7 @@ expect ok use rules session principal=$who token=$token -do CreateRule ledgerId=1 trigger=0 matchText="authorised" action=0 actionValue="Authorised category" +do CreateRule ledgerId=1 trigger="DescriptionContains" matchText="authorised" action="SetCategory" actionValue="Authorised category" expect ok capture rule=$ do UpdateRule ruleId=$rule matchText="authorised work" actionValue="Authorised category" expectedVersion=1 @@ -92,7 +92,7 @@ expect ok field version == 2 use books do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$spend,\"name\":\"Authorised spend\",\"kind\":1,\"currency\":1,\"balance\":.\"num\":800" +expect ok field accounts ~ "\"id\":$spend,\"name\":\"Authorised spend\",\"kind\":\"Expense\",\"currency\":\"EUR\",\"balance\":.\"num\":800" # ── Dropping the session closes the doors again ─────────────────────── session principal="" token="" @@ -107,4 +107,4 @@ expect err message == "unauthorized" session principal=$who token=$token do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Authorised cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-800" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Authorised cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-800" diff --git a/scripts/scenario/scenarios/ledger/accounts-of-every-kind.scenario b/scripts/scenario/scenarios/ledger/accounts-of-every-kind.scenario index 74c06e1d..5a077bfc 100644 --- a/scripts/scenario/scenarios/ledger/accounts-of-every-kind.scenario +++ b/scripts/scenario/scenarios/ledger/accounts-of-every-kind.scenario @@ -12,8 +12,10 @@ # for a liability, so nothing here assumes a sign convention the model does # not have. # -# Wire shapes: AccountKind::Asset is 0, ::Expense 1, ::Revenue 2, -# ::Liability 3; Currency::USD is 0 and ::EUR is 1. Both travel as integers. +# Wire shapes: both enums travel as their *enumerator names*, not as their +# underlying integers (morph#444 gave each a `glz::meta`/`glz::enumerate`) -- +# AccountKind is "Asset"/"Expense"/"Revenue"/"Liability" and Currency is its +# ISO code, "USD"/"EUR"/"JPY"/"KRW". # # Re-runnable: every account is opened here, so the balances are this run's. @@ -25,36 +27,36 @@ expect ok capture who=$.principal client books model=LedgerModel session principal=$who token=$token -do OpenAccount ledgerId=1 name="Kinds asset" kind=0 currency=1 +do OpenAccount ledgerId=1 name="Kinds asset" kind="Asset" currency="EUR" expect ok capture asset=$.id -expect ok field kind == 0 +expect ok field kind == "Asset" expect ok field balance.num == 0 -do OpenAccount ledgerId=1 name="Kinds expense" kind=1 currency=1 +do OpenAccount ledgerId=1 name="Kinds expense" kind="Expense" currency="EUR" expect ok capture expense=$.id -expect ok field kind == 1 +expect ok field kind == "Expense" -do OpenAccount ledgerId=1 name="Kinds revenue" kind=2 currency=1 +do OpenAccount ledgerId=1 name="Kinds revenue" kind="Revenue" currency="EUR" expect ok capture revenue=$.id -expect ok field kind == 2 +expect ok field kind == "Revenue" -do OpenAccount ledgerId=1 name="Kinds liability" kind=3 currency=1 +do OpenAccount ledgerId=1 name="Kinds liability" kind="Liability" currency="EUR" expect ok capture liability=$.id -expect ok field kind == 3 +expect ok field kind == "Liability" # ── One entry across all four kinds ─────────────────────────────────────── # Salary arrives (revenue -> asset), rent is paid on credit (expense -> # liability). Four legs, one currency, summing to zero. do StoreTransaction ledgerId=1 description="salary in, rent on credit" date="2026-08-01T00:00:00.000Z" legs=[{"accountId":$asset,"amount":{"num":200000,"den":1,"dp":2}},{"accountId":$revenue,"amount":{"num":-200000,"den":1,"dp":2}},{"accountId":$expense,"amount":{"num":90000,"den":1,"dp":2}},{"accountId":$liability,"amount":{"num":-90000,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$asset,\"name\":\"Kinds asset\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":200000" -expect ok field accounts ~ "\"id\":$expense,\"name\":\"Kinds expense\",\"kind\":1,\"currency\":1,\"balance\":.\"num\":90000" -expect ok field accounts ~ "\"id\":$revenue,\"name\":\"Kinds revenue\",\"kind\":2,\"currency\":1,\"balance\":.\"num\":-200000" -expect ok field accounts ~ "\"id\":$liability,\"name\":\"Kinds liability\",\"kind\":3,\"currency\":1,\"balance\":.\"num\":-90000" +expect ok field accounts ~ "\"id\":$asset,\"name\":\"Kinds asset\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":200000" +expect ok field accounts ~ "\"id\":$expense,\"name\":\"Kinds expense\",\"kind\":\"Expense\",\"currency\":\"EUR\",\"balance\":.\"num\":90000" +expect ok field accounts ~ "\"id\":$revenue,\"name\":\"Kinds revenue\",\"kind\":\"Revenue\",\"currency\":\"EUR\",\"balance\":.\"num\":-200000" +expect ok field accounts ~ "\"id\":$liability,\"name\":\"Kinds liability\",\"kind\":\"Liability\",\"currency\":\"EUR\",\"balance\":.\"num\":-90000" # The read agrees with the write's own reply. do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$asset,\"name\":\"Kinds asset\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":200000" -expect ok field accounts ~ "\"id\":$liability,\"name\":\"Kinds liability\",\"kind\":3,\"currency\":1,\"balance\":.\"num\":-90000" +expect ok field accounts ~ "\"id\":$asset,\"name\":\"Kinds asset\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":200000" +expect ok field accounts ~ "\"id\":$liability,\"name\":\"Kinds liability\",\"kind\":\"Liability\",\"currency\":\"EUR\",\"balance\":.\"num\":-90000" # ── Kind does not exempt anything from the invariant ────────────────────── # Two legs across two kinds that do not cancel. Two, not one: a single-leg @@ -70,9 +72,9 @@ expect err message == "action failed validation: LedgerModel/StoreTransaction" expect err message !~ "zero-sum" do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$revenue,\"name\":\"Kinds revenue\",\"kind\":2,\"currency\":1,\"balance\":.\"num\":-200000" +expect ok field accounts ~ "\"id\":$revenue,\"name\":\"Kinds revenue\",\"kind\":\"Revenue\",\"currency\":\"EUR\",\"balance\":.\"num\":-200000" # ── Paying the liability down moves two kinds and still balances ────────── do StoreTransaction ledgerId=1 description="pay the landlord" date="2026-08-03T00:00:00.000Z" legs=[{"accountId":$liability,"amount":{"num":90000,"den":1,"dp":2}},{"accountId":$asset,"amount":{"num":-90000,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$liability,\"name\":\"Kinds liability\",\"kind\":3,\"currency\":1,\"balance\":.\"num\":0" -expect ok field accounts ~ "\"id\":$asset,\"name\":\"Kinds asset\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":110000" +expect ok field accounts ~ "\"id\":$liability,\"name\":\"Kinds liability\",\"kind\":\"Liability\",\"currency\":\"EUR\",\"balance\":.\"num\":0" +expect ok field accounts ~ "\"id\":$asset,\"name\":\"Kinds asset\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":110000" diff --git a/scripts/scenario/scenarios/ledger/an-unbalanced-entry-changes-nothing.scenario b/scripts/scenario/scenarios/ledger/an-unbalanced-entry-changes-nothing.scenario index 2ec9ab0e..8a488dc3 100644 --- a/scripts/scenario/scenarios/ledger/an-unbalanced-entry-changes-nothing.scenario +++ b/scripts/scenario/scenarios/ledger/an-unbalanced-entry-changes-nothing.scenario @@ -32,30 +32,30 @@ expect ok capture who=$.principal client books model=LedgerModel session principal=$who token=$token -do OpenAccount ledgerId=1 name="Untouched cash" kind=0 currency=1 +do OpenAccount ledgerId=1 name="Untouched cash" kind="Asset" currency="EUR" expect ok capture cash=$.id -do OpenAccount ledgerId=1 name="Untouched spend" kind=1 currency=1 +do OpenAccount ledgerId=1 name="Untouched spend" kind="Expense" currency="EUR" expect ok capture spend=$.id # ── A known-good baseline to measure the refusals against ────────────── do StoreTransaction ledgerId=1 description="the only entry that lands" date="2026-08-01T00:00:00.000Z" legs=[{"accountId":$cash,"amount":{"num":-1000,"den":1,"dp":2}},{"accountId":$spend,"amount":{"num":1000,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Untouched cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-1000" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Untouched cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-1000" # ── Refused: the legs do not cancel ──────────────────────────────────── do StoreTransaction ledgerId=1 description="short by one euro" date="2026-08-02T00:00:00.000Z" legs=[{"accountId":$cash,"amount":{"num":-1100,"den":1,"dp":2}},{"accountId":$spend,"amount":{"num":1000,"den":1,"dp":2}}] expect err message ~ "zero-sum violation in EUR" do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Untouched cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-1000" -expect ok field accounts ~ "\"id\":$spend,\"name\":\"Untouched spend\",\"kind\":1,\"currency\":1,\"balance\":.\"num\":1000" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Untouched cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-1000" +expect ok field accounts ~ "\"id\":$spend,\"name\":\"Untouched spend\",\"kind\":\"Expense\",\"currency\":\"EUR\",\"balance\":.\"num\":1000" # ── Refused: one leg names nothing ───────────────────────────────────── do StoreTransaction ledgerId=1 description="a leg from nowhere" date="2026-08-03T00:00:00.000Z" legs=[{"accountId":$cash,"amount":{"num":-500,"den":1,"dp":2}},{"accountId":999999,"amount":{"num":500,"den":1,"dp":2}}] expect err message == "StoreTransaction: no such account" do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Untouched cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-1000" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Untouched cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-1000" # ── Refused: fewer legs than an entry can have ───────────────────────── do StoreTransaction ledgerId=1 description="one leg only" date="2026-08-04T00:00:00.000Z" legs=[{"accountId":$cash,"amount":{"num":-500,"den":1,"dp":2}}] @@ -65,7 +65,7 @@ do StoreTransaction ledgerId=1 description="no legs at all" date="2026-08-04T00: expect err message == "action failed validation: LedgerModel/StoreTransaction" do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Untouched cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-1000" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Untouched cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-1000" # ── The other door: the same invariant, reached through the import ───── # A CSV row posts a two-leg entry against the row's account and the chunk's @@ -77,7 +77,7 @@ do ImportLedgerChunk ledgerId=1 counterAccountId=999999 opId="untouched-$cash" c expect err message == "ImportLedgerChunk: no such account" do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Untouched cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-1000" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Untouched cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-1000" # ── Where the all-or-nothing stops: a malformed row mid-chunk ────────── # @@ -107,8 +107,8 @@ do ImportLedgerChunk ledgerId=1 counterAccountId=$spend opId="untouched-2-$cash" expect err message == "ImportLedgerChunk: malformed date" do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Untouched cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-1100" -expect ok field accounts ~ "\"id\":$spend,\"name\":\"Untouched spend\",\"kind\":1,\"currency\":1,\"balance\":.\"num\":1100" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Untouched cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-1100" +expect ok field accounts ~ "\"id\":$spend,\"name\":\"Untouched spend\",\"kind\":\"Expense\",\"currency\":\"EUR\",\"balance\":.\"num\":1100" # ── ...and the corrected chunk imports only what is missing ──────────── # The content hash recognises the row that already landed, so re-uploading the @@ -120,8 +120,8 @@ expect ok field imported == 1 expect ok field duplicates == 1 do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Untouched cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-1300" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Untouched cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-1300" # ── ...and the books still take good work afterwards ─────────────────── do StoreTransaction ledgerId=1 description="the second entry that lands" date="2026-08-07T00:00:00.000Z" legs=[{"accountId":$cash,"amount":{"num":-250,"den":1,"dp":2}},{"accountId":$spend,"amount":{"num":250,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Untouched cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-1550" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Untouched cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-1550" diff --git a/scripts/scenario/scenarios/ledger/bootstrap-a-book-over-the-wire.scenario b/scripts/scenario/scenarios/ledger/bootstrap-a-book-over-the-wire.scenario index 725d5fd2..e5b486de 100644 --- a/scripts/scenario/scenarios/ledger/bootstrap-a-book-over-the-wire.scenario +++ b/scripts/scenario/scenarios/ledger/bootstrap-a-book-over-the-wire.scenario @@ -30,8 +30,9 @@ # bare-scalar replies do; # * every strong id is its bare underlying number on the wire # (ledger/core/types.hpp's LEDGER_DEFINE_STRONG_ID_WIRE); -# * enums are their integer values — AccountKind::Asset is 0 and -# ::Expense is 1, Currency::EUR is 1; +# * enums are their enumerator names, not their integer values — +# AccountKind::Asset is "Asset" and ::Expense is "Expense", +# Currency::EUR is "EUR"; # * a Rational is `{"num","den","dp"}` and comes back canonicalised. # ── Sign in ──────────────────────────────────────────────────────────────── @@ -54,11 +55,11 @@ do GetLedger ledgerId=$book expect ok field accounts !~ "\"id\"" # ── ...and the whole of step 1 now hangs off it ───────────────────────────── -do OpenAccount ledgerId=$book name="Wallet" kind=0 currency=1 +do OpenAccount ledgerId=$book name="Wallet" kind="Asset" currency="EUR" expect ok capture wallet=$.id expect ok field name == "Wallet" -do OpenAccount ledgerId=$book name="Groceries" kind=1 currency=1 +do OpenAccount ledgerId=$book name="Groceries" kind="Expense" currency="EUR" expect ok capture groceries=$.id # Two legs in one currency summing to canonical zero. The posted balances are @@ -66,8 +67,8 @@ expect ok capture groceries=$.id # file created — pinned against captured ids, so no other row in a shared # database can satisfy them by accident. do StoreTransaction ledgerId=$book description="weekly shop" date="2026-08-30T10:00:00.000Z" legs=[{"accountId":$wallet,"amount":{"num":-4250,"den":1,"dp":2}},{"accountId":$groceries,"amount":{"num":4250,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$wallet,\"name\":\"Wallet\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-4250" -expect ok field accounts ~ "\"id\":$groceries,\"name\":\"Groceries\",\"kind\":1,\"currency\":1,\"balance\":.\"num\":4250" +expect ok field accounts ~ "\"id\":$wallet,\"name\":\"Wallet\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-4250" +expect ok field accounts ~ "\"id\":$groceries,\"name\":\"Groceries\",\"kind\":\"Expense\",\"currency\":\"EUR\",\"balance\":.\"num\":4250" do GetLedger ledgerId=$book expect ok field accounts ~ "\"id\":$wallet," diff --git a/scripts/scenario/scenarios/ledger/budget-limit-and-spend-report.scenario b/scripts/scenario/scenarios/ledger/budget-limit-and-spend-report.scenario index 6aef27c0..44ab6f40 100644 --- a/scripts/scenario/scenarios/ledger/budget-limit-and-spend-report.scenario +++ b/scripts/scenario/scenarios/ledger/budget-limit-and-spend-report.scenario @@ -15,7 +15,7 @@ # A month with no limit row set does not report a limit of zero and does not # refuse. `BudgetModel::execute(GetBudgetReport)` sets **limit = spent** in # that case ("no limit was ever set, which reports as a limit equal to what -# was spent"), and falls back to Currency::USD -- 0 on the wire -- for the +# was spent"), and falls back to Currency::USD -- "USD" on the wire -- for the # report currency, because the currency is read off the limit row that does # not exist. So an unlimited month always reads as exactly on budget, in # dollars, whatever the ledger is denominated in. @@ -34,10 +34,10 @@ expect ok capture who=$.principal client books model=LedgerModel session principal=$who token=$token -do OpenAccount ledgerId=1 name="Budgeted cash" kind=0 currency=1 +do OpenAccount ledgerId=1 name="Budgeted cash" kind="Asset" currency="EUR" expect ok capture cash=$.id -do OpenAccount ledgerId=1 name="Budgeted groceries" kind=1 currency=1 +do OpenAccount ledgerId=1 name="Budgeted groceries" kind="Expense" currency="EUR" expect ok capture groceries=$.id client budgets model=BudgetModel @@ -52,14 +52,14 @@ expect ok do CreateBudget ledgerId=1 name="Budgeted month" categoryId=$food expect ok capture budget=$ -do SetBudgetLimit budgetId=$budget month="2026-08" limit={"num":30000,"den":1,"dp":2} currency=1 +do SetBudgetLimit budgetId=$budget month="2026-08" limit={"num":30000,"den":1,"dp":2} currency="EUR" expect ok # ── Before any spending ─────────────────────────────────────────────────── do GetBudgetReport budgetId=$budget month="2026-08" expect ok field limit.num == 30000 expect ok field spent.num == 0 -expect ok field currency == 1 +expect ok field currency == "EUR" # ── Two entries in August ───────────────────────────────────────────────── use books @@ -79,7 +79,7 @@ do GetBudgetReport budgetId=$budget month="2026-08" expect ok field limit.num == 30000 expect ok field spent.num == 5425 expect ok field spent.dp == 2 -expect ok field currency == 1 +expect ok field currency == "EUR" # ── One entry in September, which August must not see ──────────────────── use books @@ -97,10 +97,10 @@ expect ok field spent.num == 5425 do GetBudgetReport budgetId=$budget month="2026-09" expect ok field spent.num == 9900 expect ok field limit.num == 9900 -expect ok field currency == 0 +expect ok field currency == "USD" # ── Setting September's limit changes September and not August ─────────── -do SetBudgetLimit budgetId=$budget month="2026-09" limit={"num":12000,"den":1,"dp":2} currency=1 +do SetBudgetLimit budgetId=$budget month="2026-09" limit={"num":12000,"den":1,"dp":2} currency="EUR" expect ok do GetBudgetReport budgetId=$budget month="2026-09" @@ -117,7 +117,7 @@ expect ok field spent.num == 5425 do GetBudgetReport budgetId=$budget month="2026-07" expect ok field limit.num == 0 expect ok field spent.num == 0 -expect ok field currency == 0 +expect ok field currency == "USD" # ── The refusals ───────────────────────────────────────────────────────── do GetBudgetReport budgetId=999999 month="2026-08" diff --git a/scripts/scenario/scenarios/ledger/categorise-an-account.scenario b/scripts/scenario/scenarios/ledger/categorise-an-account.scenario index 54543d02..adc7fd37 100644 --- a/scripts/scenario/scenarios/ledger/categorise-an-account.scenario +++ b/scripts/scenario/scenarios/ledger/categorise-an-account.scenario @@ -26,10 +26,10 @@ expect ok capture who=$.principal client books model=LedgerModel session principal=$who token=$token -do OpenAccount ledgerId=1 name="Categorised cash" kind=0 currency=1 +do OpenAccount ledgerId=1 name="Categorised cash" kind="Asset" currency="EUR" expect ok capture cash=$.id -do OpenAccount ledgerId=1 name="Categorised coffee" kind=1 currency=1 +do OpenAccount ledgerId=1 name="Categorised coffee" kind="Expense" currency="EUR" expect ok capture coffee=$.id client budgets model=BudgetModel @@ -51,7 +51,7 @@ expect ok client rules model=RuleModel session principal=$who token=$token -do CreateRule ledgerId=1 trigger=0 matchText="flat white" action=0 actionValue="Categorised drinks" +do CreateRule ledgerId=1 trigger="DescriptionContains" matchText="flat white" action="SetCategory" actionValue="Categorised drinks" expect ok capture rule=$ use books @@ -71,7 +71,7 @@ use budgets do CreateBudget ledgerId=1 name="Categorised budget" categoryId=$drinks expect ok capture budget=$ -do SetBudgetLimit budgetId=$budget month="2026-08" limit={"num":50000,"den":1,"dp":2} currency=1 +do SetBudgetLimit budgetId=$budget month="2026-08" limit={"num":50000,"den":1,"dp":2} currency="EUR" expect ok use books @@ -85,7 +85,7 @@ use budgets # the second SetCategory -- so the entry above is counted once for each side. do GetBudgetReport budgetId=$budget month="2026-08" expect ok field limit.num == 50000 -expect ok field currency == 1 +expect ok field currency == "EUR" # ── The refusals ────────────────────────────────────────────────────────── do LinkAccountToCategory accountId=999999 categoryId=$drinks @@ -101,4 +101,4 @@ expect err message == "SetCategory: no such account or category" # Nothing the refusals touched changed: the ledger still reads. do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$coffee,\"name\":\"Categorised coffee\",\"kind\":1,\"currency\":1,\"balance\":.\"num\":350" +expect ok field accounts ~ "\"id\":$coffee,\"name\":\"Categorised coffee\",\"kind\":\"Expense\",\"currency\":\"EUR\",\"balance\":.\"num\":350" diff --git a/scripts/scenario/scenarios/ledger/exactly-once-store-transaction.scenario b/scripts/scenario/scenarios/ledger/exactly-once-store-transaction.scenario index a0eb0b99..737b1f37 100644 --- a/scripts/scenario/scenarios/ledger/exactly-once-store-transaction.scenario +++ b/scripts/scenario/scenarios/ledger/exactly-once-store-transaction.scenario @@ -28,42 +28,42 @@ expect ok capture who=$.principal client books model=LedgerModel session principal=$who token=$token -do OpenAccount ledgerId=1 name="Exactly-once cash" kind=0 currency=1 +do OpenAccount ledgerId=1 name="Exactly-once cash" kind="Asset" currency="EUR" expect ok capture cash=$.id -do OpenAccount ledgerId=1 name="Exactly-once spend" kind=1 currency=1 +do OpenAccount ledgerId=1 name="Exactly-once spend" kind="Expense" currency="EUR" expect ok capture spend=$.id # ── Without an opId: two identical sends post twice ─────────────────────── # The disengaged default takes the ordinary insert-only path, so this is what # a retry costs a client that does not carry a key. do StoreTransaction ledgerId=1 description="no key" date="2026-08-01T00:00:00.000Z" legs=[{"accountId":$cash,"amount":{"num":-100,"den":1,"dp":2}},{"accountId":$spend,"amount":{"num":100,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Exactly-once cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-100" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Exactly-once cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-100" do StoreTransaction ledgerId=1 description="no key" date="2026-08-01T00:00:00.000Z" legs=[{"accountId":$cash,"amount":{"num":-100,"den":1,"dp":2}},{"accountId":$spend,"amount":{"num":100,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Exactly-once cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-200" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Exactly-once cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-200" # ── With an opId: the second send changes nothing ───────────────────────── do StoreTransaction ledgerId=1 description="keyed" date="2026-08-02T00:00:00.000Z" opId="once-$cash" legs=[{"accountId":$cash,"amount":{"num":-50,"den":1,"dp":2}},{"accountId":$spend,"amount":{"num":50,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Exactly-once cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-250" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Exactly-once cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-250" do StoreTransaction ledgerId=1 description="keyed" date="2026-08-02T00:00:00.000Z" opId="once-$cash" legs=[{"accountId":$cash,"amount":{"num":-50,"den":1,"dp":2}},{"accountId":$spend,"amount":{"num":50,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Exactly-once cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-250" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Exactly-once cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-250" # A third send under the same key is still a no-op. do StoreTransaction ledgerId=1 description="keyed" date="2026-08-02T00:00:00.000Z" opId="once-$cash" legs=[{"accountId":$cash,"amount":{"num":-50,"den":1,"dp":2}},{"accountId":$spend,"amount":{"num":50,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Exactly-once cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-250" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Exactly-once cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-250" # ── The independent read agrees, so this is the ledger and not the reply ── do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Exactly-once cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-250" -expect ok field accounts ~ "\"id\":$spend,\"name\":\"Exactly-once spend\",\"kind\":1,\"currency\":1,\"balance\":.\"num\":250" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Exactly-once cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-250" +expect ok field accounts ~ "\"id\":$spend,\"name\":\"Exactly-once spend\",\"kind\":\"Expense\",\"currency\":\"EUR\",\"balance\":.\"num\":250" # ── A *different* key posts, even with an identical body ────────────────── # The key is the operation's identity, not the payload's -- so a genuine # second occurrence of the same purchase is not swallowed. do StoreTransaction ledgerId=1 description="keyed" date="2026-08-02T00:00:00.000Z" opId="twice-$cash" legs=[{"accountId":$cash,"amount":{"num":-50,"den":1,"dp":2}},{"accountId":$spend,"amount":{"num":50,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Exactly-once cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-300" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Exactly-once cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-300" # ── A key does not excuse an unbalanced entry ──────────────────────────── do StoreTransaction ledgerId=1 description="keyed but wrong" date="2026-08-03T00:00:00.000Z" opId="bad-$cash" legs=[{"accountId":$cash,"amount":{"num":-10,"den":1,"dp":2}},{"accountId":$spend,"amount":{"num":90,"den":1,"dp":2}}] @@ -72,4 +72,4 @@ expect err message ~ "zero-sum violation" # ...and the refused key was not spent: the same key now works with legs that # balance, which is what makes a retry after a client-side fix possible. do StoreTransaction ledgerId=1 description="keyed and right" date="2026-08-03T00:00:00.000Z" opId="bad-$cash" legs=[{"accountId":$cash,"amount":{"num":-90,"den":1,"dp":2}},{"accountId":$spend,"amount":{"num":90,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Exactly-once cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-390" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Exactly-once cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-390" diff --git a/scripts/scenario/scenarios/ledger/import-a-statement-chunk.scenario b/scripts/scenario/scenarios/ledger/import-a-statement-chunk.scenario index cc4808dd..131b0b03 100644 --- a/scripts/scenario/scenarios/ledger/import-a-statement-chunk.scenario +++ b/scripts/scenario/scenarios/ledger/import-a-statement-chunk.scenario @@ -45,10 +45,10 @@ expect ok capture who=$.principal client books model=LedgerModel session principal=$who token=$token -do OpenAccount ledgerId=1 name="Imported current" kind=0 currency=1 +do OpenAccount ledgerId=1 name="Imported current" kind="Asset" currency="EUR" expect ok capture current=$.id -do OpenAccount ledgerId=1 name="Imported suspense" kind=1 currency=1 +do OpenAccount ledgerId=1 name="Imported suspense" kind="Expense" currency="EUR" expect ok capture suspense=$.id # ── The first chunk ────────────────────────────────────────────────────── @@ -59,8 +59,8 @@ expect ok field duplicates == 0 # The read is the proof: two rows, -5.50 off the current account and the # matching +5.50 in suspense. do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$current,\"name\":\"Imported current\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-550" -expect ok field accounts ~ "\"id\":$suspense,\"name\":\"Imported suspense\",\"kind\":1,\"currency\":1,\"balance\":.\"num\":550" +expect ok field accounts ~ "\"id\":$current,\"name\":\"Imported current\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-550" +expect ok field accounts ~ "\"id\":$suspense,\"name\":\"Imported suspense\",\"kind\":\"Expense\",\"currency\":\"EUR\",\"balance\":.\"num\":550" # ── The same chunk again, same opId: nothing is posted ─────────────────── do ImportLedgerChunk ledgerId=1 counterAccountId=$suspense opId="stmt-a-$current" csvChunk="date,description,account_id,amount\n2026-08-01T00:00:00.000Z,latte $current,$current,-3.50\n2026-08-02T00:00:00.000Z,bagel $current,$current,-2.00\n" @@ -68,7 +68,7 @@ expect ok field imported == 0 expect ok field duplicates == 2 do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$current,\"name\":\"Imported current\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-550" +expect ok field accounts ~ "\"id\":$current,\"name\":\"Imported current\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-550" # ── The same rows under a *new* opId: still nothing, by content ────────── # This is the assertion that separates the two mechanisms. If dedup were only @@ -78,7 +78,7 @@ expect ok field imported == 0 expect ok field duplicates == 2 do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$current,\"name\":\"Imported current\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-550" +expect ok field accounts ~ "\"id\":$current,\"name\":\"Imported current\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-550" # ── A chunk that overlaps: one old row, one new ───────────────────────── do ImportLedgerChunk ledgerId=1 counterAccountId=$suspense opId="stmt-c-$current" csvChunk="date,description,account_id,amount\n2026-08-02T00:00:00.000Z,bagel $current,$current,-2.00\n2026-08-03T00:00:00.000Z,newspaper $current,$current,-1.25\n" @@ -86,7 +86,7 @@ expect ok field imported == 1 expect ok field duplicates == 1 do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$current,\"name\":\"Imported current\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-675" +expect ok field accounts ~ "\"id\":$current,\"name\":\"Imported current\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-675" # ── A chunk with no rows at all is not an error ───────────────────────── do ImportLedgerChunk ledgerId=1 counterAccountId=$suspense opId="stmt-d-$current" csvChunk="date,description,account_id,amount\n" @@ -105,4 +105,4 @@ expect err message == "ImportLedgerChunk: no such account" # None of the three refusals posted anything. do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$current,\"name\":\"Imported current\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-675" +expect ok field accounts ~ "\"id\":$current,\"name\":\"Imported current\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-675" diff --git a/scripts/scenario/scenarios/ledger/open-account-transact-report-close.scenario b/scripts/scenario/scenarios/ledger/open-account-transact-report-close.scenario index 549a5d15..4c406ce3 100644 --- a/scripts/scenario/scenarios/ledger/open-account-transact-report-close.scenario +++ b/scripts/scenario/scenarios/ledger/open-account-transact-report-close.scenario @@ -17,8 +17,9 @@ # (ledger/core/types.hpp's LEDGER_DEFINE_STRONG_ID_WIRE), so `ledgerId=$l` # sends `"ledgerId": 7`, and an *empty* id is omitted from the JSON # entirely rather than sent as null; -# * enums are their integer values, not their names — AccountKind::Asset is -# 0 and ::Expense is 1, Currency::EUR is 1; +# * enums are their enumerator names, not their integer values — +# AccountKind::Asset is "Asset" and ::Expense is "Expense", +# Currency::EUR is "EUR"; # * a Rational is `{"num","den","dp"}` and arrives *canonicalised*: -250/100 # is stored and returned as -5/2 with dp=2, so assertions here name the # canonical form rather than what was sent; @@ -46,12 +47,12 @@ expect ok field principal == "alice" client books model=LedgerModel session principal=$who token=$token -do OpenAccount ledgerId=1 name="Cash" kind=0 currency=1 +do OpenAccount ledgerId=1 name="Cash" kind="Asset" currency="EUR" expect ok capture cash=$.id expect ok field name == "Cash" expect ok field balance.num == 0 -do OpenAccount ledgerId=1 name="Coffee" kind=1 currency=1 +do OpenAccount ledgerId=1 name="Coffee" kind="Expense" currency="EUR" expect ok capture coffee=$.id # ── Record a balanced transaction ─────────────────────────────────────────── @@ -64,8 +65,8 @@ do StoreTransaction ledgerId=1 description="flat white" date="2026-08-30T09:15:0 # ...and the posted balances prove the legs landed on the right accounts. # Pinned against the ids captured above, so other rows in a shared database # cannot satisfy this by accident. -expect ok field accounts ~ "\"id\":$cash,\"name\":\"Cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-250" -expect ok field accounts ~ "\"id\":$coffee,\"name\":\"Coffee\",\"kind\":1,\"currency\":1,\"balance\":.\"num\":250" +expect ok field accounts ~ "\"id\":$cash,\"name\":\"Cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-250" +expect ok field accounts ~ "\"id\":$coffee,\"name\":\"Coffee\",\"kind\":\"Expense\",\"currency\":\"EUR\",\"balance\":.\"num\":250" # The read is what proves the write landed — not the write's own ok. Balances # come back canonicalised: -250/100 is -5/2. @@ -92,12 +93,12 @@ expect ok do CreateBudget ledgerId=1 name="August" categoryId=$drinks expect ok capture budget=$ -do SetBudgetLimit budgetId=$budget month="2026-08" limit={"num":5000,"den":1,"dp":2} currency=1 +do SetBudgetLimit budgetId=$budget month="2026-08" limit={"num":5000,"den":1,"dp":2} currency="EUR" expect ok do GetBudgetReport budgetId=$budget month="2026-08" expect ok field limit.num == 5000 -expect ok field currency == 1 +expect ok field currency == "EUR" # ── Submit a report ───────────────────────────────────────────────────────── # SubmitReport only enqueues: the job is computed later by the server's own @@ -105,15 +106,15 @@ expect ok field currency == 1 # for that (this runner has no sleeps by design) and must not assert against # it either, so this asserts only what does not race with the sweep. use books -do SubmitReport ledgerId=1 kind=0 params="{\"year\":2026,\"month\":8,\"timezoneOffsetMinutes\":0}" +do SubmitReport ledgerId=1 kind="MonthlyStatement" params="{\"year\":2026,\"month\":8,\"timezoneOffsetMinutes\":0}" expect ok capture job=$ -# Not `status == 0`: `ledger::app::App` sweeps for Pending jobs once a second -# and runs them, so a job's status races that timer from the moment it is -# submitted. What does not race is that a submitted job's id resolves and that -# a job never lands in Failed (2). +# Not `status == "Pending"`: `ledger::app::App` sweeps for Pending jobs once a +# second and runs them, so a job's status races that timer from the moment it +# is submitted. What does not race is that a submitted job's id resolves and +# that a job never lands in "Failed". do GetReportStatus jobId=$job -expect ok field status != 2 +expect ok field status != "Failed" # ...and the authorization boundary that makes the wait unavoidable: # LedgerModel::execute(const RunReportJob&) accepts only the report runner's diff --git a/scripts/scenario/scenarios/ledger/store-list-and-undo-an-entry.scenario b/scripts/scenario/scenarios/ledger/store-list-and-undo-an-entry.scenario index 6564ddd4..3ea3f405 100644 --- a/scripts/scenario/scenarios/ledger/store-list-and-undo-an-entry.scenario +++ b/scripts/scenario/scenarios/ledger/store-list-and-undo-an-entry.scenario @@ -49,10 +49,10 @@ # `kind="Asset"`, `currency="EUR"`. That is what morph#444 made of them # when it gave every ledger enum a `glz::meta`/`glz::enumerate` # (ledger/core/types.hpp), and it is what a real server accepts today. -# Every other file in this directory still sends the pre-#444 integers and -# is refused by the running server for it — a corpus-wide breakage that -# predates this change and is filed as morph#460; this file is written -# against the wire as it actually is. +# Every other file in this directory sends the same enumerator names -- +# they spelled the pre-#444 integers until morph#460 rewrote them, which +# is why nothing in this directory carries a bare integer for an enum any +# more. # ── Sign in ──────────────────────────────────────────────────────────────── client auth model=AuthModel diff --git a/scripts/scenario/scenarios/ledger/submit-a-report-and-poll-it.scenario b/scripts/scenario/scenarios/ledger/submit-a-report-and-poll-it.scenario index 69afe9c0..5ba32666 100644 --- a/scripts/scenario/scenarios/ledger/submit-a-report-and-poll-it.scenario +++ b/scripts/scenario/scenarios/ledger/submit-a-report-and-poll-it.scenario @@ -26,20 +26,21 @@ # job's status is a *race* against that timer from the moment it is submitted: # the first poll usually sees Pending, and a poll a few steps later may not. # -# Asserting `status == 0` therefore makes a scenario depend on wall-clock -# timing through the back door, which is exactly what this runner exists not -# to do (morph#147) -- and it was caught the way such things should be, by a -# mutation run whose repeated passes gave the runner time to tick: the mutant -# `status != 0` survived, because by then the job had genuinely completed. +# Asserting `status == "Pending"` therefore makes a scenario depend on +# wall-clock timing through the back door, which is exactly what this runner +# exists not to do (morph#147) -- and it was caught the way such things should +# be, by a mutation run whose repeated passes gave the runner time to tick: the +# mutant `status != "Pending"` survived, because by then the job had genuinely +# completed. # # What is asserted instead is everything about a job that does *not* race: a -# submitted job's id resolves, a job never reaches `Failed` (2), an id that +# submitted job's id resolves, a job never reaches `"Failed"`, an id that # was never issued is refused, and a user client may not run a job itself. # -# Wire shapes: ReportKind::MonthlyStatement is 0 and ::BudgetReport is 1; -# ReportStatus::Pending is 0, ::Done 1, ::Failed 2. `params` is a JSON -# *string*, not an object -- so it arrives escaped. SubmitReport answers with -# a bare job id, captured with `$`. +# Wire shapes: enums travel as their enumerator names, so ReportKind is +# "MonthlyStatement"/"BudgetReport" and ReportStatus is "Pending"/"Done"/ +# "Failed". `params` is a JSON *string*, not an object -- so it arrives +# escaped. SubmitReport answers with a bare job id, captured with `$`. # # Re-runnable: every job is submitted by this file and polled by the id its # own submit returned. @@ -52,36 +53,36 @@ expect ok capture who=$.principal client books model=LedgerModel session principal=$who token=$token -do OpenAccount ledgerId=1 name="Reported cash" kind=0 currency=1 +do OpenAccount ledgerId=1 name="Reported cash" kind="Asset" currency="EUR" expect ok capture cash=$.id -do OpenAccount ledgerId=1 name="Reported spend" kind=1 currency=1 +do OpenAccount ledgerId=1 name="Reported spend" kind="Expense" currency="EUR" expect ok capture spend=$.id do StoreTransaction ledgerId=1 description="something to report on" date="2026-08-09T00:00:00.000Z" legs=[{"accountId":$cash,"amount":{"num":-1234,"den":1,"dp":2}},{"accountId":$spend,"amount":{"num":1234,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$spend,\"name\":\"Reported spend\",\"kind\":1,\"currency\":1,\"balance\":.\"num\":1234" +expect ok field accounts ~ "\"id\":$spend,\"name\":\"Reported spend\",\"kind\":\"Expense\",\"currency\":\"EUR\",\"balance\":.\"num\":1234" # ── Submit a monthly statement ────────────────────────────────────────── -do SubmitReport ledgerId=1 kind=0 params="{\"year\":2026,\"month\":8,\"timezoneOffsetMinutes\":0}" +do SubmitReport ledgerId=1 kind="MonthlyStatement" params="{\"year\":2026,\"month\":8,\"timezoneOffsetMinutes\":0}" expect ok capture statement=$ do GetReportStatus jobId=$statement -expect ok field status != 2 +expect ok field status != "Failed" # Polling again is a read: it neither advances the job nor fails it. do GetReportStatus jobId=$statement -expect ok field status != 2 +expect ok field status != "Failed" # ── A second job, of the other kind, gets its own id ──────────────────── -do SubmitReport ledgerId=1 kind=1 params="{\"year\":2026,\"month\":8,\"timezoneOffsetMinutes\":0}" +do SubmitReport ledgerId=1 kind="BudgetReport" params="{\"year\":2026,\"month\":8,\"timezoneOffsetMinutes\":0}" expect ok capture budgetJob=$ do GetReportStatus jobId=$budgetJob -expect ok field status != 2 +expect ok field status != "Failed" # The two ids are distinct, and each answers for itself. do GetReportStatus jobId=$statement -expect ok field status != 2 +expect ok field status != "Failed" # ── The boundary that makes the wait unavoidable ──────────────────────── # LedgerModel::execute(const RunReportJob&) accepts only the report runner's @@ -96,7 +97,7 @@ expect err message == "RunReportJob: only the report runner may run a report job # The refusal did not fail the job, which is what makes it a refusal rather # than a failure. do GetReportStatus jobId=$statement -expect ok field status != 2 +expect ok field status != "Failed" # ── A job id that does not exist ──────────────────────────────────────── do GetReportStatus jobId=999999 @@ -107,4 +108,4 @@ expect err message == "RunReportJob: only the report runner may run a report job # ── The ledger is unchanged by all of it ──────────────────────────────── do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$spend,\"name\":\"Reported spend\",\"kind\":1,\"currency\":1,\"balance\":.\"num\":1234" +expect ok field accounts ~ "\"id\":$spend,\"name\":\"Reported spend\",\"kind\":\"Expense\",\"currency\":\"EUR\",\"balance\":.\"num\":1234" diff --git a/scripts/scenario/scenarios/ledger/two-books-are-isolated.scenario b/scripts/scenario/scenarios/ledger/two-books-are-isolated.scenario index b59cc9fe..884d8530 100644 --- a/scripts/scenario/scenarios/ledger/two-books-are-isolated.scenario +++ b/scripts/scenario/scenarios/ledger/two-books-are-isolated.scenario @@ -24,16 +24,16 @@ session principal=$who token=$token # ── One account in each book, with deliberately identical names ──────── # Identical names, so nothing below can pass by matching on a name alone. -do OpenAccount ledgerId=1 name="Isolated wallet" kind=0 currency=1 +do OpenAccount ledgerId=1 name="Isolated wallet" kind="Asset" currency="EUR" expect ok capture first=$.id -do OpenAccount ledgerId=2 name="Isolated wallet" kind=0 currency=1 +do OpenAccount ledgerId=2 name="Isolated wallet" kind="Asset" currency="EUR" expect ok capture second=$.id -do OpenAccount ledgerId=1 name="Isolated spend" kind=1 currency=1 +do OpenAccount ledgerId=1 name="Isolated spend" kind="Expense" currency="EUR" expect ok capture firstSpend=$.id -do OpenAccount ledgerId=2 name="Isolated spend" kind=1 currency=1 +do OpenAccount ledgerId=2 name="Isolated spend" kind="Expense" currency="EUR" expect ok capture secondSpend=$.id # ── Each book's listing holds its own accounts and not the other's ───── @@ -51,18 +51,18 @@ expect ok field accounts !~ "\"id\":$firstSpend," # ── A transaction in book one moves book one's balances only ─────────── do StoreTransaction ledgerId=1 description="book one only" date="2026-08-01T00:00:00.000Z" legs=[{"accountId":$first,"amount":{"num":-4000,"den":1,"dp":2}},{"accountId":$firstSpend,"amount":{"num":4000,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$first,\"name\":\"Isolated wallet\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-4000" +expect ok field accounts ~ "\"id\":$first,\"name\":\"Isolated wallet\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-4000" expect ok field accounts !~ "\"id\":$second," do GetLedger ledgerId=2 -expect ok field accounts ~ "\"id\":$second,\"name\":\"Isolated wallet\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":0" +expect ok field accounts ~ "\"id\":$second,\"name\":\"Isolated wallet\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":0" # ── ...and one in book two leaves book one alone ─────────────────────── do StoreTransaction ledgerId=2 description="book two only" date="2026-08-01T00:00:00.000Z" legs=[{"accountId":$second,"amount":{"num":-1500,"den":1,"dp":2}},{"accountId":$secondSpend,"amount":{"num":1500,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$second,\"name\":\"Isolated wallet\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-1500" +expect ok field accounts ~ "\"id\":$second,\"name\":\"Isolated wallet\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-1500" do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$first,\"name\":\"Isolated wallet\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-4000" +expect ok field accounts ~ "\"id\":$first,\"name\":\"Isolated wallet\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-4000" # ── Budgets are scoped to their own book too ─────────────────────────── client budgets model=BudgetModel @@ -84,7 +84,7 @@ expect ok do CreateBudget ledgerId=1 name="Isolated budget" categoryId=$category expect ok capture budget=$ -do SetBudgetLimit budgetId=$budget month="2026-08" limit={"num":100000,"den":1,"dp":2} currency=1 +do SetBudgetLimit budgetId=$budget month="2026-08" limit={"num":100000,"den":1,"dp":2} currency="EUR" expect ok # 40.00 from book one, and nothing from book two's 15.00, even though both @@ -96,17 +96,17 @@ expect ok field limit.num == 100000 # ── A report submitted for one book names that book ──────────────────── use books -do SubmitReport ledgerId=1 kind=0 params="{\"year\":2026,\"month\":8,\"timezoneOffsetMinutes\":0}" +do SubmitReport ledgerId=1 kind="MonthlyStatement" params="{\"year\":2026,\"month\":8,\"timezoneOffsetMinutes\":0}" expect ok capture jobOne=$ -do SubmitReport ledgerId=2 kind=0 params="{\"year\":2026,\"month\":8,\"timezoneOffsetMinutes\":0}" +do SubmitReport ledgerId=2 kind="MonthlyStatement" params="{\"year\":2026,\"month\":8,\"timezoneOffsetMinutes\":0}" expect ok capture jobTwo=$ do GetReportStatus jobId=$jobOne -expect ok field status == 0 +expect ok field status == "Pending" do GetReportStatus jobId=$jobTwo -expect ok field status == 0 +expect ok field status == "Pending" # ── A leg may not name another book's account ────────────────────────── # @@ -131,10 +131,10 @@ expect err message == "StoreTransaction: account does not belong to this ledger" # Neither book moved: book one is still at the 40.00 it posted for itself... do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$first,\"name\":\"Isolated wallet\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-4000" +expect ok field accounts ~ "\"id\":$first,\"name\":\"Isolated wallet\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-4000" # ...and book two still at its own 15.00, with no foreign posting on top. do GetLedger ledgerId=2 -expect ok field accounts ~ "\"id\":$secondSpend,\"name\":\"Isolated spend\",\"kind\":1,\"currency\":1,\"balance\":.\"num\":1500" +expect ok field accounts ~ "\"id\":$secondSpend,\"name\":\"Isolated spend\",\"kind\":\"Expense\",\"currency\":\"EUR\",\"balance\":.\"num\":1500" # ── An account that exists in no book at all is refused differently ──── # Which is what shows the two refusals are not one: this leg's id names no row @@ -143,7 +143,7 @@ do StoreTransaction ledgerId=1 description="a leg from nowhere" date="2026-08-03 expect err message == "StoreTransaction: no such account" do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$first,\"name\":\"Isolated wallet\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-4000" +expect ok field accounts ~ "\"id\":$first,\"name\":\"Isolated wallet\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-4000" # ── An import chunk may not reach into the other book either ─────────── # Both of `ImportLedgerChunk`'s account lookups are scoped: the chunk-wide @@ -159,9 +159,9 @@ expect err message == "ImportLedgerChunk: account does not belong to this ledger # Neither refusal posted anything, in either book. do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$first,\"name\":\"Isolated wallet\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-4000" -expect ok field accounts ~ "\"id\":$firstSpend,\"name\":\"Isolated spend\",\"kind\":1,\"currency\":1,\"balance\":.\"num\":4000" +expect ok field accounts ~ "\"id\":$first,\"name\":\"Isolated wallet\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-4000" +expect ok field accounts ~ "\"id\":$firstSpend,\"name\":\"Isolated spend\",\"kind\":\"Expense\",\"currency\":\"EUR\",\"balance\":.\"num\":4000" do GetLedger ledgerId=2 -expect ok field accounts ~ "\"id\":$second,\"name\":\"Isolated wallet\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-1500" -expect ok field accounts ~ "\"id\":$secondSpend,\"name\":\"Isolated spend\",\"kind\":1,\"currency\":1,\"balance\":.\"num\":1500" +expect ok field accounts ~ "\"id\":$second,\"name\":\"Isolated wallet\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-1500" +expect ok field accounts ~ "\"id\":$secondSpend,\"name\":\"Isolated spend\",\"kind\":\"Expense\",\"currency\":\"EUR\",\"balance\":.\"num\":1500" diff --git a/scripts/scenario/scenarios/ledger/update-rule-version-conflict.scenario b/scripts/scenario/scenarios/ledger/update-rule-version-conflict.scenario index 4dd906ce..e4621fe4 100644 --- a/scripts/scenario/scenarios/ledger/update-rule-version-conflict.scenario +++ b/scripts/scenario/scenarios/ledger/update-rule-version-conflict.scenario @@ -32,7 +32,7 @@ client editorOne model=RuleModel session principal=$who token=$token # CreateRule answers with a bare rule id, so the capture reads `$`. -do CreateRule ledgerId=1 trigger=0 matchText="corner shop" action=0 actionValue="Groceries" +do CreateRule ledgerId=1 trigger="DescriptionContains" matchText="corner shop" action="SetCategory" actionValue="Groceries" expect ok capture rule=$ # ── Both editors read the rule at version 1 ────────────────────────────── @@ -105,10 +105,10 @@ expect err message == "UpdateRule: no such rule" client books model=LedgerModel session principal=$who token=$token -do OpenAccount ledgerId=1 name="Conflicted cash" kind=0 currency=1 +do OpenAccount ledgerId=1 name="Conflicted cash" kind="Asset" currency="EUR" expect ok capture cash=$.id -do OpenAccount ledgerId=1 name="Conflicted spend" kind=1 currency=1 +do OpenAccount ledgerId=1 name="Conflicted spend" kind="Expense" currency="EUR" expect ok capture spend=$.id client budgets model=BudgetModel @@ -123,7 +123,7 @@ expect ok capture loser=$ do CreateBudget ledgerId=1 name="Conflicted survivor budget $cash" categoryId=$survivor expect ok capture survivorBudget=$ -do SetBudgetLimit budgetId=$survivorBudget month="2026-08" limit={"num":100000,"den":1,"dp":2} currency=1 +do SetBudgetLimit budgetId=$survivorBudget month="2026-08" limit={"num":100000,"den":1,"dp":2} currency="EUR" expect ok # Point the surviving rule at the surviving category, under its current @@ -140,7 +140,7 @@ do SetCategory accountId=$spend categoryId=$loser ruleId=$rule ruleVersion=7 expect ok do StoreTransaction ledgerId=1 description="CORNER-$cash shop" date="2026-08-01T00:00:00.000Z" legs=[{"accountId":$cash,"amount":{"num":-1200,"den":1,"dp":2}},{"accountId":$spend,"amount":{"num":1200,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$spend,\"name\":\"Conflicted spend\",\"kind\":1,\"currency\":1,\"balance\":.\"num\":1200" +expect ok field accounts ~ "\"id\":$spend,\"name\":\"Conflicted spend\",\"kind\":\"Expense\",\"currency\":\"EUR\",\"balance\":.\"num\":1200" use budgets diff --git a/scripts/scenario/scenarios/ledger/zero-sum-holds-per-currency.scenario b/scripts/scenario/scenarios/ledger/zero-sum-holds-per-currency.scenario index 682d91d5..8373afe8 100644 --- a/scripts/scenario/scenarios/ledger/zero-sum-holds-per-currency.scenario +++ b/scripts/scenario/scenarios/ledger/zero-sum-holds-per-currency.scenario @@ -15,8 +15,10 @@ # ── Wire shapes, from the ledger rung's own conventions ─────────────────── # * every strong id is its bare underlying number, and an empty one is # omitted from the JSON rather than sent as null; -# * enums are their integer values: AccountKind::Asset is 0 and ::Expense -# is 1, Currency::USD is 0 and ::EUR is 1; +# * enums are their enumerator names, not their integer values: +# AccountKind::Asset is "Asset" and ::Expense is "Expense", +# Currency::USD is "USD" and ::EUR is "EUR" -- inside the inline `legs` +# JSON too, where a leg's `foreignCurrency` is "USD" rather than 0; # * a Rational is {"num","den","dp"} and comes back *canonicalised* -- # -500/1 at dp=2 is stored and returned as -500/1 dp=2, but 250/100 would # come back as 5/2; @@ -38,22 +40,22 @@ expect ok capture who=$.principal client books model=LedgerModel session principal=$who token=$token -do OpenAccount ledgerId=1 name="Zero-sum EUR cash" kind=0 currency=1 +do OpenAccount ledgerId=1 name="Zero-sum EUR cash" kind="Asset" currency="EUR" expect ok capture eurCash=$.id -do OpenAccount ledgerId=1 name="Zero-sum EUR food" kind=1 currency=1 +do OpenAccount ledgerId=1 name="Zero-sum EUR food" kind="Expense" currency="EUR" expect ok capture eurFood=$.id -do OpenAccount ledgerId=1 name="Zero-sum USD cash" kind=0 currency=0 +do OpenAccount ledgerId=1 name="Zero-sum USD cash" kind="Asset" currency="USD" expect ok capture usdCash=$.id -do OpenAccount ledgerId=1 name="Zero-sum USD fees" kind=1 currency=0 +do OpenAccount ledgerId=1 name="Zero-sum USD fees" kind="Expense" currency="USD" expect ok capture usdFees=$.id # ── Four legs, two currencies, each partition balanced ──────────────────── do StoreTransaction ledgerId=1 description="one entry, two currencies" date="2026-08-01T00:00:00.000Z" legs=[{"accountId":$eurCash,"amount":{"num":-500,"den":1,"dp":2}},{"accountId":$eurFood,"amount":{"num":500,"den":1,"dp":2}},{"accountId":$usdCash,"amount":{"num":-700,"den":1,"dp":2}},{"accountId":$usdFees,"amount":{"num":700,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$eurCash,\"name\":\"Zero-sum EUR cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-500" -expect ok field accounts ~ "\"id\":$usdCash,\"name\":\"Zero-sum USD cash\",\"kind\":0,\"currency\":0,\"balance\":.\"num\":-700" +expect ok field accounts ~ "\"id\":$eurCash,\"name\":\"Zero-sum EUR cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-500" +expect ok field accounts ~ "\"id\":$usdCash,\"name\":\"Zero-sum USD cash\",\"kind\":\"Asset\",\"currency\":\"USD\",\"balance\":.\"num\":-700" # ── One currency out of balance rejects the whole entry ─────────────────── # The EUR legs balance; the USD legs do not. The message names USD, which is @@ -64,7 +66,7 @@ expect err message !~ "EUR" # The EUR legs of that rejected entry were not posted either: all-or-nothing. do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$eurCash,\"name\":\"Zero-sum EUR cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-500" +expect ok field accounts ~ "\"id\":$eurCash,\"name\":\"Zero-sum EUR cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-500" # ── Two legs that cancel numerically but not within a currency ──────────── # -1000 and +1000, which any single global sum would accept. Each currency's @@ -73,13 +75,13 @@ do StoreTransaction ledgerId=1 description="cross-currency, numerically zero" da expect err message ~ "zero-sum violation" do GetLedger ledgerId=1 -expect ok field accounts ~ "\"id\":$eurCash,\"name\":\"Zero-sum EUR cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-500" -expect ok field accounts ~ "\"id\":$usdCash,\"name\":\"Zero-sum USD cash\",\"kind\":0,\"currency\":0,\"balance\":.\"num\":-700" +expect ok field accounts ~ "\"id\":$eurCash,\"name\":\"Zero-sum EUR cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-500" +expect ok field accounts ~ "\"id\":$usdCash,\"name\":\"Zero-sum USD cash\",\"kind\":\"Asset\",\"currency\":\"USD\",\"balance\":.\"num\":-700" # ── A foreign-amount pair is display metadata, never part of the sum ────── # The EUR legs balance on their own; the foreign amount and currency ride # along for audit and are not summed against anything. -do StoreTransaction ledgerId=1 description="paid in EUR, priced in USD" date="2026-08-04T00:00:00.000Z" legs=[{"accountId":$eurCash,"amount":{"num":-100,"den":1,"dp":2},"foreignAmount":{"num":110,"den":1,"dp":2},"foreignCurrency":0},{"accountId":$eurFood,"amount":{"num":100,"den":1,"dp":2}}] -expect ok field accounts ~ "\"id\":$eurCash,\"name\":\"Zero-sum EUR cash\",\"kind\":0,\"currency\":1,\"balance\":.\"num\":-600" +do StoreTransaction ledgerId=1 description="paid in EUR, priced in USD" date="2026-08-04T00:00:00.000Z" legs=[{"accountId":$eurCash,"amount":{"num":-100,"den":1,"dp":2},"foreignAmount":{"num":110,"den":1,"dp":2},"foreignCurrency":"USD"},{"accountId":$eurFood,"amount":{"num":100,"den":1,"dp":2}}] +expect ok field accounts ~ "\"id\":$eurCash,\"name\":\"Zero-sum EUR cash\",\"kind\":\"Asset\",\"currency\":\"EUR\",\"balance\":.\"num\":-600" # The USD side is untouched: a foreign amount posts nothing. -expect ok field accounts ~ "\"id\":$usdCash,\"name\":\"Zero-sum USD cash\",\"kind\":0,\"currency\":0,\"balance\":.\"num\":-700" +expect ok field accounts ~ "\"id\":$usdCash,\"name\":\"Zero-sum USD cash\",\"kind\":\"Asset\",\"currency\":\"USD\",\"balance\":.\"num\":-700"