From 4bef8577fac11bc5be80e13e38687f613e88d98d Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sun, 18 Oct 2026 19:21:12 +0800 Subject: [PATCH] test(lastleg): verify dispatch repair with current-run HTTP evidence --- scripts/admin-test-env-modules/README.md | 4 + ...spatch-repair-acceptance-contract.test.mjs | 304 ++++++++++++ .../dispatch-repair-acceptance.mjs | 434 ++++++++++++++++++ 3 files changed, 742 insertions(+) create mode 100644 scripts/admin-test-env-modules/dispatch-repair-acceptance-contract.test.mjs create mode 100644 scripts/admin-test-env-modules/dispatch-repair-acceptance.mjs diff --git a/scripts/admin-test-env-modules/README.md b/scripts/admin-test-env-modules/README.md index a5ceb318e..2fdcca5df 100644 --- a/scripts/admin-test-env-modules/README.md +++ b/scripts/admin-test-env-modules/README.md @@ -8,6 +8,10 @@ ## 当前新增脚本的执行边界 +`dispatch-repair-acceptance.mjs` 是本批排车修复的内部增量验收助手,不是独立 CLI。`verifyDispatchRepair` 默认只返回计划,显式执行要求 TEST、Lastleg/Admin 健康及 Schema 就绪、请求前后持久化检查点。`context.producerEvidence` 指定本轮生产回执 JSON 的路径及 SHA-256;文件须含 `environment=TEST`、`marker`、已部署 Lastleg 的 `sourceCommit` 和 `requests`。请求条目保留实际 `method/path/body/actor`、`status=RESPONDED`、`http/code/requestId/traceId/result`,不得保存登录凭据或 Token。助手从正式创建响应及父来源回执提取业务号、客户和仓库,形成资源白名单;所有 fixture 关联身份在首个请求前核对,不能仅声明“本轮新建”。缺样本标记 NOT_RUN,不借旧业务样本。 + +该助手核对取消出库/取消排车/释放重排、实际出库门禁、换标全托、混托两种展示来源、拆合托后重核、原箱回仓及只读权限。两种混托须使用独立排车/托盘/预订,普通与外接来源的箱数由生产实收回执确定,并回查实物仓位组成及报价数量/单价/金额。原箱限本轮单箱出库,按原始 SKU、客户、仓分页精确筛选库存及流水,以排车号匹配 OUTBOUND;先取完成前 S、首次完成后 R,再同键重放取 R′,核对本轮唯一 SKU 的完成前零库存基线及首次总量、可用展示数量、箱数恢复、receiptNo 的 EXCEPTION_RETURN_IN 精确集合、目标托盘库位及重放零增量。HTTP availableQuantity 是受实箱可用量限制的展示值,不冒称裸余额证明。只对已授权的新排车调用 DELETE 取消状态,不运行移托 DELETE 负向分支,不物理删除、SQL 造数或清理。未知结果保存原命令并停写;证据明确标为合成测试资料,不冒充真实外部预约/POD。保留 `sourceEventsVerified=false`,来源事件、人工页面及 PDA 真机仍单列。相邻 `dispatch-repair-acceptance-contract.test.mjs` 只做离线验证,未实际执行不得表述为环境通过。 + - 人工状态矩阵的 `FEE_CONFIRMING` 使用独立新订单,在测量提交成功后暂停审核,经正式 `fees/calculate` 生成费用;回查同一费用版本、入库客户身份、测量仍待审核及入仓码精确筛选结果。该状态是必验项,不再列入不可造数缺口;不发送费用、不推进该样本、不借历史数据。入口仍为 `--modules manual-state-matrix`,对应 `order-inbound-status-fixtures-contract.test.mjs` 和 `manual-state-matrix-contract.test.mjs`。 - `WAIT_PRICING` 另建保留原始 `APPROVED` 的订单,先形成未发布的 `FEE_CONFIRMING`,再通过客户实时渠道目录与报价选择同仓、同目的仓、同税制的不同渠道,以正式申报更新触发费用失效;回查 `WAIT_PRICING/UNPRICED`、新渠道及精确筛选。缺少可计价替换渠道、客户漂移或状态未收敛均失败;不写业务SQL,退出专用目录查询会话。该状态同样为必验项,原先借历史 `WAIT_SPLIT` 样本的兜底已移除。 diff --git a/scripts/admin-test-env-modules/dispatch-repair-acceptance-contract.test.mjs b/scripts/admin-test-env-modules/dispatch-repair-acceptance-contract.test.mjs new file mode 100644 index 000000000..1c67ca6b9 --- /dev/null +++ b/scripts/admin-test-env-modules/dispatch-repair-acceptance-contract.test.mjs @@ -0,0 +1,304 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { after } from 'node:test' +import { writeFileSync, unlinkSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { randomUUID, createHash } from 'node:crypto' +import { verifyDispatchRepair } from './dispatch-repair-acceptance.mjs' + +const commit = 'a'.repeat(40) +const ok = data => ({ http: 200, body: { code: '200', data: structuredClone(data) }, requestId: 'synthetic-request', traceId: 'synthetic-trace' }) +const reject = (code = 'A0409', http = 409) => ({ http, body: { code }, requestId: 'synthetic-request', traceId: 'synthetic-trace' }) +const fresh = more => ({ customerId: 'C', warehouseId: 'W', ...more }) +const evidenceFiles = [] +after(() => { for (const file of evidenceFiles) { assert.ok(file.startsWith(join(tmpdir(), 'dispatch-repair-proof-'))); unlinkSync(file) } }) +function producerReport() { + const requests = [] + const event = (method, path, body, result, actor = 'PLATFORM') => requests.push({ method, path, body, result, actor, + status: 'RESPONDED', http: 200, code: '200', requestId: `actual-producer-${requests.length}`, traceId: `actual-trace-${requests.length}` }) + function stock(no, inboundCode, pallets, relabelNo) { + event('POST', '/api/platform/v1/last-leg/warehouse-transfers?customerId=C', { operatingWarehouseId: 'W', remark: 'CURRENT new stock' }, { transferNo: no, operatingWarehouseId: 'W' }) + event('POST', `/api/pda/v1/last-leg/warehouse-transfers/${no}/palletize`, {}, { transferNo: no, operatingWarehouseId: 'W', inboundCode, relabelNo, + pallets: pallets.map(([warehousePalletId, cartonCount]) => ({ warehousePalletId, palletNo: `PAL-${warehousePalletId}`, cartonCount })) }) + } + function candidate(planPalletId, warehousePalletId, inboundCode, extra = {}) { + const row = { planPalletId, warehousePalletId, inboundCode, orderRefs: inboundCode, customerId: 'C', overseasWarehouseId: 'W', sourceStage: 'RECEIVED', ...extra } + event('GET', '/api/platform/v1/last-leg/dispatches/candidates', undefined, [row]); return row + } + function dispatch(no, rows, relabelNo, packages = 1) { + event('POST', '/api/platform/v1/last-leg/dispatches/pre-arrange', { planPalletIds: rows.map(x => relabelNo ? x.warehousePalletId : x.planPalletId), relabelNo }, + { dispatchNo: no, overseasWarehouseId: 'W', packageCount: packages, items: rows }) + } + function trip(no) { event('POST', '/api/platform/v1/last-leg/fleet/trips', { remark: 'CURRENT new trip' }, { trip: { tripNo: no, warehouseId: 'W' } }) } + function external(no, tripNo, transferNo, inboundCode, palletId, destinationPallet) { + trip(tripNo) + const booking = { bookingNo: no, tripNo, customerId: 'C', cargo: { priceBasis: 'PALLET', palletSpaces: 1, unitPrice: 2, currency: 'USD' }, agreedAmount: 2 } + event('POST', `/api/platform/v1/last-leg/fleet/trips/${tripNo}/bookings`, {}, { booking }) + event('POST', `/api/platform/v1/last-leg/fleet/trips/${tripNo}/bookings/${no}/incoming`, {}, { booking: { ...booking, incomingNo: transferNo } }) + event('POST', `/api/pda/v1/last-leg/warehouse-transfers/${transferNo}/palletize`, {}, { transferNo, inboundCode, operatingWarehouseId: 'W', pallets: [{ warehousePalletId: palletId, palletNo: `PAL-${palletId}`, cartonCount: 2 }] }) + event('POST', `/api/platform/v1/last-leg/fleet/trips/${tripNo}/bookings/${no}/receipt-confirmation`, {}, { booking: { ...booking, status: 'RECEIVED', incomingNo: transferNo, + receipt: { source: { receiptHash: 'OLD', inboundCode, destinationKey: 'DST', pallets: [{ warehousePalletId: destinationPallet, packages: 2 }] } } } }) + } + stock('TC', 'IN-C', [['101', 1]]) + dispatch('DP', [candidate('11', '101', 'IN-C')]) + stock('TR', 'IN-R', [['201', 2], ['202', 2]], 'RL') + for (const pallet of ['201', '202']) candidate(null, pallet, 'IN-R', { sourceStage: 'RELABEL', orderRefs: 'RL', orderNo: 'CUSTOMER-ORDER' }) + for (const i of [1, 2]) { + stock(`TN${i}`, `IN-N${i}`, [[`30${i}`, 3]]) + external(`BK${i}`, `T${i}`, `TE${i}`, `IN-E${i}`, `40${i}`, `30${i}`) + const row = candidate(`3${i}`, `30${i}`, i === 1 ? `IN-N${i}` : `IN-E${i}`) + dispatch(i === 1 ? 'DP-N' : 'DP-E', [row], undefined, 5) + } + external('BK', 'TR', 'TF', 'IN-F', '501', '31') + function pallet(palletId, palletCode, locationId) { + event('POST', '/api/platform/v1/overseas-warehouse-resources/W/pallets', { palletCode }, palletId) + event('GET', `/api/platform/v1/overseas-warehouse-resources/W/pallets/${palletId}`, undefined, { id: palletId, warehouseId: 'W', palletCode, locationId }) + } + pallet('31', 'CURRENT-NEW-GEOMETRY', '113') + pallet('601', 'CURRENT-SOURCE', '114'); pallet('111', 'CURRENT-TARGET', '112') + event('GET', '/api/platform/v1/overseas-warehouse-resources/W/locations', undefined, { records: [{ id: '112', locationCode: 'LOCATION' }] }) + event('GET', '/api/merchant/v1/iam/tenant/profile', undefined, { id: 'C' }, 'MERCHANT') + event('POST', '/api/merchant/v1/last-leg/return-orders', { remark: 'CURRENT new return' }, { order: { returnNo: 'RT', overseasWarehouseId: 'W' } }, 'MERCHANT') + event('GET', '/api/platform/v1/last-leg/return-inventory/cartons/BOX', undefined, { cartonMark: 'BOX', sourceReturnNo: 'RT', customerId: 'C', overseasWarehouseId: 'W', status: 'AVAILABLE', items: [{ skuCode: 'SKU-A', quantity: 3 }, { skuCode: 'SKU-B', quantity: 2 }] }) + event('POST', '/api/merchant/v1/last-leg/relabel-applications', { operationWarehouseId: 'W' }, { application: { relabelNo: 'RL-N', customerId: 'C', overseasWarehouseId: 'W' } }, 'MERCHANT') + dispatch('NDP', [candidate(null, '601', null, { sourceStage: 'RELABEL', orderRefs: 'RL-N', returnCartonSource: true })], 'RL-N', 1) + event('POST', '/api/platform/v1/last-leg/dispatches/NDP/transfer-delivery', {}, { outboundNo: 'O', deliveries: [{ deliveryNo: 'D' }] }) + event('GET', '/api/platform/v1/last-leg/delivery-orders/D/return-inbound-options?customerId=C', undefined, [{ customerId: 'C', lastMileOrderId: '91', deliveryNo: 'D', outboundNo: 'O', returnSources: [{ returnCartonId: '81', cartonMark: 'BOX', inventoryPositionId: null }] }]) + return { environment: 'TEST', marker: 'CURRENT', sourceCommit: commit, requests } +} +function writeProducer(ctx, report = producerReport()) { + const bytes = Buffer.from(JSON.stringify(report)), path = join(tmpdir(), `dispatch-repair-proof-${randomUUID()}.json`) + writeFileSync(path, bytes); evidenceFiles.push(path); ctx.producerEvidence = { path, sha256: createHash('sha256').update(bytes).digest('hex') } +} +function context(handler = () => assert.fail('No request expected')) { + const ctx = { marker: 'CURRENT', environment: 'TEST', release: { lastlegHealthy: true, adminHealthy: true, schemaReady: true, lastlegCommit: commit }, + createdResources: {}, calls: [], checkpoints: [], + async checkpoint() { this.checkpoints.push(structuredClone(this.createdResources.dispatchRepairAcceptance)) }, + async request(method, path, body) { this.calls.push({ method, path, body: structuredClone(body) }); return handler(method, path, body) } } + writeProducer(ctx); return ctx +} +const execute = (ctx, fixtures) => verifyDispatchRepair(ctx, fixtures, { execute: true }) + +test('import/plan is zero-network and missing current fixtures never become a pass', async () => { + const ctx = context(); assert.equal((await verifyDispatchRepair(ctx, {})).status, 'PLAN_ONLY'); assert.equal(ctx.calls.length, 0) + const result = await execute(ctx, {}) + assert.equal(result.status, 'PARTIAL_NOT_ALL_VERIFIED') + assert.equal(Object.values(result.cases).every(row => row.status === 'NOT_RUN'), true) + assert.equal(result.sourceEventsVerified, false) +}) +for (const field of ['lastlegHealthy', 'adminHealthy', 'schemaReady']) test(`unready ${field} blocks before any HTTP`, async () => { + const ctx = context(); ctx.release[field] = false + await assert.rejects(execute(ctx, {}), /healthy TEST releases/); assert.equal(ctx.calls.length, 0) +}) +test('production target and an old run fixture cannot borrow TEST authorization', async () => { + const ctx = context(); ctx.environment = 'PRODUCTION'; await assert.rejects(execute(ctx, {}), /TEST releases/) + const owned = context(); await assert.rejects(execute(owned, { departedGate: fresh({ outboundNo: 'OLD-O', deliveryNo: 'OLD-D' }) }), /Unowned outbound/) + assert.equal(owned.calls.length, 0) +}) +test('self-declared current flags cannot authorize an old dispatch absent from producer receipts', async () => { + const ctx = context() + await assert.rejects(execute(ctx, { cancellation: fresh({ dispatchNo: 'OLD-DP', allowDispatchCancellation: true, + marker: 'CURRENT', createdInThisRun: true, producerRequestId: 'claimed-current-request', producerTraceId: 'claimed-current-trace', + transferBody: {}, rearrangeBody: { planPalletIds: ['11'] } }) }), /Unowned dispatch/) + assert.equal(ctx.calls.length, 0) +}) +test('a later foreign shipment blocks all cases before the first HTTP request', async () => { + const ctx = context() + await assert.rejects(execute(ctx, { cancellation: fresh({ dispatchNo: 'DP', allowDispatchCancellation: true, + transferBody: {}, rearrangeBody: { planPalletIds: ['11'] } }), departedGate: fresh({ outboundNo: 'OLD-O', deliveryNo: 'D' }) }), /Unowned outbound/) + assert.equal(ctx.calls.length, 0) +}) +test('changed producer hash or response warehouse binding is rejected with zero HTTP', async () => { + const ctx = context(); ctx.producerEvidence.sha256 = '0'.repeat(64) + await assert.rejects(execute(ctx, {}), /hash mismatch/); assert.equal(ctx.calls.length, 0) + const scoped = context(), report = producerReport() + report.requests.find(row => row.method === 'POST' && row.path.endsWith('/pre-arrange')).result.overseasWarehouseId = 'OTHER' + writeProducer(scoped, report) + await assert.rejects(execute(scoped, {}), /Dispatch birth response/); assert.equal(scoped.calls.length, 0) +}) +test('failed pre-send checkpoint leaves the command unsent', async () => { + const ctx = context(); ctx.checkpoint = async () => { throw Error('disk full') } + await assert.rejects(execute(ctx, { departedGate: fresh({ outboundNo: 'O', deliveryNo: 'D' }) }), /disk full/) + assert.equal(ctx.calls.length, 0) +}) + +test('new unpicked dispatch cancels outbound, retains cancelled history, re-arranges released pallets and replays old transfer', async () => { + let cancelled = false, dispatchCancelled = false, transfers = 0 + const ctx = context((method, path, body) => { + if (path.endsWith('/transfer-delivery')) return ok({ outboundNo: 'O', deliveries: [{ deliveryNo: 'D' }], replayed: transfers++ > 0 }) + if (path.includes('/delivery-orders/') && path.includes('/status')) { assert.ok(['DELIVERING', 'REJECTED', 'CANCELLED'].includes(body.status)); assert.equal(body.expectedVersion, 0); return reject() } + if (path.includes('/delivery-orders/')) return ok({ order: { deliveryNo: 'D', outboundNo: 'O', status: cancelled ? 'CANCELLED' : 'WAIT_DELIVERY', version: 0, packageCount: 1 } }) + if (path.endsWith('/outbound-orders/O/cancel')) { cancelled = true; return ok(null) } + if (path.endsWith('/outbound-orders/O')) return ok({ outbound: { status: cancelled ? 'CANCELLED' : 'WAIT_OUTBOUND' } }) + if (method === 'DELETE') { assert.equal(path, '/api/platform/v1/last-leg/dispatches/DP'); assert.equal(cancelled, true); dispatchCancelled = true; return ok(null) } + if (path.endsWith('/dispatches/DP')) return ok({ status: dispatchCancelled ? 'CANCELLED' : 'PENDING_DEPARTURE', createdAt: '2026-10-18T16:00:00', actions: { canTransfer: true } }) + if (path.includes('/dispatches/candidates?')) return ok([{ planPalletId: '11' }]) + if (path.endsWith('/pre-arrange')) { assert.deepEqual(body.planPalletIds, ['11']); assert.equal(dispatchCancelled, true); return ok({ dispatchNo: 'DP-NEW', status: 'PENDING_DEPARTURE', items: [{ planPalletId: '11' }] }) } + assert.fail(path) + }) + const result = await execute(ctx, { cancellation: fresh({ allowDispatchCancellation: true, dispatchNo: 'DP', transferBody: { fees: [] }, rearrangeBody: { planPalletIds: ['11'] }, candidateQuery: { sourceStage: 'RECEIVED' } }) }) + assert.equal(result.cases.cancellation.status, 'PASS') + assert.equal(ctx.calls.filter(row => row.method === 'DELETE').length, 1) + assert.equal(ctx.calls.some(row => row.method === 'DELETE' && row.path.includes('/items/')), false) +}) + +test('domain rejection cannot be faked by a missing permission; actual shipment cancellation leaves facts unchanged', async () => { + for (const forbidden of [false, true]) { + const order = { status: 'WAIT_DELIVERY', version: 4, packageCount: 1, outboundNo: 'O' } + const ctx = context((method, path) => { + if (method === 'GET') return ok(path.includes('/outbound-orders/') ? { outbound: { status: 'OUTBOUNDED' } } : { order }) + return forbidden ? reject('A0403', 403) : reject() + }) + const run = execute(ctx, { departedGate: fresh({ outboundNo: 'O', deliveryNo: 'D' }) }) + if (forbidden) await assert.rejects(run, /Unexpected result/) + else assert.equal((await run).cases.departedGate.status, 'PASS') + } +}) + +for (const broken of [false, true]) test(`two-pallet relabel rejects partial selection with unchanged candidates then accepts all; broken=${broken}`, async () => { + const ids = ['201', '202'] + const ctx = context((method, path, body) => { + if (path.includes('/relabel-applications/')) return ok({ application: { relabelNo: 'RL', status: 'READY_FOR_OUTBOUND', fulfillmentStatus: 'WAIT_PRE_DISPATCH' } }) + if (path.includes('/candidates?')) return ok(ids.map(warehousePalletId => ({ orderRefs: 'RL', orderNo: 'CUSTOMER-ORDER', warehousePalletId, planPalletId: null }))) + assert.equal(method, 'POST'); assert.equal(body.relabelNo, 'RL') + if (body.planPalletIds.length === 1 && !broken) return reject() + return ok({ status: 'PENDING_DEPARTURE', dispatchNo: 'DP', items: body.planPalletIds.map(warehousePalletId => ({ warehousePalletId, planPalletId: null })) }) + }) + const run = execute(ctx, { wholeRelabel: fresh({ relabelNo: 'RL' }) }) + if (broken) await assert.rejects(run, /Unexpected result/) + else assert.equal((await run).cases.wholeRelabel.status, 'PASS') +}) + +function mixedFixture(fault = '') { + const samples = [1, 2].map(i => ({ displayKind: i === 1 ? 'ORDINARY' : 'EXTERNAL', dispatchNo: i === 1 ? 'DP-N' : 'DP-E', + ordinaryInboundCode: `IN-N${i}`, externalInboundCode: `IN-E${i}`, bookingNo: `BK${i}`, palletId: `30${i}` })) + const ctx = context((method, path) => { + if (path.includes('/inventory/balances') && !path.includes('/positions')) return ok({ total: 4, + records: [1, 2].flatMap(i => ['N', 'E'].map(kind => ({ id: `${kind}${i}`, customerId: 'C', overseasWarehouseId: 'W', inboundCode: `IN-${kind}${i}` }))) }) + if (path.includes('/positions')) { + const balance = path.match(/balances\/([NE][12])\//)[1], kind = balance[0], i = balance[1] + return ok([{ id: `POS-${balance}`, customerId: 'C', overseasWarehouseId: 'W', inboundCode: `IN-${balance}`, + palletId: `30${i}`, availablePackages: kind === 'N' ? fault === 'wrong-component' ? 4 : 3 : 2, pickedPackages: 0 }]) + } + const sample = samples.find(row => path.includes(row.dispatchNo)), i = sample.dispatchNo === 'DP-N' ? 1 : 2 + if (path.endsWith('/external-freight')) return ok({ requiresDeliveryFee: true, snapshotHash: `hash-${i}`, quotes: [{ bookingNo: `BK${i}`, inboundCode: `IN-E${i}`, + quantity: fault === 'wrong-quantity' ? 2 : 1, amount: fault === 'wrong-amount' ? 3 : 2, unitPrice: 2, currency: 'USD', destinationKey: 'DST' }] }) + return ok({ overseasWarehouseId: 'W', items: [{ warehousePalletId: `30${i}`, palletNo: `PAL-30${i}`, + inboundCode: i === 1 && fault !== 'wrong-display' ? 'IN-N1' : `IN-E${i}` }] }) + }) + if (fault === 'duplicate-sample') samples[1] = { ...samples[0], displayKind: 'EXTERNAL' } + return { ctx, fixture: fresh({ samples }) } +} +test('independent mixed display permutations prove per-source packages and commercial quantity/amount', async () => { + const f = mixedFixture(), result = await execute(f.ctx, { mixedSources: f.fixture }) + assert.equal(result.cases.mixedSources.status, 'PASS') + assert.deepEqual(result.cases.mixedSources.evidence.map(x => x.expectedSources.map(y => y.packages)), [[3, 2], [3, 2]]) +}) +for (const fault of ['duplicate-sample', 'wrong-display', 'wrong-component', 'wrong-quantity', 'wrong-amount']) test(`mixed source ${fault} cannot pass`, async () => { + const f = mixedFixture(fault) + await assert.rejects(execute(f.ctx, { mixedSources: f.fixture }), /independent|Displayed source|Physical pallet|external quote/) + assert.ok(f.ctx.calls.every(row => row.method === 'GET')) + if (fault === 'duplicate-sample') assert.equal(f.ctx.calls.length, 0) +}) + +test('changed receiving geometry requires fresh measurements and new key; exact confirmation replay remains identical', async () => { + const bodies = [] + const receipt = { source: { receiptHash: 'NEW', pallets: [{ warehousePalletId: '31', packages: 2 }] }, pallets: [{ warehousePalletId: '31', palletSpaces: 1, weightKg: 7, volumeCbm: 0.5 }] } + const ctx = context((method, path, body) => { + if (method === 'GET' && path.endsWith('/bookings')) return ok({ records: [{ bookingNo: 'BK', customerId: 'C', status: 'RECEIVED', dispatchNo: null, receipt: { source: { receiptHash: 'OLD' } } }] }) + if (method === 'GET') return ok({ tripRecordVersion: 5, recordVersion: 6, incoming: { receiptHash: 'NEW', pallets: [{ warehousePalletId: '31', weightKg: null, volumeCbm: null }] } }) + bodies.push(structuredClone(body)); assert.equal(body.receiptHash, 'NEW'); assert.equal(body.tripRecordVersion, 5); assert.equal(body.recordVersion, 6) + if (bodies.length === 1) { assert.equal(body.pallets[0].weightKg, undefined); return reject() } + assert.deepEqual(body.pallets, receipt.pallets) + return ok({ booking: { receipt }, replayed: bodies.length === 3 }) + }) + const result = await execute(ctx, { receiptRefresh: fresh({ tripNo: 'TR', bookingNo: 'BK', oldReceiptHash: 'OLD', unitPrice: 2, + measurements: [{ warehousePalletId: '31', weightKg: 7, volumeCbm: 0.5 }] }) }) + assert.equal(result.cases.receiptRefresh.status, 'PASS') + assert.notEqual(bodies[0].idempotencyKey, bodies[1].idempotencyKey); assert.deepEqual(bodies[1], bodies[2]) +}) + +function nativeFixture(fault = '') { + let marked = false, completeCount = 0 + const quantity = sku => sku === 'SKU-A' ? 3 : 2 + const page = (u, rows) => ok({ total: rows.length, records: rows.slice(Number(u.searchParams.get('page') || 1) - 1, Number(u.searchParams.get('page') || 1)) }) + const ctx = context((method, path, body) => { + const u = new URL(path, 'https://test.invalid'), sku = u.searchParams.get('skuCode'), Q = quantity(sku) + if (u.pathname === '/api/platform/v1/last-leg/return-inventory') { + let total = completeCount ? Q : 0 + if (fault === 'missing-sku' && sku === 'SKU-B') total = 0 + if (fault === 'replay-adds-stock' && completeCount >= 2) total = Q * 2 + return page(u, [{ customerId: 'OTHER', overseasWarehouseId: 'W', skuCode: sku, totalQuantity: 999, availableQuantity: 999, cartonQuantity: 9 }, + { customerId: 'C', overseasWarehouseId: 'W', skuCode: `${sku}-FUZZY`, totalQuantity: 999, availableQuantity: 999, cartonQuantity: 9 }, + { customerId: 'C', overseasWarehouseId: 'W', skuCode: sku, totalQuantity: total, + availableQuantity: fault === 'availability-not-restored' ? 0 : total, + cartonQuantity: completeCount && fault !== 'cartons-not-restored' ? 1 : 0, + updatedAt: fault === 'replay-updates-time' && completeCount >= 2 ? '2026-10-18T16:02:00' : completeCount ? '2026-10-18T16:01:00' : '2026-10-18T16:00:00' }]) + } + if (u.pathname.endsWith('/return-inventory/movements')) { + const base = { customerId: 'C', overseasWarehouseId: 'W', skuCode: sku, occurredAt: '2026-10-18T16:00:00' } + const incoming = { ...base, movementType: 'EXCEPTION_RETURN_IN', referenceNo: 'NATIVE-RECEIPT', totalDelta: Q, availableDelta: Q, occurredAt: '2026-10-18T16:01:00' } + const rows = [{ ...base, skuCode: `${sku}-FUZZY`, movementType: 'OUTBOUND', referenceNo: 'NDP', totalDelta: -999, availableDelta: -999 }, + { ...base, movementType: 'OUTBOUND', referenceNo: fault === 'wrong-outbound-reference' ? 'O' : 'NDP', totalDelta: -Q, availableDelta: -Q }] + if (completeCount && !(fault === 'missing-movement' && sku === 'SKU-B')) rows.push(incoming) + if (fault === 'duplicate-incoming' && completeCount) rows.push(incoming) + if (fault === 'replay-adds-movement' && completeCount >= 2) rows.push({ ...incoming, occurredAt: '2026-10-18T16:02:00' }) + return page(u, rows) + } + if (path.includes('/return-inventory/cartons/')) return ok({ cartonMark: 'BOX', status: completeCount ? 'AVAILABLE' : 'OUTBOUNDED', + customerId: 'C', overseasWarehouseId: 'W', palletCode: fault === 'wrong-target' ? 'WRONG-TARGET' : 'CURRENT-TARGET', locationCode: 'LOCATION', putawayAt: completeCount ? '2026-10-18T16:01:00' : null, + items: [{ skuCode: 'SKU-A', quantity: 3, availableQuantity: completeCount ? 3 : 0 }, { skuCode: 'SKU-B', quantity: 2, availableQuantity: completeCount ? 2 : 0 }] }) + if (path.includes('/return-inbound-options')) return ok([{ lastMileOrderId: fault === 'wrong-master' ? '92' : '91', customerId: 'C', inboundOrderId: null, inboundCode: null, deliveryNo: 'D', outboundNo: 'O', + returnSources: [{ returnCartonId: '81', inventoryPositionId: null, outboundPalletItemId: '71', cartonMark: 'BOX', maxReturnPackages: 1 }] }]) + if (path.includes('/mark-return')) { + assert.equal(body.lastMileOrderId, '91'); assert.equal(Object.hasOwn(body, 'inboundOrderId'), false) + assert.deepEqual(body.items, [{ outboundPalletItemId: '71', returnCartonId: '81', packageCount: 1 }]) + const replayed = marked; marked = true; return ok({ exceptionNo: 'EX', version: 0, replayed }) + } + if (path.includes('/start-processing')) return ok({ version: 1 }) + if (path.includes('/complete')) { assert.equal(body.receipts[0].inventoryPositionId, null); assert.equal(body.receipts[0].returnCartonId, '81'); return ok({ status: 'COMPLETED', restoredPackages: 1, replayed: completeCount++ > 0 }) } + if (method === 'GET' && path.includes('/exception-orders/')) return ok({ returnReceipts: [{ receiptNo: 'NATIVE-RECEIPT', receivedPackages: 1, receivedAt: '2026-10-18T16:01:00' }] }) + if (method === 'GET' && path.includes('/delivery-orders/')) return ok({ order: { status: 'RETURNED', outboundNo: 'O', packageCount: 1, version: 2 } }) + assert.fail(path) + }) + ctx.requestMultipart = async (method, path, file) => { assert.match(new TextDecoder().decode(file.content), /SYNTHETIC TEST/); return ok({ fileId: '101' }) } + return { ctx, fixture: fresh({ cartonMark: 'BOX', returnCartonId: '81', lastMileOrderId: '91', deliveryNo: 'D', outboundNo: 'O', + target: { targetPalletId: '111', targetPalletNo: 'CURRENT-TARGET', targetLocationId: '112', targetLocationCode: 'LOCATION' } }) } +} +test('native recovery reads exact paged SKU stock between first completion and replay', async () => { + const f = nativeFixture(), result = await execute(f.ctx, { nativeCartonReturn: f.fixture }) + const evidence = result.cases.nativeCartonReturn.evidence + assert.equal(result.cases.nativeCartonReturn.status, 'PASS') + assert.deepEqual(evidence.S.stock.map(x => x.total), [0, 0]); assert.deepEqual(evidence.R.stock.map(x => x.total), [3, 2]); assert.deepEqual(evidence.Rprime, evidence.R) + assert.deepEqual(evidence.R.stock.map(x => x.availableView), [3, 2]); assert.deepEqual(evidence.R.stock.map(x => x.cartons), [1, 1]) + const completes = f.ctx.calls.map((row, i) => row.path.includes('/complete') ? i : -1).filter(i => i >= 0) + assert.equal(completes.length, 2) + assert.ok(f.ctx.calls.slice(completes[0] + 1, completes[1]).some(row => row.path.includes('/return-inventory?'))) + assert.ok(f.ctx.calls.some(row => row.path.includes('page=3'))) +}) +for (const fault of ['wrong-master', 'missing-sku', 'availability-not-restored', 'cartons-not-restored', 'missing-movement', 'duplicate-incoming', 'replay-adds-stock', 'replay-adds-movement', 'replay-updates-time', 'wrong-target', 'wrong-outbound-reference']) test(`native recovery ${fault} cannot pass`, async () => { + const f = nativeFixture(fault) + await assert.rejects(execute(f.ctx, { nativeCartonReturn: f.fixture }), /Actual native|Every original|Native replay|exact validated|Original single-carton/) + if (fault === 'wrong-master') assert.ok(f.ctx.calls.every(row => row.method === 'GET')) +}) +test('an old target pallet cannot be authorized by a valid native carton fixture', async () => { + const f = nativeFixture(); f.fixture.target.targetPalletId = 'OLD-PALLET' + await assert.rejects(execute(f.ctx, { nativeCartonReturn: f.fixture }), /Unowned pallet/) + assert.equal(f.ctx.calls.length, 0) +}) + +test('read-only list permission allows detail but rejects cancellation with unchanged outbound', async () => { + const outbound = { status: 'WAIT_OUTBOUND', version: 0, packageCount: 1 } + const ctx = context(() => ok({ outbound })) + ctx.requestReadonly = async (method) => method === 'GET' ? ok({ outbound }) : reject('A0403', 403) + assert.equal((await execute(ctx, { permissions: fresh({ outboundNo: 'O' }) })).cases.permissions.status, 'PASS') +}) + +test('unknown command response is checkpointed and never automatically retried', async () => { + const ctx = context((method) => method === 'GET' ? ok({ outbound: { status: 'OUTBOUNDED' } }) : { http: 0, body: null }) + // The uncorrelated read itself stops before any command or automatic recovery. + ctx.request = async function (method, path) { this.calls.push({ method, path }); return { http: 0, body: null } } + await assert.rejects(execute(ctx, { departedGate: fresh({ outboundNo: 'O', deliveryNo: 'D' }) }), /Unknown transport/) + assert.equal(ctx.calls.length, 1); assert.equal(ctx.checkpoints.at(-1).requests.at(-1).status, 'UNKNOWN_RESULT') +}) diff --git a/scripts/admin-test-env-modules/dispatch-repair-acceptance.mjs b/scripts/admin-test-env-modules/dispatch-repair-acceptance.mjs new file mode 100644 index 000000000..ba6952558 --- /dev/null +++ b/scripts/admin-test-env-modules/dispatch-repair-acceptance.mjs @@ -0,0 +1,434 @@ +import { randomUUID, createHash } from 'node:crypto' +import { readFile } from 'node:fs/promises' + +const BASE = '/api/platform/v1/last-leg' +const id = value => value == null ? '' : String(value) +const same = (a, b) => JSON.stringify(a) === JSON.stringify(b) +const must = (value, reason) => { if (!value) throw Error(reason) } +const success = r => r?.http === 200 && String(r?.body?.code) === '200' +const rejected = r => [400, 409, 422].includes(r?.http) + || r?.http === 200 && ['A0400', 'A0409', 'A0422'].includes(String(r?.body?.code)) +const key = () => `dispatch-repair-${randomUUID()}` +const scope = (path, customerId) => `${path}?${new URLSearchParams({ customerId })}` +const cases = ['cancellation', 'departedGate', 'wholeRelabel', 'mixedSources', 'receiptRefresh', 'nativeCartonReturn', 'permissions'] + +/** The allowlist is derived from successful producer API receipts, never from fixture ownership flags. */ +async function loadProducerResources(context) { + must(context.producerEvidence?.path && /^[a-f0-9]{64}$/.test(context.producerEvidence?.sha256 || ''), 'Hashed current-run producer evidence required') + const bytes = await readFile(context.producerEvidence.path) + must(createHash('sha256').update(bytes).digest('hex') === context.producerEvidence.sha256, 'Producer evidence hash mismatch') + const report = JSON.parse(bytes.toString('utf8')) + must(report.environment === 'TEST' && report.marker === context.marker && report.sourceCommit === context.release.lastlegCommit + && Array.isArray(report.requests), 'Producer report run/environment/release mismatch') + const own = Object.fromEntries(['transfer', 'inbound', 'candidate', 'dispatch', 'outbound', 'delivery', 'relabel', 'trip', 'booking', 'return', 'carton', 'pallet'].map(k => [k, new Map()])) + const actors = new Map() + function add(kind, key, customerId, warehouseId, event, extra = {}) { + must(key && warehouseId, `Producer ${kind} response lacks identity/warehouse`) + const previous = own[kind].get(id(key)) + if (previous) must(id(previous.customerId) === id(customerId) && id(previous.warehouseId) === id(warehouseId), 'Producer resource scope drifted') + own[kind].set(id(key), { ...previous, ...extra, key: id(key), customerId: id(customerId), warehouseId: id(warehouseId), + requestId: event.requestId, traceId: event.traceId }) + } + for (const e of report.requests) { + if (e.status !== 'RESPONDED' || e.http !== 200 || String(e.code) !== '200' || !e.requestId || !e.traceId || !e.result) continue + const u = new URL(e.path, 'https://producer.invalid'), path = u.pathname, data = e.result + if (e.method === 'GET' && path === '/api/merchant/v1/iam/tenant/profile' && data.id) actors.set(e.actor, id(data.id)) + if (e.method === 'POST' && path === `${BASE}/warehouse-transfers` && String(e.body?.remark).includes(context.marker)) { + must(data.operatingWarehouseId === e.body.operatingWarehouseId && u.searchParams.get('customerId'), 'Transfer birth scope mismatch') + add('transfer', data.transferNo, u.searchParams.get('customerId'), data.operatingWarehouseId, e) + } + if (e.method === 'POST' && path === `${BASE}/fleet/trips` && String(e.body?.remark).includes(context.marker)) + add('trip', data.trip?.tripNo, '', data.trip?.warehouseId, e) + let match = path.match(/\/fleet\/trips\/([^/]+)\/bookings(?:\/([^/]+)(?:\/incoming|\/confirm|\/receipt-confirmation|\/dispatch)?)?$/) + if (e.method === 'POST' && match && own.trip.has(decodeURIComponent(match[1])) && data.booking) { + const trip = own.trip.get(decodeURIComponent(match[1])), b = data.booking + if (!match[2] || own.booking.has(b.bookingNo)) { + add('booking', b.bookingNo, b.customerId, trip.warehouseId, e, { tripNo: b.tripNo, booking: data.booking }) + if (b.incomingNo) add('transfer', b.incomingNo, b.customerId, trip.warehouseId, e, { externalBookingNo: b.bookingNo }) + } + } + match = path.match(/\/warehouse-transfers\/([^/]+)(?:\/palletize|\/confirm-measurement|\/review)?$/) + const transfer = match && own.transfer.get(decodeURIComponent(match[1])) + if (transfer && data.transferNo === transfer.key && id(data.operatingWarehouseId) === transfer.warehouseId && data.inboundCode) { + const pallets = data.pallets || [], received = pallets.reduce((n, row) => n + Number(row.cartonCount || 0), 0) + add('inbound', data.inboundCode, transfer.customerId, transfer.warehouseId, e, { receivedPackages: received, + kind: transfer.externalBookingNo ? 'EXTERNAL' : 'ORDINARY', externalBookingNo: transfer.externalBookingNo }) + for (const row of pallets) if (row.warehousePalletId) add('pallet', row.warehousePalletId, transfer.customerId, transfer.warehouseId, e, { palletCode: row.palletNo }) + if (data.relabelNo) add('relabel', data.relabelNo, transfer.customerId, transfer.warehouseId, e) + } + if (e.method === 'POST' && path === '/api/merchant/v1/last-leg/relabel-applications' && actors.has(e.actor)) { + const a = data.application + must(a && id(a.customerId) === actors.get(e.actor) && id(a.overseasWarehouseId) === id(e.body?.operationWarehouseId), 'Relabel producer scope mismatch') + add('relabel', a.relabelNo, a.customerId, a.overseasWarehouseId, e) + } + if (e.method === 'POST' && path === '/api/merchant/v1/last-leg/return-orders' && actors.has(e.actor) + && String(e.body?.remark).includes(context.marker)) add('return', data.order?.returnNo, actors.get(e.actor), data.order?.overseasWarehouseId, e) + match = path.match(/\/overseas-warehouse-resources\/([^/]+)\/pallets(?:\/([^/]+))?$/) + if (match && e.method === 'POST' && !match[2] && String(e.body?.palletCode).includes(context.marker)) + add('pallet', typeof data === 'object' ? data.id : data, '', decodeURIComponent(match[1]), e, { palletCode: e.body.palletCode }) + if (match && e.method === 'GET' && match[2] && own.pallet.has(decodeURIComponent(match[2]))) { + const old = own.pallet.get(decodeURIComponent(match[2])) + must(id(data.warehouseId) === old.warehouseId && data.palletCode === old.palletCode, 'New pallet readback drifted') + add('pallet', old.key, old.customerId, old.warehouseId, e, { locationId: id(data.locationId), palletCode: data.palletCode }) + } + match = path.match(/\/overseas-warehouse-resources\/([^/]+)\/locations$/) + if (e.method === 'GET' && match) for (const pallet of own.pallet.values()) { + if (pallet.warehouseId !== decodeURIComponent(match[1]) || !pallet.locationId) continue + const location = (Array.isArray(data) ? data : data.records || []).find(row => id(row.id) === pallet.locationId) + if (location?.locationCode) pallet.locationCode = location.locationCode + } + match = path.match(/\/return-inventory\/cartons\/([^/]+)$/) + const returned = match && own.return.get(data.sourceReturnNo) + if (e.method === 'GET' && returned && data.cartonMark === decodeURIComponent(match[1])) { + must(id(data.customerId) === returned.customerId && id(data.overseasWarehouseId) === returned.warehouseId, 'New return carton scope mismatch') + add('carton', data.cartonMark, returned.customerId, returned.warehouseId, e, { sourceReturnNo: data.sourceReturnNo, + ...(data.status !== 'OUTBOUNDED' ? { originalSkuItems: data.items } : {}) }) + } + if (e.method === 'GET' && path === `${BASE}/dispatches/candidates` && Array.isArray(data)) { + for (const row of data) { + const parent = row.sourceStage === 'RELABEL' ? own.relabel.get(row.orderRefs) : own.inbound.get(row.inboundCode) + if (!parent || id(row.customerId) !== parent.customerId || id(row.overseasWarehouseId) !== parent.warehouseId) continue + add('candidate', row.sourceStage === 'RELABEL' ? `RL:${row.warehousePalletId}` : row.planPalletId, parent.customerId, parent.warehouseId, e, { candidate: row }) + } + } + if (e.method === 'POST' && path === `${BASE}/dispatches/pre-arrange`) { + const sources = (e.body?.planPalletIds || []).map(value => own.candidate.get(e.body.relabelNo ? `RL:${value}` : id(value))) + must(sources.length && sources.every(Boolean) && sources.every(row => row.customerId === sources[0].customerId && row.warehouseId === sources[0].warehouseId), 'Dispatch creation lacks owned source receipts') + must(id(data.overseasWarehouseId) === sources[0].warehouseId && same((data.items || []).map(row => id(row.warehousePalletId)).sort(), sources.map(row => id(row.candidate.warehousePalletId)).sort()), 'Dispatch birth response differs from producer selection') + add('dispatch', data.dispatchNo, sources[0].customerId, sources[0].warehouseId, e, { dispatch: data, arrangeBody: e.body }) + } + match = path.match(/\/dispatches\/([^/]+)\/transfer-delivery$/) + const dispatched = match && own.dispatch.get(decodeURIComponent(match[1])) + if (e.method === 'POST' && dispatched && data.outboundNo && !data.replayed) { + add('outbound', data.outboundNo, dispatched.customerId, dispatched.warehouseId, e, { dispatchNo: dispatched.key }) + for (const row of data.deliveries || []) add('delivery', row.deliveryNo, dispatched.customerId, dispatched.warehouseId, e, { outboundNo: data.outboundNo, dispatchNo: dispatched.key }) + } + match = path.match(/\/delivery-orders\/([^/]+)\/return-inbound-options$/) + if (e.method === 'GET' && match && own.delivery.has(decodeURIComponent(match[1])) && Array.isArray(data)) { + for (const relation of data) for (const source of relation.returnSources || []) { + const carton = own.carton.get(source.cartonMark) + if (carton && source.returnCartonId && source.inventoryPositionId == null + && id(relation.customerId) === carton.customerId && relation.deliveryNo === decodeURIComponent(match[1]) + && relation.outboundNo === own.delivery.get(decodeURIComponent(match[1])).outboundNo) + add('carton', carton.key, carton.customerId, carton.warehouseId, e, { returnCartonId: id(source.returnCartonId), lastMileOrderId: id(relation.lastMileOrderId), deliveryNo: relation.deliveryNo }) + } + } + } + return own +} +function requireOwned(own, kind, key, fixture) { + const row = own[kind].get(id(key)) + must(row && row.warehouseId === id(fixture.warehouseId) && (!row.customerId || row.customerId === id(fixture.customerId)), `Unowned ${kind} ${id(key)}; producer receipt identity/customer/warehouse mismatch`) + return row +} +function validateAllResources(own, fixtures) { + for (const [name, f] of Object.entries(fixtures || {})) { + must(f.customerId && f.warehouseId, 'Fixture customer and warehouse required') + if (name === 'cancellation') { + const d = requireOwned(own, 'dispatch', f.dispatchNo, f) + must(same(d.arrangeBody.planPalletIds.map(id).sort(), f.rearrangeBody?.planPalletIds?.map(id).sort()), 'Re-arrangement must reuse only the exact producer sources') + } + if (['departedGate', 'permissions', 'nativeCartonReturn'].includes(name)) { + requireOwned(own, 'outbound', f.outboundNo, f) + if (f.deliveryNo) { + const d = requireOwned(own, 'delivery', f.deliveryNo, f) + must(d.outboundNo === f.outboundNo, 'Delivery/outbound producer relation mismatch') + } + } + if (name === 'wholeRelabel') requireOwned(own, 'relabel', f.relabelNo, f) + if (name === 'receiptRefresh') { + const b = requireOwned(own, 'booking', f.bookingNo, f) + must(b.tripNo === f.tripNo, 'Booking/trip producer relation mismatch') + for (const row of f.measurements || []) requireOwned(own, 'pallet', row.warehousePalletId, f) + } + if (name === 'mixedSources') { + must(f.samples?.length === 2 && new Set(f.samples.map(x => x.dispatchNo)).size === 2 + && new Set(f.samples.map(x => id(x.palletId))).size === 2 && new Set(f.samples.map(x => x.bookingNo)).size === 2, 'Mixed-source samples must be independent dispatches, pallets and bookings') + for (const sample of f.samples) { + requireOwned(own, 'dispatch', sample.dispatchNo, f); requireOwned(own, 'pallet', sample.palletId, f) + const normal = requireOwned(own, 'inbound', sample.ordinaryInboundCode, f), external = requireOwned(own, 'inbound', sample.externalInboundCode, f) + const booking = requireOwned(own, 'booking', sample.bookingNo, f) + must(normal.kind === 'ORDINARY' && external.kind === 'EXTERNAL' && external.externalBookingNo === booking.key + && normal.receivedPackages > 0 && external.receivedPackages > 0, 'Independent ordinary/external receipt quantities required') + } + } + if (name === 'nativeCartonReturn') { + const carton = requireOwned(own, 'carton', f.cartonMark, f), pallet = requireOwned(own, 'pallet', f.target?.targetPalletId, f) + const delivery = own.delivery.get(f.deliveryNo), dispatch = own.dispatch.get(delivery.dispatchNo) + must(carton.returnCartonId === id(f.returnCartonId) && carton.lastMileOrderId === id(f.lastMileOrderId) + && carton.deliveryNo === f.deliveryNo && carton.originalSkuItems?.length && dispatch.dispatch.packageCount === 1, + 'Native carton/master must come from the owned single-carton shipment') + must(pallet.palletCode === f.target.targetPalletNo && pallet.locationId === id(f.target.targetLocationId) && pallet.locationCode === f.target.targetLocationCode, 'Return target must match the newly created physical pallet/location') + } + } +} + +/** Internal, explicit TEST-only runner. Import and plan mode never send requests. */ +export async function verifyDispatchRepair(context, fixtures, { execute = false } = {}) { + if (!execute) return { status: 'PLAN_ONLY', cases, excluded: ['item DELETE negative branch', 'UI/PDA hardware', 'external document authenticity'] } + must(context.environment === 'TEST' && context.release?.lastlegHealthy && context.release?.adminHealthy + && context.release?.schemaReady && /^[a-f0-9]{40}$/.test(context.release?.lastlegCommit || ''), 'Matching healthy TEST releases and schema readback required') + must(context.marker && typeof context.checkpoint === 'function', 'Current run marker and durable checkpoint required') + const own = await loadProducerResources(context) + validateAllResources(own, fixtures) + const proof = { marker: context.marker, release: context.release, status: 'CHECKING', requests: [], cases: {}, + externalEvidence: 'Synthetic test records only; not real appointment/POD proof', sourceEventsVerified: false } + context.createdResources ??= {} + must(!context.createdResources.dispatchRepairAcceptance, 'No automatic restart of an existing acceptance run') + context.createdResources.dispatchRepairAcceptance = proof + async function call(method, path, body, { failure = false, file = null, cancel = false, actor = 'PLATFORM' } = {}) { + must(method !== 'DELETE' || cancel && /^\/api\/platform\/v1\/last-leg\/dispatches\/[^/?]+$/.test(path), 'Item deletion and arbitrary DELETE are excluded') + const entry = { method, path, actor, body: body === undefined ? undefined : structuredClone(body), status: 'SENDING' } + if (file) entry.file = { name: file.name, type: file.type, size: file.content.byteLength, sha256: createHash('sha256').update(file.content).digest('hex') } + proof.requests.push(entry); await context.checkpoint() + let response + try { + if (actor === 'READ_ONLY') must(typeof context.requestReadonly === 'function', 'Own read-only session required') + response = file ? await context.requestMultipart(method, path, file) + : await (actor === 'READ_ONLY' ? context.requestReadonly : context.request).call(context, method, path, body) + must(Number.isInteger(response?.http) && response.http >= 200 && response.http < 500 + && response.requestId && response.traceId && response.body && typeof response.body === 'object' + && (success(response) || /^[A-Z]\d{4}$/.test(String(response.body.code))), 'Unknown transport or uncorrelated response; preserve command, do not replay') + } catch (error) { entry.status = 'UNKNOWN_RESULT'; await context.checkpoint(); throw error } + Object.assign(entry, { status: 'RESPONDED', http: response.http, code: response.body.code, + requestId: response.requestId, traceId: response.traceId }); await context.checkpoint() + must(failure === 'FORBIDDEN' ? response.http === 403 || response.http === 200 && String(response.body.code) === 'A0403' + : failure ? rejected(response) : success(response), `Unexpected result: ${method} ${path}`) + return response.body.data + } + const number = value => { const n = Number(value); must(value != null && Number.isSafeInteger(n), 'Exact safe integer quantity required'); return n } + const cents = value => { const v = String(value); must(/^\d+(?:\.\d{1,2})?$/.test(v), 'Exact nonnegative money value required'); const [a, b = ''] = v.split('.'); return BigInt(a) * 100n + BigInt(b.padEnd(2, '0')) } + async function pages(path) { + const rows = [], url = new URL(path, 'https://test.invalid') + for (let page = 1; page <= 100; page++) { + url.searchParams.set('page', String(page)); url.searchParams.set('size', '200') + const data = await call('GET', url.pathname + url.search) + must(Array.isArray(data?.records) && number(data.total) >= 0, 'Complete paged API evidence required') + rows.push(...data.records) + if (rows.length === number(data.total)) return rows + must(rows.length < number(data.total) && data.records.length > 0, 'Paged evidence has inconsistent total or missing page') + } + throw Error('Paged evidence exceeded bounded limit; not verified') + } + async function nativeSnapshot(f, skuCodes) { + const stock = [], movements = [] + for (const skuCode of skuCodes) { + const query = new URLSearchParams({ customerId: f.customerId, overseasWarehouseId: f.warehouseId, skuCode }) + const exact = row => id(row.customerId) === id(f.customerId) && id(row.overseasWarehouseId) === id(f.warehouseId) && row.skuCode === skuCode + const balances = (await pages(`${BASE}/return-inventory?${query}`)).filter(exact) + must(balances.length <= 1, 'Duplicate exact native SKU balance') + const row = balances[0] + stock.push({ skuCode, present: Boolean(row), total: row ? number(row.totalQuantity) : 0, + availableView: row ? number(row.availableQuantity) : 0, cartons: row ? number(row.cartonQuantity) : 0, + updatedAt: row?.updatedAt ?? null }) + movements.push(...(await pages(`${BASE}/return-inventory/movements?${query}`)).filter(exact).map(row => ({ + customerId: id(row.customerId), warehouseId: id(row.overseasWarehouseId), skuCode: row.skuCode, + type: row.movementType, referenceNo: row.referenceNo, totalDelta: number(row.totalDelta), availableDelta: number(row.availableDelta), occurredAt: row.occurredAt }))) + } + return { stock: stock.sort((a, b) => a.skuCode.localeCompare(b.skuCode)), movements: movements.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))) } + } + const shipment = value => ({ status: value?.status, version: value?.version, outboundNo: value?.outboundNo, + packageCount: value?.packageCount, signedAt: value?.signedAt, signedPackages: value?.signedPackages }) + const dispatch = value => ({ status: value?.status, packageCount: value?.packageCount, items: value?.items }) + async function run(name, action) { + const fixture = fixtures?.[name] + if (!fixture) { proof.cases[name] = { status: 'NOT_RUN', reason: 'Current-run producer fixture missing' }; return } + proof.cases[name] = { status: 'CHECKING', producerEvidenceSha256: context.producerEvidence.sha256 } + try { proof.cases[name].evidence = await action(fixture); proof.cases[name].status = 'PASS' } + catch (error) { Object.assign(proof.cases[name], { status: 'FAILED', reason: error.message }); throw error } + finally { await context.checkpoint() } + } + try { + await run('cancellation', async f => { + must(f.allowDispatchCancellation === true && f.dispatchNo && f.transferBody && f.rearrangeBody, 'Owned cancellation fixture and explicit business cancel scope required') + const path = `${BASE}/dispatches/${encodeURIComponent(f.dispatchNo)}` + const original = await call('GET', path) + must(original.status === 'PENDING_DEPARTURE' && original.createdAt && original.actions?.canTransfer, 'New unpicked execution required') + const transferBody = { ...f.transferBody, idempotencyKey: key() } + const transfer = await call('POST', `${path}/transfer-delivery`, transferBody) + must(transfer.outboundNo && transfer.deliveries?.length, 'New outbound and delivery required') + const dpath = scope(`${BASE}/delivery-orders/${encodeURIComponent(transfer.deliveries[0].deliveryNo)}`, f.customerId) + const before = await call('GET', dpath) + must(before.order?.status === 'WAIT_DELIVERY' && before.order.outboundNo === transfer.outboundNo, 'Delivery must belong to new unexecuted outbound') + for (const status of ['DELIVERING', 'REJECTED', 'CANCELLED']) { + await call('PUT', scope(`${BASE}/delivery-orders/${encodeURIComponent(before.order.deliveryNo)}/status`, f.customerId), + { status, expectedVersion: before.order.version, remark: context.marker }, { failure: true }) + } + must(same(shipment((await call('GET', dpath)).order), shipment(before.order)), 'Rejected status commands changed delivery') + const opath = `${BASE}/outbound-orders/${encodeURIComponent(transfer.outboundNo)}` + await call('POST', `${opath}/cancel`, { reason: context.marker }) + must((await call('GET', opath)).outbound?.status === 'CANCELLED', 'Outbound cancellation did not persist') + must((await call('GET', dpath)).order?.status === 'CANCELLED', 'Linked delivery cancellation did not persist') + await call('DELETE', path, undefined, { cancel: true }) + must((await call('GET', path)).status === 'CANCELLED', 'Original dispatch was not retained as cancelled') + const candidateQuery = new URLSearchParams(f.candidateQuery) + const candidates = await call('GET', `${BASE}/dispatches/candidates?${candidateQuery}`) + const expected = f.rearrangeBody.planPalletIds.map(id) + must(expected.length && expected.every(value => candidates.some(row => id(row.planPalletId) === value)), 'Cancelled source pallets were not released') + const replacement = await call('POST', `${BASE}/dispatches/pre-arrange`, f.rearrangeBody) + must(replacement.dispatchNo && replacement.dispatchNo !== f.dispatchNo && replacement.status === 'PENDING_DEPARTURE' + && same(replacement.items.map(row => id(row.planPalletId)).sort(), [...expected].sort()), 'Released source was not re-arranged exactly once') + const replay = await call('POST', `${path}/transfer-delivery`, transferBody) + must(replay.outboundNo === transfer.outboundNo && replay.replayed, 'Old transfer replay generated another outbound') + return { originalDispatchNo: f.dispatchNo, cancelledOutboundNo: transfer.outboundNo, replacementDispatchNo: replacement.dispatchNo } + }) + await run('departedGate', async f => { + const opath = `${BASE}/outbound-orders/${encodeURIComponent(f.outboundNo)}` + const dpath = scope(`${BASE}/delivery-orders/${encodeURIComponent(f.deliveryNo)}`, f.customerId) + const outbound = await call('GET', opath), before = await call('GET', dpath) + must(outbound.outbound?.status === 'OUTBOUNDED' && before.order?.outboundNo === f.outboundNo, 'Actual current-run shipment required') + await call('POST', `${opath}/cancel`, { reason: context.marker }, { failure: true }) + await call('PUT', scope(`${BASE}/delivery-orders/${encodeURIComponent(f.deliveryNo)}/status`, f.customerId), + { status: 'CANCELLED', expectedVersion: before.order.version, remark: context.marker }, { failure: true }) + must(same(shipment((await call('GET', dpath)).order), shipment(before.order)), 'Departure cancellation gate changed delivery') + return { outboundNo: f.outboundNo, deliveryNo: f.deliveryNo } + }) + await run('wholeRelabel', async f => { + const apath = scope(`${BASE}/relabel-applications/${encodeURIComponent(f.relabelNo)}`, f.customerId) + const cpath = `${BASE}/dispatches/candidates?${new URLSearchParams({ sourceStage: 'RELABEL', orderNo: f.relabelNo })}` + const before = await call('GET', apath), candidates = await call('GET', cpath) + const ids = candidates.filter(row => row.orderRefs === f.relabelNo).map(row => id(row.warehousePalletId)).sort() + must(ids.length === 2 && new Set(ids).size === 2, 'Exactly two real pallets from the owned relabel application required') + await call('POST', `${BASE}/dispatches/pre-arrange`, { relabelNo: f.relabelNo, planPalletIds: ids.slice(0, 1) }, { failure: true }) + must(same((await call('GET', apath)).application, before.application), 'Partial relabel rejection changed the application') + must(same((await call('GET', cpath)).filter(row => row.orderRefs === f.relabelNo).map(row => id(row.warehousePalletId)).sort(), ids), 'Partial relabel rejection retained a lock or lost cargo') + const full = await call('POST', `${BASE}/dispatches/pre-arrange`, { relabelNo: f.relabelNo, planPalletIds: ids }) + must(full.status === 'PENDING_DEPARTURE' && full.items?.length === 2 && same(full.items.map(row => id(row.warehousePalletId)).sort(), ids), 'Full relabel selection did not preserve both pallets') + return { relabelNo: f.relabelNo, dispatchNo: full.dispatchNo, pallets: ids } + }) + await run('mixedSources', async f => { + must(same(f.samples.map(row => row.displayKind).sort(), ['EXTERNAL', 'ORDINARY']), 'Both physical display permutations required') + const evidence = [] + for (const sample of f.samples) { + const normal = own.inbound.get(sample.ordinaryInboundCode), external = own.inbound.get(sample.externalInboundCode) + const booking = own.booking.get(sample.bookingNo).booking + const current = await call('GET', `${BASE}/dispatches/${encodeURIComponent(sample.dispatchNo)}`) + const displayed = current.items?.find(row => id(row.warehousePalletId) === id(sample.palletId)) + const expectedDisplay = sample.displayKind === 'ORDINARY' ? normal.key : external.key + must(displayed?.inboundCode === expectedDisplay && displayed.palletNo && id(current.overseasWarehouseId) === id(f.warehouseId), 'Displayed source does not match its independently produced ordinary/external origin') + const balances = (await pages(`${BASE}/inventory/balances?${new URLSearchParams({ customerId: f.customerId, overseasWarehouseId: f.warehouseId })}`)) + .filter(row => id(row.customerId) === id(f.customerId) && id(row.overseasWarehouseId) === id(f.warehouseId)) + const quantities = new Map(), positions = [] + for (const balance of balances) { + const rows = await call('GET', `${BASE}/inventory/balances/${encodeURIComponent(id(balance.id))}/positions?${new URLSearchParams({ customerId: f.customerId, palletNo: displayed.palletNo })}`) + must(Array.isArray(rows), 'Actual inventory positions required') + for (const row of rows.filter(row => id(row.palletId) === id(sample.palletId))) { + must(id(row.customerId) === id(f.customerId) && id(row.overseasWarehouseId) === id(f.warehouseId), 'Physical pallet source scope mismatch') + const quantity = number(row.availablePackages) + number(row.pickedPackages) + if (quantity > 0) quantities.set(row.inboundCode, (quantities.get(row.inboundCode) || 0) + quantity) + positions.push({ positionId: id(row.id), inboundCode: row.inboundCode, quantity }) + } + } + must(quantities.size === 2 && quantities.get(normal.key) === normal.receivedPackages && quantities.get(external.key) === external.receivedPackages, + 'Physical pallet composition differs from independent receiving facts') + const freight = await call('GET', `${BASE}/dispatches/${encodeURIComponent(sample.dispatchNo)}/external-freight`) + const quote = freight.quotes?.[0], cargo = booking.cargo, receipt = booking.receipt?.source + const quantity = cargo?.priceBasis === 'PALLET' ? number(cargo.palletSpaces) : 1 + must(freight.requiresDeliveryFee === true && freight.quotes?.length === 1 && quote.bookingNo === sample.bookingNo + && quote.inboundCode === external.key && quote.currency === cargo.currency && quote.destinationKey === receipt?.destinationKey + && number(quote.quantity) === quantity && cents(quote.unitPrice) === cents(cargo.unitPrice) + && cents(quote.amount) === cents(booking.agreedAmount) && cents(quote.amount) === cents(cargo.unitPrice) * BigInt(quantity) + && freight.snapshotHash, 'Actual external quote quantity/price/amount/currency differs from producer commercial facts') + evidence.push({ dispatchNo: sample.dispatchNo, palletId: sample.palletId, displayKind: sample.displayKind, + expectedSources: [{ inboundCode: normal.key, packages: normal.receivedPackages }, { inboundCode: external.key, packages: external.receivedPackages }], + positions, quote, snapshotHash: freight.snapshotHash }) + } + return evidence + }) + await run('receiptRefresh', async f => { + must(f.tripNo && f.bookingNo && f.oldReceiptHash && Number(f.unitPrice) >= 0, 'Owned booking and previous actual receipt required') + const bookingsPath = `${BASE}/fleet/trips/${encodeURIComponent(f.tripNo)}/bookings` + const listing = await call('GET', bookingsPath) + const booking = listing.records?.find(row => row.bookingNo === f.bookingNo) + must(booking && id(booking.customerId) === id(f.customerId) && booking.status === 'RECEIVED' && !booking.dispatchNo + && booking.receipt?.source?.receiptHash === f.oldReceiptHash, 'Previously accepted, currently unbound own-customer booking required') + const path = `${bookingsPath}/${encodeURIComponent(f.bookingNo)}` + const before = await call('GET', `${path}/incoming`) + must(before.incoming?.receiptHash && before.incoming.receiptHash !== f.oldReceiptHash && before.incoming.pallets?.length + && before.incoming.pallets.some(row => row.weightKg == null || row.volumeCbm == null), 'Fresh split/merge geometry requiring measurement must be proven') + must(f.measurements?.length === before.incoming.pallets.length, 'Independent new physical pallet measurements required') + const body = { tripRecordVersion: before.tripRecordVersion, recordVersion: before.recordVersion, receiptHash: before.incoming.receiptHash, + pallets: before.incoming.pallets.map(row => ({ warehousePalletId: id(row.warehousePalletId), palletSpaces: 1 })), unitPrice: f.unitPrice, idempotencyKey: key() } + await call('POST', `${path}/receipt-confirmation`, body, { failure: true }) + body.idempotencyKey = key() + body.pallets = body.pallets.map(row => ({ ...row, ...f.measurements.find(value => id(value.warehousePalletId) === row.warehousePalletId) })) + must(body.pallets.every(row => Number(row.weightKg) > 0 && Number(row.volumeCbm) > 0), 'Positive new measurements required') + const confirmed = await call('POST', `${path}/receipt-confirmation`, body) + const replay = await call('POST', `${path}/receipt-confirmation`, body) + must(confirmed.booking?.receipt?.source?.receiptHash === body.receiptHash && replay.replayed === true + && same(replay.booking.receipt, confirmed.booking.receipt), 'Re-confirmed receipt or exact replay drifted') + return { tripNo: f.tripNo, bookingNo: f.bookingNo, receipt: confirmed.booking.receipt } + }) + await run('nativeCartonReturn', async f => { + must(f.cartonMark && f.returnCartonId && f.lastMileOrderId && f.target, 'Owned native carton, actual master and actual target resources required') + const cartonPath = `${BASE}/return-inventory/cartons/${encodeURIComponent(f.cartonMark)}` + const before = await call('GET', cartonPath) + must(before.status === 'OUTBOUNDED' && id(before.customerId) === id(f.customerId) && id(before.overseasWarehouseId) === id(f.warehouseId), 'Native carton must be actually shipped for this customer and warehouse') + const relations = await call('GET', scope(`${BASE}/delivery-orders/${encodeURIComponent(f.deliveryNo)}/return-inbound-options`, f.customerId)) + const relation = relations.find(row => id(row.lastMileOrderId) === id(f.lastMileOrderId) && row.deliveryNo === f.deliveryNo && row.outboundNo === f.outboundNo) + const source = relation?.returnSources?.find(row => id(row.returnCartonId) === id(f.returnCartonId) && row.inventoryPositionId == null && row.cartonMark === f.cartonMark && Number(row.maxReturnPackages) === 1) + must(source, 'Actual native carton source is missing; do not invent an inventory position or inbound') + const body = { lastMileOrderId: id(f.lastMileOrderId), reason: `${context.marker} synthetic refusal`, returnDirection: 'OVERSEAS_WAREHOUSE', + items: [{ outboundPalletItemId: id(source.outboundPalletItemId), returnCartonId: id(source.returnCartonId), packageCount: 1 }], idempotencyKey: key() } + const markPath = scope(`${BASE}/delivery-orders/${encodeURIComponent(f.deliveryNo)}/mark-return`, f.customerId) + const marked = await call('POST', markPath, body), replayMark = await call('POST', markPath, body) + must(marked.exceptionNo && replayMark.exceptionNo === marked.exceptionNo && replayMark.replayed, 'Native mark-return replay drifted') + const path = `${BASE}/exception-orders/${encodeURIComponent(marked.exceptionNo)}` + const started = await call('POST', scope(`${path}/start-processing`, f.customerId), { expectedVersion: marked.version, idempotencyKey: key(), handlingAction: 'SYNTHETIC TEST CARTON RETURN' }) + const file = { name: `${context.marker}-native-return.txt`, type: 'text/plain', content: new TextEncoder().encode('SYNTHETIC TEST EVIDENCE; not a real external POD') } + const uploaded = await call('POST', scope(`${path}/evidence`, f.customerId), undefined, { file }) + const completeBody = { expectedVersion: started.version, idempotencyKey: key(), handlingResult: 'SYNTHETIC TEST ACTUAL RETURN', evidenceFileIds: [uploaded.fileId], + receipts: [{ outboundPalletItemId: id(source.outboundPalletItemId), inventoryPositionId: null, returnCartonId: id(source.returnCartonId), receivedPackages: 1, ...f.target }] } + const born = own.carton.get(f.cartonMark), expected = new Map(born.originalSkuItems.map(row => [row.skuCode, number(row.quantity)])) + must(expected.size === born.originalSkuItems.length && [...expected.values()].every(n => n > 0), 'Unique original native SKU quantities required') + const skus = [...expected.keys()].sort(), S = await nativeSnapshot(f, skus) + const dispatchNo = own.delivery.get(f.deliveryNo).dispatchNo + const outboundEvidence = S.movements.filter(row => row.type === 'OUTBOUND' && row.referenceNo === dispatchNo) + must(outboundEvidence.length === skus.length && skus.every(sku => outboundEvidence.filter(row => row.skuCode === sku).length === 1 + && outboundEvidence.find(row => row.skuCode === sku).totalDelta === -expected.get(sku)), 'Original single-carton per-SKU outbound movement does not match producer carton') + const completed = await call('POST', scope(`${path}/complete`, f.customerId), completeBody) + const R = await nativeSnapshot(f, skus), history = await call('GET', scope(path, f.customerId)), after = await call('GET', cartonPath) + must(completed.status === 'COMPLETED' && number(completed.restoredPackages) === 1 && history.returnReceipts?.length === 1 + && number(history.returnReceipts[0].receivedPackages) === 1 && history.returnReceipts[0].receiptNo && history.returnReceipts[0].receivedAt, 'Native receipt identity/box count is incomplete') + const receiptNo = history.returnReceipts[0].receiptNo + const incoming = R.movements.filter(row => row.type === 'EXCEPTION_RETURN_IN' && row.referenceNo === receiptNo) + must(!S.movements.some(row => row.referenceNo === receiptNo) && incoming.length === skus.length + && same(incoming.map(row => row.skuCode).sort(), skus) && skus.every(sku => { + const previous = S.stock.find(row => row.skuCode === sku), restored = R.stock.find(row => row.skuCode === sku) + const movement = incoming.find(row => row.skuCode === sku) + return restored.present && restored.updatedAt && restored.total - previous.total === expected.get(sku) + && previous.total === 0 && previous.availableView === 0 && previous.cartons === 0 + && restored.availableView === expected.get(sku) && restored.cartons === 1 + && movement.totalDelta === expected.get(sku) && movement.availableDelta === expected.get(sku) && movement.occurredAt + }), 'Every original SKU must restore its exact total, visible availability, carton count and one immutable receipt movement') + must(after.status === 'AVAILABLE' && id(after.customerId) === id(f.customerId) && id(after.overseasWarehouseId) === id(f.warehouseId) + && after.cartonMark === f.cartonMark && after.palletCode === f.target.targetPalletNo && after.locationCode === f.target.targetLocationCode && after.putawayAt, + 'Native carton did not return to the exact validated physical pallet/location') + const deliveryAfter = await call('GET', scope(`${BASE}/delivery-orders/${encodeURIComponent(f.deliveryNo)}`, f.customerId)) + must(deliveryAfter.order?.status === 'RETURNED', 'Actual single-carton recovery did not close its delivery') + const replay = await call('POST', scope(`${path}/complete`, f.customerId), completeBody) + const Rprime = await nativeSnapshot(f, skus), historyAgain = await call('GET', scope(path, f.customerId)), cartonAgain = await call('GET', cartonPath) + const deliveryAgain = await call('GET', scope(`${BASE}/delivery-orders/${encodeURIComponent(f.deliveryNo)}`, f.customerId)) + must(replay.replayed === true && same(Rprime, R) && same(historyAgain.returnReceipts, history.returnReceipts) + && same({ pallet: cartonAgain.palletCode, location: cartonAgain.locationCode, at: cartonAgain.putawayAt, status: cartonAgain.status }, + { pallet: after.palletCode, location: after.locationCode, at: after.putawayAt, status: after.status }) + && same(shipment(deliveryAgain.order), shipment(deliveryAfter.order)), 'Native replay changed SKU stock, receipt rows/times, target or delivery') + return { exceptionNo: marked.exceptionNo, cartonMark: f.cartonMark, dispatchNo, receiptNo, S, R, Rprime, + expectedSkuQuantities: Object.fromEntries(expected), target: f.target, + availableQuantityMeaning: 'HTTP availableQuantity is a capped visible value; total and immutable availableDelta verified, no naked-ledger availability claim' } + }) + await run('permissions', async f => { + must(typeof context.requestReadonly === 'function' && f.outboundNo, 'Dedicated read-only session and owned fresh outbound required') + const path = `${BASE}/outbound-orders/${encodeURIComponent(f.outboundNo)}` + const before = await call('GET', path, undefined, { actor: 'READ_ONLY' }) + must(before.outbound?.status === 'WAIT_OUTBOUND', 'Current unexecuted outbound required for permission isolation') + await call('POST', `${path}/cancel`, { reason: context.marker }, { failure: 'FORBIDDEN', actor: 'READ_ONLY' }) + const after = await call('GET', path) + must(same(shipment(after.outbound), shipment(before.outbound)), 'Forbidden read-only command changed outbound') + return { outboundNo: f.outboundNo, readAllowed: true, cancellationForbidden: true } + }) + proof.status = cases.every(name => proof.cases[name]?.status === 'PASS') ? 'HTTP_CASES_PASSED_REVIEW_REQUIRED' : 'PARTIAL_NOT_ALL_VERIFIED' + return proof + } catch (error) { proof.status = 'FAILED'; proof.error = error.message; throw error } + finally { await context.checkpoint() } +}