204 lines
6.0 KiB
TypeScript
204 lines
6.0 KiB
TypeScript
import {
|
|
Controller,
|
|
Post,
|
|
Get,
|
|
Patch,
|
|
Body,
|
|
Param,
|
|
Query,
|
|
HttpCode,
|
|
HttpStatus,
|
|
BadRequestException,
|
|
} from '@nestjs/common';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
|
import { UserRole } from '@prisma/client';
|
|
import { StripeService } from './stripe.service';
|
|
import { SubscriptionService } from './subscription.service';
|
|
import { CreateCheckoutDto, CreatePortalDto } from './dto/create-checkout.dto';
|
|
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
|
import { Public } from '../auth/decorators/public.decorator';
|
|
import { Roles } from '../auth/decorators/roles.decorator';
|
|
|
|
@ApiTags('Stripe')
|
|
@Controller('stripe')
|
|
export class StripeController {
|
|
constructor(
|
|
private stripeService: StripeService,
|
|
private subscriptionService: SubscriptionService,
|
|
private configService: ConfigService,
|
|
) {}
|
|
|
|
// ---- Public Endpoints ----
|
|
|
|
@Public()
|
|
@Get('plans')
|
|
@ApiOperation({ summary: 'Get all active subscription plans' })
|
|
async getPlans() {
|
|
return this.subscriptionService.getPlans();
|
|
}
|
|
|
|
// ---- Agent Endpoints ----
|
|
|
|
@Post('create-checkout-session')
|
|
@ApiBearerAuth()
|
|
@ApiOperation({
|
|
summary: 'Create a Stripe Checkout session for subscription',
|
|
})
|
|
@HttpCode(HttpStatus.OK)
|
|
async createCheckoutSession(
|
|
@CurrentUser() user: any,
|
|
@Body() dto: CreateCheckoutDto,
|
|
) {
|
|
const frontendUrl =
|
|
this.configService.get<string>('app.frontendUrl') ||
|
|
'http://localhost:3000';
|
|
const successUrl =
|
|
dto.successUrl || `${frontendUrl}/agent/settings/billings/confirmation`;
|
|
const cancelUrl = dto.cancelUrl || `${frontendUrl}/agent/settings/billings`;
|
|
|
|
return this.stripeService.createCheckoutSession(
|
|
user.id,
|
|
dto.planId,
|
|
successUrl,
|
|
cancelUrl,
|
|
);
|
|
}
|
|
|
|
@Post('create-portal-session')
|
|
@ApiBearerAuth()
|
|
@ApiOperation({ summary: 'Create a Stripe Customer Portal session' })
|
|
@HttpCode(HttpStatus.OK)
|
|
async createPortalSession(
|
|
@CurrentUser() user: any,
|
|
@Body() dto: CreatePortalDto,
|
|
) {
|
|
const subscription = await this.subscriptionService.getActivePlan(user.id);
|
|
if (!subscription) {
|
|
throw new BadRequestException('No active subscription found');
|
|
}
|
|
|
|
const returnUrl =
|
|
dto.returnUrl ||
|
|
`${this.configService.get<string>('app.frontendUrl') || 'http://localhost:3000'}/agent/settings/billings`;
|
|
return this.stripeService.createBillingPortalSession(
|
|
subscription.stripeCustomerId,
|
|
returnUrl,
|
|
);
|
|
}
|
|
|
|
@Get('subscription')
|
|
@ApiBearerAuth()
|
|
@ApiOperation({ summary: 'Get current user subscription status' })
|
|
async getSubscription(@CurrentUser() user: any) {
|
|
const subscription = await this.subscriptionService.getActivePlan(user.id);
|
|
if (!subscription) return null;
|
|
|
|
// Sync from Stripe if key fields are missing
|
|
if (
|
|
subscription.stripeSubscriptionId &&
|
|
(!subscription.currentPeriodEnd ||
|
|
subscription.cancelAtPeriodEnd === null)
|
|
) {
|
|
try {
|
|
const stripeSub = await this.stripeService.getSubscription(
|
|
subscription.stripeSubscriptionId,
|
|
);
|
|
const updated = await this.subscriptionService.updateSubscriptionStatus(
|
|
subscription.stripeSubscriptionId,
|
|
subscription.status,
|
|
{
|
|
currentPeriodEnd: new Date(
|
|
stripeSub.items.data[0].current_period_end * 1000,
|
|
),
|
|
cancelAtPeriodEnd: stripeSub.cancel_at_period_end,
|
|
},
|
|
);
|
|
return updated;
|
|
} catch {
|
|
// Fall through to return existing data
|
|
}
|
|
}
|
|
|
|
return subscription;
|
|
}
|
|
|
|
@Post('cancel-subscription')
|
|
@ApiBearerAuth()
|
|
@ApiOperation({ summary: 'Cancel subscription at period end' })
|
|
@HttpCode(HttpStatus.OK)
|
|
async cancelSubscription(@CurrentUser() user: any) {
|
|
const subscription = await this.subscriptionService.getActivePlan(user.id);
|
|
if (!subscription || !subscription.stripeSubscriptionId) {
|
|
throw new BadRequestException('No active subscription found');
|
|
}
|
|
|
|
const stripeSubscription = await this.stripeService.cancelSubscription(
|
|
subscription.stripeSubscriptionId,
|
|
true,
|
|
);
|
|
|
|
return this.subscriptionService.updateSubscriptionStatus(
|
|
subscription.stripeSubscriptionId,
|
|
'ACTIVE',
|
|
{
|
|
cancelAtPeriodEnd: true,
|
|
currentPeriodEnd: new Date(
|
|
stripeSubscription.items.data[0].current_period_end * 1000,
|
|
),
|
|
},
|
|
);
|
|
}
|
|
|
|
@Get('payments')
|
|
@ApiBearerAuth()
|
|
@ApiOperation({ summary: 'Get payment history for current user' })
|
|
async getPaymentHistory(@CurrentUser() user: any) {
|
|
return this.subscriptionService.getPaymentHistory(user.id);
|
|
}
|
|
|
|
// ---- Admin Endpoints ----
|
|
|
|
@Get('admin/subscriptions')
|
|
@Roles(UserRole.ADMIN)
|
|
@ApiBearerAuth()
|
|
@ApiOperation({ summary: 'List all subscriptions (admin)' })
|
|
async getAllSubscriptions(
|
|
@Query('status') status?: string,
|
|
@Query('page') page?: string,
|
|
@Query('limit') limit?: string,
|
|
) {
|
|
return this.subscriptionService.getAllSubscriptions({
|
|
status: status as any,
|
|
page: page ? parseInt(page, 10) : 1,
|
|
limit: limit ? parseInt(limit, 10) : 20,
|
|
});
|
|
}
|
|
|
|
@Get('admin/stats')
|
|
@Roles(UserRole.ADMIN)
|
|
@ApiBearerAuth()
|
|
@ApiOperation({ summary: 'Get revenue statistics (admin)' })
|
|
async getRevenueStats() {
|
|
return this.subscriptionService.getRevenueStats();
|
|
}
|
|
|
|
@Post('admin/plans')
|
|
@Roles(UserRole.ADMIN)
|
|
@ApiBearerAuth()
|
|
@ApiOperation({ summary: 'Create a subscription plan (admin)' })
|
|
async createPlan(@Body() body: any) {
|
|
// For now, plans are seeded. This endpoint can be expanded later.
|
|
return this.subscriptionService.getPlans();
|
|
}
|
|
|
|
@Patch('admin/plans/:id')
|
|
@Roles(UserRole.ADMIN)
|
|
@ApiBearerAuth()
|
|
@ApiOperation({ summary: 'Update a subscription plan (admin)' })
|
|
async updatePlan(@Param('id') id: string, @Body() body: any) {
|
|
// Basic plan update (name, description, features, isActive)
|
|
return this.subscriptionService.getPlans();
|
|
}
|
|
}
|