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 relabelNo?: 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 containerNode?: 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 receivedPackageCount?: number | null packageDifference?: number | null grossWeightKg?: number | null netWeightKg?: number | null volumeCbm?: number | null productQuantity?: number | null volumeWeightKg?: number | null estimatedArrivalAt?: string | null actualArrivalAt?: string | null estimatedWarehouseAt?: string | null inboundOrderNo?: string | null dispatchStatus?: 'SCHEDULED' | 'UNSCHEDULED' | 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 netWeightKg?: 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; sourceFeeId: string; sourceVersion: number; customerId: string; orderId?: string | null feeItemCode: string; feeItemName: string; direction: 'RECEIVABLE' | 'PAYABLE' currency: 'CNY'; amount: number; originalCurrency: 'CNY' | 'USD'; originalAmount: number exchangeRate: number; exchangeRateMonth: string; reviewStatus: 'DRAFT' | 'PENDING' | 'APPROVED' | 'REJECTED' publicationStatus: string; sourceStatus: string; supplierId?: string | null; supplierName?: string | null settlementMode?: 'ON_ACCOUNT' | 'DIRECT_PAID' | null; businessDate: string chargeableQuantity?: number | null; quantityUnit?: string | null; unitPrice?: number | null periodStart?: string | null; periodEnd?: string | null; createdAt: string; updatedAt: string } export interface LastMileOperationDto { id: string operationNode: string operatorName: string result: string createdAt: string } export interface CreateLastMileFeePayload { feeItemCode: string; feeItemName: string; direction: 'RECEIVABLE' | 'PAYABLE'; currency: 'CNY' | 'USD' amount: number; businessDate: string; supplierId?: string; supplierName?: string; supplierCategory?: string settlementMode?: 'ON_ACCOUNT' | 'DIRECT_PAID'; paidAt?: string; paymentMethod?: 'ONLINE' | 'BANK_TRANSFER' | 'CASH' | 'OTHER' paymentReferenceNo?: string; paymentVoucherFileIds?: string[]; chargeableQuantity?: number quantityUnit?: string; unitPrice?: number; periodStart?: string; periodEnd?: 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 resourcePendingPlanCount: 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 snapshotVersion: number snapshotHash: string } export type ContainerInboundPlanPreviewReadiness = 'READY' | 'RESOURCE_PENDING' | 'PLANNED' | 'BLOCKED' export interface ContainerInboundPlanPreviewOrderDto { customerId: string customerName: string | null orderId: string businessOrderNo: string inboundCode: string | null orderType: string destinationType: 'FBA' | 'ONE_CLICK_DROPSHIP' | 'PRIVATE_ADDRESS' destinationWarehouseId: string | null destinationWarehouseCode: string | null destinationWarehouseName: string | null fbaWarehouseCode: string | null packageCount: number volumeCbm: number suggestedPalletCount: number | null sourcePalletCount: number targetPalletCount: number supplementPalletCount: number plannedPalletCount: number | null existingPlanNo: string | null planStatus: string | null orderVersion: number readiness: ContainerInboundPlanPreviewReadiness blockers: string[] } export interface ContainerInboundPlanPreviewGroupDto { destinationType: 'FBA' | 'ONE_CLICK_DROPSHIP' | 'PRIVATE_ADDRESS' destinationWarehouseId: string | null destinationWarehouseCode: string | null destinationWarehouseName: string | null orderCount: number pendingOrderCount: number pendingPalletCount: number sourcePalletCount: number supplementPalletCount: number existingPlannedPalletCount: number suggestedPalletCount: number plannedPalletCount: number orders: ContainerInboundPlanPreviewOrderDto[] } export interface ContainerInboundPlanPreviewDto { sourceContainerBookingId: string containerNo: string | null overseasWarehouseId: string | null overseasWarehouseCode: string | null overseasWarehouseName: string | null snapshotVersion: number snapshotHash: string totalOrderCount: number readyOrderCount: number blockedOrderCount: number pendingOrderCount: number pendingPalletCount: number sourcePalletCount: number supplementPalletCount: number existingPlannedPalletCount: number totalSuggestedPalletCount: number totalPlannedPalletCount: number ready: boolean groups: ContainerInboundPlanPreviewGroupDto[] } 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 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 reviewReturnOrder(returnNo: string, customerId: string, decision: 'APPROVE' | 'REJECT', remark?: string) { const response = await overseasHttp.post, ApiEnvelope>( `/platform/v1/last-leg/return-orders/${encodeURIComponent(returnNo)}/review`, { decision, remark: remark?.trim() || undefined }, { headers: { 'X-Customer-Id': customerId } }, ) 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, { headers: { 'Idempotency-Key': `last-mile-fee-${orderId}-${Date.now()}-${Math.random().toString(36).slice(2)}` } }, ) 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 getContainerInboundPlanPreview(sourceContainerBookingId: string) { const response = await overseasHttp.get, ApiEnvelope>( '/platform/v1/last-leg/inbound-plans/container-preview', { params: { sourceContainerBookingId } }, ) return unwrapApiResponse(response) } export async function convertLastMileContainerToInboundPlans(payload: ConvertContainerInboundPlanPayload, idempotencyKey: string) { const response = await overseasHttp.post, ApiEnvelope>( '/platform/v1/last-leg/inbound-plans/convert-container', payload, { headers: { 'Idempotency-Key': idempotencyKey } }, ) return unwrapApiResponse(response) }