fix: Stripe IDs and admin overrides
All checks were successful
Deploy to Staging / Build Images (push) Successful in 3m38s
Deploy to Staging / Deploy to Staging (push) Successful in 53s
Deploy to Staging / Verify Staging (push) Successful in 9s
Deploy to Staging / Notify Staging Ready (push) Successful in 8s
Deploy to Staging / Notify Staging Failure (push) Has been skipped

This commit is contained in:
Eric Gullickson
2026-02-15 21:26:38 -06:00
parent c1e8807bda
commit ddae397cb3
4 changed files with 61 additions and 16 deletions

View File

@@ -134,7 +134,8 @@ export class SubscriptionsController {
userId, userId,
tier, tier,
billingCycle, billingCycle,
paymentMethodId || '' paymentMethodId || '',
email
); );
reply.status(200).send(updatedSubscription); reply.status(200).send(updatedSubscription);
@@ -207,6 +208,7 @@ export class SubscriptionsController {
): Promise<void> { ): Promise<void> {
try { try {
const userId = (request as any).user.sub; const userId = (request as any).user.sub;
const email = (request as any).user.email;
const { paymentMethodId } = request.body; const { paymentMethodId } = request.body;
// Validate input // Validate input
@@ -218,19 +220,8 @@ export class SubscriptionsController {
return; return;
} }
// Get subscription // Update payment method via service (handles admin_override_ customer IDs)
const subscription = await this.service.getSubscription(userId); await this.service.updatePaymentMethod(userId, paymentMethodId, email);
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);
reply.status(200).send({ reply.status(200).send({
message: 'Payment method updated successfully', message: 'Payment method updated successfully',

View File

@@ -146,6 +146,10 @@ export class SubscriptionsRepository {
const values = []; const values = [];
let paramCount = 1; let paramCount = 1;
if (data.stripeCustomerId !== undefined) {
fields.push(`stripe_customer_id = $${paramCount++}`);
values.push(data.stripeCustomerId);
}
if (data.stripeSubscriptionId !== undefined) { if (data.stripeSubscriptionId !== undefined) {
fields.push(`stripe_subscription_id = $${paramCount++}`); fields.push(`stripe_subscription_id = $${paramCount++}`);
values.push(data.stripeSubscriptionId); values.push(data.stripeSubscriptionId);

View File

@@ -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<string> {
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 * Upgrade from current tier to new tier
*/ */
@@ -172,7 +204,8 @@ export class SubscriptionsService {
userId: string, userId: string,
newTier: 'pro' | 'enterprise', newTier: 'pro' | 'enterprise',
billingCycle: 'monthly' | 'yearly', billingCycle: 'monthly' | 'yearly',
paymentMethodId: string paymentMethodId: string,
email: string
): Promise<Subscription> { ): Promise<Subscription> {
try { try {
logger.info('Upgrading subscription', { userId, newTier, billingCycle }); logger.info('Upgrading subscription', { userId, newTier, billingCycle });
@@ -183,12 +216,15 @@ export class SubscriptionsService {
throw new Error('No subscription found for user'); 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 // Determine price ID from environment variables
const priceId = this.getPriceId(newTier, billingCycle); const priceId = this.getPriceId(newTier, billingCycle);
// Create or update Stripe subscription // Create or update Stripe subscription
const stripeSubscription = await this.stripeClient.createSubscription( const stripeSubscription = await this.stripeClient.createSubscription(
currentSubscription.stripeCustomerId, stripeCustomerId,
priceId, priceId,
paymentMethodId 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<void> {
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 * Get invoices for a user's subscription
*/ */

View File

@@ -118,6 +118,7 @@ export interface CreateTierVehicleSelectionRequest {
// Service layer types // Service layer types
export interface UpdateSubscriptionData { export interface UpdateSubscriptionData {
stripeCustomerId?: string;
stripeSubscriptionId?: string; stripeSubscriptionId?: string;
tier?: SubscriptionTier; tier?: SubscriptionTier;
billingCycle?: BillingCycle; billingCycle?: BillingCycle;