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 }