feat: Implement Stripe integration for subscription management, including new services, controllers, Prisma models, and webhook handling.
This commit is contained in:
154
src/stripe/stripe.service.ts
Normal file
154
src/stripe/stripe.service.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
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.');
|
||||
}
|
||||
}
|
||||
|
||||
private 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 already has an active subscription
|
||||
const activeSubscription = await this.prisma.agentSubscription.findFirst({
|
||||
where: { userId, status: 'ACTIVE' },
|
||||
});
|
||||
|
||||
if (activeSubscription) {
|
||||
throw new BadRequestException('You already have an active subscription');
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
constructWebhookEvent(
|
||||
payload: Buffer,
|
||||
signature: string,
|
||||
): Stripe.Event {
|
||||
const webhookSecret = this.configService.get<string>('stripe.webhookSecret') || '';
|
||||
return this.getStripe().webhooks.constructEvent(payload, signature, webhookSecret);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user