diff --git a/src/ozonetel/ozonetel-agent.controller.ts b/src/ozonetel/ozonetel-agent.controller.ts
index 5490545..495bc25 100644
--- a/src/ozonetel/ozonetel-agent.controller.ts
+++ b/src/ozonetel/ozonetel-agent.controller.ts
@@ -64,16 +64,7 @@ export class OzonetelAgentController {
this.logger.log(`Dial request: ${body.phoneNumber} (lead: ${body.leadId ?? 'none'})`);
try {
- // Ensure agent is logged in before dialing
- await this.ozonetelAgent.loginAgent({
- agentId: this.defaultAgentId,
- password: this.defaultAgentPassword,
- phoneNumber: this.defaultSipId,
- mode: 'blended',
- });
-
const result = await this.ozonetelAgent.dialCustomer({
- agentId: this.defaultAgentId,
customerNumber: body.phoneNumber,
});
return result;
diff --git a/src/ozonetel/ozonetel-agent.service.ts b/src/ozonetel/ozonetel-agent.service.ts
index 1459b47..5bfd5c3 100644
--- a/src/ozonetel/ozonetel-agent.service.ts
+++ b/src/ozonetel/ozonetel-agent.service.ts
@@ -64,40 +64,52 @@ export class OzonetelAgentService {
}
async dialCustomer(params: {
- agentId: string;
customerNumber: string;
- campaignName?: string;
- }): Promise<{ type: string; agentId: string; user: string }> {
- const url = `https://${this.apiDomain}/CAServicesV3/mdlConnection.php`;
- const basicAuth = Buffer.from(`${this.accountId}:${this.apiKey}`).toString('base64');
+ callbackUrl?: string;
+ ivrUrl?: string;
+ }): Promise<{ status: string; message: string }> {
+ const callerId = process.env.OZONETEL_DID ?? '918041763265';
+ const callbackBase = process.env.KOOKOO_CALLBACK_URL ?? 'https://engage-api.srv1477139.hstgr.cloud';
- this.logger.log(`Dialing ${params.customerNumber} for agent ${params.agentId}`);
+ this.logger.log(`Kookoo outbound: dialing ${params.customerNumber}`);
try {
- const response = await axios.post(
- url,
- {
- type: 'tbManualDial',
- ns: 'ozonetel.cloudagent',
- customer: this.accountId,
- agentId: params.agentId,
- custNumber: params.customerNumber,
- campaignName: params.campaignName ?? 'Inbound_918041763265',
- timestamp: Date.now(),
- },
- {
- headers: {
- 'Apikey': this.apiKey,
- 'Authorization': `Basic ${basicAuth}`,
- 'Content-Type': 'application/json',
- },
- },
+ const queryParams = new URLSearchParams({
+ phone_no: params.customerNumber,
+ api_key: this.apiKey,
+ outbound_version: '2',
+ caller_id: callerId,
+ callback_url: params.callbackUrl ?? `${callbackBase}/webhooks/kookoo/callback`,
+ });
+
+ // If IVR URL provided, Kookoo will hit it when call connects
+ // Otherwise use extra_data for simple TTS + hangup
+ if (params.ivrUrl) {
+ queryParams.set('url', params.ivrUrl);
+ } else {
+ queryParams.set('extra_data', 'Connecting you now');
+ }
+
+ const response = await axios.get(
+ `https://in1-cpaas.ozonetel.com/outbound/outbound.php?${queryParams.toString()}`,
);
- this.logger.log(`Dial response: ${JSON.stringify(response.data)}`);
- return response.data;
+ const responseText = typeof response.data === 'string' ? response.data : String(response.data);
+ this.logger.log(`Kookoo dial response: ${responseText}`);
+
+ // Parse XML response: queuedSID
+ const statusMatch = responseText.match(/(.*?)<\/status>/);
+ const messageMatch = responseText.match(/(.*?)<\/message>/);
+ const status = statusMatch?.[1] ?? 'unknown';
+ const message = messageMatch?.[1] ?? responseText;
+
+ if (status === 'error') {
+ throw new Error(message);
+ }
+
+ return { status, message };
} catch (error: any) {
- this.logger.error(`Dial failed: ${error.response?.data?.message ?? error.message}`);
+ this.logger.error(`Kookoo dial failed: ${error.message}`);
throw error;
}
}
diff --git a/src/worklist/kookoo-callback.controller.ts b/src/worklist/kookoo-callback.controller.ts
new file mode 100644
index 0000000..932e0c2
--- /dev/null
+++ b/src/worklist/kookoo-callback.controller.ts
@@ -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('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,
+ 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) {
+ this.logger.error(`Kookoo callback processing failed: ${err}`);
+ return { received: true, processed: false };
+ }
+ }
+}
diff --git a/src/worklist/worklist.module.ts b/src/worklist/worklist.module.ts
index 10eb99a..5b7731c 100644
--- a/src/worklist/worklist.module.ts
+++ b/src/worklist/worklist.module.ts
@@ -3,10 +3,11 @@ import { PlatformModule } from '../platform/platform.module';
import { WorklistController } from './worklist.controller';
import { WorklistService } from './worklist.service';
import { MissedCallWebhookController } from './missed-call-webhook.controller';
+import { KookooCallbackController } from './kookoo-callback.controller';
@Module({
imports: [PlatformModule],
- controllers: [WorklistController, MissedCallWebhookController],
+ controllers: [WorklistController, MissedCallWebhookController, KookooCallbackController],
providers: [WorklistService],
})
export class WorklistModule {}