/** * 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; let ozonetel: jest.Mocked; 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); }); }); });