your message
This commit is contained in:
@ -28,34 +28,7 @@ export class PaymentsService {
|
||||
}
|
||||
|
||||
async createCheckoutSession(userId: string, tier: 'PREMIUM' | 'PRO') {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: { subscription: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
|
||||
// Get or create Stripe customer
|
||||
let stripeCustomerId = user.subscription?.stripeCustomerId;
|
||||
|
||||
if (!stripeCustomerId) {
|
||||
const customer = await this.stripeService.createCustomer(user.email, user.name || undefined);
|
||||
stripeCustomerId = customer.id;
|
||||
|
||||
await this.prisma.subscription.upsert({
|
||||
where: { userId },
|
||||
create: {
|
||||
userId,
|
||||
tier: SubscriptionTier.FREE,
|
||||
stripeCustomerId: customer.id,
|
||||
},
|
||||
update: {
|
||||
stripeCustomerId: customer.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
const { stripeCustomerId } = await this.getOrCreateStripeCustomer(userId);
|
||||
|
||||
// Get price ID for tier
|
||||
const priceId =
|
||||
@ -83,6 +56,57 @@ export class PaymentsService {
|
||||
return { url: session.url };
|
||||
}
|
||||
|
||||
async createCourseCheckoutSession(userId: string, courseId: string) {
|
||||
const [course, existingPurchase] = await Promise.all([
|
||||
this.prisma.course.findUnique({
|
||||
where: { id: courseId },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
description: true,
|
||||
price: true,
|
||||
currency: true,
|
||||
isPublished: true,
|
||||
status: true,
|
||||
},
|
||||
}),
|
||||
this.prisma.purchase.findUnique({
|
||||
where: { userId_courseId: { userId, courseId } },
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!course) {
|
||||
throw new NotFoundException('Course not found');
|
||||
}
|
||||
if (!course.price) {
|
||||
throw new Error('Course is free, checkout is not required');
|
||||
}
|
||||
if (existingPurchase?.status === 'completed') {
|
||||
throw new Error('Course is already purchased');
|
||||
}
|
||||
|
||||
const { stripeCustomerId } = await this.getOrCreateStripeCustomer(userId);
|
||||
const appUrl = this.configService.get<string>('NEXT_PUBLIC_APP_URL') || 'http://localhost:3000';
|
||||
const unitAmount = Math.round(Number(course.price) * 100);
|
||||
|
||||
const session = await this.stripeService.createOneTimeCheckoutSession({
|
||||
customerId: stripeCustomerId,
|
||||
currency: course.currency || 'USD',
|
||||
unitAmount,
|
||||
productName: course.title,
|
||||
productDescription: course.description || undefined,
|
||||
successUrl: `${appUrl}/dashboard/catalog/${courseId}?purchase=success`,
|
||||
cancelUrl: `${appUrl}/dashboard/catalog/${courseId}?purchase=canceled`,
|
||||
metadata: {
|
||||
type: 'course_purchase',
|
||||
userId,
|
||||
courseId,
|
||||
},
|
||||
});
|
||||
|
||||
return { url: session.url };
|
||||
}
|
||||
|
||||
async createPortalSession(userId: string) {
|
||||
const subscription = await this.prisma.subscription.findUnique({
|
||||
where: { userId },
|
||||
@ -107,8 +131,8 @@ export class PaymentsService {
|
||||
case 'checkout.session.completed':
|
||||
await this.handleCheckoutCompleted(event.data.object as {
|
||||
customer: string;
|
||||
subscription: string;
|
||||
metadata: { userId: string; tier: string };
|
||||
subscription?: string;
|
||||
metadata: { userId?: string; tier?: string; type?: string; courseId?: string };
|
||||
});
|
||||
break;
|
||||
|
||||
@ -133,10 +157,26 @@ export class PaymentsService {
|
||||
|
||||
private async handleCheckoutCompleted(session: {
|
||||
customer: string;
|
||||
subscription: string;
|
||||
metadata: { userId: string; tier: string };
|
||||
subscription?: string;
|
||||
metadata: { userId?: string; tier?: string; type?: string; courseId?: string };
|
||||
}) {
|
||||
const { customer, subscription: subscriptionId, metadata } = session;
|
||||
if (!metadata?.userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (metadata.type === 'course_purchase') {
|
||||
await this.handleCoursePurchaseCompleted({
|
||||
userId: metadata.userId,
|
||||
courseId: metadata.courseId || '',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!subscriptionId || !metadata.tier) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tier = metadata.tier as SubscriptionTier;
|
||||
|
||||
const stripeSubscription = await this.stripeService.getSubscription(subscriptionId);
|
||||
@ -161,6 +201,95 @@ export class PaymentsService {
|
||||
});
|
||||
}
|
||||
|
||||
private async handleCoursePurchaseCompleted(params: { userId: string; courseId: string }) {
|
||||
const { userId, courseId } = params;
|
||||
if (!courseId) return;
|
||||
|
||||
const course = await this.prisma.course.findUnique({
|
||||
where: { id: courseId },
|
||||
select: { id: true, price: true, currency: true, authorId: true },
|
||||
});
|
||||
if (!course || !course.price) return;
|
||||
|
||||
await this.prisma.purchase.upsert({
|
||||
where: { userId_courseId: { userId, courseId } },
|
||||
create: {
|
||||
userId,
|
||||
courseId,
|
||||
amount: course.price,
|
||||
currency: course.currency,
|
||||
status: 'completed',
|
||||
},
|
||||
update: {
|
||||
status: 'completed',
|
||||
amount: course.price,
|
||||
currency: course.currency,
|
||||
},
|
||||
});
|
||||
|
||||
await this.prisma.enrollment.upsert({
|
||||
where: { userId_courseId: { userId, courseId } },
|
||||
create: { userId, courseId },
|
||||
update: {},
|
||||
});
|
||||
|
||||
const defaultGroup = await this.ensureDefaultCourseGroup(courseId);
|
||||
await this.prisma.groupMember.upsert({
|
||||
where: { groupId_userId: { groupId: defaultGroup.id, userId } },
|
||||
create: { groupId: defaultGroup.id, userId, role: 'student' },
|
||||
update: {},
|
||||
});
|
||||
}
|
||||
|
||||
private async getOrCreateStripeCustomer(userId: string): Promise<{ stripeCustomerId: string; email: string; name: string | null }> {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: { subscription: true },
|
||||
});
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
|
||||
let stripeCustomerId = user.subscription?.stripeCustomerId;
|
||||
if (!stripeCustomerId) {
|
||||
const customer = await this.stripeService.createCustomer(user.email, user.name || undefined);
|
||||
stripeCustomerId = customer.id;
|
||||
await this.prisma.subscription.upsert({
|
||||
where: { userId },
|
||||
create: {
|
||||
userId,
|
||||
tier: SubscriptionTier.FREE,
|
||||
stripeCustomerId: customer.id,
|
||||
},
|
||||
update: {
|
||||
stripeCustomerId: customer.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
stripeCustomerId,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
};
|
||||
}
|
||||
|
||||
private async ensureDefaultCourseGroup(courseId: string) {
|
||||
const existing = await this.prisma.courseGroup.findFirst({
|
||||
where: { courseId, isDefault: true },
|
||||
});
|
||||
if (existing) return existing;
|
||||
|
||||
return this.prisma.courseGroup.create({
|
||||
data: {
|
||||
courseId,
|
||||
name: 'Основная группа',
|
||||
description: 'Обсуждение курса и вопросы преподавателю',
|
||||
isDefault: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async handleSubscriptionUpdated(subscription: {
|
||||
id: string;
|
||||
customer: string;
|
||||
|
||||
@ -45,6 +45,38 @@ export class StripeService {
|
||||
});
|
||||
}
|
||||
|
||||
async createOneTimeCheckoutSession(params: {
|
||||
customerId: string;
|
||||
successUrl: string;
|
||||
cancelUrl: string;
|
||||
metadata?: Record<string, string>;
|
||||
currency: string;
|
||||
unitAmount: number;
|
||||
productName: string;
|
||||
productDescription?: string;
|
||||
}): Promise<Stripe.Checkout.Session> {
|
||||
return this.stripe.checkout.sessions.create({
|
||||
customer: params.customerId,
|
||||
mode: 'payment',
|
||||
line_items: [
|
||||
{
|
||||
price_data: {
|
||||
currency: params.currency.toLowerCase(),
|
||||
unit_amount: params.unitAmount,
|
||||
product_data: {
|
||||
name: params.productName,
|
||||
description: params.productDescription,
|
||||
},
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
success_url: params.successUrl,
|
||||
cancel_url: params.cancelUrl,
|
||||
metadata: params.metadata,
|
||||
});
|
||||
}
|
||||
|
||||
async createPortalSession(customerId: string, returnUrl: string): Promise<Stripe.BillingPortal.Session> {
|
||||
return this.stripe.billingPortal.sessions.create({
|
||||
customer: customerId,
|
||||
|
||||
Reference in New Issue
Block a user