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, 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', `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, 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) }