Merge origin/dev into release/test preserving current workflows

This commit is contained in:
2026-09-14 11:05:47 +08:00
18 changed files with 1362 additions and 211 deletions
@@ -12,8 +12,8 @@ export interface DispatchQuery {
}
export interface DispatchPalletCandidateDto {
planId: string
planPalletId: string
planId?: string | null
planPalletId?: string | null
warehousePalletId?: string | null
orderId: string
customerId?: string | null
@@ -36,8 +36,9 @@ export interface DispatchPalletCandidateDto {
fbaWarehouseCode?: string | null
packageCount?: number | null
volumeCbm?: number | null
sourceStage?: 'PLAN' | 'RECEIVED' | null
sourceStage?: 'PLAN' | 'RECEIVED' | 'RELABEL' | null
cartonMarks?: string | null
relabelCartonMarks?: string | null
}
export interface DispatchCandidateQuery {
@@ -57,6 +58,7 @@ export interface PreArrangeDispatchPayload {
fbaAppointmentStatus?: string
vehicleAppointmentStatus?: string
draftToken?: string
relabelNo?: string
}
export interface DispatchSummaryDto {
@@ -166,6 +168,13 @@ export async function cancelDispatch(dispatchNo: string) {
return unwrapApiResponse(response)
}
export async function removeDispatchItem(dispatchNo: string, itemId: string) {
const response = await overseasHttp.delete<ApiEnvelope<DispatchSummaryDto>, ApiEnvelope<DispatchSummaryDto>>(
`/platform/v1/last-leg/dispatches/${encodeURIComponent(dispatchNo)}/items/${encodeURIComponent(itemId)}`,
)
return unwrapApiResponse(response)
}
export async function listDispatchCandidates(query: DispatchCandidateQuery) {
const response = await overseasHttp.get<ApiEnvelope<DispatchPalletCandidateDto[]>, ApiEnvelope<DispatchPalletCandidateDto[]>>(
'/platform/v1/last-leg/dispatches/candidates',
@@ -25,6 +25,7 @@ export interface LastMileOrderQuery {
page: number
size: number
orderNo?: string
relabelNo?: string
orderType?: string
inboundCode?: string
lxCode?: string
@@ -77,6 +78,7 @@ export interface LastMileOrderSummaryDto {
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
@@ -0,0 +1,28 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { overseasHttp } from '@/shared/api/overseasHttp'
import { getWarehouseTransfer, listWarehouseTransfers, reviewWarehouseTransfer } from './warehouseTransfers'
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn() } }))
describe('warehouseTransfers API', () => {
beforeEach(() => vi.clearAllMocks())
it('always scopes platform list and detail requests to the selected customer', async () => {
vi.mocked(overseasHttp.get).mockResolvedValueOnce({ code: '200', data: { records: [], page: 1, size: 10, total: 0, pages: 0 } })
vi.mocked(overseasHttp.get).mockResolvedValueOnce({ code: '200', data: { transferNo: 'WT-1', items: [], cartons: [], relabelBindings: [] } })
await listWarehouseTransfers('20001', { page: 1, size: 10, status: 'PENDING_REVIEW' })
await getWarehouseTransfer('20001', 'WT-1')
expect(vi.mocked(overseasHttp.get).mock.calls[0]).toEqual(['/platform/v1/last-leg/warehouse-transfers', { params: { page: 1, size: 10, status: 'PENDING_REVIEW', customerId: '20001' } }])
expect(vi.mocked(overseasHttp.get).mock.calls[1]).toEqual(['/platform/v1/last-leg/warehouse-transfers/WT-1', { params: { customerId: '20001' } }])
})
it('posts the real review decision with customer scope', async () => {
vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', data: { transferNo: 'WT-1', items: [], cartons: [], relabelBindings: [] } })
await reviewWarehouseTransfer('20001', 'WT-1', 'APPROVE', '资料无误')
expect(overseasHttp.post).toHaveBeenCalledWith(
'/platform/v1/last-leg/warehouse-transfers/WT-1/review',
{ decision: 'APPROVE', remark: '资料无误' },
{ params: { customerId: '20001' } },
)
})
})
@@ -0,0 +1,117 @@
import { overseasHttp } from '@/shared/api/overseasHttp'
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders'
export interface WarehouseTransferQuery {
page: number
size: number
transferNo?: string
operatingWarehouseId?: string
fbaWarehouseCode?: string
status?: string
needRelabel?: boolean
}
export interface WarehouseTransferSummaryDto {
id: string
transferNo: string
inboundCode?: string | null
lastlegOrderId?: string | null
lastlegOrderNo?: string | null
operatingWarehouseId?: string | null
fbaWarehouseCode?: string | null
packageCount: number
deliveryMethod?: string | null
needRelabel: boolean
relabelNo?: string | null
status: string
estimatedArrivalAt?: string | null
createdAt?: string | null
}
export interface WarehouseTransferItemDto {
id: string
skuCode: string
productNameZh: string
packageCount: number
shippingMark?: string | null
}
export interface WarehouseTransferCartonDto {
cartonMark?: string | null
customerCartonMark?: string | null
cartonIndex: number
totalCartons: number
status: string
}
export interface WarehouseTransferRelabelBindingDto {
operationType: string
customerCartonMark: string
originalSku: string
newSku?: string | null
quantity: number
fbaCartonMark?: string | null
destinationWarehouseCode?: string | null
remark?: string | null
}
export interface WarehouseTransferPalletDto {
id: string
sequenceNo: number
palletNo?: string | null
cartonCount: number
grossWeightKg: number
volumeCbm: number
status: string
}
export interface WarehouseTransferDetailDto extends WarehouseTransferSummaryDto {
inboundOrderId?: string | null
orderType?: string | null
orderStatus?: string | null
relabelApplicationId?: string | null
relabelStatus?: string | null
deliveryAddress?: string | null
estimatedShipAt?: string | null
fbaShipmentNo?: string | null
trackingNo?: string | null
fbaBoxNos?: string | null
remark?: string | null
items: WarehouseTransferItemDto[]
cartons: WarehouseTransferCartonDto[]
pallets: WarehouseTransferPalletDto[]
relabelBindings: WarehouseTransferRelabelBindingDto[]
customerConfirmedAt?: string | null
actualShipAt?: string | null
}
export async function listWarehouseTransfers(customerId: string, query: WarehouseTransferQuery) {
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<WarehouseTransferSummaryDto>>, ApiEnvelope<OverseasPagePayload<WarehouseTransferSummaryDto>>>(
'/platform/v1/last-leg/warehouse-transfers',
{ params: { ...query, customerId } },
)
return normalizeOverseasPage(unwrapApiResponse(response))
}
export async function getWarehouseTransfer(customerId: string, transferNo: string) {
const response = await overseasHttp.get<ApiEnvelope<WarehouseTransferDetailDto>, ApiEnvelope<WarehouseTransferDetailDto>>(
`/platform/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}`,
{ params: { customerId } },
)
return unwrapApiResponse(response)
}
export async function reviewWarehouseTransfer(
customerId: string,
transferNo: string,
decision: 'APPROVE' | 'REJECT',
remark?: string,
) {
const response = await overseasHttp.post<ApiEnvelope<WarehouseTransferDetailDto>, ApiEnvelope<WarehouseTransferDetailDto>>(
`/platform/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/review`,
{ decision, remark },
{ params: { customerId } },
)
return unwrapApiResponse(response)
}