mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 18:48:11 +00:00
142 lines
5.4 KiB
TypeScript
142 lines
5.4 KiB
TypeScript
import { BookingLifecycleNotifierService } from './booking-lifecycle-notifier.service';
|
|
import type { Booking } from './entities/booking.entity';
|
|
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
|
|
|
/**
|
|
* Who hears "Operations wants changes" depends on who owns the booking. A
|
|
* customs (Path B) booking is created BY GL Ethiopia on the customer's behalf —
|
|
* the customer can neither edit nor resubmit it, so the note has to reach the GL
|
|
* who made it, not the portal.
|
|
*/
|
|
describe('BookingLifecycleNotifierService — operation changes requested', () => {
|
|
const booking = (over: Partial<Booking> = {}): Booking =>
|
|
({
|
|
id: 'b-1',
|
|
reference: 'BKG-0001',
|
|
companyId: 'co-1',
|
|
contractId: 'ctr-1',
|
|
createdByRole: 'CUSTOMER',
|
|
company: { email: 'customer@example.com' },
|
|
...over,
|
|
}) as Booking;
|
|
|
|
let notifications: { directSend: jest.Mock };
|
|
let inbox: { notify: jest.Mock };
|
|
let service: BookingLifecycleNotifierService;
|
|
|
|
const flush = () => new Promise((resolve) => setImmediate(resolve));
|
|
|
|
beforeEach(() => {
|
|
notifications = { directSend: jest.fn().mockResolvedValue(undefined) };
|
|
inbox = { notify: jest.fn().mockResolvedValue(undefined) };
|
|
service = new BookingLifecycleNotifierService(
|
|
notifications as never,
|
|
inbox as never,
|
|
{ query: jest.fn().mockResolvedValue([{ phone: '+251900000000' }]) } as never,
|
|
);
|
|
});
|
|
|
|
it('sends a GL-created customs booking back to the GL who created it, not the customer', async () => {
|
|
service.operationChangesRequested(
|
|
booking({
|
|
customsClearingEnabled: true,
|
|
createdByRole: 'GL_ET',
|
|
createdByUserId: 'gl-user-1',
|
|
}),
|
|
'Cargo weight does not match the declaration',
|
|
);
|
|
await flush();
|
|
|
|
expect(inbox.notify).toHaveBeenCalledTimes(1);
|
|
const sent = inbox.notify.mock.calls[0][0];
|
|
expect(sent.recipients).toEqual({ userIds: ['gl-user-1'] });
|
|
expect(sent.audience).toBe('BACKOFFICE');
|
|
expect(sent.body).toContain('Cargo weight does not match the declaration');
|
|
// Deep-links the clearance page GL works from, not the portal booking.
|
|
expect(sent.link).toBe('/dashboard/contracts/clearance/ctr-1');
|
|
// The customer is not told to fix something they cannot touch.
|
|
expect(notifications.directSend).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('still tells the customer when the booking is a non-customs self-service booking', async () => {
|
|
service.operationChangesRequested(booking(), 'Please attach the packing list');
|
|
await flush();
|
|
|
|
const sent = inbox.notify.mock.calls[0][0];
|
|
expect(sent.recipients).toEqual({ companyId: 'co-1' });
|
|
expect(sent.audience).toBe('PORTAL');
|
|
expect(sent.link).toBe('/bookings/b-1');
|
|
expect(notifications.directSend).toHaveBeenCalled();
|
|
});
|
|
|
|
it('routes a customer-opened customs booking to the clearance desk, not the customer', async () => {
|
|
// Path B lets the customer open the ONE_TIME shipment instance themselves
|
|
// (contract-booking.service assertGate's customerMayInitiate) — createdByRole
|
|
// stays 'CUSTOMER', but GL still owns completing/resubmitting it.
|
|
service.operationChangesRequested(
|
|
booking({ customsClearingEnabled: true, createdByRole: 'CUSTOMER' }),
|
|
'Fix the declaration',
|
|
);
|
|
await flush();
|
|
|
|
const sent = inbox.notify.mock.calls[0][0];
|
|
expect(sent.recipients).toEqual({
|
|
permissionKeys: [FREIGHT_PERMS.bookings.clearanceGetNotification],
|
|
});
|
|
expect(sent.audience).toBe('BACKOFFICE');
|
|
expect(notifications.directSend).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('falls back to the clearance desk when the GL creator is unknown (legacy rows)', async () => {
|
|
service.operationChangesRequested(
|
|
booking({
|
|
customsClearingEnabled: true,
|
|
createdByRole: 'GL_ET',
|
|
createdByUserId: null,
|
|
}),
|
|
'Fix the declaration',
|
|
);
|
|
await flush();
|
|
|
|
expect(inbox.notify.mock.calls[0][0].recipients).toEqual({
|
|
permissionKeys: [FREIGHT_PERMS.bookings.clearanceGetNotification],
|
|
});
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Staff notifications used to go to every employee in every organization. They
|
|
* now target a desk — and the two desks are disjoint: the GL presets hold no
|
|
* bookings:view and no intake keys, so intake pings would be noise they cannot
|
|
* act on. Both branches run through the same `inAppStaff` helper, which is the
|
|
* easy place to lose the distinction again.
|
|
*/
|
|
describe('BookingLifecycleNotifierService — staff desk targeting', () => {
|
|
const booking = () =>
|
|
({ id: 'b-1', reference: 'BKG-0001', companyId: 'co-1' }) as Booking;
|
|
|
|
let inbox: { notify: jest.Mock };
|
|
let service: BookingLifecycleNotifierService;
|
|
|
|
beforeEach(() => {
|
|
inbox = { notify: jest.fn().mockResolvedValue(undefined) };
|
|
service = new BookingLifecycleNotifierService(
|
|
{ directSend: jest.fn().mockResolvedValue(undefined) } as never,
|
|
inbox as never,
|
|
{ query: jest.fn().mockResolvedValue([]) } as never,
|
|
);
|
|
});
|
|
|
|
it('routes intake items to the booking desk and clearance items to the clearance desk', () => {
|
|
service.submittedToStaff(booking());
|
|
service.clearanceDocsUploadedToStaff(booking());
|
|
|
|
expect(inbox.notify.mock.calls[0][0].recipients).toEqual({
|
|
permissionKeys: [FREIGHT_PERMS.bookings.getNotification],
|
|
});
|
|
expect(inbox.notify.mock.calls[1][0].recipients).toEqual({
|
|
permissionKeys: [FREIGHT_PERMS.bookings.clearanceGetNotification],
|
|
});
|
|
});
|
|
});
|