mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
856 lines
27 KiB
TypeScript
856 lines
27 KiB
TypeScript
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;
|
||
},
|
||
};
|