import test from 'node:test' import assert from 'node:assert/strict' import { verifyClientRelabelFulfillment } from './client-relabel-fulfillment.mjs' function harness(fault = '', transfer = false) { const calls = [], saved = [], commands = new Set() const proof = { marker: 'RELABEL-TEST', relabelNo: 'TB202610220001', customerId: '101', warehouseId: '201', sourcePositionId: '301', sourcePalletNo: 'SOURCE', sourceOrderNo: 'WC202610220001' } let status = 'PENDING_REVIEW', moved = false, scanned = false, version = 0, taskStatus = 'AVAILABLE' const palletMeasurements = ['SOURCE', 'TARGET'].map((palletCode, index) => ({ palletId: String(500 + index), palletCode, packages: index ? 1 : 2, stockVersion: `CURRENT-${palletCode}`, cartonAggregated: false })) const item = { itemId: '401', expectedPackages: 1, cartonMark: 'BOX-TEST', fbaCartonMark: 'FBA-TEST', operationType: 'CARTON_AND_SKU', skuItems: [{ skuCode: 'SKU', newSkuCode: 'NEW', quantity: 1 }] } const detail = () => ({ application: { relabelNo: proof.relabelNo, customerOrderNo: proof.sourceOrderNo, lastlegOrderId: '1001', reviewChannel: transfer ? 'RELABEL_ORDER' : 'TAIL_ORDER', overseasWarehouseId: fault === 'wrong warehouse' ? '999' : '201', status, walletStatus: fault === 'no funds' ? 'PENDING' : 'FROZEN', fulfillmentStatus: status === 'READY_FOR_OUTBOUND' ? transfer && fault !== 'wrong fulfillment' ? 'WAIT_TRANSFER_OUTBOUND' : 'WAIT_PRE_DISPATCH' : null }, items: [{ sourceInventoryPositionId: '301', cartonMark: 'BOX-TEST' }] }) const task = () => ({ relabelNo: proof.relabelNo, taskStatus, version, applicationStatus: status, relabelledPackages: scanned ? 1 : 0, skippedPackages: 0, exceptionPackages: 0, items: [item] }) const context = { marker: proof.marker, token: 'admin-token', context: { merchantQualificationFixture: { tenantId: '101' } }, assert(_module, name, _response, passed) { assert.ok(passed, name) }, async request(method, path, body, token) { calls.push({ method, path, body, token }) assert.equal(saved.at(-1)?.requests.at(-1)?.status, 'SENDING') const repeated = body?.idempotencyKey && commands.has(body.idempotencyKey) if (body?.idempotencyKey) commands.add(body.idempotencyKey) let data if (path.endsWith('/tenant/profile')) data = { id: fault === 'wrong customer' ? '999' : '101', status: 'ACTIVE' } else if (path.endsWith('/status') || path.includes('/relabel-review?')) { assert.equal(path.endsWith('/status'), transfer) status = 'PENDING_PROCESSING'; data = detail() } else if (path.endsWith('/claim')) { taskStatus = 'PROCESSING'; if (!repeated) version++; data = task() } else if (path.endsWith('/cartons/scan')) { if (!repeated) version++; data = task() } else if (path.endsWith('/units')) { if (fault === 'unknown scan') throw new Error('unknown scan response') scanned = true; if (!repeated) version++; data = task() if (fault === 'scan replay drift' && repeated) data.relabelledPackages = 2 } else if (path.endsWith('/relabel-pallet-measurements')) data = structuredClone(palletMeasurements) else if (path.endsWith('/relabel-pallet-measure')) { const row = palletMeasurements.find(value => value.palletCode === body.palletCode) assert.equal(body.stockVersion, row.stockVersion) if (fault !== 'measurement not saved') Object.assign(row, { lengthCm: body.lengthCm, widthCm: body.widthCm, heightCm: body.heightCm, grossWeightKg: body.grossWeightKg, volumeCbm: 0.96 }) data = structuredClone(palletMeasurements) } else if (path.endsWith('/relabel-pallet-label')) data = { palletId: '501', palletCode: 'TARGET' } else if (path.endsWith('/pallets/resolve')) { const source = body.code === 'SOURCE' data = { palletCode: body.code, palletVersion: moved ? 1 : 0, totalPackages: source ? moved ? fault === 'no source deduction' ? 3 : 2 : 3 : moved ? 1 : 0 } } else if (path.endsWith('/relabel-disassemble')) { moved = true; data = { status: 'COMPLETED', operationId: '601', replayed: Boolean(repeated) } } else if (path.endsWith('/confirm-completion')) { status = 'READY_FOR_OUTBOUND'; data = detail() } else if (path.endsWith('/complete')) { status = 'CUSTOMER_CONFIRM_PENDING'; taskStatus = 'WAIT_REVIEW'; data = task() } else if (path.includes('/relabel-tasks/')) data = task() else if (path.includes('/warehouse-transfers/')) data = { transferNo: proof.sourceOrderNo, status: 'ALLOCATED', relabelNo: proof.relabelNo, relabelApplicationId: '801', allocations: [{ status: 'RESERVED', packageCount: 1, palletId: fault === 'wrong target' ? '999' : '501', inboundCode: 'IN-1', inventoryPositionId: '901', skuCode: 'NEW' }] } else data = detail() return { http: 200, body: { code: '200', data } } }, async requestMultipart(method, path, body, token) { calls.push({ method, path, token }) assert.equal(saved.at(-1)?.requests.at(-1)?.status, 'SENDING') return { http: 200, body: { code: '200', data: { fileId: '701' } } } } } const inputs = { proof, merchantToken: 'client-secret', pdaToken: 'pda-secret', delay: async () => {}, checkpoint: async () => { if (fault === 'disk') throw new Error('disk full'); saved.push(structuredClone(proof.execution)) } } if (transfer) { inputs.transferProof = { ...proof, transferNo: proof.sourceOrderNo, status: 'WAIT_RELABEL', inboundCode: 'IN-1', relabelApplicationId: '801', allocation: { status: 'ALLOCATED_WAIT_RELABEL' }, relabelFulfillment: proof } context.createdResources = { phase4OverseasClientServices: { transferFulfillment: inputs.transferProof } } } return { context, inputs, calls, saved } } test('original customer relabel reaches ready for dispatch through real command sequence and exact replays', async () => { const h = harness() const result = await verifyClientRelabelFulfillment(h.context, h.inputs) assert.equal(result.status, 'READY_FOR_OUTBOUND') assert.deepEqual(result.targetPalletIds, ['501']) assert.equal(result.execution.status, 'HTTP_VERIFIED_READY_FOR_DISPATCH') assert.equal(h.calls.filter(call => call.path.endsWith('/units')).length, 2) assert.equal(h.calls.filter(call => call.path.endsWith('/relabel-disassemble')).length, 2) assert.equal(h.calls.filter(call => call.path.endsWith('/confirm-completion')).length, 2) assert.ok(!JSON.stringify(h.saved).includes('client-secret')) assert.ok(!JSON.stringify(h.saved).includes('pda-secret')) await assert.rejects(verifyClientRelabelFulfillment(h.context, h.inputs), /不自动重跑/) }) test('same operator and batch can execute customer and transfer applications without sharing operation keys', async () => { const first = harness(), second = harness('', true) second.inputs.proof.relabelNo = 'TB202610220002' second.inputs.transferProof.relabelNo = second.inputs.proof.relabelNo await verifyClientRelabelFulfillment(first.context, first.inputs) const used = new Map(first.calls.filter(call => call.body?.idempotencyKey).map(call => [call.body.idempotencyKey, call.path])) const request = second.context.request second.context.request = async (method, path, body, token) => { if (body?.idempotencyKey && used.has(body.idempotencyKey) && used.get(body.idempotencyKey) !== path) return { http: 200, body: { code: 'A0404' } } return request(method, path, body, token) } const result = await verifyClientRelabelFulfillment(second.context, second.inputs) assert.equal(result.execution.status, 'HTTP_VERIFIED_READY_FOR_TRANSFER_OUTBOUND') assert.ok(second.calls.filter(call => call.body?.idempotencyKey).every(call => !used.has(call.body.idempotencyKey))) }) for (const fault of ['wrong customer', 'wrong warehouse', 'no funds', 'unknown scan', 'scan replay drift', 'no source deduction', 'disk']) { test(`relabel stops before completion on ${fault}`, async () => { const h = harness(fault) await assert.rejects(verifyClientRelabelFulfillment(h.context, h.inputs)) assert.equal(h.inputs.proof.execution.status, 'FAILED') assert.equal(h.calls.some(call => call.path.endsWith('/confirm-completion')), false) if (fault === 'no funds') assert.equal(h.calls.some(call => call.path.endsWith('/claim')), false) if (fault === 'unknown scan') { assert.equal(h.calls.filter(call => call.path.endsWith('/units')).length, 1) assert.equal(h.inputs.proof.execution.requests.at(-1).status, 'UNKNOWN_RESULT') } if (fault === 'disk') assert.equal(h.calls.length, 0) }) } test('foreign-batch relabel is rejected before any HTTP', async () => { const h = harness() h.inputs.proof.marker = 'OLD' await assert.rejects(verifyClientRelabelFulfillment(h.context, h.inputs), /本轮客户申请/) assert.equal(h.calls.length, 0) }) test('已有库存转运原关联换标完成后回到原转运新SKU预留', async () => { const h = harness('', true) await verifyClientRelabelFulfillment(h.context, h.inputs) assert.equal(h.inputs.transferProof.status, 'ALLOCATED') assert.equal(h.inputs.proof.execution.status, 'HTTP_VERIFIED_READY_FOR_TRANSFER_OUTBOUND') assert.equal(h.inputs.transferProof.outboundAllocations[0].palletId, '501') }) test('转运拆合托补测未保存时禁止完工和客户确认', async () => { const h = harness('measurement not saved', true) await assert.rejects(verifyClientRelabelFulfillment(h.context, h.inputs), /补测回执/) assert.equal(h.inputs.proof.execution.status, 'FAILED') assert.equal(h.calls.some(call => call.path.endsWith('/complete') || call.path.endsWith('/confirm-completion')), false) }) for (const fault of ['wrong fulfillment', 'wrong target']) { test(`转运关联换标拒绝 ${fault}`, async () => { const h = harness(fault, true) await assert.rejects(verifyClientRelabelFulfillment(h.context, h.inputs)) assert.equal(h.inputs.transferProof.status, 'WAIT_RELABEL') assert.equal(h.inputs.proof.execution.status, 'FAILED') }) } test('未登记转运分配证据不得操作关联换标', async () => { const h = harness('', true) h.inputs.transferProof = { ...h.inputs.transferProof } await assert.rejects(verifyClientRelabelFulfillment(h.context, h.inputs), /原分配证据/) assert.equal(h.calls.length, 0) })