plans: names late credit tasks after re-review

This commit is contained in:
shum committed 2026-10-22 09:55:52 +00:00
1 parent 0244e247d7
commit a047ec1801
1 file changed
+17 -15
@@ -301,12 +301,12 @@ The owner's decisions of 2026-10-22 remove the code for a taken order and keep a
#### 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, 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).
- **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; `StoreService/Web/Names/Orders.hs` also for `/api/invoice`'s call of `createPaidRegistration` (line 129 at e6fb909d6), which passes its `OTInvoice` terms; `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). Name invoices gain `late_credited` (cents of a late payment already credited from this invoice, 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.
- **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). Name invoices gain `late_credited` (cents of a late payment already credited from this invoice, default 0). Payments gain `confirmed_amount` (cents of confirmed money, default 0; T24 writes it, only rising). `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); `creditLatePayment` (in the caller's transaction, after the invoice's lock: the cents the payment row reports received, less the invoice's `late_credited`, added to the order's balance by `creditBalance`, and `late_credited` raised to it, conditional on its old value; nothing when the increase is not positive; a result saying whether the payment row has no cents figure); `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).
- **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, following `balance_source` only from orders past `unpaid`, since a closed difference order released its source's balance; each with its invoice, method, the cents it paid and its invoice's `late_credited`); `creditLatePayment` (in the caller's transaction, after the invoice's lock: the payment row's `confirmed_amount` less the invoice's `late_credited`, added to the order's balance by `creditBalance`, and `late_credited` raised to it, conditional on its old value; nothing when the increase is not positive; it returns the cents credited, and whether money is seen but not confirmed: received money with `confirmed_amount` at or below `late_credited`); `uncreditedLateInvoices` (closed unpaid name invoices past a given time whose payment row holds money and whose `late_credited` is 0); `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 = refunded + cents`, `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.
- **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 released balance alone (T24 credits it to the closed order's own balance).
- **`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`.
@@ -314,22 +314,24 @@ The owner's decisions of 2026-10-22 remove the code for a taken order and keep a
- **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, 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.
- **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; credit, refund, credit, refund, with `refunded` the sum and the invariant holding; `balanceChain` of a closed difference order stopping at itself; `creditLatePayment` with nothing confirmed, with a confirmed rise, and repeated; `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`, `spendHeldBalance` against `refundBalance`, and `creditLatePayment` against `refundBalance` (the refund refusing once the credit committed, as `amount = cents` is re-checked), each pair started together many times, exactly one conditional write 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 late payment is credited to its order's balance (owner, 2026-10-22); 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, late credits); `StoreService/Poller.hs` (the cleanup's line); `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`.
- **Files:** `StoreService/Names/Worker.hs`; `StoreService/Orders.hs` (settlement's spend, late credits); `StoreService/Poller.hs` (the cleanup's lines); `StoreService/Providers.hs` (`Received` gains `rcvConfirmed`); `StoreService/Providers/BTCPay.hs` (each payment's `value` decoded, the settled sum); `StoreService/Providers/Stripe.hs` (the succeeded amount as confirmed); `StoreService/Store/Invoices.hs` (`confirmed_amount` in the payment upsert, only rising); the fake providers in the tests; `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.
- **Late payments** (contract sections 1 and 8): every outcome that was `NOLate` (paid after the close, `NotLive`, `LiveOrderExists`, a provider close holding money, a new figure on a closed invoice) and `cancelOpenInvoice`'s late case run `creditLatePayment` in their own transaction, so the closed order's balance gains what arrived and a repeated or growing report credits only its increase. The operator alert becomes an info log naming the invoice, the order and the cents credited: the money is the buyer's balance, and support acts only when the buyer asks. A payment row with no cents figure credits nothing and keeps the alert (`logError`), for support to settle by hand.
- The poller: the cleanup's "received a payment; refund it" line becomes an info line saying the order is kept with its balance; its comment on settlement's alert follows. The badge poller's refund alert is unchanged.
- **Confirmed money** (contract section 8): `Received` gains `rcvConfirmed`. BTCPay: the sum of the chosen method's payments with status `Settled`, each `value` times the method's rate, in cents. Stripe: the succeeded amount (`SigSettled` only), else 0. The payment upsert writes `confirmed_amount` with the larger of the old and new values, and `newPayment` counts a rise in it as a new payment even when the totals are unchanged, so an `Expired` invoice whose late payment confirms later reaches the late outcome again. Badge settlement ignores it.
- **Late payments:** every outcome that was `NOLate` (paid after the close, `NotLive`, a provider close holding money, a new figure on a closed invoice) and `cancelOpenInvoice`'s late case run `creditLatePayment` inside the same transaction, beside `upsertPayment`, never after its commit (a crash between two commits would lose the credit: the same figure is not a new payment again). `NameOutcome`'s late case carries the cents credited out of the transaction for the log. A credit is an info log naming the invoice, the order and the cents: the money is the buyer's balance, and support acts only when the buyer asks. Money seen but not confirmed (a mempool payment, Monero confirming with a zero figure) credits nothing and is an info log saying it is pending.
- **`LiveOrderExists` at settlement** (another live order holds the commitment, or the owner and the name; impossible while the unique indexes hold): the order is closed there as `closeOrder` closes it (not live, its hold released), its payment credited as a late payment, and the operator alerted, since an invariant broke. No sweep closes a paid invoice, so leaving it live would keep its hold for ever.
- The poller: the cleanup's "received a payment; refund it" line becomes an info line saying the order is kept with its balance; its comment on settlement's alert follows. Its daily pass alerts (`logError`, once a day per invoice) for each of `uncreditedLateInvoices` past `settleWindow`: money seen that never confirmed within the poller's read window, for support to check at the provider. The badge poller's refund alert is unchanged.
- `/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`; 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 credited to the closed order's balance, the released source balance untouched, logged at info; a full late payment, a crypto payment growing over two reports (credited twice, the increase only), the same report twice (credited once), a provider close holding a part payment, a cancel meeting money, and a payment row without a cents figure (nothing credited, the alert kept); the cleanup keeping such an order with its balance; the credited balance spent by `/spend` from the closed order and refunded by `recordRefund`; 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.
- **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 credited to the closed order's balance, the released source balance untouched, logged at info; a full late payment, a crypto payment confirming over two reports (credited twice, the increase only), the same report twice (credited once), a provider close holding a confirmed part payment, a cancel meeting confirmed money; an unconfirmed late payment crediting nothing (logged as pending), then credited once when its payment reads `Settled` with the totals unchanged; an unconfirmed payment replaced (RBF) or dropped never crediting, and alerted by the daily pass once `settleWindow` has passed; Monero's zero figure while confirming logged as pending, not alerted; the BTCPay settled sum decoded from a recorded payment-methods body with one `Processing` and one `Settled` payment; Stripe's succeeded amount confirmed; the credit and the payment written in one transaction (a failure after the credit leaves neither); `LiveOrderExists` at settlement closing the order, crediting and alerting; a credit while the closed order's balance is held by a spend (the held spend settles, the credit stays); a covered `/spend` that read the amount before a credit answering `balance_changed`; credit, refund, late credit, refund, with `refunded` the sum; the cleanup keeping such an order with its balance; the credited balance spent by `/spend` from the closed order and refunded by `recordRefund`; 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
@@ -349,7 +351,7 @@ The owner's decisions of 2026-10-22 remove the code for a taken order and keep a
- `//refund <reference> <cents>` runs `recordRefund` and answers "Recorded a refund of $100.00 for order <id>. Refund it now, by: <the chain>", or why nothing was recorded: no balance; the balance is $X now, not the cents given; "held by order <id>, its invoice expired at <time>; the poller closes it within <grace>; run the command again then" (or "open until <time>" for a holder not yet overdue).
- The reference is an invoice id or a registration id, as the page shows it. The command is the service console's `//` only, as `//revoke` of a prepaid code is: it takes the balance's turn, which the group lane does not hold. A second process on the same database (the CLI mode) is kept safe by the conditional write, not the turn.
- Each recorded refund is logged with its reference and cents.
- **Tests:** a refund shown, recorded, then shown as refunded; each refusal; a held balance refused, then recorded once its holder's invoice is cancelled (T23's release in `closeOrder`); an overdue holder refused with its expiry, then recorded once the sweep closed it; a chain of three orders in the reply; a reference by invoice id and by registration id; the usage line.
- **Tests:** a refund shown, recorded, then shown as refunded; each refusal; a held balance refused, then recorded once its holder's invoice is cancelled (T23's release in `closeOrder`); an overdue holder refused with its expiry, then recorded once the sweep closed it; a chain of three orders in the reply; a late-paid difference order whose chain stops at itself, its cents marked late; a reference by invoice id and by registration id; the usage line.
- **Depends on:** T23.
#### T27. The gas key in the README
@@ -398,8 +400,8 @@ Each wave starts from `sh/store-service` as merged at the end of the last one. "
| 6 | T14, T15, T20 | T14: `Web/Names/Registration.hs`. T15: `Names/Orders.hs`. T20: `Web/Server.hs`, `README.md` |
| 7 | T17 | `Service.hs`, `Names/Worker.hs`, `FakeEthNode.hs` |
| 8 | T18, T22 | T18: `Web/Names/Registration.hs`, `Names/Worker.hs`. T22: `Names/Orders.hs`, `Names/Store.hs`, the migrations, `Names/View.hs`, `Names/Signatures.hs`, `Web/Names/Orders.hs`, `Web/Names/Owner.hs` |
| 9 | T23, T27 | `Names/Store.hs`, the migrations, `Orders.hs`, `Names/Turns.hs`, `Names/Balances.hs`, `Names/View.hs`, `Names/Orders.hs`, and, for `RPBalance`'s case and `balance_changed`'s response only, `Names/Worker.hs`, `Web/Names/Orders.hs`, `Web/Names/Owner.hs`. T27: `README.md` |
| 10 | T24, T25, T26 | T24: `Names/Worker.hs`, `Orders.hs`, `Poller.hs`, `Names/Store.hs`, `Names/Orders.hs`, `Web/Names/Owner.hs`. T25: `Web/Names/Orders.hs`, `Names/Balances.hs`, `Names/Signatures.hs`. T26: `Service.hs`, `README.md` |
| 9 | T23, T27 | `Names/Store.hs`, the migrations, `Orders.hs`, `Names/Turns.hs`, `Names/Balances.hs`, `Names/View.hs`, `Names/Orders.hs`, and, for `RPBalance`'s case, `balance_changed`'s response and `createPaidRegistration`'s call only, `Names/Worker.hs`, `Web/Names/Orders.hs`, `Web/Names/Owner.hs`. T27: `README.md` |
| 10 | T24, T25, T26 | T24: `Names/Worker.hs`, `Orders.hs`, `Poller.hs`, `Providers.hs`, `Providers/BTCPay.hs`, `Providers/Stripe.hs`, `Store/Invoices.hs`, `Names/Store.hs`, `Names/Orders.hs`, `Web/Names/Owner.hs`. T25: `Web/Names/Orders.hs`, `Names/Balances.hs`, `Names/Signatures.hs`. T26: `Service.hs`, `README.md` |
| 11 | T21 | `README.md` |
The wave-10 integrator, after merging T24, T25 and T26, adds one end-to-end spec to `NamesSpendTests.hs`: a difference spend paid at `FakeBTCPay` and settled (the order `queued`, the hold spent); the same cancelled (the hold released); a covered spend's order ending `taken` (a balance of its own, its whole `price`); a `//refund` of the rest; and a difference invoice paid after it closed, credited to its order's balance. Wave 10 has no other dependency between its tasks.
@@ -423,7 +425,7 @@ In simplex.domains, branch `sh/names-webapp`, in the webapp's workstream beside
- **Goal:** the mock serves the contract's balances and `/spend`; `src/api.ts` and `src/flow.ts` use them.
- **Files:** `dev/mock/service.ts`; `dev/mock.ts`; `src/api.ts`; `src/flow.ts`; `src/store.ts`; `test/api.test.ts`; `test/flow.test.ts`; the mock's tests.
- **Do (mock):** a balance per order (`amount`, `credited`, `refunded`, the holder); credited with `price` when a paid order is taken (`/control/take`), and with the difference on a cheaper `retarget`; `taken` not live and not movable; `spend` in the contract's order of checks, with `balance_changed`; settlement (`/control/settle`) queues the order and spends its hold; a cancel, an expiry (`/control/expire`, `/control/overdue`) and an overdue order closed by a next order or a spend release it; a hold by an overdue order reads free; `POST /control/refund/:reference`, with optional `cents`, answering as `recordRefund`; the view's `balance` (`amount`, `held`, `heldUntil`, `refunded`) and `fromBalance`; a refund of a held balance refused; a late payment (`/control/settle` on a closed invoice, and on an expired one with a part payment) credited to that order's balance, each figure once; `find`'s `invoiceId` null for an order paid wholly from a balance, and older orders with a balance after the 50 newest.
- **Do (mock):** a balance per order (`amount`, `credited`, `refunded`, the holder); credited with `price` when a paid order is taken (`/control/take`), and with the difference on a cheaper `retarget`; `taken` not live and not movable; `spend` in the contract's order of checks, with `balance_changed`; settlement (`/control/settle`) queues the order and spends its hold; a cancel, an expiry (`/control/expire`, `/control/overdue`) and an overdue order closed by a next order or a spend release it; a hold by an overdue order reads free; `POST /control/refund/:reference`, with optional `cents`, answering as `recordRefund`; the view's `balance` (`amount`, `held`, `heldUntil`, `refunded`) and `fromBalance`; a refund of a held balance refused; a late payment (`/control/settle` on a closed invoice, and on an expired one with a part payment) credited to that order's balance, each confirmed figure once, and a `/control/partial` or `/control/confirming` on a closed invoice crediting nothing until settled; `find`'s `invoiceId` null for an order paid wholly from a balance, and older orders with a balance after the 50 newest.
- **Do (client):** `RegistrationView` gains `balance` and `fromBalance`; `api.spend`; `flow.spendBalance(record, label, links, quote, method)` builds the order with the source record's owner code (the same owner, `sealed` under it), signs `simplex.domains spend …`, and keeps the new order's record with that owner code, `method` (`"balance"` when the balance covers it) and `balanceSource` (the paying order's id, so the page can name a balance's holder); `OrderRecord.method` gains `"balance"`; `paid` is the registration's `price`, not the invoice `amount`, for every order (`paidWith` today takes `amount`, which for a difference invoice is the difference only); `balance_changed` gives the current balance to the caller.
- **Tests:** the mock's `spend` in each answer and refusal of T25's list; settlement and every close of a hold; a refund; `spendBalance`'s message and body against a vector from a fixed key, the same vector T25's test pastes.
- **Depends on:** W1f-1.
@@ -433,7 +435,7 @@ In simplex.domains, branch `sh/names-webapp`, in the webapp's workstream beside
- **Goal:** the buyer sees and uses a balance in Your names, the search, Check your order, the taken page and the payment page.
- **Files:** `src/views/names.ts`, `search.ts`, `checkout.ts`, `order.ts`, `shell.ts` (the support popup); `src/screens/names.ts`, `search.ts`, `checkout.ts`, `order.ts`; their unit tests; `e2e/taken.spec.ts`; new `e2e/balance.spec.ts`.
- **Do:**
- **Your names:** an order whose view has a `balance` with an `amount` shows "Balance $X" with **Use** (the search, `?balance=<registrationId>`) and **Refund** (a popup: "Write to chat@simplex.chat with the reference <id>; support refunds it by hand", the reference being the order's invoice id, else its registration id); a held balance says it is in use by an open invoice until `heldUntil`, and links the holder when this browser's records have it (`balanceSource`); a refunded one "Refunded $X by support". An order paid from a balance says so in its line.
- **Your names:** an order whose view has a `balance` with an `amount` shows "Balance $X" with **Use** (the search, `?balance=<registrationId>`) and **Refund** (a popup: "Write to chat@simplex.chat with the reference <id>; support refunds it by hand", the reference being the order's invoice id, else its registration id); a balance and a refund show together ("Balance $X" and "Refunded $Y", `refunded` being the sum of refunds); a held balance says it is in use by an open invoice until `heldUntil`, and links the holder when this browser's records have it (`balanceSource`); a refunded one "Refunded $X by support". An order paid from a balance says so in its line.
- **The search with a balance:** "Your balance: $X" in place of the retarget alert; an available name says "Covered by your balance; $Y stays" or "Pay $Z more".
- **Check your order** with a balance: the price, "Balance −$X", then either "Nothing to pay" with Register, or "Pay $Z more" with the payment methods; the order goes out by `flow.spendBalance`; on `balance_changed` the page reads the balance again and says what changed.
- **The taken page:** "Someone registered it first. Your payment of $X is kept as a balance: use it for another name, paying only the difference, or ask support for a refund." with Use and Refund; Choose another name leaves it.
@@ -488,7 +490,7 @@ Manager rulings (2026-10-22): the refund popup writes to chat@simplex.chat (`CON
- **`/name` no longer moves a taken order**: the balance spend replaces it, so one path buys another name with a taken order's money. `/name` from `failed` and after refused links stays.
- **A move to a cheaper name keeps the difference as balance** instead of refunding nothing, as the owner's leftover rule does for a spend (manager's ruling, 2026-10-22).
- **A taken order credits its whole `price`**, including any part from another balance, as a balance of its own: two balances, never merged.
- **A late payment credits what the provider reports received**, in cents at the invoice's rate, once per figure (`late_credited` per invoice), in settlement's transaction; the alert becomes an info log, kept as an alert only when no cents figure is reported.
- **A late payment credits only confirmed money** (BTCPay payments `Settled`, Stripe succeeded), in cents at the invoice's rate, each rise once (`confirmed_amount` per payment, `late_credited` per invoice), inside settlement's own transaction. Reason: a 0-conf payment can be replaced or dropped, and a credit spent by a covered `/spend` registers a name at once. Money seen but unconfirmed is logged as pending; only an invoice still holding money with nothing credited after `settleWindow` alerts.
- **Balance changes leave `version` alone**: the order is final once `taken`, and the page reads the view for its balance.
- **The view says a balance is held, not by whom** (`held`, `heldUntil`): the holder's id would give whoever holds the source's link the holder's capabilities. The page names the holder from its own records.
- **`fromBalance` is fixed when the order is made**; a move does not change it.