Files
baiyue-firstleg/scripts/admin-test-env-modules/fleet-external-receipt-acceptance-contract.test.mjs
T

151 lines
11 KiB
JavaScript

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