Commit fa7097b
fix(payments): read the clover field paths in the platform Stripe webhook (#544)
lib/stripe.ts pins apiVersion 2026-02-25.clover, where
Subscription.current_period_start/end moved onto SubscriptionItem and
Invoice.subscription moved to parent.subscription_details.subscription.
The student webhook was migrated for this; the platform webhook was not,
and it reads through an `any` cast so TypeScript never flagged it.
Every handler was therefore dead in production:
- checkout.session.completed and customer.subscription.updated did
`new Date(undefined * 1000).toISOString()` → RangeError, thrown while
building the upsert object, before any write. A school paid and got no
platform_subscriptions row, no tenants.plan, no revenue split; the
route 500'd and Stripe retried forever.
- invoice.payment_failed / invoice.paid resolved an undefined
subscription id and silently broke out, so past_due was never set and
the dunning email never sent.
Mirrors the accessors in lib/payments/stripe-provider.ts:454-460, with
the pre-clover paths kept as `??` fallbacks. toIso() guards the numeric
check so a future shape change degrades (period column omitted, error
logged) instead of 500-ing.
Also in the same route:
- Idempotency: events are recorded in webhook_events under provider
'stripe_platform' before handling and replayed ids are a no-op, same
as app/api/payments/webhook/[provider].
- Ordering: customer.subscription.updated compares the event's period
end against the stored one and drops strictly-older events whole,
so an out-of-order delivery cannot rewind current_period_end /
tenants.billing_period_end (which feed the expiry cron and the "next
payment" date) or the plan via applyPortalPlanChange.
- Errors: every read/write goes through unwrap() and a failure now 500s
so Stripe retries, instead of being discarded behind a 200.
- Stripe statuses outside the platform_subscriptions CHECK set (today
'paused') map to past_due instead of failing the constraint; the
checkout metadata interval is pinned to monthly|yearly the same way.
tests/unit/platform-webhook.test.ts drives POST with clover-shaped
fixtures for all five handled event types. 18 of its 28 cases fail
against the pre-fix route.
Closes #544
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnvphWuXtdxC2fkRCcw3t91 parent 146a8cf commit fa7097b
2 files changed
Lines changed: 886 additions & 96 deletions
0 commit comments