import { Controller, Post, Put, Get, Body, Param, Query, Req, UseGuards, RawBodyRequest, BadRequestException, ForbiddenException, Request, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; import { Throttle } from '@nestjs/throttler'; import { Request as ExpressRequest } from 'express'; import { DataSource } from 'typeorm'; import { BillingService } from './billing.service'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; @ApiTags('billing') @Controller() export class BillingController { constructor( private billingService: BillingService, private dataSource: DataSource, ) {} @Post('billing/start-trial') @ApiOperation({ summary: 'Start a free trial (no card required)' }) @Throttle({ default: { limit: 10, ttl: 60000 } }) async startTrial( @Body() body: { planId: string; billingInterval?: 'month' | 'year'; email: string; businessName: string }, ) { if (!body.planId) throw new BadRequestException('planId is required'); if (!body.email) throw new BadRequestException('email is required'); if (!body.businessName) throw new BadRequestException('businessName is required'); return this.billingService.startTrial( body.planId, body.billingInterval || 'month', body.email, body.businessName, ); } @Post('billing/create-checkout-session') @ApiOperation({ summary: 'Create a Stripe Checkout Session' }) @Throttle({ default: { limit: 10, ttl: 60000 } }) async createCheckout( @Body() body: { planId: string; billingInterval?: 'month' | 'year'; email?: string; businessName?: string }, ) { if (!body.planId) throw new BadRequestException('planId is required'); return this.billingService.createCheckoutSession( body.planId, body.billingInterval || 'month', body.email, body.businessName, ); } @Post('webhooks/stripe') @ApiOperation({ summary: 'Stripe webhook endpoint' }) async handleWebhook(@Req() req: RawBodyRequest) { const signature = req.headers['stripe-signature'] as string; if (!signature) throw new BadRequestException('Missing Stripe signature'); if (!req.rawBody) throw new BadRequestException('Missing raw body'); await this.billingService.handleWebhook(req.rawBody, signature); return { received: true }; } @Get('billing/status') @ApiOperation({ summary: 'Check provisioning status for a checkout session or subscription' }) async getStatus(@Query('session_id') sessionId: string) { if (!sessionId) throw new BadRequestException('session_id required'); return this.billingService.getProvisioningStatus(sessionId); } @Get('billing/subscription') @ApiOperation({ summary: 'Get current subscription info' }) @ApiBearerAuth() @UseGuards(JwtAuthGuard) async getSubscription(@Request() req: any) { const orgId = req.user.orgId; if (!orgId) throw new BadRequestException('No organization context'); return this.billingService.getSubscriptionInfo(orgId); } @Post('billing/portal') @ApiOperation({ summary: 'Create Stripe Customer Portal session' }) @ApiBearerAuth() @UseGuards(JwtAuthGuard) async createPortal(@Request() req: any) { const orgId = req.user.orgId; if (!orgId) throw new BadRequestException('No organization context'); return this.billingService.createPortalSession(orgId); } // ─── Admin: Switch Billing Method (ACH / Invoice) ────────── @Put('admin/organizations/:id/billing') @ApiOperation({ summary: 'Switch organization billing method (superadmin only)' }) @ApiBearerAuth() @UseGuards(JwtAuthGuard) async updateBillingMethod( @Request() req: any, @Param('id') id: string, @Body() body: { collectionMethod: 'charge_automatically' | 'send_invoice'; daysUntilDue?: number }, ) { // Require superadmin const userId = req.user.userId || req.user.sub; const userRows = await this.dataSource.query( `SELECT is_superadmin FROM shared.users WHERE id = $1`, [userId], ); if (!userRows.length || !userRows[0].is_superadmin) { throw new ForbiddenException('Superadmin access required'); } if (!['charge_automatically', 'send_invoice'].includes(body.collectionMethod)) { throw new BadRequestException('collectionMethod must be "charge_automatically" or "send_invoice"'); } await this.billingService.switchToInvoiceBilling( id, body.collectionMethod, body.daysUntilDue || 30, ); return { success: true }; } }