优化
This commit is contained in:
@@ -403,6 +403,14 @@ export async function run(context) {
|
||||
'使用订单域真实 waybillId、shipmentId 或 orderNo,不使用数据库主键冒充运单标识', OPTIONS)
|
||||
context.createdResources.financeFullFixtures = {
|
||||
marker: context.marker, customerId,
|
||||
prerequisites: {
|
||||
container: containerOwned && Boolean(containerFixture?.feeSourceId) && allocations.length > 0
|
||||
? { ready: true, category: null }
|
||||
: { ready: false, category: 'container-prerequisite', evidence: '缺少同 marker 已完成柜次、有效 containerBookingId、按柜费用来源或真实分摊' },
|
||||
financeSources: fixtures.length > 0
|
||||
? { ready: true, category: null }
|
||||
: { ready: false, category: 'finance-source-prerequisite', evidence: '本轮 marker 未产生可消费 Finance 费用来源' },
|
||||
},
|
||||
orders: measured.map((item) => ({ orderId: id(item.orderId), orderNo: item.orderNo || null,
|
||||
shipmentId: item.shipmentId ? id(item.shipmentId) : null,
|
||||
waybillId: item.waybillId ? id(item.waybillId) : (item.shipmentId ? id(item.shipmentId) : null), inboundId: id(item.inboundId),
|
||||
|
||||
@@ -323,20 +323,25 @@ export async function run(context) {
|
||||
finish(context, start)
|
||||
return
|
||||
}
|
||||
fixture.root.reconciliationPrerequisite = null
|
||||
const permissionResponse = await context.request('GET', '/api/platform/v1/iam/authorization/permissions')
|
||||
const permissions = context.apiSuccess(MODULE, '查询当前账号 Finance 有效权限', permissionResponse, '', OPTIONS)
|
||||
const granted = new Set(list(permissions).map(String))
|
||||
if (granted.has(LEGACY_STATEMENT_VIEW_PERMISSION)) {
|
||||
fixture.root.reconciliationPrerequisite = { ready: false, category: 'permission-prerequisite',
|
||||
required: [STATEMENT_LIST_PERMISSION], legacy: [LEGACY_STATEMENT_VIEW_PERMISSION] }
|
||||
blocker(context, '客户对账单旧 view 权限兼容阻断',
|
||||
`检测到旧权限=${LEGACY_STATEMENT_VIEW_PERMISSION};列表和详情只能以正式权限=${STATEMENT_LIST_PERMISSION} 验收,旧 view 不得作为通过依据`,
|
||||
`检测到旧权限=${LEGACY_STATEMENT_VIEW_PERMISSION};列表和详情只能以正式权限=${STATEMENT_LIST_PERMISSION} 验收,旧 view 不得作为通过依据;需由 IAM 角色初始化为验收账号授予 ${STATEMENT_LIST_PERMISSION} 并移除旧资源,Finance 脚本不兼容旧权限`,
|
||||
'legacy-customer-statement-view-permission', permissionResponse)
|
||||
finish(context, start)
|
||||
return
|
||||
}
|
||||
const missingPermissions = REQUIRED_PERMISSIONS.filter((permission) => !granted.has(permission))
|
||||
if (missingPermissions.length > 0) {
|
||||
fixture.root.reconciliationPrerequisite = { ready: false, category: 'permission-prerequisite',
|
||||
required: missingPermissions, legacy: [] }
|
||||
blocker(context, 'Finance 写权限门禁',
|
||||
`缺少权限=${missingPermissions.join(',')}; 对账接口将返回 A0403,禁止继续写入`,
|
||||
`缺少正式权限资源=${missingPermissions.join(',')}; 需由 IAM 角色初始化授予验收账号;对账接口将返回 A0403,禁止继续写入`,
|
||||
'missing-finance-write-permissions', permissionResponse)
|
||||
finish(context, start)
|
||||
return
|
||||
@@ -353,6 +358,8 @@ export async function run(context) {
|
||||
const invalid = fixture.sources.filter((source) => !source.feeSourceId || !source.customerId
|
||||
|| !source.businessDate || !source.waybillId || source.marker !== context.marker)
|
||||
if (invalid.length > 0) {
|
||||
fixture.root.reconciliationPrerequisite = { ready: false, category: 'finance-source-prerequisite',
|
||||
invalidSourceCount: invalid.length }
|
||||
blocker(context, 'Fixture 来源契约完整',
|
||||
`存在 ${invalid.length} 条来源缺少 feeSourceId/customerId/businessDate/运单标识/marker`,
|
||||
'invalid-finance-fixture-contract')
|
||||
@@ -364,6 +371,8 @@ export async function run(context) {
|
||||
&& source.direction === 'RECEIVABLE' && source.currency === 'CNY')
|
||||
const businessDates = new Set(receivables.map((source) => source.businessDate))
|
||||
if (receivables.length < 4 || businessDates.size < 2) {
|
||||
fixture.root.reconciliationPrerequisite = { ready: false, category: 'finance-source-prerequisite',
|
||||
receivableCount: receivables.length, businessDateCount: businessDates.size }
|
||||
blocker(context, '三张 AR 与日期范围 Fixture 门禁',
|
||||
`至少需要同客户 4 条 CNY 应收来源且跨 2 个真实 businessDate;actual sources=${receivables.length}, dates=${businessDates.size}`,
|
||||
'insufficient-receivable-date-fixture')
|
||||
|
||||
@@ -125,6 +125,9 @@ test('AR/AP 数组为空或不完整时原子阻断且不查询、不写入', as
|
||||
assert.equal(empty.gaps.length, 1)
|
||||
assert.equal(empty.steps[0].passed, false)
|
||||
assert.match(empty.steps[0].evidence, /actual AR=0, AP=0/)
|
||||
assert.match(empty.steps[0].evidence, /上游 Finance fixture 前置条件未满足,非结算服务故障/)
|
||||
assert.match(empty.gaps[0][4], /prerequisite/)
|
||||
assert.equal(empty.calls.some((call) => call.method === 'POST'), false)
|
||||
|
||||
const partial = fakeContext({ receivableSettlements: [
|
||||
{ settlementId: '101', statementId: '11', direction: 'RECEIVABLE', status: 'ISSUED' },
|
||||
@@ -133,4 +136,6 @@ test('AR/AP 数组为空或不完整时原子阻断且不查询、不写入', as
|
||||
assert.equal(partial.calls.length, 0)
|
||||
assert.equal(partial.gaps.length, 1)
|
||||
assert.match(partial.steps[0].evidence, /actual AR=1, AP=1/)
|
||||
assert.match(partial.steps[0].evidence, /receivable-settlement-prerequisite/)
|
||||
assert.equal(partial.calls.some((call) => call.method === 'POST'), false)
|
||||
})
|
||||
|
||||
@@ -128,9 +128,17 @@ export async function run(context) {
|
||||
const arSettlementIds = new Set(validReceivables.map(settlementId))
|
||||
const arStatementIds = new Set(validReceivables.map(statementId))
|
||||
if (validReceivables.length !== 3 || arSettlementIds.size !== 3 || arStatementIds.size !== 3 || validPayables.length !== 1) {
|
||||
const upstream = fixtures.reconciliationPrerequisite?.ready === false
|
||||
? fixtures.reconciliationPrerequisite
|
||||
: Object.values(fixtures.prerequisites || {}).find((item) => item?.ready === false)
|
||||
const missing = []
|
||||
if (statements.filter((item) => item.status === 'ISSUED').length < 3) missing.push('reconciliation-issued-statement-prerequisite')
|
||||
if (validReceivables.length !== 3) missing.push('receivable-settlement-prerequisite')
|
||||
if (validPayables.length !== 1) missing.push('payable-settlement-prerequisite')
|
||||
const category = upstream?.category || (missing.length === 1 ? missing[0] : 'upstream-finance-fixture-prerequisite')
|
||||
gap(context, '取得完整收付结算 Fixture', 'three AR and one AP settlements',
|
||||
`需要 3 张关联本轮 ISSUED 对账单的独立 AR 和 1 张带 supplierId 的 AP;actual AR=${validReceivables.length}, AP=${validPayables.length}`,
|
||||
'incomplete-settlement-fixture')
|
||||
`上游 Finance fixture 前置条件未满足,非结算服务故障;需要 3 张关联本轮 ISSUED 对账单的独立 AR 和 1 张带 supplierId 的 AP;actual AR=${validReceivables.length}, AP=${validPayables.length}; prerequisite=${category}; missing=${missing.join(',') || 'unknown'}`,
|
||||
category)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,19 @@ function validPdf(response) {
|
||||
&& response.bytes?.length > 4 && new TextDecoder().decode(response.bytes.slice(0, 4)) === '%PDF'
|
||||
}
|
||||
|
||||
async function waitForCompletedMeasurement(context, measurementPath, attempts = 30) {
|
||||
let response = null
|
||||
let measurement = null
|
||||
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
||||
response = await context.request('GET', measurementPath)
|
||||
measurement = response?.http === 200 && String(response?.body?.code) === '200'
|
||||
? response.body.data : null
|
||||
if (measurement?.processStatus === 'WAIT_SPLIT') return { response, measurement, attempts: attempt }
|
||||
if (attempt < attempts) await new Promise((resolve) => setTimeout(resolve, 1_000))
|
||||
}
|
||||
return { response, measurement, attempts }
|
||||
}
|
||||
|
||||
async function provisionWarehouseResources(context, { inboundId, warehouseId, totalVolumeCbm }) {
|
||||
const palletCount = Math.ceil(Number(totalVolumeCbm) / DEFAULT_PALLET_CAPACITY_CBM)
|
||||
if (!warehouseId || !Number.isFinite(palletCount) || palletCount < 1) return false
|
||||
@@ -394,8 +407,13 @@ export async function run(context) {
|
||||
context.apiSuccess(MODULE, 'Admin 审核通过墨家测量', approvalResponse,
|
||||
`measurementId=${asId(draft.measurementId)}`, OPTIONS)
|
||||
|
||||
const finalResponse = await context.request('GET', measurementPath)
|
||||
const finalMeasurement = context.apiSuccess(MODULE, '查询审核后的最终测量状态', finalResponse, '', OPTIONS)
|
||||
// Measurement approval may enqueue pricing/inbound completion. Poll the
|
||||
// public projection instead of treating the transient WAIT_PRICING state as
|
||||
// a smart-loading candidate or manufacturing fixture state in the script.
|
||||
const completed = await waitForCompletedMeasurement(context, measurementPath)
|
||||
const finalResponse = completed.response
|
||||
const finalMeasurement = context.apiSuccess(MODULE, '查询审核后的最终测量状态', finalResponse,
|
||||
`pollAttempts=${completed.attempts}`, OPTIONS)
|
||||
context.assert(MODULE, '审核后入库与测量状态闭环且结果未变化', finalResponse,
|
||||
finalMeasurement?.processStatus === 'WAIT_SPLIT' && finalMeasurement?.measureReviewStatus === 'APPROVED'
|
||||
&& finalMeasurement?.measurementStatus === 'APPROVED'
|
||||
@@ -407,6 +425,7 @@ export async function run(context) {
|
||||
context.createdResources.inboundMeasurement = {
|
||||
marker: context.marker, tenantId, inboundId, measurementId: asId(draft.measurementId),
|
||||
boxCode, finalStatus: finalMeasurement?.measurementStatus || null,
|
||||
processStatus: finalMeasurement?.processStatus || null,
|
||||
}
|
||||
finishScenario(context, start)
|
||||
}
|
||||
|
||||
@@ -34,14 +34,20 @@ async function request(context, method, path, body, headers = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function fixture(context) {
|
||||
return context.createdResources?.orchestrationFixtures?.phase3Container
|
||||
|| context.createdResources?.inboundMeasurement
|
||||
|| context.createdResources?.orders
|
||||
export function selectContainerLoadingFixture(context) {
|
||||
const candidates = [
|
||||
context.createdResources?.orchestrationFixtures?.phase3Container,
|
||||
context.createdResources?.inboundMeasurement,
|
||||
context.createdResources?.orders,
|
||||
].filter(Boolean)
|
||||
// Finance and downstream modules temporarily switch context.marker for
|
||||
// their own isolated runs. Never select the older CAB fixture merely because
|
||||
// it is first in the object; ownership is the marker contract.
|
||||
return candidates.find((candidate) => candidate.marker === context.marker) || candidates[0]
|
||||
}
|
||||
|
||||
export async function run(context) {
|
||||
const source = fixture(context)
|
||||
const source = selectContainerLoadingFixture(context)
|
||||
const tenantId = id(source?.tenantId)
|
||||
const inboundId = id(source?.inboundId)
|
||||
if (!tenantId || !inboundId || source?.marker !== context.marker) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import test from 'node:test'
|
||||
|
||||
import { selectCustomsFlow } from './phase3-customs.mjs'
|
||||
import { buildFinalizeResource } from './phase3-loading-finalize.mjs'
|
||||
import { selectContainerLoadingFixture } from './phase3-container-loading.mjs'
|
||||
import { AcceptanceRun } from '../admin-test-env-acceptance.mjs'
|
||||
|
||||
function source(name) {
|
||||
@@ -126,6 +127,7 @@ test('PDA inventory snapshot gates batch and records the only safe mutation gap'
|
||||
|
||||
test('container loading creates and confirms booking before scoped candidates', () => {
|
||||
const value = source('./phase3-container-loading.mjs')
|
||||
assert.match(value, /candidates\.find\(\(candidate\) => candidate\.marker === context\.marker\)/)
|
||||
assertOrder(value, [
|
||||
"const bookingCreate = await request",
|
||||
"const submit = await request",
|
||||
@@ -147,6 +149,33 @@ test('container loading creates and confirms booking before scoped candidates',
|
||||
assert.doesNotMatch(value, /POST', CONTAINERS, containerPayload/)
|
||||
})
|
||||
|
||||
test('container loading selects the current marker instead of a stale CAB fixture', () => {
|
||||
const selected = selectContainerLoadingFixture({
|
||||
marker: 'ADMIN-RUN-AUTO1',
|
||||
createdResources: {
|
||||
orchestrationFixtures: {
|
||||
phase3Container: { marker: 'ADMIN-RUN-CAB', inboundId: 'CAB-INBOUND' },
|
||||
},
|
||||
inboundMeasurement: { marker: 'ADMIN-RUN-AUTO1', inboundId: 'FINANCE-INBOUND' },
|
||||
orders: { marker: 'ADMIN-RUN-AUTO1', inboundId: 'FINANCE-ORDER-INBOUND' },
|
||||
},
|
||||
})
|
||||
assert.equal(selected.inboundId, 'FINANCE-INBOUND')
|
||||
assert.equal(selected.marker, 'ADMIN-RUN-AUTO1')
|
||||
})
|
||||
|
||||
test('container loading keeps an unmatched candidate so marker failure stays observable', () => {
|
||||
const selected = selectContainerLoadingFixture({
|
||||
marker: 'ADMIN-RUN-AUTO1',
|
||||
createdResources: {
|
||||
orchestrationFixtures: {
|
||||
phase3Container: { marker: 'ADMIN-RUN-CAB', inboundId: 'CAB-INBOUND' },
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.equal(selected.marker, 'ADMIN-RUN-CAB')
|
||||
})
|
||||
|
||||
test('container loading enforces channel isolation and stable PDA identity across container number changes', () => {
|
||||
const value = source('./phase3-container-loading.mjs')
|
||||
assertOrder(value, [
|
||||
@@ -269,6 +298,20 @@ test('phase3 loading provisions its own marker-isolated WAIT_SPLIT fixture', asy
|
||||
assert.equal(run.marker.includes('-LOAD'), false)
|
||||
})
|
||||
|
||||
test('container fixture keeps the run marker and does not publish incomplete WAIT_PRICING resources', () => {
|
||||
const entry = source('../admin-test-env-acceptance.mjs')
|
||||
assert.match(entry, /const fixtureMarker = originalMarker/)
|
||||
assert.match(entry, /fixture\.finalStatus !== 'APPROVED'/)
|
||||
assert.match(entry, /禁止回退使用 WAIT_PRICING 订单/)
|
||||
})
|
||||
|
||||
test('measurement fixture polls asynchronous completion before exporting loading identity', () => {
|
||||
const measurement = source('./inbound-measurement.mjs')
|
||||
assert.match(measurement, /waitForCompletedMeasurement/)
|
||||
assert.match(measurement, /measurement\?\.processStatus === 'WAIT_SPLIT'/)
|
||||
assert.match(measurement, /setTimeout\(resolve, 1_000\)/)
|
||||
})
|
||||
|
||||
test('customs and finalize consume the same loading inbound identity', () => {
|
||||
const loadingFixture = { marker: 'RUN-1', tenantId: 10001, orderId: 101, inboundId: 201 }
|
||||
const loading = {
|
||||
|
||||
Reference in New Issue
Block a user