import { overseasHttp } from '@/shared/api/overseasHttp' import { ApiResponseError, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' import type { InboundPlanDetailDto } from './inboundPlans' export interface OverseasPageResult { records: T[] page: number size: number total: number pages: number } export type OverseasPagePayload = Omit, 'page' | 'size' | 'total' | 'pages'> & { page: number | string size: number | string total: number | string pages: number | string } export function normalizeOverseasPage(value: OverseasPagePayload): OverseasPageResult { return { ...value, page: Number(value.page), size: Number(value.size), total: Number(value.total), pages: Number(value.pages) } } export interface LastMileOrderQuery { page: number size: number orderNo?: string orderType?: string inboundCode?: string lxCode?: string containerNo?: string currentStatus?: string status?: string warehouse?: string sourceOrderNo?: string transferWarehouseCode?: string abnormal?: boolean detained?: boolean createdFrom?: string createdTo?: string } export interface LastMileOrderSummaryDto { id: string customerId: string sourceContainerBookingId?: string | null orderNo: string orderType?: string | null orderTypeName?: string | null inboundCode?: string | null lxCode?: string | null sourceOrderNo?: string | null sourceInboundId?: string | null customerName?: string | null overseasWarehouseId?: string | null overseasWarehouseCode?: string | null overseasWarehouseName?: string | null transferWarehouseName?: string | null transferWarehouseId?: string | null transferWarehouseCode?: string | null fbaWarehouseCode?: string | null containerNo?: string | null containerStatus?: string | null abnormal?: boolean | null detained?: boolean | null holdChargeable?: boolean | null holdReason?: string | null manualOverrideEnabled?: boolean | null nodeSource?: 'CONTAINER' | 'MANUAL' | null version?: number | null palletCount?: number | null packageCount?: number | null volumeCbm?: number | null productQuantity?: number | null volumeWeightKg?: number | null estimatedArrivalAt?: string | null actualArrivalAt?: string | null estimatedWarehouseAt?: string | null inboundOrderNo?: string | null /** Legacy read-only association retained for compatibility; absent from current service DTO. */ deliveryOrderNo?: string | null outboundOrderNo?: string | null relabelOrderNo?: string | null relabelStatus?: string | null palletMergeFlag?: string | null /** Optional grouping facts returned by newer last-leg services. */ palletMergeBatchId?: string | null palletMergeInboundNos?: string[] | null currentStatus?: string | null privateRemark?: string | null customerRemark?: string | null warehouseRemark?: string | null createdAt?: string | null } export interface LastMileOrderDto extends LastMileOrderSummaryDto { customerId: string sourceOrderNo?: string | null subOrderStatus?: string | null updatedAt?: string | null } export interface LastMileCargoDto { id: string skuCode?: string | null productNameZh?: string | null productNameEn?: string | null packageCount?: number | null productQuantity?: number | null palletCount?: number | null grossWeightKg?: number | null dimensionalWeightKg?: number | null volumeCbm?: number | null destinationWarehouseName?: string | null fbaTrackingNo?: string | null shipmentNo?: string | null } export interface LastMileStatusHistoryDto { id: string fromStatus?: string | null toStatus?: string | null remark?: string | null createdAt?: string | null } export interface LastMileFeeDto { id: string customerId: string orderId: string feeItem: string currency: string amount: number status: string supplierDescription?: string | null createdBy: string createdAt: string updatedAt: string } export interface LastMileOperationDto { id: string operationNode: string operatorName: string result: string createdAt: string } export interface CreateLastMileFeePayload { feeItem: string currency: string amount: number status: string supplierDescription?: string } export interface TransferDestinationPayload { orderIds: string[] warehouseId: string warehouseCode: string warehouseName: string remark?: string } export interface ContainerPlanProgressQuery { page: number size: number containerNo?: string createdFrom?: string createdTo?: string } export interface ContainerPlanProgressSummaryDto { sourceContainerBookingId: string containerNo: string | null overseasWarehouseId: string | null overseasWarehouseCode: string | null overseasWarehouseName: string | null assignmentVersion: number canChangeReceivingWarehouse: boolean receivingWarehouseLockReason: string | null totalOrderCount: number inboundCodeCount: number missingInboundCodeCount: number planCount: number plannedInboundCodeCount: number pendingPlanCount: number plannedArrivalAt: string | null allPlansReady: boolean blockingReason: string | null } export interface ContainerPlanProgressItemDto { orderId: string customerId: string orderNo: string | null inboundCode: string | null orderStatus: string planId: string | null planNo: string | null planStatus: string | null planValid: boolean plannedArrivalAt: string | null } export interface ContainerPlanProgressDetailDto { summary: ContainerPlanProgressSummaryDto orders: ContainerPlanProgressItemDto[] } export interface ConvertContainerInboundPlanPayload { sourceContainerBookingId: string } export type ManualNodeCode = | 'DOMESTIC_CUSTOMS_COMPLETED' | 'ESTIMATED_DEPARTURE' | 'ACTUAL_DEPARTURE' | 'ESTIMATED_ARRIVAL' | 'OVERSEAS_CUSTOMS_COMPLETED' | 'ACTUAL_ARRIVAL_WAITING_UNLOAD' | 'PICKUP_APPOINTED' | 'UNLOADED' export interface ManualInboundPlanPayload { customerId: string orderId: string expectedOrderVersion: number plannedArrivalDate: string palletCount: number palletType: 'STANDARD' | 'WOOD' | 'PAPER' assigned: boolean mergeRequired: boolean urgent: boolean remark?: string } export interface HoldLastMileOrderPayload { held: boolean chargeable: boolean reason: string expectedVersion: number } export interface HoldLastMileOrderResponse { orderId: string held: boolean chargeable: boolean reason?: string | null version: number } export interface ManualNodeValue { nodeCode: ManualNodeCode nodeAt: string | null } export interface ManualNodesResponse { orderId: string manualOverrideEnabled: boolean nodes: Array version: number } export interface ChangeContainerReceivingWarehousePayload { overseasWarehouseId: string expectedVersion: number remark?: string } export interface LastMileOrderDetailDto { order: LastMileOrderDto manualNodes?: ManualNodesResponse['nodes'] cargo: LastMileCargoDto[] statusHistory: LastMileStatusHistoryDto[] fees: LastMileFeeDto[] operationHistory: LastMileOperationDto[] } export async function listLastMileOrders(query: LastMileOrderQuery) { const response = await overseasHttp.get>, ApiEnvelope>>( '/platform/v1/last-leg/orders', { params: query }, ) return normalizeOverseasPage(unwrapApiResponse(response)) } export async function getLastMileOrder(orderId: string) { const response = await overseasHttp.get, ApiEnvelope>( `/platform/v1/last-leg/orders/${encodeURIComponent(orderId)}`, ) return unwrapApiResponse(response) } export async function downloadLastMileOrders(query: Partial) { const { page: _page, size: _size, ...filters } = query const params = Object.fromEntries( Object.entries(filters).filter(([, value]) => value !== undefined && value !== null && value !== ''), ) const blob = await overseasHttp.get('/platform/v1/last-leg/orders/export', { params, responseType: 'blob' }) if (!blob.type.toLowerCase().includes('json')) return blob let error: Partial> try { error = JSON.parse(await blob.text()) as Partial> } catch { throw new ApiResponseError('导出接口返回了非 CSV 内容', 'INVALID_EXPORT_RESPONSE') } throw new ApiResponseError( error.message || '尾程订单导出失败', error.code || 'INVALID_EXPORT_RESPONSE', undefined, error.data, error.requestId, error.traceId, ) } export async function createLastMileOrderFee(orderId: string, payload: CreateLastMileFeePayload) { const response = await overseasHttp.post, ApiEnvelope>( `/platform/v1/last-leg/orders/${encodeURIComponent(orderId)}/fees`, payload, ) return unwrapApiResponse(response) } export async function transferLastMileOrders(payload: TransferDestinationPayload) { const response = await overseasHttp.put, ApiEnvelope>( '/platform/v1/last-leg/orders/destination', payload, ) return unwrapApiResponse(response) } export async function listContainerPlanProgress(query: ContainerPlanProgressQuery): Promise> { const response = await overseasHttp.get>, ApiEnvelope>>( '/platform/v1/last-leg/orders/container-summaries', { params: query }, ) return normalizeOverseasPage(unwrapApiResponse(response)) } export async function getContainerPlanProgress(sourceContainerBookingId: string) { const response = await overseasHttp.get, ApiEnvelope>( `/platform/v1/last-leg/orders/container-summaries/${encodeURIComponent(sourceContainerBookingId)}`, ) return unwrapApiResponse(response) } export async function createManualInboundPlan(payload: ManualInboundPlanPayload, idempotencyKey: string) { const response = await overseasHttp.post, ApiEnvelope>( '/platform/v1/last-leg/inbound-plans/manual', payload, { headers: { 'Idempotency-Key': idempotencyKey } }, ) return unwrapApiResponse(response) } export async function updateLastMileOrderHold(orderId: string, payload: HoldLastMileOrderPayload, idempotencyKey: string) { const response = await overseasHttp.put, ApiEnvelope>( `/platform/v1/last-leg/orders/${encodeURIComponent(orderId)}/hold`, payload, { headers: { 'Idempotency-Key': idempotencyKey } }, ) return unwrapApiResponse(response) } export async function getLastMileOrderManualNodes(orderId: string) { const response = await overseasHttp.get, ApiEnvelope>( `/platform/v1/last-leg/orders/${encodeURIComponent(orderId)}/manual-nodes`, ) return unwrapApiResponse(response) } export async function updateLastMileOrderManualNodes( orderId: string, expectedVersion: number, nodes: ManualNodeValue[], idempotencyKey: string, ) { const response = await overseasHttp.put, ApiEnvelope>( `/platform/v1/last-leg/orders/${encodeURIComponent(orderId)}/manual-nodes`, { expectedVersion, nodes }, { headers: { 'Idempotency-Key': idempotencyKey } }, ) return unwrapApiResponse(response) } export async function changeContainerReceivingWarehouse( sourceContainerBookingId: string, payload: ChangeContainerReceivingWarehousePayload, idempotencyKey: string, ) { const response = await overseasHttp.put, ApiEnvelope>( `/platform/v1/last-leg/orders/container-summaries/${encodeURIComponent(sourceContainerBookingId)}/receiving-warehouse`, payload, { headers: { 'Idempotency-Key': idempotencyKey } }, ) return unwrapApiResponse(response) } export async function convertLastMileContainerToInboundPlans(payload: ConvertContainerInboundPlanPayload) { const response = await overseasHttp.post, ApiEnvelope>( '/platform/v1/last-leg/inbound-plans/convert-container', payload, ) return unwrapApiResponse(response) }