mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-01 10:03:26 +00:00
103 lines
3.5 KiB
TypeScript
103 lines
3.5 KiB
TypeScript
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' }),
|
|
);
|
|
});
|
|
});
|