Skip to content

Hosted: billing simplify pass from the review - #1421

Merged
nedtwigg merged 7 commits into
mainfrom
billing-review-simplify
Oct 29, 2026
Merged

nedtwigg merged 7 commits into
mainfrom
billing-review-simplify

Conversation

@nedtwigg

@nedtwigg nedtwigg commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

The /simplify pass over the whole billing range (#1405–#1414), stacked on #1420. Billing still ships dark.

  • One price catalog (priceCatalog) replaces publishedPrice, priceUsd, usableChanges, and the two recognized-Price lists; Plan/isPlan live in policy-constants, PLAN_NAMES/planInterval beside the Tiers; checkout's offers come from the Tiers. The /api/billing body is a shared no-I/O type (hosted/server/billing-summary.ts) the route satisfies.
  • Server helpers, one copy each: run, DAY_MS, idOf, smallBody, isRecentLogin; no more held! assertions or ENDED_STATUSES; test-only and file-local exports dropped; readCapped uses concatBytes.
  • Client pieces, one copy each: Facts, refundWindow, day, CHECK_CLASS, ManageBilling, ChoosePlan, forgetAccount, the CHECKOUT_CLOSED text.
  • Closed billing is a state, not an outage: GET /api/billing answers 200 { closed: true }, mutations 409 (closure's refusal too), so the shared request() loses pass503. The webhook and cohort routes still answer 503 so Stripe and the page retry.
  • A withdrawal's acknowledgement is a withdrawal notice (migration 020), recorded in the withdrawal's own statement and sent by the notices outbox; acknowledgedAt and retryWithdrawals are gone. The email is byte-identical (all 13 notice/acknowledgement outputs snapshotted before and after). FAIL IF S289 reworded to the mechanism.
  • Fewer round trips: the cohort snapshot reads in parallel and keeps loaded avatars across snapshots; the notice batch is leased as it is picked, one Stripe contact per account per pass; the withdrawal acknowledgement reuses the contact the offer just read; the account page runs one refresh per tab return.

Notes: migration 020 backfills already-sent acknowledgements with email = '' (no address was stored); a failed notice send now waits out its 5-minute lease. Left for the pgstencil 0.4.2 adoption: the three cancel-now copies, endForRefund's double reconcile, the accounts join. Skipped: the metric SAVEPOINT (keeps the never-fails-the-webhook guarantee). Edge-caching /api/hosted/* needs S81 amended.

Tests: hosted pnpm test 304, website pnpm test 461, tsc (hosted, website, lib), spec-lint, md-unwrap, lint:public-docs; new tests mutation-checked.

🤖 Generated with Claude Code

nedtwigg and others added 6 commits October 28, 2026 18:56
…d by server and page

Plan/isPlan live beside BILLING_PLANS; PLAN_NAMES and planInterval beside
the Tiers; checkout's offers spread the Tiers. The server reads every
Price's plan and dollars from one priceCatalog built per billing run, in
place of publishedPrice, priceUsd, usableChanges, and two recognized-Price
lists. GET /api/billing's body is a shared no-I/O type the route satisfies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ns, and Stripe ids

One run() for raw statements, one DAY_MS, and idOf for expanded Stripe
references; smallBody and isRecentLogin come from account-gate for the
Relay approval, closure, and billing routes. The summary holds the
subscription itself instead of a flag and non-null assertions, and
ENDED_STATUSES, the complement of CURRENT_STATUSES, is gone. Test-only
re-exports and exports only their own file used are dropped, readCapped
uses concatBytes, and CLOSE_UNAVAILABLE names SUPPORT_EMAIL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d plan actions

Facts renders the plan confirmation's and both withdrawal cards' term
lists; refundWindow states the guarantee checkout and the confirmation
both quote, from REFUND_DAYS, now beside the other shared policy values.
day, CHECK_CLASS, ManageBilling, and ChoosePlan each have one copy,
signing out and closing share forgetAccount, and the closed-checkout line
reads CHECKOUT_CLOSED. The pages render as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /api/billing answers 200 { closed: true } while billing is closed, and
every billing change but leaving the founders row answers 409
CHECKOUT_CLOSED, as a closure holding a subscription answers 409
CLOSE_UNAVAILABLE. The page's request() drops pass503 and billingRefused's
message sniffing: any 5xx is an outage. Stripe's webhook and the public
cohort read still answer 503 while closed, so events wait for billing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Migration 020 adds the withdrawal notice kind, recorded in the statement
that records the withdrawal and leased, sent, and retried by the notice
pass like the others; existing acknowledgements move over and
acknowledgedAt is dropped. The withdraw route sends the account's pending
notices, then settles; the cron settles unsettled withdrawals
(settleWithdrawals replaces retryWithdrawals). The acknowledgement email
is byte-identical, now composed by noticeMessage with a term-list block.
FAIL IF S289 names the new mechanism.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The billing page reuses the open cohort for COHORT_CACHE_MS per isolate;
  a refused changed price and any webhook event forget it.
- withBilling reuses the scheduled price changes for a minute per isolate
  (resolved rows only); the cron reads them afresh, and its stale-owner
  query runs on the same connection.
- The cohort snapshot counts purchases and reads the founders row in
  parallel, without the price changes, and keeps a picture already loaded
  from the same source across answers instead of refetching it each minute.
- sendNotices leases its batch in the statement that picks it and reads
  each account's contact from Stripe once per pass; a withdrawal's
  acknowledgement reuses the contact its offer just read.
- The account page re-reads the plan only after signing in or leaving the
  founders row, not after a change that cannot move it or while following
  an approval, and one refresh answers both of a tab return's events.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 31568bd
Status: ✅  Deploy successful!
Preview URL: https://24bd89ea.mouseterm.pages.dev
Branch Preview URL: https://billing-review-simplify.mouseterm.pages.dev

View logs

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The consolidation, the closed-billing state, and the withdrawal notice all look right. Two of the new "fewer round trips" caches add per-isolate state, mode flags, and invalidation sites, and nothing reachable calls for them. Billing still ships dark, and neither the PR nor the code points at load that needs them. I'd drop both unless there's a measured cost behind them. The inline comments give the specifics.

Comment thread hosted/server/billing.ts Outdated
Comment thread hosted/server/billing-routes.ts Outdated
Comment thread hosted/src/App.tsx Outdated
Review of #1421: withBilling reads the scheduled price changes on every
run, with no per-isolate cache or read mode; the billing page reads the
open cohort fresh, so it shows the current price; and every account
refresh reads billing again, since a superseded refresh could otherwise
swallow a plan read. One refresh still answers both of a tab return's
events.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from billing-review-fixes to main October 29, 2026 02:38
@nedtwigg
nedtwigg merged commit 328c185 into main Oct 29, 2026
21 checks passed
@nedtwigg
nedtwigg deleted the billing-review-simplify branch October 29, 2026 02:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants