import test from 'node:test' import assert from 'node:assert/strict' import { verifyFleetExternalReceipt } from './fleet-external-receipt-acceptance.mjs' function fixture(fault = '') { const token = 'offline-secret-pda-token' const customerId = '900719925474099301' const tripProof = { tripNo: 'T-RECEIPT', warehouseId: '92', stops: [{ name: '起点' }, { name: 'ONT8', destinationKey: 'FBA:ONT8' }] } const baseline = [{ fromIndex: 0, toIndex: 1, occupied: { palletSpaces: 1, weightKg: 100, volumeCbm: 1 }, remaining: { palletSpaces: 29, weightKg: 17900, volumeCbm: 89 } }] const finalSegments = [{ fromIndex: 0, toIndex: 1, occupied: { palletSpaces: 2, weightKg: 200, volumeCbm: 2 }, remaining: { palletSpaces: 28, weightKg: 17800, volumeCbm: 88 } }] const reservedSegments = [{ fromIndex: 0, toIndex: 1, occupied: { palletSpaces: 3, weightKg: 150, volumeCbm: 1.5 }, remaining: { palletSpaces: 27, weightKg: 17850, volumeCbm: 88.5 } }] const calls = [], assertions = [], commands = new Map() let tripVersion = 10, booking, pallet, taskStatus = 'WAIT_PDA_RECEIVING', stock = 0, ledgers = [] const ok = (data, message = '') => ({ http: 200, body: { code: message ? 'A0409' : '200', message, data: structuredClone(data) }, requestId: 'req', traceId: 'trace' }) const result = replayed => ok({ tripRecordVersion: tripVersion, booking: { ...booking, updatedAt: replayed ? '2026-10-02T21:55:52.334Z' : '2026-10-02T21:55:52.334272005Z' }, replayed }) const task = () => ({ transferNo: 'WT-NEW', inboundCode: 'WT-NEW', operatingWarehouseId: fault === 'foreign-warehouse' ? '93' : '92', status: taskStatus, pallets: pallet ? [pallet] : [] }) const evidence = () => ({ status: taskStatus, warehouseId: '92', destinationKey: 'FBA:ONT8', incomingNo: 'WT-NEW', inboundCode: 'WT-NEW', receiptHash: 'a'.repeat(64), receivedPackages: pallet?.status === 'COMPLETED' ? 1 : 0, pallets: pallet?.status === 'COMPLETED' ? [{ warehousePalletId: pallet.warehousePalletId, palletNo: pallet.palletNo, packages: 1, weightKg: 100, volumeCbm: 1 }] : [] }) const ledger = () => ({ id: 'ledger-1', customerId, overseasWarehouseId: '92', inboundCode: 'WT-NEW', palletNo: 'PLT-NEW', businessOrderNo: 'WT-NEW', beforePackages: 0, deltaPackages: 1, afterPackages: 1 }) const context = { marker: 'TEST-RECEIPT', createdResources: { fleetTrips: [tripProof] }, assert(module, name, response, passed, details, options) { assertions.push({ name, passed, options }) }, async requestMultipart(method, path, file, auth) { assert.equal(auth, token); assert.equal(file.type, 'image/png'); assert.ok(file.name.includes('TEST-MEASUREMENT')) calls.push({ method, path }); return ok({ fileId: fault === 'missing-file' ? null : '901', sizeBytes: file.content.length }) }, async request(method, path, body, auth) { calls.push({ method, path, body: structuredClone(body) }) if (path.includes('/inventory/')) { const url = new URL(path, 'https://test.invalid') assert.equal(url.searchParams.get('customerId'), customerId) const balance = { id: '900719925474099900', customerId, overseasWarehouseId: '92', inboundCode: 'WT-NEW', availablePackages: stock, holdPackages: 0, allocatedPackages: 0, pickedPackages: 0, receivingPackages: 0, inTransitPackages: 0, procurementPackages: 0 } if (url.pathname.endsWith('/positions')) return ok([{ id: 'pos-1', balanceId: balance.id, customerId, overseasWarehouseId: '92', inboundCode: 'WT-NEW', palletId: '900719925474099311', palletNo: 'PLT-NEW', availablePackages: stock }]) if (url.pathname.endsWith('/balances/900719925474099900')) return ok({ ...balance, inboundCode: fault === 'wrong-balance-source' ? 'WT-OTHER' : balance.inboundCode }) assert.equal(url.searchParams.get('overseasWarehouseId'), '92') assert.equal(url.searchParams.get('inboundCode'), 'WT-NEW') const { inboundCode: omitted, ...group } = balance const rows = path.includes('/ledger?') ? ledgers : stock ? [group] : [] return ok({ records: rows, total: rows.length }) } if (path.endsWith('/capacity-plan')) return ok({ planValid: true, blockReason: null, recordVersion: tripVersion, cargo: [{ customerId, destinationKey: 'FBA:ONT8', warehousePalletId: '123' }], availableSegments: booking?.status === 'RECEIVED' && fault !== 'keeps-reservation' ? finalSegments : booking?.status === 'CONFIRMED' ? reservedSegments : baseline }) if (path.startsWith('/api/pda/')) { assert.equal(auth, token) if (method === 'GET') return ok(task()) if (commands.has(body.idempotencyKey)) { if (fault === 'pda-replay-stock' && path.endsWith('/palletize')) { stock++; ledgers.push({ ...ledger(), id: 'ledger-2' }) } return ok(task()) } commands.set(body.idempotencyKey, true) if (path.endsWith('/pallets/drafts')) pallet = { id: 'pallet-record', status: 'DRAFT', cartonCount: body.cartonCount } else if (path.endsWith('/generate')) Object.assign(pallet, { status: 'GENERATED', warehousePalletId: '900719925474099311', palletNo: 'PLT-NEW' }) else if (path.endsWith('/bind')) { assert.equal(body.palletCode, pallet.palletNo); pallet.status = 'BOUND' } else if (path.endsWith('/palletize')) { assert.equal(pallet.status, 'BOUND'); assert.deepEqual(body.photoFileIds, ['901']) pallet.status = 'COMPLETED'; taskStatus = 'WAIT_CUSTOMER_CONFIRMATION' if (fault !== 'no-stock') { stock = 1; ledgers = [ledger()] } } else throw new Error(`unexpected PDA path ${path}`) return ok(task()) } if (method === 'GET' && path.endsWith('/incoming')) return ok({ tripRecordVersion: tripVersion, recordVersion: booking.recordVersion, incoming: evidence() }) if (method === 'GET' && path.endsWith('/bookings')) { if (fault === 'persisted-read') throw Error('read interrupted after successful confirmation') return ok({ tripRecordVersion: tripVersion, records: [{ ...booking, updatedAt: '2026-10-02T21:55:52.334Z' }] }) } if (commands.has(body.idempotencyKey)) return result(true) assert.equal(body.tripRecordVersion, tripVersion) if (path.endsWith('/bookings')) booking = { bookingNo: 'FB-NEW', customerId, status: 'DRAFT', recordVersion: 0, cargo: body.cargo } else if (path.endsWith('/confirm')) { booking.status = 'CONFIRMED'; booking.agreedAmount = 50; booking.recordVersion++ } else if (path.endsWith('/incoming')) { booking.incomingNo = 'WT-NEW'; booking.recordVersion++ } else if (path.endsWith('/receipt-confirmation')) { if (taskStatus === 'WAIT_PDA_RECEIVING') return ok(null, '仓库尚未完成实收及测量') assert.equal(body.receiptHash, 'a'.repeat(64)); assert.equal(body.pallets[0].warehousePalletId, pallet.warehousePalletId) booking.status = 'RECEIVED'; booking.agreedAmount = fault === 'old-price' ? 50 : 30; booking.recordVersion++ booking.cargo = { ...booking.cargo, palletSpaces: 1, weightKg: 100, volumeCbm: 1, unitPrice: 30 } taskStatus = 'READY_FOR_DISPATCH'; booking.receipt = { source: evidence() } if (fault === 'confirmation-stock') { stock++; ledgers.push({ ...ledger(), id: 'ledger-3' }) } } else throw new Error(`unexpected platform path ${path}`) tripVersion++; commands.set(body.idempotencyKey, true) return result(false) }, } return { context, tripProof, token, calls, assertions, clearFault() { fault = '' } } } test('external receipt proof drives real PDA command chain and checks quantity/price/reservation separately', async () => { const f = fixture() const proof = await verifyFleetExternalReceipt(f.context, 'fleet-receipt', f.tripProof, f.token) assert.equal(proof.status, 'VERIFIED') assert.equal(proof.finalBooking.status, 'RECEIVED') assert.equal(proof.finalBooking.agreedAmount, 30) assert.equal(proof.finalInventory.ledger.length, 1) assert.ok(f.assertions.every(row => row.passed && row.options.deploymentBlocker)) assert.ok(!JSON.stringify(proof).includes(f.token)) assert.ok(f.calls.every(row => !/transfer-delivery|departure|cancel/.test(row.path))) await assert.rejects(() => verifyFleetExternalReceipt(f.context, 'fleet-receipt', f.tripProof, f.token), /保留原证据/) }) test('reviewed palletized sample continues with the original PDA key and never recreates or rereceives cargo', async () => { const f = fixture('wrong-balance-source') await assert.rejects(() => verifyFleetExternalReceipt(f.context, 'fleet-receipt', f.tripProof, f.token), /余额与位置确属/) const original = structuredClone(f.tripProof.externalReceipt.palletizeRequest) const priorCalls = f.calls.length assert.equal(f.tripProof.externalReceipt.receiptRequest, undefined) f.clearFault() const proof = await verifyFleetExternalReceipt(f.context, 'fleet-receipt', f.tripProof, f.token, { reconcilePalletized: true }) const writes = f.calls.slice(priorCalls).filter(row => row.method !== 'GET') assert.deepEqual(writes.map(row => row.path.split('/').at(-1)), ['palletize', 'receipt-confirmation', 'receipt-confirmation']) assert.deepEqual(writes[0].body, original) assert.equal(proof.status, 'VERIFIED') assert.equal(proof.finalInventory.balances[0].availablePackages, 1) assert.equal(proof.finalInventory.positions[0].availablePackages, 1) assert.equal(proof.finalInventory.ledger.length, 1) assert.equal(proof.finalBooking.status, 'RECEIVED') await assert.rejects(() => verifyFleetExternalReceipt(f.context, 'fleet-receipt', f.tripProof, f.token, { reconcilePalletized: true }), /仅核对/) }) test('confirmed receipt reconciliation replays only the saved confirmation and preserves persisted timestamp and stock', async () => { const f = fixture('persisted-read') await assert.rejects(() => verifyFleetExternalReceipt(f.context, 'fleet-receipt', f.tripProof, f.token), /read interrupted/) const request = structuredClone(f.tripProof.externalReceipt.receiptRequest), previous = f.calls.length f.clearFault() const proof = await verifyFleetExternalReceipt(f.context, 'fleet-receipt', f.tripProof, f.token, { reconcileConfirmed: true }) const writes = f.calls.slice(previous).filter(row => row.method !== 'GET') assert.equal(writes.length, 2) assert.ok(writes.every(row => row.path.endsWith('/receipt-confirmation'))) for (const row of writes) assert.deepEqual(row.body, request) assert.equal(proof.finalInventory.ledger.length, 1) assert.equal(proof.finalInventory.balances[0].availablePackages, 1) assert.equal(proof.status, 'VERIFIED') }) for (const fault of ['foreign-warehouse', 'missing-file', 'no-stock', 'pda-replay-stock', 'confirmation-stock', 'keeps-reservation', 'old-price']) { test(`external receipt helper blocks ${fault} instead of treating status as inventory proof`, async () => { const f = fixture(fault) await assert.rejects(() => verifyFleetExternalReceipt(f.context, 'fleet-receipt', f.tripProof, f.token)) assert.equal(f.tripProof.externalReceipt.status, 'CHECKING') assert.ok(f.assertions.some(row => !row.passed)) }) }