Monthly/yearly purchases were one-time charges: our "subscription" was just
an end_date stamp, access silently died at period end and no renewal
machinery existed (no auto-charge, no reminder) — every paying customer had
to notice the lockout and re-buy by hand.
- Checkout now mode:"subscription" with inline recurring price_data; the
Stripe customer is stored on first purchase and reused (saved card +
invoice history on one record, with a stale-customer retry guard)
- invoice.paid webhook: extends end_date to the billing-line period end,
records a completed payment (deduped on stripe_invoice_id against webhook
retries), captures subscription_renewed with $revenue, mails the receipt;
late dunning recovery re-activates the row and re-grants Full-plan brands
- invoice.payment_failed webhook: dunning mail with Stripe's next retry
date; access is NOT cut — end_date governs and the nightly cron closes it
if every retry fails. Product rule: mail on success, mail on failure,
never a pre-charge reminder
- customer.subscription.deleted: stamps cancelledAt; renewals stop and
access runs out at end_date naturally
- cancel()/resume() sync cancel_at_period_end to Stripe (forwardRef pair) —
an in-app cancel that leaves the card being charged was unacceptable
- subscription_create invoices only enrich the checkout's payment row
(payment intent + invoice id for receipts/panel refunds); activation,
revenue and the receipt stay on checkout.session.completed
- migration 0020: users.stripe_customer_id,
user_subscriptions.stripe_subscription_id (+idx),
payments.stripe_invoice_id (+idx)
Legacy one-time subs (3 live payers) are untouched: they expire at their
end_date as before and board recurring on their next manual checkout.
Promote checklist: add invoice.paid / invoice.payment_failed /
customer.subscription.deleted to the prod webhook endpoint; verify Stripe
"Customer emails" upcoming-renewal reminders stay OFF.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>