504 lines
17 KiB
TypeScript
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)
|
|
}
|