完善财务全量验收链路
This commit is contained in:
@@ -2,6 +2,15 @@ const MODULE = 'finance-reconciliation-full'
|
||||
const SCENARIO = 'finance-full-reconciliation'
|
||||
const BASE = '/api/platform/finance/v1'
|
||||
const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true }
|
||||
const REQUIRED_PERMISSIONS = [
|
||||
'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 id(value) { return value == null ? '' : String(value) }
|
||||
function list(value) { return Array.isArray(value) ? value : [] }
|
||||
@@ -11,8 +20,15 @@ function settlementId(value) { return id(value?.settlementId ?? value?.id) }
|
||||
function sourceId(value) { return id(value?.feeSourceId ?? value?.financeFeeSourceId ?? value?.id) }
|
||||
function sourceDate(value) { return value?.businessDate ?? value?.billingDate ?? value?.feeDate ?? '' }
|
||||
function sourceWaybill(value) { return id(value?.waybillId ?? value?.shipmentId ?? value?.orderId ?? value?.inboundId ?? value?.sourceNo) }
|
||||
function isUnboundContainerCost(raw) {
|
||||
return raw?.sourceType === 'MANUAL' && raw?.billingBasis === 'CONTAINER_SHARED'
|
||||
&& !sourceWaybill(raw)
|
||||
}
|
||||
function lineSourceIds(value) { return list(value?.lines).map(sourceId).filter(Boolean) }
|
||||
function apiRejected(response) { return response?.http === 200 && response?.body && String(response.body.code) !== '200' }
|
||||
function rejectedWith(response, code) {
|
||||
return response?.http === 200 && String(response?.body?.code) === code
|
||||
}
|
||||
function accessDenied(response) { return rejectedWith(response, 'A0403') }
|
||||
function sameIds(actual, expected) {
|
||||
const left = [...new Set(actual)].sort()
|
||||
const right = [...new Set(expected)].sort()
|
||||
@@ -65,10 +81,11 @@ function normalizeFixture(context) {
|
||||
supplierName: raw.supplierName ?? raw.partnerName ?? root.supplierName ?? root.partnerName ?? null,
|
||||
}
|
||||
})
|
||||
const uncreatedSources = attemptedSources.filter((source) => !source.feeSourceId || isUnboundContainerCost(source.raw))
|
||||
return {
|
||||
root, customerId,
|
||||
sources: attemptedSources.filter((source) => source.feeSourceId),
|
||||
uncreatedSources: attemptedSources.filter((source) => !source.feeSourceId),
|
||||
sources: attemptedSources.filter((source) => source.feeSourceId && !isUnboundContainerCost(source.raw)),
|
||||
uncreatedSources,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,14 +199,25 @@ export async function run(context) {
|
||||
finish(context, start)
|
||||
return
|
||||
}
|
||||
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))
|
||||
const missingPermissions = REQUIRED_PERMISSIONS.filter((permission) => !granted.has(permission))
|
||||
if (missingPermissions.length > 0) {
|
||||
blocker(context, 'Finance 写权限门禁',
|
||||
`缺少权限=${missingPermissions.join(',')}; 对账接口将返回 A0403,禁止继续写入`,
|
||||
'missing-finance-write-permissions', permissionResponse)
|
||||
finish(context, start)
|
||||
return
|
||||
}
|
||||
if (fixture.uncreatedSources.length > 0) {
|
||||
const evidence = fixture.uncreatedSources.map((source) => {
|
||||
const raw = source.raw
|
||||
return `sourceType=${raw.sourceType ?? 'missing'}, billingBasis=${raw.billingBasis ?? 'missing'}, containerBookingId=${id(raw.containerBookingId) || 'missing'}`
|
||||
return `sourceType=${raw.sourceType ?? 'missing'}, billingBasis=${raw.billingBasis ?? 'missing'}, feeSourceId=${source.feeSourceId || 'missing'}, containerBookingId=${id(raw.containerBookingId) || 'missing'}`
|
||||
}).join('; ')
|
||||
context.addCoverageGap(MODULE, SCENARIO, '忽略未创建的费用来源尝试',
|
||||
`Finance 未返回 feeSourceId,不得作为费用来源消费:${evidence}`,
|
||||
'uncreated-fee-source-attempt', { marker: context.marker, count: fixture.uncreatedSources.length })
|
||||
context.addCoverageGap(MODULE, SCENARIO, '忽略不完整的柜次成本尝试',
|
||||
`Finance 未返回 feeSourceId 或逐订单/运单 allocation 关联,不得作为费用来源消费:${evidence}`,
|
||||
'incomplete-container-cost-attempt', { marker: context.marker, count: fixture.uncreatedSources.length })
|
||||
}
|
||||
const invalid = fixture.sources.filter((source) => !source.feeSourceId || !source.customerId
|
||||
|| !source.businessDate || !source.waybillId || source.marker !== context.marker)
|
||||
@@ -219,7 +247,7 @@ export async function run(context) {
|
||||
feeSourceIds: [foreign.feeSourceId], periodStart: foreign.businessDate,
|
||||
periodEnd: foreign.businessDate, remark: `${context.marker}-foreign`,
|
||||
}, 'cross-customer-rejected')
|
||||
context.assert(MODULE, 'SELECTED 拒绝混入其他客户来源', response, apiRejected(response),
|
||||
context.assert(MODULE, 'SELECTED 拒绝混入其他客户来源', response, rejectedWith(response, 'FIN_409'),
|
||||
`code=${response.body?.code ?? 'missing'}`, OPTIONS)
|
||||
} else {
|
||||
context.addCoverageGap(MODULE, SCENARIO, 'cross-customer-isolation',
|
||||
@@ -231,7 +259,14 @@ export async function run(context) {
|
||||
feeSourceIds: receivables.map((source) => source.feeSourceId),
|
||||
periodStart: allPeriod.start, periodEnd: allPeriod.end, remark: `${context.marker}-usd`,
|
||||
}, 'currency-rejected')
|
||||
context.assert(MODULE, '对账单拒绝非 CNY 币种', currencyResponse, apiRejected(currencyResponse),
|
||||
if (accessDenied(currencyResponse)) {
|
||||
blocker(context, 'Finance 对账写权限与当前令牌不一致',
|
||||
`POST ${BASE}/statements 返回 A0403 Access denied;customerId=${fixture.customerId}, currency=USD, requestId=${currencyResponse.requestId ?? 'null'}, traceId=${currencyResponse.traceId ?? 'null'};有效权限查询与 Finance JWT authority 不一致,禁止继续写入`,
|
||||
'stale-finance-token-authorities', currencyResponse)
|
||||
finish(context, start)
|
||||
return
|
||||
}
|
||||
context.assert(MODULE, '对账单拒绝非 CNY 币种', currencyResponse, rejectedWith(currencyResponse, 'FIN_400'),
|
||||
`code=${currencyResponse.body?.code ?? 'missing'}`, OPTIONS)
|
||||
|
||||
const cutoffDate = [...businessDates].sort()[0]
|
||||
@@ -259,6 +294,13 @@ export async function run(context) {
|
||||
for (let index = 0; index < groups.length; index += 1) {
|
||||
const created = await createStatement(context, groups[index], `selected-${index + 1}`,
|
||||
`创建第 ${index + 1} 张独立 SELECTED 对账单`)
|
||||
if (accessDenied(created.response)) {
|
||||
blocker(context, `第 ${index + 1} 张 SELECTED 写权限`,
|
||||
`POST ${BASE}/statements 返回 A0403 Access denied;customerId=${created.body.customerId}, currency=${created.body.currency}, sourceIds=${created.body.feeSourceIds.join(',')}, requestId=${created.response.requestId ?? 'null'}, traceId=${created.response.traceId ?? 'null'};请求符合 DTO,但当前令牌缺少 platform:finance:reconciliation:create authority`,
|
||||
'finance-reconciliation-create-forbidden', 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)
|
||||
|
||||
Reference in New Issue
Block a user