import { Logger } from '@nestjs/common'; import { CsvBookingService } from './csv-booking.service'; import { CsvBookingStatus } from '@domain/entities/csv-booking.entity'; /** * Validation d'un virement par un administrateur : le booking passe en PENDING * et la demande part chez le transporteur. Un echec de cet email etait avale * sans que l'administrateur le sache. */ function pendingTransferBooking() { const booking = { id: 'b-1', bookingNumber: 'XPD-2026-AAAAAA', userId: 'u-1', organizationId: 'org-1', carrierName: 'SSC Consolidation', carrierEmail: 'booking@ssc.test', origin: { getValue: () => 'FRLEH' }, destination: { getValue: () => 'EGEDK' }, volumeCBM: 2.4, weightKG: 850, palletCount: 2, priceUSD: 200, priceEUR: 180, primaryCurrency: 'EUR', transitDays: 11, containerType: 'LCL', status: CsvBookingStatus.PENDING_BANK_TRANSFER as CsvBookingStatus, documents: [], confirmationToken: 'token', requestedAt: new Date('2026-09-01T10:00:00Z'), notes: undefined, getRouteDescription: () => 'FRLEH → EGEDK', isExpired: () => false, getPriceInCurrency: () => 180, options: {}, markBankTransferValidated() { booking.status = CsvBookingStatus.PENDING; }, }; return booking; } function buildService(options: { emailFails: boolean }) { const booking = pendingTransferBooking(); const csvBookingRepository = { findById: jest.fn(async () => booking), update: jest.fn(async (b: unknown) => b), repository: { findOne: jest.fn(async () => ({ bookingNumber: booking.bookingNumber, passwordHash: null })), save: jest.fn(async (b: unknown) => b), }, }; const emailAdapter = { sendCsvBookingRequest: jest.fn(async () => { if (options.emailFails) throw new Error('550 sender not valid'); }), }; const notificationRepository = { save: jest.fn(async () => undefined) }; const unused = {} as never; const service = new CsvBookingService( csvBookingRepository as never, notificationRepository as never, emailAdapter as never, unused, unused, unused, unused ); return { service, booking, emailAdapter }; } describe('CsvBookingService.validateBankTransfer', () => { beforeEach(() => { jest.spyOn(Logger.prototype, 'log').mockImplementation(() => undefined); jest.spyOn(Logger.prototype, 'error').mockImplementation(() => undefined); }); afterEach(() => jest.restoreAllMocks()); it('active le booking et envoie la demande au transporteur', async () => { const { service, booking, emailAdapter } = buildService({ emailFails: false }); const result = await service.validateBankTransfer('b-1'); expect(booking.status).toBe(CsvBookingStatus.PENDING); expect(emailAdapter.sendCsvBookingRequest).toHaveBeenCalledWith( 'booking@ssc.test', expect.objectContaining({ bookingNumber: 'XPD-2026-AAAAAA', confirmationToken: 'token' }) ); expect(result.carrierEmailSent).toBe(true); }, 20000); it("signale l'echec de l'email au lieu de l'avaler, sans annuler la validation", async () => { const { service, booking } = buildService({ emailFails: true }); const result = await service.validateBankTransfer('b-1'); expect(booking.status).toBe(CsvBookingStatus.PENDING); expect(result.carrierEmailSent).toBe(false); }, 20000); });