your message

This commit is contained in:
root
2026-02-06 14:53:52 +00:00
parent c809d049fe
commit 3d488f22b7
47 changed files with 3127 additions and 425 deletions

View File

@ -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;

View File

@ -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,