From 4375beaa13a50987b06831c32cf75346adf158d1 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 12 Aug 2026 13:55:16 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=E8=B4=A2=E5=8A=A1=E4=B8=BB?= =?UTF-8?q?=E4=BD=93=E7=BC=BA=E5=A4=B1=E9=98=BB=E6=96=AD=E8=AF=8A=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...inance-reconciliation-full-contract.test.mjs | 17 ++++++++++++++++- .../finance-reconciliation-full.mjs | 11 +++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/scripts/admin-test-env-modules/finance-reconciliation-full-contract.test.mjs b/scripts/admin-test-env-modules/finance-reconciliation-full-contract.test.mjs index 784cf0c93..3a2c20606 100644 --- a/scripts/admin-test-env-modules/finance-reconciliation-full-contract.test.mjs +++ b/scripts/admin-test-env-modules/finance-reconciliation-full-contract.test.mjs @@ -18,7 +18,7 @@ function ok(data) { return { http: 200, body: { code: '200', data }, requestId: 'request-test', traceId: 'trace-test' } } -function fakeContext(withFixture = true, permissions = financePermissions, denyFinanceWrites = false) { +function fakeContext(withFixture = true, permissions = financePermissions, denyFinanceWrites = false, missingSubject = false) { const marker = 'FIN-TEST-MARKER' const receivables = [ { feeSourceId: '101', orderId: '201', direction: 'RECEIVABLE', currency: 'CNY', businessDate: '2026-08-10' }, @@ -85,6 +85,8 @@ function fakeContext(withFixture = true, permissions = financePermissions, denyF } if (method === 'POST' && requestPath.endsWith('/statements')) { if (body.currency !== 'CNY') return { http: 200, body: { code: 'FIN_400' } } + if (missingSubject) return { http: 200, body: { code: 'FIN_409', message: '平台财务主体未配置' }, + requestId: 'request-subject', traceId: 'trace-subject' } const selected = body.generationMode === 'BY_CUTOFF' ? receivables.filter((item) => item.businessDate >= body.periodStart && item.businessDate <= body.periodEnd) : feeSources.filter((item) => body.feeSourceIds.includes(item.feeSourceId)) @@ -146,6 +148,7 @@ test('静态契约覆盖日期、SELECTED、三 AR、AP、状态机和数组输 assert.match(source, /\/api\/platform\/v1\/iam\/authorization\/permissions/) assert.match(source, /rejectedWith\(currencyResponse, 'FIN_400'\)/) assert.match(source, /stale-finance-token-authorities/) + assert.match(source, /missing-active-finance-subject/) assert.match(source, /businessDates\.size < 2/) assert.match(source, /receivables\.length < 4/) assert.match(source, /const groups = \[cutoffSources/) @@ -225,3 +228,15 @@ test('权限快照已授权但 Finance 返回 A0403 时立即停止并保留诊 assert.equal(context.calls.some((call) => call.body?.generationMode === 'SELECTED' && call.body?.currency === 'CNY'), false) }) + +test('平台财务主体缺失时报明确 blocker 且不继续状态流转', async () => { + const context = fakeContext(true, financePermissions, false, true) + await run(context) + assert.equal(context.coverageGaps.some((gap) => gap.category === 'missing-active-finance-subject' + && gap.reason.includes('requestId=request-subject') && gap.reason.includes('traceId=trace-subject')), true) + assert.equal(context.calls.filter((call) => call.method === 'POST' + && call.path.endsWith('/statements')).length, 2) + assert.equal(context.calls.some((call) => call.path.includes('/actions') + || call.path.endsWith('/receivable-settlements') || call.path.endsWith('/payable-settlements')), false) + assert.equal(context.steps.some((step) => step.name === '第 1 张对账单出账'), false) +}) diff --git a/scripts/admin-test-env-modules/finance-reconciliation-full.mjs b/scripts/admin-test-env-modules/finance-reconciliation-full.mjs index b908d52ae..480daabcb 100644 --- a/scripts/admin-test-env-modules/finance-reconciliation-full.mjs +++ b/scripts/admin-test-env-modules/finance-reconciliation-full.mjs @@ -29,6 +29,10 @@ function rejectedWith(response, code) { return response?.http === 200 && String(response?.body?.code) === code } function accessDenied(response) { return rejectedWith(response, 'A0403') } +function financeSubjectMissing(response) { + return rejectedWith(response, 'FIN_409') + && String(response?.body?.message ?? '').includes('平台财务主体未配置') +} function sameIds(actual, expected) { const left = [...new Set(actual)].sort() const right = [...new Set(expected)].sort() @@ -301,6 +305,13 @@ export async function run(context) { finish(context, start) return } + if (financeSubjectMissing(created.response)) { + blocker(context, '平台财务主体 Fixture 门禁', + `POST ${BASE}/statements 返回 FIN_409 平台财务主体未配置;sourceIds=${created.body.feeSourceIds.join(',')}, requestId=${created.response.requestId ?? 'null'}, traceId=${created.response.traceId ?? 'null'};需在测试库 fin_subject 中提供 ACTIVE 主体`, + 'missing-active-finance-subject', created.response) + finish(context, start) + return + } context.assert(MODULE, `第 ${index + 1} 张仅使用分组来源`, created.response, created.statement && sameIds(lineSourceIds(created.statement), groups[index].map((source) => source.feeSourceId)), `expected=${groups[index].length}, actual=${lineSourceIds(created.statement).length}`, OPTIONS)