mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-04-11 18:08:16 +00:00
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>
179 lines
7.0 KiB
TypeScript
179 lines
7.0 KiB
TypeScript
/**
|
|
* Missed Queue Service — unit tests
|
|
*
|
|
* QA coverage: TC-MC-03 (missed calls appear in pending section)
|
|
*
|
|
* Tests the abandon call polling + ingestion logic:
|
|
* - Fetches from Ozonetel getAbandonCalls
|
|
* - Deduplicates by UCID
|
|
* - Creates Call records with callbackstatus=PENDING_CALLBACK
|
|
* - Normalizes phone numbers
|
|
* - Converts IST→UTC timestamps
|
|
*/
|
|
import { Test } from '@nestjs/testing';
|
|
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', () => {
|
|
let service: MissedQueueService;
|
|
let platform: jest.Mocked<PlatformGraphqlService>;
|
|
let ozonetel: jest.Mocked<OzonetelAgentService>;
|
|
|
|
beforeEach(async () => {
|
|
const module = await Test.createTestingModule({
|
|
providers: [
|
|
MissedQueueService,
|
|
{
|
|
provide: ConfigService,
|
|
useValue: {
|
|
get: jest.fn((key: string, defaultVal?: any) => {
|
|
if (key === 'missedQueue.pollIntervalMs') return 999999; // don't actually poll
|
|
if (key === 'platform.apiKey') return 'test-key';
|
|
return defaultVal;
|
|
}),
|
|
},
|
|
},
|
|
{
|
|
provide: PlatformGraphqlService,
|
|
useValue: {
|
|
query: jest.fn().mockResolvedValue({}),
|
|
queryWithAuth: jest.fn().mockImplementation((query: string) => {
|
|
if (query.includes('createCall')) {
|
|
return Promise.resolve({ createCall: { id: 'call-missed-001' } });
|
|
}
|
|
if (query.includes('calls')) {
|
|
return Promise.resolve({ calls: { edges: [] } }); // no existing calls
|
|
}
|
|
return Promise.resolve({});
|
|
}),
|
|
},
|
|
},
|
|
{
|
|
provide: OzonetelAgentService,
|
|
useValue: {
|
|
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();
|
|
|
|
service = module.get(MissedQueueService);
|
|
platform = module.get(PlatformGraphqlService);
|
|
ozonetel = module.get(OzonetelAgentService);
|
|
});
|
|
|
|
// ── Utility functions ────────────────────────────────────────
|
|
|
|
describe('istToUtc', () => {
|
|
it('should subtract 5:30 from a valid IST timestamp', () => {
|
|
const result = istToUtc('2026-04-09 14:30:00');
|
|
expect(result).toBeDefined();
|
|
const d = new Date(result!);
|
|
expect(d.toISOString()).toBeTruthy();
|
|
});
|
|
|
|
it('should return null for null input', () => {
|
|
expect(istToUtc(null)).toBeNull();
|
|
});
|
|
|
|
it('should return null for invalid date string', () => {
|
|
expect(istToUtc('not-a-date')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('normalizePhone', () => {
|
|
it('should strip +91 and format to +91XXXXXXXXXX', () => {
|
|
expect(normalizePhone('+919949879837')).toBe('+919949879837');
|
|
});
|
|
|
|
it('should strip 0091 prefix', () => {
|
|
expect(normalizePhone('00919949879837')).toBe('+919949879837');
|
|
});
|
|
|
|
it('should strip leading 0', () => {
|
|
expect(normalizePhone('09949879837')).toBe('+919949879837');
|
|
});
|
|
|
|
it('should handle raw 10-digit number', () => {
|
|
expect(normalizePhone('9949879837')).toBe('+919949879837');
|
|
});
|
|
|
|
it('should strip non-digits', () => {
|
|
expect(normalizePhone('+91-994-987-9837')).toBe('+919949879837');
|
|
});
|
|
});
|
|
|
|
// ── Ingestion ────────────────────────────────────────────────
|
|
|
|
describe('ingest', () => {
|
|
it('TC-MC-03: should create MISSED call with PENDING_CALLBACK status', async () => {
|
|
const result = await service.ingest();
|
|
|
|
expect(result.created).toBeGreaterThanOrEqual(0);
|
|
|
|
// Verify createCall was called
|
|
const createCalls = platform.queryWithAuth.mock.calls.filter(
|
|
c => typeof c[0] === 'string' && c[0].includes('createCall'),
|
|
);
|
|
|
|
if (createCalls.length > 0) {
|
|
const data = createCalls[0][1]?.data;
|
|
expect(data).toMatchObject(
|
|
expect.objectContaining({
|
|
callStatus: 'MISSED',
|
|
direction: 'INBOUND',
|
|
callbackstatus: 'PENDING_CALLBACK',
|
|
}),
|
|
);
|
|
}
|
|
});
|
|
|
|
it('should deduplicate by UCID on second ingest', async () => {
|
|
await service.ingest();
|
|
const firstCallCount = platform.queryWithAuth.mock.calls.filter(
|
|
c => typeof c[0] === 'string' && c[0].includes('createCall'),
|
|
).length;
|
|
|
|
// Same UCID on second ingest
|
|
await service.ingest();
|
|
const secondCallCount = platform.queryWithAuth.mock.calls.filter(
|
|
c => typeof c[0] === 'string' && c[0].includes('createCall'),
|
|
).length;
|
|
|
|
// Should not create duplicate — count stays the same
|
|
expect(secondCallCount).toBe(firstCallCount);
|
|
});
|
|
|
|
it('should handle empty abandon list gracefully', async () => {
|
|
ozonetel.getAbandonCalls.mockResolvedValueOnce([]);
|
|
|
|
const result = await service.ingest();
|
|
|
|
expect(result.created).toBe(0);
|
|
expect(result.updated).toBe(0);
|
|
});
|
|
|
|
it('should handle Ozonetel API failure gracefully', async () => {
|
|
ozonetel.getAbandonCalls.mockRejectedValueOnce(new Error('API timeout'));
|
|
|
|
const result = await service.ingest();
|
|
|
|
expect(result.created).toBe(0);
|
|
});
|
|
});
|
|
});
|