fix finance acceptance source ownership checks
This commit is contained in:
@@ -75,6 +75,9 @@ function fakeContext(withFixture = true, permissions = financePermissions, denyF
|
||||
if (method === 'GET' && requestPath === '/api/platform/v1/iam/authorization/permissions') {
|
||||
return ok(permissions)
|
||||
}
|
||||
if (method === 'GET' && requestPath.startsWith('/api/platform/v1/iam/tenants?')) {
|
||||
return ok({ records: [{ id: '11', status: 'ACTIVE' }, { id: '12', status: 'ACTIVE' }] })
|
||||
}
|
||||
if (denyFinanceWrites && method === 'POST' && requestPath.endsWith('/statements')) {
|
||||
return { http: 200, body: { code: 'A0403', message: 'Access denied' },
|
||||
requestId: 'request-denied', traceId: 'trace-denied' }
|
||||
@@ -88,6 +91,7 @@ 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 (String(body.customerId) !== '11') return { http: 200, body: { code: 'FIN_404' } }
|
||||
if (missingSubject) return { http: 200, body: { code: 'FIN_409', message: '平台财务主体未配置' },
|
||||
requestId: 'request-subject', traceId: 'trace-subject' }
|
||||
const selected = body.generationMode === 'BY_CUTOFF'
|
||||
@@ -160,6 +164,7 @@ test('静态契约覆盖日期、SELECTED、三 AR、AP、状态机和数组输
|
||||
assert.match(source, /fixture\.root\.receivableSettlements = arRows/)
|
||||
assert.doesNotMatch(source, /generationMode:\s*['"]BY_CUTOFF['"]/)
|
||||
assert.match(source, /\/fee-pool/)
|
||||
assert.match(source, /\/api\/platform\/v1\/iam\/tenants\?page=1&size=100&status=ACTIVE/)
|
||||
assert.match(source, /http: response\?\.http \?\? null/)
|
||||
assert.match(source, /requestId: response\?\.requestId \?\? null/)
|
||||
assert.match(source, /traceId: response\?\.traceId \?\? null/)
|
||||
|
||||
@@ -104,11 +104,16 @@ async function normalizeFixture(context) {
|
||||
supplierName: raw.supplierName ?? raw.partnerName ?? root.supplierName ?? root.partnerName ?? null,
|
||||
}
|
||||
})
|
||||
const uncreatedSources = attemptedSources.filter((source) => !source.feeSourceId || isUnboundContainerCost(source.raw))
|
||||
const hasContainerAllocations = list(root.allocations).length > 0
|
||||
const uncreatedSources = attemptedSources.filter((source) => !source.feeSourceId
|
||||
|| (isUnboundContainerCost(source.raw) && !hasContainerAllocations))
|
||||
return {
|
||||
root, customerId,
|
||||
sources: attemptedSources.filter((source) => source.feeSourceId && !isUnboundContainerCost(source.raw)),
|
||||
uncreatedSources,
|
||||
sourceMappings: attemptedSources.filter((source) => source.feeSourceId && !isUnboundContainerCost(source.raw))
|
||||
.map((source) => ({ sourceFeeId: source.sourceFeeId, feeSourceId: source.feeSourceId,
|
||||
sourceType: source.raw?.sourceType ?? null, direction: source.direction })),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,19 +268,6 @@ export async function run(context) {
|
||||
return
|
||||
}
|
||||
|
||||
const foreign = fixture.sources.find((source) => source.customerId !== fixture.customerId)
|
||||
if (foreign) {
|
||||
const response = await write(context, 'POST', `${BASE}/statements`, {
|
||||
customerId: fixture.customerId, currency: 'CNY', generationMode: 'SELECTED',
|
||||
feeSourceIds: [foreign.feeSourceId], periodStart: foreign.businessDate,
|
||||
periodEnd: foreign.businessDate, remark: `${context.marker}-foreign`,
|
||||
}, 'cross-customer-rejected')
|
||||
context.assert(MODULE, 'SELECTED 拒绝混入其他客户来源', response, rejectedWith(response, 'FIN_409'),
|
||||
`code=${response.body?.code ?? 'missing'}`, OPTIONS)
|
||||
} else {
|
||||
context.addCoverageGap(MODULE, SCENARIO, 'cross-customer-isolation',
|
||||
'Fixture 未提供其他客户 marker 来源,无法执行跨客户负向请求', 'missing-cross-customer-fixture')
|
||||
}
|
||||
const allPeriod = dates(receivables)
|
||||
const currencyResponse = await write(context, 'POST', `${BASE}/statements`, {
|
||||
customerId: fixture.customerId, currency: 'USD', generationMode: 'SELECTED',
|
||||
@@ -331,6 +323,25 @@ export async function run(context) {
|
||||
finish(context, start)
|
||||
return
|
||||
}
|
||||
if (index === 0) {
|
||||
const customerResponse = await context.request('GET', '/api/platform/v1/iam/tenants?page=1&size=100&status=ACTIVE')
|
||||
const otherCustomer = rows(customerResponse?.body?.data)
|
||||
.find((item) => id(item?.id ?? item?.tenantId) !== fixture.customerId)
|
||||
if (otherCustomer) {
|
||||
const otherCustomerId = id(otherCustomer.id ?? otherCustomer.tenantId)
|
||||
const isolationResponse = await write(context, 'POST', `${BASE}/statements`, {
|
||||
customerId: otherCustomerId, currency: 'CNY', generationMode: 'SELECTED',
|
||||
feeSourceIds: [groups[index][0].feeSourceId], periodStart: groups[index][0].businessDate,
|
||||
periodEnd: groups[index][0].businessDate, remark: `${context.marker}-foreign`,
|
||||
}, 'cross-customer-rejected')
|
||||
context.assert(MODULE, 'SELECTED 拒绝归入其他客户的本轮来源', isolationResponse,
|
||||
rejectedWith(isolationResponse, 'FIN_404'),
|
||||
`sourceCustomerId=${fixture.customerId}, requestedCustomerId=${otherCustomerId}, code=${isolationResponse.body?.code ?? 'missing'}`, OPTIONS)
|
||||
} else {
|
||||
context.addCoverageGap(MODULE, SCENARIO, 'cross-customer-isolation',
|
||||
'未找到另一个 ACTIVE 客户,无法用本轮来源执行跨客户归属负向请求', 'missing-cross-customer-fixture')
|
||||
}
|
||||
}
|
||||
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)
|
||||
@@ -414,5 +425,6 @@ export async function run(context) {
|
||||
paymentStatus: payableSettlement.paymentStatus, version: payableSettlement.version,
|
||||
feeSourceIds: payables.map((source) => source.feeSourceId),
|
||||
}] : []
|
||||
fixture.root.sourceMappings = fixture.sourceMappings
|
||||
finish(context, start)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ function fakeContext(overrides = {}) {
|
||||
const billToSettlement = new Map([...settlementToBill].map(([s, b]) => [b, s]))
|
||||
const fixture = { marker: 'T1', customerId: '9', voucherFileId: '77',
|
||||
feeSources: [{ feeSourceId: '501', sourceType: 'MANUAL' }],
|
||||
sourceMappings: [{ sourceFeeId: '9001', feeSourceId: '501', sourceType: 'MANUAL', direction: 'RECEIVABLE' },
|
||||
{ sourceFeeId: '9002', feeSourceId: '601', sourceType: 'MANUAL', direction: 'PAYABLE' }],
|
||||
statements: [
|
||||
{ kind: 'BY_CUTOFF', statementId: '10', status: 'VOIDED', feeSourceIds: [] },
|
||||
{ kind: 'SELECTED_FULL_CHAIN', statementId: '11', status: 'ISSUED', feeSourceIds: ['501'] },
|
||||
@@ -21,7 +23,7 @@ function fakeContext(overrides = {}) {
|
||||
{ settlementId: '102', statementId: '12', direction: 'RECEIVABLE', status: 'ISSUED' },
|
||||
{ settlementId: '103', statementId: '13', direction: 'RECEIVABLE', status: 'ISSUED' },
|
||||
],
|
||||
payableSettlements: [{ settlementId: '201', direction: 'PAYABLE', supplierId: '301', status: 'ISSUED' }],
|
||||
payableSettlements: [{ settlementId: '201', direction: 'PAYABLE', supplierId: '301', status: 'ISSUED', feeSourceIds: ['601'] }],
|
||||
receivablePartialAmount: 40, payablePartialAmount: 40, multiBillAllocationAmount: 10,
|
||||
...overrides }
|
||||
const calls = []; const assertions = []; const gaps = []; const steps = []
|
||||
|
||||
@@ -77,8 +77,12 @@ export async function run(context) {
|
||||
return
|
||||
}
|
||||
|
||||
const manualIds = values(fixtures.feeSources).filter((fee) => fee.sourceType === 'MANUAL').map((fee) => id(fee.feeSourceId))
|
||||
const usedIds = new Set(statements.flatMap((statement) => values(statement.feeSourceIds).map(id)))
|
||||
const manualIds = values(fixtures.sourceMappings).filter((source) => source.sourceType === 'MANUAL')
|
||||
.map((source) => id(source.feeSourceId)).filter(Boolean)
|
||||
const usedIds = new Set([
|
||||
...statements.flatMap((statement) => values(statement.feeSourceIds).map(id)),
|
||||
...payables.flatMap((settlement) => values(settlement.feeSourceIds).map(id)),
|
||||
])
|
||||
context.assert(MODULE, '手动上传费用出账后来源完结', null,
|
||||
manualIds.length > 0 && manualIds.every((feeId) => usedIds.has(feeId)),
|
||||
`manual=${manualIds.length}, settled=${manualIds.filter((feeId) => usedIds.has(feeId)).length}`, OPTIONS)
|
||||
|
||||
Reference in New Issue
Block a user