Conversation
…on as the entitlement The account Worker sells monthly, yearly and founding through @pgstencil/stripe (named plans, no trial, Managed Payments): checkout, the return's confirm, the customer portal, the founders-row opt-in, the Van Westendorp answers, and Stripe's signed webhook. Price ids come from Worker vars; billing is off (503) until all five Stripe bindings are set, and a founding ladder whose length differs from the published one fails. The entitlement predicate is now an active subscription under status() semantics, resolved in SQL beside each bearer, plus the verified ADMIN_EMAIL as a standing comp. The relay and voice roles read only the subscription columns it needs. GET /api/hosted/cohorts answers the open cohort, its seats, and the opted-in founders (names only), cached 60 s per isolate; the account Worker also answers it, and nothing else, on dormouse.sh through a pinned zone route. An hourly cron resyncs subscriptions whose period ends within the hour, so a missed renewal webhook never lapses a member. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… guard hosted.md gains "Billing" (routes, bindings, the founding offer, the webhook, the cohort endpoint and its site route, the hourly resync) and states the entitlement as the subscription plus the ADMIN_EMAIL comp. pricing.md promotes what is built into "Checkout and entitlement" and keeps the account pages, the site's buy links, turning billing on, and founder avatars under Future. security-hosted.md gets a Billing boundary, the site-origin exception, and the Stripe package in the provenance checks; the Hosted audit prompt asks about billing. The page now drops seats unless the endpoint's cohort is the one its price was prerendered at, so a stale deploy never prints the next cohort's seats beside the old price. The README lists the Stripe setup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cting one pgstencil's checkout now takes a null email (named-plans dab7292), so a provider-only account or a GitHub login with a private address can buy: the Stripe customer is created without one and Checkout asks the buyer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… on StripeDev in the dev loop `/checkout?plan=` signs in first, shows the plan and its price now (the founding step from the server's open cohort), then hands off to Stripe. Stripe's return to `/billing?checkout=` confirms the checkout and shows the welcome page: the founders-row opt-in, unticked, and the four Van Westendorp questions, sent only when the buyer sends them. The account page gains a Plan section with Manage billing and the founders toggle. A pending checkout's plan name rides provider sign-in in session storage. `dor tool hosted` now bills against StripeDev (no card, no charge), and the dev guard admits StripeDev's one cross-site navigation back to /billing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`CHECKOUT_OPEN` in hosted-pricing.ts decides both what a buy button does (the unbuilt-checkout notice, or a link to the account origin's /checkout?plan= by checkout's plan names) and the offers' JSON-LD availability (PreOrder or InStock). It ships false. The founding card promises a name, not an avatar, in the founders row. The specs promote the account pages and the buy links; desktop sign-in and turning billing on stay under Future. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- entitlement.ts owns the access rule (`accessSql`) and the subscribed check (`subscribedSql`, one aggregate instead of two subqueries); the founders row and the resync reuse them, so the founders row now also requires exactly one current subscription, as the entitlement does. - The account summary reads status, entitlement, founder and cohort on the one Billing pool and no longer calls Stripe per view; confirm builds it inside its own Billing. - The test bundle's injected Date already scopes the system clock, so the clock parameter through accountApp is gone; `scheduled` runs in the same scope. The cohort cache keeps text, never a Response. - Plan names live once in hosted-pricing.ts (tier ids are checkout's plans); the return path once in policy-constants; StripeDev's billing setup once in billing-dev.ts; the 413 body limit once in account-gate. - The account app derives one page and writes the address from it, and the account frontend's billing calls go through `request`. - Previews rebuild for any website/src/lib/hosted-* change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…en_burrow Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd, every 10 minutes - A subscription Stripe still holds (past_due, unpaid, incomplete) keeps its plan on the account page with Manage billing and "a payment is due", rather than "No plan" and a checkout that refuses; `active` says whether it grants access. - The resync now runs every 10 minutes on subscriptions still active or trialing past their period or trial end (a resync before the renewal read the old period), picking at random so one account that always fails cannot starve the rest. - The welcome page shows only for a completed checkout; a pending plan rides a provider sign-in for 10 minutes and once. - Spec rules lead with Must/May; cleanups in billing-dev.ts and style.css. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…where it landed pricing.md takes hosted-signin's front door, sign-in surface, managed voice pointer and open question, and drops "the subscription as the entitlement" from its Future and Status. hosted.md's Future no longer lists the entitlement swap, and the hand-minted token routes are no longer "the admin test path". ADMIN_EMAIL's comment names the standing comp instead of "until billing ships". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drop this commit once pgstencil 0.3.0 (diffplug/pgstencil named-plans, packed clean from dab7292) is released to npm, then run `pnpm install` and commit the lockfile: the code commits already declare ^0.3.0. vendor/ holds the three archives, pnpm-workspace.yaml overrides the three package names to them (and lets the file: override satisfy their pgstencil peer), and the lockfile test is marked as expected to fail while they are vendored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deploying mouseterm with
|
| Latest commit: |
cc2075a
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://5007d763.mouseterm.pages.dev |
| Branch Preview URL: | https://hosted-billing.mouseterm.pages.dev |
dormouse-bot
left a comment
There was a problem hiding this comment.
openCohort can reopen a cheaper founding step, which breaks pricing.md -> "Never reopen the ladder at a lower step once a cohort has closed". The step only stays closed after someone has bought at the next one. Take a cohort that has just filled and has no purchase yet at the next step, so sold = [100, 0, …]. If one of its buyers refunds within REFUND_DAYS and cancels, sold becomes [99, 0, …], last is 0 again, and checkout offers the cohort-0 Price. A deploy after the close has already prerendered the cohort-1 price. The page then drops the seats line because the cohorts don't match, while checkout charges the old price. The test at billing.test.ts ([full - 1, 2, …]) covers the refund only after the next cohort has a sale.
One fix: decide whether a step is closed from gross completed purchases, with no refundDays exclusion, and take seats left from the net count. A refunded seat still returns to its own cohort, but that cohort can't become the open one again. hosted.md -> "Billing" describes the open-cohort rule in a way that needs the same change.
| sold.forEach((count, step) => { | ||
| if (count > 0) last = step; | ||
| }); | ||
| const cohort = (sold[last] ?? 0) >= FOUNDING_COHORT_SIZE ? last + 1 : last; |
There was a problem hiding this comment.
With sold = [FOUNDING_COHORT_SIZE - 1, 0, …] (a refund in a cohort that just closed, before any sale at the next step), last is 0 and this reopens cohort 0. The doc comment above says that can't happen.
Stacked on #998 (→ #1000 → #999 → #996). Checkout, the subscription as the entitlement, and founding cohorts, on
@pgstencil/stripe0.3.0 (diffplug/pgstencil#54).Server (account Worker)
/api/billingwithcheckout,confirm,portal,founderandsurvey, plus the Stripe webhook. The webhook takes the raw body, checks the signature, caps the body at 1 MiB, and answers 2xx only after commit.status()rules) or the standingADMIN_EMAILcomp.GET /api/hosted/cohortsreturns seats left and the founders row, with names only. It is also served ondormouse.sh/api/hosted/*through a pinned zone route.006_billing_founders.sql, andruntime-roles.sqlgrants for the Workers that read entitlement.verifyPackagesnow covers@pgstencil/stripe.Account app
/checkout?plan=: signs in first, then shows the plan and today's price. A pending plan survives a provider sign-in./billing?checkout=: confirms the checkout, then shows the founders-row opt-in (unticked, with a chosen name) and the optional Van Westendorp survey.dor tool hostedbills against StripeDev.Website
CHECKOUT_OPENinhosted-pricing.tsswitches the buy buttons between the "not wired up" notice and real checkout links, and the JSON-LD betweenPreOrderandInStock. It is committed as false. The seats line shows only while the endpoint's cohort matchesFOUNDING_COHORTS_CLOSED.Decisions to check
ADMIN_EMAILstays a standing comp.pricing.mdaccepts.security-remote.md-> "Cloud-hosted mode" requires.Test plan
pnpm test(website 347)dormouse.sh/api/hosted/*takes precedence over Pages🤖 Generated with Claude Code