From 35d40fa558c3b000b4e12fa0481896c267b4f60a Mon Sep 17 00:00:00 2001 From: shum Date: Thu, 22 Oct 2026 13:52:04 +0000 Subject: [PATCH] plans: names T24 shows holds, late waits, find --- plans/names-codes/2026-10-21-store-service-tasks.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) 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 0876948c7c..d3ad7f4865 100644 --- a/plans/names-codes/2026-10-21-store-service-tasks.md +++ b/plans/names-codes/2026-10-21-store-service-tasks.md @@ -321,7 +321,7 @@ The owner's decisions of 2026-10-22 remove the code for a taken order and keep a #### 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 lines); `StoreService/Providers.hs` (`Received` gains `rcvConfirmed`; `Provider` gains `pCurrentRate :: Maybe (CryptoCurrency -> IO (Either ProviderError Rate))`, `Nothing` for Stripe, so the step never asks a card provider for a rate); `StoreService/Providers/BTCPay.hs` (each payment's `value` decoded, the confirmed crypto, the store rates call); `StoreService/Providers/Stripe.hs` (the succeeded amount as confirmed; no rate); `StoreService/Store/Invoices.hs` (the confirmed figures in the payment write, only rising); new `StoreService/Names/LateCredits.hs` (the credit step); `tests/Bots/StoreService/BTCPayTests.hs`, `StripeTests.hs` and `WebTests.hs` (their `Received` values, at `BTCPayTests.hs:169, 172, 204, 499, 603, 606`, `StripeTests.hs:76, 79` and `WebTests.hs:2122` at e6fb909d6); `tests/Bots/StoreService/FakeBTCPay.hs` (payments with a status and `value` per state, the rates endpoint, a failing rates read) and `FakeStripe.hs`; new `tests/Bots/StoreService/NamesLateCreditTests.hs`; `simplex-chat.cabal` (`Names/LateCredits.hs`, `NamesLateCreditTests.hs`); `tests/Test.hs`; and in `WebTests.hs` also the stub provider's `pCurrentRate` (`WebTests.hs:880`) and its direct `upsertPayment` calls (`WebTests.hs:2432-2443`) if the signature changes; `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/Names/View.hs` (`latePayment`, `heldUntil` for a funded holder); `StoreService/Web/Names/Owner.hs` also for `find`'s new fields; the two migration modules (migration `20261022_name_late_wait`, sorting after `20261022_name_balances`); `StoreService/Poller.hs` (the cleanup's lines); `StoreService/Providers.hs` (`Received` gains `rcvConfirmed`; `Provider` gains `pCurrentRate :: Maybe (CryptoCurrency -> IO (Either ProviderError Rate))`, `Nothing` for Stripe, so the step never asks a card provider for a rate); `StoreService/Providers/BTCPay.hs` (each payment's `value` decoded, the confirmed crypto, the store rates call); `StoreService/Providers/Stripe.hs` (the succeeded amount as confirmed; no rate); `StoreService/Store/Invoices.hs` (the confirmed figures in the payment write, only rising); new `StoreService/Names/LateCredits.hs` (the credit step); `tests/Bots/StoreService/BTCPayTests.hs`, `StripeTests.hs` and `WebTests.hs` (their `Received` values, at `BTCPayTests.hs:169, 172, 204, 499, 603, 606`, `StripeTests.hs:76, 79` and `WebTests.hs:2122` at e6fb909d6); `tests/Bots/StoreService/FakeBTCPay.hs` (payments with a status and `value` per state, the rates endpoint, a failing rates read) and `FakeStripe.hs`; new `tests/Bots/StoreService/NamesLateCreditTests.hs`; `simplex-chat.cabal` (`Names/LateCredits.hs`, `NamesLateCreditTests.hs`); `tests/Test.hs`; and in `WebTests.hs` also the stub provider's `pCurrentRate` (`WebTests.hs:880`) and its direct `upsertPayment` calls (`WebTests.hs:2432-2443`) if the signature changes; `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`. @@ -337,9 +337,14 @@ The owner's decisions of 2026-10-22 remove the code for a taken order and keep a - A credit is an info log naming the invoice, the order, the units, the rates and the cents. Money `latePending` is an info log, only for uncredited money. - **Why not in settlement's transaction:** the rate read cannot run under the invoice's lock. The stored state (confirmed money not all credited) is what the poller's pass re-reads, so a crash or a failed read between the settlement's commit and the credit loses nothing, which the earlier same-transaction rule existed to ensure. - **`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. + - **What the page needs to see** (contract sections 6 and 8, from the webapp's W1f review; new in T24 so T23, in its fix round, is unaffected): + - `heldUntil` null while the holder's invoice holds money (funded, whatever its expiry), `held` still true; `getBalance` (T23's) gains the funded case. The holder's expiry otherwise, as T23 reads it. + - `balance` stays an object once credited, `amount` 0 when spent or refunded; null only before the first credit (T23's view already does this; T24 adds the test). + - `latePayment`: null, or `{"waiting": …}` for an `unpaid`, not live order whose money is not all credited: `support` when `name_invoices.late_support` is set; else `confirming` when received units (cents for card) exceed confirmed; else `crediting`. Migration `20261022_name_late_wait` adds `late_support` (text, nullable: `high_speed`, `units`, `rate_floor`, `unconfirmed`; the view shows only `support`, the log and the alert the reason). It is set by settlement's payment write (an invoice made under `HighSpeed`, units that could not be counted), by the credit step (a rate below the floor), and by `claimLateAlert` (unconfirmed money past the window), and never cleared: once all is credited, `latePayment` is null whatever it holds. + - `find`: each entry gains `balanceSource` (its `balance_source`, or null) and `heldBy` (the holder of its balance, read as `getBalance` reads `held`, or null), by a join in `findRegistrations` (`Names/Store.hs`) and `foundJSON` (`Web/Names/Owner.hs`). - 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. It runs the credit pass at its idle cadence. Its daily pass, for each `lateAlertDue` entry whose payment row is older than `settleWindow`, claims it with `claimLateAlert` and logs only when the claim applied (`logError`). The alert names crypto received, confirmed and credited in units with the currency (cents for card), and its reason: unconfirmed money, "invoice of order : received, confirmed, credited; refund by hand only a transaction confirmed on chain that BTCPay no longer monitors; one still `Processing` there waits"; confirmed money waiting on a failing rate read, "…: confirmed, the credit waits on the rate read; do not refund; fix the rate read"; confirmed money waiting on a rate below the floor, "…: confirmed; the coin's price fell by more than half since the invoice (, ); the owner decides: credit at the current rate, or refund by hand"; or money that could not be counted in units, "…: an amount BTCPay reported with more decimals than its unit; check it at the provider". It alerts again only when more money arrives. 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 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 once by the daily pass after its payment row passed `settleWindow`, never again; a payment first seen just before the window not alerted until its own row is past it; a part credit with an unconfirmed rest alerted once; Monero's zero figure while confirming logged as pending, not alerted; a falling price (the current rate below the invoice's: credited at the current rate) and a rising one (credited at the invoice's), each storing both rates and the rate used; a failed rate read crediting nothing, then credited by the next pass; a zero rate, a rate with `errors`, a missing pair and a rate below the floor each crediting nothing (the floor logged as an error, and its alert naming both rates and the owner's decision); trailing zeros past the unit accepted; a badge invoice whose amount has a ninth non-zero decimal read and settled as before, only its units missing; one rate read per currency pair per pass for several due invoices, none when nothing is due, and one warning per pass while the rate source fails; an invoice made under `HighSpeed` with its payment `Settled`, the configuration then changed to `MediumSpeed`, still not credited; a cancel meeting money reading no rate, its money credited by the pass; a late `Settled` invoice (status `paid`, order `unpaid`) with the credit step not run, credited by the pass; the step refusing to credit an order that is live again; the alert's two reasons with their figures in units, one alert of two instances, and a second alert when more money arrives; a crash between the settlement's commit and the credit (the credit step not run) credited by the pass; the confirmed figure from a body with a non-zero `fee`, and from two `Settled` payments whose separate rounding would differ (rounded once, capped at `paymentMethodPaid`); an invoice-level `Settled` with one payment still `Processing` credited in full; a settled sum that falls (a payment turned `Invalid`) leaving the stored figure and the credit, and the same sum again crediting nothing; an invoice past `settleWindow`, not read by the pass, credited after a hinted read; a cancel meeting unconfirmed money crediting nothing, then once when the poller reads it `Settled`; `HighSpeed` configured: a late `Settled` payment not credited, pending and alerted; Stripe's succeeded amount confirmed, credited without a rate read; the decoders against the cited shapes (hand-written fixtures named as such until a regtest capture replaces them); `LiveOrderExists` at settlement closing the order, crediting and alerting; the same for a difference order, the source's hold released in the settlement's transaction and the order's own balance credited after it, by the credit step; 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. +- **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 once by the daily pass after its payment row passed `settleWindow`, never again; a payment first seen just before the window not alerted until its own row is past it; a part credit with an unconfirmed rest alerted once; Monero's zero figure while confirming logged as pending, not alerted; a falling price (the current rate below the invoice's: credited at the current rate) and a rising one (credited at the invoice's), each storing both rates and the rate used; a failed rate read crediting nothing, then credited by the next pass; a zero rate, a rate with `errors`, a missing pair and a rate below the floor each crediting nothing (the floor logged as an error, and its alert naming both rates and the owner's decision); trailing zeros past the unit accepted; a badge invoice whose amount has a ninth non-zero decimal read and settled as before, only its units missing; one rate read per currency pair per pass for several due invoices, none when nothing is due, and one warning per pass while the rate source fails; an invoice made under `HighSpeed` with its payment `Settled`, the configuration then changed to `MediumSpeed`, still not credited; a cancel meeting money reading no rate, its money credited by the pass; a late `Settled` invoice (status `paid`, order `unpaid`) with the credit step not run, credited by the pass; the step refusing to credit an order that is live again; the alert's two reasons with their figures in units, one alert of two instances, and a second alert when more money arrives; a crash between the settlement's commit and the credit (the credit step not run) credited by the pass; the confirmed figure from a body with a non-zero `fee`, and from two `Settled` payments whose separate rounding would differ (rounded once, capped at `paymentMethodPaid`); an invoice-level `Settled` with one payment still `Processing` credited in full; a settled sum that falls (a payment turned `Invalid`) leaving the stored figure and the credit, and the same sum again crediting nothing; an invoice past `settleWindow`, not read by the pass, credited after a hinted read; a cancel meeting unconfirmed money crediting nothing, then once when the poller reads it `Settled`; `HighSpeed` configured: a late `Settled` payment not credited, pending and alerted; Stripe's succeeded amount confirmed, credited without a rate read; the decoders against the cited shapes (hand-written fixtures named as such until a regtest capture replaces them); `LiveOrderExists` at settlement closing the order, crediting and alerting; the same for a difference order, the source's hold released in the settlement's transaction and the order's own balance credited after it, by the credit step; 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; a funded holder past its expiry shown `held` with `heldUntil` null, and an unfunded one before its expiry with its `expiresAt`; a balance spent to 0 and one refunded read as objects with `amount` 0; `latePayment` `confirming` for unconfirmed money, `crediting` for confirmed money before the credit step and while the rate read fails, `support` for a `HighSpeed` invoice, uncountable units, a rate below the floor and unconfirmed money once alerted, and null once all is credited (also after `support`); `find` entries with `balanceSource` for a spend order and `heldBy` for a held balance, null otherwise, an overdue unfunded holder giving null; the migration up and down on SQLite and Postgres; - **Postgres** (two connections, many rounds, in the wave's Postgres build): two `creditLateInvoice` for one invoice started together (one credit row, the balance up once); `creditLateInvoice` against a `settleOrder` raising the confirmed figure (the credited units end equal to the confirmed units, nothing counted twice). Every step must return cleanly, with no unique-key violation and no refusal: under the lock the second writer reads nothing due and inserts nothing. Each pair also runs against a variant that reads `lateCreditState` before `lockInvoice`; its totals still come out right through the unique key, so the spec asserts that the variant hits the unique key or reports a refusal at least once, which proves the lock is what keeps the real step clean. - **Depends on:** T23, T18. @@ -348,7 +353,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 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. +- **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 (its body's `heldUntil` the holder's expiry, or null for a funded holder after T24's merge), 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 @@ -410,7 +415,7 @@ Each wave starts from `sh/store-service` as merged at the end of the last one. " | 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, `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` | +| 10 | T24, T25, T26 | T24: `Names/Worker.hs`, `Orders.hs`, `Names/View.hs`, the migrations, `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.