diff --git a/src/worklist/missed-call-webhook.controller.ts b/src/worklist/missed-call-webhook.controller.ts index cf922bd..15fdf77 100644 --- a/src/worklist/missed-call-webhook.controller.ts +++ b/src/worklist/missed-call-webhook.controller.ts @@ -53,9 +53,17 @@ export class MissedCallWebhookController { return { received: true, processed: false }; } + // Skip outbound calls — an unanswered outbound dial is NOT a + // "missed call" in the call-center sense. Outbound call records + // are created by the disposition flow, not the webhook. + if (type === 'Manual' || type === 'OutBound') { + this.logger.log(`Skipping outbound call webhook (type=${type}, status=${status})`); + return { received: true, processed: false, reason: 'outbound' }; + } + // Determine call status for our platform const callStatus = status === 'Answered' ? 'COMPLETED' : 'MISSED'; - const direction = type === 'InBound' ? 'INBOUND' : 'OUTBOUND'; + const direction = 'INBOUND'; // only inbound reaches here now // Use API key auth for server-to-server writes const authHeader = this.apiKey ? `Bearer ${this.apiKey}` : ''; diff --git a/src/worklist/missed-queue.service.ts b/src/worklist/missed-queue.service.ts index e755344..e133b7c 100644 --- a/src/worklist/missed-queue.service.ts +++ b/src/worklist/missed-queue.service.ts @@ -2,6 +2,7 @@ import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { PlatformGraphqlService } from '../platform/platform-graphql.service'; import { OzonetelAgentService } from '../ozonetel/ozonetel-agent.service'; +import { TelephonyConfigService } from '../config/telephony-config.service'; // Ozonetel sends all timestamps in IST — convert to UTC for storage export function istToUtc(istDateStr: string | null): string | null { @@ -33,10 +34,16 @@ export class MissedQueueService implements OnModuleInit { private readonly config: ConfigService, private readonly platform: PlatformGraphqlService, private readonly ozonetel: OzonetelAgentService, + private readonly telephony: TelephonyConfigService, ) { this.pollIntervalMs = this.config.get('missedQueue.pollIntervalMs', 30000); } + // Read-through so admin config changes take effect without restart + private get ownCampaign(): string { + return this.telephony.getConfig().ozonetel.campaignName ?? ''; + } + onModuleInit() { this.logger.log(`Starting missed call ingestion polling every ${this.pollIntervalMs}ms`); setInterval(() => this.ingest().catch(err => this.logger.error('Ingestion failed', err)), this.pollIntervalMs); @@ -61,7 +68,17 @@ export class MissedQueueService implements OnModuleInit { if (!abandonCalls?.length) return { created: 0, updated: 0 }; - for (const call of abandonCalls) { + // Filter to this sidecar's campaign only — the Ozonetel API + // returns ALL abandoned calls across the account. + const filtered = this.ownCampaign + ? abandonCalls.filter((c: any) => c.campaign === this.ownCampaign) + : abandonCalls; + + if (filtered.length < abandonCalls.length) { + this.logger.log(`Filtered ${abandonCalls.length - filtered.length} calls from other campaigns (own=${this.ownCampaign})`); + } + + for (const call of filtered) { const ucid = call.monitorUCID; if (!ucid || this.processedUcids.has(ucid)) continue; this.processedUcids.add(ucid); diff --git a/src/worklist/missed-queue.spec.ts b/src/worklist/missed-queue.spec.ts index 6e2565d..716161c 100644 --- a/src/worklist/missed-queue.spec.ts +++ b/src/worklist/missed-queue.spec.ts @@ -15,6 +15,7 @@ import { ConfigService } from '@nestjs/config'; import { MissedQueueService, istToUtc, normalizePhone } from './missed-queue.service'; import { PlatformGraphqlService } from '../platform/platform-graphql.service'; import { OzonetelAgentService } from '../ozonetel/ozonetel-agent.service'; +import { TelephonyConfigService } from '../config/telephony-config.service'; import { ABANDON_CALL_RECORD } from '../__fixtures__/ozonetel-payloads'; describe('MissedQueueService', () => { @@ -57,6 +58,16 @@ describe('MissedQueueService', () => { getAbandonCalls: jest.fn().mockResolvedValue([ABANDON_CALL_RECORD]), }, }, + { + provide: TelephonyConfigService, + useValue: { + getConfig: () => ({ + ozonetel: { campaignName: 'Inbound_918041763400', agentId: '', agentPassword: '', did: '918041763400', sipId: '' }, + sip: { domain: 'test', wsPort: '444' }, + exotel: { apiKey: '', accountSid: '', subdomain: '' }, + }), + }, + }, ], }).compile(); diff --git a/src/worklist/worklist.module.ts b/src/worklist/worklist.module.ts index 0b492c1..3870089 100644 --- a/src/worklist/worklist.module.ts +++ b/src/worklist/worklist.module.ts @@ -3,6 +3,7 @@ import { PlatformModule } from '../platform/platform.module'; import { OzonetelAgentModule } from '../ozonetel/ozonetel-agent.module'; import { AuthModule } from '../auth/auth.module'; import { RulesEngineModule } from '../rules-engine/rules-engine.module'; +import { TelephonyConfigService } from '../config/telephony-config.service'; import { WorklistController } from './worklist.controller'; import { WorklistService } from './worklist.service'; import { MissedQueueService } from './missed-queue.service'; @@ -12,7 +13,7 @@ import { KookooCallbackController } from './kookoo-callback.controller'; @Module({ imports: [PlatformModule, forwardRef(() => OzonetelAgentModule), forwardRef(() => AuthModule), RulesEngineModule], controllers: [WorklistController, MissedCallWebhookController, KookooCallbackController], - providers: [WorklistService, MissedQueueService], + providers: [WorklistService, MissedQueueService, TelephonyConfigService], exports: [MissedQueueService], }) export class WorklistModule {}