/** * Subscriptions Controller * * Handles subscription management endpoints: * - GET /subscriptions - Get subscription overview * - GET /subscriptions/plans - Get all available plans * - GET /subscriptions/can-invite - Check if can invite users * - POST /subscriptions/checkout - Create Stripe checkout session * - POST /subscriptions/portal - Create Stripe portal session * - POST /subscriptions/webhook - Handle Stripe webhooks */ import { Controller, Get, Post, Body, UseGuards, HttpCode, HttpStatus, Logger, Headers, RawBodyRequest, Req, Inject, ForbiddenException, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiExcludeEndpoint, } from '@nestjs/swagger'; import { Request } from 'express'; import { SubscriptionService } from '../services/subscription.service'; import { CreateCheckoutSessionDto, CreatePortalSessionDto, SyncSubscriptionDto, SubscriptionOverviewResponseDto, CanInviteResponseDto, CheckoutSessionResponseDto, PortalSessionResponseDto, AllPlansResponseDto, } from '../dto/subscription.dto'; import { JwtAuthGuard } from '../guards/jwt-auth.guard'; import { RolesGuard } from '../guards/roles.guard'; import { Roles } from '../decorators/roles.decorator'; import { CurrentUser, UserPayload } from '../decorators/current-user.decorator'; import { Public } from '../decorators/public.decorator'; import { OrganizationRepository, ORGANIZATION_REPOSITORY, } from '@domain/ports/out/organization.repository'; @ApiTags('Subscriptions') @Controller('subscriptions') export class SubscriptionsController { private readonly logger = new Logger(SubscriptionsController.name); constructor( private readonly subscriptionService: SubscriptionService, @Inject(ORGANIZATION_REPOSITORY) private readonly organizationRepository: OrganizationRepository ) {} /** * Get subscription overview for current organization */ @Get() @UseGuards(JwtAuthGuard, RolesGuard) @Roles('admin', 'manager') @ApiBearerAuth() @ApiOperation({ summary: 'Get subscription overview', description: 'Get the subscription details including licenses for the current organization. Admin/manager only.', }) @ApiResponse({ status: 200, description: 'Subscription overview retrieved successfully', type: SubscriptionOverviewResponseDto, }) @ApiResponse({ status: 403, description: 'Forbidden - requires admin or manager role', }) async getSubscriptionOverview( @CurrentUser() user: UserPayload ): Promise { this.logger.log(`[User: ${user.email}] Getting subscription overview`); return this.subscriptionService.getSubscriptionOverview(user.organizationId, user.role); } /** * Get all available plans */ @Get('plans') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Get all plans', description: 'Get details of all available subscription plans.', }) @ApiResponse({ status: 200, description: 'Plans retrieved successfully', type: AllPlansResponseDto, }) getAllPlans(): AllPlansResponseDto { this.logger.log('Getting all subscription plans'); return this.subscriptionService.getAllPlans(); } /** * Check if organization can invite more users */ @Get('can-invite') @UseGuards(JwtAuthGuard, RolesGuard) @Roles('admin', 'manager') @ApiBearerAuth() @ApiOperation({ summary: 'Check license availability', description: 'Check if the organization can invite more users based on license availability. Admin/manager only.', }) @ApiResponse({ status: 200, description: 'License availability check result', type: CanInviteResponseDto, }) @ApiResponse({ status: 403, description: 'Forbidden - requires admin or manager role', }) async canInvite(@CurrentUser() user: UserPayload): Promise { this.logger.log(`[User: ${user.email}] Checking license availability`); return this.subscriptionService.canInviteUser(user.organizationId, user.role); } /** * Create Stripe Checkout session for subscription upgrade */ @Post('checkout') @HttpCode(HttpStatus.OK) @UseGuards(JwtAuthGuard, RolesGuard) @Roles('admin', 'manager') @ApiBearerAuth() @ApiOperation({ summary: 'Create checkout session', description: 'Create a Stripe Checkout session for upgrading subscription. Admin/Manager only.', }) @ApiResponse({ status: 200, description: 'Checkout session created successfully', type: CheckoutSessionResponseDto, }) @ApiResponse({ status: 400, description: 'Bad request - invalid plan or already subscribed', }) @ApiResponse({ status: 403, description: 'Forbidden - requires admin or manager role', }) async createCheckoutSession( @Body() dto: CreateCheckoutSessionDto, @CurrentUser() user: UserPayload ): Promise { this.logger.log(`[User: ${user.email}] Creating checkout session for plan: ${dto.plan}`); // ADMIN users bypass all payment restrictions if (user.role !== 'ADMIN') { // SIRET verification gate: organization must have a verified SIRET before purchasing const organization = await this.organizationRepository.findById(user.organizationId); if (!organization || !organization.siretVerified) { throw new ForbiddenException( 'Le numero SIRET de votre organisation doit etre verifie par un administrateur avant de pouvoir effectuer un achat. Contactez votre administrateur.' ); } } return this.subscriptionService.createCheckoutSession(user.organizationId, user.id, dto); } /** * Create Stripe Customer Portal session */ @Post('portal') @HttpCode(HttpStatus.OK) @UseGuards(JwtAuthGuard, RolesGuard) @Roles('admin', 'manager') @ApiBearerAuth() @ApiOperation({ summary: 'Create portal session', description: 'Create a Stripe Customer Portal session for subscription management. Admin/Manager only.', }) @ApiResponse({ status: 200, description: 'Portal session created successfully', type: PortalSessionResponseDto, }) @ApiResponse({ status: 400, description: 'Bad request - no Stripe customer found', }) @ApiResponse({ status: 403, description: 'Forbidden - requires admin or manager role', }) async createPortalSession( @Body() dto: CreatePortalSessionDto, @CurrentUser() user: UserPayload ): Promise { this.logger.log(`[User: ${user.email}] Creating portal session`); return this.subscriptionService.createPortalSession(user.organizationId, dto); } /** * Sync subscription from Stripe * Useful when webhooks are not available (e.g., local development) */ @Post('sync') @HttpCode(HttpStatus.OK) @UseGuards(JwtAuthGuard, RolesGuard) @Roles('admin', 'manager') @ApiBearerAuth() @ApiOperation({ summary: 'Sync subscription from Stripe', description: 'Manually sync subscription data from Stripe. Useful when webhooks are not working (local dev). Pass sessionId after checkout to sync new subscription. Admin/Manager only.', }) @ApiResponse({ status: 200, description: 'Subscription synced successfully', type: SubscriptionOverviewResponseDto, }) @ApiResponse({ status: 400, description: 'Bad request - no Stripe subscription found', }) @ApiResponse({ status: 403, description: 'Forbidden - requires admin or manager role', }) async syncFromStripe( @Body() dto: SyncSubscriptionDto, @CurrentUser() user: UserPayload ): Promise { this.logger.log( `[User: ${user.email}] Syncing subscription from Stripe${dto.sessionId ? ` (sessionId: ${dto.sessionId})` : ''}` ); return this.subscriptionService.syncFromStripe(user.organizationId, dto.sessionId); } /** * Handle Stripe webhook events */ @Post('webhook') @Public() @HttpCode(HttpStatus.OK) @ApiExcludeEndpoint() async handleWebhook( @Headers('stripe-signature') signature: string, @Req() req: RawBodyRequest ): Promise<{ received: boolean }> { const rawBody = req.rawBody; if (!rawBody) { this.logger.error('No raw body found in request'); return { received: false }; } try { await this.subscriptionService.handleStripeWebhook(rawBody, signature); return { received: true }; } catch (error) { this.logger.error('Webhook processing failed', error); return { received: false }; } } }