feat: phase1 platform upgrade with moderation, dev payments, admin panel and landing updates
This commit is contained in:
22
apps/api/src/payments/dev-payments.controller.ts
Normal file
22
apps/api/src/payments/dev-payments.controller.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import { User } from '@coursecraft/database';
|
||||
import { PaymentsService } from './payments.service';
|
||||
|
||||
@ApiTags('payments')
|
||||
@Controller('payments')
|
||||
@ApiBearerAuth()
|
||||
export class DevPaymentsController {
|
||||
constructor(private paymentsService: PaymentsService) {}
|
||||
|
||||
@Post('dev/yoomoney/complete')
|
||||
@ApiOperation({ summary: 'Complete DEV YooMoney payment (mock flow)' })
|
||||
async completeDevYoomoneyPayment(
|
||||
@CurrentUser() user: User,
|
||||
@Body('courseId') courseId: string,
|
||||
) {
|
||||
return this.paymentsService.completeDevYoomoneyPayment(user.id, courseId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,9 +3,10 @@ import { PaymentsController } from './payments.controller';
|
||||
import { PaymentsService } from './payments.service';
|
||||
import { StripeService } from './stripe.service';
|
||||
import { WebhooksController } from './webhooks.controller';
|
||||
import { DevPaymentsController } from './dev-payments.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [PaymentsController, WebhooksController],
|
||||
controllers: [PaymentsController, WebhooksController, DevPaymentsController],
|
||||
providers: [PaymentsService, StripeService],
|
||||
exports: [PaymentsService, StripeService],
|
||||
})
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PrismaService } from '../common/prisma/prisma.service';
|
||||
import { StripeService } from './stripe.service';
|
||||
import { SubscriptionTier } from '@coursecraft/database';
|
||||
import { PaymentMode, PaymentProvider, SubscriptionTier } from '@coursecraft/database';
|
||||
import { SUBSCRIPTION_PLANS } from '@coursecraft/shared';
|
||||
|
||||
@Injectable()
|
||||
@ -78,6 +78,9 @@ export class PaymentsService {
|
||||
if (!course) {
|
||||
throw new NotFoundException('Course not found');
|
||||
}
|
||||
if (!course.isPublished) {
|
||||
throw new ForbiddenException('Course is not available for purchase');
|
||||
}
|
||||
if (!course.price) {
|
||||
throw new Error('Course is free, checkout is not required');
|
||||
}
|
||||
@ -85,8 +88,26 @@ export class PaymentsService {
|
||||
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 paymentMode = this.getPaymentMode();
|
||||
|
||||
if (paymentMode === PaymentMode.DEV) {
|
||||
await this.handleCoursePurchaseCompleted({
|
||||
userId,
|
||||
courseId,
|
||||
provider: PaymentProvider.YOOMONEY,
|
||||
mode: PaymentMode.DEV,
|
||||
eventCode: 'DEV_PAYMENT_SUCCESS',
|
||||
});
|
||||
console.log('DEV_PAYMENT_SUCCESS', { userId, courseId, provider: 'YOOMONEY' });
|
||||
return {
|
||||
url: `${appUrl}/courses/${courseId}?purchase=success&devPayment=1`,
|
||||
mode: 'DEV',
|
||||
provider: 'YOOMONEY',
|
||||
};
|
||||
}
|
||||
|
||||
const { stripeCustomerId } = await this.getOrCreateStripeCustomer(userId);
|
||||
const unitAmount = Math.round(Number(course.price) * 100);
|
||||
|
||||
const session = await this.stripeService.createOneTimeCheckoutSession({
|
||||
@ -104,7 +125,23 @@ export class PaymentsService {
|
||||
},
|
||||
});
|
||||
|
||||
return { url: session.url };
|
||||
return { url: session.url, mode: 'PROD', provider: 'STRIPE' };
|
||||
}
|
||||
|
||||
async completeDevYoomoneyPayment(userId: string, courseId: string) {
|
||||
await this.handleCoursePurchaseCompleted({
|
||||
userId,
|
||||
courseId,
|
||||
provider: PaymentProvider.YOOMONEY,
|
||||
mode: PaymentMode.DEV,
|
||||
eventCode: 'DEV_PAYMENT_SUCCESS',
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
eventCode: 'DEV_PAYMENT_SUCCESS',
|
||||
provider: 'YOOMONEY',
|
||||
mode: 'DEV',
|
||||
};
|
||||
}
|
||||
|
||||
async createPortalSession(userId: string) {
|
||||
@ -169,6 +206,9 @@ export class PaymentsService {
|
||||
await this.handleCoursePurchaseCompleted({
|
||||
userId: metadata.userId,
|
||||
courseId: metadata.courseId || '',
|
||||
provider: PaymentProvider.STRIPE,
|
||||
mode: PaymentMode.PROD,
|
||||
eventCode: 'STRIPE_PAYMENT_SUCCESS',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -201,7 +241,13 @@ export class PaymentsService {
|
||||
});
|
||||
}
|
||||
|
||||
private async handleCoursePurchaseCompleted(params: { userId: string; courseId: string }) {
|
||||
private async handleCoursePurchaseCompleted(params: {
|
||||
userId: string;
|
||||
courseId: string;
|
||||
provider?: PaymentProvider;
|
||||
mode?: PaymentMode;
|
||||
eventCode?: string;
|
||||
}) {
|
||||
const { userId, courseId } = params;
|
||||
if (!courseId) return;
|
||||
|
||||
@ -219,11 +265,27 @@ export class PaymentsService {
|
||||
amount: course.price,
|
||||
currency: course.currency,
|
||||
status: 'completed',
|
||||
provider: params.provider || PaymentProvider.STRIPE,
|
||||
mode: params.mode || PaymentMode.PROD,
|
||||
eventCode: params.eventCode || null,
|
||||
metadata: {
|
||||
eventCode: params.eventCode || null,
|
||||
provider: params.provider || PaymentProvider.STRIPE,
|
||||
mode: params.mode || PaymentMode.PROD,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
status: 'completed',
|
||||
amount: course.price,
|
||||
currency: course.currency,
|
||||
provider: params.provider || PaymentProvider.STRIPE,
|
||||
mode: params.mode || PaymentMode.PROD,
|
||||
eventCode: params.eventCode || null,
|
||||
metadata: {
|
||||
eventCode: params.eventCode || null,
|
||||
provider: params.provider || PaymentProvider.STRIPE,
|
||||
mode: params.mode || PaymentMode.PROD,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -241,6 +303,11 @@ export class PaymentsService {
|
||||
});
|
||||
}
|
||||
|
||||
private getPaymentMode(): PaymentMode {
|
||||
const raw = (this.configService.get<string>('PAYMENT_MODE') || 'PROD').toUpperCase();
|
||||
return raw === PaymentMode.DEV ? PaymentMode.DEV : PaymentMode.PROD;
|
||||
}
|
||||
|
||||
private async getOrCreateStripeCustomer(userId: string): Promise<{ stripeCustomerId: string; email: string; name: string | null }> {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
|
||||
Reference in New Issue
Block a user