180 lines
12 KiB
TypeScript
180 lines
12 KiB
TypeScript
import axios from 'axios'
|
|
import { ApiResponseError, http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
|
import type { OceanBookingActualCargoRequest, OceanBookingAgentOption, OceanBookingCarrierCode, OceanBookingDetail, OceanBookingDocument, OceanBookingDocumentInvalidation, OceanBookingDocumentMutation, OceanBookingDocumentType, OceanBookingLinkCandidatePage, OceanBookingLinkCandidateQuery, OceanBookingPage, OceanBookingPrintType, OceanBookingQuery, OceanBookingResource, OceanBookingSaveRequest, OceanBookingStatus, OceanBookingReleaseRequest } from '../types'
|
|
|
|
const basePath = '/platform/v1/ocean-bookings'
|
|
const maxErrorEnvelopeBytes = 1024 * 1024
|
|
|
|
export interface OceanLogisticsChannel {
|
|
channelCode: string
|
|
channelName: string
|
|
}
|
|
function pageNumber(value: number | string, fallback: number) { const n = Number(value); return Number.isFinite(n) ? Math.trunc(n) : fallback }
|
|
function booking(raw: OceanBookingResource): OceanBookingResource {
|
|
const item = raw as OceanBookingResource & { version?: number | string | null }
|
|
return {
|
|
...raw,
|
|
id: String(raw.id),
|
|
version: Number(item.version ?? 0),
|
|
customerId: raw.customerId == null ? null : String(raw.customerId),
|
|
shippingCompanyId: raw.shippingCompanyId == null ? null : String(raw.shippingCompanyId),
|
|
carrierCode: raw.carrierCode ?? null,
|
|
bookingAgentId: raw.bookingAgentId == null ? null : String(raw.bookingAgentId),
|
|
containerCount: Number(raw.containerCount ?? 0),
|
|
declaredPackageCount: raw.declaredPackageCount == null ? null : Number(raw.declaredPackageCount),
|
|
declaredGrossWeightKg: raw.declaredGrossWeightKg == null ? null : Number(raw.declaredGrossWeightKg),
|
|
declaredVolumeCbm: raw.declaredVolumeCbm == null ? null : Number(raw.declaredVolumeCbm),
|
|
actualPackageCount: raw.actualPackageCount == null ? null : Number(raw.actualPackageCount),
|
|
actualGrossWeightKg: raw.actualGrossWeightKg == null ? null : Number(raw.actualGrossWeightKg),
|
|
actualVolumeCbm: raw.actualVolumeCbm == null ? null : Number(raw.actualVolumeCbm),
|
|
pol: raw.pol ?? raw.departurePortName ?? null,
|
|
pod: raw.pod ?? raw.destinationPortName ?? null,
|
|
carrierName: raw.carrierName ?? raw.shippingCompanyName ?? null,
|
|
bookingAgent: raw.bookingAgent ?? raw.bookingAgentName ?? null,
|
|
etd: raw.etd ?? raw.estimatedSailingAt ?? null,
|
|
eta: raw.eta ?? raw.estimatedArrivalAt ?? null,
|
|
}
|
|
}
|
|
function detail(raw: OceanBookingDetail): OceanBookingDetail {
|
|
const source = booking(raw.oceanBooking || raw)
|
|
const histories = (raw.histories || raw.statusHistory || []).map((event) => ({ ...event, id: String(event.id) }))
|
|
return {
|
|
...raw,
|
|
...source,
|
|
oceanBooking: source,
|
|
parties: (raw.parties || []).map((party) => ({ ...party, id: party.id == null ? undefined : String(party.id) })),
|
|
containerRequirements: (raw.containerRequirements || []).map((requirement) => ({
|
|
...requirement,
|
|
id: requirement.id == null ? undefined : String(requirement.id),
|
|
requirementId: requirement.requirementId == null && requirement.id == null ? undefined : String(requirement.requirementId ?? requirement.id),
|
|
quantity: Number(requirement.quantity),
|
|
remainingQuantity: requirement.remainingQuantity == null ? undefined : Number(requirement.remainingQuantity),
|
|
capacityCbm: Number(requirement.capacityCbm),
|
|
maxWeightKg: Number(requirement.maxWeightKg),
|
|
})),
|
|
containers: (raw.containers || []).map((container) => ({
|
|
...container,
|
|
containerBookingId: String(container.containerBookingId),
|
|
requirementId: String(container.requirementId),
|
|
capacityCbm: Number(container.capacityCbm),
|
|
maxWeightKg: Number(container.maxWeightKg),
|
|
})),
|
|
documents: (raw.documents || []).map(document),
|
|
histories,
|
|
statusHistory: histories,
|
|
}
|
|
}
|
|
export async function listOceanBookings(query: OceanBookingQuery) {
|
|
const response = await http.get<ApiEnvelope<OceanBookingPage>, ApiEnvelope<OceanBookingPage>>(basePath, { params: query })
|
|
const result = unwrapApiResponse(response)
|
|
return { ...result, records: (result.records || []).map(booking), page: pageNumber(result.page, query.page), size: pageNumber(result.size, query.size), total: pageNumber(result.total, 0), pages: pageNumber(result.pages, 0) } satisfies OceanBookingPage
|
|
}
|
|
export async function listOceanBookingLinkCandidates(query: OceanBookingLinkCandidateQuery) {
|
|
const response = await http.get<ApiEnvelope<OceanBookingLinkCandidatePage>, ApiEnvelope<OceanBookingLinkCandidatePage>>(`${basePath}/link-candidates`, { params: query })
|
|
const result = unwrapApiResponse(response)
|
|
return {
|
|
...result,
|
|
records: (result.records || []).map((candidate) => ({
|
|
...candidate,
|
|
oceanBookingId: String(candidate.oceanBookingId),
|
|
requirementId: String(candidate.requirementId),
|
|
capacityCbm: Number(candidate.capacityCbm),
|
|
maxWeightKg: Number(candidate.maxWeightKg),
|
|
requiredQuantity: Number(candidate.requiredQuantity),
|
|
allocatedQuantity: Number(candidate.allocatedQuantity),
|
|
remainingQuantity: Number(candidate.remainingQuantity),
|
|
})),
|
|
page: pageNumber(result.page, query.page),
|
|
size: pageNumber(result.size, query.size),
|
|
total: pageNumber(result.total, 0),
|
|
pages: pageNumber(result.pages, 0),
|
|
} satisfies OceanBookingLinkCandidatePage
|
|
}
|
|
export async function getOceanBooking(id: string) { const response = await http.get<ApiEnvelope<OceanBookingDetail>, ApiEnvelope<OceanBookingDetail>>(`${basePath}/${encodeURIComponent(id)}`); return detail(unwrapApiResponse(response)) }
|
|
function idempotencyKey() { return `ocean-booking-${Date.now()}-${Math.random().toString(36).slice(2)}` }
|
|
export async function createOceanBooking(payload: OceanBookingSaveRequest) { const response = await http.post<ApiEnvelope<OceanBookingDetail>, ApiEnvelope<OceanBookingDetail>>(basePath, payload, { headers: { 'Idempotency-Key': idempotencyKey() } }); return detail(unwrapApiResponse(response)) }
|
|
export async function updateOceanBookingDraft(id: string, payload: OceanBookingSaveRequest) { const response = await http.put<ApiEnvelope<OceanBookingDetail>, ApiEnvelope<OceanBookingDetail>>(`${basePath}/${encodeURIComponent(id)}`, payload, { headers: { 'Idempotency-Key': idempotencyKey() } }); return detail(unwrapApiResponse(response)) }
|
|
export async function updateOceanBookingActualCargo(id: string, payload: OceanBookingActualCargoRequest) {
|
|
const response = await http.put<ApiEnvelope<OceanBookingDetail>, ApiEnvelope<OceanBookingDetail>>(`${basePath}/${encodeURIComponent(id)}/actual-cargo`, payload, { headers: { 'Idempotency-Key': idempotencyKey() } })
|
|
return detail(unwrapApiResponse(response))
|
|
}
|
|
export async function deleteOceanBookingDraft(id: string, expectedVersion: number) { await http.delete(`${basePath}/${encodeURIComponent(id)}`, { data: { expectedVersion }, headers: { 'Idempotency-Key': idempotencyKey() } }) }
|
|
export async function releaseOceanBooking(id: string, request: OceanBookingReleaseRequest) { const response = await http.post<ApiEnvelope<OceanBookingDetail>, ApiEnvelope<OceanBookingDetail>>(`${basePath}/${encodeURIComponent(id)}/release`, request, { headers: { 'Idempotency-Key': idempotencyKey() } }); return detail(unwrapApiResponse(response)) }
|
|
const printPaths: Record<OceanBookingPrintType, string> = {
|
|
SHIPPING_INSTRUCTION: 'shipping-instruction',
|
|
BILL_OF_LADING_CONFIRMATION: 'bill-of-lading-confirmation',
|
|
}
|
|
function document(raw: OceanBookingDocument): OceanBookingDocument {
|
|
return {
|
|
...raw,
|
|
id: String(raw.id),
|
|
oceanBookingId: String(raw.oceanBookingId),
|
|
fileId: String(raw.fileId),
|
|
sizeBytes: raw.sizeBytes == null ? null : Number(raw.sizeBytes),
|
|
uploadedBy: String(raw.uploadedBy),
|
|
invalidatedBy: raw.invalidatedBy == null ? null : String(raw.invalidatedBy),
|
|
}
|
|
}
|
|
export async function downloadOceanBookingPrint(id: string, type: OceanBookingPrintType) {
|
|
return requestBlob(`${basePath}/${encodeURIComponent(id)}/prints/${printPaths[type]}`)
|
|
}
|
|
async function parseBlobError(blob: Blob, status?: number) {
|
|
if (!blob.type.toLowerCase().includes('json')) return null
|
|
if (blob.size > maxErrorEnvelopeBytes) return new ApiResponseError('文件接口返回了超出限制的 JSON 响应', 'INVALID_FILE_RESPONSE', status)
|
|
try {
|
|
const payload = JSON.parse(await blob.text()) as Partial<ApiEnvelope<unknown>>
|
|
return new ApiResponseError(payload.code === '200' ? '文件接口未返回文件内容' : payload.message || '文件请求失败', payload.code === '200' ? 'INVALID_FILE_RESPONSE' : payload.code || (status ? `HTTP_${status}` : 'FILE_REQUEST_FAILED'), status, payload.data, payload.requestId, payload.traceId)
|
|
} catch {
|
|
return new ApiResponseError('文件接口返回了无法识别的 JSON 响应', 'INVALID_FILE_RESPONSE', status)
|
|
}
|
|
}
|
|
async function requestBlob(path: string) {
|
|
try {
|
|
const blob = await http.get<Blob, Blob>(path, { responseType: 'blob' })
|
|
const responseError = await parseBlobError(blob, 200)
|
|
if (responseError) throw responseError
|
|
return blob
|
|
} catch (error) {
|
|
if (axios.isAxiosError(error) && error.response?.data instanceof Blob) {
|
|
const responseError = await parseBlobError(error.response.data, error.response.status)
|
|
if (responseError) throw responseError
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
export async function listOceanBookingDocuments(id: string) {
|
|
const response = await http.get<ApiEnvelope<OceanBookingDocument[]>, ApiEnvelope<OceanBookingDocument[]>>(`${basePath}/${encodeURIComponent(id)}/documents`)
|
|
return (unwrapApiResponse(response) || []).map(document)
|
|
}
|
|
export async function referenceOceanBookingDocument(id: string, expectedVersion: number, fileId: string, documentType: OceanBookingDocumentType) {
|
|
const response = await http.post<ApiEnvelope<OceanBookingDocumentMutation>, ApiEnvelope<OceanBookingDocumentMutation>>(`${basePath}/${encodeURIComponent(id)}/documents`, { expectedVersion, fileId, documentType }, { headers: { 'Idempotency-Key': idempotencyKey() } })
|
|
const result = unwrapApiResponse(response)
|
|
return { document: document(result.document), bookingVersion: Number(result.bookingVersion) }
|
|
}
|
|
export async function uploadOceanBookingDocument(id: string, expectedVersion: number, documentType: OceanBookingDocumentType, file: File) {
|
|
const form = new FormData()
|
|
form.append('expectedVersion', String(expectedVersion))
|
|
form.append('documentType', documentType)
|
|
form.append('file', file)
|
|
const response = await http.post<ApiEnvelope<OceanBookingDocumentMutation>, ApiEnvelope<OceanBookingDocumentMutation>>(`${basePath}/${encodeURIComponent(id)}/documents/upload`, form, { headers: { 'Content-Type': 'multipart/form-data', 'Idempotency-Key': idempotencyKey() } })
|
|
const result = unwrapApiResponse(response)
|
|
return { document: document(result.document), bookingVersion: Number(result.bookingVersion) }
|
|
}
|
|
export async function invalidateOceanBookingDocument(id: string, documentId: string, expectedVersion: number) {
|
|
const response = await http.delete<ApiEnvelope<OceanBookingDocumentInvalidation>, ApiEnvelope<OceanBookingDocumentInvalidation>>(`${basePath}/${encodeURIComponent(id)}/documents/${encodeURIComponent(documentId)}`, { data: { expectedVersion }, headers: { 'Idempotency-Key': idempotencyKey() } })
|
|
const result = unwrapApiResponse(response)
|
|
return { documentId: String(result.documentId), bookingVersion: Number(result.bookingVersion) }
|
|
}
|
|
export async function downloadOceanBookingDocument(id: string, documentId: string) {
|
|
return requestBlob(`${basePath}/${encodeURIComponent(id)}/documents/${encodeURIComponent(documentId)}/download`)
|
|
}
|
|
export async function listWarehouseLogisticsChannels(warehouseId: string): Promise<OceanLogisticsChannel[]> {
|
|
const response = await http.get<ApiEnvelope<OceanLogisticsChannel[]>, ApiEnvelope<OceanLogisticsChannel[]>>(`${basePath}/channels`, { params: { originWarehouseId: warehouseId } })
|
|
return (unwrapApiResponse(response) || []).map((item) => ({ channelCode: String(item.channelCode), channelName: String(item.channelName) }))
|
|
}
|
|
export async function listOceanBookingAgents(carrierCode: OceanBookingCarrierCode): Promise<OceanBookingAgentOption[]> {
|
|
const response = await http.get<ApiEnvelope<OceanBookingAgentOption[]>, ApiEnvelope<OceanBookingAgentOption[]>>(`${basePath}/booking-agents`, { params: { carrierCode } })
|
|
return (unwrapApiResponse(response) || []).map((item) => ({ ...item, supplierId: String(item.supplierId), name: String(item.name), carrierCodes: [...(item.carrierCodes || [])] }))
|
|
}
|
|
export type { OceanBookingResource, OceanBookingStatus }
|