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(
|
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',
|
'Cargo weight does not match the declaration',
|
||||||
);
|
);
|
||||||
await flush();
|
await flush();
|
||||||
@@ -54,7 +58,7 @@ describe('BookingLifecycleNotifierService — operation changes requested', () =
|
|||||||
expect(notifications.directSend).not.toHaveBeenCalled();
|
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');
|
service.operationChangesRequested(booking(), 'Please attach the packing list');
|
||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
@@ -65,14 +69,38 @@ describe('BookingLifecycleNotifierService — operation changes requested', () =
|
|||||||
expect(notifications.directSend).toHaveBeenCalled();
|
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(
|
service.operationChangesRequested(
|
||||||
booking({ createdByRole: 'GL_ET', createdByUserId: null }),
|
booking({ customsClearingEnabled: true, createdByRole: 'CUSTOMER' }),
|
||||||
'Fix the declaration',
|
'Fix the declaration',
|
||||||
);
|
);
|
||||||
await flush();
|
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.
|
* Operations returned the operation request for changes.
|
||||||
*
|
*
|
||||||
* A customs (Path B) booking was created BY GL Ethiopia on the customer's
|
* A customs (Path B) booking is completed by GL Ethiopia on the customer's
|
||||||
* behalf — the customer cannot edit or resubmit it, so telling them to "update
|
* behalf regardless of who opened the shipment instance — the customer-opened
|
||||||
* from the portal" is a dead end. Those go to the GL who created it, linking
|
* ONE_TIME case (see contract-booking.service assertGate) still stamps
|
||||||
* the contract clearance page they work from. Everything else (customer-made
|
* createdByRole 'CUSTOMER', so gate on customsClearingEnabled, not on who
|
||||||
* bookings) keeps the portal message.
|
* 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 {
|
operationChangesRequested(b: Booking, note: string): void {
|
||||||
if (b.createdByRole === 'GL_ET' && b.createdByUserId) {
|
if (b.customsClearingEnabled) {
|
||||||
const msg =
|
const msg =
|
||||||
`Operations returned booking ${b.reference} for changes: ${note}. ` +
|
`Operations returned booking ${b.reference} for changes: ${note}. ` +
|
||||||
`Address it on the contract clearance page and resubmit to Operations.`;
|
`Address it on the contract clearance page and resubmit to Operations.`;
|
||||||
this.logger.log(`OPERATION CHANGES REQUESTED (to GL) — ${this.ref(b)}`);
|
this.logger.log(`OPERATION CHANGES REQUESTED (to GL) — ${this.ref(b)}`);
|
||||||
void this.inbox.notify({
|
void this.inbox.notify({
|
||||||
recipients: { userIds: [b.createdByUserId] },
|
recipients:
|
||||||
|
b.createdByRole === 'GL_ET' && b.createdByUserId
|
||||||
|
? { userIds: [b.createdByUserId] }
|
||||||
|
: CLEARANCE_DESK,
|
||||||
audience: NotificationAudience.BACKOFFICE,
|
audience: NotificationAudience.BACKOFFICE,
|
||||||
type: NotificationType.BOOKING_STATUS,
|
type: NotificationType.BOOKING_STATUS,
|
||||||
title: `Booking ${b.reference} needs changes`,
|
title: `Booking ${b.reference} needs changes`,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
import type { Freight } from "@edr/types";
|
import type { Freight } from "@edr/types";
|
||||||
|
|
||||||
import { ClearanceOpsTabs } from "@/components/contracts/ClearanceOpsTabs";
|
import { ClearanceOpsTabs } from "@/components/contracts/ClearanceOpsTabs";
|
||||||
|
import { BookingChangesRequestedAlert } from "@/components/contracts/BookingChangesRequestedAlert";
|
||||||
import { PageContainer, PageHeader, KpiStrip } from "@/components/page";
|
import { PageContainer, PageHeader, KpiStrip } from "@/components/page";
|
||||||
import type { KpiItem } from "@/components/page";
|
import type { KpiItem } from "@/components/page";
|
||||||
import {
|
import {
|
||||||
@@ -134,6 +135,20 @@ export default function DocumentClearanceDetailPage() {
|
|||||||
hasPermission(user, FREIGHT_PERMS.contracts.createBooking) &&
|
hasPermission(user, FREIGHT_PERMS.contracts.createBooking) &&
|
||||||
!isDjiboutiGl(user);
|
!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 =
|
const docsPhaseComplete =
|
||||||
clearance?.milestones?.some(
|
clearance?.milestones?.some(
|
||||||
(m) => m.milestoneCode === "DOCUMENTS_APPROVED" && m.status === "COMPLETED",
|
(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} />
|
<KpiStrip items={kpis} />
|
||||||
|
|
||||||
{requestedLines ? (
|
{requestedLines ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user