xpeditis2.0/apps/backend/src/application/controllers/subscriptions.controller.ts
2026-03-19 19:04:31 +01:00

284 lines
8.4 KiB
TypeScript

/**
* 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<SubscriptionOverviewResponseDto> {
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<CanInviteResponseDto> {
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<CheckoutSessionResponseDto> {
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<PortalSessionResponseDto> {
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<SubscriptionOverviewResponseDto> {
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<Request>
): 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 };
}
}
}