Skip to content

Hosted: EEA/UK withdrawal, account closure, and support paths - #1413

Merged
nedtwigg merged 6 commits into
mainfrom
hosted-billing-rights
Oct 29, 2026
Merged

nedtwigg merged 6 commits into
mainfrom
hosted-billing-rights

Conversation

@nedtwigg

@nedtwigg nedtwigg commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Billing PR 4b, on main after #1407 and #1408. Billing stays closed.

  • EEA/UK statutory withdrawal (FAIL IF S289).
    • Disclosed on /checkout before purchase and in the /billing confirmation, with the model form and where the withdrawal function is.
    • POST /api/billing/withdraw records the withdrawal in a new table (migration 017) before it answers. It then emails the acknowledgement, with the statement and its date and time, to the Stripe billing email, ends the subscription at once, and refunds every paid invoice payment in full, tax included and with no use deduction. If a step fails, the withdrawal still stands and the 10-minute cron retries it.
    • Prominent on the billing page, separate from cancelling renewal. It is offered for 21 days from the start of each subscription, to an EEA/UK billing country or an unknown one.
  • Account closure cancels renewal (FAIL IF S290). POST /api/account/close requires a login from the last 10 minutes. It expires any open checkout and cancels every current subscription in Stripe, then records the closure and deletes the account in one statement. While billing is closed, an account that still holds a subscription gets a 503 and is kept. The dialog explains remaining access and refund eligibility before closing.
  • Support paths for anyone who has lost account access (support@dormouse.sh) are on the pages and in the Terms.
  • Terms and Privacy match what is built, including Postmark sending the acknowledgement. pricing.md's withdrawal and closure gates move above the fold.

Decisions for review:

  • The withdrawal window is 21 days, not 14, because weekend and holiday extensions can push the statutory end later.
  • Gibraltar and the Crown Dependencies are treated as eligible.
  • Closure ends access at once, with no refund of the remaining period. Should closing within the 30-day guarantee refund instead?
  • Support-path withdrawals are refunded by hand in Stripe and not recorded in the new table.
  • Terms text changed on the same date as the current TERMS_VERSION (2026-10-28), so the version string doesn't distinguish this revision. Before launch only the admin account has accepted any terms.

Legal wording for review:

  • website/src/lib/hosted-withdrawal.ts: the statement and the model form.
  • website/src/pages/Terms.tsx: withdrawal now covers "a Hosted subscription contract" rather than the initial one, and adds "An automatic renewal does not start a new withdrawal period". The online function is described as built, and the closure paragraph is rewritten.
  • website/src/pages/Privacy.tsx: the withdrawal and closure records, Postmark, retention, and self-service closure.
  • hosted/src/Withdrawal.tsx, hosted/src/Plan.tsx (the Withdrawal row, the support-cancel sentence, and the founders label, which now names the profile picture), hosted/src/Closure.tsx, and the acknowledgement email in hosted/server/withdrawal.ts.

Not verified yet: StripeDev models neither refunds nor immediate cancel, so withdrawal and closure run end to end only against the test fake. Nobody has walked them in the running app.

Left for a later PR: renewal, annual, price-change and California notices; the dispute-suspension notice; billing-email routing for notices; and notice-delivery records.

Rule IDs: S289 and S290 (renumbered after #1408 took S288).

🤖 Generated with Claude Code

nedtwigg and others added 3 commits October 28, 2026 05:54
POST /api/billing/withdraw records a withdrawal from the held subscription
(dormouse_withdrawals, migration 017) before answering, emails the durable
acknowledgement to the Stripe billing email, ends the subscription at once,
and refunds every paid invoice payment in full; the cron retries either step.
GET /api/billing offers it within three weeks of the start to an EEA, UK, or
unknown billing country, and shows the latest withdrawal.

POST /api/account/close needs a recent login, cancels every current
subscription in Stripe, then records the closure and deletes the account in
one statement; billing closed, it refuses an account still holding one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ccount app

Checkout discloses the EEA/UK withdrawal right, where the online withdrawal
is, the support path, and the model form. /billing adds the Withdrawal row
to the confirmation and the Withdraw from contract function, which shows the
contract, the statement, and where the acknowledgement goes before Confirm
withdrawal, then keeps the acknowledgement. The account page links to it and
gains Close account, whose dialog says what closing does to the plan first.
The founders opt-in names the profile picture it shows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rt paths

The Terms' withdrawal section covers every subscription contract, describes
the online function as built, and takes the model form from the shared
constant; account closure is self-service and cancels the subscription at
once. Privacy records withdrawals and closures, Postmark's acknowledgements,
and self-service deletion. pricing.md promotes the withdrawal and closure
gates above the fold; hosted.md documents the withdraw route and Account
closure; security-hosted gains FAIL IF S288 (withdrawal) and S289 (closure).

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

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

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 68a205a
Status: ✅  Deploy successful!
Preview URL: https://2c0ba29e.mouseterm.pages.dev
Branch Preview URL: https://hosted-billing-rights.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.

After a member withdraws and later buys again, /billing still leads with the old "Withdrawal received" card, which says "Your plan has ended", above the confirmation of the new plan. The summary returns the account's latest withdrawal whatever subscription is now held, and HostedPage renders it unconditionally. The suggestion keeps the card while it describes the held subscription (its settlement still pending) or while nothing is held.

Comment thread hosted/server/billing-routes.ts Outdated
GET /api/billing returned the account's latest withdrawal whatever it now
held, so after withdrawing and buying again /billing still led with
"Withdrawal received" above the new plan. It now returns it only while
nothing is held or it is the held subscription's, its settlement pending.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nedtwigg
nedtwigg added this pull request to stack #1417 October 28, 2026 20:06
Base automatically changed from hosted-billing-app to main October 29, 2026 00:06
nedtwigg and others added 2 commits October 28, 2026 17:09
# Conflicts:
#	.github/audit/hosted.md
#	docs/specs/hosted.md
#	docs/specs/security-hosted.md
#	hosted/server/account-app.ts
#	hosted/server/tests/billing-routes.test.ts
#	scripts/security-rule-ids.json
#	scripts/spec-word-budgets.json
#	website/src/pages/Privacy.tsx
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nedtwigg
nedtwigg merged commit cf5de99 into main Oct 29, 2026
18 checks passed
@nedtwigg
nedtwigg deleted the hosted-billing-rights branch October 29, 2026 00:24
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