Files
admin-web/src/modules/overseas-warehouse/api/lastMileOrders.ts
T
2026-09-23 11:25:23 +08:00

504 lines
17 KiB
TypeScript

import { overseasHttp } from '@/shared/api/overseasHttp'
import { ApiResponseError, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import type { InboundPlanDetailDto } from './inboundPlans'
export interface OverseasPageResult<T> {
records: T[]
page: number
size: number
total: number
pages: number
}
export type OverseasPagePayload<T> = Omit<OverseasPageResult<T>, 'page' | 'size' | 'total' | 'pages'> & {
page: number | string
size: number | string
total: number | string
pages: number | string
}
export function normalizeOverseasPage<T>(value: OverseasPagePayload<T>): OverseasPageResult<T> {
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
relabelApplications?: Array<{ relabelNo: string; status: string; quantity: number; createdAt: string }>
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<ManualNodeValue & { source: 'MANUAL' }>
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<OverseasPagePayload<LastMileOrderSummaryDto>>, ApiEnvelope<OverseasPagePayload<LastMileOrderSummaryDto>>>(
'/platform/v1/last-leg/orders',
{ params: query },
)
return normalizeOverseasPage(unwrapApiResponse(response))
}
export async function getLastMileOrder(orderId: string) {
const response = await overseasHttp.get<ApiEnvelope<LastMileOrderDetailDto>, ApiEnvelope<LastMileOrderDetailDto>>(
`/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<unknown>, ApiEnvelope<unknown>>(
`/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 reviewDirectRelabelOrder(orderId: string, customerId: string, decision: 'APPROVE' | 'REJECT', remark?: string) {
const response = await overseasHttp.put<ApiEnvelope<unknown>, ApiEnvelope<unknown>>(
`/platform/v1/last-leg/orders/${encodeURIComponent(orderId)}/relabel-review`,
{ status: decision === 'APPROVE' ? 'PENDING_PROCESSING' : 'REJECTED', remark: remark?.trim() || undefined },
{ params: { customerId } },
)
return unwrapApiResponse(response)
}
export async function downloadLastMileOrders(query: Partial<LastMileOrderQuery>) {
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<Blob, Blob>('/platform/v1/last-leg/orders/export', { params, responseType: 'blob' })
if (!blob.type.toLowerCase().includes('json')) return blob
let error: Partial<ApiEnvelope<unknown>>
try {
error = JSON.parse(await blob.text()) as Partial<ApiEnvelope<unknown>>
} 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<LastMileFeeDto>, ApiEnvelope<LastMileFeeDto>>(
`/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<number>, ApiEnvelope<number>>(
'/platform/v1/last-leg/orders/destination', payload,
)
return unwrapApiResponse(response)
}
export async function listContainerPlanProgress(query: ContainerPlanProgressQuery): Promise<OverseasPageResult<ContainerPlanProgressSummaryDto>> {
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<ContainerPlanProgressSummaryDto>>, ApiEnvelope<OverseasPagePayload<ContainerPlanProgressSummaryDto>>>(
'/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<ContainerPlanProgressDetailDto>, ApiEnvelope<ContainerPlanProgressDetailDto>>(
`/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<InboundPlanDetailDto>, ApiEnvelope<InboundPlanDetailDto>>(
'/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<HoldLastMileOrderResponse>, ApiEnvelope<HoldLastMileOrderResponse>>(
`/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<ManualNodesResponse>, ApiEnvelope<ManualNodesResponse>>(
`/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<ManualNodesResponse>, ApiEnvelope<ManualNodesResponse>>(
`/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<ContainerPlanProgressSummaryDto>, ApiEnvelope<ContainerPlanProgressSummaryDto>>(
`/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<ContainerInboundPlanPreviewDto>, ApiEnvelope<ContainerInboundPlanPreviewDto>>(
'/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<InboundPlanDetailDto[]>, ApiEnvelope<InboundPlanDetailDto[]>>(
'/platform/v1/last-leg/inbound-plans/convert-container', payload,
{ headers: { 'Idempotency-Key': idempotencyKey } },
)
return unwrapApiResponse(response)
}