Files
edr-platform/apps/edr-freight-web/backoffice/src/services/importOperations.service.ts
hager 2e51342d1e feat(import-operations): record empty container return per booking
Container Returns only ever offered the last-mile path: a booking reached
the list once it had a truck assigned and warehouse inventory flagged as
returning. Bookings that ship WITH equipment return had no way in, so the
empties they owe were invisible until that path happened to fire.

Adds GET /import-operations/empty-return-bookings — the containers a
booking flagged is_return, carrying whichever of them already has an
empty return recorded, grouped one row per booking and dropped from the
list once nothing is pending. Covers both spellings of the booking's
equipment_return (WITH_RETURN and the older RETURN) and skips bookings
that never ship.

Backoffice grows a "Bookings With Empty Container Return" card above the
existing sections: pick the booking, tick the containers coming back,
say where they landed, and each tick becomes an empty container return
on that booking — which is what the Returned Containers table then
advances. The existing last-mile, standalone and bulk flows are
untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 10:17:19 +00:00

174 lines
5.6 KiB
TypeScript

import { api as client } from '../auth/http';
import { URL_CONSTANTS } from '@/constants/URLS';
import { unwrap } from '@/utils/endpoint';
import type {
AssignCustomsRiskPayload,
CreateDjiboutiIncidentPayload,
CreateEmptyContainerReturnPayload,
LoadEmptyContainersOnTrainPayload,
DjiboutiIncident,
EmptyContainerReturn,
EmptyReturnBooking,
ImportCustomsFinalization,
ImportOperationActionPayload,
RecordDeclarationPayload,
UpdateEmptyContainerReturnStatusPayload,
UploadImportCustomsDocumentPayload,
} from '@/types/importOperations';
export const importOperationsService = {
listIncidents: async (bookingId?: string): Promise<DjiboutiIncident[]> => {
const response = await client.get<DjiboutiIncident[]>(
URL_CONSTANTS.IMPORT_OPERATIONS.DJIBOUTI_INCIDENTS,
{ params: { bookingId } },
);
return unwrap(response.data);
},
createIncident: async (
payload: CreateDjiboutiIncidentPayload,
): Promise<DjiboutiIncident> => {
const response = await client.post<DjiboutiIncident>(
URL_CONSTANTS.IMPORT_OPERATIONS.DJIBOUTI_INCIDENTS,
payload,
);
return unwrap(response.data);
},
getCustoms: async (bookingId: string): Promise<ImportCustomsFinalization> => {
const response = await client.get<ImportCustomsFinalization>(
URL_CONSTANTS.IMPORT_OPERATIONS.CUSTOMS(bookingId),
);
return unwrap(response.data);
},
uploadCustomsDocument: async (
bookingId: string,
payload: UploadImportCustomsDocumentPayload,
): Promise<ImportCustomsFinalization> => {
const response = await client.post<ImportCustomsFinalization>(
URL_CONSTANTS.IMPORT_OPERATIONS.CUSTOMS_DOCUMENTS(bookingId),
payload,
);
return unwrap(response.data);
},
recordDeclaration: async (
bookingId: string,
payload: RecordDeclarationPayload,
): Promise<ImportCustomsFinalization> => {
const response = await client.post<ImportCustomsFinalization>(
URL_CONSTANTS.IMPORT_OPERATIONS.CUSTOMS_DECLARATION(bookingId),
payload,
);
return unwrap(response.data);
},
notifyDutiesTaxes: async (
bookingId: string,
payload: ImportOperationActionPayload = {},
): Promise<ImportCustomsFinalization> => {
const response = await client.post<ImportCustomsFinalization>(
URL_CONSTANTS.IMPORT_OPERATIONS.CUSTOMS_NOTIFY_DUTIES_TAXES(bookingId),
payload,
);
return unwrap(response.data);
},
markDutiesTaxesPaid: async (
bookingId: string,
payload: ImportOperationActionPayload = {},
): Promise<ImportCustomsFinalization> => {
const response = await client.post<ImportCustomsFinalization>(
URL_CONSTANTS.IMPORT_OPERATIONS.CUSTOMS_DUTIES_TAXES_PAID(bookingId),
payload,
);
return unwrap(response.data);
},
assignRisk: async (
bookingId: string,
payload: AssignCustomsRiskPayload,
): Promise<ImportCustomsFinalization> => {
const response = await client.post<ImportCustomsFinalization>(
URL_CONSTANTS.IMPORT_OPERATIONS.CUSTOMS_RISK(bookingId),
payload,
);
return unwrap(response.data);
},
markReleasePermitted: async (
bookingId: string,
payload: ImportOperationActionPayload = {},
): Promise<ImportCustomsFinalization> => {
const response = await client.post<ImportCustomsFinalization>(
URL_CONSTANTS.IMPORT_OPERATIONS.CUSTOMS_RELEASE_PERMITTED(bookingId),
payload,
);
return unwrap(response.data);
},
listEmptyReturns: async (): Promise<EmptyContainerReturn[]> => {
const response = await client.get<EmptyContainerReturn[]>(
URL_CONSTANTS.IMPORT_OPERATIONS.EMPTY_CONTAINER_RETURNS,
);
return unwrap(response.data);
},
/** Bookings that ship with empty-container return and still owe empties back. */
listEmptyReturnBookings: async (): Promise<EmptyReturnBooking[]> => {
const response = await client.get<EmptyReturnBooking[]>(
URL_CONSTANTS.IMPORT_OPERATIONS.EMPTY_RETURN_BOOKINGS,
);
return unwrap(response.data);
},
createEmptyReturn: async (
payload: CreateEmptyContainerReturnPayload,
): Promise<EmptyContainerReturn> => {
const response = await client.post<EmptyContainerReturn>(
URL_CONSTANTS.IMPORT_OPERATIONS.EMPTY_CONTAINER_RETURNS,
payload,
);
return unwrap(response.data);
},
/** Backfill empties already in the yard. All-or-nothing on the server. */
bulkCreateEmptyReturns: async (
returns: CreateEmptyContainerReturnPayload[],
): Promise<EmptyContainerReturn[]> => {
const response = await client.post<EmptyContainerReturn[]>(
URL_CONSTANTS.IMPORT_OPERATIONS.EMPTY_CONTAINER_RETURNS_BULK,
{ returns },
);
return unwrap(response.data);
},
loadEmptyContainersOnTrain: async (
payload: LoadEmptyContainersOnTrainPayload,
): Promise<EmptyContainerReturn[]> => {
const response = await client.post<EmptyContainerReturn[]>(
URL_CONSTANTS.IMPORT_OPERATIONS.EMPTY_CONTAINER_RETURNS_LOAD_ON_TRAIN,
payload,
);
return unwrap(response.data);
},
updateEmptyReturnStatus: async (
id: string,
payload: UpdateEmptyContainerReturnStatusPayload,
): Promise<EmptyContainerReturn> => {
const response = await client.post<EmptyContainerReturn>(
URL_CONSTANTS.IMPORT_OPERATIONS.EMPTY_CONTAINER_RETURN_STATUS(id),
payload,
);
return unwrap(response.data);
},
/** Equipment interchange receipt — the doc handed to the customer at handover. */
downloadEquipmentInterchangeDocument: (id: string) =>
client.get<Blob>(URL_CONSTANTS.IMPORT_OPERATIONS.EMPTY_CONTAINER_RETURN_DOCUMENT(id), {
responseType: 'blob',
}),
};