mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-04-11 10:07:22 +00:00
fix: skip outbound calls in webhook + filter abandon polls by campaign
Webhook controller now skips outbound calls (type=Manual/OutBound). An unanswered outbound dial is NOT a missed inbound call — it was being incorrectly created as MISSED with PENDING_CALLBACK status. MissedQueueService now filters the Ozonetel abandonCalls API response by campaign name (read from TelephonyConfigService). Prevents cross-tenant ingestion when multiple sidecars share the same Ozonetel account. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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}` : '';
|
||||
|
||||
@@ -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<number>('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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
Reference in New Issue
Block a user