191 lines
15 KiB
JavaScript
191 lines
15 KiB
JavaScript
const MODULE = 'inbound-fees'
|
|
const OPTIONS = { scenario: 'inbound-fee-lifecycle', deploymentBlocker: true }
|
|
|
|
function id(value) { return value === null || value === undefined ? '' : String(value) }
|
|
function success(response) { return response?.http === 200 && String(response?.body?.code) === '200' }
|
|
function money(value) { return Number(value) }
|
|
function requestHeaders(key) { return { 'Idempotency-Key': key.slice(0, 128) } }
|
|
|
|
function finish(context, start) {
|
|
const steps = context.steps.slice(start)
|
|
context.scenarios.push({
|
|
name: OPTIONS.scenario,
|
|
module: MODULE,
|
|
success: steps.every((step) => step.passed),
|
|
deploymentBlocked: steps.some((step) => step.deploymentBlocker),
|
|
passed: steps.filter((step) => step.passed).length,
|
|
failed: steps.filter((step) => !step.passed).length,
|
|
})
|
|
}
|
|
|
|
async function listDetails(context, tenantId, inboundId, name) {
|
|
const response = await context.request('GET',
|
|
`/api/platform/v1/inbounds/${encodeURIComponent(inboundId)}/fee-details?accountId=${encodeURIComponent(tenantId)}`)
|
|
const rows = context.apiSuccess(MODULE, name, response, '', OPTIONS)
|
|
return { response, rows: Array.isArray(rows) ? rows : [] }
|
|
}
|
|
|
|
export async function run(context) {
|
|
const start = context.steps.length
|
|
const fixture = context.createdResources.inboundMeasurement
|
|
const tenantId = id(fixture?.tenantId)
|
|
const inboundId = id(fixture?.inboundId)
|
|
context.assert(MODULE, '复用本轮审核通过且进入 WAIT_SPLIT 的入库单', null,
|
|
Boolean(tenantId && inboundId && fixture?.finalStatus === 'APPROVED' && fixture?.processStatus === 'WAIT_SPLIT'),
|
|
`tenantId=${tenantId || 'missing'}, inboundId=${inboundId || 'missing'}, measurement=${fixture?.finalStatus || 'missing'}, process=${fixture?.processStatus || 'missing'}`, OPTIONS)
|
|
if (!tenantId || !inboundId) { finish(context, start); return }
|
|
|
|
const latestPath = `/api/platform/v1/inbounds/${encodeURIComponent(inboundId)}/fees/latest?tenantId=${encodeURIComponent(tenantId)}`
|
|
const latestResponse = await context.request('GET', latestPath)
|
|
const initial = context.apiSuccess(MODULE, '审核成功后读取自动初算费用', latestResponse, '', OPTIONS)
|
|
context.assert(MODULE, '自动初算保持 WAIT_SPLIT 且尚未发布', latestResponse,
|
|
initial?.feeStatus === 'CALCULATED' && initial?.nextProcessStatus === 'WAIT_SPLIT'
|
|
&& Number(initial?.feeVersion) >= 1 && money(initial?.totalAmount) >= 0,
|
|
`feeId=${id(initial?.feeId) || 'missing'}, version=${initial?.feeVersion ?? 'missing'}, status=${initial?.feeStatus || 'missing'}, process=${initial?.nextProcessStatus || 'missing'}`, OPTIONS)
|
|
if (!initial?.feeId || initial?.feeStatus !== 'CALCULATED') { finish(context, start); return }
|
|
|
|
const initialDetails = await listDetails(context, tenantId, inboundId, '查询自动初算派生费用明细')
|
|
const initialDerived = initialDetails.rows.find((row) => row.sourceType === 'FBA_PRICING'
|
|
&& id(row.sourceFeeId) === id(initial.feeId))
|
|
context.assert(MODULE, 'FBA_PRICING 明细是初算版本的只读草稿派生', initialDetails.response,
|
|
initialDerived?.sourceFeeVersion === initial.feeVersion && initialDerived?.reviewStatus === 'PENDING'
|
|
&& initialDerived?.publicationStatus === 'DRAFT' && initialDerived?.direction === 'RECEIVABLE'
|
|
&& money(initialDerived?.amount) === money(initial?.totalAmount),
|
|
`sourceType=${initialDerived?.sourceType || 'missing'}, sourceVersion=${initialDerived?.sourceFeeVersion ?? 'missing'}, review=${initialDerived?.reviewStatus || 'missing'}, publication=${initialDerived?.publicationStatus || 'missing'}`, OPTIONS)
|
|
if (initialDerived?.id) {
|
|
const derivedUpdateResponse = await context.request('PUT',
|
|
`/api/platform/v1/inbounds/${encodeURIComponent(inboundId)}/fee-details/${encodeURIComponent(initialDerived.id)}?accountId=${encodeURIComponent(tenantId)}`,
|
|
{ feeItemId: initialDerived.feeItemId, direction: 'RECEIVABLE',
|
|
businessDate: initialDerived.businessDate, unitPrice: initialDerived.unitPrice,
|
|
currency: initialDerived.currency, amount: initialDerived.amount,
|
|
settlementIncluded: initialDerived.settlementIncluded, remark: `${context.marker}-禁止修改派生` })
|
|
context.assert(MODULE, 'FBA_PRICING 派生详情拒绝通用编辑', derivedUpdateResponse,
|
|
derivedUpdateResponse.http === 200 && !success(derivedUpdateResponse)
|
|
&& String(derivedUpdateResponse.body?.message || '').includes('只读派生'),
|
|
`code=${derivedUpdateResponse.body?.code ?? 'missing'}, message=${derivedUpdateResponse.body?.message || 'missing'}`, OPTIONS)
|
|
}
|
|
|
|
const adjustedQuantity = Math.max(0.0001, money(initial.chargeableQuantity) + 0.01)
|
|
const adjustedUnitPrice = Math.max(0, money(initial.unitPrice))
|
|
const adjustedAdditional = money(initial.additionalAmount) + 0.01
|
|
const adjustResponse = await context.request('PUT',
|
|
`/api/platform/v1/inbounds/${encodeURIComponent(inboundId)}/fees/published?tenantId=${encodeURIComponent(tenantId)}`,
|
|
{ expectedFeeVersion: Number(initial.feeVersion), chargeableQuantity: adjustedQuantity,
|
|
unitPrice: adjustedUnitPrice, additionalAmount: adjustedAdditional,
|
|
reason: `${context.marker}-自动费用版本调整` }, undefined,
|
|
requestHeaders(`${context.marker}-inbound-fee-adjust`))
|
|
const adjusted = context.apiSuccess(MODULE, '调整初算并生成新版本', adjustResponse, '', OPTIONS)
|
|
context.assert(MODULE, '调整版本仍为 CALCULATED 且不阻塞 WAIT_SPLIT', adjustResponse,
|
|
Number(adjusted?.feeVersion) > Number(initial.feeVersion) && adjusted?.feeStatus === 'CALCULATED'
|
|
&& adjusted?.nextProcessStatus === 'WAIT_SPLIT' && money(adjusted?.totalAmount) >= adjustedAdditional,
|
|
`oldVersion=${initial.feeVersion}, newVersion=${adjusted?.feeVersion ?? 'missing'}, status=${adjusted?.feeStatus || 'missing'}, process=${adjusted?.nextProcessStatus || 'missing'}`, OPTIONS)
|
|
if (!adjusted?.feeId) { finish(context, start); return }
|
|
|
|
const adjustedDetails = await listDetails(context, tenantId, inboundId, '回查调整后的派生详情版本')
|
|
const currentDerived = adjustedDetails.rows.find((row) => row.sourceType === 'FBA_PRICING'
|
|
&& id(row.sourceFeeId) === id(adjusted.feeId))
|
|
context.assert(MODULE, '派生详情切换到最新费用版本且旧版本不再当前发布', adjustedDetails.response,
|
|
currentDerived?.sourceFeeVersion === adjusted.feeVersion && currentDerived?.publicationStatus === 'DRAFT'
|
|
&& !adjustedDetails.rows.some((row) => row.sourceType === 'FBA_PRICING'
|
|
&& id(row.sourceFeeId) !== id(adjusted.feeId) && row.publicationStatus === 'PUBLISHED'),
|
|
`derivedFeeId=${id(currentDerived?.sourceFeeId) || 'missing'}, derivedVersion=${currentDerived?.sourceFeeVersion ?? 'missing'}`, OPTIONS)
|
|
|
|
const confirmResponse = await context.request('POST',
|
|
`/api/platform/v1/inbounds/${encodeURIComponent(inboundId)}/fees/confirm?tenantId=${encodeURIComponent(tenantId)}`,
|
|
undefined, undefined, requestHeaders(`${context.marker}-inbound-fee-confirm`))
|
|
const confirmed = context.apiSuccess(MODULE, '人工确认最新头程费用', confirmResponse, '', OPTIONS)
|
|
context.assert(MODULE, '人工确认只改变费用状态且保持 WAIT_SPLIT', confirmResponse,
|
|
confirmed?.feeStatus === 'CONFIRMED' && confirmed?.feeVersion === adjusted.feeVersion
|
|
&& confirmed?.nextProcessStatus === 'WAIT_SPLIT',
|
|
`version=${confirmed?.feeVersion ?? 'missing'}, status=${confirmed?.feeStatus || 'missing'}, process=${confirmed?.nextProcessStatus || 'missing'}`, OPTIONS)
|
|
|
|
const sendResponse = await context.request('POST',
|
|
`/api/platform/v1/inbounds/${encodeURIComponent(inboundId)}/fees/send?tenantId=${encodeURIComponent(tenantId)}`,
|
|
undefined, undefined, requestHeaders(`${context.marker}-inbound-fee-send`))
|
|
const sent = context.apiSuccess(MODULE, '人工发布头程费用并触发 Finance Outbox', sendResponse, '', OPTIONS)
|
|
const publishedDetails = await listDetails(context, tenantId, inboundId, '发布后回查对客费用派生')
|
|
const published = publishedDetails.rows.find((row) => row.sourceType === 'FBA_PRICING'
|
|
&& id(row.sourceFeeId) === id(sent?.feeId))
|
|
context.assert(MODULE, '仅人工发布后费用为 SENT 且派生明细为 PUBLISHED', sendResponse,
|
|
sent?.feeStatus === 'SENT' && sent?.nextProcessStatus === 'WAIT_SPLIT'
|
|
&& published?.publicationStatus === 'PUBLISHED' && published?.reviewStatus === 'APPROVED'
|
|
&& published?.settlementIncluded === true,
|
|
`status=${sent?.feeStatus || 'missing'}, process=${sent?.nextProcessStatus || 'missing'}, publication=${published?.publicationStatus || 'missing'}`, OPTIONS)
|
|
|
|
const feeItemsResponse = await context.request('GET', '/api/platform/v1/fee-items?page=1&size=500&status=ACTIVE')
|
|
const feeItemsPage = context.apiSuccess(MODULE, '查询可用于其他应收的费用项目', feeItemsResponse, '', OPTIONS)
|
|
const feeItems = Array.isArray(feeItemsPage?.records) ? feeItemsPage.records
|
|
: Array.isArray(feeItemsPage) ? feeItemsPage : []
|
|
const manualItem = feeItems.find((item) => item.status === 'ACTIVE' && item.warehouseReceivable === true
|
|
&& item.feeCode !== 'FBA_HEAD_FREIGHT')
|
|
context.assert(MODULE, '存在启用的非头程运费仓储应收项目', feeItemsResponse,
|
|
Boolean(manualItem?.id), `feeItemId=${id(manualItem?.id) || 'missing'}, feeCode=${manualItem?.feeCode || 'missing'}`, OPTIONS)
|
|
if (!manualItem?.id) { finish(context, start); return }
|
|
|
|
const detailsBase = `/api/platform/v1/inbounds/${encodeURIComponent(inboundId)}/fee-details`
|
|
const manualPayload = { feeItemId: manualItem.id, direction: 'RECEIVABLE',
|
|
businessDate: new Date().toISOString().slice(0, 10), unitPrice: 12.34, currency: 'CNY', amount: 12.34,
|
|
settlementIncluded: true, remark: `${context.marker}-其他应收` }
|
|
const manualCreateResponse = await context.request('POST', `${detailsBase}?accountId=${encodeURIComponent(tenantId)}`, manualPayload)
|
|
const manual = context.apiSuccess(MODULE, '新增独立 MANUAL 其他应收', manualCreateResponse, '', OPTIONS)
|
|
context.assert(MODULE, 'MANUAL 初始版本待审核且不冒充 FBA_PRICING', manualCreateResponse,
|
|
manual?.sourceType === 'MANUAL' && manual?.sourceFeeVersion === 1
|
|
&& manual?.reviewStatus === 'PENDING' && manual?.publicationStatus === 'DRAFT',
|
|
`detailId=${id(manual?.id) || 'missing'}, sourceType=${manual?.sourceType || 'missing'}, sourceVersion=${manual?.sourceFeeVersion ?? 'missing'}`, OPTIONS)
|
|
if (!manual?.id) { finish(context, start); return }
|
|
|
|
const approveResponse = await context.request('POST',
|
|
`${detailsBase}/${encodeURIComponent(manual.id)}/approve?accountId=${encodeURIComponent(tenantId)}`)
|
|
context.apiSuccess(MODULE, '审核通过 MANUAL 其他应收', approveResponse, '', OPTIONS)
|
|
const afterApproval = await listDetails(context, tenantId, inboundId, '回查已审核 MANUAL 其他应收')
|
|
const approved = afterApproval.rows.find((row) => id(row.id) === id(manual.id))
|
|
context.assert(MODULE, '已审核 MANUAL 纳入对客应收但与头程费用独立', afterApproval.response,
|
|
approved?.sourceType === 'MANUAL' && approved?.reviewStatus === 'APPROVED'
|
|
&& approved?.settlementIncluded === true,
|
|
`review=${approved?.reviewStatus || 'missing'}, settlementIncluded=${approved?.settlementIncluded ?? 'missing'}`, OPTIONS)
|
|
|
|
const adjustedManualPayload = { ...manualPayload, unitPrice: 15.67, amount: 15.67,
|
|
remark: `${context.marker}-其他应收版本调整` }
|
|
const manualAdjustResponse = await context.request('POST',
|
|
`${detailsBase}/${encodeURIComponent(manual.id)}/adjust?accountId=${encodeURIComponent(tenantId)}&expectedSourceVersion=1`,
|
|
adjustedManualPayload)
|
|
const manualV2 = context.apiSuccess(MODULE, '调整已审核 MANUAL 并生成新版本', manualAdjustResponse, '', OPTIONS)
|
|
context.assert(MODULE, 'MANUAL V2 重新进入待审核且不影响头程 SENT', manualAdjustResponse,
|
|
id(manualV2?.id) !== id(manual.id) && id(manualV2?.sourceFeeId) === id(manual.id)
|
|
&& manualV2?.sourceType === 'MANUAL' && manualV2?.sourceFeeVersion === 2
|
|
&& manualV2?.reviewStatus === 'PENDING' && manualV2?.publicationStatus === 'DRAFT'
|
|
&& sent?.feeStatus === 'SENT',
|
|
`sourceVersion=${manualV2?.sourceFeeVersion ?? 'missing'}, review=${manualV2?.reviewStatus || 'missing'}, freight=${sent?.feeStatus || 'missing'}`, OPTIONS)
|
|
|
|
const currentOnly = await listDetails(context, tenantId, inboundId, '默认列表隐藏 MANUAL 已替换版本')
|
|
const historyResponse = await context.request('GET',
|
|
`${detailsBase}?accountId=${encodeURIComponent(tenantId)}&includeHistory=true`)
|
|
const historyRows = context.apiSuccess(MODULE, '显式查询费用历史版本', historyResponse, '', OPTIONS)
|
|
context.assert(MODULE, 'includeHistory 仅扩展 SUPERSEDED 历史且当前版本不重复', historyResponse,
|
|
!currentOnly.rows.some((row) => id(row.id) === id(manual.id))
|
|
&& Array.isArray(historyRows) && historyRows.some((row) => id(row.id) === id(manual.id)
|
|
&& row.publicationStatus === 'SUPERSEDED')
|
|
&& historyRows.some((row) => id(row.id) === id(manualV2.id) && row.sourceFeeVersion === 2),
|
|
`currentHasV1=${currentOnly.rows.some((row) => id(row.id) === id(manual.id))}, historyCount=${Array.isArray(historyRows) ? historyRows.length : 'n/a'}`, OPTIONS)
|
|
|
|
const approveV2Response = await context.request('POST',
|
|
`${detailsBase}/${encodeURIComponent(manualV2.id)}/approve?accountId=${encodeURIComponent(tenantId)}`)
|
|
context.apiSuccess(MODULE, '审核通过 MANUAL 新版本', approveV2Response, '', OPTIONS)
|
|
const finalDetails = await listDetails(context, tenantId, inboundId, '汇总已发布头程与已审核其他应收')
|
|
const receivableTotal = finalDetails.rows.filter((row) => row.direction === 'RECEIVABLE'
|
|
&& ((row.sourceType === 'FBA_PRICING' && row.publicationStatus === 'PUBLISHED')
|
|
|| (row.sourceType === 'MANUAL' && row.reviewStatus === 'APPROVED')))
|
|
.reduce((sum, row) => sum + money(row.amount), 0)
|
|
const payableTotal = finalDetails.rows.filter((row) => row.direction === 'PAYABLE'
|
|
&& row.sourceType === 'MANUAL' && row.reviewStatus === 'APPROVED')
|
|
.reduce((sum, row) => sum + money(row.amount), 0)
|
|
context.assert(MODULE, '对客合计仅含已发布头程与已审核其他应收且其他应付单列', finalDetails.response,
|
|
receivableTotal >= money(published?.amount) + money(manualV2?.amount) && payableTotal >= 0,
|
|
`receivableTotal=${receivableTotal.toFixed(2)}, freight=${money(published?.amount).toFixed(2)}, manual=${money(manualV2?.amount).toFixed(2)}, payableTotal=${payableTotal.toFixed(2)}`, OPTIONS)
|
|
|
|
context.createdResources.inboundFees = { marker: context.marker, tenantId, inboundId,
|
|
freightFeeId: id(sent.feeId), freightVersion: sent.feeVersion, manualDetailId: id(manualV2.id),
|
|
manualVersion: manualV2.sourceFeeVersion }
|
|
finish(context, start)
|
|
}
|