Repository navigation
Hosted: adopt pgstencil 0.4.2's billing helpers - #1426
Conversation
Bumps pgstencil, @pgstencil/auth, and @pgstencil/stripe together, as production's provenance check reads all three. Its migrations 007 (the owner_access view) and 008 (subscriptions.end_reason) join through the billing migrations directory Hosted already composes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The entitlement, the founders and cohort rows read pgstencil's owner_access view instead of restating its access rule; the relay and voice roles get SELECT (owner_id, access_until) on the view and nothing on subscriptions. The resync is reconcileStale, the held-subscription checks use the exported SUBSCRIBED. - Checkout passes replace and expectPrice and records the priceId it returns, so a Price the page did not show is refused before Stripe opens a session; unexpected-price answers the existing PRICE_CHANGED 409. - A refund, a withdrawal, and a closure end through billing.end with a recorded reason; the webhook classifies subscription.refunded by that reason instead of Stripe's cancellation comment, which a customer can write. endForRefund drops its reconciles. - customerOf and ownerOfCustomer replace the reads of pgstencil's accounts table; the webhook is verified once, and a dispute's owner is pgstencil's, resolved through its charge. - foundingPurchases counts both windows in one purchaseCounts query. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deploying mouseterm with
|
| Latest commit: |
2a133e4
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://2b9b1a5c.mouseterm.pages.dev |
| Branch Preview URL: | https://hosted-pgstencil-042.mouseterm.pages.dev |
dormouse-bot
left a comment
There was a problem hiding this comment.
A redelivered refund or lost-dispute event no longer ends the subscription, so S285 and "Billing"'s "on every delivery until ended" no longer hold. owner is set only inside apply. pgstencil 0.4.2's webhook() returns before calling apply when the event row already has processed_at (if (saved?.processed_at) return; in @pgstencil/stripe's dist/index.js). Here is the path:
- A full
charge.refundedcommits. billing.endfails on a Stripe error and the route answers 503.- Stripe redelivers.
ownerstaysnull,endForRefundis skipped, and the route answers 200. The subscription stays current and keeps its entitlement.
Nothing else picks it up: a refund has no cron path, and a lost dispute's dormouse_billing_disputes row was already deleted in the committed transaction, so resyncDisputes never sees it. Before this PR, ownerOf ran outside the transaction, so it covered this case. No test covers a failed end followed by a redelivery. f.failing.cancels is only exercised for a withdrawal.
This PR overlaps with #1425 (the Renovate bump of @pgstencil/stripe to 0.4.2), which this one supersedes.
pgstencil's webhook() skips apply for an event already processed, so a full refund or a lost dispute whose billing.end failed after the commit answered 200 on Stripe's redelivery without ending the subscription. The owner now falls back to the reversed charge's customer, as before 0.4.2, so every delivery ends it until it has, and a dispute's notices still go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moves Hosted billing onto pgstencil 0.4.2 (diffplug/pgstencil#81), so Hosted no longer copies pgstencil's rules or reads its private tables. The last of the billing-review fixups. Billing still ships dark.
pgstencil_billing.owner_access(migration 007), the one definitionstatus()also reads. Relay and voice get SELECT on the view'sowner_idandaccess_untilonly, and nothing onsubscriptions. The resync isreconcileStale; held checks use pgstencil'sSUBSCRIBED.replaceandexpectPriceand keeps the returnedpriceId;unexpected-priceanswers the existingPRICE_CHANGED409. A price the page didn't show is now refused before Stripe opens a session, instead of opening one and expiring it.billing.endwith a recorded reason (migration 008); the webhook counts an end as refunded inside the refund window or by reasonrefund, the same results as before.endForRefundno longer reconciles.customerOf/ownerOfCustomerreplace the accounts reads; the event is verified once; a dispute's owner comes from pgstencil's webhook context; onepurchaseCountsquery for gross and net.hosted.md,security-hosted.mdS282 and S110.Direct
pgstencil_billing.subscriptionsreads that remain, each with no pgstencil helper: closure listing every current subscription to end it (the view omits an owner with two) and its held check while billing is closed, and the notices' renewal and price-change queries.Deploy:
db:migrateapplies migrations 007 and 008 and reapplies the role grants before the new code runs.Tests: hosted test:deploy 46/46, vitest 302; hosted and lib tsc, spec-lint, md-unwrap, lint:public-docs; new tests mutation-checked.
🤖 Generated with Claude Code