import { BadRequestException } from '@nestjs/common'; import { Freight } from '@edr/types'; import { GlOperationsService } from './gl-operations.service'; import { Booking } from '../bookings/entities/booking.entity'; /** * The GL Djibouti final invoice lands as a DRAFT: the customer must approve it * (which issues it) before a payment slip is accepted. */ describe('GlOperationsService — final invoice approval', () => { const invoice = (status: Freight.InvoiceStatus, issuedAt: Date | null = null) => ({ id: 'inv-1', invoiceNumber: 'INV-1', status, totalAmount: 1500, currency: 'ETB', issuedAt, paidAt: null, }); let billingService: { findInvoice: jest.Mock; updateStatus: jest.Mock }; let filesService: { findByResource: jest.Mock; upsertByCode: jest.Mock }; let notifier: { finalInvoiceApprovedToStaff: jest.Mock; dutySlipUploadedToStaff: jest.Mock }; let service: GlOperationsService; beforeEach(() => { billingService = { findInvoice: jest.fn(), updateStatus: jest.fn().mockResolvedValue(undefined), }; filesService = { findByResource: jest.fn().mockResolvedValue([]), upsertByCode: jest.fn().mockResolvedValue(undefined), }; notifier = { finalInvoiceApprovedToStaff: jest.fn(), dutySlipUploadedToStaff: jest.fn(), }; const dataSource = { getRepository: (entity: unknown) => entity === Booking ? { findOne: jest.fn().mockResolvedValue({ id: 'bk-1', reference: 'BKG-1' }) } : { findOne: jest.fn().mockResolvedValue({ description: 'Post-offload charges' }) }, }; service = new GlOperationsService( dataSource as never, filesService as never, {} as never, // milestoneService billingService as never, notifier as never, ); }); it('issues the draft on customer approval and reports approvedAt', async () => { const issued = new Date('2026-07-28T09:00:00.000Z'); billingService.findInvoice .mockResolvedValueOnce(invoice(Freight.InvoiceStatus.Draft)) .mockResolvedValueOnce(invoice(Freight.InvoiceStatus.Issued, issued)); const summary = await service.approveFinalInvoice('bk-1', 'user-1'); expect(billingService.updateStatus).toHaveBeenCalledWith( 'inv-1', Freight.InvoiceStatus.Issued, ); expect(notifier.finalInvoiceApprovedToStaff).toHaveBeenCalled(); expect(summary.approvedAt).toBe(issued.toISOString()); }); it('is a no-op when the invoice was already approved', async () => { billingService.findInvoice.mockResolvedValue( invoice(Freight.InvoiceStatus.Issued, new Date()), ); await service.approveFinalInvoice('bk-1'); expect(billingService.updateStatus).not.toHaveBeenCalled(); }); it('refuses a payment slip while the invoice is still a draft', async () => { billingService.findInvoice.mockResolvedValue(invoice(Freight.InvoiceStatus.Draft)); await expect( service.uploadFinalInvoiceSlip('bk-1', { originalname: 'slip.pdf' } as never), ).rejects.toThrow(BadRequestException); expect(filesService.upsertByCode).not.toHaveBeenCalled(); }); it('accepts the payment slip once approved', async () => { billingService.findInvoice.mockResolvedValue( invoice(Freight.InvoiceStatus.Issued, new Date()), ); await service.uploadFinalInvoiceSlip('bk-1', { originalname: 'slip.pdf' } as never); expect(filesService.upsertByCode).toHaveBeenCalledWith( expect.objectContaining({ code: 'final_invoice_slip' }), ); }); });