diff --git a/backend/src/features/subscriptions/api/subscriptions.controller.ts b/backend/src/features/subscriptions/api/subscriptions.controller.ts index 36616c1..e4c6f70 100644 --- a/backend/src/features/subscriptions/api/subscriptions.controller.ts +++ b/backend/src/features/subscriptions/api/subscriptions.controller.ts @@ -134,7 +134,8 @@ export class SubscriptionsController { userId, tier, billingCycle, - paymentMethodId || '' + paymentMethodId || '', + email ); reply.status(200).send(updatedSubscription); @@ -207,6 +208,7 @@ export class SubscriptionsController { ): Promise { try { const userId = (request as any).user.sub; + const email = (request as any).user.email; const { paymentMethodId } = request.body; // Validate input @@ -218,19 +220,8 @@ export class SubscriptionsController { return; } - // Get subscription - const subscription = await this.service.getSubscription(userId); - if (!subscription) { - reply.status(404).send({ - error: 'Subscription not found', - message: 'No subscription exists for this user', - }); - return; - } - - // Update payment method via Stripe - const stripeClient = new StripeClient(); - await stripeClient.updatePaymentMethod(subscription.stripeCustomerId, paymentMethodId); + // Update payment method via service (handles admin_override_ customer IDs) + await this.service.updatePaymentMethod(userId, paymentMethodId, email); reply.status(200).send({ message: 'Payment method updated successfully', diff --git a/backend/src/features/subscriptions/data/subscriptions.repository.ts b/backend/src/features/subscriptions/data/subscriptions.repository.ts index 747820e..23e83dd 100644 --- a/backend/src/features/subscriptions/data/subscriptions.repository.ts +++ b/backend/src/features/subscriptions/data/subscriptions.repository.ts @@ -146,6 +146,10 @@ export class SubscriptionsRepository { const values = []; let paramCount = 1; + if (data.stripeCustomerId !== undefined) { + fields.push(`stripe_customer_id = $${paramCount++}`); + values.push(data.stripeCustomerId); + } if (data.stripeSubscriptionId !== undefined) { fields.push(`stripe_subscription_id = $${paramCount++}`); values.push(data.stripeSubscriptionId); diff --git a/backend/src/features/subscriptions/domain/subscriptions.service.ts b/backend/src/features/subscriptions/domain/subscriptions.service.ts index fc13873..9e5fffe 100644 --- a/backend/src/features/subscriptions/domain/subscriptions.service.ts +++ b/backend/src/features/subscriptions/domain/subscriptions.service.ts @@ -165,6 +165,38 @@ export class SubscriptionsService { } } + /** + * Resolve admin_override_ placeholder customer IDs to real Stripe customers. + * When an admin overrides a user's tier without Stripe, a placeholder ID is stored. + * This method creates a real Stripe customer and updates the subscription record. + */ + private async resolveStripeCustomerId( + subscription: Subscription, + email: string + ): Promise { + if (!subscription.stripeCustomerId.startsWith('admin_override_')) { + return subscription.stripeCustomerId; + } + + logger.info('Replacing admin_override_ placeholder with real Stripe customer', { + subscriptionId: subscription.id, + userId: subscription.userId, + }); + + const stripeCustomer = await this.stripeClient.createCustomer(email); + + await this.repository.update(subscription.id, { + stripeCustomerId: stripeCustomer.id, + }); + + logger.info('Stripe customer created for admin-overridden subscription', { + subscriptionId: subscription.id, + stripeCustomerId: stripeCustomer.id, + }); + + return stripeCustomer.id; + } + /** * Upgrade from current tier to new tier */ @@ -172,7 +204,8 @@ export class SubscriptionsService { userId: string, newTier: 'pro' | 'enterprise', billingCycle: 'monthly' | 'yearly', - paymentMethodId: string + paymentMethodId: string, + email: string ): Promise { try { logger.info('Upgrading subscription', { userId, newTier, billingCycle }); @@ -183,12 +216,15 @@ export class SubscriptionsService { throw new Error('No subscription found for user'); } + // Resolve admin_override_ placeholder to real Stripe customer if needed + const stripeCustomerId = await this.resolveStripeCustomerId(currentSubscription, email); + // Determine price ID from environment variables const priceId = this.getPriceId(newTier, billingCycle); // Create or update Stripe subscription const stripeSubscription = await this.stripeClient.createSubscription( - currentSubscription.stripeCustomerId, + stripeCustomerId, priceId, paymentMethodId ); @@ -923,6 +959,19 @@ export class SubscriptionsService { } } + /** + * Update payment method for a user's subscription + */ + async updatePaymentMethod(userId: string, paymentMethodId: string, email: string): Promise { + const subscription = await this.repository.findByUserId(userId); + if (!subscription) { + throw new Error('No subscription found for user'); + } + + const stripeCustomerId = await this.resolveStripeCustomerId(subscription, email); + await this.stripeClient.updatePaymentMethod(stripeCustomerId, paymentMethodId); + } + /** * Get invoices for a user's subscription */ diff --git a/backend/src/features/subscriptions/domain/subscriptions.types.ts b/backend/src/features/subscriptions/domain/subscriptions.types.ts index 1d6ca83..7c34020 100644 --- a/backend/src/features/subscriptions/domain/subscriptions.types.ts +++ b/backend/src/features/subscriptions/domain/subscriptions.types.ts @@ -118,6 +118,7 @@ export interface CreateTierVehicleSelectionRequest { // Service layer types export interface UpdateSubscriptionData { + stripeCustomerId?: string; stripeSubscriptionId?: string; tier?: SubscriptionTier; billingCycle?: BillingCycle;