284 lines
8.4 KiB
TypeScript
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 };
|
|
}
|
|
}
|
|
}
|