mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-04-12 02:18:18 +00:00
- Fix Call record field names (recording, callerNumber, durationSec) - Add POST /api/ozonetel/agent-ready using logout+login for Force Ready - Add callerNumber to kookoo callback - Better error logging with response body Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
93 lines
4.1 KiB
TypeScript
93 lines
4.1 KiB
TypeScript
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,
|
|
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<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: 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 };
|
|
}
|
|
}
|
|
}
|