Files
baiyue-firstleg/scripts/admin-test-env-modules/inbound-fees.mjs
T

204 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 ensureInitialCalculated(context, fixture,
options = { scenario: 'inbound-fee-initial-calculated', deploymentBlocker: true }) {
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) return null
const latestPath = `/api/platform/v1/inbounds/${encodeURIComponent(inboundId)}/fees/latest?tenantId=${encodeURIComponent(tenantId)}`
let initialResponse = await context.request('GET', latestPath)
if (!success(initialResponse) && String(initialResponse?.body?.code) === 'A0404') {
initialResponse = await context.request('POST',
`/api/platform/v1/inbounds/${encodeURIComponent(inboundId)}/fees/calculate?tenantId=${encodeURIComponent(tenantId)}`,
undefined, undefined, requestHeaders(`${context.marker}-inbound-fee-initial-calculate`))
}
const initial = context.apiSuccess(MODULE, '读取或补齐审核后初始费用', initialResponse, '', options)
context.assert(MODULE, '初始费用保持 WAIT_SPLIT 且尚未发布', initialResponse,
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)
return initial?.feeId && initial?.feeStatus === 'CALCULATED'
? { initial, initialResponse, tenantId, inboundId }
: null
}
export async function run(context) {
const start = context.steps.length
const fixture = context.createdResources.inboundMeasurement
const prepared = await ensureInitialCalculated(context, fixture, OPTIONS)
if (!prepared) { finish(context, start); return }
const { initial, initialResponse, tenantId, inboundId } = prepared
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,
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}-自动费用版本调整`, toleranceConfirmed: true }, 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',
`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=200&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,
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?.publicationStatus === 'PUBLISHED',
`review=${approved?.reviewStatus || 'missing'}, publication=${approved?.publicationStatus || '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)
}