This commit is contained in:
2026-08-13 14:03:54 +08:00
parent 18cebd3fdd
commit bc62650ca9
11 changed files with 349 additions and 18 deletions
@@ -8,11 +8,12 @@ import { run } from './finance-reconciliation-full.mjs'
const here = path.dirname(fileURLToPath(import.meta.url))
const source = fs.readFileSync(path.join(here, 'finance-reconciliation-full.mjs'), 'utf8')
const financePermissions = [
'platform:finance:customer-account:list', 'platform:finance:customer-statement:list',
'platform:finance:reconciliation:create', 'platform:finance:reconciliation:review',
'platform:finance:reconciliation:issue', 'platform:finance:reconciliation:adjust',
'platform:finance:reconciliation:void', 'platform:finance:settlement-ar:create',
'platform:finance:settlement-ar:view', 'platform:finance:settlement-ap:create',
'platform:finance:settlement-ap:view',
'platform:finance:settlement-ar:list', 'platform:finance:settlement-ap:create',
'platform:finance:settlement-ap:list',
]
function ok(data) {
@@ -76,6 +77,12 @@ function fakeContext(withFixture = true, permissions = financePermissions, denyF
if (method === 'GET' && requestPath === '/api/platform/v1/iam/authorization/permissions') {
return ok(permissions)
}
if (method === 'GET' && requestPath === '/api/platform/finance/v1/accounts?page=1&pageSize=20') {
return ok({ records: [{ customerId: '11', accountName: 'Fixture Account' }] })
}
if (method === 'GET' && requestPath.startsWith('/api/platform/finance/v1/accounts/11/ledger?')) {
return ok({ records: [{ customerId: '11', statementId: '1', remark: marker }] })
}
if (method === 'GET' && requestPath.startsWith('/api/platform/v1/iam/tenants?')) {
return ok({ records: [{ id: '11', status: 'ACTIVE' }, { id: '12', status: 'ACTIVE' }] })
}
@@ -100,7 +107,8 @@ function fakeContext(withFixture = true, permissions = financePermissions, denyF
: feeSources.filter((item, index) => body.feeSourceIds.includes(String(1001 + index)))
if (selected.some((item) => item.direction !== 'RECEIVABLE')) return { http: 200, body: { code: 'FIN_409' } }
const statementId = String(++statementSequence)
const statement = { statementId, status: 'DRAFT', version: 0, revisions: [],
const statement = { statementId, customerId: '11', status: 'DRAFT', version: 0, revisions: [],
createdBy: '7001', createdAt: '2026-08-10T09:00:00', updatedBy: '7001', updatedAt: '2026-08-10T09:00:00',
lines: selected.map((item, index) => ({ statementLineId: `${statementId}${index + 1}`,
feeSourceId: String(1001 + feeSources.indexOf(item)), statementAmount: 100 + index, adjustmentReason: null })) }
statements.set(statementId, statement)
@@ -137,6 +145,14 @@ function fakeContext(withFixture = true, permissions = financePermissions, denyF
if (method === 'GET' && requestPath.startsWith('/api/platform/finance/v1/payable-settlements?')) {
return ok([...settlements.values()].filter((item) => item.direction === 'PAYABLE'))
}
if (method === 'GET' && requestPath === '/api/platform/finance/v1/statements?customerId=11') {
return ok([...statements.values()].map((item) => ({
statementId: item.statementId, customerId: item.customerId, status: item.status,
createdBy: item.createdBy, createdAt: item.createdAt, updatedBy: item.updatedBy, updatedAt: item.updatedAt,
})))
}
const detailMatch = requestPath.match(/\/reconciliation-statements\/(\d+)$/)
if (method === 'GET' && detailMatch) return ok(responseForStatement(statements.get(detailMatch[1])))
throw new Error(`Unexpected request: ${method} ${requestPath}`)
},
addStep(module, name, response, passed, evidence = '', options = {}) {
@@ -170,6 +186,16 @@ test('静态契约覆盖日期、SELECTED、三 AR、AP、状态机和数组输
assert.match(source, /const groups = \[cutoffSources/)
assert.match(source, /\/payable-settlements/)
assert.match(source, /findSettlementInOfficialList/)
assert.match(source, /verifyIssuedStatementReadback/)
assert.match(source, /\/statements\?customerId=/)
assert.match(source, /\/reconciliation-statements\/\$\{encodeURIComponent\(targetId\)\}/)
assert.match(source, /statementRows\.every/)
assert.match(source, /Array\.isArray\(detail\?\.lines\)/)
assert.match(source, /Array\.isArray\(detail\?\.revisions\)/)
assert.match(source, /\['createdBy', 'createdAt', 'updatedBy', 'updatedAt'\]/)
assert.match(source, /STATEMENT_LIST_PERMISSION = 'platform:finance:customer-statement:list'/)
assert.match(source, /LEGACY_STATEMENT_VIEW_PERMISSION = 'platform:finance:bill:view'/)
assert.match(source, /legacy-customer-statement-view-permission/)
assert.match(source, /`正式\$\{label\}列表包含同一 settlementId`/)
assert.match(source, /findSettlementInOfficialList\(context, 'RECEIVABLE'/)
assert.match(source, /findSettlementInOfficialList\(context, 'PAYABLE'/)
@@ -181,6 +207,15 @@ test('静态契约覆盖日期、SELECTED、三 AR、AP、状态机和数组输
assert.match(source, /http: response\?\.http \?\? null/)
assert.match(source, /requestId: response\?\.requestId \?\? null/)
assert.match(source, /traceId: response\?\.traceId \?\? null/)
assert.match(source, /platform:finance:customer-account:list/)
assert.match(source, /platform:finance:customer-statement:list/)
assert.match(source, /platform:finance:settlement-ar:list/)
assert.match(source, /platform:finance:settlement-ap:list/)
assert.doesNotMatch(source, /platform:finance:settlement-(ar|ap):view/)
assert.match(source, /\$\{BASE\}\/accounts\?page=1&pageSize=20/)
assert.match(source, /\/accounts\/\$\{encodeURIComponent\(customerId\)\}\/ledger\?page=1&pageSize=200/)
assert.match(source, /balance.*recharge.*prepayment/)
assert.match(source, /accountReadback/)
for (const action of ['SUBMIT', 'APPROVE', 'REJECT', 'ISSUE', 'ADJUST', 'VOID', 'REOPEN']) {
assert.match(source, new RegExp(`['"]${action}['"]`))
}
@@ -210,11 +245,24 @@ test('run 发布一条日期链、三张独立已出账单、三张 AR 和一张
&& call.path.includes('/receivable-settlements?customerId=11')).length, 3)
assert.equal(context.calls.filter((call) => call.method === 'GET'
&& call.path.includes('/payable-settlements?customerId=11')).length, 1)
assert.equal(context.calls.filter((call) => call.method === 'GET'
&& call.path === '/api/platform/finance/v1/statements?customerId=11').length, 3)
assert.equal(context.calls.filter((call) => call.method === 'GET'
&& /\/reconciliation-statements\/\d+$/.test(call.path)).length, 3)
assert.equal(context.calls.some((call) => call.body?.generationMode === 'BY_CUTOFF'), false)
const failedSteps = context.steps.filter((step) => !step.passed)
assert.equal(failedSteps.length, 0, JSON.stringify(failedSteps, null, 2))
})
test('旧客户对账单 view 权限只能形成兼容 blocker,不能作为列表/详情验收通过依据', async () => {
const context = fakeContext(true, [...financePermissions, 'platform:finance:bill:view'])
await run(context)
assert.equal(context.calls.filter((call) => call.method !== 'GET').length, 0)
assert.equal(context.steps.some((step) => !step.passed && step.deploymentBlocker
&& step.evidence.includes('platform:finance:bill:view')), true)
assert.equal(context.coverageGaps.some((gap) => gap.category === 'legacy-customer-statement-view-permission'), true)
})
test('run 在 fixture 缺失时记录 blocker 且不发送 HTTP 请求', async () => {
const context = fakeContext(false)
await run(context)