mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
lifecycle
This commit is contained in:
@@ -36,9 +36,13 @@ describe('BookingLifecycleNotifierService — operation changes requested', () =
|
||||
);
|
||||
});
|
||||
|
||||
it('sends a GL-created booking back to the GL who created it, not the customer', async () => {
|
||||
it('sends a GL-created customs booking back to the GL who created it, not the customer', async () => {
|
||||
service.operationChangesRequested(
|
||||
booking({ createdByRole: 'GL_ET', createdByUserId: 'gl-user-1' }),
|
||||
booking({
|
||||
customsClearingEnabled: true,
|
||||
createdByRole: 'GL_ET',
|
||||
createdByUserId: 'gl-user-1',
|
||||
}),
|
||||
'Cargo weight does not match the declaration',
|
||||
);
|
||||
await flush();
|
||||
@@ -54,7 +58,7 @@ describe('BookingLifecycleNotifierService — operation changes requested', () =
|
||||
expect(notifications.directSend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('still tells the customer when the booking is their own', async () => {
|
||||
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();
|
||||
|
||||
@@ -65,14 +69,38 @@ describe('BookingLifecycleNotifierService — operation changes requested', () =
|
||||
expect(notifications.directSend).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('falls back to the customer when the GL creator is unknown (legacy rows)', async () => {
|
||||
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({ createdByRole: 'GL_ET', createdByUserId: null }),
|
||||
booking({ customsClearingEnabled: true, createdByRole: 'CUSTOMER' }),
|
||||
'Fix the declaration',
|
||||
);
|
||||
await flush();
|
||||
|
||||
expect(inbox.notify.mock.calls[0][0].recipients).toEqual({ companyId: 'co-1' });
|
||||
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],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -247,20 +247,27 @@ export class BookingLifecycleNotifierService {
|
||||
/**
|
||||
* Operations returned the operation request for changes.
|
||||
*
|
||||
* A customs (Path B) booking was created BY GL Ethiopia on the customer's
|
||||
* behalf — the customer cannot edit or resubmit it, so telling them to "update
|
||||
* from the portal" is a dead end. Those go to the GL who created it, linking
|
||||
* the contract clearance page they work from. Everything else (customer-made
|
||||
* bookings) keeps the portal message.
|
||||
* A customs (Path B) booking is completed by GL Ethiopia on the customer's
|
||||
* behalf regardless of who opened the shipment instance — the customer-opened
|
||||
* ONE_TIME case (see contract-booking.service assertGate) still stamps
|
||||
* createdByRole 'CUSTOMER', so gate on customsClearingEnabled, not on who
|
||||
* created it. The customer cannot edit or resubmit a customs booking, so
|
||||
* telling them to "update from the portal" is a dead end. Those go to the GL
|
||||
* who created it when known, else the clearance desk, linking the contract
|
||||
* clearance page they work from. Everything else (customer-made bookings)
|
||||
* keeps the portal message.
|
||||
*/
|
||||
operationChangesRequested(b: Booking, note: string): void {
|
||||
if (b.createdByRole === 'GL_ET' && b.createdByUserId) {
|
||||
if (b.customsClearingEnabled) {
|
||||
const msg =
|
||||
`Operations returned booking ${b.reference} for changes: ${note}. ` +
|
||||
`Address it on the contract clearance page and resubmit to Operations.`;
|
||||
this.logger.log(`OPERATION CHANGES REQUESTED (to GL) — ${this.ref(b)}`);
|
||||
void this.inbox.notify({
|
||||
recipients: { userIds: [b.createdByUserId] },
|
||||
recipients:
|
||||
b.createdByRole === 'GL_ET' && b.createdByUserId
|
||||
? { userIds: [b.createdByUserId] }
|
||||
: CLEARANCE_DESK,
|
||||
audience: NotificationAudience.BACKOFFICE,
|
||||
type: NotificationType.BOOKING_STATUS,
|
||||
title: `Booking ${b.reference} needs changes`,
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
import type { Freight } from "@edr/types";
|
||||
|
||||
import { ClearanceOpsTabs } from "@/components/contracts/ClearanceOpsTabs";
|
||||
import { BookingChangesRequestedAlert } from "@/components/contracts/BookingChangesRequestedAlert";
|
||||
import { PageContainer, PageHeader, KpiStrip } from "@/components/page";
|
||||
import type { KpiItem } from "@/components/page";
|
||||
import {
|
||||
@@ -134,6 +135,20 @@ export default function DocumentClearanceDetailPage() {
|
||||
hasPermission(user, FREIGHT_PERMS.contracts.createBooking) &&
|
||||
!isDjiboutiGl(user);
|
||||
|
||||
// Operations sent this GL-created booking back for changes. Customs bookings
|
||||
// are never self-booked (see BookingChangesRequestedAlert) — the note and the
|
||||
// resubmit belong here, on the page GL works from, not the customer's portal.
|
||||
const bookingNeedsChanges = booking?.status === "OPERATION_CHANGES_REQUESTED";
|
||||
const isGlBookingOwner =
|
||||
hasPermission(user, FREIGHT_PERMS.contracts.createBooking) &&
|
||||
!isDjiboutiGl(user);
|
||||
const changeRequestNote =
|
||||
[...(booking?.reviewNotes ?? [])]
|
||||
.filter((n) => n.type === "CHANGES_REQUESTED")
|
||||
.sort(
|
||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||
)[0]?.note ?? null;
|
||||
|
||||
const docsPhaseComplete =
|
||||
clearance?.milestones?.some(
|
||||
(m) => m.milestoneCode === "DOCUMENTS_APPROVED" && m.status === "COMPLETED",
|
||||
@@ -279,6 +294,22 @@ export default function DocumentClearanceDetailPage() {
|
||||
}
|
||||
/>
|
||||
|
||||
{bookingNeedsChanges ? (
|
||||
<BookingChangesRequestedAlert
|
||||
bookingId={id!}
|
||||
reference={booking?.reference}
|
||||
note={changeRequestNote}
|
||||
scheduledDate={booking?.scheduledDate}
|
||||
canResubmit={isGlBookingOwner}
|
||||
editHref={
|
||||
booking?.contractId
|
||||
? `/dashboard/contracts/${booking.contractId}/bookings/${id}/complete?copyFrom=${id}`
|
||||
: undefined
|
||||
}
|
||||
onResubmitted={() => void refetch()}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<KpiStrip items={kpis} />
|
||||
|
||||
{requestedLines ? (
|
||||
|
||||
Reference in New Issue
Block a user