Files
baiyue-firstleg/scripts/admin-test-env-modules/fleet-fee-coverage-acceptance.mjs
T

168 lines
14 KiB
JavaScript
Raw Normal View History

import { randomUUID } from 'node:crypto'
const OPTIONS = { scenario: 'fleet-original-fee-coverage', deploymentBlocker: true }
const id = value => value == null ? '' : String(value)
const ok = response => response?.http === 200 && id(response?.body?.code) === '200'
const businessConflict = response => response?.http === 409 || (response?.http === 200 && id(response?.body?.code) === 'A0409')
const canonical = rows => JSON.stringify(rows.map(row => JSON.stringify(row)).sort())
const cargoIdentity = row => ({ dispatchItemId: id(row.dispatchItemId), warehousePalletId: id(row.warehousePalletId),
orderId: id(row.orderId), sourceInboundId: id(row.sourceInboundId), packages: Number(row.packages) })
const feeIdentity = row => ({ financeFeeId: id(row.financeFeeId), sourceVersion: Number(row.sourceVersion), sourceHash: row.sourceHash,
sourceInboundId: id(row.sourceInboundId), customerId: id(row.customerId), currency: row.currency, amount: Number(row.amount), status: row.status })
/** Dedicated current-run dispatches only: one FULL case and a separate PARTIAL case with >=2 actual inbound sources. */
export async function verifyFleetFeeCoverage(context, module, source, { reconcilePersisted = false } = {}) {
if (!context.createdResources?.fleetFeeCoverageCases?.includes(source) || source.marker !== context.marker || (source.proof && !reconcilePersisted)
|| !['FULL', 'PARTIAL'].includes(source.mode) || !source.dispatchNo || !source.customerId)
throw new Error('原费用验收需要本轮专用、未执行的排车用例;失败后保留原请求,不重复创建')
if (reconcilePersisted && (source.mode !== 'FULL' || source.proof?.status !== 'CHECKING'
|| !source.proof.outboundNo || !source.proof.deliveryNos?.length || !source.proof.confirmationRequest?.idempotencyKey))
throw new Error('已落单核对须有原成功请求、出库单及派送集合,不允许重造')
const proof = reconcilePersisted ? source.proof : source.proof = { status: 'CHECKING', mode: source.mode, dispatchNo: source.dispatchNo, requests: [] }
const dispatchPath = `/api/platform/v1/last-leg/dispatches/${encodeURIComponent(source.dispatchNo)}`
const outboundPath = `/api/platform/v1/last-leg/outbound-orders?size=200&customerId=${encodeURIComponent(source.customerId)}`
async function check(name, passed, response) {
context.assert(module, name, response, Boolean(passed), '', OPTIONS)
if (!passed) throw new Error(`${module}: ${name}`)
}
async function request(method, path, body, label, expectedMessage) {
const response = await context.request(method, path, body)
proof.requests.push({ label, method, path, http: response?.http, code: response?.body?.code,
requestId: response?.requestId, traceId: response?.traceId })
await check(label, Boolean(response?.requestId) && Boolean(response?.traceId)
&& (expectedMessage ? businessConflict(response) && expectedMessage.test(id(response?.body?.message)) : ok(response)), response)
return response.body?.data
}
async function noOutbound(label) {
const rows = []
let total
for (let index = 1; ; index++) {
const page = await request('GET', `${outboundPath}&page=${index}`, undefined, label)
await check('客户出库列表分页元数据完整', Array.isArray(page?.records) && Number.isSafeInteger(Number(page.total))
&& Number(page.total) >= 0 && Number(page.page) === index && Number(page.size) === 200)
total ??= Number(page.total)
await check('客户出库列表跨页范围稳定', total === Number(page.total) && (page.records.length > 0 || rows.length === total))
rows.push(...page.records)
if (rows.length >= total) break
}
await check('失败命令没有生成本排车出库事实', rows.length === total
&& new Set(rows.map(row => row.outboundNo)).size === rows.length && rows.every(row => row.dispatchNo !== source.dispatchNo))
}
const dispatch = await request('GET', dispatchPath, undefined, '核对本轮费用验收排车来源')
await check('只处理本轮尚未拣货及发车的排车', dispatch?.dispatchNo === source.dispatchNo
&& Number(dispatch.pickedPackageCount) === 0
&& Number(dispatch.shippedPackageCount) === 0 && dispatch.items?.length > 0)
if (!reconcilePersisted) await noOutbound('确认专用排车尚无出库单')
const preview = await request('GET', `${dispatchPath}/fee-coverage`, undefined, '读取本次实际货物及原 Finance 费用来源')
await check('原费用范围完整且可确认', preview?.sourceComplete === true && !preview.blockReason
&& preview.dispatchNo === source.dispatchNo && /^[0-9a-f]{64}$/.test(preview.cargoHash || '')
&& preview.cargo?.length > 0 && preview.cargo.every(row => row.sourceRegion === 'FIRSTLEG' && id(row.sourceInboundId)))
const itemTotals = new Map()
for (const row of preview.cargo) itemTotals.set(id(row.dispatchItemId), (itemTotals.get(id(row.dispatchItemId)) || 0) + Number(row.packages))
await check('费用预览逐条排车箱数核平', itemTotals.size === dispatch.items.length
&& dispatch.items.every(row => itemTotals.get(id(row.id)) === Number(row.packageCount)))
const inboundIds = [...new Set(preview.cargo.map(row => id(row.sourceInboundId)))].sort()
await check('原费用均属于当前客户和实际原入仓', Array.isArray(preview.feeSources) && preview.feeSources.every(fee =>
id(fee.customerId) === id(source.customerId) && inboundIds.includes(id(fee.sourceInboundId))
&& ['AVAILABLE', 'SETTLED'].includes(fee.status) && ['INBOUND_FEE', 'CONTAINER_FEE'].includes(fee.sourceType)
&& fee.currency === 'CNY' && Number(fee.sourceVersion) >= 1 && fee.sourceHash && id(fee.financeFeeId)))
const sources = inboundIds.map(inboundId => preview.feeSources.find(fee => id(fee.sourceInboundId) === inboundId))
await check('本轮每票已产生可引用原费用,不借其他票费用补齐', sources.every(Boolean)
&& (source.mode === 'FULL' || inboundIds.length >= 2))
const selectedFees = source.mode === 'FULL' ? sources : [sources[0]]
const selections = selectedFees.map(fee => ({ financeFeeId: id(fee.financeFeeId), sourceVersion: Number(fee.sourceVersion),
sourceHash: fee.sourceHash, services: ['DELIVERY_FEE'] }))
const freight = await request('GET', `${dispatchPath}/external-freight`, undefined, '核对本专用排车没有外接报价混入覆盖用例')
await check('本用例为原费用货物且使用托盘派送自有预约', Array.isArray(freight?.quotes) && freight.quotes.length === 0
&& freight.requiresDeliveryFee === true && source.transferBody?.loadingType === 'PALLET'
&& source.transferBody.appointmentAccount === 'SELF' && source.transferBody.fees?.length === 1
&& source.transferBody.fees[0].direction === 'RECEIVABLE' && source.transferBody.fees[0].feeType === 'DELIVERY_FEE'
&& Number(source.transferBody.fees[0].amount) > 0)
const body = reconcilePersisted ? proof.confirmationRequest : { ...source.transferBody, idempotencyKey: `fleet-fee-${randomUUID()}`,
externalFreightHash: freight.snapshotHash, feeCoverageCargoHash: preview.cargoHash, feeCoverage: selections,
fees: source.mode === 'FULL' ? [] : source.transferBody.fees }
proof.confirmationRequest = body
if (!reconcilePersisted) proof.originalFees = preview.feeSources.map(feeIdentity)
const transferPath = `${dispatchPath}/transfer-delivery`
let transferred
if (reconcilePersisted) {
await check('原成功请求的货物与引用仍对应本次核对范围', body.feeCoverageCargoHash === preview.cargoHash
&& canonical(body.feeCoverage) === canonical(selections))
const stored = await request('GET', `/api/platform/v1/last-leg/outbound-orders/${encodeURIComponent(proof.outboundNo)}`, undefined, '先核对已持久化的原出库与派送集合')
await check('原出库和派送存在且尚未执行', stored?.outbound?.dispatchNo === source.dispatchNo
&& stored.outbound.outboundNo === proof.outboundNo && stored.outbound.status === 'WAIT_OUTBOUND'
&& canonical((stored.deliveries || []).map(row => row.deliveryNo)) === canonical(proof.deliveryNos)
&& stored.deliveries.every(row => row.status === 'WAIT_DELIVERY'))
transferred = await request('POST', transferPath, body, '确认原持久化单据后仅重放同键同载荷成功命令')
await check('核对重放未新建任何业务单据', transferred?.replayed === true && transferred.outboundNo === proof.outboundNo
&& canonical((transferred.deliveries || []).map(row => row.deliveryNo)) === canonical(proof.deliveryNos))
} else {
const staleHash = `${preview.cargoHash[0] === '0' ? '1' : '0'}${preview.cargoHash.slice(1)}`
await request('POST', transferPath, { ...body, idempotencyKey: `stale-cargo-${randomUUID()}`, feeCoverageCargoHash: staleHash },
'货物摘要陈旧时拒绝转派送', /货物已变化/)
await noOutbound('陈旧货物拒绝后回查无业务写入')
await request('POST', transferPath, { ...body, idempotencyKey: `stale-fee-${randomUUID()}`,
feeCoverage: selections.map((row, index) => index === 0 ? { ...row, sourceVersion: row.sourceVersion + 1 } : row) },
'原费用版本陈旧时拒绝确认', /原费用版本已变化/)
await noOutbound('陈旧原费用拒绝后回查无业务写入')
await request('POST', transferPath, { ...body, idempotencyKey: `fee-scope-${randomUUID()}`,
fees: source.mode === 'FULL' ? source.transferBody.fees : [] },
source.mode === 'FULL' ? '全覆盖禁止再登记派送应收' : '部分覆盖仍须登记未覆盖派送费',
source.mode === 'FULL' ? /已由原费用覆盖/ : /须填写派送费/)
await noOutbound('覆盖金额校验拒绝后回查无业务写入')
transferred = await request('POST', transferPath, body, '正式保存本次原费用服务覆盖并转派送')
}
await check('转派送生成本次真实出库和各目的地派送', transferred?.dispatchNo === source.dispatchNo && transferred.outboundNo
&& transferred.outboundStatus === 'WAIT_OUTBOUND' && transferred.deliveries?.length > 0
&& transferred.deliveries.every(row => row.deliveryNo && row.deliveryStatus === 'WAIT_DELIVERY'))
proof.outboundNo = transferred.outboundNo
proof.deliveryNos = transferred.deliveries.map(row => row.deliveryNo).sort()
const evidenceByDelivery = {}
const feesByDelivery = {}
let newReceivable = 0
const allCargo = []
for (const number of proof.deliveryNos) {
const detailPath = `/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(number)}?customerId=${encodeURIComponent(source.customerId)}`
const detail = await request('GET', detailPath, undefined, '回查派送详情中的原费用确认快照')
const evidence = detail?.feeCoverage
const destination = id(detail?.order?.destinationWarehouseCode || detail?.order?.destinationWarehouseId)
const destinationItems = new Set(dispatch.items.filter(row => id(row.destinationWarehouseCode || row.fbaWarehouseCode) === destination).map(row => id(row.id)))
await check('费用确认货物只属于当前派送目的地', destination && destinationItems.size > 0
&& evidence?.cargo?.every(row => destinationItems.has(id(row.dispatchItemId))))
await check('确认快照属于本次派送及实际原费用版本', detail?.order?.dispatchNo === source.dispatchNo
&& detail.order.deliveryNo === number && evidence?.cargoHash === preview.cargoHash
&& evidence.operatorId && Number.isFinite(Date.parse(evidence.confirmedAt)) && evidence.cargo?.length > 0
&& Array.isArray(evidence.references) && evidence.references.every(ref => evidence.cargo.some(row => id(row.sourceInboundId) === id(ref.source?.sourceInboundId)) && selectedFees.some(fee =>
id(fee.financeFeeId) === id(ref.source?.financeFeeId) && Number(fee.sourceVersion) === Number(ref.source?.sourceVersion)
&& fee.sourceHash === ref.source?.sourceHash && canonical(ref.services || []) === canonical(['DELIVERY_FEE']))))
const selectedInbounds = new Set(selectedFees.map(fee => id(fee.sourceInboundId)))
await check('快照保留本派送已选择的每票服务引用', evidence.cargo.every(row => !selectedInbounds.has(id(row.sourceInboundId))
|| evidence.references.some(ref => id(ref.source?.sourceInboundId) === id(row.sourceInboundId) && ref.services.includes('DELIVERY_FEE'))))
await check('费用明细为明确应收列表', Array.isArray(detail.fees) && detail.fees.every(row => row.feeDirection && row.feeType))
const added = detail.fees.filter(row => row.feeDirection === 'RECEIVABLE' && row.feeType === 'DELIVERY_FEE' && row.sourceStatus !== 'VOIDED')
await check('已全覆盖目的地不承担其他目的地的新增派送费', !evidence.cargo.every(row => selectedInbounds.has(id(row.sourceInboundId))) || added.length === 0)
await check('新增派送费没有伪零值或混合币种', added.every(row => Number(row.amount) > 0 && row.currency === source.transferBody.fees[0].currency))
newReceivable += added.reduce((sum, row) => sum + Number(row.amount), 0)
allCargo.push(...evidence.cargo.map(cargoIdentity))
evidenceByDelivery[number] = evidence
feesByDelivery[number] = canonical(detail.fees)
}
await check('多目的地快照汇总与实际原货物一致且不重复', canonical(allCargo) === canonical(preview.cargo.map(cargoIdentity)))
await check('仅未覆盖部分产生本次新增应收', Math.round(newReceivable * 100)
=== (source.mode === 'FULL' ? 0 : Math.round(Number(source.transferBody.fees[0].amount) * 100)))
const replay = await request('POST', transferPath, body, '原费用确认成功后同键重放')
await check('重放保持同一出库和派送集合', replay?.replayed === true && replay.outboundNo === proof.outboundNo
&& canonical((replay.deliveries || []).map(row => row.deliveryNo)) === canonical(proof.deliveryNos))
for (const number of proof.deliveryNos) {
const detail = await request('GET', `/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(number)}?customerId=${encodeURIComponent(source.customerId)}`, undefined, '重放后回查覆盖证据')
await check('重放未改写确认人时间和原费用快照', JSON.stringify(detail.feeCoverage) === JSON.stringify(evidenceByDelivery[number]))
await check('成功命令重放未追加费用', Array.isArray(detail.fees) && canonical(detail.fees) === feesByDelivery[number])
}
const after = await request('GET', `${dispatchPath}/fee-coverage`, undefined, '回查引用原费用未改变其来源事实')
await check('原费用版本金额与状态保持不变', canonical(after.feeSources.map(feeIdentity)) === canonical(proof.originalFees))
proof.snapshots = evidenceByDelivery
proof.newReceivable = newReceivable
proof.status = 'VERIFIED'
return proof
}