124 lines
6.3 KiB
JavaScript
124 lines
6.3 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { readFileSync } from 'node:fs'
|
|
import test from 'node:test'
|
|
|
|
import { approvedInboundReady, verifyOrderApprovalReplay } from './orders.mjs'
|
|
|
|
const tenantId = '10001'
|
|
const orderId = '20001'
|
|
const taskResponse = {
|
|
http: 200,
|
|
body: { code: '200', data: { id: '30001', tenantId, orderId } },
|
|
}
|
|
|
|
test('draft inbound id alone does not mean approval outbox has converged', () => {
|
|
assert.equal(approvedInboundReady({
|
|
summary: { inboundId: '40001', mainStatus: 'PREPARING' },
|
|
}, taskResponse, tenantId, orderId), false)
|
|
})
|
|
|
|
test('measurable inbound without fulfillment task remains pending', () => {
|
|
assert.equal(approvedInboundReady({
|
|
summary: { inboundId: '40001', mainStatus: 'WAIT_MEASURE' },
|
|
}, { http: 200, body: { code: 'A0404' } }, tenantId, orderId), false)
|
|
})
|
|
|
|
test('approval polling remains pending before the first fulfillment task response', () => {
|
|
assert.equal(approvedInboundReady({
|
|
summary: { inboundId: null, mainStatus: 'PENDING_REVIEW' },
|
|
}, null, tenantId, orderId), false)
|
|
})
|
|
|
|
test('approval convergence requires matching inbound state, order and customer task identity', () => {
|
|
const detail = { summary: { inboundId: '40001', mainStatus: 'WAIT_MEASURE' } }
|
|
assert.equal(approvedInboundReady(detail, taskResponse, tenantId, orderId), true)
|
|
assert.equal(approvedInboundReady(detail, {
|
|
http: 200, body: { code: '200', data: { id: '30001', tenantId, orderId: '20002' } },
|
|
}, tenantId, orderId), false)
|
|
assert.equal(approvedInboundReady(detail, {
|
|
http: 200, body: { code: '200', data: { id: '30001', tenantId: '10002', orderId } },
|
|
}, tenantId, orderId), false)
|
|
})
|
|
|
|
test('orders acceptance polls approval convergence instead of only inbound id visibility', () => {
|
|
const source = readFileSync(new URL('orders.mjs', import.meta.url), 'utf8')
|
|
assert.match(source, /审核后入库单与履约任务异步收敛/)
|
|
assert.match(source, /approvedInboundReady\(candidate, taskResponse, tenantId, orderId\)/)
|
|
assert.match(source, /MEASURABLE_STATUSES\.has\(summary\?\.mainStatus\)/)
|
|
assert.doesNotMatch(source, /MEASURABLE_STATUSES\.has\(summary\?\.processStatus\)/)
|
|
assert.doesNotMatch(source, /if \(success\(response\) && response\.body\?\.data\?\.summary\?\.inboundId\)/)
|
|
})
|
|
|
|
function approvalReplayContext(mutate = () => {}, approvalCode = 'A0409') {
|
|
const inboundId = '40001'; const inboundCode = 'AT-APPROVAL'
|
|
const state = {
|
|
order: { order: { id: orderId, status: 'APPROVED', inboundCode, version: 2 },
|
|
cargoes: [], parties: [], internalNotes: [], statusHistory: [{ id: 'history-order', action: 'APPROVED' }] },
|
|
task: { id: '30001', tenantId, orderId, status: 'WAIT_MEASURE' },
|
|
inbound: { summary: { id: inboundId, tenantId, inboundCode, version: 2, processStatus: 'WAIT_MEASURE' },
|
|
statusHistory: [{ id: 'history-inbound', action: 'ORDER_APPROVED' }], files: [], palletLocations: [] },
|
|
rows: [{ id: inboundId, tenantId, orderId, inboundCode }],
|
|
}
|
|
const calls = []; const checks = []
|
|
const context = { marker: 'APPROVAL-REPLAY', createdResources: { orders: {} },
|
|
request: async (method, requestPath, body, token, headers) => {
|
|
calls.push({ method, requestPath, body, token, headers })
|
|
if (method === 'POST') {
|
|
assert.match(requestPath, /\/orders\/first-leg\/20001\/approve\?tenantId=10001$/)
|
|
assert.equal(body, undefined); assert.equal(headers, undefined)
|
|
mutate(state)
|
|
return { http: 200, body: { code: approvalCode }, requestId: 'repeat-approve', traceId: 'trace-approve' }
|
|
}
|
|
let data
|
|
if (requestPath.includes('/orders/first-leg/')) data = state.order
|
|
else if (requestPath.includes('/tasks/by-order/')) data = state.task
|
|
else if (requestPath.startsWith(`/api/platform/inbounds/${inboundId}?`)) data = state.inbound
|
|
else if (requestPath.startsWith('/api/platform/inbounds?')) data = {
|
|
records: state.rows, page: 1, size: 100, total: state.rows.length, pages: 1,
|
|
}
|
|
else throw new Error(`unexpected ${method} ${requestPath}`)
|
|
return { http: 200, body: { code: '200', data: structuredClone(data) }, requestId: 'read', traceId: 'trace' }
|
|
},
|
|
apiSuccess: (_module, _name, response) => {
|
|
assert.equal(response.http, 200); assert.equal(response.body.code, '200'); return response.body.data
|
|
},
|
|
assert: (_module, name, _response, passed) => {
|
|
checks.push({ name, passed }); assert.ok(passed, name)
|
|
},
|
|
}
|
|
return { context, calls, checks, input: { tenantId, orderId, inboundId, inboundCode } }
|
|
}
|
|
|
|
test('repeated approval requires A0409 and compares order, task, inbound identity, histories and unique count', async () => {
|
|
const fixture = approvalReplayContext()
|
|
const result = await verifyOrderApprovalReplay(fixture.context, fixture.input)
|
|
assert.equal(result.rejected, true)
|
|
assert.equal(result.unchanged, true)
|
|
assert.equal(result.inboundCount, 1)
|
|
assert.equal(result.orderHistoryCount, 1)
|
|
assert.equal(result.inboundHistoryCount, 1)
|
|
assert.equal(result.beforeDigest, result.afterDigest)
|
|
assert.equal(fixture.calls.filter(call => call.method === 'POST').length, 1)
|
|
assert.equal(fixture.context.createdResources.orders.approvalReplay, result)
|
|
assert.equal(Object.hasOwn(result, 'order'), false)
|
|
assert.match(result.beforeDigest, /^[a-f0-9]{64}$/)
|
|
})
|
|
|
|
for (const [name, mutate] of [
|
|
['extra approval history', state => state.order.statusHistory.push({ id: 'repeat-history', action: 'APPROVED' })],
|
|
['changed inbound version', state => state.inbound.summary.version += 1],
|
|
['extra inbound history', state => state.inbound.statusHistory.push({ id: 'repeat-inbound-history' })],
|
|
['new fulfillment task', state => { state.task.id = '30002' }],
|
|
['duplicate source inbound', state => state.rows.push({ ...state.rows[0], id: '40002' })],
|
|
]) {
|
|
test(`approval replay detects ${name} instead of accepting unchanged response status`, async () => {
|
|
const fixture = approvalReplayContext(mutate)
|
|
await assert.rejects(verifyOrderApprovalReplay(fixture.context, fixture.input), /零漂移|唯一入库事实有效/)
|
|
})
|
|
}
|
|
|
|
test('successful repeated approval is not accepted as a substitute for the formal conflict contract', async () => {
|
|
const fixture = approvalReplayContext(() => {}, '200')
|
|
await assert.rejects(verifyOrderApprovalReplay(fixture.context, fixture.input), /再次审核明确A0409/)
|
|
})
|