优化
This commit is contained in:
@@ -19,6 +19,7 @@ x-common: &common
|
||||
REDIS_DATABASE: ${REDIS_DATABASE:-0}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
INTERNAL_SERVICE_TOKEN: ${INTERNAL_SERVICE_TOKEN}
|
||||
BAIYUE_AUDIT_ENDPOINT: ${BAIYUE_AUDIT_ENDPOINT:-http://audit-service:9700/internal/audit/events}
|
||||
SERVER_MAX_HTTP_REQUEST_HEADER_SIZE: ${SERVER_MAX_HTTP_REQUEST_HEADER_SIZE:-32KB}
|
||||
AUDIT_OUTBOX_ENABLED: ${AUDIT_OUTBOX_ENABLED:-false}
|
||||
AUDIT_OUTBOX_PUBLISHER_ENABLED: ${AUDIT_OUTBOX_PUBLISHER_ENABLED:-false}
|
||||
|
||||
@@ -138,6 +138,24 @@ test('run behavior preserves null IDs and deployment blockers when supplier, vou
|
||||
assert.ok(context.steps.some((step) => step.deploymentBlocker && step.name.includes('voucherFileId')))
|
||||
})
|
||||
|
||||
test('run reports missing measurement prerequisites without throwing or borrowing historical fixtures', async () => {
|
||||
const context = fakeContext()
|
||||
context.financeFixtureHooks = {
|
||||
...hookSet(),
|
||||
measureOrder: async () => null,
|
||||
}
|
||||
|
||||
await run(context)
|
||||
|
||||
const fixture = context.createdResources.financeFullFixtures
|
||||
assert.equal(fixture.orders.length, 0)
|
||||
assert.deepEqual(fixture.container, { containerBookingId: null, containerNo: null })
|
||||
assert.deepEqual(fixture.allocations, [])
|
||||
assert.equal(fixture.prerequisites.container.category, 'container-prerequisite')
|
||||
assert.ok(fixture.deploymentBlocker.some((item) => item.includes('缺少已完成柜次')))
|
||||
assert.ok(context.steps.some((step) => step.deploymentBlocker && step.name.includes('真实订单/运单分摊')))
|
||||
})
|
||||
|
||||
test('manual fee approval path keeps accountId as a query parameter', () => {
|
||||
assert.match(source, /basePath = `\/api\/platform\/v1\/inbounds\/.*fee-details`/)
|
||||
assert.match(source, /\$\{basePath\}\/\$\{feeId\}\/approve\?accountId=/)
|
||||
|
||||
@@ -231,6 +231,11 @@ async function loadPricingEvidence(context) {
|
||||
|
||||
async function createMarkerContainer(context, resource) {
|
||||
if (context.financeFixtureHooks?.createMarkerContainer) return context.financeFixtureHooks.createMarkerContainer(context, resource)
|
||||
if (!resource) {
|
||||
blocker(context, '取得可创建柜次的测量入库单',
|
||||
'本轮没有完成测量的 marker-owned 入库单;跳过柜次及整柜成本 fixture,等待订单/测量前置修复')
|
||||
return null
|
||||
}
|
||||
if (context.financeFixtureHooks) return null
|
||||
const original = context.createdResources.phase3ContainerLoading
|
||||
const originalMarker = context.marker
|
||||
@@ -349,7 +354,7 @@ export async function run(context) {
|
||||
}
|
||||
}
|
||||
|
||||
const container = await createMarkerContainer(context, measured[0])
|
||||
const container = (measured[0] ? await createMarkerContainer(context, measured[0]) : null)
|
||||
|| context.createdResources?.phase3ContainerLoading
|
||||
const containerOwned = (container?.marker === context.marker || String(container?.marker || '').startsWith(`${context.marker}-`))
|
||||
&& container?.loadingPlanId && container?.containerBookingId
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
import { approvedInboundReady } 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 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\)/)
|
||||
})
|
||||
@@ -16,6 +16,14 @@ function pageOk(value) { return Array.isArray(value?.records) && Number.isFinite
|
||||
function success(response) { return response.http === 200 && String(response.body?.code) === '200' }
|
||||
function sleep(milliseconds) { return new Promise((resolve) => setTimeout(resolve, milliseconds)) }
|
||||
function key(context, operation) { return `${context.marker}-${operation}`.slice(0, 128) }
|
||||
export function approvedInboundReady(orderDetail, taskResponse, tenantId, orderId) {
|
||||
const summary = orderDetail?.summary
|
||||
const task = success(taskResponse) ? taskResponse.body?.data : null
|
||||
return Boolean(summary?.inboundId)
|
||||
&& MEASURABLE_STATUSES.has(summary?.mainStatus)
|
||||
&& id(task?.orderId) === id(orderId)
|
||||
&& id(task?.tenantId) === id(tenantId)
|
||||
}
|
||||
function tomorrow(days = 30) {
|
||||
const date = new Date(); date.setUTCDate(date.getUTCDate() + days)
|
||||
return date.toISOString().slice(0, 10)
|
||||
@@ -202,18 +210,27 @@ async function assertDimensionConfig(context, tenantId) {
|
||||
|
||||
async function waitForInbound(context, tenantId, orderId) {
|
||||
const path = `/api/platform/v1/orders/${encodeURIComponent(orderId)}?tenantId=${encodeURIComponent(tenantId)}`
|
||||
let response = null; let detail = null
|
||||
for (let attempt = 1; attempt <= 20; attempt += 1) {
|
||||
const taskPath = `/api/platform/v1/first-leg/tasks/by-order/${encodeURIComponent(orderId)}?tenantId=${encodeURIComponent(tenantId)}`
|
||||
let response = null; let detail = null; let taskResponse = null; let task = null
|
||||
for (let attempt = 1; attempt <= 30; attempt += 1) {
|
||||
response = await context.request('GET', path)
|
||||
if (success(response) && response.body?.data?.summary?.inboundId) {
|
||||
detail = response.body.data
|
||||
const candidate = success(response) ? response.body?.data : null
|
||||
if (candidate?.summary?.inboundId) {
|
||||
taskResponse = await context.request('GET', taskPath)
|
||||
}
|
||||
if (approvedInboundReady(candidate, taskResponse, tenantId, orderId)) {
|
||||
detail = candidate
|
||||
task = taskResponse.body.data
|
||||
break
|
||||
}
|
||||
if (attempt < 20) await sleep(1_000)
|
||||
if (attempt < 30) await sleep(1_000)
|
||||
}
|
||||
context.addStep(MODULE, '审核后生成隔离入库单', response, Boolean(detail),
|
||||
detail ? `inboundId=${detail.summary.inboundId}` : '20秒内未返回 inboundId', CONTRACT_OPTIONS)
|
||||
return { response, detail }
|
||||
context.addStep(MODULE, '审核后入库单与履约任务异步收敛', response, Boolean(detail && task),
|
||||
detail
|
||||
? `inboundId=${detail.summary.inboundId}, status=${detail.summary.mainStatus}, taskId=${id(task?.id)}`
|
||||
: `30秒内未收敛;status=${response?.body?.data?.summary?.mainStatus ?? 'missing'}, taskCode=${taskResponse?.body?.code ?? 'not-requested'}`,
|
||||
CONTRACT_OPTIONS)
|
||||
return { response, detail, taskResponse, task }
|
||||
}
|
||||
|
||||
export async function run(context) {
|
||||
@@ -320,7 +337,9 @@ export async function run(context) {
|
||||
|
||||
const inboundResult = await waitForInbound(context, tenantId, orderId)
|
||||
const inboundId = id(inboundResult.detail?.summary?.inboundId)
|
||||
if (!inboundId) throw new Error('订单审核后的入库单契约未部署,无法向 OCR/墨家模块交接')
|
||||
if (!inboundId || !inboundResult.task) {
|
||||
throw new Error('订单审核后的入库单与履约任务未在30秒内收敛,无法向 OCR/墨家模块交接')
|
||||
}
|
||||
|
||||
const inboundPath = `/api/platform/inbounds/${encodeURIComponent(inboundId)}?tenantId=${encodeURIComponent(tenantId)}`
|
||||
const inboundResponse = await context.request('GET', inboundPath)
|
||||
@@ -358,10 +377,10 @@ export async function run(context) {
|
||||
context.context.testInbound = context.context.inboundWeightOcr
|
||||
context.context.createdInbound = context.context.inboundWeightOcr
|
||||
|
||||
const taskResponse = await context.request('GET',
|
||||
`/api/platform/v1/first-leg/tasks/by-order/${encodeURIComponent(orderId)}?tenantId=${encodeURIComponent(tenantId)}`)
|
||||
const task = context.apiSuccess(MODULE, 'Admin 查询本轮订单履约任务', taskResponse,
|
||||
'新订单应生成可查询的履约任务', CONTRACT_OPTIONS)
|
||||
const taskResponse = inboundResult.taskResponse
|
||||
const task = inboundResult.task
|
||||
context.apiSuccess(MODULE, 'Admin 查询本轮订单履约任务', taskResponse,
|
||||
'审批 Outbox 收敛后应生成可查询的履约任务', CONTRACT_OPTIONS)
|
||||
context.assert(MODULE, '履约任务与本轮订单和客户一致', taskResponse,
|
||||
id(task?.orderId) === orderId && id(task?.tenantId) === tenantId,
|
||||
`taskId=${id(task?.id) || 'missing'}, status=${task?.status ?? 'missing'}`, CONTRACT_OPTIONS)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import test from 'node:test'
|
||||
|
||||
const read = async (path) => (await readFile(new URL(`../${path}`, import.meta.url), 'utf8'))
|
||||
.replaceAll('\r\n', '\n')
|
||||
|
||||
test('基础 Compose 显式配置审计服务地址,避免静默退化为日志输出', async () => {
|
||||
const compose = await read('deploy/docker-compose.yml')
|
||||
|
||||
assert.match(compose,
|
||||
/BAIYUE_AUDIT_ENDPOINT: \$\{BAIYUE_AUDIT_ENDPOINT:-http:\/\/audit-service:9700\/internal\/audit\/events\}/)
|
||||
assert.doesNotMatch(compose, /^\s+AUDIT_SERVICE_URL:/m)
|
||||
assert.match(compose, /^ audit-service:\s*$/m)
|
||||
assert.match(compose, /container_name: firstleg-\$\{DEPLOY_ENV:-test\}-audit/)
|
||||
assert.match(compose, /networks: \[firstleg-network, infra-network\]/)
|
||||
})
|
||||
|
||||
test('可靠审计仍由独立 Compose 显式启用,不强制基础环境依赖 RocketMQ', async () => {
|
||||
const baseCompose = await read('deploy/docker-compose.yml')
|
||||
const reliableCompose = await read('deploy/docker-compose.audit.yml')
|
||||
|
||||
assert.match(baseCompose, /AUDIT_OUTBOX_ENABLED: \$\{AUDIT_OUTBOX_ENABLED:-false\}/)
|
||||
assert.match(reliableCompose, /AUDIT_OUTBOX_ENABLED: "true"/)
|
||||
assert.match(reliableCompose, /AUDIT_MQ_CONSUMER_ENABLED: "true"/)
|
||||
})
|
||||
Reference in New Issue
Block a user