import { api as client } from "../auth/http"; import { unwrap } from "@/utils/endpoint"; import { URL_CONSTANTS } from "@/constants/URLS"; import type { Freight } from "@edr/types"; const C = URL_CONSTANTS.CONTRACTS; export interface ContractListFilter { status?: string; /** Comma-separated statuses for grouped tabs. */ statuses?: string; /** Tab key for React Query cache (not sent to API). */ tab?: string; companyId?: string; freightType?: string; tradeDirection?: string; contractKind?: string; paymentCurrency?: string; page?: number; pageSize?: number; sortBy?: string; sortOrder?: "ASC" | "DESC"; } export interface PaginatedContracts { items: Freight.IContract[]; total: number; } export interface ContractListSummaryMetrics { inQueue: number; needsAction: number; urgent: number; completed: number; } export interface ContractListSummaryTabs { all: number; intake: number; in_approval: number; approved_contract: number; clearance: number; active: number; closed: number; } export interface ContractListSummary { metrics: ContractListSummaryMetrics; tabs: ContractListSummaryTabs; } export interface ContractView { contractId: 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; }>; savedSignature?: { signerDisplayName: string; signatureImageUrl?: string | null; } | null; } export interface SignContractPayload { role: "CUSTOMER" | "STAFF"; signatureImageBase64: string; signerDisplayName: string; consentText?: string; } async function postContract(url: string, body?: unknown): Promise { const response = await client.post(url, body ?? {}); return unwrap(response.data); } function buildListParams(filter?: ContractListFilter) { const params: Record = {}; if (filter) { if (filter.statuses) params.statuses = filter.statuses; else if (filter.status) params.status = filter.status; if (filter.page != null) params.page = filter.page; if (filter.pageSize != null) params.pageSize = filter.pageSize; if (filter.sortBy) params.sortBy = filter.sortBy; if (filter.sortOrder) params.sortOrder = filter.sortOrder; if (filter.companyId) params.companyId = filter.companyId; if (filter.freightType) params.freightType = filter.freightType; if (filter.tradeDirection) params.tradeDirection = filter.tradeDirection; if (filter.contractKind) params.contractKind = filter.contractKind; if (filter.paymentCurrency) params.paymentCurrency = filter.paymentCurrency; } return params; } export const contractsService = { getListSummary: async ( filter?: ContractListFilter, ): Promise => { const response = await client.get(C.LIST_SUMMARY, { params: buildListParams(filter), }); return unwrap(response.data) as ContractListSummary; }, list: async (filter?: ContractListFilter): Promise => { const response = await client.get(C.BASE, { params: buildListParams(filter), }); const data = unwrap(response.data); return { items: (data.items ?? []) as Freight.IContract[], total: data.total ?? 0, }; }, getById: async (id: string): Promise => { const response = await client.get(C.BY_ID(id)); return unwrap(response.data) as Freight.IContract; }, // ── Staff review ── staffAccept: (id: string, validityDays: number) => postContract(C.STAFF_ACCEPT(id), { validityDays }), requestChanges: (id: string, note: string) => postContract(C.STAFF_REQUEST_CHANGES(id), { note }), reject: (id: string, reason: string) => postContract(C.STAFF_REJECT(id), { reason }), approveStep: ({ id, stepId, requiredRole, }: { id: string; stepId: string; requiredRole: string; }) => postContract(C.APPROVE_STEP(id, stepId), { requiredRole, }), // ── Contract document ── generateContract: (id: string) => postContract(C.CONTRACT_GENERATE(id)), getContractView: async (id: string): Promise => { const response = await client.get(C.CONTRACT_VIEW(id)); return unwrap(response.data) as ContractView; }, downloadContractDocument: async (id: string): Promise => { const response = await client.get(C.CONTRACT_DOCUMENT(id), { responseType: "blob", }); return response.data as Blob; }, signContract: (id: string, payload: SignContractPayload) => postContract(C.CONTRACT_SIGN(id), payload), // ── Pre-booking clearance (Path B — GL ET) ── getClearanceQueue: async (): Promise => { const response = await client.get(C.CLEARANCE_QUEUE); const data = unwrap(response.data); return { items: (data.items ?? []) as Freight.IContract[], total: data.total ?? 0, }; }, getClearance: async (id: string): Promise => { const response = await client.get(C.CLEARANCE(id)); return unwrap(response.data) as Freight.ContractClearanceView; }, reviewClearanceDocument: ( id: string, payload: { fileKey: string; status: "APPROVED" | "QUERIED"; note?: string }, ) => postContract(C.CLEARANCE_REVIEW(id), payload), uploadClearanceOutput: async ( id: string, files: Record, ): Promise => { const form = new FormData(); for (const [key, file] of Object.entries(files)) { if (file) form.append(key, file); } const response = await client.post(C.CLEARANCE_OUTPUT_DOCUMENTS(id), form, { headers: { "Content-Type": "multipart/form-data" }, }); return unwrap(response.data) as Freight.IContract; }, finalizeClearance: (id: string) => postContract(C.CLEARANCE_FINALIZE(id)), getEtClearanceQueue: async (): Promise => { const response = await client.get(C.CLEARANCE_ET_QUEUE); const data = unwrap(response.data); return { items: (data.items ?? []) as Freight.IContract[], total: data.total ?? 0, }; }, getDjClearanceQueue: async (): Promise => { const response = await client.get(C.CLEARANCE_DJ_QUEUE); const data = unwrap(response.data); return { items: (data.items ?? []) as Freight.IContract[], total: data.total ?? 0, }; }, uploadDeclaration: async ( id: string, files: Record, ): Promise => { const form = new FormData(); for (const [key, file] of Object.entries(files)) { if (file) form.append(key, file); } const response = await client.post(C.CLEARANCE_DECLARATION(id), form, { headers: { "Content-Type": "multipart/form-data" }, }); return unwrap(response.data) as Freight.IContract; }, adviseContractDuty: async ( id: string, payload: { dutyRequired: boolean; amount?: number; currency?: string; declarationSerial?: string; attachment?: File | null; }, ): Promise => { const form = new FormData(); form.append("dutyRequired", String(payload.dutyRequired)); if (payload.amount != null) form.append("amount", String(payload.amount)); if (payload.currency) form.append("currency", payload.currency); if (payload.declarationSerial) { form.append("declarationSerial", payload.declarationSerial); } if (payload.attachment) form.append("attachment", payload.attachment); const response = await client.post(C.CLEARANCE_DUTY(id), form, { headers: { "Content-Type": "multipart/form-data" }, }); return unwrap(response.data) as Freight.IContract; }, finalizePreClearance: (id: string) => postContract(C.CLEARANCE_FINALIZE_PRE(id)), uploadContractTransitPermit: async ( id: string, files: Record, ): Promise => { const form = new FormData(); for (const [key, file] of Object.entries(files)) { if (file) form.append(key, file); } const response = await client.post(C.CLEARANCE_TRANSIT_PERMIT(id), form, { headers: { "Content-Type": "multipart/form-data" }, }); return unwrap(response.data) as Freight.IContract; }, uploadDeliveryOrder: async ( id: string, file: File, ): Promise => { const form = new FormData(); form.append("file", file); const response = await client.post(C.CLEARANCE_DELIVERY_ORDER(id), form, { headers: { "Content-Type": "multipart/form-data" }, }); return unwrap(response.data) as Freight.IContract; }, uploadReleaseOrder: async ( id: string, file: File, vesselDepartureDate: string, ): Promise<{ contract: Freight.IContract; hold: boolean; holdReason?: string }> => { const form = new FormData(); form.append("file", file); form.append("vesselDepartureDate", vesselDepartureDate); const response = await client.post(C.CLEARANCE_RELEASE_ORDER(id), form, { headers: { "Content-Type": "multipart/form-data" }, }); return unwrap(response.data) as { contract: Freight.IContract; hold: boolean; holdReason?: string; }; }, requestRoAmendment: (id: string, note?: string) => postContract(C.CLEARANCE_RO_AMENDMENT(id), { note }), confirmExportRelease: (id: string) => postContract(C.CLEARANCE_EXPORT_RELEASE(id)), finalizeExportClearance: (id: string) => postContract(C.CLEARANCE_FINALIZE_EXPORT(id)), uploadTransportDocument: async ( bookingId: string, files: Record, ) => { const form = new FormData(); for (const [key, file] of Object.entries(files)) { if (file) form.append(key, file); } const response = await client.post(C.BOOKING_TRANSPORT_DOCUMENT(bookingId), form, { headers: { "Content-Type": "multipart/form-data" }, }); return unwrap(response.data); }, /** GL Djibouti uploads T1 transit documents (multi-file, post wagon allocation). */ uploadT1Documents: async ( bookingId: string, files: Record, ) => { const form = new FormData(); for (const [key, file] of Object.entries(files)) { if (file) form.append(key, file); } const response = await client.post(C.BOOKING_T1_DOCUMENTS(bookingId), form, { headers: { "Content-Type": "multipart/form-data" }, }); return unwrap(response.data); }, /** GL Ethiopia closes (accepts) the T1 document set after the train arrives. */ closeT1: async (bookingId: string): Promise => { const response = await client.post(C.BOOKING_T1_CLOSE(bookingId)); return unwrap(response.data) as Freight.ClearanceT1State; }, // ── Path A self-clearance (Operations review) ── getOpsClearanceQueue: async (): Promise => { const response = await client.get( C.OPS_CLEARANCE_QUEUE, ); const data = unwrap(response.data); return { items: (data.items ?? []) as Freight.IContract[], total: data.total ?? 0, }; }, getClearanceHistory: async (): Promise => { const response = await client.get(C.CLEARANCE_HISTORY); const data = unwrap(response.data); return { items: (data.items ?? []) as Freight.IContract[], total: data.total ?? 0 }; }, getOpsClearanceHistory: async (): Promise => { const response = await client.get(C.OPS_CLEARANCE_HISTORY); const data = unwrap(response.data); return { items: (data.items ?? []) as Freight.IContract[], total: data.total ?? 0 }; }, opsReviewClearanceDocument: ( id: string, payload: { fileKey: string; status: "APPROVED" | "QUERIED"; note?: string }, ) => postContract(C.OPS_CLEARANCE_REVIEW(id), payload), opsFinalizeClearance: (id: string) => postContract(C.OPS_CLEARANCE_FINALIZE(id)), // ── Booking under contract (GL ET — Path B) ── createBookingUnderContract: ( id: string, payload: Freight.CreateBookingUnderContractDto, ) => postContract<{ id: string; reference: string }>(C.BOOKINGS(id), payload), /** Remaining bookable quantity per cargo line (GENERAL draw-down cap). */ getCapacity: async (id: string): Promise => { const response = await client.get(C.CAPACITY(id)); return (unwrap(response.data) ?? []) as Freight.ContractCapacityLine[]; }, // ── Shipment requests (GENERAL + customs) ── /** GL queue of pending shipment requests across contracts. */ getBookingRequestQueue: async (): Promise => { const response = await client.get(C.BOOKING_REQUEST_QUEUE); return (unwrap(response.data) ?? []) as Freight.IBookingRequest[]; }, listBookingRequests: async ( id: string, ): Promise => { const response = await client.get(C.BOOKING_REQUESTS(id)); return (unwrap(response.data) ?? []) as Freight.IBookingRequest[]; }, getBookingRequest: async ( reqId: string, ): Promise => { const response = await client.get(C.BOOKING_REQUEST_BY_ID(reqId)); return unwrap(response.data) as Freight.IBookingRequest; }, acceptBookingRequest: (reqId: string, bookingId: string) => postContract(C.BOOKING_REQUEST_ACCEPT(reqId), { bookingId, }), rejectBookingRequest: (reqId: string, note?: string) => postContract(C.BOOKING_REQUEST_REJECT(reqId), { note, }), // ── Clearance milestones ── listMilestonesForContract: async ( id: string, ): Promise => { const response = await client.get(C.MILESTONES(id)); return (unwrap(response.data) ?? []) as Freight.IClearanceMilestone[]; }, listMilestonesForBooking: async ( bookingId: string, ): Promise => { const response = await client.get(C.BOOKING_MILESTONES(bookingId)); return (unwrap(response.data) ?? []) as Freight.IClearanceMilestone[]; }, completeMilestone: (bookingId: string, code: string, note?: string) => postContract( C.COMPLETE_BOOKING_MILESTONE(bookingId, code), { note }, ), // ── GL post-booking operational actions ── assignRisk: ( bookingId: string, payload: { riskLevel: Freight.CustomsRiskLevel; note?: string }, ) => postContract( C.BOOKING_RISK(bookingId), payload, ), adviseDuty: ( bookingId: string, payload: { amount: number; currency: string; declarationSerial?: string; note?: string; }, ) => postContract( C.BOOKING_DUTY(bookingId), payload, ), assignStation: ( bookingId: string, payload: { stationYardId: string; staffId?: string }, ) => postContract(C.BOOKING_STATION_ASSIGN(bookingId), payload), uploadGlDocuments: async ( bookingId: string, files: Record, ): Promise<{ uploaded: number; completedMilestones: string[] }> => { const form = new FormData(); for (const [key, file] of Object.entries(files)) { if (file) form.append(key, file); } const response = await client.post(C.BOOKING_GL_DOCUMENTS(bookingId), form, { headers: { "Content-Type": "multipart/form-data" }, }); return unwrap(response.data) as { uploaded: number; completedMilestones: string[]; }; }, listIncidents: async ( bookingId: string, ): Promise => { const response = await client.get(C.BOOKING_INCIDENTS(bookingId)); return (unwrap(response.data) ?? []) as Freight.IClearanceIncident[]; }, reportIncident: async ( bookingId: string, payload: { incidentType: Freight.IncidentType; description: string; photos: File[]; }, ): Promise => { const form = new FormData(); form.append("incidentType", payload.incidentType); form.append("description", payload.description); for (const photo of payload.photos) form.append("photos", photo); const response = await client.post(C.BOOKING_INCIDENTS(bookingId), form, { headers: { "Content-Type": "multipart/form-data" }, }); return unwrap(response.data) as Freight.IClearanceIncident; }, };