feat(transfer): expose stock outbound and durable receipt recovery UI
This commit is contained in:
@@ -1,11 +1,27 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { http } from '@/shared/api/http'
|
||||
import { allocateWarehouseTransfer, getWarehouseTransfer, listWarehouseTransfers, reviewWarehouseTransfer } from './warehouseTransfers'
|
||||
import { allocateWarehouseTransfer, getWarehouseTransfer, listWarehouseTransfers, reviewWarehouseTransfer, outboundWarehouseTransfer, receiveWarehouseTransfer, listTransferReceivingWarehouses } from './warehouseTransfers'
|
||||
|
||||
vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: { get: vi.fn(), post: vi.fn() } }))
|
||||
|
||||
describe('warehouseTransfers API', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
it('uses the resource-role receiving options contract, never warehouse master list', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({ code: '200', data: { records: [{ id: 'NY', customerId: 0, warehouseCode: 'NY', warehouseName: '纽约', warehouseType: 'OVERSEAS', status: 'ACTIVE' }], total: 1, page: 1, size: 200, pages: 1 } })
|
||||
expect((await listTransferReceivingWarehouses()).records[0]?.customerId).toBe('0')
|
||||
expect(http.get).toHaveBeenCalledExactlyOnceWith('/platform/v1/warehouse-admin/overseas-receiving-options', { params: { page: 1, size: 200 } })
|
||||
})
|
||||
it('keeps outbound and receipt original bodies and customer scope without inventing receipt rows', async () => {
|
||||
vi.mocked(http.post).mockResolvedValue({ code: '200', data: { transferNo: 'WT/1', packageCount: '2', status: 'PARTIALLY_RECEIVED', allocations: [] } })
|
||||
const outbound = { idempotencyKey: 'out-original' }
|
||||
await outboundWarehouseTransfer('customer', 'WT/1', outbound)
|
||||
expect(http.post).toHaveBeenLastCalledWith('/platform/v1/last-leg/warehouse-transfers/WT%2F1/outbound', outbound, { params: { customerId: 'customer' } })
|
||||
const receipt = { sourceInventoryPositionId: 'position', destinationOverseasWarehouseId: 'NY', destinationOverseasWarehouseCode: 'NY-CODE', destinationOverseasWarehouseName: '纽约仓', destinationWarehouseCode: 'ABE4', destinationPalletId: 'pallet', destinationPalletNo: 'PALLET-NY', warehouseLocationId: 'location', warehouseLocationCode: 'LOC-NY', batchNo: 'WT/1', idempotencyKey: 'receipt-original' }
|
||||
await receiveWarehouseTransfer('customer', 'WT/1', receipt)
|
||||
await receiveWarehouseTransfer('customer', 'WT/1', receipt)
|
||||
expect(vi.mocked(http.post).mock.calls[1]).toEqual(vi.mocked(http.post).mock.calls[2])
|
||||
expect(http.post).toHaveBeenLastCalledWith('/platform/v1/last-leg/warehouse-transfers/WT%2F1/receipts', receipt, { params: { customerId: 'customer' } })
|
||||
})
|
||||
|
||||
it('normalizes detail, review and allocation box counts at the API boundary', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({ code: '200', data: { transferNo: 'WT-1', packageCount: '1' } })
|
||||
|
||||
@@ -2,6 +2,7 @@ import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
|
||||
import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders'
|
||||
import { normalizeWarehouseTransferQuantity } from './warehouseTransferQuantities'
|
||||
import type { ReceivingOverseasWarehouseDto } from './pallets'
|
||||
|
||||
export interface WarehouseTransferQuery {
|
||||
page: number
|
||||
@@ -85,8 +86,20 @@ export interface WarehouseTransferDetailDto extends WarehouseTransferSummaryDto
|
||||
relabelBindings: WarehouseTransferRelabelBindingDto[]
|
||||
customerConfirmedAt?: string | null
|
||||
actualShipAt?: string | null
|
||||
destinationWarehouseId?: string | null
|
||||
destinationWarehouseCode?: string | null
|
||||
destinationWarehouseName?: string | null
|
||||
sourceOrders?: Array<{ lastlegOrderId: string; lastlegOrderNo?: string | null; inboundOrderId: string; inboundCode?: string | null }>
|
||||
allocations?: Array<{ inventoryPositionId: string; inboundOrderId: string; inboundCode?: string | null; palletId: string; palletNo?: string | null; packageCount: number; status: string; destinationInventoryPositionId?: string | null; skuCode?: string | null }>
|
||||
receiptCommands?: WarehouseTransferReceiptCommandDto[]
|
||||
}
|
||||
|
||||
export interface WarehouseTransferReceiptCommandDto {
|
||||
sourceInventoryPositionId: string
|
||||
status: 'PREPARED' | 'RESERVED' | 'COMPLETED'
|
||||
requestHash: string
|
||||
request: ReceiveWarehouseTransferPayload
|
||||
target: { warehouseId: string; warehouseCode: string; warehouseName: string; palletId: string; palletNo: string; locationId: string; locationCode: string; fbaId?: string | null; fbaCode: string; fbaName?: string | null }
|
||||
}
|
||||
|
||||
export interface AllocateWarehouseTransferPayload {
|
||||
@@ -94,6 +107,57 @@ export interface AllocateWarehouseTransferPayload {
|
||||
allocations: Array<{ inventoryPositionId: string; palletId: string; packageCount: number }>
|
||||
}
|
||||
|
||||
export interface OutboundWarehouseTransferPayload { idempotencyKey: string; remark?: string }
|
||||
export interface ReceiveWarehouseTransferPayload {
|
||||
sourceInventoryPositionId: string
|
||||
destinationOverseasWarehouseId: string
|
||||
destinationOverseasWarehouseCode: string
|
||||
destinationOverseasWarehouseName: string
|
||||
destinationWarehouseId?: string
|
||||
destinationWarehouseCode: string
|
||||
destinationWarehouseName?: string
|
||||
destinationPalletId: string
|
||||
destinationPalletNo: string
|
||||
warehouseLocationId: string
|
||||
warehouseLocationCode: string
|
||||
batchNo: string
|
||||
idempotencyKey: string
|
||||
remark?: string
|
||||
}
|
||||
|
||||
export async function outboundWarehouseTransfer(customerId: string, transferNo: string, payload: OutboundWarehouseTransferPayload) {
|
||||
const response = await http.post<ApiEnvelope<WarehouseTransferDetailWire>, ApiEnvelope<WarehouseTransferDetailWire>>(
|
||||
`/platform/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/outbound`, payload, { params: { customerId } },
|
||||
)
|
||||
return normalizeTransferDetail(unwrapApiResponse(response))
|
||||
}
|
||||
export async function receiveWarehouseTransfer(customerId: string, transferNo: string, payload: ReceiveWarehouseTransferPayload) {
|
||||
const response = await http.post<ApiEnvelope<WarehouseTransferDetailWire>, ApiEnvelope<WarehouseTransferDetailWire>>(
|
||||
`/platform/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/receipts`, payload, { params: { customerId } },
|
||||
)
|
||||
return normalizeTransferDetail(unwrapApiResponse(response))
|
||||
}
|
||||
|
||||
export async function listTransferReceivingWarehouses() {
|
||||
const records: ReceivingOverseasWarehouseDto[] = []
|
||||
let total: number | undefined
|
||||
for (let page = 1; page <= 100; page++) {
|
||||
const response = await http.get<ApiEnvelope<OverseasPagePayload<ReceivingOverseasWarehouseDto>>, ApiEnvelope<OverseasPagePayload<ReceivingOverseasWarehouseDto>>>(
|
||||
'/platform/v1/warehouse-admin/overseas-receiving-options', { params: { page, size: 200 } },
|
||||
)
|
||||
const value = normalizeOverseasPage(unwrapApiResponse(response))
|
||||
if (total !== undefined && total !== value.total) throw new Error('收货仓列表已变化,请重新查询')
|
||||
total = value.total
|
||||
records.push(...value.records.map(w => ({ ...w, id: String(w.id), customerId: String(w.customerId) })))
|
||||
if (records.length === total) {
|
||||
if (new Set(records.map(w => w.id)).size !== records.length || records.some(w => w.customerId !== '0' || w.warehouseType !== 'OVERSEAS' || w.status !== 'ACTIVE')) throw new Error('收货仓返回范围不符合要求')
|
||||
return { records }
|
||||
}
|
||||
if (!value.records.length || records.length > total) throw new Error('收货仓分页不完整,请重新查询')
|
||||
}
|
||||
throw new Error('收货仓分页超出查询限制')
|
||||
}
|
||||
|
||||
type WarehouseTransferDetailWire = Omit<WarehouseTransferDetailDto, 'packageCount'> & { packageCount: number | string }
|
||||
function normalizeTransferDetail(value: WarehouseTransferDetailWire): WarehouseTransferDetailDto {
|
||||
return { ...value, packageCount: normalizeWarehouseTransferQuantity(value.packageCount, '本票转运箱数') }
|
||||
|
||||
Reference in New Issue
Block a user