Skip to content

Commit 2acade0

Browse files
fix(payments): read the clover field paths in the platform Stripe webhook (#544) (#551)
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 Claude-Session: https://claude.ai/code/session_01RnvphWuXtdxC2fkRCcw3t9 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 50537e6 commit 2acade0

2 files changed

Lines changed: 886 additions & 96 deletions

File tree

0 commit comments

Comments
 (0)