Fix native dispatch warehouse text projection and TEST receipt setup

This commit is contained in:
2026-10-20 09:16:16 +08:00
parent d7616899d8
commit a8037fd97d
4 changed files with 143 additions and 15 deletions
@@ -76,10 +76,14 @@ function model(fault = '', countPatch = {}) {
if (path === '/api/merchant/v1/iam/tenant/profile') return response({ id: fault === 'foreign login' ? 'OTHER' : merchant.id })
if (path === '/api/merchant/v1/last-leg/warehouse-transfers') {
assert.ok(merchant); assert.equal(body.inboundCode, undefined); assert.deepEqual(body.sourceLastlegOrderIds, [])
assert.equal(body.needRelabel, false); assert.equal(body.items.length, 1); assert.equal(body.items[0].packageCount, 1)
assert.equal(body.needRelabel, false); assert.equal(body.items.length, 1); assert.equal(body.items[0].packageCount, 1); assert.equal(body.items[0].quantity, 1)
const transferNo = `IN${transfers.length}`
const itemId = `FORECAST-ITEM${transfers.length}`
const transfer = { transferNo, inboundCode: transferNo, customerId: merchant.id, operatingWarehouseId: body.operatingWarehouseId,
status: 'DRAFT', pallets: [] }
status: 'DRAFT', packageCount: 1, pallets: [],
items: [{ ...copy(body.items[0]), id: itemId, ...(fault === 'missing declared quantity' ? { quantity: null } : {}) }],
cartons: fault === 'missing forecast carton' ? [] : [{ cartonMark: `BX-FORECAST${transfers.length}`, status: 'FORECAST',
transferItemId: fault === 'foreign forecast item' ? 'OTHER-ITEM' : itemId }] }
transfers.push(transfer); return response(transfer)
}
if (path.includes('/warehouse-transfers/')) {
@@ -88,10 +92,39 @@ function model(fault = '', countPatch = {}) {
if (path.startsWith('/api/merchant/')) assert.equal(transfer.customerId, merchant.id)
if (path.endsWith('/submit')) transfer.status = 'PENDING_REVIEW'
else if (path.endsWith('/review')) { assert.equal(url.searchParams.get('customerId'), transfer.customerId); assert.equal(body.decision, 'APPROVE'); transfer.status = 'WAIT_PDA_RECEIVING' }
else if (path.endsWith('/pallets/drafts')) transfer.pallets = [{ id: `DRAFT${n}`, status: 'DRAFT', cartonCount: body.cartonCount }]
else if (path.endsWith('/pallets/drafts')) {
// The actual PalletDraft DTO accepts identity only; never derive measurement from ignored request fields.
assert.deepEqual(Object.keys(body), ['idempotencyKey'])
transfer.pallets = [{ id: `DRAFT${n}`, status: 'DRAFT', cartonCount: 0, lengthCm: 0, widthCm: 0, heightCm: 0, volumeCbm: 0, grossWeightKg: 0 }]
}
else if (path.endsWith('/generate')) Object.assign(transfer.pallets[0], { status: 'GENERATED', warehousePalletId: `PAL${n}`, palletNo: `PALLET${n}` })
else if (path.endsWith('/bind')) { assert.equal(body.palletCode, transfer.pallets[0].palletNo); transfer.pallets[0].status = 'BOUND' }
else if (path.endsWith('/palletize')) { transfer.status = 'WAIT_CUSTOMER_CONFIRMATION'; transfer.pallets[0].status = 'COMPLETED' }
else if (path.endsWith('/cartons/count')) {
assert.equal(method, 'POST'); assert.equal(transfer.pallets[0].status, 'BOUND')
assert.equal(transfer.pallets[0].cartonCount, 0); assert.equal(body.forecastPackageCount, transfer.packageCount)
assert.equal(body.boxes.length, 1)
const box = body.boxes[0], item = transfer.items[0], carton = transfer.cartons[0], pallet = transfer.pallets[0]
assert.equal(box.cartonMark, carton.cartonMark); assert.equal(box.transferItemId, item.id)
assert.equal(box.transferPalletId, pallet.id); assert.notEqual(box.transferPalletId, pallet.warehousePalletId)
assert.deepEqual([box.length, box.width, box.height, box.weight], [100, 100, 100, 100])
assert.deepEqual(box.items, [{ skuCode: item.skuCode, quantity: item.quantity }])
Object.assign(carton, { transferPalletId: pallet.id, status: 'COUNTED', countedAt: '2026-10-20T09:00:00',
length: box.length, width: box.width, height: box.height, weight: box.weight, quantity: 1,
items: [{ transferItemId: item.id, skuCode: item.skuCode, quantity: 1 }] })
Object.assign(pallet, { cartonCount: 1, volumeCbm: 1, grossWeightKg: 100 })
if (fault === 'uncounted carton') { carton.status = 'FORECAST'; carton.countedAt = null }
if (fault === 'wrong counted pallet') carton.transferPalletId = 'OTHER-PALLET'
if (fault === 'wrong counted sku') carton.items[0].skuCode = 'OTHER-SKU'
if (fault === 'missing carton measurement') carton.weight = null
if (fault === 'zero counted pallet') pallet.cartonCount = 0
if (fault === 'wrong measured pallet weight') pallet.grossWeightKg = 0
}
else if (path.endsWith('/palletize')) {
assert.equal(transfer.cartons.length, 1); assert.equal(transfer.cartons[0].status, 'COUNTED'); assert.ok(transfer.cartons[0].countedAt)
assert.equal(transfer.pallets[0].cartonCount, 1); assert.equal(transfer.pallets[0].grossWeightKg, 100)
assert.equal(transfer.pallets[0].volumeCbm, 1); assert.ok(body.photoFileIds.length)
transfer.status = 'WAIT_CUSTOMER_CONFIRMATION'; transfer.pallets[0].status = 'COMPLETED'
}
else if (path.endsWith('/confirm-measurement')) transfer.status = 'READY_FOR_DISPATCH'
else assert.equal(method, 'GET')
return response(transfer)
@@ -182,13 +215,26 @@ test('real helpers build independent current-run customers, receipts and one com
assert.equal(proof.status, 'READY'); assert.deepEqual(proof.sessionCleanup.map(row => row.status), ['LOGGED_OUT', 'LOGGED_OUT'])
assert.equal(m.trip.dispatches.length, 1); assert.equal(new Set(fixture.members.map(row => row.dispatchNo)).size, 1); assert.deepEqual(m.trip.trip.stops.map(row => row.destinationKey).filter(Boolean), ['FBA:FBA'])
assert.equal(m.calls.filter(row => row.method === 'POST' && row.raw === '/api/platform/v1/last-leg/dispatches').length, 1)
assert.equal(m.calls.filter(row => row.raw.endsWith('/cartons/count')).length, 2)
for (const member of fixture.members) {
const prefix = `/api/pda/v1/last-leg/warehouse-transfers/${member.incomingNo}`
const draft = m.calls.findIndex(row => row.raw === `${prefix}/pallets/drafts`)
const bind = m.calls.findIndex(row => row.raw.startsWith(`${prefix}/pallets/`) && row.raw.endsWith('/bind'))
const count = m.calls.findIndex(row => row.raw === `${prefix}/cartons/count`)
const photo = m.calls.findIndex(row => row.raw === `${prefix}/measurement-photos/files`)
const receipt = m.calls.findIndex(row => row.raw === `${prefix}/palletize`)
assert.ok(draft < bind && bind < count && count < photo && photo < receipt)
assert.equal(member.measurementReceipt.countedCarton.cartonMark, m.calls[count].body.boxes[0].cartonMark)
assert.equal(member.measurementReceipt.syntheticTestMaterials, true)
assert.equal(member.measurementReceipt.finalizedPallet.cartonCount, 1)
}
assert.equal(m.calls.filter(row => row.raw.endsWith('/transfer-delivery')).length, 0)
assert.ok(!m.calls.some(row => /\/pick$|\/depart$|\/bookings|\/capacity-plan/.test(row.raw)))
const persisted = JSON.stringify(m.context.createdResources)
for (const secret of ['caller-pda-secret', 'admin-secret', 'fresh-secret-', 'merchant-100-secret', 'refresh-100-secret']) assert.ok(!persisted.includes(secret))
})
for (const fault of ['disabled receiving', 'reused customer', 'foreign login', 'foreign position', 'truncated ledger', 'wrong raw scope', 'wrong raw ledger position', 'wrong raw ledger balance', 'foreign candidate', 'missing second cargo', 'foreign delivery', 'logout failure']) {
for (const fault of ['disabled receiving', 'reused customer', 'foreign login', 'missing forecast carton', 'missing declared quantity', 'foreign forecast item', 'uncounted carton', 'wrong counted pallet', 'wrong counted sku', 'missing carton measurement', 'zero counted pallet', 'wrong measured pallet weight', 'foreign position', 'truncated ledger', 'wrong raw scope', 'wrong raw ledger position', 'wrong raw ledger balance', 'foreign candidate', 'missing second cargo', 'foreign delivery', 'logout failure']) {
test(`fail closed, retain partial resources and clean owned sessions: ${fault}`, async () => {
const m = model(fault)
await assert.rejects(createFleetCrossCustomerFixture(m.context, m.options))
@@ -198,6 +244,8 @@ for (const fault of ['disabled receiving', 'reused customer', 'foreign login', '
assert.equal(logouts.length, logins.length)
assert.ok(!m.calls.some(row => row.method === 'DELETE' || row.raw.includes('/users/')))
if (fault === 'disabled receiving') assert.equal(m.calls.filter(row => row.method !== 'GET').length, 0)
if (['missing forecast carton', 'missing declared quantity', 'foreign forecast item', 'uncounted carton', 'wrong counted pallet', 'wrong counted sku', 'missing carton measurement', 'zero counted pallet', 'wrong measured pallet weight'].includes(fault))
assert.equal(m.calls.some(row => row.raw.endsWith('/palletize') || row.raw === '/api/platform/v1/last-leg/dispatches'), false)
const previousCount = m.calls.length
await assert.rejects(createFleetCrossCustomerFixture(m.context, m.options), /already started/)
assert.equal(m.calls.length, previousCount)
@@ -151,7 +151,7 @@ export async function createFleetCrossCustomerFixture(context, { execute = false
destinationWarehouseCode: destination.warehouse.warehouseCode, destinationWarehouseName: destination.warehouse.warehouseName,
deliveryAddress, packageCount: 1, volumeCbm: 1, deliveryMethod: 'CUSTOMER_DELIVERY', estimatedArrivalAt: localTime(1800000),
needRelabel: false, remark: member.customerMarker, idempotencyKey: key(`CREATE-${suffix}`),
items: [{ skuCode: `CROSS-${key(suffix).slice(0, 16)}`, productNameZh: '受控测试一箱货物', packageCount: 1 }] }
items: [{ skuCode: `CROSS-${key(suffix).slice(0, 16)}`, productNameZh: '受控测试一箱货物', packageCount: 1, quantity: 1 }] }
let incoming = await request('POST', MERCHANT, createBody, `Create new incoming ${suffix}`, auth.accessToken)
member.incomingNo = incoming?.transferNo
check('Ordinary transfer creates fresh incoming without borrowed source inventory', member.incomingNo && incoming.inboundCode === member.incomingNo
@@ -167,8 +167,14 @@ export async function createFleetCrossCustomerFixture(context, { execute = false
check('PDA task is untouched and belongs to the requested receiving warehouse', task?.transferNo === member.incomingNo
&& task.inboundCode === member.incomingNo && id(task.operatingWarehouseId) === id(warehouseId)
&& task.status === 'WAIT_PDA_RECEIVING' && task.pallets?.length === 0)
const measured = await request('POST', `${pdaPath}/pallets/drafts`, { idempotencyKey: key(`MEASURE-${suffix}`),
cartonCount: 1, lengthCm: 100, widthCm: 100, heightCm: 100, volumeCbm: 1, grossWeightKg: 100 }, 'Measure one real test pallet', pdaToken)
const forecastItem = task.items?.[0], forecastCarton = task.cartons?.[0]
check('One original forecast carton and declared SKU belong to this fresh one-box incoming', safeFixtureCount(task.packageCount) === 1
&& task.items?.length === 1 && forecastItem?.id && forecastItem.skuCode === createBody.items[0].skuCode
&& safeFixtureCount(forecastItem.packageCount) === 1 && safeFixtureCount(forecastItem.quantity) === 1
&& task.cartons?.length === 1 && forecastCarton?.cartonMark
&& (forecastCarton.transferItemId == null || id(forecastCarton.transferItemId) === id(forecastItem.id)))
// PalletDraft creates only an empty identity; measurements belong to the real carton-count command below.
const measured = await request('POST', `${pdaPath}/pallets/drafts`, { idempotencyKey: key(`MEASURE-${suffix}`) }, 'Create empty TEST pallet draft', pdaToken)
check('Measurement created one pallet draft', measured?.pallets?.length === 1 && measured.pallets[0].id && measured.pallets[0].status === 'DRAFT')
const palletPath = `${pdaPath}/pallets/${encode(measured.pallets[0].id)}`
const generated = await request('POST', `${palletPath}/generate`, { idempotencyKey: key(`GENERATE-${suffix}`) }, 'Generate authoritative warehouse pallet', pdaToken)
@@ -176,8 +182,28 @@ export async function createFleetCrossCustomerFixture(context, { execute = false
check('Generated physical pallet has real references', generated?.pallets?.length === 1 && pallet.status === 'GENERATED' && pallet.warehousePalletId && pallet.palletNo)
member.palletId = id(pallet.warehousePalletId); member.palletNo = pallet.palletNo
const bound = await request('POST', `${palletPath}/bind`, { palletCode: member.palletNo, idempotencyKey: key(`BIND-${suffix}`) }, 'Bind scanned physical pallet', pdaToken)
check('Bound pallet is exactly the measured one-box pallet', bound?.pallets?.length === 1 && bound.pallets[0].status === 'BOUND'
&& id(bound.pallets[0].warehousePalletId) === member.palletId && safeFixtureCount(bound.pallets[0].cartonCount) === 1)
check('Bound pallet is exactly the generated empty physical pallet before carton counting', bound?.pallets?.length === 1 && bound.pallets[0].status === 'BOUND'
&& id(bound.pallets[0].id) === id(measured.pallets[0].id)
&& id(bound.pallets[0].warehousePalletId) === member.palletId && safeFixtureCount(bound.pallets[0].cartonCount) === 0)
const counted = await request('POST', `${pdaPath}/cartons/count`, { idempotencyKey: key(`COUNT-${suffix}`),
forecastPackageCount: safeFixtureCount(task.packageCount), boxes: [{ cartonMark: forecastCarton.cartonMark,
transferItemId: id(forecastItem.id), transferPalletId: id(measured.pallets[0].id),
length: 100, width: 100, height: 100, weight: 100,
items: [{ skuCode: forecastItem.skuCode, quantity: safeFixtureCount(forecastItem.quantity) }] }] },
'Count original TEST carton, record dimensions/SKU and allocate it to the bound pallet', pdaToken)
const countedCarton = counted?.cartons?.[0], countedPallet = counted?.pallets?.[0]
check('Actual carton count proves one declared box/SKU on the original bound pallet', counted?.transferNo === member.incomingNo
&& counted.cartons?.length === 1 && countedCarton.cartonMark === forecastCarton.cartonMark
&& countedCarton.status === 'COUNTED' && countedCarton.countedAt
&& id(countedCarton.transferPalletId) === id(measured.pallets[0].id)
&& id(countedCarton.transferItemId) === id(forecastItem.id)
&& ['length', 'width', 'height', 'weight'].every(field => Number(countedCarton[field]) === 100)
&& safeFixtureCount(countedCarton.quantity) === 1 && countedCarton.items?.length === 1
&& countedCarton.items[0].skuCode === forecastItem.skuCode
&& id(countedCarton.items[0].transferItemId) === id(forecastItem.id) && safeFixtureCount(countedCarton.items[0].quantity) === 1
&& counted.pallets?.length === 1 && countedPallet.status === 'BOUND' && id(countedPallet.id) === id(measured.pallets[0].id)
&& id(countedPallet.warehousePalletId) === member.palletId && safeFixtureCount(countedPallet.cartonCount) === 1
&& Number(countedPallet.grossWeightKg) === 100 && Number(countedPallet.volumeCbm) === 1)
const photoPath = `${pdaPath}/measurement-photos/files`
const photo = record('POST', photoPath, undefined, await context.requestMultipart('POST', photoPath,
{ name: `${member.customerMarker}-TEST-MEASUREMENT.png`, type: 'image/png', content: testImage }, pdaToken), 'Upload TEST measurement evidence')
@@ -189,7 +215,7 @@ export async function createFleetCrossCustomerFixture(context, { execute = false
check('Customer confirmation makes ordinary incoming dispatchable', incoming?.transferNo === member.incomingNo && incoming.status === 'READY_FOR_DISPATCH')
member.receipt = await stock(member)
member.balanceId = id(member.receipt.balance.id); member.positionId = id(member.receipt.positions[0].id); member.receiptVerified = true
member.measurementReceipt = { sourceBusinessNo: member.incomingNo, syntheticTestMaterials: true,
member.measurementReceipt = { sourceBusinessNo: member.incomingNo, syntheticTestMaterials: true, countedCarton,
sourceType: 'WAREHOUSE_TRANSFER_PDA', finalizedPallet: incoming.pallets?.find(row => id(row.warehousePalletId) === member.palletId) }
const candidates = await request('GET', `${ROOT}/dispatches/candidates?${new URLSearchParams({ warehouseId: id(warehouseId), orderNo: member.incomingNo })}`,
undefined, 'Read own native received source selection')