feat: switch outbound dial to Kookoo API — outbound calls now work

- Replace CloudAgent V3 tbManualDial with Kookoo outbound.php
- Simple HTTP GET with api_key — no auth issues
- Kookoo callback endpoint: POST /webhooks/kookoo/callback
  - Creates Call record in platform
  - Matches caller to Lead by phone
- Remove agent login requirement before dial
- Tested: call queued successfully, phone rang

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 18:03:44 +05:30
parent ea482d0fed
commit 6812006b53
4 changed files with 131 additions and 37 deletions

View File

@@ -0,0 +1,90 @@
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<string>('platform.apiKey') ?? '';
}
@Post('callback')
async handleCallback(@Body() body: Record<string, any>, @Query() query: Record<string, any>) {
// 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<any>(
`mutation($data: CallCreateInput!) { createCall(data: $data) { id } }`,
{
data: {
name: `Outbound — ${phoneNumber}`,
direction: 'OUTBOUND',
callStatus,
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<any>(
`{ 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<any>(
`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) {
this.logger.error(`Kookoo callback processing failed: ${err}`);
return { received: true, processed: false };
}
}
}