/** * Caller Resolution Service — unit tests * * QA coverage: TC-IB-05 (lead creation from enquiry), * TC-IB-06 (new patient registration), TC-IB-07/08 (AI context) * * Tests the phone→lead+patient resolution logic: * - Existing patient + existing lead → returns both, links if needed * - Existing lead, no patient → creates patient, links * - Existing patient, no lead → creates lead, links * - New caller (neither exists) → creates both * - Phone normalization (strips +91, non-digits) * - Cache hit/miss behavior */ import { Test } from '@nestjs/testing'; import { CallerResolutionService, ResolvedCaller } from './caller-resolution.service'; import { PlatformGraphqlService } from '../platform/platform-graphql.service'; import { SessionService } from '../auth/session.service'; describe('CallerResolutionService', () => { let service: CallerResolutionService; let platform: jest.Mocked; let cache: jest.Mocked; const AUTH = 'Bearer test-token'; const existingLead = { id: 'lead-001', contactName: { firstName: 'Priya', lastName: 'Sharma' }, contactPhone: { primaryPhoneNumber: '+919949879837' }, patientId: 'patient-001', }; const existingPatient = { id: 'patient-001', fullName: { firstName: 'Priya', lastName: 'Sharma' }, phones: { primaryPhoneNumber: '+919949879837' }, }; beforeEach(async () => { const module = await Test.createTestingModule({ providers: [ CallerResolutionService, { provide: PlatformGraphqlService, useValue: { queryWithAuth: jest.fn(), }, }, { provide: SessionService, useValue: { getCache: jest.fn().mockResolvedValue(null), // no cache by default setCache: jest.fn().mockResolvedValue(undefined), }, }, ], }).compile(); service = module.get(CallerResolutionService); platform = module.get(PlatformGraphqlService); cache = module.get(SessionService); }); // ── TC-IB-05: Existing lead + existing patient ─────────────── it('TC-IB-05: should return existing lead+patient when both found by phone', async () => { platform.queryWithAuth // leads query .mockResolvedValueOnce({ leads: { edges: [{ node: existingLead }] } }) // patients query .mockResolvedValueOnce({ patients: { edges: [{ node: existingPatient }] } }); const result = await service.resolve('9949879837', AUTH); expect(result.leadId).toBe('lead-001'); expect(result.patientId).toBe('patient-001'); expect(result.isNew).toBe(false); expect(result.firstName).toBe('Priya'); }); // ── TC-IB-06: New caller → creates both lead + patient ────── it('TC-IB-06: should create both lead+patient for unknown caller', async () => { platform.queryWithAuth // leads query → empty .mockResolvedValueOnce({ leads: { edges: [] } }) // patients query → empty .mockResolvedValueOnce({ patients: { edges: [] } }) // createPatient .mockResolvedValueOnce({ createPatient: { id: 'new-patient-001' } }) // createLead .mockResolvedValueOnce({ createLead: { id: 'new-lead-001' } }); const result = await service.resolve('6309248884', AUTH); expect(result.leadId).toBe('new-lead-001'); expect(result.patientId).toBe('new-patient-001'); expect(result.isNew).toBe(true); }); // ── Lead exists, no patient → creates patient ──────────────── it('should create patient when lead exists without patient match', async () => { const leadNoPatient = { ...existingLead, patientId: null }; platform.queryWithAuth .mockResolvedValueOnce({ leads: { edges: [{ node: leadNoPatient }] } }) .mockResolvedValueOnce({ patients: { edges: [] } }) // createPatient .mockResolvedValueOnce({ createPatient: { id: 'new-patient-002' } }) // linkLeadToPatient (updateLead) .mockResolvedValueOnce({ updateLead: { id: 'lead-001' } }); const result = await service.resolve('9949879837', AUTH); expect(result.patientId).toBe('new-patient-002'); expect(result.leadId).toBe('lead-001'); expect(result.isNew).toBe(false); }); // ── Patient exists, no lead → creates lead ─────────────────── it('should create lead when patient exists without lead match', async () => { platform.queryWithAuth .mockResolvedValueOnce({ leads: { edges: [] } }) .mockResolvedValueOnce({ patients: { edges: [{ node: existingPatient }] } }) // createLead .mockResolvedValueOnce({ createLead: { id: 'new-lead-002' } }); const result = await service.resolve('9949879837', AUTH); expect(result.leadId).toBe('new-lead-002'); expect(result.patientId).toBe('patient-001'); expect(result.isNew).toBe(false); }); // ── Phone normalization ────────────────────────────────────── it('should normalize phone: strip +91 prefix and non-digits', async () => { platform.queryWithAuth .mockResolvedValueOnce({ leads: { edges: [] } }) .mockResolvedValueOnce({ patients: { edges: [] } }) .mockResolvedValueOnce({ createPatient: { id: 'p' } }) .mockResolvedValueOnce({ createLead: { id: 'l' } }); const result = await service.resolve('+91-994-987-9837', AUTH); expect(result.phone).toBe('9949879837'); }); it('should reject invalid short phone numbers', async () => { await expect(service.resolve('12345', AUTH)).rejects.toThrow('Invalid phone'); }); // ── Cache hit ──────────────────────────────────────────────── it('should return cached result without hitting platform', async () => { const cached: ResolvedCaller = { leadId: 'cached-lead', patientId: 'cached-patient', firstName: 'Cache', lastName: 'Hit', phone: '9949879837', isNew: false, }; cache.getCache.mockResolvedValueOnce(JSON.stringify(cached)); const result = await service.resolve('9949879837', AUTH); expect(result).toEqual(cached); expect(platform.queryWithAuth).not.toHaveBeenCalled(); }); // ── Cache write ────────────────────────────────────────────── it('should cache result after successful resolve', async () => { platform.queryWithAuth .mockResolvedValueOnce({ leads: { edges: [{ node: existingLead }] } }) .mockResolvedValueOnce({ patients: { edges: [{ node: existingPatient }] } }); await service.resolve('9949879837', AUTH); expect(cache.setCache).toHaveBeenCalledWith( 'caller:9949879837', expect.any(String), 3600, ); }); // ── Links unlinked lead to patient ─────────────────────────── it('should link lead to patient when both exist but are unlinked', async () => { const unlinkedLead = { ...existingLead, patientId: null }; platform.queryWithAuth .mockResolvedValueOnce({ leads: { edges: [{ node: unlinkedLead }] } }) .mockResolvedValueOnce({ patients: { edges: [{ node: existingPatient }] } }) // updateLead to link .mockResolvedValueOnce({ updateLead: { id: 'lead-001' } }); const result = await service.resolve('9949879837', AUTH); expect(result.leadId).toBe('lead-001'); expect(result.patientId).toBe('patient-001'); // Verify the link mutation was called const linkCall = platform.queryWithAuth.mock.calls.find( c => typeof c[0] === 'string' && c[0].includes('updateLead'), ); expect(linkCall).toBeDefined(); }); });