fix: prevent duplicate subscriptions by validating active status in webhook and cleaning up stale records during creation
This commit is contained in:
@@ -91,12 +91,19 @@ export class StripeWebhookController {
|
|||||||
const stripeCustomerId = session.customer as string;
|
const stripeCustomerId = session.customer as string;
|
||||||
|
|
||||||
// Check if subscription already exists (idempotency)
|
// Check if subscription already exists (idempotency)
|
||||||
const existing = await this.subscriptionService.getSubscriptionByStripeId(stripeSubscriptionId);
|
const existingByStripeId = await this.subscriptionService.getSubscriptionByStripeId(stripeSubscriptionId);
|
||||||
if (existing) {
|
if (existingByStripeId) {
|
||||||
this.logger.log(`Subscription ${stripeSubscriptionId} already exists, skipping`);
|
this.logger.log(`Subscription ${stripeSubscriptionId} already exists, skipping`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if user already has an active subscription (prevent duplicates)
|
||||||
|
const existingActive = await this.subscriptionService.getActivePlan(userId);
|
||||||
|
if (existingActive) {
|
||||||
|
this.logger.log(`User ${userId} already has active subscription ${existingActive.id}, skipping duplicate`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await this.subscriptionService.createSubscription({
|
await this.subscriptionService.createSubscription({
|
||||||
userId,
|
userId,
|
||||||
planId,
|
planId,
|
||||||
|
|||||||
@@ -80,6 +80,34 @@ export class StripeService {
|
|||||||
throw new BadRequestException('You already have an active subscription');
|
throw new BadRequestException('You already have an active subscription');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up any incomplete/failed subscriptions to prevent duplicates
|
||||||
|
// when user retries payment after a previous failure
|
||||||
|
const staleSubscriptions = await this.prisma.agentSubscription.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
status: { in: ['INCOMPLETE', 'UNPAID', 'PAST_DUE'] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (staleSubscriptions.length > 0) {
|
||||||
|
// Cancel stale Stripe subscriptions before deleting DB records
|
||||||
|
for (const sub of staleSubscriptions) {
|
||||||
|
if (!sub.stripeSubscriptionId) continue;
|
||||||
|
try {
|
||||||
|
await this.getStripe().subscriptions.cancel(sub.stripeSubscriptionId);
|
||||||
|
} catch (err: any) {
|
||||||
|
this.logger.warn(`Could not cancel stale Stripe subscription ${sub.stripeSubscriptionId}: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.prisma.agentSubscription.deleteMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
status: { in: ['INCOMPLETE', 'UNPAID', 'PAST_DUE'] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.logger.log(`Cleaned up ${staleSubscriptions.length} stale subscription(s) for user ${userId}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Get or create Stripe customer
|
// Get or create Stripe customer
|
||||||
const user = await this.prisma.user.findUnique({
|
const user = await this.prisma.user.findUnique({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
|
|||||||
Reference in New Issue
Block a user