fix finance reconciliation fee pool ids
This commit is contained in:
@@ -80,8 +80,11 @@ function fakeContext(withFixture = true, permissions = financePermissions, denyF
|
|||||||
requestId: 'request-denied', traceId: 'trace-denied' }
|
requestId: 'request-denied', traceId: 'trace-denied' }
|
||||||
}
|
}
|
||||||
if (method === 'GET' && requestPath.includes('/fee-pool')) {
|
if (method === 'GET' && requestPath.includes('/fee-pool')) {
|
||||||
return ok(receivables.map((item) => ({ ...item, reviewStatus: 'APPROVED',
|
return ok(feeSources.filter((item) => item.feeSourceId).map((item, index) => {
|
||||||
settlementIncluded: true, sourceStatus: 'AVAILABLE' })))
|
const { feeSourceId, ...rest } = item
|
||||||
|
return { ...rest, poolItemId: `${item.direction}:${1001 + index}`, sourceId: feeSourceId,
|
||||||
|
reviewStatus: 'APPROVED', settlementIncluded: true, sourceStatus: 'AVAILABLE' }
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
if (method === 'POST' && requestPath.endsWith('/statements')) {
|
if (method === 'POST' && requestPath.endsWith('/statements')) {
|
||||||
if (body.currency !== 'CNY') return { http: 200, body: { code: 'FIN_400' } }
|
if (body.currency !== 'CNY') return { http: 200, body: { code: 'FIN_400' } }
|
||||||
@@ -89,12 +92,12 @@ function fakeContext(withFixture = true, permissions = financePermissions, denyF
|
|||||||
requestId: 'request-subject', traceId: 'trace-subject' }
|
requestId: 'request-subject', traceId: 'trace-subject' }
|
||||||
const selected = body.generationMode === 'BY_CUTOFF'
|
const selected = body.generationMode === 'BY_CUTOFF'
|
||||||
? receivables.filter((item) => item.businessDate >= body.periodStart && item.businessDate <= body.periodEnd)
|
? receivables.filter((item) => item.businessDate >= body.periodStart && item.businessDate <= body.periodEnd)
|
||||||
: feeSources.filter((item) => body.feeSourceIds.includes(item.feeSourceId))
|
: feeSources.filter((item, index) => body.feeSourceIds.includes(String(1001 + index)))
|
||||||
if (selected.some((item) => item.direction !== 'RECEIVABLE')) return { http: 200, body: { code: 'FIN_409' } }
|
if (selected.some((item) => item.direction !== 'RECEIVABLE')) return { http: 200, body: { code: 'FIN_409' } }
|
||||||
const statementId = String(++statementSequence)
|
const statementId = String(++statementSequence)
|
||||||
const statement = { statementId, status: 'DRAFT', version: 0, revisions: [],
|
const statement = { statementId, status: 'DRAFT', version: 0, revisions: [],
|
||||||
lines: selected.map((item, index) => ({ statementLineId: `${statementId}${index + 1}`,
|
lines: selected.map((item, index) => ({ statementLineId: `${statementId}${index + 1}`,
|
||||||
feeSourceId: item.feeSourceId, statementAmount: 100 + index, adjustmentReason: null })) }
|
feeSourceId: String(1001 + feeSources.indexOf(item)), statementAmount: 100 + index, adjustmentReason: null })) }
|
||||||
statements.set(statementId, statement)
|
statements.set(statementId, statement)
|
||||||
if (idem) idempotency.set(idem, responseForStatement(statement))
|
if (idem) idempotency.set(idem, responseForStatement(statement))
|
||||||
return ok(responseForStatement(statement))
|
return ok(responseForStatement(statement))
|
||||||
@@ -156,7 +159,7 @@ test('静态契约覆盖日期、SELECTED、三 AR、AP、状态机和数组输
|
|||||||
assert.match(source, /fixture\.root\.statements = statementRows/)
|
assert.match(source, /fixture\.root\.statements = statementRows/)
|
||||||
assert.match(source, /fixture\.root\.receivableSettlements = arRows/)
|
assert.match(source, /fixture\.root\.receivableSettlements = arRows/)
|
||||||
assert.doesNotMatch(source, /generationMode:\s*['"]BY_CUTOFF['"]/)
|
assert.doesNotMatch(source, /generationMode:\s*['"]BY_CUTOFF['"]/)
|
||||||
assert.doesNotMatch(source, /\/fee-pool/)
|
assert.match(source, /\/fee-pool/)
|
||||||
assert.match(source, /http: response\?\.http \?\? null/)
|
assert.match(source, /http: response\?\.http \?\? null/)
|
||||||
assert.match(source, /requestId: response\?\.requestId \?\? null/)
|
assert.match(source, /requestId: response\?\.requestId \?\? null/)
|
||||||
assert.match(source, /traceId: response\?\.traceId \?\? null/)
|
assert.match(source, /traceId: response\?\.traceId \?\? null/)
|
||||||
@@ -184,9 +187,10 @@ test('run 发布一条日期链、三张独立已出账单、三张 AR 和一张
|
|||||||
&& gap.reason.includes('containerBookingId=CB-COST-RESPONSE')), true)
|
&& 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(null)), false)
|
||||||
assert.equal(context.calls.some((call) => call.body?.feeSourceIds?.includes('106')), false)
|
assert.equal(context.calls.some((call) => call.body?.feeSourceIds?.includes('106')), false)
|
||||||
assert.equal(context.calls.some((call) => call.path.includes('/fee-pool')
|
assert.equal(context.calls.some((call) => call.path.includes('/fee-pool')), true)
|
||||||
|| call.body?.generationMode === 'BY_CUTOFF'), false)
|
assert.equal(context.calls.some((call) => call.body?.generationMode === 'BY_CUTOFF'), false)
|
||||||
assert.equal(context.steps.filter((step) => !step.passed).length, 0)
|
const failedSteps = context.steps.filter((step) => !step.passed)
|
||||||
|
assert.equal(failedSteps.length, 0, JSON.stringify(failedSteps, null, 2))
|
||||||
})
|
})
|
||||||
|
|
||||||
test('run 在 fixture 缺失时记录 blocker 且不发送 HTTP 请求', async () => {
|
test('run 在 fixture 缺失时记录 blocker 且不发送 HTTP 请求', async () => {
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ function settlementId(value) { return id(value?.settlementId ?? value?.id) }
|
|||||||
function sourceId(value) { return id(value?.feeSourceId ?? value?.financeFeeSourceId ?? value?.id) }
|
function sourceId(value) { return id(value?.feeSourceId ?? value?.financeFeeSourceId ?? value?.id) }
|
||||||
function sourceDate(value) { return value?.businessDate ?? value?.billingDate ?? value?.feeDate ?? '' }
|
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 sourceWaybill(value) { return id(value?.waybillId ?? value?.shipmentId ?? value?.orderId ?? value?.inboundId ?? value?.sourceNo) }
|
||||||
|
function financePoolId(value) {
|
||||||
|
const poolItemId = id(value?.poolItemId)
|
||||||
|
if (poolItemId) return poolItemId.includes(':') ? poolItemId.slice(poolItemId.indexOf(':') + 1) : poolItemId
|
||||||
|
return id(value?.feeSourceId ?? value?.financeFeeSourceId ?? value?.id)
|
||||||
|
}
|
||||||
function isUnboundContainerCost(raw) {
|
function isUnboundContainerCost(raw) {
|
||||||
return raw?.sourceType === 'MANUAL' && raw?.billingBasis === 'CONTAINER_SHARED'
|
return raw?.sourceType === 'MANUAL' && raw?.billingBasis === 'CONTAINER_SHARED'
|
||||||
&& !sourceWaybill(raw)
|
&& !sourceWaybill(raw)
|
||||||
@@ -68,16 +73,21 @@ function blocker(context, name, evidence, category, response = null) {
|
|||||||
{ marker: context.marker, ...diagnostic(response) })
|
{ marker: context.marker, ...diagnostic(response) })
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeFixture(context) {
|
async function normalizeFixture(context) {
|
||||||
const root = context.createdResources?.financeFullFixtures
|
const root = context.createdResources?.financeFullFixtures
|
||||||
if (!root || root.marker !== context.marker || !root.customerId
|
if (!root || root.marker !== context.marker || !root.customerId
|
||||||
|| !Array.isArray(root.orders) || !Array.isArray(root.feeSources)) return null
|
|| !Array.isArray(root.orders) || !Array.isArray(root.feeSources)) return null
|
||||||
const customerId = id(root.customerId)
|
const customerId = id(root.customerId)
|
||||||
const orders = new Map(root.orders.map((order) => [id(order.orderId ?? order.id), order]))
|
const orders = new Map(root.orders.map((order) => [id(order.orderId ?? order.id), order]))
|
||||||
|
const poolResponse = await context.request('GET',
|
||||||
|
`${BASE}/fee-pool?customerId=${encodeURIComponent(customerId)}&direction=ALL`)
|
||||||
|
const pool = rows(poolResponse?.body?.data).filter((item) => String(item?.sourceType ?? '') !== 'MANUAL_COST')
|
||||||
|
const poolBySource = new Map(pool.map((item) => [id(item?.sourceId), item]))
|
||||||
const attemptedSources = root.feeSources.map((raw) => {
|
const attemptedSources = root.feeSources.map((raw) => {
|
||||||
const order = orders.get(id(raw.orderId))
|
const order = orders.get(id(raw.orderId))
|
||||||
|
const financeSource = poolBySource.get(sourceId(raw))
|
||||||
return {
|
return {
|
||||||
raw, feeSourceId: sourceId(raw), customerId: id(raw.customerId ?? customerId),
|
raw, feeSourceId: financePoolId(financeSource), sourceFeeId: sourceId(raw), customerId: id(raw.customerId ?? customerId),
|
||||||
direction: String(raw.direction ?? ''), currency: String(raw.currency ?? raw.currencyCode ?? '').toUpperCase(),
|
direction: String(raw.direction ?? ''), currency: String(raw.currency ?? raw.currencyCode ?? '').toUpperCase(),
|
||||||
businessDate: sourceDate(raw), waybillId: sourceWaybill(raw) || sourceWaybill(order),
|
businessDate: sourceDate(raw), waybillId: sourceWaybill(raw) || sourceWaybill(order),
|
||||||
marker: raw.marker ?? root.marker,
|
marker: raw.marker ?? root.marker,
|
||||||
@@ -195,7 +205,7 @@ async function createReceivable(context, statement, index) {
|
|||||||
|
|
||||||
export async function run(context) {
|
export async function run(context) {
|
||||||
const start = context.steps.length
|
const start = context.steps.length
|
||||||
const fixture = normalizeFixture(context)
|
const fixture = await normalizeFixture(context)
|
||||||
if (!fixture) {
|
if (!fixture) {
|
||||||
blocker(context, '取得本轮财务全量 Fixture',
|
blocker(context, '取得本轮财务全量 Fixture',
|
||||||
'financeFullFixtures 必须提供匹配 marker 的 customerId/orders/feeSources;禁止猜测旧字段或历史来源',
|
'financeFullFixtures 必须提供匹配 marker 的 customerId/orders/feeSources;禁止猜测旧字段或历史来源',
|
||||||
|
|||||||
Reference in New Issue
Block a user