import test from 'node:test' import assert from 'node:assert/strict' import { mkdtemp, readFile, writeFile, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { fingerprint, validateOrderInput, withOrderJournal, runOrderStage } from './prod-release-order-stage.mjs' import { executeOrder } from './prod-release-acceptance.mjs' function fixture() { return { batch: 'release-fixture-001', environment: 'https://fixture.invalid/api', operator: { merchant: 'fixture-client', admin: 'fixture-admin' }, customer: { id: '1', name: 'fixture', code: 'fixture' }, origin: { id: '2', code: 'CN-YIWU-01' }, destination: { id: '3', code: 'FIXTURE', type: 'FBA' }, payload: { customerName: 'fixture', receivingWarehouseId: '2', destinationWarehouseId: '3', amazonWarehouseCode: 'FIXTURE', transportMode: 'SEA', destinationType: 'FBA', fulfillmentType: 'STANDARD', containerMode: 'LCL', pickupMethod: 'SELF_EXPRESS', channelCode: 'FIXTURE', pricingTaxMode: 'TAX_INCLUDED', customerRemark: 'RELEASE-ACCEPTANCE:release-fixture-001', cargoes: [{ skuNo: 'fixture-sku', packageCount: 2, unitsPerPackage: 1, totalQuantity: 2, lengthCm: 40, widthCm: 30, heightCm: 30, grossWeightKg: 15, volumeCbm: 0.072, productImageFileId: '4', woodenProduct: false, dangerousGoods: false }] } } } async function sandbox(t) { const path = await mkdtemp(join(tmpdir(), 'prod-order-contract-')) t.after(() => rm(path, { recursive: true, force: true })) return join(path, 'journal.json') } function server(input, fail) { let status = null let failed = false const calls = [] const state = { calls, created: 0, approved: 0, async transport(request) { calls.push(request) const { method, path, idempotencyKey } = request if (path.endsWith('/inbound-code-policy')) return { mode: 'AUTO', manualEntryRequired: false } if (method === 'GET') return { order: { ...input.payload, id: '5', tenantId: '1', customerId: '1', status, orderNo: 'FIXTURE-ORDER', inboundCode: status === 'DRAFT' ? null : 'FIXTURE-INBOUND' }, cargoes: input.payload.cargoes } let op if (path.endsWith('/first-leg')) { op = 'create' assert.ok(idempotencyKey) if (!status) { state.created++; status = 'DRAFT' } } else if (path.endsWith('/submit')) { op = 'submit' assert.ok(idempotencyKey) status = 'SUBMITTED' } else { assert.match(path, /^\/platform\/v1\/orders\/first-leg\/5\/approve\?tenantId=1$/) assert.equal(request.role, 'ADMIN') op = 'approve' state.approved++ if (fail !== 'approve-before') status = 'APPROVED' } if ((fail === op || fail === 'approve-before' && op === 'approve') && !failed) { failed = true throw new Error('simulated response loss') } return op === 'create' ? '5' : null }, } return state } async function run(path, input, api, resume = false, authorize = async () => {}) { return withOrderJournal(path, input, (state, save) => runOrderStage({ input, state, save, transport: api.transport, authorize, resume })) } test('fingerprint is stable for reordered keys', () => { assert.equal(fingerprint({ a: 1, b: 2 }), fingerprint({ b: 2, a: 1 })) }) test('rejects multiple SKUs, excess cartons, incorrect company and volume', () => { for (const change of [i => i.payload.cargoes.push(i.payload.cargoes[0]), i => i.payload.cargoes[0].packageCount = 300, i => i.payload.customerId = '999', i => i.origin.code = 'CN-OTHER', i => i.payload.customerRemark += '-other', i => i.payload.cargoes[0].volumeCbm = 2]) { const input = fixture(); change(input) assert.throws(() => validateOrderInput(input)) } }) test('one create, one submit, one approval; completed resume performs reads only', async t => { const path = await sandbox(t), input = fixture(), api = server(input) const result = await run(path, input, api) assert.equal(result.status, 'ORDER_APPROVED') assert.equal(result.fullFlowVerified, false) const count = api.calls.filter(c => c.method === 'POST').length await run(path, input, api, true) assert.equal(api.calls.filter(c => c.method === 'POST').length, count) assert.equal(api.created, 1) assert.equal(api.approved, 1) const disk = await readFile(path, 'utf8') assert.doesNotMatch(disk, /fixture-client|fixture-admin|productImageFileId|https:/) }) for (const failure of ['create', 'submit', 'approve']) { test(`recover lost ${failure} response without duplicate business records`, async t => { const path = await sandbox(t), input = fixture(), api = server(input, failure) await assert.rejects(run(path, input, api), /response loss/) await assert.rejects(run(path, input, api), /显式续跑|禁止重发/) await run(path, input, api, true) assert.equal(api.created, 1) assert.equal(api.approved, 1) if (failure === 'create') { const creates = api.calls.filter(c => c.method === 'POST' && c.path.endsWith('/first-leg')) assert.equal(creates.length, 2) assert.equal(creates[0].idempotencyKey, creates[1].idempotencyKey) } }) } test('uncertain approval that still reads submitted must not be resent', async t => { const path = await sandbox(t), input = fixture(), api = server(input, 'approve-before') await assert.rejects(run(path, input, api)) await assert.rejects(run(path, input, api, true), /禁止重发审核/) assert.equal(api.approved, 1) }) test('change in environment, account, payload or batch is blocked before HTTP', async t => { const path = await sandbox(t), input = fixture(), api = server(input) await run(path, input, api) for (const change of [i => i.environment = 'https://other.invalid/api', i => i.operator.merchant = 'other', i => i.payload.channelCode = 'OTHER', i => { i.batch = 'release-fixture-002'; i.payload.customerRemark = `RELEASE-ACCEPTANCE:${i.batch}` }]) { const other = fixture(); change(other) const count = api.calls.length await assert.rejects(run(path, other, api, true), /已变化/) assert.equal(api.calls.length, count) } }) test('authorization failure prevents first write', async t => { const path = await sandbox(t), input = fixture(), api = server(input) await assert.rejects(run(path, input, api, false, async () => { throw new Error('wrong customer') })) assert.equal(api.calls.length, 0) }) test('foreign order detail blocks submission', async t => { const path = await sandbox(t), input = fixture(), api = server(input) const original = api.transport api.transport = async req => { const data = await original(req) if (data?.order) data.order.customerId = '999' return data } await assert.rejects(run(path, input, api), /归属/) assert.equal(api.calls.filter(c => c.method === 'POST').length, 1) }) test('concurrent journal process is rejected', async t => { const path = await sandbox(t), input = fixture() await withOrderJournal(path, input, async () => { await assert.rejects(withOrderJournal(path, input, async () => {}), { code: 'EEXIST' }) }) }) test('corrupt journal is preserved and fails closed', async t => { const path = await sandbox(t), input = fixture(), api = server(input) await writeFile(path, '{broken') await assert.rejects(run(path, input, api), /检查点损坏/) assert.equal(await readFile(path, 'utf8'), '{broken') assert.equal(api.calls.length, 0) }) for (const quoteMismatch of [false, true]) { test(`entry integration: customer/profile/route checks and HTTP headers (quote mismatch=${quoteMismatch})`, async t => { const path = await sandbox(t), input = fixture(), api = server(input) const settings = { cn: 'https://fixture.invalid/api', overseas: 'https://fixture.invalid/overseas-api', customer: input.customer, credentials: Object.fromEntries(['ADMIN', 'MERCHANT', 'CN_PDA', 'OVERSEAS_LAX_PDA'].map(role => [role, { loginId: role, password: 'secret' }])) } const fetcher = async (url, options) => { const endpoint = new URL(url).pathname.replace(/^\/(overseas-api|api)/, '') + new URL(url).search let data if (endpoint === '/auth/login') { const role = JSON.parse(options.body).loginId data = { accessToken: role, overseasAccessToken: `${role}-overseas` } } else if (endpoint.includes('/iam/')) { data = { id: '1', tenantCode: 'fixture', tenantName: 'fixture', status: 'ACTIVE', dimensionInitializationStatus: 'READY' } } else if (endpoint.includes('/warehouse-admin?')) { const domestic = endpoint.includes('warehouseType=DOMESTIC') data = { total: '1', records: [{ id: domestic ? '2' : '6', warehouseCode: domestic ? 'CN-YIWU-01' : 'US-LAX-01', status: 'ACTIVE' }] } } else if (endpoint.endsWith('/pda-loading/warehouses')) { assert.equal(options.headers.Authorization, 'Bearer CN_PDA') data = [{ id: '2', warehouseCode: 'CN-YIWU-01' }] } else if (endpoint.endsWith('/warehouse-profile')) { assert.equal(options.headers.Authorization, 'Bearer OVERSEAS_LAX_PDA-overseas') data = { warehouseId: '6', warehouseCode: 'US-LAX-01' } } else if (/\/platform\/v1\/(pallets|locations)\?/.test(endpoint)) { data = { total: '1', records: [{ warehouseId: '2', status: 'AVAILABLE' }] } } else if (endpoint === '/merchant/v1/warehouses/3') { data = { warehouse: { id: '3', warehouseCode: 'FIXTURE', warehouseType: 'FBA', status: 'ACTIVE', warehouseName: 'fixture' } } } else if (endpoint.includes('/pricing/channels?')) { data = [{ channelCode: 'FIXTURE', priceBookVersionId: '7', pricingTaxModes: ['TAX_INCLUDED'] }] } else if (endpoint.endsWith('/pricing/estimate')) { assert.equal(options.headers.Authorization, 'Bearer MERCHANT') const body = JSON.parse(options.body) assert.equal(body.actualWeightKg, 15) assert.equal(body.actualVolumeCbm, 0.072) data = { channelCode: 'FIXTURE', priceBookVersionId: quoteMismatch ? '8' : '7', pricingTaxMode: 'TAX_INCLUDED', estimatedAmount: 10 } } else { const role = endpoint.includes('/approve?') ? 'ADMIN' : 'MERCHANT' assert.equal(options.headers.Authorization, `Bearer ${role}`) data = await api.transport({ role, path: endpoint, method: options.method, body: options.body ? JSON.parse(options.body) : undefined, idempotencyKey: options.headers['Idempotency-Key'] }) } return { ok: true, status: 200, json: async () => ({ code: '200', data }) } } if (quoteMismatch) { await assert.rejects(executeOrder(settings, input, path, false, fetcher), /报价不匹配/) assert.equal(api.created, 0) } else { assert.equal((await executeOrder(settings, input, path, false, fetcher)).status, 'ORDER_APPROVED') assert.equal(api.created, 1) assert.equal(api.approved, 1) await executeOrder(settings, input, path, true, fetcher) assert.equal(api.created, 1) assert.equal(api.approved, 1) } }) }