Files
backend/src/stripe/stripe.service.ts
pradeepkumar 0dc13cb43a fix
2026-04-07 19:55:12 +05:30

208 lines
5.7 KiB
TypeScript

import { Injectable, Logger, BadRequestException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import Stripe from 'stripe';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class StripeService {
private stripe: Stripe | null = null;
private readonly logger = new Logger(StripeService.name);
constructor(
private configService: ConfigService,
private prisma: PrismaService,
) {
const secretKey = this.configService.get<string>('stripe.secretKey');
if (secretKey) {
this.stripe = new Stripe(secretKey, {
apiVersion: '2025-02-24.acacia' as any,
});
} else {
this.logger.warn(
'Stripe secret key not configured. Stripe features will be unavailable.',
);
}
}
getStripe(): Stripe {
if (!this.stripe) {
throw new BadRequestException(
'Stripe is not configured. Please set STRIPE_SECRET_KEY in environment.',
);
}
return this.stripe;
}
async createCustomer(
userId: string,
email: string,
name?: string,
): Promise<string> {
// Check if user already has a subscription with a Stripe customer ID
const existing = await this.prisma.agentSubscription.findFirst({
where: { userId },
select: { stripeCustomerId: true },
});
if (existing) {
return existing.stripeCustomerId;
}
const customer = await this.getStripe().customers.create({
email,
name,
metadata: { userId },
});
return customer.id;
}
async createCheckoutSession(
userId: string,
planId: string,
successUrl: string,
cancelUrl: string,
): Promise<{ url: string }> {
const plan = await this.prisma.subscriptionPlan.findUnique({
where: { id: planId },
});
if (!plan || !plan.isActive) {
throw new BadRequestException('Invalid or inactive plan');
}
// Check if user has a VALID active subscription (ACTIVE with valid period end in the future)
const now = new Date();
const validActive = await this.prisma.agentSubscription.findFirst({
where: {
userId,
status: 'ACTIVE',
currentPeriodEnd: { gt: now },
},
});
if (validActive) {
throw new BadRequestException('You already have an active subscription');
}
// Clean up stale subscriptions (failed states OR ACTIVE with no period / expired period)
// 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({
where: {
userId,
OR: [
{ status: { in: ['INCOMPLETE', 'UNPAID', 'PAST_DUE', 'CANCELED'] } },
{ status: 'ACTIVE', currentPeriodEnd: null },
{ status: 'ACTIVE', currentPeriodEnd: { lte: now } },
],
},
});
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: {
id: { in: staleSubscriptions.map((s) => s.id) },
},
});
this.logger.log(`Cleaned up ${staleSubscriptions.length} stale subscription(s) for user ${userId}`);
}
// Get or create Stripe customer
const user = await this.prisma.user.findUnique({
where: { id: userId },
include: { agentProfile: true },
});
if (!user) {
throw new BadRequestException('User not found');
}
const customerName = user.agentProfile
? `${user.agentProfile.firstName || ''} ${user.agentProfile.lastName || ''}`.trim()
: undefined;
const customerId = await this.createCustomer(
userId,
user.email,
customerName,
);
const session = await this.getStripe().checkout.sessions.create({
customer: customerId,
mode: 'subscription',
payment_method_types: ['card'],
line_items: [
{
price: plan.stripePriceId,
quantity: 1,
},
],
success_url: `${successUrl}?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: cancelUrl,
metadata: {
userId,
planId,
},
subscription_data: {
metadata: {
userId,
planId,
},
},
});
return { url: session.url! };
}
async createBillingPortalSession(
customerId: string,
returnUrl: string,
): Promise<{ url: string }> {
const session = await this.getStripe().billingPortal.sessions.create({
customer: customerId,
return_url: returnUrl,
});
return { url: session.url };
}
async cancelSubscription(
stripeSubscriptionId: string,
atPeriodEnd = true,
): Promise<Stripe.Subscription> {
if (atPeriodEnd) {
return this.getStripe().subscriptions.update(stripeSubscriptionId, {
cancel_at_period_end: true,
});
}
return this.getStripe().subscriptions.cancel(stripeSubscriptionId);
}
async getSubscription(
stripeSubscriptionId: string,
): Promise<Stripe.Subscription> {
return this.getStripe().subscriptions.retrieve(stripeSubscriptionId);
}
constructWebhookEvent(payload: Buffer, signature: string): Stripe.Event {
const webhookSecret =
this.configService.get<string>('stripe.webhookSecret') || '';
return this.getStripe().webhooks.constructEvent(
payload,
signature,
webhookSecret,
);
}
}