297 lines
8.6 KiB
TypeScript
297 lines
8.6 KiB
TypeScript
![]() |
import {
|
||
|
Controller,
|
||
|
Post,
|
||
|
Get,
|
||
|
Body,
|
||
|
Param,
|
||
|
UseGuards,
|
||
|
Request,
|
||
|
RawBodyRequest,
|
||
|
Req,
|
||
|
Headers,
|
||
|
HttpStatus,
|
||
|
HttpException,
|
||
|
Logger,
|
||
|
} from '@nestjs/common';
|
||
|
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||
|
import { JwtAuthGuard } from '../auth/auth.guard';
|
||
|
import { PaymentsService } from './payments.service';
|
||
|
import { StripeService } from './services/stripe.service';
|
||
|
import { WebhookService } from './services/webhook.service';
|
||
|
import { CreateCheckoutSessionDto } from './dto/create-checkout-session.dto';
|
||
|
import { CreatePortalSessionDto } from './dto/create-portal-session.dto';
|
||
|
import { Plan } from '@prisma/client';
|
||
|
|
||
|
@ApiTags('payments')
|
||
|
@Controller('payments')
|
||
|
export class PaymentsController {
|
||
|
private readonly logger = new Logger(PaymentsController.name);
|
||
|
|
||
|
constructor(
|
||
|
private readonly paymentsService: PaymentsService,
|
||
|
private readonly stripeService: StripeService,
|
||
|
private readonly webhookService: WebhookService,
|
||
|
) {}
|
||
|
|
||
|
@Post('checkout')
|
||
|
@UseGuards(JwtAuthGuard)
|
||
|
@ApiBearerAuth()
|
||
|
@ApiOperation({ summary: 'Create Stripe checkout session' })
|
||
|
@ApiResponse({ status: 201, description: 'Checkout session created successfully' })
|
||
|
async createCheckoutSession(
|
||
|
@Request() req: any,
|
||
|
@Body() createCheckoutSessionDto: CreateCheckoutSessionDto,
|
||
|
) {
|
||
|
try {
|
||
|
const userId = req.user.id;
|
||
|
const session = await this.stripeService.createCheckoutSession(
|
||
|
userId,
|
||
|
createCheckoutSessionDto.plan,
|
||
|
createCheckoutSessionDto.successUrl,
|
||
|
createCheckoutSessionDto.cancelUrl,
|
||
|
);
|
||
|
|
||
|
return {
|
||
|
sessionId: session.id,
|
||
|
url: session.url,
|
||
|
};
|
||
|
} catch (error) {
|
||
|
this.logger.error('Failed to create checkout session:', error);
|
||
|
throw new HttpException(
|
||
|
'Failed to create checkout session',
|
||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Post('portal')
|
||
|
@UseGuards(JwtAuthGuard)
|
||
|
@ApiBearerAuth()
|
||
|
@ApiOperation({ summary: 'Create Stripe customer portal session' })
|
||
|
@ApiResponse({ status: 201, description: 'Portal session created successfully' })
|
||
|
async createPortalSession(
|
||
|
@Request() req: any,
|
||
|
@Body() createPortalSessionDto: CreatePortalSessionDto,
|
||
|
) {
|
||
|
try {
|
||
|
const userId = req.user.id;
|
||
|
const session = await this.stripeService.createPortalSession(
|
||
|
userId,
|
||
|
createPortalSessionDto.returnUrl,
|
||
|
);
|
||
|
|
||
|
return {
|
||
|
url: session.url,
|
||
|
};
|
||
|
} catch (error) {
|
||
|
this.logger.error('Failed to create portal session:', error);
|
||
|
throw new HttpException(
|
||
|
'Failed to create portal session',
|
||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Get('subscription')
|
||
|
@UseGuards(JwtAuthGuard)
|
||
|
@ApiBearerAuth()
|
||
|
@ApiOperation({ summary: 'Get user subscription details' })
|
||
|
@ApiResponse({ status: 200, description: 'Subscription details retrieved successfully' })
|
||
|
async getSubscription(@Request() req: any) {
|
||
|
try {
|
||
|
const userId = req.user.id;
|
||
|
const subscription = await this.paymentsService.getUserSubscription(userId);
|
||
|
return subscription;
|
||
|
} catch (error) {
|
||
|
this.logger.error('Failed to get subscription:', error);
|
||
|
throw new HttpException(
|
||
|
'Failed to get subscription details',
|
||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Get('plans')
|
||
|
@ApiOperation({ summary: 'Get available subscription plans' })
|
||
|
@ApiResponse({ status: 200, description: 'Plans retrieved successfully' })
|
||
|
async getPlans() {
|
||
|
return {
|
||
|
plans: [
|
||
|
{
|
||
|
id: Plan.BASIC,
|
||
|
name: 'Basic',
|
||
|
price: 0,
|
||
|
currency: 'usd',
|
||
|
interval: 'month',
|
||
|
features: [
|
||
|
'50 images per month',
|
||
|
'AI-powered naming',
|
||
|
'Keyword enhancement',
|
||
|
'ZIP download',
|
||
|
],
|
||
|
quotaLimit: 50,
|
||
|
},
|
||
|
{
|
||
|
id: Plan.PRO,
|
||
|
name: 'Pro',
|
||
|
price: 900, // $9.00 in cents
|
||
|
currency: 'usd',
|
||
|
interval: 'month',
|
||
|
features: [
|
||
|
'500 images per month',
|
||
|
'AI-powered naming',
|
||
|
'Keyword enhancement',
|
||
|
'ZIP download',
|
||
|
'Priority support',
|
||
|
],
|
||
|
quotaLimit: 500,
|
||
|
},
|
||
|
{
|
||
|
id: Plan.MAX,
|
||
|
name: 'Max',
|
||
|
price: 1900, // $19.00 in cents
|
||
|
currency: 'usd',
|
||
|
interval: 'month',
|
||
|
features: [
|
||
|
'1000 images per month',
|
||
|
'AI-powered naming',
|
||
|
'Keyword enhancement',
|
||
|
'ZIP download',
|
||
|
'Priority support',
|
||
|
'Advanced analytics',
|
||
|
],
|
||
|
quotaLimit: 1000,
|
||
|
},
|
||
|
],
|
||
|
};
|
||
|
}
|
||
|
|
||
|
@Post('cancel-subscription')
|
||
|
@UseGuards(JwtAuthGuard)
|
||
|
@ApiBearerAuth()
|
||
|
@ApiOperation({ summary: 'Cancel user subscription' })
|
||
|
@ApiResponse({ status: 200, description: 'Subscription cancelled successfully' })
|
||
|
async cancelSubscription(@Request() req: any) {
|
||
|
try {
|
||
|
const userId = req.user.id;
|
||
|
await this.paymentsService.cancelSubscription(userId);
|
||
|
return { message: 'Subscription cancelled successfully' };
|
||
|
} catch (error) {
|
||
|
this.logger.error('Failed to cancel subscription:', error);
|
||
|
throw new HttpException(
|
||
|
'Failed to cancel subscription',
|
||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Post('reactivate-subscription')
|
||
|
@UseGuards(JwtAuthGuard)
|
||
|
@ApiBearerAuth()
|
||
|
@ApiOperation({ summary: 'Reactivate cancelled subscription' })
|
||
|
@ApiResponse({ status: 200, description: 'Subscription reactivated successfully' })
|
||
|
async reactivateSubscription(@Request() req: any) {
|
||
|
try {
|
||
|
const userId = req.user.id;
|
||
|
await this.paymentsService.reactivateSubscription(userId);
|
||
|
return { message: 'Subscription reactivated successfully' };
|
||
|
} catch (error) {
|
||
|
this.logger.error('Failed to reactivate subscription:', error);
|
||
|
throw new HttpException(
|
||
|
'Failed to reactivate subscription',
|
||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Get('payment-history')
|
||
|
@UseGuards(JwtAuthGuard)
|
||
|
@ApiBearerAuth()
|
||
|
@ApiOperation({ summary: 'Get user payment history' })
|
||
|
@ApiResponse({ status: 200, description: 'Payment history retrieved successfully' })
|
||
|
async getPaymentHistory(@Request() req: any) {
|
||
|
try {
|
||
|
const userId = req.user.id;
|
||
|
const payments = await this.paymentsService.getPaymentHistory(userId);
|
||
|
return { payments };
|
||
|
} catch (error) {
|
||
|
this.logger.error('Failed to get payment history:', error);
|
||
|
throw new HttpException(
|
||
|
'Failed to get payment history',
|
||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Post('webhook')
|
||
|
@ApiOperation({ summary: 'Handle Stripe webhooks' })
|
||
|
@ApiResponse({ status: 200, description: 'Webhook processed successfully' })
|
||
|
async handleWebhook(
|
||
|
@Req() req: RawBodyRequest<Request>,
|
||
|
@Headers('stripe-signature') signature: string,
|
||
|
) {
|
||
|
try {
|
||
|
await this.webhookService.handleWebhook(req.rawBody, signature);
|
||
|
return { received: true };
|
||
|
} catch (error) {
|
||
|
this.logger.error('Webhook processing failed:', error);
|
||
|
throw new HttpException(
|
||
|
'Webhook processing failed',
|
||
|
HttpStatus.BAD_REQUEST,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Post('upgrade')
|
||
|
@UseGuards(JwtAuthGuard)
|
||
|
@ApiBearerAuth()
|
||
|
@ApiOperation({ summary: 'Upgrade subscription plan' })
|
||
|
@ApiResponse({ status: 200, description: 'Plan upgraded successfully' })
|
||
|
async upgradePlan(
|
||
|
@Request() req: any,
|
||
|
@Body() body: { plan: Plan; successUrl: string; cancelUrl: string },
|
||
|
) {
|
||
|
try {
|
||
|
const userId = req.user.id;
|
||
|
const session = await this.paymentsService.upgradePlan(
|
||
|
userId,
|
||
|
body.plan,
|
||
|
body.successUrl,
|
||
|
body.cancelUrl,
|
||
|
);
|
||
|
|
||
|
return {
|
||
|
sessionId: session.id,
|
||
|
url: session.url,
|
||
|
};
|
||
|
} catch (error) {
|
||
|
this.logger.error('Failed to upgrade plan:', error);
|
||
|
throw new HttpException(
|
||
|
'Failed to upgrade plan',
|
||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Post('downgrade')
|
||
|
@UseGuards(JwtAuthGuard)
|
||
|
@ApiBearerAuth()
|
||
|
@ApiOperation({ summary: 'Downgrade subscription plan' })
|
||
|
@ApiResponse({ status: 200, description: 'Plan downgraded successfully' })
|
||
|
async downgradePlan(
|
||
|
@Request() req: any,
|
||
|
@Body() body: { plan: Plan },
|
||
|
) {
|
||
|
try {
|
||
|
const userId = req.user.id;
|
||
|
await this.paymentsService.downgradePlan(userId, body.plan);
|
||
|
return { message: 'Plan downgraded successfully' };
|
||
|
} catch (error) {
|
||
|
this.logger.error('Failed to downgrade plan:', error);
|
||
|
throw new HttpException(
|
||
|
'Failed to downgrade plan',
|
||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|