fix
This commit is contained in:
@@ -97,19 +97,38 @@ export class StripeWebhookController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user already has an active subscription (prevent duplicates)
|
// Check if user already has an active subscription with this same Stripe ID
|
||||||
|
// (prevent true duplicates only — new Stripe subscriptions for same user are allowed
|
||||||
|
// because createCheckoutSession cleanup handles stale records)
|
||||||
const existingActive = await this.subscriptionService.getActivePlan(userId);
|
const existingActive = await this.subscriptionService.getActivePlan(userId);
|
||||||
if (existingActive) {
|
if (existingActive && existingActive.stripeSubscriptionId === stripeSubscriptionId) {
|
||||||
this.logger.log(`User ${userId} already has active subscription ${existingActive.id}, skipping duplicate`);
|
this.logger.log(`User ${userId} already has this exact active subscription, skipping duplicate`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch full subscription from Stripe to get period dates immediately
|
||||||
|
let currentPeriodStart: Date | undefined;
|
||||||
|
let currentPeriodEnd: Date | undefined;
|
||||||
|
try {
|
||||||
|
const stripeSub: any = await this.stripeService.getStripe().subscriptions.retrieve(stripeSubscriptionId);
|
||||||
|
if (stripeSub.current_period_start) {
|
||||||
|
currentPeriodStart = new Date(stripeSub.current_period_start * 1000);
|
||||||
|
}
|
||||||
|
if (stripeSub.current_period_end) {
|
||||||
|
currentPeriodEnd = new Date(stripeSub.current_period_end * 1000);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
this.logger.warn(`Could not fetch Stripe subscription ${stripeSubscriptionId}: ${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
await this.subscriptionService.createSubscription({
|
await this.subscriptionService.createSubscription({
|
||||||
userId,
|
userId,
|
||||||
planId,
|
planId,
|
||||||
stripeCustomerId,
|
stripeCustomerId,
|
||||||
stripeSubscriptionId,
|
stripeSubscriptionId,
|
||||||
status: SubscriptionStatus.ACTIVE,
|
status: SubscriptionStatus.ACTIVE,
|
||||||
|
currentPeriodStart,
|
||||||
|
currentPeriodEnd,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.log(`Created subscription for user ${userId}, plan ${planId}`);
|
this.logger.log(`Created subscription for user ${userId}, plan ${planId}`);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export class StripeService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getStripe(): Stripe {
|
getStripe(): Stripe {
|
||||||
if (!this.stripe) {
|
if (!this.stripe) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
'Stripe is not configured. Please set STRIPE_SECRET_KEY in environment.',
|
'Stripe is not configured. Please set STRIPE_SECRET_KEY in environment.',
|
||||||
@@ -71,21 +71,31 @@ export class StripeService {
|
|||||||
throw new BadRequestException('Invalid or inactive plan');
|
throw new BadRequestException('Invalid or inactive plan');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user already has an active subscription
|
// Check if user has a VALID active subscription (ACTIVE with valid period end in the future)
|
||||||
const activeSubscription = await this.prisma.agentSubscription.findFirst({
|
const now = new Date();
|
||||||
where: { userId, status: 'ACTIVE' },
|
const validActive = await this.prisma.agentSubscription.findFirst({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
status: 'ACTIVE',
|
||||||
|
currentPeriodEnd: { gt: now },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (activeSubscription) {
|
if (validActive) {
|
||||||
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
|
// Clean up stale subscriptions (failed states OR ACTIVE with no period / expired period)
|
||||||
// when user retries payment after a previous failure
|
// This handles the case where checkout completed but invoice.paid never fired,
|
||||||
|
// leaving an orphaned ACTIVE record without proper period dates
|
||||||
const staleSubscriptions = await this.prisma.agentSubscription.findMany({
|
const staleSubscriptions = await this.prisma.agentSubscription.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId,
|
userId,
|
||||||
status: { in: ['INCOMPLETE', 'UNPAID', 'PAST_DUE'] },
|
OR: [
|
||||||
|
{ status: { in: ['INCOMPLETE', 'UNPAID', 'PAST_DUE', 'CANCELED'] } },
|
||||||
|
{ status: 'ACTIVE', currentPeriodEnd: null },
|
||||||
|
{ status: 'ACTIVE', currentPeriodEnd: { lte: now } },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -101,8 +111,7 @@ export class StripeService {
|
|||||||
}
|
}
|
||||||
await this.prisma.agentSubscription.deleteMany({
|
await this.prisma.agentSubscription.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
userId,
|
id: { in: staleSubscriptions.map((s) => s.id) },
|
||||||
status: { in: ['INCOMPLETE', 'UNPAID', 'PAST_DUE'] },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.logger.log(`Cleaned up ${staleSubscriptions.length} stale subscription(s) for user ${userId}`);
|
this.logger.log(`Cleaned up ${staleSubscriptions.length} stale subscription(s) for user ${userId}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user