diff --git a/plans/names-codes/2026-10-21-store-service-tasks.md b/plans/names-codes/2026-10-21-store-service-tasks.md index f28a3c88f1..17aa10eb7a 100644 --- a/plans/names-codes/2026-10-21-store-service-tasks.md +++ b/plans/names-codes/2026-10-21-store-service-tasks.md @@ -292,40 +292,42 @@ The owner's decisions of 2026-10-22 remove the code for a taken order and keep a - **Files:** `StoreService/Web/Names/Owner.hs`; `StoreService/Web/Names.hs` (its route line); `StoreService/Names/Orders.hs`; `StoreService/Web/Names/Orders.hs` (the refusals' responses); `StoreService/Names/Signatures.hs`; `StoreService/Names/Contracts.hs`; `StoreService/Names/Abi.hs`; `StoreService/Names/Store.hs`; `StoreService/Names/View.hs`; the two migration modules (migration `20261022_drop_taken_codes`); `tests/BadgeTests.hs`; `tests/Bots/StoreService/` `NamesOwnerTests.hs`, `NamesOrdersTests.hs`, `NamesStoreTests.hs`, `NamesContractsTests.hs`, `NamesSignatureTests.hs`, `NamesWiringTests.hs`. - **Do:** - Delete `takenCodeHandler`, `TakenCodeRequest`, `parseTakenCode` and the route; `issuable`, `issuedCodeHash`, `checkTakenCodeLength`, `issueTakenCode`, `TakenCodeConflict`; the refusals `ORCodeLengthChanged`, `ORCodeUnavailable`, `ORCodeConflict`, and `ORProviderUnavailable` once nothing else uses it, with their lines in `orderRefusalResponse`; `takenCodeMessage`; `codeLengthFor`, and `decodePrices`, its only caller's decoder; `PCodeIssued`, `nrIssuedCodeId`, `setIssuedCode`; the view's `codeLength`, `vrCodeLength` and `readViewChain`'s `taken` and `code_issued` branches, so `unread` names only `intentNonce`. - - Migration `20261022_drop_taken_codes`, SQLite and Postgres: `DROP INDEX` of `idx_name_registrations_issued_code`, then `ALTER TABLE … DROP COLUMN issued_code_id`; the down migration adds both back. SQLite 3.45 drops a column with a `REFERENCES` clause once its index is gone (checked 2026-10-22 with the `sqlite3` CLI); the migration test runs it on the bundled SQLite. + - Migration `20261022_drop_taken_codes`, SQLite and Postgres: first `UPDATE … SET phase = 'taken', price = 0, live = 0 WHERE phase = 'code_issued'` (its worth went into the issued code, which stays a valid prepaid code, so it gets no balance in T23 and every row still decodes); then `DROP INDEX` of `idx_name_registrations_issued_code`, then `ALTER TABLE … DROP COLUMN issued_code_id`. The down migration adds the column and index back; converted rows stay `taken`. No database holds names rows yet; the step keeps the migration correct anyway. SQLite has `DROP COLUMN` from 3.35: checked with the `sqlite3` CLI (3.45.1, `foreign_keys` on) for a column with a `REFERENCES` clause once its index is gone; the bundled SQLCipher is SQLite 3.39.2, and the migration test runs it there. - `codeConflict` in `Web/Http.hs` stays: badge invoices answer it. `/name` from `taken` stays until T24. - **Acceptance:** `rg -n 'code_issued|PCodeIssued|takenCode|TakenCode|issued_code_id|IssuedCode|codeLengthFor|decodePrices|"codeLength"|code_length_changed|code_unavailable' apps/simplex-store-service/src src tests` finds nothing; `POST /api/registration//code` answers `404 not_found`; the badge specs green, their `code_conflict` included. -- **Tests:** deleted with the code: the "a code instead" block, the `/code` half of the polls test, the `codeLengthFor` and `decodePrices` vectors, `takenCodeMessage`'s vector, the taken-order code vectors in `BadgeTests`. Changed: the live table without `code_issued`; the view's field list and `readViewChain`'s cases without `codeLength`; the store's column list; migration up and down with `20261022_drop_taken_codes`, on SQLite and Postgres; the wiring test's `404` for `/code`. +- **Tests:** deleted with the code: the "a code instead" block, the `/code` half of the polls test, the `codeLengthFor` and `decodePrices` vectors, `takenCodeMessage`'s vector, the taken-order code vectors in `BadgeTests`. Changed: the live table without `code_issued`; the view's field list and `readViewChain`'s cases without `codeLength`; the store's column list; migration up and down with `20261022_drop_taken_codes`, on SQLite and Postgres, with a `code_issued` row before it read back as `taken`, price 0, not live; the wiring test's `404` for `/code`. - **Depends on:** T15 (merged). It runs in wave 8 beside T18, which owns none of its files; both add lines to `Web/Names.hs` and `NamesOwnerTests.hs`, which the integrator merges. #### T23. Balances: storage, turns and the view -- **Goal:** the balance of contract section 8 stored, with every query the later tasks use, its turn, its fields in the view, and `placeOrder` able to close a balance's overdue holder. Nothing credits, holds or spends yet. -- **Files:** the two migration modules (migration `20261023_name_balances`); `StoreService/Names/Store.hs`; `StoreService/Names/Turns.hs`; new `StoreService/Names/Balances.hs`; `StoreService/Names/View.hs`; `StoreService/Names/Orders.hs`; the modules matching on `RegistrationPayment` (`rg -n 'RPInvoice|RPCode' apps/simplex-store-service/src`), for the new constructor only; `simplex-chat.cabal`; `tests/Test.hs`; `tests/Bots/StoreService/NamesStoreTests.hs`; new `tests/Bots/StoreService/NamesBalancesTests.hs`; `tests/Bots/StoreService/NamesOrdersTests.hs` (the view). +- **Goal:** the balance of contract section 8 stored, with every query the later tasks use, its turn, its fields in the view, every close of an order releasing its hold, and `placeOrder` able to close a balance's overdue holder. Nothing credits, holds or spends yet. +- **Files:** the two migration modules (migration `20261022_name_balances`, which sorts after `20261022_drop_taken_codes`); `StoreService/Names/Store.hs`; `StoreService/Orders.hs` (`closeOrder`'s release); `StoreService/Names/Turns.hs`; new `StoreService/Names/Balances.hs`; `StoreService/Names/View.hs`; `StoreService/Names/Orders.hs`; the modules matching on `RegistrationPayment` (`rg -n 'RPInvoice|RPCode' apps/simplex-store-service/src`), for the new constructor only; `simplex-chat.cabal`; `tests/Test.hs`; `tests/Bots/StoreService/NamesStoreTests.hs`; new `tests/Bots/StoreService/NamesBalancesTests.hs`; `tests/Bots/StoreService/NamesOrdersTests.hs` (the view). - **Do:** - - **Schema:** table `name_balances`: `registration_id` (the primary key: the order the balance belongs to), `amount` (cents left, `CHECK (amount >= 0)`), `credited`, `refunded` (default 0), `reserved_by` (the holder; unique, nullable), `refunded_at`, `created_at`, `updated_at`. Registrations gain `balance_source` (the order whose balance paid, indexed) and `balance_used` (cents, default 0). Up and down, SQLite and Postgres. - - **Types:** `RPBalance`, the payment of an order with neither invoice nor code; `nrBalanceSource`, `nrBalanceUsed`; `Balance`; `OTBalance` in `orderHash`, tagged `name-balance`, over the order fields, the owner, the source's id, `price`, the balance seen and the method or "". - - **Queries**, each one conditional statement reporting whether it applied: `getBalance` (a hold by an order whose invoice is past its expiry with no money received reads as free, by the sweep's `unfundedQuery`); `overdueHolder`; `balanceByReference` (a registration id, or a name invoice id); `creditBalance` (an upsert adding to `amount` and `credited`); `createBalanceRegistration` (a `queued` order, and its price spent where `amount` is the amount seen and nothing holds it); `createPaidRegistration` with an optional held part (the hold written after the registration's insert, on the same condition); `spendHeldBalance` (where this holder holds it and `amount ≥ used`; the hold cleared); `releaseHeldBalance` (by holder); `refundBalance` (where `amount` equals the cents given, above 0, and nothing holds it: `amount` 0, `refunded` and `refunded_at` set). + - **Schema:** table `name_balances`: `registration_id` (the primary key: the order the balance belongs to), `amount` (cents left, `CHECK (amount >= 0)`), `credited`, `refunded` (default 0), `reserved_by` (the holder; unique, nullable), `refunded_at`, `created_at`, `updated_at`. Registrations gain `balance_source` (the order whose balance paid, indexed) and `balance_used` (cents, default 0). `reserved_by` and `balance_source` reference registrations with the default action (restrict): the 30-day cleanup skips an unpaid order that a balance still names as its holder, and logs it, instead of deleting it. Up and down, SQLite and Postgres. + - **Data step**, in the same migration: `UPDATE … SET live = 0 WHERE phase = 'taken'`, and a balance of `price` for every `taken` order without a code and with `price` above 0 (credited = amount = price). Until T24 changes `orderLive`, `/name` from such a row is refused by `updateRegistration` (`NotLive`, `bad_request`). No database holds names rows yet. + - **Types:** `RPBalance`, the payment of an order with neither invoice nor code; `nrBalanceSource`, `nrBalanceUsed`; `Balance`; `OTBalance` in `orderHash`, tagged `name-balance`, over the order fields, the owner, the source's id, `price`, the balance seen and the method or ""; `ORBalanceChanged` in `OrderRefusal`, answered `409 balance_changed` with the current balance (`Web/Names/Orders.hs`). + - **Queries**, each one conditional statement reporting whether it applied: `getBalance` (`held` and `heldUntil` from the holder's invoice; a hold by an order whose invoice is past its expiry with no money received reads as none, by the sweep's `unfundedQuery`); `overdueHolder`; `balanceByReference` (a registration id, or a name invoice id); `balanceChain` (from an order back along `balance_source` to the first, each with its invoice, method and the cents it paid); `creditBalance` (an upsert adding to `amount` and `credited`); `createBalanceRegistration` (a `queued` order, and its price spent where `amount` is the amount seen and nothing holds it); `createPaidRegistration` taking the order's `OrderTerms` (so a difference spend is stored under its `name-balance` hash, which its exact resend looks up) and an optional held part (the hold written after the registration's insert, on the same condition); `spendHeldBalance` (where this holder holds it and `amount ≥ used`; the hold cleared); `releaseHeldBalance` (by holder); `refundBalance` (where `amount` equals the cents given, above 0, and nothing holds it: `amount` 0, `refunded` and `refunded_at` set). - **Turns:** `withBalanceTurn :: Turns -> RegistrationId -> IO a -> IO a`, taken after any label turn and never with a code turn; the module's header says so. - - **`Names/Balances.hs`:** `freeOverdueHold` (in the caller's transaction, closes the balance's overdue holder by `closeOverdue` and returns it as a `ClosedOrder`); `recordRefund :: Turns -> DBStore -> Text -> Int -> UTCTime -> IO (Either RefundRefusal RefundRecord)` (in the balance's turn and one transaction: the reference resolved, an overdue hold freed, then `refundBalance`; the record names the order, the cents, and what paid for it: the order's invoice and method, and the source of a part paid from a balance). - - **`placeOrder`** closes, besides the overdue orders of the same wallet and name, the extra overdue orders its caller names (a balance's holder) in its transaction, and returns them all for `announceClosed`. - - **The view:** `balance` and `fromBalance` (contract section 6), read with `getBalance` in `registrationView`; `find`'s `invoiceId` null for `RPBalance`. + - **Releases:** every close of an unpaid order releases its hold in the transaction that clears `live`: `closeOverdue` (`Names/Store.hs`: a next order, `/spend`) and `closeOrder` (`Orders.hs`: settlement's expiry, cancel, the poller's sweep). A late payment leaves the balance alone. + - **`Names/Balances.hs`:** `recordRefund :: Turns -> DBStore -> Text -> Int -> UTCTime -> IO (Either RefundRefusal RefundRecord)` (in the balance's turn and one transaction: the reference resolved, then `refundBalance`). It refuses any held balance, overdue holder or not, naming the holder and its invoice's expiry, so the console never closes an order it cannot announce or cancel at the provider; the poller's sweep closes it. The record names the order, the cents, and the whole `balanceChain`. + - **`placeOrder`** closes, besides the overdue orders of the same wallet and name, the extra orders its caller names (a balance's holder) in its transaction, and returns them all for `announceClosed`. A named holder no longer live is skipped (the caller's conditional hold or spend then decides); one still live that `closeOverdue` cannot close refuses with `ORBalanceChanged`. + - **The view:** `balance` (`amount`, `held`, `heldUntil`, `refunded`; no holder id) and `fromBalance` (contract section 6), read with `getBalance` in `registrationView`; `find`'s `invoiceId` null for `RPBalance`, and `find` returning the 50 newest and then every older order holding a balance above 0. - **`RPBalance` everywhere** behaves as a paid order: no code to reserve, release or spend; `/name`'s price cap; the worker keeps it through `taken` as it keeps an invoice order. -- **Acceptance:** migration up and down on SQLite and Postgres; for orders without a balance every answer is as before, except the view's new `balance` (null) and `fromBalance` (0). -- **Tests:** each query's effect, and its refusal leaving the row unchanged; two holds of one balance, one winning; a refund refused while held and accepted once released; `spendHeldBalance` refused for another holder; `amount = credited − spent − refunded` after a sequence of credits, a spend at once, a held spend and a refund; a hold by an overdue holder read as free and freed by `freeOverdueHold` (its invoice `expired`, its registration not live, the hold gone, all in the caller's transaction); a hold and a spend read back through a new store connection (a restart); `balanceByReference` by either id; the `name-balance` `order_hash` changing with each field and not with the timestamp or the signature; `recordRefund`'s refusals (no balance, another amount, held) and its record; `withBalanceTurn` running two holders of one balance one after the other, and nested in `withLabelTurn` without deadlock; the view's `balance` and `fromBalance`. +- **Acceptance:** migration up and down on SQLite and Postgres, with the data step; for orders without a balance every answer is as before, except the view's new `balance` (null) and `fromBalance` (0). +- **Tests:** each query's effect, and its refusal leaving the row unchanged; two holds of one balance, one winning; a refund refused while held and accepted once released; `spendHeldBalance` refused for another holder; `amount = credited − spent − refunded` after a sequence of credits, a spend at once, a held spend, a released hold and a refund, where spent is the sum of `balance_used` over the orders with this `balance_source` that are past `unpaid` (a closed unpaid holder recorded `balance_used` but spent nothing); the hold released by each close: `cancelOpenInvoice`, the sweep's `expireOverdue`, settlement's expiry, and `closeOverdue`; a hold by an overdue holder read as none and closed by `placeOrder` (its invoice `expired`, its registration not live, the hold gone, all in the caller's transaction); a named holder no longer live skipped, and one funded meanwhile refusing `ORBalanceChanged`; a hold and a spend read back through a new store connection (a restart); the cleanup skipping and logging an unpaid order still named as a holder; `balanceByReference` by either id; `balanceChain` over three orders; the `name-balance` `order_hash` changing with each field and not with the timestamp or the signature, and stored by `createPaidRegistration` for a difference spend; `recordRefund`'s refusals (no balance, another amount, held by an open holder, held by an overdue holder) and its record with the chain; `find` with 51 orders returning an older one with a balance; the data step on a `taken` paid row (balance of its price, not live) and on a `taken` code row (none); `withBalanceTurn` running two holders of one balance one after the other, and nested in `withLabelTurn` without deadlock; the view's `balance` and `fromBalance`. +- **Postgres:** one spec on two connections, in the wave's Postgres build: a spend at once against `refundBalance`, and `spendHeldBalance` against `refundBalance`, each pair started together many times, exactly one applying each time and `amount` never negative. The safety argument rests on READ COMMITTED re-reading the row's `WHERE` after the lock. - **Depends on:** T22. #### T24. The balance's life: taken, settlement, closes and moves -- **Goal:** a paid order that ends `taken` keeps its worth as a balance; a difference invoice spends its hold when paid and releases it when it closes; `/name` no longer moves a taken order, and a move to a cheaper name keeps the difference. -- **Files:** `StoreService/Names/Worker.hs`; `StoreService/Orders.hs`; `StoreService/Names/Store.hs`; `StoreService/Names/Orders.hs`; `StoreService/Web/Names/Owner.hs`; `tests/Bots/StoreService/` `NamesWorkerTests.hs`, `NamesOrdersTests.hs`, `NamesOwnerTests.hs`, `NamesStoreTests.hs`, `NamesPhasesTests.hs`. +- **Goal:** a paid order that ends `taken` keeps its worth as a balance; a difference invoice spends its hold when paid; `/name` no longer moves a taken order, and a move to a cheaper name keeps the difference. (T23 releases holds at every close.) +- **Files:** `StoreService/Names/Worker.hs`; `StoreService/Orders.hs` (settlement's spend); `StoreService/Names/Store.hs` (`orderLive`); `StoreService/Names/Orders.hs`; `StoreService/Web/Names/Owner.hs`; `tests/Bots/StoreService/` `NamesWorkerTests.hs`, `NamesOrdersTests.hs`, `NamesOwnerTests.hs`, `NamesStoreTests.hs`, `NamesPhasesTests.hs`. - **Do:** - The worker's `taken` for a paid order (invoice, balance or both): the `taken` write and `creditBalance` of its `price` in one transaction (registration before balance), in the name's turn and then the balance's turn. A code order releases its code as now. - `orderLive`: `taken` is not live. `movable` drops `taken`, so `/name` from `taken` answers `bad_request`. - Settlement (`nameSettled`): the queue write and `spendHeldBalance` in one transaction. A held spend that does not apply cannot happen while every close releases its hold; if it does, the order is queued all the same, as the buyer paid the difference, and the operator is alerted with both ids. - - Every close of an unpaid order releases its hold in the transaction that clears `live`: `closeOrder` (settlement's expiry, cancel, the poller's sweep) and `closeOverdue` (a next order, `/spend`, `recordRefund`). A late payment leaves the balance alone. - `/name` of a paid order to a cheaper name: `price` lowered and the difference credited to the order's balance in the move's transaction (registration before balance), in the label turns and then the balance's turn. -- **Tests:** two wallets' paid orders for one name, the later `taken` with a balance of its price and not live; a taken order paid wholly from a balance and one paid partly, each credited its whole `price` as a balance of its own, the source's balance untouched; a code order taken: its code released, no balance; a refused credit write leaving the order as it was (one transaction); a held order settled: `queued`, the hold spent, the source's `amount` down by `fromBalance`; its invoice cancelled, expired by the sweep, expired at settlement's read, and closed as overdue by a next order, each releasing the hold; a late payment after the close leaving the balance and alerting; settlement racing `recordRefund` (the refund refused while held, the balance spent after); the alert when the held spend does not apply (a hold removed by hand); `/name` from `taken` refused; a cheaper move crediting the difference, and a dearer one still `price_too_high`; a held poll woken by the `taken` write and not by a balance change. +- **Tests:** two wallets' paid orders for one name, the later `taken` with a balance of its price and not live; a taken order paid wholly from a balance and one paid partly, each credited its whole `price` as a balance of its own, the source's balance untouched; a code order taken: its code released, no balance; a refused credit write leaving the order as it was (one transaction); a held order settled: `queued`, the hold spent, the source's `amount` down by `fromBalance`; a difference invoice funded before its expiry time and settled after it, still `open` in the database: `queued`, the hold spent; a late payment after the close leaving the balance and alerting; settlement racing `recordRefund` (the refund refused while held, the balance spent after); the alert when the held spend does not apply (a hold removed by hand); `/name` from `taken` refused; a cheaper move crediting the difference, a cheaper move whose compare-and-set loses leaving no credit, and a dearer one still `price_too_high`; `fromBalance` unchanged by a move; a held poll woken by the `taken` write and not by a balance change. - **Depends on:** T23, T18. #### T25. Spending a balance @@ -333,7 +335,7 @@ The owner's decisions of 2026-10-22 remove the code for a taken order and keep a - **Goal:** `POST /api/registration/:id/spend`. - **Files:** `StoreService/Web/Names/Orders.hs` (the handler; `createAtProvider` takes the invoice's amount and the insert, shared with `/api/invoice`; the resend answer for `RPBalance`); `StoreService/Names/Balances.hs` (the balance check and the order's placement); `StoreService/Names/Signatures.hs` (`spendMessage`); `StoreService/Web/Names.hs` (its route); new `tests/Bots/StoreService/NamesSpendTests.hs`; `tests/Bots/StoreService/NamesWiringTests.hs` (the route's `405` with `Allow`); `tests/Test.hs`; `simplex-chat.cabal`. - **Do:** the contract's `/spend` in its order of checks, under the new name's label turn and then the balance's turn: the order fields by `parseOrderFields`; existing orders by `existingOrder` with the `name-balance` hash; the balance's overdue holder closed with the wallet's overdue orders in `placeOrder`'s transaction; a covered price by `createBalanceRegistration`, a difference by the provider and then `createPaidRegistration` with its hold; a write refused at the balance answering `balance_changed`, with the provider's invoice cancelled as for an order that lost; answers after the turn (`Answer`), publishing closed orders and cancelling their invoices at the provider. -- **Tests:** a covered spend (`queued`, `fromBalance` the price, the source's `amount` less the price) and an exact one (`amount` 0); a difference spend (the invoice view with `amount` the difference, the registration `unpaid` with `fromBalance` the balance, the source's `reservedBy` its id); an exact resend in `unpaid` and in later phases (moved there by T23's store writes), with a new timestamp and signature, after the balance changed; `balance_changed` for a stale `balance`, a held balance, and an order with none (`balance: null`); a `method` with a covered price, and none with a difference, `bad_request`; another signer, a stale timestamp, an unknown id `404`; `order_exists`; `name_taken`; `catalog_changed`; `name_check_unavailable`; `provider_unavailable`; a balance held by an overdue order spent (that order closed, its invoice `expired`, published, cancelled at the provider); two concurrent spends of one balance for two names, one winning and the other `balance_changed` with its provider invoice cancelled; a spend racing `recordRefund`, one winning; the create limit; the `sealed` and body caps. +- **Tests:** a covered spend (`queued`, `fromBalance` the price, the source's `amount` less the price) and an exact one (`amount` 0); a difference spend (the invoice view with `amount` the difference, the registration `unpaid` with `fromBalance` the balance, the source's balance `held` with `heldUntil` the invoice's expiry); an exact resend of a difference spend in `unpaid` (found by its `name-balance` hash, not met as `order_exists`) and in later phases (moved there by T23's store writes), with a new timestamp and signature, after the balance changed; an `/api/invoice` body with the same order fields not matching it; `balance_changed` for a stale `balance`, a held balance, and an order with none (`balance: null`); a `method` with a covered price, and none with a difference, `bad_request`; another signer, a stale timestamp, an unknown id `404`; `order_exists`; `name_taken`; `catalog_changed`; `name_check_unavailable`; `provider_unavailable`; a balance held by an overdue order spent (that order closed, its invoice `expired`, published, cancelled at the provider); the holder closed meanwhile by a cancel (the spend goes through) and funded meanwhile (`balance_changed`, the provider invoice cancelled); two concurrent spends of one balance for two names, one winning and the other `balance_changed`; the write refused at the balance after the provider invoice was made, by a store write between check 6 and the transaction through a second `DBStore` (`balance_changed`, the provider invoice cancelled); a spend racing `recordRefund`, one winning; the create limit; the `sealed` and body caps. - **Depends on:** T23. Its paid path is end to end only with T24's settlement: the wave-10 integrator adds that spec (below). #### T26. The operator's refund @@ -341,11 +343,11 @@ The owner's decisions of 2026-10-22 remove the code for a taken order and keep a - **Goal:** support records a refund, which zeroes a balance (contract section 8, Balances). - **Files:** `StoreService/Service.hs` (the `//refund` parser, its run and replies, the usage line); `apps/simplex-store-service/README.md` (the operator commands, `//refund` with support's steps); `tests/Bots/StoreService/GroupIntegrationTests.hs` (the operator command specs); `tests/Bots/StoreService/BotTests.hs` (`badgeCmdUsage`). - **Do:** - - `//refund ` answers the balance and the command that records it: "Order : balance $100.00 (credited $100.00, refunded $0.00), paid by btc, invoice . To record its refund: //refund 10000". A held balance names its holder, to wait for its invoice to close; an order without a balance says so. - - `//refund ` runs `recordRefund` and answers "Recorded a refund of $100.00 for order . Refund it now: ", or why nothing was recorded: no balance; the balance is $X now, not the cents given; held by . + - `//refund ` answers the balance and the command that records it: "Order : balance $100.00 (credited $100.00, refunded $0.00). Paid by: . To record its refund: //refund 10000". The chain lists, from this order back along `balance_source` to the first, each order's invoice, method and cents. A held balance names its holder and its invoice's expiry; an order without a balance says so. + - `//refund ` runs `recordRefund` and answers "Recorded a refund of $100.00 for order . Refund it now, by: ", or why nothing was recorded: no balance; the balance is $X now, not the cents given; "held by order , its invoice expired at