import { Controller, Post, Body, Query, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { PlatformGraphqlService } from '../platform/platform-graphql.service'; @Controller('webhooks/kookoo') export class KookooCallbackController { private readonly logger = new Logger(KookooCallbackController.name); private readonly apiKey: string; constructor( private readonly platform: PlatformGraphqlService, private readonly config: ConfigService, ) { this.apiKey = config.get('platform.apiKey') ?? ''; } @Post('callback') async handleCallback(@Body() body: Record, @Query() query: Record) { // Kookoo sends params as both query and body const params = { ...query, ...body }; this.logger.log(`Kookoo callback: sid=${params.sid} status=${params.status} phone=${params.phone_no} duration=${params.duration}`); const phoneNumber = (params.phone_no ?? '').replace(/^\+?91/, ''); const status = params.status ?? 'unknown'; const duration = parseInt(params.duration ?? '0', 10); const callerId = params.caller_id ?? ''; const startTime = params.start_time ?? null; const endTime = params.end_time ?? null; const sid = params.sid ?? null; if (!phoneNumber) { return { received: true, processed: false }; } const callStatus = status === 'answered' ? 'COMPLETED' : 'MISSED'; const authHeader = this.apiKey ? `Bearer ${this.apiKey}` : ''; if (!authHeader) { this.logger.warn('No PLATFORM_API_KEY — cannot write call records'); return { received: true, processed: false }; } try { // Create call record const callResult = await this.platform.queryWithAuth( `mutation($data: CallCreateInput!) { createCall(data: $data) { id } }`, { data: { name: `Outbound — ${phoneNumber}`, direction: 'OUTBOUND', callStatus, callerNumber: { primaryPhoneNumber: `+91${phoneNumber}` }, startedAt: startTime ? new Date(startTime).toISOString() : new Date().toISOString(), endedAt: endTime ? new Date(endTime).toISOString() : null, durationSec: duration, }, }, authHeader, ); this.logger.log(`Created outbound call record: ${callResult.createCall.id} (${callStatus}, ${duration}s)`); // Try to match to a lead const leadResult = await this.platform.queryWithAuth( `{ leads(first: 50) { edges { node { id name contactPhone { primaryPhoneNumber } } } } }`, undefined, authHeader, ); const leads = leadResult.leads.edges.map((e: any) => e.node); const cleanPhone = phoneNumber.replace(/\D/g, ''); const matchedLead = leads.find((l: any) => { const lp = (l.contactPhone?.primaryPhoneNumber ?? '').replace(/\D/g, ''); return lp.endsWith(cleanPhone) || cleanPhone.endsWith(lp); }); if (matchedLead) { await this.platform.queryWithAuth( `mutation($id: UUID!, $data: CallUpdateInput!) { updateCall(id: $id, data: $data) { id } }`, { id: callResult.createCall.id, data: { leadId: matchedLead.id } }, authHeader, ); this.logger.log(`Linked call to lead ${matchedLead.id} (${matchedLead.name})`); } return { received: true, processed: true, callId: callResult.createCall.id }; } catch (err: any) { const responseData = err?.response?.data ? JSON.stringify(err.response.data) : ''; this.logger.error(`Kookoo callback processing failed: ${err.message} ${responseData}`); return { received: true, processed: false }; } } }