import { Controller, Post, Get, Body, Req, Res, UseGuards, Logger, Query, } from '@nestjs/common'; import { FastifyRequest, FastifyReply } from 'fastify'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { VoiceService } from './voice.service'; import { VoiceGateway } from './voice.gateway'; import { InitiateCallDto } from './dto/initiate-call.dto'; import { TenantId } from '../tenant/tenant.decorator'; @Controller('voice') export class VoiceController { private readonly logger = new Logger(VoiceController.name); constructor( private readonly voiceService: VoiceService, private readonly voiceGateway: VoiceGateway, ) {} /** * Initiate outbound call via REST */ @Post('call') @UseGuards(JwtAuthGuard) async initiateCall( @Body() body: InitiateCallDto, @Req() req: any, @TenantId() tenantId: string, ) { const userId = req.user?.userId || req.user?.sub; const result = await this.voiceService.initiateCall({ tenantId, userId, toNumber: body.toNumber, }); return { success: true, data: result, }; } /** * Generate Twilio access token for browser client */ @Get('token') @UseGuards(JwtAuthGuard) async getAccessToken( @Req() req: any, @TenantId() tenantId: string, ) { const userId = req.user?.userId || req.user?.sub; const token = await this.voiceService.generateAccessToken(tenantId, userId); return { success: true, data: { token }, }; } /** * Get call history */ @Get('calls') @UseGuards(JwtAuthGuard) async getCallHistory( @Req() req: any, @TenantId() tenantId: string, @Query('limit') limit?: string, ) { const userId = req.user?.userId || req.user?.sub; const calls = await this.voiceService.getCallHistory( tenantId, userId, limit ? parseInt(limit) : 50, ); return { success: true, data: calls, }; } /** * TwiML for outbound calls from browser (Twilio Device) */ @Post('twiml/outbound') async outboundTwiml(@Req() req: FastifyRequest, @Res() res: FastifyReply) { const body = req.body as any; const to = body.To; const from = body.From; const callSid = body.CallSid; this.logger.log(`=== TwiML OUTBOUND REQUEST RECEIVED ===`); this.logger.log(`CallSid: ${callSid}, Body From: ${from}, Body To: ${to}`); this.logger.log(`Full body: ${JSON.stringify(body)}`); try { // Extract tenant domain from Host header const host = req.headers.host || ''; const tenantDomain = host.split('.')[0]; // e.g., "tenant1" from "tenant1.routebox.co" this.logger.log(`Extracted tenant domain: ${tenantDomain}`); // Look up tenant's Twilio phone number from config let callerId = to; // Fallback (will cause error if not found) try { // Get Twilio config to find the phone number const { config } = await this.voiceService['getTwilioClient'](tenantDomain); callerId = config.phoneNumber; this.logger.log(`Retrieved Twilio phone number for tenant: ${callerId}`); } catch (error: any) { this.logger.error(`Failed to get Twilio config: ${error.message}`); } const dialNumber = to; this.logger.log(`Using callerId: ${callerId}, dialNumber: ${dialNumber}`); // Return TwiML to DIAL the phone number with proper callerId const twiml = ` ${dialNumber} `; this.logger.log(`Returning TwiML with Dial verb - callerId: ${callerId}, to: ${dialNumber}`); res.type('text/xml').send(twiml); } catch (error: any) { this.logger.error(`=== ERROR GENERATING TWIML ===`); this.logger.error(`Error: ${error.message}`); this.logger.error(`Stack: ${error.stack}`); const errorTwiml = ` An error occurred while processing your call. `; res.type('text/xml').send(errorTwiml); } } /** * TwiML for inbound calls */ @Post('twiml/inbound') async inboundTwiml(@Req() req: FastifyRequest, @Res() res: FastifyReply) { const body = req.body as any; const callSid = body.CallSid; const fromNumber = body.From; const toNumber = body.To; this.logger.log(`=== INBOUND CALL RECEIVED ===`); this.logger.log(`CallSid: ${callSid}, From: ${fromNumber}, To: ${toNumber}`); this.logger.log(`Full body: ${JSON.stringify(body)}`); try { // Extract tenant domain from Host header const host = req.headers.host || ''; const tenantDomain = host.split('.')[0]; // e.g., "tenant1" from "tenant1.routebox.co" this.logger.log(`Extracted tenant domain: ${tenantDomain}`); // Get all connected users for this tenant const connectedUsers = this.voiceGateway.getConnectedUsers(tenantDomain); this.logger.log(`Connected users for tenant ${tenantDomain}: ${connectedUsers.length}`); if (connectedUsers.length === 0) { // No users online - send to voicemail or play message const twiml = ` Sorry, no agents are currently available. Please try again later. `; this.logger.log(`No users online - returning unavailable message`); return res.type('text/xml').send(twiml); } // Build TwiML to dial all connected clients (first to answer gets the call) const clientElements = connectedUsers.map(userId => ` ${userId}`).join('\n'); const twiml = ` Connecting your call ${clientElements} `; this.logger.log(`Returning inbound TwiML - dialing ${connectedUsers.length} client(s)`); res.type('text/xml').send(twiml); } catch (error: any) { this.logger.error(`Error generating inbound TwiML: ${error.message}`); const errorTwiml = ` Sorry, we are unable to connect your call at this time. `; res.type('text/xml').send(errorTwiml); } } /** * Twilio status webhook */ @Post('webhook/status') async statusWebhook(@Req() req: FastifyRequest) { const body = req.body as any; const callSid = body.CallSid; const status = body.CallStatus; const duration = body.CallDuration ? parseInt(body.CallDuration) : undefined; this.logger.log(`Call status webhook - CallSid: ${callSid}, Status: ${status}, Duration: ${duration}`); this.logger.log(`Full status webhook body:`, JSON.stringify(body)); return { success: true }; } /** * Twilio recording webhook */ @Post('webhook/recording') async recordingWebhook(@Req() req: FastifyRequest) { const body = req.body as any; const callSid = body.CallSid; const recordingSid = body.RecordingSid; const recordingStatus = body.RecordingStatus; this.logger.log(`Recording webhook - CallSid: ${callSid}, RecordingSid: ${recordingSid}, Status: ${recordingStatus}`); return { success: true }; } }