feat: add Exotel webhook controller and service for call event parsing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-17 09:04:15 +05:30
parent 5b35c65e6e
commit 30df1d0158
7 changed files with 215 additions and 0 deletions

View File

View File

@@ -0,0 +1,24 @@
import { Controller, Post, Body, Logger, HttpCode } from '@nestjs/common';
import { ExotelService } from './exotel.service';
import type { ExotelWebhookPayload } from './exotel.types';
@Controller('webhooks/exotel')
export class ExotelController {
private readonly logger = new Logger(ExotelController.name);
constructor(private readonly exotelService: ExotelService) {}
@Post('call-status')
@HttpCode(200)
async handleCallStatus(@Body() payload: ExotelWebhookPayload) {
this.logger.log(`Received Exotel webhook: ${payload.event_details?.event_type}`);
const callEvent = this.exotelService.parseWebhook(payload);
// TODO: Forward to CallEventsService (Task 4)
// For now, just log
this.logger.log(`Call event: ${JSON.stringify(callEvent)}`);
return { status: 'received' };
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { ExotelController } from './exotel.controller';
import { ExotelService } from './exotel.service';
@Module({
controllers: [ExotelController],
providers: [ExotelService],
exports: [ExotelService],
})
export class ExotelModule {}

View File

@@ -0,0 +1,31 @@
import { Injectable, Logger } from '@nestjs/common';
import type { ExotelWebhookPayload, CallEvent } from './exotel.types';
@Injectable()
export class ExotelService {
private readonly logger = new Logger(ExotelService.name);
parseWebhook(payload: ExotelWebhookPayload): CallEvent {
const { event_details, call_details } = payload;
const eventType = event_details.event_type === 'answered' ? 'answered'
: event_details.event_type === 'terminal' ? 'ended'
: 'ringing';
const callEvent: CallEvent = {
exotelCallSid: call_details.call_sid,
eventType,
direction: call_details.direction,
callerPhone: call_details.customer_details?.number ?? '',
agentName: call_details.assigned_agent_details?.name ?? 'Unknown',
agentPhone: call_details.assigned_agent_details?.number ?? '',
duration: call_details.total_talk_time,
recordingUrl: call_details.recordings?.[0]?.url,
callStatus: call_details.call_status,
timestamp: new Date().toISOString(),
};
this.logger.log(`Parsed Exotel event: ${eventType} for call ${call_details.call_sid}`);
return callEvent;
}
}

View File

@@ -0,0 +1,35 @@
// Exotel webhook payload (from their API docs)
export type ExotelWebhookPayload = {
event_details: {
event_type: 'answered' | 'terminal';
};
call_details: {
call_sid: string;
direction: 'inbound' | 'outbound';
call_status?: string;
total_talk_time?: number;
assigned_agent_details?: {
name: string;
number: string;
};
customer_details?: {
number: string;
name?: string;
};
recordings?: { url: string }[];
};
};
// Internal call event (normalized)
export type CallEvent = {
exotelCallSid: string;
eventType: 'ringing' | 'answered' | 'ended';
direction: 'inbound' | 'outbound';
callerPhone: string;
agentName: string;
agentPhone: string;
duration?: number;
recordingUrl?: string;
callStatus?: string;
timestamp: string;
};