From 8a73352ddcf2e1427d23af0b71ad5ea07b34ce02 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:40:58 -0600 Subject: [PATCH] fix: charge immediately on subscription and read item-level period dates Three fixes to the Stripe subscription flow: 1. Change payment_behavior from 'default_incomplete' to 'error_if_incomplete' so Stripe charges the card immediately instead of leaving the subscription in incomplete status waiting for frontend payment confirmation that never happens. 2. Read currentPeriodStart/End from subscription items instead of the top-level subscription object. Stripe moved these fields to items.data[0] in API version 2025-03-31.basil, causing epoch-zero dates (Dec 31, 1969). 3. Map Stripe 'incomplete' status to 'active' in mapStripeStatus() so it doesn't fall through to the default 'canceled' mapping. Co-Authored-By: Claude Opus 4.6 --- .../domain/subscriptions.service.ts | 13 ++++++++---- .../external/stripe/stripe.client.ts | 20 ++++++++++++------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/backend/src/features/subscriptions/domain/subscriptions.service.ts b/backend/src/features/subscriptions/domain/subscriptions.service.ts index f36b562..2daf97b 100644 --- a/backend/src/features/subscriptions/domain/subscriptions.service.ts +++ b/backend/src/features/subscriptions/domain/subscriptions.service.ts @@ -570,11 +570,13 @@ export class SubscriptionsService { } // Update subscription with Stripe subscription ID + // Period dates moved from subscription to items in API 2025-03-31.basil + const item = stripeSubscription.items?.data?.[0]; await this.repository.update(subscription.id, { stripeSubscriptionId: stripeSubscription.id, status: this.mapStripeStatus(stripeSubscription.status), - currentPeriodStart: new Date(stripeSubscription.current_period_start * 1000), - currentPeriodEnd: new Date(stripeSubscription.current_period_end * 1000), + currentPeriodStart: new Date((item?.current_period_start ?? 0) * 1000), + currentPeriodEnd: new Date((item?.current_period_end ?? 0) * 1000), }); // Log event @@ -608,11 +610,13 @@ export class SubscriptionsService { const tier = this.determineTierFromStripeSubscription(stripeSubscription); // Update subscription + // Period dates moved from subscription to items in API 2025-03-31.basil + const item = stripeSubscription.items?.data?.[0]; const updateData: UpdateSubscriptionData = { status: this.mapStripeStatus(stripeSubscription.status), tier, - currentPeriodStart: new Date(stripeSubscription.current_period_start * 1000), - currentPeriodEnd: new Date(stripeSubscription.current_period_end * 1000), + currentPeriodStart: new Date((item?.current_period_start ?? 0) * 1000), + currentPeriodEnd: new Date((item?.current_period_end ?? 0) * 1000), cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end || false, }; @@ -849,6 +853,7 @@ export class SubscriptionsService { switch (stripeStatus) { case 'active': case 'trialing': + case 'incomplete': return 'active'; case 'past_due': return 'past_due'; diff --git a/backend/src/features/subscriptions/external/stripe/stripe.client.ts b/backend/src/features/subscriptions/external/stripe/stripe.client.ts index 1105f1b..26fdb1f 100644 --- a/backend/src/features/subscriptions/external/stripe/stripe.client.ts +++ b/backend/src/features/subscriptions/external/stripe/stripe.client.ts @@ -86,7 +86,7 @@ export class StripeClient { const subscriptionParams: Stripe.SubscriptionCreateParams = { customer: customerId, items: [{ price: priceId }], - payment_behavior: 'default_incomplete', + payment_behavior: 'error_if_incomplete', payment_settings: { save_default_payment_method: 'on_subscription', }, @@ -101,13 +101,16 @@ export class StripeClient { logger.info('Stripe subscription created', { subscriptionId: subscription.id }); + // Period dates moved from subscription to items in API 2025-03-31.basil + const item = subscription.items?.data?.[0]; + return { id: subscription.id, customer: subscription.customer as string, status: subscription.status as StripeSubscription['status'], items: subscription.items, - currentPeriodStart: (subscription as any).current_period_start || 0, - currentPeriodEnd: (subscription as any).current_period_end || 0, + currentPeriodStart: item?.current_period_start ?? 0, + currentPeriodEnd: item?.current_period_end ?? 0, cancelAtPeriodEnd: subscription.cancel_at_period_end, canceledAt: subscription.canceled_at || undefined, created: subscription.created, @@ -148,13 +151,15 @@ export class StripeClient { logger.info('Stripe subscription canceled immediately', { subscriptionId }); } + const item = subscription.items?.data?.[0]; + return { id: subscription.id, customer: subscription.customer as string, status: subscription.status as StripeSubscription['status'], items: subscription.items, - currentPeriodStart: (subscription as any).current_period_start || 0, - currentPeriodEnd: (subscription as any).current_period_end || 0, + currentPeriodStart: item?.current_period_start ?? 0, + currentPeriodEnd: item?.current_period_end ?? 0, cancelAtPeriodEnd: subscription.cancel_at_period_end, canceledAt: subscription.canceled_at || undefined, created: subscription.created, @@ -294,14 +299,15 @@ export class StripeClient { logger.info('Retrieving Stripe subscription', { subscriptionId }); const subscription = await this.stripe.subscriptions.retrieve(subscriptionId); + const item = subscription.items?.data?.[0]; return { id: subscription.id, customer: subscription.customer as string, status: subscription.status as StripeSubscription['status'], items: subscription.items, - currentPeriodStart: (subscription as any).current_period_start || 0, - currentPeriodEnd: (subscription as any).current_period_end || 0, + currentPeriodStart: item?.current_period_start ?? 0, + currentPeriodEnd: item?.current_period_end ?? 0, cancelAtPeriodEnd: subscription.cancel_at_period_end, canceledAt: subscription.canceled_at || undefined, created: subscription.created,