From b62df3b629deee27b810399f41c788445d1fc967 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sun, 24 May 2026 20:49:05 -0700 Subject: [PATCH 1/2] chore(stripe): regenerate price IDs after subscription sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ran scripts/stripe/sync-products.ts against the live Stripe test-mode account after PR #532 landed. The script created 4 recurring prices (monthly + annual × developer_seat + team) and archived the prior one-time-payment prices. Operational follow-ups completed in parallel: - DB migration 0002 applied to the minting Neon DB (stripe_payment_id → stripe_subscription_id) - Webhook endpoint we_1TZcsHGYRsLErhxbdN2JTFTr enabled_events updated: customer.subscription.{created,updated,deleted}, invoice.paid, charge.refunded (dropped checkout.session.completed) Co-Authored-By: Claude Opus 4.7 (1M context) --- pricing/tiers.generated.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pricing/tiers.generated.ts b/pricing/tiers.generated.ts index aedd6752..8c2853cf 100644 --- a/pricing/tiers.generated.ts +++ b/pricing/tiers.generated.ts @@ -4,7 +4,7 @@ import type { TierSlug, BillingCycle } from './tiers.config'; type BuyableSlug = Exclude; -// Empty stub — run `STRIPE_SECRET_KEY=sk_... pnpm tsx scripts/stripe/sync-products.ts` -// to populate. Until that runs, the checkout API returns a 503 with a helpful -// message. -export const STRIPE_PRICE_IDS: Partial>> = {}; +export const STRIPE_PRICE_IDS: Partial>> = { + developer_seat: { monthly: "price_1TapR1GYRsLErhxb83221xMU", annual: "price_1TapR1GYRsLErhxb67dc67h1" }, + team: { monthly: "price_1TapR2GYRsLErhxbBbrJMLpk", annual: "price_1TapR2GYRsLErhxbYsWAkYuE" }, +}; From 1ad6e21660088cf709f943559979a8f4fa40d531 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sun, 24 May 2026 21:22:28 -0700 Subject: [PATCH 2/2] fix(minting): read current_period_end from subscription item, not the sub object Stripe API version 2026-04-22 (dahlia) moved `current_period_end` and `current_period_start` from the Subscription object to each SubscriptionItem. The handler was reading the legacy subscription-level field, which is now always null, causing `handleSubscriptionCreated` to throw with `subscription has no current_period_end` for every new subscription. Read the item-level field first, fall back to the legacy subscription-level field for replayed historical events or older API versions. Verified with a live test-mode Team annual subscription (sub_1Tapk3GYRsLErhxb3jnWnxPy): handler now mints + emails the license with expires_at = 2027-05-25 (matches Stripe's item.current_period_end). Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/minting-service/src/lib/handlers.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/minting-service/src/lib/handlers.ts b/apps/minting-service/src/lib/handlers.ts index 574024b6..e2545679 100644 --- a/apps/minting-service/src/lib/handlers.ts +++ b/apps/minting-service/src/lib/handlers.ts @@ -101,8 +101,17 @@ function readCustomerId(subscription: Stripe.Subscription): string { } function periodEnd(subscription: Stripe.Subscription): Date { - // current_period_end is unix seconds; convert to Date. - const epoch = (subscription as unknown as { current_period_end?: number }).current_period_end; + // current_period_end is unix seconds. As of Stripe API 2026-04-22, it moved + // off the subscription object and onto each subscription item. Read item + // first, fall back to the legacy subscription-level field for older API + // versions or replayed historical events. + const subRecord = subscription as unknown as { + current_period_end?: number; + items?: { data?: Array<{ current_period_end?: number }> }; + }; + const itemEpoch = subRecord.items?.data?.[0]?.current_period_end; + const subEpoch = subRecord.current_period_end; + const epoch = itemEpoch ?? subEpoch; if (!epoch) { throw new Error(`subscription ${subscription.id} has no current_period_end`); }