Files
edr-platform/apps/edr-freight-web/portal/src/services/bookings.service.ts
marshal 105879b531 Merge pull request #1399 from Tria-plc/freight_feature/usermanagement
feat(train-scheduling): mid-route consist changes, audit history, saf…
2026-08-23 06:59:27 +03:00

856 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { Freight, PaginatedResponse } from "@edr/types";
import { URL_CONSTANTS } from "@/constants/URLS";
import type { BookingDocuments } from "@/pages/bookings/new-booking-form/schema";
import { buildBookingFormData } from "./booking-form-data";
import { client } from "../utils/api";
const B = URL_CONSTANTS.BOOKINGS;
export interface EmptyContainerReturn {
id: string;
containerNumber: string;
containerSize: "20" | "40" | null;
returnDate: string;
facility: string | null;
yard: string | null;
zone: string | null;
condition: string | null;
status: string;
returnedBy: "EDR" | "CUSTOMER" | null;
}
export interface MileVehicleSummary {
plate: string | null;
code: string | null;
driverName: string | null;
containerNumber: string | null;
distanceKm: number | null;
}
export interface MileLegSummary {
status: string;
exactKm: number | null;
remainingPayment: number | null;
currency: string;
invoiced: boolean;
vehicles: MileVehicleSummary[];
}
export interface MileSummaryResponse {
firstMile: MileLegSummary | null;
lastMile: MileLegSummary | null;
}
export type CreateBookingPayload = Freight.CreateBookingDto;
export interface ContractView {
bookingId: string;
reference: string;
status: string;
templateKey: string;
title: string;
html: string;
canSignCustomer: boolean;
canSignStaff: boolean;
hasContractDocument: boolean;
signatures: Array<{
role: string;
signerDisplayName: string;
signedAt: string;
signatureImageUrl?: string | null;
stampImageUrl?: string | null;
}>;
/** Current viewer's reusable saved signature, if they have one. */
savedSignature?: {
signerDisplayName: string;
signatureImageUrl?: string | null;
stampImageUrl?: string | null;
} | null;
}
export interface PriceLineItem {
code: string;
description: string;
/** Computed line total (unitAmount × quantity). */
amount: number;
/** Price for a single unit of this charge (e.g. one 20ft container, one ton). */
unitAmount?: number;
/** Unit the rate is charged per: PER_CONTAINER | PER_TON | PER_WAGON | PER_KM | FLAT. */
unit?: string;
/** How many units this charge applies to (containers, tons, wagons; 1 for FLAT). */
quantity?: number;
currency: string;
}
/**
* An announced upcoming/open booking window, shown to every signed-in customer
* regardless of contract. Import trains open a window on one booking day;
* export trains open 24h before departure (first come, first served).
*/
export interface MyBookingWindow {
scheduleId: string;
/** Train schedule reference (e.g. TS-2026-000123), shown on the window card. */
reference: string | null;
/** Operational run number (e.g. 8001 import / 8002 export), when staff set one. */
trainNumber: string | null;
/**
* The customer's active contract on this lane, when they hold one — enables
* "Book now" to target it. Null for lanes they have no contract on.
*/
contractId: string | null;
/** ONE_TIME contracts can't draw down against a window — button is hidden. */
contractKind: "ONE_TIME" | "GENERAL" | null;
direction: "IMPORT" | "EXPORT" | null;
windowPhase: string | null;
isOpenNow: boolean;
windowOpensAt: string | null;
windowClosesAt: string | null;
docReviewEndsAt: string | null;
paymentPhaseEndsAt: string | null;
/** End of the payment drain tail — pending payments may settle until then. */
paymentDrainEndsAt: string | null;
bookingWindowStatus: string;
bookingCycleNo: number;
departureDate: string;
origin: string | null;
destination: string | null;
/**
* Full ordered corridor for the window's route — origin, every intermediate
* milestone stop, then destination (e.g. Djibouti → Adama → Dire Dawa).
* Falls back to [origin, destination] when the route has no milestones.
*/
routeStations: string[];
}
export interface GeneratePriceResponse {
bookingId: string;
totalAmount: number;
currency: string;
lineItems: PriceLineItem[];
warnings: string[];
}
export interface SubmitBookingResponse {
bookingId: string;
status: string;
priceChanged: boolean;
previousTotalAmount?: number;
totalAmount: number;
currency: string;
lineItems?: PriceLineItem[];
message?: string;
}
export interface SignContractPayload {
role: "CUSTOMER" | "STAFF";
signatureImageBase64: string;
/** Company stamp/seal image; required to sign a contract (not booking contracts). */
stampImageBase64?: string;
signerDisplayName: string;
consentText?: string;
/** Sudo-mode OTP challenge; required when role=CUSTOMER. */
otp?: string;
}
export interface ApproveDeliveryResponse {
bookingId: string;
inventoryId: string;
approvedAt: string;
signerDisplayName: string;
}
/** One import handover record — booking-level or per truck (EDR last-mile). */
export interface BookingHandoverRecord {
id: string;
reference: string;
truckPlate: string | null;
mileType: "SELF_HAUL" | "EDR_LAST_MILE";
generatedAt: string;
signedAt: string | null;
signerName: string | null;
deliveredAt: string | null;
}
export interface SignHandoverResponse {
handoverId: string;
bookingId: string;
signedAt: string | null;
signerDisplayName: string;
allSigned: boolean;
}
export interface CustomerTruckAssignmentPayload {
truckPlateNumber: string;
driverName: string;
truckType: string;
containerNumberToLoad: string;
}
export interface BookingListFilter {
status?: string;
/** Comma-separated statuses (overrides `status` when set). */
statuses?: string;
/** 'true' = has a train assigned (allocated), 'false' = not yet assigned. */
assignedToSchedule?: "true" | "false";
/** ONE_TIME or GENERAL_CONTRACT. */
bookingType?: string;
/** CONTAINER or BULK. */
freightType?: string;
/** Service type (rule-engine service_types.id). */
serviceTypeId?: string;
/** IMPORT / EXPORT / DOMESTIC. */
tradeDirection?: string;
/** Narrow to a single operational profile (importer/exporter/freight_forwarder). */
companyProfileId?: string;
/** Created-date range (ISO). */
createdFrom?: string;
createdTo?: string;
/** Free-text search: booking reference, company name, contract reference (server-side). */
search?: string;
page?: number;
pageSize?: number;
sortBy?: string;
sortOrder?: "ASC" | "DESC";
}
export interface BookingWagonContainer {
containerNumber: string | null;
sealNumber: string | null;
positionOnWagon: number | null;
grossWeightTons: string | null;
/** Container size in feet (20/40) — identifies the shared consolidation wagon. */
sizeFt: number | null;
}
/** One allocated wagon of a booking, as returned by GET /bookings/:id/wagons. */
export interface BookingWagonAllocation {
/** wagon_booking_allocations id — the handle for cancelling this specific wagon. */
allocationId: string;
sequenceNo: number;
wagonNumber: string | null;
wagonType: string | null;
wagonTypeCode: string | null;
tareWeightTons: string | null;
capacityTons: string | null;
lengthMeters: string | null;
allocatedWeightTons: string | null;
loadType: "CONTAINER" | "BULK";
status: "PLANNED" | "RESERVED" | "LOADED" | "DEPARTED";
trainNumber: string | null;
departureAt: string | null;
originStation: string | null;
destinationStation: string | null;
bulkCargoDescription: string | null;
bulkQuantity: string | null;
containers: BookingWagonContainer[];
}
// ── Partial wagon cancellation (paid bookings) ──────────────────────────────
export type WagonCancellationStatus =
| "FEE_PENDING"
| "CREDIT_AVAILABLE"
| "REBOOKED"
| "WITHDRAWN"
| "EXPIRED";
/** One partial-cancellation ledger row of a paid booking. */
export interface WagonCancellation {
id: string;
bookingId: string;
rebookedBookingId?: string | null;
wagonsCancelled: number;
weightTons: number;
/** What was cut: bulk tons, or container units per size (ft). */
cancelledQuantities: {
bulkTons?: number;
bySize?: Record<string, number>;
/** Exact physical containers leaving with the cancelled wagons. */
units?: Array<{
containerSize: string;
containerNumber: string;
sealNumber?: string | null;
vgmTons: number;
isHazardous: boolean;
isReefer: boolean;
}>;
/** Wagons already left the schedule when the request was made. */
releasedAtRequest?: boolean;
};
/** Rebooking credit — the cancelled share of the original freight price. */
creditAmount: number;
feeAmount: number;
feeCurrency: string;
feeInvoiceId?: string | null;
feePaidAt?: string | null;
status: WagonCancellationStatus;
reason?: string | null;
rebookedAt?: string | null;
createdAt: string;
booking?: { id: string; reference: string } | null;
rebookedBooking?: { id: string; reference: string } | null;
}
/** Fee/credit preview of a partial wagon cancellation (no writes). */
export interface WagonCancellationPreview {
wagons: number;
weightTons: number;
feePerWagon: number;
feeAmount: number;
feeCurrency: string;
creditAmount: number;
}
export interface RequestWagonCancellationPayload {
/** Cancel SPECIFIC wagons: allocationIds from getWagons. Overrides the fields below. */
wagonAllocationIds?: string[];
/** BULK bookings: number of wagons to cancel (tons derived proportionally). */
wagons?: number;
/** CONTAINER bookings: units to cancel per size ("20"/"40", as stored on the line). */
containers?: Array<{ containerSize: string; quantity: number }>;
reason?: string;
}
export interface WagonCancellationListFilter {
statuses?: string[];
search?: string;
from?: string;
to?: string;
page?: number;
pageSize?: number;
}
export interface WagonCancellationListResponse {
items: WagonCancellation[];
total: number;
}
export const bookingsService = {
list: async (
filter: BookingListFilter | void = {},
): Promise<PaginatedResponse<Freight.IBooking>> => {
const { data } = await client.get("/api/bookings", { params: filter });
return data.data;
},
get: async (id: string): Promise<Freight.IBooking> => {
const { data } = await client.get(`/api/bookings/${id}`);
return data.data;
},
mileSummary: async (id: string): Promise<MileSummaryResponse> => {
const { data } = await client.get(`/api/bookings/${id}/mile-summary`);
return data.data;
},
/** Ad-hoc extra charges finance has raised against this booking. */
getAdditionalCharges: async (id: string): Promise<Freight.AdditionalCharge[]> => {
const { data } = await client.get(`/api/bookings/${id}/additional-charges`);
return data.data;
},
assignCustomerTruck: async (
id: string,
payload: CustomerTruckAssignmentPayload,
): Promise<Freight.IBooking> => {
const { data } = await client.post(
`/api/bookings/${id}/customer-truck-assignment`,
payload,
);
return data.data;
},
downloadCustomerTruckFreightOrder: async (id: string, copies?: number[]): Promise<Blob> => {
const { data } = await client.get(
`/api/bookings/${id}/customer-truck-assignment/freight-order`,
{ responseType: "blob", params: copies?.length ? { copies: copies.join(",") } : undefined },
);
return data;
},
downloadHandoverDocument: async (inventoryId: string): Promise<Blob> => {
const { data } = await client.get(
`/api/warehouse-inventory/${inventoryId}/handover-document`,
{ responseType: "blob" },
);
return data;
},
downloadBookingHandoverDocument: async (
bookingId: string,
handoverId?: string,
): Promise<Blob> => {
const { data } = await client.get(
`/api/warehouse-inventory/bookings/${bookingId}/handover-document`,
{ responseType: "blob", params: handoverId ? { handoverId } : undefined },
);
return data;
},
listBookingHandovers: async (
bookingId: string,
): Promise<BookingHandoverRecord[]> => {
const { data } = await client.get(
`/api/warehouse-inventory/bookings/${bookingId}/handovers`,
);
return data.data ?? data;
},
signHandover: async (
handoverId: string,
signerName: string,
): Promise<SignHandoverResponse> => {
const { data } = await client.post(
`/api/warehouse-inventory/handovers/${handoverId}/sign`,
{ signerName },
);
return data.data ?? data;
},
listEmptyContainerReturns: async (bookingId: string): Promise<EmptyContainerReturn[]> => {
const { data } = await client.get(
`/api/import-operations/bookings/${bookingId}/empty-container-returns`,
);
return data.data ?? data;
},
downloadEquipmentInterchangeDocument: async (returnId: string): Promise<Blob> => {
const { data } = await client.get(
`/api/import-operations/empty-container-returns/${returnId}/document`,
{ responseType: "blob" },
);
return data;
},
downloadBookingGrnDocument: async (bookingId: string): Promise<Blob> => {
const { data } = await client.get(
`/api/warehouse-inventory/bookings/${bookingId}/grn-document`,
{ responseType: "blob" },
);
return data;
},
downloadBookingReleaseDocument: async (bookingId: string): Promise<Blob> => {
const { data } = await client.get(
`/api/warehouse-inventory/bookings/${bookingId}/release-document`,
{ responseType: "blob" },
);
return data;
},
tracking: async (id: string): Promise<Freight.IBookingTracking> => {
const { data } = await client.get(`/api/bookings/${id}/tracking`);
return data.data;
},
create: async (
payload: CreateBookingPayload,
documents?: BookingDocuments,
): Promise<Freight.IBooking> => {
const formData = buildBookingFormData(payload, documents);
const { data } = await client.post("/api/bookings", formData, {
headers: { "Content-Type": "multipart/form-data" },
});
return data.data.booking;
},
getReferenceData: async (): Promise<Freight.BookingReferenceData> => {
const { data } = await client.get("/api/bookings/reference-data");
return data.data;
},
update: async (
id: string,
payload: Partial<CreateBookingPayload>,
documents?: BookingDocuments,
): Promise<{ booking: Freight.IBooking; warnings: string[] }> => {
const formData = buildBookingFormData(payload, documents);
const { data } = await client.patch(`/api/bookings/${id}`, formData, {
headers: { "Content-Type": "multipart/form-data" },
});
return data.data;
},
remove: async (id: string): Promise<void> => {
await client.delete(`/api/bookings/${id}`);
},
cancel: async (id: string, reason: string): Promise<Freight.IBooking> => {
const { data } = await client.post(`/api/bookings/${id}/cancel`, { reason });
return data.data;
},
customerCancel: async (
id: string,
reason?: string,
): Promise<Freight.IBooking> => {
const { data } = await client.post(`/api/bookings/${id}/customer-cancel`, {
reason,
});
return data.data;
},
reject: async (id: string, reason?: string): Promise<Freight.IBooking> => {
const { data } = await client.post(`/api/bookings/${id}/reject`, { reason });
return data.data;
},
generatePrice: async (id: string): Promise<GeneratePriceResponse> => {
const { data } = await client.post(`/api/bookings/${id}/generate-price`);
return data.data;
},
submit: async (id: string): Promise<SubmitBookingResponse> => {
const { data } = await client.post(`/api/bookings/${id}/submit`);
return data.data;
},
confirmSubmit: async (id: string): Promise<SubmitBookingResponse> => {
const { data } = await client.post(`/api/bookings/${id}/confirm-submit`);
return data.data;
},
uploadDocuments: async (
id: string,
files: Record<string, File | File[] | null>,
): Promise<Freight.IBooking> => {
const formData = new FormData();
for (const [key, fileOrFiles] of Object.entries(files)) {
if (!fileOrFiles) continue;
if (Array.isArray(fileOrFiles)) {
for (const f of fileOrFiles) formData.append(key, f);
} else {
formData.append(key, fileOrFiles);
}
}
const { data } = await client.post(
`/api/bookings/${id}/documents`,
formData,
{ headers: { "Content-Type": "multipart/form-data" } },
);
return data.data;
},
// ── Document clearance ──
getClearance: async (id: string): Promise<Freight.ClearanceView> => {
const { data } = await client.get(`/api/bookings/${id}/clearance`);
return data.data ?? data;
},
submitClearanceDocuments: async (
id: string,
files: Record<string, File | null>,
): Promise<Freight.IBooking> => {
const formData = new FormData();
for (const [key, file] of Object.entries(files)) {
if (file) formData.append(key, file);
}
const { data } = await client.post(
`/api/bookings/${id}/clearance/documents`,
formData,
{ headers: { "Content-Type": "multipart/form-data" } },
);
return data.data;
},
proceedToOperation: async (
id: string,
scheduledDate: string,
trainScheduleId?: string,
): Promise<Freight.IBooking> => {
const { data } = await client.post(
`/api/bookings/${id}/clearance/proceed`,
{ scheduledDate, ...(trainScheduleId ? { trainScheduleId } : {}) },
);
return data.data;
},
uploadBookingClearanceDutySlip: async (
id: string,
file: File,
): Promise<Freight.IBooking> => {
const form = new FormData();
form.append("file", file);
const { data } = await client.post(`/api/bookings/${id}/clearance/duty-slip`, form, {
headers: { "Content-Type": "multipart/form-data" },
});
return data.data ?? data;
},
/** Outstanding payments per booking — drives the "Pay" badge on list/home rows. */
getMyPayables: async (): Promise<Freight.BookingPayableSummary[]> => {
const { data } = await client.get(`/api/bookings/my-payables`);
return data.data ?? data;
},
// ── Clearance charges (port + miscellaneous) the customer approves, then pays ──
getClearanceCharges: async (id: string): Promise<Freight.ClearanceCharge[]> => {
const { data } = await client.get(`/api/bookings/${id}/clearance/charges`);
return data.data ?? data;
},
acceptClearanceCharge: async (
id: string,
chargeId: string,
): Promise<Freight.ClearanceCharge[]> => {
const { data } = await client.post(
`/api/bookings/${id}/clearance/charges/${chargeId}/accept`,
);
return data.data ?? data;
},
rejectClearanceCharge: async (
id: string,
chargeId: string,
note: string,
): Promise<Freight.ClearanceCharge[]> => {
const { data } = await client.post(
`/api/bookings/${id}/clearance/charges/${chargeId}/reject`,
{ note },
);
return data.data ?? data;
},
acceptDraftDeclaration: async (id: string): Promise<Freight.IBooking> => {
const { data } = await client.post(
`/api/bookings/${id}/clearance/draft-declaration/accept`,
);
return data.data ?? data;
},
requestDraftDeclarationChange: async (
id: string,
note: string,
): Promise<Freight.IBooking> => {
const { data } = await client.post(
`/api/bookings/${id}/clearance/draft-declaration/change`,
{ note },
);
return data.data ?? data;
},
getContractView: async (id: string): Promise<ContractView> => {
const { data } = await client.get(B.CONTRACT_VIEW(id));
return data.data ?? data;
},
downloadContractDocument: async (id: string): Promise<Blob> => {
const { data } = await client.get(B.CONTRACT_DOCUMENT(id), {
responseType: "blob",
});
return data;
},
downloadCarriageAcceptanceSheet: async (id: string): Promise<Blob> => {
const { data } = await client.get(B.CARRIAGE_ACCEPTANCE_SHEET(id), {
responseType: "blob",
});
return data;
},
checkPayment: async (orderId: string): Promise<{ status: string }> => {
const { data } = await client.post(`/api/payments/bookings/check-payment/${orderId}`);
return data.data ?? data;
},
signContract: async (
id: string,
payload: SignContractPayload,
): Promise<Freight.IBooking> => {
const { data } = await client.post(B.CONTRACT_SIGN(id), payload);
return data.data ?? data;
},
approveDelivery: async (
id: string,
signerName: string,
): Promise<ApproveDeliveryResponse> => {
const { data } = await client.post(
`/api/warehouse-inventory/bookings/${id}/approve-delivery`,
{ signerName },
);
return data.data ?? data;
},
getBookableSchedules: async (
query: Freight.BookableSchedulesQuery = {},
): Promise<Freight.BookableScheduleItem[]> => {
const { data } = await client.get(
URL_CONSTANTS.TRAIN_SCHEDULING.BOOKABLE_SCHEDULES,
{ params: query },
);
return data.data;
},
/**
* Day-level pool: the days that have a departure on the route. The customer
* picks a day; the engine assigns the train. No capacity is returned.
*/
getAvailableDays: async (
query: Freight.AvailableDaysQuery = {},
): Promise<string[]> => {
const { data } = await client.get(
URL_CONSTANTS.TRAIN_SCHEDULING.AVAILABLE_DAYS,
{ params: query },
);
return (data.data as Freight.AvailableDaysResponse).days;
},
// Cargo-aware day pool: only days where a train has remaining capacity AND a
// wagon TYPE that can carry this cargo. `containers`/`containerTypeIds` are
// serialized as JSON string params (the server parses them). Days only — no
// capacity counts are ever returned.
getAvailableDaysForCargo: async (
query: Freight.AvailableDaysForCargoQuery,
): Promise<string[]> => {
const { containers, containerTypeIds, ...rest } = query;
const { data } = await client.get(
URL_CONSTANTS.TRAIN_SCHEDULING.AVAILABLE_DAYS_FOR_CARGO,
{
params: {
...rest,
...(containers ? { containers: JSON.stringify(containers) } : {}),
...(containerTypeIds?.length
? { containerTypeIds: JSON.stringify(containerTypeIds) }
: {}),
},
},
);
return (data.data as Freight.AvailableDaysResponse).days;
},
// Days bookable for an EXISTING booking (operation-request step): the server
// derives the cargo from the booking and applies the wagon-type gate.
getAvailableDaysForBooking: async (bookingId: string): Promise<string[]> => {
const { data } = await client.get(
`/api/bookings/${bookingId}/available-days`,
);
return (data.data as Freight.AvailableDaysResponse).days;
},
// Export train picker: the day's export trains with per-wagon-type free space.
// The cargo params cover bare contract instances (nothing persisted yet) —
// sizes/code/wagons come from what the customer is entering on the form.
getExportTrains: async (
bookingId: string,
date: string,
cargo?: { containerSizes?: string[]; cargoTypeCode?: string; wagons?: number },
): Promise<Freight.ExportTrainOption[]> => {
const { data } = await client.get(
`/api/bookings/${bookingId}/export-trains`,
{
params: {
date,
...(cargo?.containerSizes?.length
? { containerSizes: cargo.containerSizes.join(",") }
: {}),
...(cargo?.cargoTypeCode ? { cargoTypeCode: cargo.cargoTypeCode } : {}),
...(cargo?.wagons ? { wagons: cargo.wagons } : {}),
},
},
);
return data.data as Freight.ExportTrainOption[];
},
// Advisory free-wagon count for a shipment day (planning hint, not enforced).
getDayAvailability: async (
bookingId: string,
date: string,
): Promise<Freight.DayAvailabilityResponse> => {
const { data } = await client.get(
`/api/bookings/${bookingId}/day-availability`,
{ params: { date } },
);
return data.data as Freight.DayAvailabilityResponse;
},
/**
* Allocated wagons for a paid booking (empty until placed on a train).
* One row per wagon with its containers / bulk load.
*/
getWagons: async (bookingId: string): Promise<BookingWagonAllocation[]> => {
const { data } = await client.get(`/api/bookings/${bookingId}/wagons`);
return (data.data ?? data) as BookingWagonAllocation[];
},
// ── Partial wagon cancellation ──
/** Fee/credit preview for the confirm dialog — same math as the request, no writes. */
previewWagonCancellation: async (
id: string,
payload: RequestWagonCancellationPayload,
): Promise<WagonCancellationPreview> => {
const { data } = await client.post(
`/api/bookings/${id}/wagon-cancellations/preview`,
payload,
);
return data.data ?? data;
},
/** Open a cancellation: issues the fee invoice; wagons release once the fee settles. */
requestWagonCancellation: async (
id: string,
payload: RequestWagonCancellationPayload,
): Promise<WagonCancellation> => {
const { data } = await client.post(
`/api/bookings/${id}/wagon-cancellations`,
payload,
);
return data.data ?? data;
},
/** Cancellation history of one booking (as source and as rebooked target). */
listWagonCancellations: async (
bookingId: string,
): Promise<WagonCancellationListResponse> => {
const { data } = await client.get(
`/api/bookings/${bookingId}/wagon-cancellations`,
);
return data.data ?? data;
},
/** The signed-in customer's wagon cancellations (paginated, filterable). */
listMyWagonCancellations: async (
filter: WagonCancellationListFilter | void = {},
): Promise<WagonCancellationListResponse> => {
const { data } = await client.get("/api/bookings/wagon-cancellations/my", {
params: filter,
});
return data.data ?? data;
},
// Withdraw was removed from the portal on purpose: a customer's cancellation
// request is final — only backoffice staff (void permission) can revert it.
/** Rebook a CREDIT_AVAILABLE cancellation onto a shipment day → new PAID booking. */
rebookWagonCancellation: async (
cancellationId: string,
payload: {
scheduledDate: string;
/** Optional unit edits — sizes/quantities must match the credit exactly. */
containers?: Array<{
containerSize: string;
units: Array<{
containerNumber: string;
sealNumber?: string;
vgmTons?: number;
}>;
}>;
},
): Promise<{ cancellation: WagonCancellation; bookingId: string }> => {
const { data } = await client.post(
`/api/bookings/wagon-cancellations/${cancellationId}/rebook`,
payload,
);
return data.data ?? data;
},
/**
* Upcoming/open booking windows on the signed-in customer's active-contract
* lanes (import booking-day windows + export 24h pre-departure windows).
*/
getMyBookingWindows: async (): Promise<MyBookingWindow[]> => {
const { data } = await client.get(
URL_CONSTANTS.TRAIN_SCHEDULING.MY_BOOKING_WINDOWS,
);
return data.data ?? data;
},
/**
* Booking windows for a single contract's routes (same row shape as
* `getMyBookingWindows`). Used to gate the direct "New shipment booking"
* entry on the contract detail page and the new-shipment form.
*/
getContractBookingWindows: async (
contractId: string,
): Promise<MyBookingWindow[]> => {
const { data } = await client.get(
URL_CONSTANTS.TRAIN_SCHEDULING.CONTRACT_BOOKING_WINDOWS(contractId),
);
return data.data ?? data;
},
};