// Mocks DB + pont AXA AVANT le require du service (jest hoiste les jest.mock). const mockRecords = { getList: jest.fn(), getOne: jest.fn(), create: jest.fn(), update: jest.fn() }; const mockRunQt550 = jest.fn().mockResolvedValue({ ok: true }); jest.mock('../../db/db-connect', () => ({ db: { baseUrl: 'http://localhost:8090/', records: mockRecords } })); jest.mock('../axaBridgeService', () => ({ runQt550: (...args) => mockRunQt550(...args), lookupContract: jest.fn(), getHealth: jest.fn() })); const path = require('path'); const PizZip = require('pizzip'); const advaloService = require('../advaloService'); const { buildCommonDocContext, normalizeLookupInfo, normalizeMergedFallbackRow, renderAdvaloDocx, advaloCache } = advaloService.__private; function docxText(buffer) { const xml = new PizZip(buffer).file('word/document.xml').asText(); return (xml.match(/]*>(.*?)<\/w:t>/g) || []).map((t) => t.replace(/<[^>]+>/g, '')).join(' '); } const actor = { userMatricule: 'A123BC', userAuthGroupe: 'MANAGER', userFirstName: 'Jean', userLastName: 'Dupont' }; describe('Advalo — parité v1 du calcul documentaire', () => { test('buildCommonDocContext injecte les prix réellement saisis (pas les valeurs figées)', () => { const row = { numContrat: '0000022126873304', capital: '100000', tarif: '336,00', dateDebut: '01/04/2026', dateFin: '02/04/2026', mode: 'Terrestre', marchandise: 'Moteurs' }; const contractInfo = { nomClient: 'DURAND & FILS', adresseAgent: '1 rue de Paris', postalAgent: '75001 PARIS', telAgent: '0102030405', faxAgent: '0102030406', numAgent: '0009', nomAgent: 'AGENCE' }; const pricing = { valeurAssuree: '100000', taux: '0.5', primeMinimum: '20', cotisationHT: '500', coutActe: '36', cotisationTTC: '536' }; const ctx = buildCommonDocContext(row, contractInfo, actor, pricing); expect(ctx.tauxField).toBe('0,50'); expect(ctx.primeMinimum).toBe('20,00'); expect(ctx.cotisationField).toBe('500,00'); expect(ctx.coutActeField).toBe('36,00'); expect(ctx.cotisationTTC).toBe('536,00'); // Coordonnées agent propagées dans l'en-tête (parité v1 Avenant). expect(ctx.adresseAgent).toBe('1 rue de Paris'); expect(ctx.telAgent).toBe('0102030405'); expect(ctx.faxAgent).toBe('0102030406'); // Valeurs brutes (pas de double-échappement XML) — docxtemplater échappera. expect(ctx.nomClient).toBe('DURAND & FILS'); }); test('rendu réel Avenant_Ponctuel.docx (docxtemplater) contient les prix saisis + phrase TTC', () => { const row = { numContrat: '0000022126873304', capital: '100000', tarif: '536,00', dateDebut: '01/04/2026', dateFin: '02/04/2026', mode: 'Terrestre, Aérien', marchandise: 'Moteurs', depart: 'Paris', arrivee: 'Lyon' }; const contractInfo = { nomClient: 'DURAND & FILS', adresseAgent: '1 rue de Paris' }; const pricing = { valeurAssuree: '100000', taux: '0.5', primeMinimum: '20', cotisationHT: '500', coutActe: '36', cotisationTTC: '536' }; const ctx = buildCommonDocContext(row, contractInfo, actor, pricing); const tplPath = path.resolve(__dirname, '..', '..', 'templates', 'advalo', 'Avenant_Ponctuel.docx'); const text = docxText(renderAdvaloDocx(tplPath, ctx)); expect(text).toContain('500,00'); // cotisation HT expect(text).toContain('536,00'); // TTC expect(text).toContain('Il est perçu'); expect(text).toContain('Moteurs'); expect(text).toContain('DURAND & FILS'); // échappé une seule fois }); test('buildCommonDocContext: défauts v1 (0,30 / 15 / 36) seulement à défaut de pricing, TTC = HT + acte', () => { const row = { numContrat: '0000022126873304', capital: '3000', tarif: '15,00' }; const ctx = buildCommonDocContext(row, {}, actor, {}); expect(ctx.tauxField).toBe('0,30'); expect(ctx.primeMinimum).toBe('15,00'); expect(ctx.cotisationField).toBe('15,00'); // HT = tarif (grille déléguée) expect(ctx.coutActeField).toBe('36,00'); expect(ctx.cotisationTTC).toBe('51,00'); // 15 + 36 }); }); describe('Advalo — lookup contrat (coordonnées intermédiaire)', () => { test('normalizeLookupInfo conserve adresseAgent/postalAgent/telAgent/faxAgent', () => { const raw = { numContrat: '0000022126873304', numClient: '0858406820', nomClient: 'CLIENT', nomAgent: 'AGENCE', numAgent: '0009876543', adresseAgent: '1 rue de Paris', postalAgent: '75001 PARIS', telAgent: '0102030405', faxAgent: '0102030406' }; const info = normalizeLookupInfo(raw, '0000022126873304'); expect(info.adresseAgent).toBe('1 rue de Paris'); expect(info.postalAgent).toBe('75001 PARIS'); expect(info.telAgent).toBe('0102030405'); expect(info.faxAgent).toBe('0102030406'); }); }); describe('Advalo — enrichissement Cumul/Reporting via référentiel contrat', () => { test('normalizeMergedFallbackRow enrichit region/dpt/souscripteur depuis le ref (parité v1 getVarByNumContrat)', () => { advaloCache.refContratById.set('0000022126873304', { nomClient: 'KANGOUROUBOX', region: 'ILE DE FRANCE', dpt: '75', souscripteur: 'Z999ZZ' }); const delegueeRow = normalizeMergedFallbackRow( { id: 'g1', numContrat: '0000022126873304', tarif: '15 €', statutFacturation: 'Non facturé' }, 'deleguee' ); expect(delegueeRow.region).toBe('ILE DE FRANCE'); expect(delegueeRow.dpt).toBe('75'); expect(delegueeRow.souscripteur).toBe('Z999ZZ'); expect(delegueeRow.nomClient).toBe('KANGOUROUBOX'); advaloCache.refContratById.delete('0000022126873304'); }); }); describe('Advalo — modification demande hors-grille (parité v1 "Modification")', () => { beforeEach(() => { Object.values(mockRecords).forEach((fn) => fn.mockReset()); advaloCache.loaded = false; }); test('updateDemande met à jour les champs et journalise le pricing (event update)', async () => { mockRecords.getOne.mockResolvedValue({ id: 'dem9', numContrat: '0000022126873304', numClient: '0858406820', statutFacturation: 'Non facturé', isDeleted: false }); mockRecords.update.mockImplementation(async (collection, id, patch) => ({ id, ...patch })); mockRecords.create.mockResolvedValue({ id: 'aud9' }); const updated = await advaloService.updateDemande('d:dem9', { marchandise: 'Pièces auto', mode: 'Terrestre, Maritime', depart: 'Lyon', arrivee: 'Gênes', dateDebut: '05/04/2026', dateFin: '06/04/2026', capital: '50000', taux: '0.4', primeMinimum: '15', coutActe: '36', cotisationHT: '200', cotisationTTC: '236' }, actor); expect(updated.marchandise).toBe('Pièces auto'); expect(updated.mode).toBe('Terrestre, Maritime'); expect(updated.tarif).toBe('236'); const audit = mockRecords.create.mock.calls.find((c) => c[0] === 'advalo_audit'); expect(audit[1].eventType).toBe('update'); expect(audit[1].data.pricing.cotisationHT).toBe('200'); }); test('updateDemande refuse une demande déjà facturée', async () => { mockRecords.getOne.mockResolvedValue({ id: 'dem8', numContrat: '0000022126873304', statutFacturation: 'Facturé 01/04/2026', isDeleted: false }); await expect(advaloService.updateDemande('d:dem8', { marchandise: 'x', mode: 'Terrestre', depart: 'a', arrivee: 'b', dateDebut: '01/04/2026', dateFin: '02/04/2026', capital: '1', taux: '0.3', primeMinimum: '15', cotisationHT: '15', cotisationTTC: '51' }, actor)) .rejects.toThrow(/facturée ne peut pas être modifiée/); }); }); describe('Advalo — facturation QT550 (parité v1: HT à Pos=329, coût d acte une seule fois)', () => { const realPlatform = process.platform; beforeEach(() => { Object.values(mockRecords).forEach((fn) => fn.mockReset()); mockRunQt550.mockClear().mockResolvedValue({ ok: true }); // Cache vide forcé à chaque test pour isoler. advaloCache.loaded = false; advaloCache.loadingPromise = null; Object.defineProperty(process, 'platform', { value: 'win32', configurable: true }); }); afterEach(() => { Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true }); }); test('facturerBatch envoie la cotisation HT (audit) et le coût d acte 36 une seule fois — pas de double comptage', async () => { const demande = { id: 'dem1', numContrat: '0000022126873304', numClient: '0858406820', nomClient: 'CLIENT', capital: '100000', tarif: '336,00', // TTC stocké (HT 300 + 36) dateDebut: '01/04/2026', dateFin: '02/04/2026', statutFacturation: 'Non facturé', isDeleted: false }; const auditRec = { eventType: 'create', created: '2026-04-01T10:00:00.000Z', data: { demandeId: 'dem1', pricing: { cotisationHT: '300,00', coutActe: '36,00', cotisationTTC: '336,00' } } }; mockRecords.getOne.mockImplementation(async (collection, id) => { if (collection === 'advalo_demande' && id === 'dem1') return { ...demande }; throw new Error(`unexpected getOne ${collection} ${id}`); }); mockRecords.getList.mockImplementation(async (collection) => { if (collection === 'advalo_audit') return { items: [auditRec], totalItems: 1, totalPages: 1 }; if (collection === 'advalo_facturation_batch') return { items: [], totalItems: 0, totalPages: 1 }; return { items: [], totalItems: 0, totalPages: 1 }; }); mockRecords.create.mockImplementation(async (collection, payload) => ({ id: `${collection}-1`, ...payload })); mockRecords.update.mockImplementation(async (collection, id, patch) => ({ id, ...patch })); const result = await advaloService.facturerBatch( { demandeIds: ['d:dem1'], sourceMode: 'hors_grille', includeTransportDetails: true }, actor ); expect(result.idempotent).toBe(false); // QT550 reçoit le HT (300), PAS le TTC (336), et coutActe '36' une seule fois. expect(mockRunQt550).toHaveBeenCalledTimes(1); const qtArgs = mockRunQt550.mock.calls[0][0]; expect(qtArgs.totalCotisation).toBe(300); expect(qtArgs.coutActe).toBe('36'); // Le lot persiste le total HT + l acte séparé. const batchCreate = mockRecords.create.mock.calls.find((c) => c[0] === 'advalo_facturation_batch'); expect(batchCreate).toBeDefined(); expect(batchCreate[1].totalCotisation).toBe(300); expect(batchCreate[1].totalActe).toBe(advaloService.__private.COUT_ACTE); }); });