优化
This commit is contained in:
@@ -21,8 +21,8 @@ test('fixture covers weight, volume, manual order, and container allocation cont
|
||||
})
|
||||
|
||||
test('fee writes use verified Firstleg paths and do not fabricate historical IDs', () => {
|
||||
assert.match(source, /\/api\/platform\/v1\/inbounds\/.*fee-details\?accountId=/)
|
||||
assert.match(source, /\/api\/platform\/v1\/loading-plans\/.*fee-details\?accountId=/)
|
||||
assert.match(source, /listPath = `\$\{basePath\}\?accountId=/)
|
||||
assert.match(source, /const basePath = `\/api\/platform\/v1\/loading-plans\//)
|
||||
assert.match(source, /sourceType: detail\?\.sourceType \|\| 'MANUAL'/)
|
||||
assert.match(source, /created\?\.marker === context\.marker/)
|
||||
assert.match(source, /\/fees`/)
|
||||
@@ -91,13 +91,15 @@ test('run behavior emits consumer shape, both billing bases, distinct dates, sup
|
||||
assert.equal(fixture.orders[0].waybillId, 'WB-1')
|
||||
assert.equal(fixture.orders[0].actualWeightKg, 12.5)
|
||||
assert.equal(fixture.orders[0].totalVolumeCbm, 0.072)
|
||||
assert.deepEqual(new Set(fixture.feeSources.map((item) => item.billingBasis).filter((value) => value !== 'MANUAL_ORDER_FEE')),
|
||||
assert.deepEqual(new Set(fixture.feeSources.map((item) => item.billingBasis).filter((value) => ['WEIGHT_KG', 'VOLUME_CBM'].includes(value))),
|
||||
new Set(['WEIGHT_KG', 'VOLUME_CBM']))
|
||||
assert.ok(new Set(fixture.feeSources.map((item) => item.businessDate)).size >= 2)
|
||||
assert.equal(fixture.supplier.supplierId, 'PARTNER-1')
|
||||
assert.equal(fixture.supplier.supplierName, '往来单位一')
|
||||
assert.equal(fixture.voucherFileId, 'VOUCHER-1')
|
||||
assert.ok(Array.isArray(fixture.allocations))
|
||||
assert.ok(fixture.feeSources.filter((item) => item.direction === 'RECEIVABLE').length >= 4)
|
||||
assert.ok(new Set(fixture.feeSources.filter((item) => item.direction === 'RECEIVABLE').map((item) => item.businessDate)).size >= 2)
|
||||
})
|
||||
|
||||
test('run behavior preserves null IDs and deployment blockers when supplier, voucher and container are unavailable', async () => {
|
||||
@@ -113,3 +115,16 @@ test('run behavior preserves null IDs and deployment blockers when supplier, vou
|
||||
assert.ok(context.steps.some((step) => step.deploymentBlocker && step.name.includes('往来单位')))
|
||||
assert.ok(context.steps.some((step) => step.deploymentBlocker && step.name.includes('voucherFileId')))
|
||||
})
|
||||
|
||||
test('manual fee approval path keeps accountId as a query parameter', () => {
|
||||
assert.match(source, /basePath = `\/api\/platform\/v1\/inbounds\/.*fee-details`/)
|
||||
assert.match(source, /\$\{basePath\}\/\$\{feeId\}\/approve\?accountId=/)
|
||||
assert.doesNotMatch(source, /fee-details\?accountId=\$\{encodeURIComponent\(resource\.tenantId\)\}`\)/)
|
||||
})
|
||||
|
||||
test('fixture creates four real receivable manual sources before payable source', () => {
|
||||
assert.match(source, /const receivableResources = measured\.slice\(0, 4\)/)
|
||||
assert.match(source, /feeItems\.RECEIVABLE, 'RECEIVABLE'/)
|
||||
assert.match(source, /'MANUAL_RECEIVABLE'/)
|
||||
assert.match(source, /dateOffset\(-1 - \(index % 2\)\)/)
|
||||
})
|
||||
|
||||
@@ -77,17 +77,18 @@ async function loadFeeItem(context, direction) {
|
||||
|
||||
async function createManualFee(context, resource, feeItem, direction, amount, basis, businessDate) {
|
||||
if (context.financeFixtureHooks?.createManualFee) return context.financeFixtureHooks.createManualFee(context, resource, feeItem, direction, amount, basis, businessDate)
|
||||
const path = `/api/platform/v1/inbounds/${encodeURIComponent(resource.inboundId)}/fee-details?accountId=${encodeURIComponent(resource.tenantId)}`
|
||||
const basePath = `/api/platform/v1/inbounds/${encodeURIComponent(resource.inboundId)}/fee-details`
|
||||
const listPath = `${basePath}?accountId=${encodeURIComponent(resource.tenantId)}`
|
||||
const body = { feeItemId: Number(feeItem.id), direction, businessDate,
|
||||
unitPrice: amount, currency: 'CNY', amount, settlementIncluded: true, remark: `${context.marker}-${basis}` }
|
||||
const response = await request(context, 'POST', path, body)
|
||||
const response = await request(context, 'POST', listPath, body)
|
||||
const detail = context.apiSuccess(MODULE, `${basis}人工费用明细`, response, `inboundId=${resource.inboundId}`, OPTIONS)
|
||||
const feeId = id(detail?.id)
|
||||
context.assert(MODULE, `${basis}费用来源身份和金额`, response,
|
||||
Boolean(feeId) && id(detail?.inboundId) === id(resource.inboundId) && Number(detail?.amount) === Number(amount),
|
||||
`feeId=${feeId || 'missing'}, amount=${detail?.amount ?? 'missing'}`, OPTIONS)
|
||||
if (!feeId) return null
|
||||
const approve = await request(context, 'POST', `${path}/${feeId}/approve`)
|
||||
const approve = await request(context, 'POST', `${basePath}/${feeId}/approve?accountId=${encodeURIComponent(resource.tenantId)}`)
|
||||
context.apiSuccess(MODULE, `${basis}费用审核`, approve, '', OPTIONS)
|
||||
return { feeSourceId: feeId, sourceType: detail?.sourceType || 'MANUAL', direction, amount: Number(amount), businessDate,
|
||||
currency: 'CNY', billingBasis: basis, inboundId: id(resource.inboundId), orderId: id(resource.orderId),
|
||||
@@ -218,10 +219,21 @@ export async function run(context) {
|
||||
`activePriceVersions=${pricingEvidence.activeVersionIds?.join(',') || 'none'};本轮隔离订单未产生 ${basis} 来源,价格配置/订单条件不支持,不能伪装成功`)
|
||||
}
|
||||
}
|
||||
const manualResource = measured.at(-1)
|
||||
if (manualResource && feeItems.PAYABLE && supplier?.id) {
|
||||
const fee = await createManualFee(context, manualResource, feeItems.PAYABLE, 'PAYABLE', 80, 'MANUAL_ORDER_FEE', dateOffset(-1))
|
||||
if (fee) fixtures.push({ ...fee, supplierId: id(supplier.id), partnerId: id(supplier.id), supplierName: supplier.partnerName || null })
|
||||
const receivableResources = measured.slice(0, 4)
|
||||
if (feeItems.RECEIVABLE) {
|
||||
for (const [index, resource] of receivableResources.entries()) {
|
||||
const fee = await createManualFee(context, resource, feeItems.RECEIVABLE, 'RECEIVABLE', 100 + index * 10,
|
||||
'MANUAL_RECEIVABLE', dateOffset(-1 - (index % 2)))
|
||||
if (fee) fixtures.push(fee)
|
||||
}
|
||||
}
|
||||
const manualResources = [measured.at(-1)].filter(Boolean)
|
||||
if (feeItems.PAYABLE && supplier?.id) {
|
||||
for (const [index, manualResource] of manualResources.entries()) {
|
||||
const fee = await createManualFee(context, manualResource, feeItems.PAYABLE, 'PAYABLE', 80 + index * 10,
|
||||
'MANUAL_ORDER_FEE', dateOffset(-3 - index))
|
||||
if (fee) fixtures.push({ ...fee, supplierId: id(supplier.id), partnerId: id(supplier.id), supplierName: supplier.partnerName || null })
|
||||
}
|
||||
}
|
||||
|
||||
const container = context.createdResources?.phase3ContainerLoading
|
||||
@@ -229,12 +241,13 @@ export async function run(context) {
|
||||
&& container?.loadingPlanId && container?.containerBookingId
|
||||
let containerFixture = null
|
||||
if (containerOwned && feeItems.PAYABLE && supplier?.id) {
|
||||
const path = `/api/platform/v1/loading-plans/${encodeURIComponent(container.loadingPlanId)}/fee-details?accountId=${encodeURIComponent(container.tenantId)}`
|
||||
const basePath = `/api/platform/v1/loading-plans/${encodeURIComponent(container.loadingPlanId)}/fee-details`
|
||||
const path = `${basePath}?accountId=${encodeURIComponent(container.tenantId)}`
|
||||
const body = { feeItemId: Number(feeItems.PAYABLE.id), direction: 'PAYABLE', businessDate: dateOffset(-2), amount: 300, currency: 'CNY', settlementIncluded: true, remark: `${context.marker}-CONTAINER_SHARED` }
|
||||
const response = await request(context, 'POST', path, body)
|
||||
const detail = context.apiSuccess(MODULE, '创建按柜费用来源', response, '', OPTIONS)
|
||||
const feeId = id(detail?.id)
|
||||
if (feeId) { const approval = await request(context, 'POST', `${path}/${feeId}/approve`); context.apiSuccess(MODULE, '审核按柜费用来源', approval, '', OPTIONS) }
|
||||
if (feeId) { const approval = await request(context, 'POST', `${basePath}/${feeId}/approve?accountId=${encodeURIComponent(container.tenantId)}`); context.apiSuccess(MODULE, '审核按柜费用来源', approval, '', OPTIONS) }
|
||||
containerFixture = { feeSourceId: feeId || null, sourceType: 'MANUAL', direction: 'PAYABLE', amount: 300,
|
||||
businessDate: body.businessDate, currency: 'CNY', billingBasis: 'CONTAINER_SHARED',
|
||||
supplierId: supplier?.id ? id(supplier.id) : null, partnerId: supplier?.id ? id(supplier.id) : null,
|
||||
|
||||
@@ -41,7 +41,7 @@ function fakeContext(overrides = {}) {
|
||||
} }
|
||||
}
|
||||
|
||||
test('values 兼容数组、records 和 named map', () => {
|
||||
test('values helper supports nested arrays while run consumes frozen top-level arrays', () => {
|
||||
assert.deepEqual(values([{ id: 1 }]), [{ id: 1 }])
|
||||
assert.deepEqual(values({ records: [{ id: 2 }] }), [{ id: 2 }])
|
||||
assert.deepEqual(values({ marker: 'x', selected: { id: 3 } }), [{ id: 3 }])
|
||||
@@ -83,3 +83,20 @@ test('缺 fixture 或真实 voucher 时记录 blocker 且不写接口', async ()
|
||||
assert.equal(noVoucher.steps.length, 1); assert.equal(noVoucher.steps[0].passed, false)
|
||||
assert.equal(noVoucher.steps[0].options.deploymentBlocker, true)
|
||||
})
|
||||
|
||||
test('AR/AP 数组为空或不完整时原子阻断且不查询、不写入', async () => {
|
||||
const empty = fakeContext({ statements: [], receivableSettlements: [], payableSettlements: [] })
|
||||
await run(empty)
|
||||
assert.equal(empty.calls.length, 0)
|
||||
assert.equal(empty.gaps.length, 1)
|
||||
assert.equal(empty.steps[0].passed, false)
|
||||
assert.match(empty.steps[0].evidence, /actual AR=0, AP=0/)
|
||||
|
||||
const partial = fakeContext({ receivableSettlements: [
|
||||
{ settlementId: '101', statementId: '11', direction: 'RECEIVABLE', status: 'ISSUED' },
|
||||
] })
|
||||
await run(partial)
|
||||
assert.equal(partial.calls.length, 0)
|
||||
assert.equal(partial.gaps.length, 1)
|
||||
assert.match(partial.steps[0].evidence, /actual AR=1, AP=1/)
|
||||
})
|
||||
|
||||
@@ -54,8 +54,9 @@ export async function run(context) {
|
||||
gap(context, '取得本轮财务 Fixture', 'financeFullFixtures', '缺少匹配 marker 的 financeFullFixtures,禁止借历史单据')
|
||||
return
|
||||
}
|
||||
const statements = values(fixtures.statements)
|
||||
const receivables = values(fixtures.receivableSettlements)
|
||||
const statements = Array.isArray(fixtures.statements) ? fixtures.statements.filter(Boolean) : []
|
||||
const receivables = Array.isArray(fixtures.receivableSettlements) ? fixtures.receivableSettlements.filter(Boolean) : []
|
||||
const payables = Array.isArray(fixtures.payableSettlements) ? fixtures.payableSettlements.filter(Boolean) : []
|
||||
const customerId = id(fixtures.customerId)
|
||||
const voucherFileId = id(fixtures.voucherFileId ?? fixtures.voucher?.fileId ?? fixtures.voucher?.id)
|
||||
if (!customerId || !voucherFileId) {
|
||||
@@ -63,13 +64,26 @@ export async function run(context) {
|
||||
return
|
||||
}
|
||||
|
||||
const issuedStatementIds = new Set(statements.filter((item) => item.status === 'ISSUED').map(statementId).filter(Boolean))
|
||||
const validReceivables = receivables.filter((item) => item.direction === 'RECEIVABLE'
|
||||
&& settlementId(item) && statementId(item) && issuedStatementIds.has(statementId(item)))
|
||||
const validPayables = payables.filter((item) => item.direction === 'PAYABLE' && settlementId(item) && item.supplierId)
|
||||
const arSettlementIds = new Set(validReceivables.map(settlementId))
|
||||
const arStatementIds = new Set(validReceivables.map(statementId))
|
||||
if (validReceivables.length !== 3 || arSettlementIds.size !== 3 || arStatementIds.size !== 3 || validPayables.length !== 1) {
|
||||
gap(context, '取得完整收付结算 Fixture', 'three AR and one AP settlements',
|
||||
`需要 3 张关联本轮 ISSUED 对账单的独立 AR 和 1 张带 supplierId 的 AP;actual AR=${validReceivables.length}, AP=${validPayables.length}`,
|
||||
'incomplete-settlement-fixture')
|
||||
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)))
|
||||
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)
|
||||
|
||||
const multiTargets = receivables.filter((item) => settlementId(item) && statementId(item)).slice(0, 3)
|
||||
const multiTargets = validReceivables
|
||||
if (multiTargets.length < 3) gap(context, '取得三张应收结算单', 'multi receipt allocation', '本轮没有三张独占应收单,禁止借历史单据')
|
||||
else {
|
||||
const details = await Promise.all(multiTargets.map((item) => detail(context, settlementId(item))))
|
||||
@@ -81,7 +95,7 @@ export async function run(context) {
|
||||
fixtures.receipts = { ...(fixtures.receipts || {}), multi: data(response) }
|
||||
}
|
||||
|
||||
const primary = receivables.find((item) => settlementId(item) && statementId(item))
|
||||
const primary = validReceivables[0]
|
||||
if (!primary) gap(context, '取得应收结算单', 'receivable partial/full', 'receivableSettlements 未提供 settlementId 与 statementId')
|
||||
else {
|
||||
const initial = await detail(context, settlementId(primary))
|
||||
@@ -95,7 +109,7 @@ export async function run(context) {
|
||||
fixtures.settlementFinalStates = { ...(fixtures.settlementFinalStates || {}), receivable: final.settlement }
|
||||
}
|
||||
|
||||
const payable = values(fixtures.payableSettlements).find((item) => item.direction === 'PAYABLE' && settlementId(item))
|
||||
const payable = validPayables[0]
|
||||
const payableId = settlementId(payable)
|
||||
if (!payableId) gap(context, '取得应付结算单', 'payable partial/full/overpayment', '前置 Fixture 的 payableSettlements 数组为空或缺少 PAYABLE 结算单')
|
||||
else {
|
||||
|
||||
Reference in New Issue
Block a user