|
|
|
|
@@ -166,35 +166,42 @@ 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.
|
|
|
|
|
* Create or return existing Stripe customer for a subscription.
|
|
|
|
|
* Admin-set subscriptions have NULL stripeCustomerId. On first Stripe payment,
|
|
|
|
|
* the customer is created in-place. Includes cleanup of orphaned Stripe customer
|
|
|
|
|
* if the DB update fails after customer creation.
|
|
|
|
|
*/
|
|
|
|
|
private async resolveStripeCustomerId(
|
|
|
|
|
private async ensureStripeCustomer(
|
|
|
|
|
subscription: Subscription,
|
|
|
|
|
email: string
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
if (!subscription.stripeCustomerId.startsWith('admin_override_')) {
|
|
|
|
|
if (subscription.stripeCustomerId) {
|
|
|
|
|
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;
|
|
|
|
|
try {
|
|
|
|
|
await this.repository.update(subscription.id, { stripeCustomerId: stripeCustomer.id });
|
|
|
|
|
logger.info('Created Stripe customer for subscription', {
|
|
|
|
|
subscriptionId: subscription.id,
|
|
|
|
|
stripeCustomerId: stripeCustomer.id,
|
|
|
|
|
});
|
|
|
|
|
return stripeCustomer.id;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// Attempt cleanup of orphaned Stripe customer
|
|
|
|
|
try {
|
|
|
|
|
await this.stripeClient.deleteCustomer(stripeCustomer.id);
|
|
|
|
|
logger.warn('Rolled back orphaned Stripe customer after DB update failure', {
|
|
|
|
|
stripeCustomerId: stripeCustomer.id,
|
|
|
|
|
});
|
|
|
|
|
} catch (cleanupError: any) {
|
|
|
|
|
logger.error('Failed to cleanup orphaned Stripe customer', {
|
|
|
|
|
stripeCustomerId: stripeCustomer.id,
|
|
|
|
|
cleanupError: cleanupError.message,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@@ -216,8 +223,8 @@ 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);
|
|
|
|
|
// Ensure Stripe customer exists (creates one for admin-set subscriptions)
|
|
|
|
|
const stripeCustomerId = await this.ensureStripeCustomer(currentSubscription, email);
|
|
|
|
|
|
|
|
|
|
// Determine price ID from environment variables
|
|
|
|
|
const priceId = this.getPriceId(newTier, billingCycle);
|
|
|
|
|
@@ -292,6 +299,10 @@ export class SubscriptionsService {
|
|
|
|
|
throw new Error('No subscription found for user');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!currentSubscription.stripeCustomerId) {
|
|
|
|
|
throw new Error('Cannot cancel subscription without active Stripe billing');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!currentSubscription.stripeSubscriptionId) {
|
|
|
|
|
throw new Error('No active Stripe subscription to cancel');
|
|
|
|
|
}
|
|
|
|
|
@@ -339,6 +350,10 @@ export class SubscriptionsService {
|
|
|
|
|
throw new Error('No subscription found for user');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!currentSubscription.stripeCustomerId) {
|
|
|
|
|
throw new Error('Cannot reactivate subscription without active Stripe billing');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!currentSubscription.stripeSubscriptionId) {
|
|
|
|
|
throw new Error('No active Stripe subscription to reactivate');
|
|
|
|
|
}
|
|
|
|
|
@@ -802,17 +817,8 @@ export class SubscriptionsService {
|
|
|
|
|
* Sync subscription tier to user_profiles table
|
|
|
|
|
*/
|
|
|
|
|
private async syncTierToUserProfile(userId: string, tier: SubscriptionTier): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
await this.userProfileRepository.updateSubscriptionTier(userId, tier);
|
|
|
|
|
logger.info('Subscription tier synced to user profile', { userId, tier });
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error('Failed to sync tier to user profile', {
|
|
|
|
|
userId,
|
|
|
|
|
tier,
|
|
|
|
|
error: error.message,
|
|
|
|
|
});
|
|
|
|
|
// Don't throw - we don't want to fail the subscription operation if sync fails
|
|
|
|
|
}
|
|
|
|
|
await this.userProfileRepository.updateSubscriptionTier(userId, tier);
|
|
|
|
|
logger.info('Subscription tier synced to user profile', { userId, tier });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@@ -968,7 +974,7 @@ export class SubscriptionsService {
|
|
|
|
|
throw new Error('No subscription found for user');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const stripeCustomerId = await this.resolveStripeCustomerId(subscription, email);
|
|
|
|
|
const stripeCustomerId = await this.ensureStripeCustomer(subscription, email);
|
|
|
|
|
await this.stripeClient.updatePaymentMethod(stripeCustomerId, paymentMethodId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -978,7 +984,7 @@ export class SubscriptionsService {
|
|
|
|
|
async getInvoices(userId: string): Promise<any[]> {
|
|
|
|
|
try {
|
|
|
|
|
const subscription = await this.repository.findByUserId(userId);
|
|
|
|
|
if (!subscription?.stripeCustomerId || subscription.stripeCustomerId.startsWith('admin_override_')) {
|
|
|
|
|
if (!subscription?.stripeCustomerId) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
return this.stripeClient.listInvoices(subscription.stripeCustomerId);
|
|
|
|
|
|