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) } })