123 lines
8.1 KiB
JavaScript
123 lines
8.1 KiB
JavaScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { receiveLooseboardPool } from './looseboard-pool-receipt.mjs'
|
|
|
|
function fixture(fault = '') {
|
|
const cargo = { destinationKey: 'FBA:TEST', packageCount: 3, unitPrice: 25 }
|
|
const tripProof = { tripNo: 'TR-1', warehouseId: '42' }
|
|
const demand = { marker: 'fixture', orderNo: 'LB-1', customerId: '101', poolBinding: {
|
|
tripNo: 'TR-1', bookingNo: 'BK-1', status: 'CONFIRMED_ORIGINAL_ORDER', createRequest: { cargo, expectedPalletCount: 2 } } }
|
|
const booking = { bookingNo: 'BK-1', tripNo: 'TR-1', customerId: '101', status: 'CONFIRMED', recordVersion: 1, cargo }
|
|
const pallets = [], calls = [], snapshots = []; let incomingWrites = 0, receiptWrites = 0, palletized = false
|
|
const cartons = [1, 2, 3].map(index => ({ cartonMark: `BX-${index}`, transferItemId: '81', status: 'EXPECTED' }))
|
|
const source = () => ({ incomingNo: 'IN-1', warehouseId: fault === 'warehouse' ? '99' : '42', destinationKey: cargo.destinationKey,
|
|
status: palletized ? 'WAIT_CUSTOMER_CONFIRMATION' : 'WAIT_PDA_RECEIVING', receiptHash: 'hash',
|
|
receivedPackages: fault === 'quantity' ? 4 : 3,
|
|
pallets: palletized ? pallets.map(p => ({ warehousePalletId: p.warehousePalletId, palletNo: p.palletNo, packages: p.cartonCount })) : [] })
|
|
const context = { marker: 'fixture', createdResources: { phase4OverseasBusiness: { looseboard: demand }, fleetTrips: [tripProof] },
|
|
assert(_m, name, _r, ok) { assert.ok(ok, name) }, apiSuccess() {},
|
|
async checkpoint() { if (fault === 'disk') throw Error('disk'); snapshots.push(structuredClone(demand.receipt)) },
|
|
async request(method, path, body, token) {
|
|
calls.push({ method, path, body: structuredClone(body) })
|
|
let data
|
|
if (path.endsWith('/bookings')) data = { tripRecordVersion: 1, records: [booking] }
|
|
else if (path.endsWith('/incoming') && method === 'POST') {
|
|
incomingWrites += 1
|
|
if (fault === 'network') return { http: 0 }
|
|
data = { tripRecordVersion: 2, booking: { ...booking, incomingNo: 'IN-1' }, replayed: incomingWrites > 1 }
|
|
if (fault === 'replay' && incomingWrites > 1) data.booking.incomingNo = 'IN-2'
|
|
} else if (path.endsWith('/fulfillment')) data = { preview: { tripRecordVersion: 2, recordVersion: 2, incoming: source() } }
|
|
else if (path.endsWith('/pallets/drafts')) {
|
|
assert.equal(token, 'pda'); assert.deepEqual(Object.keys(body), ['idempotencyKey'])
|
|
pallets.push({ id: String(pallets.length + 1), cartonCount: 0, status: 'DRAFT' })
|
|
data = { pallets }
|
|
} else if (path.endsWith('/generate')) {
|
|
const pallet = pallets.find(p => path.includes(`/pallets/${p.id}/`))
|
|
Object.assign(pallet, { palletNo: `P-${pallet.id}`, warehousePalletId: String(70 + Number(pallet.id)), status: 'GENERATED' })
|
|
data = { pallets }
|
|
} else if (path.endsWith('/bind')) { pallets.find(p => path.includes(`/pallets/${p.id}/`)).status = 'BOUND'; data = { pallets } }
|
|
else if (path.endsWith('/cartons/count')) {
|
|
assert.equal(token, 'pda'); assert.equal(body.forecastPackageCount, 3)
|
|
assert.equal(body.boxes.length, 3)
|
|
assert.ok(pallets.every(row => row.status === 'BOUND'))
|
|
if (fault !== 'uncounted') {
|
|
for (const input of body.boxes) Object.assign(cartons.find(box => box.cartonMark === input.cartonMark), input, { status: 'COUNTED' })
|
|
for (const pallet of pallets) {
|
|
const boxes = body.boxes.filter(box => box.transferPalletId === pallet.id)
|
|
Object.assign(pallet, { cartonCount: boxes.length, volumeCbm: boxes.reduce((sum, box) => sum + box.length * box.width * box.height / 1000000, 0),
|
|
grossWeightKg: boxes.reduce((sum, box) => sum + box.weight, 0) })
|
|
}
|
|
}
|
|
data = { pallets, cartons }
|
|
}
|
|
else if (path.endsWith('/palletize')) {
|
|
assert.ok(cartons.every(box => box.status === 'COUNTED'))
|
|
assert.deepEqual(body.photoFileIds, ['9'])
|
|
palletized = true; pallets.forEach(p => { p.status = 'COMPLETED' })
|
|
data = { status: 'WAIT_CUSTOMER_CONFIRMATION', pallets }
|
|
} else if (path.endsWith('/receipt-confirmation')) {
|
|
receiptWrites += 1
|
|
assert.deepEqual(body.pallets, [{ warehousePalletId: '71', palletSpaces: 1 }, { warehousePalletId: '72', palletSpaces: 1 }])
|
|
assert.equal(body.unitPrice, 25)
|
|
data = { tripRecordVersion: 3, booking: { ...booking, status: 'RECEIVED', incomingNo: 'IN-1',
|
|
receipt: { source: { ...source(), status: 'READY_FOR_DISPATCH' } } }, replayed: receiptWrites > 1 }
|
|
} else if (method === 'GET' && path.includes('/warehouse-transfers/')) data = { operatingWarehouseId: '42', packageCount: 3,
|
|
items: [{ id: '81', skuCode: 'DECLARED-SKU' }], cartons, pallets }
|
|
else throw Error(`Unexpected ${method} ${path}`)
|
|
if (path.includes('/warehouse-transfers/')) data.transferNo = fault === 'wrong incoming' ? 'OTHER' : 'IN-1'
|
|
return { http: 200, body: { code: '200', data: structuredClone(data) }, requestId: 'req', traceId: 'trace' }
|
|
},
|
|
async requestMultipart(method, path, file, token) {
|
|
assert.equal(token, 'pda'); assert.equal(file.type, 'image/png'); assert.ok(file.content.length > 0)
|
|
calls.push({ method, path })
|
|
return { http: 200, body: { code: '200', data: { fileId: fault === 'file' ? null : '9' } } }
|
|
} }
|
|
return { context, calls, snapshots, counts: () => ({ incomingWrites, receiptWrites }),
|
|
args: { demand, tripProof, pdaToken: 'pda', measurements: [2, 1].map(cartonCount => ({ cartonCount, lengthCm: 100, widthCm: 100,
|
|
heightCm: 90, volumeCbm: 0.9, grossWeightKg: 40,
|
|
boxes: cartonCount === 2 ? [{ length: 100, width: 100, height: 45, weight: 20, quantity: 1 }, { length: 100, width: 100, height: 45, weight: 20, quantity: 1 }]
|
|
: [{ length: 100, width: 100, height: 90, weight: 40, quantity: 1 }] })) } }
|
|
}
|
|
|
|
test('original looseboard booking receives two pallets three boxes and confirms once with replay', async () => {
|
|
const f = fixture()
|
|
const result = await receiveLooseboardPool(f.context, f.args)
|
|
assert.equal(result.status, 'RECEIVED_READY_FOR_DISPATCH')
|
|
assert.deepEqual(result.pallets.map(p => p.packages), [2, 1])
|
|
assert.deepEqual(f.counts(), { incomingWrites: 2, receiptWrites: 2 })
|
|
assert.equal(JSON.stringify(f.snapshots).includes('pdaToken'), false)
|
|
await assert.rejects(receiveLooseboardPool(f.context, f.args))
|
|
})
|
|
|
|
for (const native of [true, false]) test(`native Long receipt versions remain bounded: ${native ? 'encoded strings' : 'invalid unsafe value'}`, async () => {
|
|
const f = fixture(), request = f.context.request
|
|
f.context.request = async (...args) => {
|
|
const response = await request(...args)
|
|
response.body.data = JSON.parse(JSON.stringify(response.body.data, (key, value) =>
|
|
/^(recordVersion|tripRecordVersion)$/.test(key) ? native ? String(value) : '9007199254740992' : value))
|
|
return response
|
|
}
|
|
if (native) assert.equal((await receiveLooseboardPool(f.context, f.args)).status, 'RECEIVED_READY_FOR_DISPATCH')
|
|
else { await assert.rejects(receiveLooseboardPool(f.context, f.args), /来货前/); assert.equal(f.counts().incomingWrites, 0) }
|
|
})
|
|
|
|
for (const fault of ['network', 'replay', 'warehouse', 'quantity', 'file', 'disk', 'wrong incoming', 'uncounted']) {
|
|
test(`receipt stops on ${fault} before operational confirmation`, async () => {
|
|
const f = fixture(fault)
|
|
await assert.rejects(receiveLooseboardPool(f.context, f.args))
|
|
assert.equal(f.args.demand.receipt.status, 'FAILED')
|
|
assert.equal(f.counts().receiptWrites, 0)
|
|
if (fault === 'network') assert.equal(f.counts().incomingWrites, 1)
|
|
if (fault === 'disk') assert.equal(f.calls.length, 0)
|
|
if (fault === 'uncounted') assert.equal(f.calls.some(call => call.path.endsWith('/palletize')), false)
|
|
})
|
|
}
|
|
|
|
test('wrong batch and inconsistent measured totals are blocked before HTTP', async () => {
|
|
for (const change of [f => { f.args.demand.marker = 'other' }, f => { f.args.measurements[0].cartonCount = 3 }]) {
|
|
const f = fixture(); change(f)
|
|
await assert.rejects(receiveLooseboardPool(f.context, f.args))
|
|
assert.equal(f.calls.length, 0)
|
|
}
|
|
})
|