完善财务全量验收链路
This commit is contained in:
@@ -7,12 +7,18 @@ 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: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-ap:create',
|
||||
]
|
||||
|
||||
function ok(data) {
|
||||
return { http: 200, body: { code: '200', data }, requestId: 'request-test', traceId: 'trace-test' }
|
||||
}
|
||||
|
||||
function fakeContext(withFixture = true) {
|
||||
function fakeContext(withFixture = true, permissions = financePermissions, denyFinanceWrites = false) {
|
||||
const marker = 'FIN-TEST-MARKER'
|
||||
const receivables = [
|
||||
{ feeSourceId: '101', orderId: '201', direction: 'RECEIVABLE', currency: 'CNY', businessDate: '2026-08-10' },
|
||||
@@ -26,6 +32,9 @@ function fakeContext(withFixture = true) {
|
||||
{ feeSourceId: null, sourceType: 'MANUAL', direction: 'PAYABLE', currency: 'CNY',
|
||||
businessDate: '2026-08-10', billingBasis: 'CONTAINER_SHARED', partnerId: '501',
|
||||
containerBookingId: 'CB-FAILED' }]
|
||||
feeSources.push({ feeSourceId: '106', sourceType: 'MANUAL', direction: 'PAYABLE', currency: 'CNY',
|
||||
businessDate: '2026-08-10', billingBasis: 'CONTAINER_SHARED', partnerId: '501',
|
||||
containerBookingId: 'CB-COST-RESPONSE' })
|
||||
const fixture = { marker, customerId: '11',
|
||||
orders: feeSources.filter((item) => item.orderId)
|
||||
.map((item) => ({ orderId: item.orderId, shipmentId: `SHIP-${item.orderId}` })),
|
||||
@@ -63,6 +72,13 @@ function fakeContext(withFixture = true) {
|
||||
calls.push({ method, path: requestPath, body, headers })
|
||||
const idem = headers['Idempotency-Key']
|
||||
if (idem && idempotency.has(idem)) return ok(structuredClone(idempotency.get(idem)))
|
||||
if (method === 'GET' && requestPath === '/api/platform/v1/iam/authorization/permissions') {
|
||||
return ok(permissions)
|
||||
}
|
||||
if (denyFinanceWrites && method === 'POST' && requestPath.endsWith('/statements')) {
|
||||
return { http: 200, body: { code: 'A0403', message: 'Access denied' },
|
||||
requestId: 'request-denied', traceId: 'trace-denied' }
|
||||
}
|
||||
if (method === 'GET' && requestPath.includes('/fee-pool')) {
|
||||
return ok(receivables.map((item) => ({ ...item, reviewStatus: 'APPROVED',
|
||||
settlementIncluded: true, sourceStatus: 'AVAILABLE' })))
|
||||
@@ -126,6 +142,10 @@ function fakeContext(withFixture = true) {
|
||||
|
||||
test('静态契约覆盖日期、SELECTED、三 AR、AP、状态机和数组输出', () => {
|
||||
assert.match(source, /generationMode === 'SELECTED'/)
|
||||
assert.match(source, /platform:finance:reconciliation:create/)
|
||||
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, /businessDates\.size < 2/)
|
||||
assert.match(source, /receivables\.length < 4/)
|
||||
assert.match(source, /const groups = \[cutoffSources/)
|
||||
@@ -156,9 +176,11 @@ test('run 发布一条日期链、三张独立已出账单、三张 AR 和一张
|
||||
assert.equal(fixture.payableSettlements[0].supplierId, '501')
|
||||
assert.equal(fixture.payableSettlements[0].partnerId, '501')
|
||||
assert.equal(fixture.payableSettlements[0].supplierName, 'Fixture Partner')
|
||||
assert.equal(context.coverageGaps.some((gap) => gap.category === 'uncreated-fee-source-attempt'
|
||||
&& gap.reason.includes('containerBookingId=CB-FAILED')), true)
|
||||
assert.equal(context.coverageGaps.some((gap) => gap.category === 'incomplete-container-cost-attempt'
|
||||
&& gap.reason.includes('containerBookingId=CB-FAILED')
|
||||
&& gap.reason.includes('containerBookingId=CB-COST-RESPONSE')), true)
|
||||
assert.equal(context.calls.some((call) => call.body?.feeSourceIds?.includes(null)), false)
|
||||
assert.equal(context.calls.some((call) => call.body?.feeSourceIds?.includes('106')), false)
|
||||
assert.equal(context.calls.some((call) => call.path.includes('/fee-pool')
|
||||
|| call.body?.generationMode === 'BY_CUTOFF'), false)
|
||||
assert.equal(context.steps.filter((step) => !step.passed).length, 0)
|
||||
@@ -181,3 +203,25 @@ test('PAYABLE 接受 partnerId 及名称快照,不依赖 SUPPLIER 类型', asy
|
||||
assert.equal(payableRequest.body.supplierId, '501')
|
||||
assert.doesNotMatch(source, /partnerType|types\?\.includes|SUPPLIER.*阻断/)
|
||||
})
|
||||
|
||||
test('缺少 Finance 创建权限时 fail-closed 且不发送财务写请求', async () => {
|
||||
const context = fakeContext(true, financePermissions.filter((item) => item !== 'platform:finance:reconciliation:create'))
|
||||
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:reconciliation:create')), true)
|
||||
assert.equal(context.coverageGaps.some((gap) => gap.category === 'missing-finance-write-permissions'), true)
|
||||
})
|
||||
|
||||
test('权限快照已授权但 Finance 返回 A0403 时立即停止并保留诊断', async () => {
|
||||
const context = fakeContext(true, financePermissions, true)
|
||||
await run(context)
|
||||
const writes = context.calls.filter((call) => call.method === 'POST' && call.path.endsWith('/statements'))
|
||||
assert.equal(writes.length, 1)
|
||||
assert.equal(writes[0].body.customerId, '11')
|
||||
assert.equal(writes[0].body.currency, 'USD')
|
||||
assert.equal(context.coverageGaps.some((gap) => gap.category === 'stale-finance-token-authorities'
|
||||
&& gap.reason.includes('requestId=request-denied') && gap.reason.includes('traceId=trace-denied')), true)
|
||||
assert.equal(context.calls.some((call) => call.body?.generationMode === 'SELECTED'
|
||||
&& call.body?.currency === 'CNY'), false)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user