/** * @file external-integrations.test.ts * @description Contract/mock tests for all external integrations. * These tests validate the SHAPE and BEHAVIOUR of each mock so that * when real APIs are wired, only the mock needs to be swapped. * * Integrations covered: * 1. SAP OData — Dealer Code generation (mockGenerateSapCodes, mockSyncDealerStatusToSap) * 2. Google Calendar — Interview invite scheduling (mockScheduleMeeting) * 3. WhatsApp — Notification delivery (mockSendWhatsApp) * 4. Gemini AI — Panel evaluation summary (mockGenerateAiSummary) * * SRS Coverage: * §6.17.3.1 — SAP OData API for Sales/Service/GMA/Gear codes * §6.9.2 — Google Calendar invites for all participants * §1.1.1 — WhatsApp as supported notification channel * §6.10.4 — AI-assisted recommendation via Gemini API */ import { ExternalMocksService } from '../common/utils/externalMocks.service.js'; // ─── 1. SAP Dealer Code Generation ─────────────────────────────────────────── describe('SAP Mock — Dealer Code Generation (SRS §6.17.3.1)', () => { it('TC-SAP-01: returns success=true', async () => { const result = await ExternalMocksService.mockGenerateSapCodes('app-001'); expect(result.success).toBe(true); }); it('TC-SAP-02: returns salesCode in SLS-XXXX format', async () => { const { data } = await ExternalMocksService.mockGenerateSapCodes('app-001'); expect(data.salesCode).toMatch(/^SLS-\d{4}$/); }); it('TC-SAP-03: returns serviceCode in SRV-XXXX format', async () => { const { data } = await ExternalMocksService.mockGenerateSapCodes('app-001'); expect(data.serviceCode).toMatch(/^SRV-\d{4}$/); }); it('TC-SAP-04: returns gmaCode in GMA-XXXX format (Genuine Motorcycle Accessories)', async () => { const { data } = await ExternalMocksService.mockGenerateSapCodes('app-001'); expect(data.gmaCode).toMatch(/^GMA-\d{4}$/); }); it('TC-SAP-05: returns gearCode in GER-XXXX format', async () => { const { data } = await ExternalMocksService.mockGenerateSapCodes('app-001'); expect(data.gearCode).toMatch(/^GER-\d{4}$/); }); it('TC-SAP-06: returns a non-empty sapMasterId', async () => { const { data } = await ExternalMocksService.mockGenerateSapCodes('app-001'); expect(data.sapMasterId).toBeTruthy(); expect(typeof data.sapMasterId).toBe('string'); }); it('TC-SAP-07: each call generates unique codes (no collisions)', async () => { const r1 = await ExternalMocksService.mockGenerateSapCodes('app-001'); const r2 = await ExternalMocksService.mockGenerateSapCodes('app-002'); // Codes are random; while collision is statistically possible, sapMasterIds must differ expect(r1.data.sapMasterId).not.toBe(r2.data.sapMasterId); }); it('TC-SAP-08: returns all 4 code types in a single call (no missing fields)', async () => { const { data } = await ExternalMocksService.mockGenerateSapCodes('app-001'); expect(data).toHaveProperty('salesCode'); expect(data).toHaveProperty('serviceCode'); expect(data).toHaveProperty('gmaCode'); expect(data).toHaveProperty('gearCode'); expect(data).toHaveProperty('sapMasterId'); }); }); // ─── SAP Status Sync ───────────────────────────────────────────────────────── describe('SAP Mock — Dealer Status Synchronization', () => { it('TC-SAP-09: mockSyncDealerStatusToSap returns success=true', async () => { const result = await ExternalMocksService.mockSyncDealerStatusToSap('SLS-1234', 'Active'); expect(result.success).toBe(true); }); it('TC-SAP-10: sync result includes a sapTransactionId and ISO timestamp', async () => { const result = await ExternalMocksService.mockSyncDealerStatusToSap('SLS-1234', 'Terminated'); expect(result.sapTransactionId).toMatch(/^SAP-TX-/); expect(new Date(result.timestamp).toISOString()).toBe(result.timestamp); }); }); // ─── SAP Financial Dues ─────────────────────────────────────────────────────── describe('SAP Mock — Financial Dues (F&F Context)', () => { it('TC-SAP-11: mockGetFinancialDuesFromSap returns financial data for a dealer', async () => { const result = await ExternalMocksService.mockGetFinancialDuesFromSap('SLS-5678'); expect(result.success).toBe(true); expect(result.data).toHaveProperty('outstandingInvoices'); expect(result.data).toHaveProperty('securityDeposit'); expect(result.data).toHaveProperty('creditLimit'); expect(result.data).toHaveProperty('pendingClaims'); }); it('TC-SAP-12: returned financial amounts are positive numbers', async () => { const { data } = await ExternalMocksService.mockGetFinancialDuesFromSap('SLS-5678'); expect(data.securityDeposit).toBeGreaterThan(0); expect(data.creditLimit).toBeGreaterThan(0); }); }); // ─── 2. Google Calendar Mock ────────────────────────────────────────────────── describe('Google Calendar Mock — Interview Scheduling (SRS §6.9.2)', () => { const interviewPayload = { type: 'Level 1 Interview', scheduledAt: '2026-05-15T10:00:00Z', participants: ['zm@re.com', 'rbm@re.com', 'applicant@gmail.com'], mode: 'Virtual', applicationId: 'app-uuid-001', }; it('TC-CAL-01: mockScheduleMeeting returns success=true', async () => { const result = await ExternalMocksService.mockScheduleMeeting(interviewPayload); expect(result.success).toBe(true); }); it('TC-CAL-02: returns a Google Meet link URL', async () => { const result = await ExternalMocksService.mockScheduleMeeting(interviewPayload); expect(result.meetLink).toContain('meet.google.com'); }); it('TC-CAL-03: returns a non-empty calendarEventId (UUID format)', async () => { const result = await ExternalMocksService.mockScheduleMeeting(interviewPayload); expect(result.calendarEventId).toMatch( /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i ); }); it('TC-CAL-04: each scheduling call returns a unique meetLink (no duplicate links)', async () => { const r1 = await ExternalMocksService.mockScheduleMeeting(interviewPayload); const r2 = await ExternalMocksService.mockScheduleMeeting({ ...interviewPayload, type: 'Level 2 Interview' }); expect(r1.meetLink).not.toBe(r2.meetLink); }); it('TC-CAL-05: mock works for Level 1, Level 2, and Level 3 interview types', async () => { for (const type of ['Level 1 Interview', 'Level 2 Interview', 'Level 3 Interview']) { const result = await ExternalMocksService.mockScheduleMeeting({ ...interviewPayload, type }); expect(result.success).toBe(true); } }); it('TC-CAL-06: mock works for both Virtual and Physical interview modes', async () => { const virtualResult = await ExternalMocksService.mockScheduleMeeting({ ...interviewPayload, mode: 'Virtual' }); const physicalResult = await ExternalMocksService.mockScheduleMeeting({ ...interviewPayload, mode: 'Physical' }); expect(virtualResult.success).toBe(true); expect(physicalResult.success).toBe(true); }); }); // ─── 3. WhatsApp Mock ───────────────────────────────────────────────────────── describe('WhatsApp Mock — Notification Delivery (SRS §1.1.1)', () => { it('TC-WA-01: mockSendWhatsApp resolves successfully', async () => { const result = await ExternalMocksService.mockSendWhatsApp( '+919876543210', 'Questionnaire reminder for your dealership application.' ); expect(result.success).toBe(true); }); it('TC-WA-02: returned messageId starts with WA- prefix', async () => { const result = await ExternalMocksService.mockSendWhatsApp( '+919876543210', 'Your resignation request has been received.' ); expect(result.messageId).toMatch(/^WA-/); }); it('TC-WA-03: LOI-related messages must NOT be sent via WhatsApp (SRS §1.1.2)', () => { // Contract test: LOI channel must not include WhatsApp const loiChannels = ['email', 'system']; // per SRS §1.1.2 expect(loiChannels).not.toContain('whatsapp'); }); it('TC-WA-04: questionnaire reminders MUST include WhatsApp channel (SRS §1.1.1)', () => { const questionnaireChannels = ['email', 'whatsapp']; // per SRS §1.1.1 expect(questionnaireChannels).toContain('whatsapp'); }); it('TC-WA-05: resignation submission acknowledgement includes WhatsApp channel (SRS §1.1.5)', () => { const resignationChannels = ['email', 'whatsapp']; expect(resignationChannels).toContain('whatsapp'); }); it('TC-WA-06: each WhatsApp call generates a unique messageId', async () => { const r1 = await ExternalMocksService.mockSendWhatsApp('+91111', 'Msg 1'); const r2 = await ExternalMocksService.mockSendWhatsApp('+91222', 'Msg 2'); expect(r1.messageId).not.toBe(r2.messageId); }); }); // ─── 4. Gemini AI Mock ──────────────────────────────────────────────────────── describe('Gemini AI Mock — Panel Evaluation Summary (SRS §6.10.4)', () => { const allApprovedFeedback = [ { recommendation: 'Approve', score: 85 }, { recommendation: 'Approve', score: 90 }, { recommendation: 'Approve', score: 88 }, ]; const mixedFeedback = [ { recommendation: 'Approve', score: 80 }, { recommendation: 'Approve', score: 75 }, { recommendation: 'Reject', score: 40 }, ]; const allRejectFeedback = [ { recommendation: 'Reject', score: 30 }, { recommendation: 'Reject', score: 25 }, ]; it('TC-AI-01: mockGenerateAiSummary returns success=true', async () => { const result = await ExternalMocksService.mockGenerateAiSummary('app-001', allApprovedFeedback); expect(result.success).toBe(true); }); it('TC-AI-02: returns a non-empty summary string', async () => { const result = await ExternalMocksService.mockGenerateAiSummary('app-001', allApprovedFeedback); expect(typeof result.summary).toBe('string'); expect(result.summary.length).toBeGreaterThan(10); }); it('TC-AI-03: unanimous approval panel produces positive consensus summary', async () => { const result = await ExternalMocksService.mockGenerateAiSummary('app-001', allApprovedFeedback); // Unanimous approval → strong consensus message expect(result.summary).toMatch(/strong consensus|strong candidate|exceptional/i); }); it('TC-AI-04: majority approval produces cautiously positive summary', async () => { const result = await ExternalMocksService.mockGenerateAiSummary('app-001', mixedFeedback); // Majority but not all → cautious message expect(result.summary).toMatch(/majority|recommend approval|monitored/i); }); it('TC-AI-05: rejection-leaning panel produces concern-focused summary', async () => { const result = await ExternalMocksService.mockGenerateAiSummary('app-001', allRejectFeedback); expect(result.summary).toMatch(/divided|rejection|concern/i); }); it('TC-AI-06: empty feedback list produces a valid (non-crashing) summary', async () => { const result = await ExternalMocksService.mockGenerateAiSummary('app-001', []); expect(result.success).toBe(true); expect(result.summary).toBeDefined(); }); it('TC-AI-07: summary is presentable to NBH — 2 to 3 sentences', async () => { const result = await ExternalMocksService.mockGenerateAiSummary('app-001', allApprovedFeedback); // SRS §6.10.4: "two- to three-line summarized recommendation" const sentenceCount = result.summary.split(/[.!?]/).filter(Boolean).length; expect(sentenceCount).toBeGreaterThanOrEqual(1); expect(sentenceCount).toBeLessThanOrEqual(5); }); it('TC-AI-08: mixed feedback with exactly half approvals falls into majority branch', async () => { const halfHalf = [ { recommendation: 'Approve', score: 75 }, { recommendation: 'Reject', score: 40 }, ]; // 1 approve out of 2 = not > total/2, so should fall into rejection branch const result = await ExternalMocksService.mockGenerateAiSummary('app-001', halfHalf); expect(result.success).toBe(true); // The summary should NOT be the "strong consensus" one expect(result.summary).not.toMatch(/strong consensus|exceptional/i); }); });