215 lines
23 KiB
JavaScript
215 lines
23 KiB
JavaScript
import { DISPATCH_BASE as BASE, id as stringId, version, commandKey, dispatchIo, replay, readDispatch } from './grouped-dispatch-acceptance.mjs'
|
|
import { pickAll } from './phase5-overseas-fulfillment.mjs'
|
|
import { confirmFleetLoading } from './fleet-trip-fulfillment.mjs'
|
|
|
|
export const stableRows = rows => JSON.stringify(rows.map(row => JSON.stringify(row)).sort())
|
|
const clone = value => structuredClone(value)
|
|
const id = value => { if (typeof value === 'number' && !Number.isSafeInteger(value)) throw Error('服务端ID精度无效,必须保留Long字符串'); return stringId(value) }
|
|
const positive = value => /^[1-9]\d*$/.test(id(value))
|
|
const sha = value => /^[a-f0-9]{64}$/i.test(value || '')
|
|
const cents = value => { const text = String(value); if (!/^\d+(?:\.\d{1,2})?$/.test(text)) throw Error('费用必须为明确的非负两位金额'); const [whole, fraction = ''] = text.split('.'); return BigInt(whole) * 100n + BigInt(fraction.padEnd(2, '0')) }
|
|
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 = fee => ({ financeFeeId: id(fee.financeFeeId), customerId: id(fee.customerId), currency: fee.currency, amount: String(fee.amount), sourceVersion: fee.sourceVersion, sourceHash: fee.sourceHash, status: fee.status, sourceKeys: [...fee.sourceKeys].sort() })
|
|
|
|
export function fleetConsumerIo(context, module, proof, namespace) {
|
|
const io = dispatchIo(context, module, proof, namespace)
|
|
const request = async (method, path, body, label, expected) => {
|
|
const response = await io.send(method, path, body, label)
|
|
io.check(`${label}诊断回执完整`, response, Boolean(response?.requestId) && Boolean(response?.traceId))
|
|
io.check(label, response, expected ? [200, 409].includes(response.http) && String(response.body?.code) === 'A0409' && expected.test(String(response.body?.message))
|
|
: response.http === 200 && String(response.body?.code) === '200')
|
|
return response.body?.data
|
|
}
|
|
return { ...io, request, get: (path, label) => request('GET', path, undefined, label) }
|
|
}
|
|
|
|
export async function readyFleetDispatch(context, module, io, trip, { pickerToken, actualMeasurements } = {}) {
|
|
io.check('真实履约写入须有正式持久化检查点', null, typeof context.checkpoint === 'function')
|
|
let current = await readDispatch(io, trip.dispatchNo)
|
|
io.check('本轮实车仓库及未发车范围', null, context.createdResources.fleetTrips?.includes(trip) && trip.tripNo
|
|
&& current.warehouseId === id(trip.warehouseId) && current.data.status === 'ACTIVE' && Number(current.data.counts.shippedPackageCount) === 0)
|
|
if (current.data.stage === 'LOADING') {
|
|
io.check('真实扫码需本轮PDA身份', null, typeof pickerToken === 'string' && pickerToken.length > 0)
|
|
io.check('PDA逐来源真实扫码完成', null, await pickAll(context, pickerToken, trip.dispatchNo))
|
|
await confirmFleetLoading(context, module, trip, pickerToken, actualMeasurements)
|
|
current = await readDispatch(io, trip.dispatchNo)
|
|
}
|
|
io.check('安排前已真实扫码并显式装车完成', null, current.data.stage === 'AFTER_LOADING' && current.data.pickingReconciled === true
|
|
&& Number(current.data.counts.packageCount) > 0 && Number(current.data.counts.pickedPackageCount) === Number(current.data.counts.packageCount)
|
|
&& current.items.items.every(i => Number(i.pickedPackages) === Number(i.scheduledPackages)))
|
|
return current
|
|
}
|
|
|
|
// FeeCandidate未暴露sourceInboundId,仅在sourceKeys可唯一映射到实际原入仓时选择,混托歧义直接阻断。
|
|
export function coveragePlan(preview, source, externalReceipt) {
|
|
if (!preview.groups?.length) throw Error('缺少当前全车派送分组')
|
|
const firstleg = preview.groups.flatMap(g => g.cargo.filter(c => c.sourceRegion === 'FIRSTLEG'))
|
|
const inboundIds = [...new Set(firstleg.map(c => id(c.sourceInboundId)))].sort()
|
|
if (!inboundIds.length || inboundIds.some(v => !positive(v)) || source.mode === 'PARTIAL' && inboundIds.length < 2) throw Error('FULL/PARTIAL需要本轮真实原入仓,PARTIAL至少两票')
|
|
const selectedInbounds = new Set(source.mode === 'FULL' ? inboundIds : [inboundIds[0]])
|
|
const originalFees = [], selections = new Map(), uncovered = []
|
|
for (const group of preview.groups) {
|
|
if (!group.groupKey || !positive(group.customerId) || !group.destination?.key || !sha(group.groupCargoHash) || !sha(group.feeCoverageCargoHash)
|
|
|| !sha(group.externalFreightHash) || !Array.isArray(group.blockReasons) || group.blockReasons.length || !Array.isArray(group.feeSources) || !Array.isArray(group.externalQuotes)
|
|
|| !group.cargo.length || group.cargo.some(c => id(c.customerId) !== id(group.customerId) || c.destination?.key !== group.destination.key)) throw Error('客户目的地或费用预览范围不完整')
|
|
const own = group.cargo.filter(c => c.sourceRegion === 'FIRSTLEG')
|
|
if (own.some(c => id(c.customerId) !== id(source.customerId))) throw Error('原费用跨客户,不能沿其他客户确认覆盖')
|
|
if (group.externalQuotes.some(q => !externalReceipt || q.bookingNo !== externalReceipt.bookingNo || q.inboundCode !== externalReceipt.incomingNo
|
|
|| id(q.customerId) !== id(group.customerId) || q.groupKey !== group.groupKey || q.sourceHash !== group.externalFreightHash
|
|
|| q.currency !== externalReceipt.finalBooking.cargo.currency || cents(q.amount) !== cents(externalReceipt.finalBooking.agreedAmount))) throw Error('当前外接报价与本轮运营确认金额或来源摘要不一致')
|
|
for (const cargo of group.cargo.filter(c => c.sourceRegion !== 'FIRSTLEG')) {
|
|
if (!externalReceipt || cargo.inboundCode !== externalReceipt.incomingNo || !externalReceipt.createRequest
|
|
|| id(cargo.customerId) !== id(externalReceipt.createRequest.customerId)
|
|
|| !group.externalQuotes.some(q => q.bookingNo === externalReceipt.bookingNo && q.inboundCode === cargo.inboundCode && id(q.customerId) === id(group.customerId))) throw Error('非头程货物没有本轮已确认外接报价')
|
|
}
|
|
const byInbound = new Map()
|
|
for (const fee of group.feeSources) {
|
|
if (id(fee.customerId) !== id(group.customerId) || !positive(fee.financeFeeId) || fee.currency !== 'CNY' || !['AVAILABLE', 'SETTLED'].includes(fee.status)
|
|
|| !Number.isSafeInteger(fee.sourceVersion) || fee.sourceVersion < 1 || !fee.sourceHash || !Array.isArray(fee.sourceKeys) || !fee.sourceKeys.length
|
|
|| !fee.services?.includes('DELIVERY_FEE')) throw Error('原费用客户、版本或来源不可用')
|
|
cents(fee.amount)
|
|
const actual = own.filter(c => fee.sourceKeys.includes(c.sourceKey)), ids = new Set(actual.map(c => id(c.sourceInboundId)))
|
|
if (ids.size !== 1 || fee.sourceKeys.some(k => !actual.some(c => c.sourceKey === k))) throw Error('原费用sourceKeys不能唯一映射原入仓;需生产预览提供原费用sourceInboundId或无混托真实来源')
|
|
const inbound = [...ids][0]
|
|
if (!byInbound.has(inbound)) byInbound.set(inbound, fee)
|
|
originalFees.push({ groupKey: group.groupKey, ...feeIdentity(fee) })
|
|
}
|
|
if (own.some(c => !byInbound.has(id(c.sourceInboundId)))) throw Error('本轮每票必须有实际原费用,不能借其他票补齐')
|
|
selections.set(group.groupKey, [...byInbound].filter(([inbound]) => selectedInbounds.has(inbound)).map(([, fee]) => ({ financeFeeId: id(fee.financeFeeId), sourceVersion: fee.sourceVersion, sourceHash: fee.sourceHash, services: ['DELIVERY_FEE'] })))
|
|
if (own.some(c => !selectedInbounds.has(id(c.sourceInboundId)))) uncovered.push(group.groupKey)
|
|
}
|
|
const template = source.transferBody
|
|
if (template?.loadingType !== 'PALLET' || template.appointmentAccount !== 'SELF' || template.fees?.length !== 1
|
|
|| template.fees[0].direction !== 'RECEIVABLE' || template.fees[0].feeType !== 'DELIVERY_FEE' || cents(template.fees[0].amount) <= 0n) throw Error('覆盖用例须有明确的未覆盖派送应收金额')
|
|
if (uncovered.length > 1 && uncovered.some(key => !Array.isArray(template.groupFees?.[key]) || template.groupFees[key].length !== 1)) throw Error('多个未覆盖目的地须由本轮用例明确逐组费用,不能自动分摊')
|
|
const groups = preview.groups.map(group => {
|
|
const fees = uncovered.includes(group.groupKey) ? clone(template.groupFees?.[group.groupKey] || template.fees) : []
|
|
if (uncovered.includes(group.groupKey) && fees.length !== 1 || fees.some(f => f.direction !== 'RECEIVABLE' || f.feeType !== 'DELIVERY_FEE' || cents(f.amount) <= 0n || !['CNY', 'USD'].includes(f.currency) || f.currency !== template.fees[0].currency)) throw Error('逐组未覆盖应收不完整')
|
|
return { groupKey: group.groupKey, groupCargoHash: group.groupCargoHash, plannedDeliveryAt: template.plannedDeliveryAt || new Date(Date.now() + 21600000).toISOString(),
|
|
deliveryMethod: template.deliveryMethod || 'TRUCK_PALLET', waybillNo: template.waybillNo || null, carrierName: template.carrierName || null,
|
|
pickupMethod: template.pickupMethod || 'WAREHOUSE_PICKUP', loadingType: 'PALLET', vehicleType: template.vehicleType || 'TRUCK', vehicleSize: template.vehicleSize || null,
|
|
appointmentAccount: 'SELF', sellerName: null, remark: template.remark || null, externalFreightHash: group.externalFreightHash,
|
|
feeCoverageCargoHash: group.feeCoverageCargoHash, feeCoverage: selections.get(group.groupKey), fees }
|
|
})
|
|
return { groups, originalFees, selectedInbounds: [...selectedInbounds], inboundIds }
|
|
}
|
|
|
|
async function noDocuments(io, source, label) {
|
|
let total, rows = []
|
|
for (let page = 1; ; page++) {
|
|
const data = await io.get(`/api/platform/v1/last-leg/outbound-orders?${new URLSearchParams({ customerId: id(source.customerId), size: '200', page: String(page) })}`, label)
|
|
io.check('客户出库分页完整稳定', null, Array.isArray(data?.records) && Number.isSafeInteger(Number(data.total)) && Number(data.page) === page && Number(data.size) === 200
|
|
&& Number(data.total) >= 0 && (total === undefined || total === Number(data.total)) && (data.records.length > 0 || rows.length === Number(data.total)))
|
|
total ??= Number(data.total); rows.push(...data.records); if (rows.length >= total) break
|
|
}
|
|
io.check('拒绝后无本排车出库写入', null, rows.length === total && new Set(rows.map(r => r.outboundNo)).size === rows.length && rows.every(r => r.dispatchNo !== source.dispatchNo))
|
|
}
|
|
|
|
/** 两个本轮不重叠来源用例必须先准备;从不取消已扫码FULL再复用同批实物。 */
|
|
export async function verifyFleetFeeCoverage(context, module, source, { reconcilePersisted = false, pickerToken, actualMeasurements, externalTrip } = {}) {
|
|
const cases = context.createdResources?.fleetFeeCoverageCases
|
|
if (!cases?.includes(source) || source.marker !== context.marker || !['FULL', 'PARTIAL'].includes(source.mode) || source.proof && !reconcilePersisted) throw Error('原费用验收需要本轮专用、未执行的排车用例;保留原请求')
|
|
const pair = ['FULL', 'PARTIAL'].map(mode => cases.filter(c => c.marker === context.marker && c.mode === mode))
|
|
if (pair.some(rows => rows.length !== 1) || pair[0][0].dispatchNo === pair[1][0].dispatchNo) throw Error('需本轮生产者预先准备FULL/PARTIAL两组不重叠真实排车,不能取消已扫码FULL复用同批实物')
|
|
if (reconcilePersisted && (!source.proof?.confirmationRequest || !source.proof?.outboundNo || !source.proof?.deliveryNos?.length)) throw Error('续验须有原成功命令和已持久化单据,不可重造')
|
|
const proof = reconcilePersisted ? source.proof : source.proof = { marker: context.marker, status: 'CHECKING', mode: source.mode, dispatchNo: source.dispatchNo, requests: [] }
|
|
const io = fleetConsumerIo(context, module, proof, `FEE-${source.mode}`), path = `${BASE}/${encodeURIComponent(source.dispatchNo)}`
|
|
try {
|
|
const scopes = []
|
|
for (const [candidate] of pair) scopes.push(await readDispatch(io, candidate.dispatchNo))
|
|
io.check('两组真实来源和实托均不重叠', null, scopes.every(s => s.items.items.length > 0) && scopes[0].items.items.every(i => !scopes[1].items.items.some(j => id(i.warehousePalletId) === id(j.warehousePalletId) || i.sourceKey === j.sourceKey)))
|
|
const trip = context.createdResources.fleetTrips?.find(t => t.dispatchNo === source.dispatchNo)
|
|
io.check('本排车有本轮正式车辆审核生产回执', null, Boolean(trip))
|
|
const loaded = await readyFleetDispatch(context, module, io, trip, { pickerToken, actualMeasurements })
|
|
const preview = await io.get(`${path}/delivery-arrangement`, '读取原生全车费用预览')
|
|
io.check('原生派送门禁、版本及实载证据完整', null, preview?.dispatchNo === source.dispatchNo && preview.status === 'ACTIVE' && preview.stage === 'AFTER_LOADING'
|
|
&& id(version(preview.recordVersion)) === id(version(loaded.data.recordVersion)) && sha(preview.cargoHash) && sha(preview.loadingCompletionHash)
|
|
&& (reconcilePersisted ? Boolean(preview.arrangement) : preview.arrange?.allowed === true && !preview.arrangement))
|
|
const receipt = externalTrip?.externalReceipt
|
|
if (externalTrip && (!context.createdResources.fleetTrips.includes(externalTrip) || externalTrip.dispatchNo !== source.dispatchNo || receipt?.status !== 'VERIFIED')) throw Error('外接费用只接受本轮同主单真实运营确认')
|
|
const plan = coveragePlan(preview, source, receipt)
|
|
const totals = new Map()
|
|
for (const c of preview.groups.flatMap(g => g.cargo)) totals.set(id(c.dispatchItemId), (totals.get(id(c.dispatchItemId)) || 0) + Number(c.packages))
|
|
io.check('费用分组逐实物客户与箱数核平', null, totals.size === loaded.items.items.length && loaded.items.items.every(i => totals.get(id(i.dispatchItemId)) === Number(i.scheduledPackages)
|
|
&& preview.groups.filter(g => g.cargo.some(c => id(c.dispatchItemId) === id(i.dispatchItemId))).every(g => id(g.customerId) === id(i.customerId) && g.destination.key === i.destination.key)))
|
|
const body = reconcilePersisted ? proof.confirmationRequest : { recordVersion: version(preview.recordVersion), idempotencyKey: commandKey(context.marker, `FEE-${source.mode}-${source.dispatchNo}`, 'arrange'),
|
|
cargoHash: preview.cargoHash, loadingCompletionHash: preview.loadingCompletionHash, groups: plan.groups }
|
|
if (!reconcilePersisted) { proof.confirmationRequest = clone(body); proof.originalFees = clone(plan.originalFees); await context.checkpoint?.() }
|
|
else io.check('原成功请求仍为同货物和实载', null, body.cargoHash === preview.cargoHash && body.loadingCompletionHash === preview.loadingCompletionHash)
|
|
const reject = async (label, changed, pattern) => {
|
|
const before = await readDispatch(io, source.dispatchNo)
|
|
await io.request('POST', `${path}/transfer-delivery`, { ...clone(body), ...changed, idempotencyKey: commandKey(context.marker, `${source.dispatchNo}-${source.mode}`, label) }, label, pattern)
|
|
for (const customerId of new Set(preview.groups.map(g => id(g.customerId)))) await noDocuments(io, { ...source, customerId }, `${label}后无单据`)
|
|
io.check(`${label}后整车及实物零漂移`, null, JSON.stringify(await readDispatch(io, source.dispatchNo)) === JSON.stringify(before))
|
|
}
|
|
if (!reconcilePersisted) {
|
|
for (const customerId of new Set(preview.groups.map(g => id(g.customerId)))) await noDocuments(io, { ...source, customerId }, '执行前确认无出库单')
|
|
await reject('陈旧全车货物摘要拒绝', { cargoHash: `${body.cargoHash[0] === '0' ? '1' : '0'}${body.cargoHash.slice(1)}` }, /货物.*变化/)
|
|
const stale = clone(body.groups), selected = stale.find(g => g.feeCoverage.length).feeCoverage[0]; selected.sourceVersion = selected.sourceVersion === 2147483647 ? selected.sourceVersion - 1 : selected.sourceVersion + 1
|
|
await reject('陈旧原费用版本拒绝', { groups: stale }, /原费用版本.*变化/)
|
|
const wrong = clone(body.groups)
|
|
if (source.mode === 'FULL') wrong[0].fees = clone(source.transferBody.fees)
|
|
else wrong.find(g => g.fees.length).fees = []
|
|
await reject(source.mode === 'FULL' ? '全覆盖重复应收拒绝' : '部分覆盖漏填应收拒绝', { groups: wrong }, source.mode === 'FULL' ? /覆盖.*重复|覆盖.*登记/ : /未覆盖.*费用/)
|
|
if (externalTrip) {
|
|
const extra = clone(body.groups), quoted = preview.groups.find(g => g.externalQuotes.length), input = extra.find(g => g.groupKey === quoted.groupKey), quote = quoted.externalQuotes[0]
|
|
input.fees.push({ direction: 'RECEIVABLE', feeType: 'DELIVERY_FEE', feeName: '本轮重复外接派送应收反例', currency: quote.currency, amount: quote.amount })
|
|
await reject('外接已有约定运费禁止追加重复应收', { groups: extra }, /重复|覆盖.*登记/)
|
|
}
|
|
} else {
|
|
const stored = await io.get(`/api/platform/v1/last-leg/outbound-orders/${encodeURIComponent(proof.outboundNo)}`, '先核对原成功出库和派送集合')
|
|
io.check('原单据完整且未执行', null, stored?.outbound?.dispatchNo === source.dispatchNo && stored.outbound.status === 'WAIT_OUTBOUND'
|
|
&& stableRows((stored.deliveries || []).map(d => d.deliveryNo)) === stableRows(proof.deliveryNos) && stored.deliveries.every(d => d.status === 'WAIT_DELIVERY'))
|
|
}
|
|
const result = await io.request('POST', `${path}/transfer-delivery`, body, '按全车真实分组确认原费用并安排派送'), arranged = result?.result
|
|
io.check('原生命令返回完整客户出库和客户目的地派送', null, result?.dispatchNo === source.dispatchNo && result.action === 'ARRANGE_DELIVERY' && result.status === 'ACTIVE' && result.stage === 'AFTER_LOADING'
|
|
&& BigInt(version(result.recordVersion)) > BigInt(version(body.recordVersion))
|
|
&& arranged?.dispatchNo === source.dispatchNo && arranged.cargoHash === body.cargoHash && arranged.deliveries?.length === preview.groups.length
|
|
&& new Set(arranged.deliveries.map(d => d.deliveryNo)).size === arranged.deliveries.length
|
|
&& arranged.outbounds?.length === new Set(preview.groups.map(g => id(g.customerId))).size
|
|
&& arranged.outbounds.every(o => o.outboundNo && o.status === 'WAIT_OUTBOUND' && arranged.deliveries.filter(d => d.outboundNo === o.outboundNo).reduce((n, d) => n + Number(d.packageCount), 0) === Number(o.packageCount)) && arranged.deliveries.every(d => d.deliveryNo && d.status === 'WAIT_DELIVERY'
|
|
&& preview.groups.some(g => g.groupKey === d.groupKey && id(g.customerId) === id(d.customerId) && g.destination.key === d.destination?.key
|
|
&& g.cargo.reduce((n, c) => n + Number(c.packages), 0) === Number(d.packageCount))
|
|
&& arranged.outbounds.some(o => o.outboundNo === d.outboundNo && id(o.customerId) === id(d.customerId))))
|
|
if (reconcilePersisted) io.check('仅恢复原出库和派送集合', null, result.replayed === true && stableRows(arranged.deliveries.map(d => d.deliveryNo)) === stableRows(proof.deliveryNos))
|
|
proof.outbounds = arranged.outbounds; proof.deliveries = arranged.deliveries; proof.command = result
|
|
proof.outboundNo = arranged.outbounds.find(o => id(o.customerId) === id(source.customerId))?.outboundNo
|
|
proof.deliveryNos = arranged.deliveries.map(d => d.deliveryNo).sort()
|
|
const snapshots = {}, feeRows = {}
|
|
let added = 0n
|
|
for (const ref of arranged.deliveries) {
|
|
const group = preview.groups.find(g => g.groupKey === ref.groupKey), input = body.groups.find(g => g.groupKey === ref.groupKey)
|
|
const detail = await io.get(`/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(ref.deliveryNo)}?customerId=${encodeURIComponent(id(ref.customerId))}`, '回查当前客户目的地费用和原费快照')
|
|
io.check('派送只归当前客户和目的地', null, detail?.order?.dispatchNo === source.dispatchNo && detail.order.deliveryNo === ref.deliveryNo
|
|
&& id(detail.order.customerId) === id(group.customerId) && detail.order.outboundNo === ref.outboundNo
|
|
&& id(detail.order.destinationWarehouseCode || detail.order.destinationWarehouseId) === id(group.destination.warehouseCode || group.destination.warehouseId))
|
|
const evidence = detail.feeCoverage, expectedCargo = group.cargo.map(cargoIdentity)
|
|
if (input.feeCoverage.length) {
|
|
io.check('原费快照保留真实货物、确认人、时间和每票引用', null, evidence?.cargoHash === group.feeCoverageCargoHash && evidence.operatorId && Number.isFinite(Date.parse(evidence.confirmedAt))
|
|
&& stableRows(evidence.cargo.map(cargoIdentity)) === stableRows(expectedCargo) && Array.isArray(evidence.references)
|
|
&& stableRows(evidence.references.map(r => ({ financeFeeId: id(r.source?.financeFeeId), sourceVersion: r.source?.sourceVersion, sourceHash: r.source?.sourceHash, services: r.services }))) === stableRows(input.feeCoverage)
|
|
&& evidence.references.every(r => id(r.source.customerId) === id(group.customerId) && ['INBOUND_FEE', 'CONTAINER_FEE'].includes(r.source.sourceType)
|
|
&& group.cargo.some(c => id(c.sourceInboundId) === id(r.source.sourceInboundId)) && group.feeSources.some(f => id(f.financeFeeId) === id(r.source.financeFeeId)
|
|
&& f.currency === r.source.currency && cents(f.amount) === cents(r.source.amount) && f.status === r.source.status)))
|
|
} else io.check('未选原费不伪造覆盖证据', null, evidence == null || evidence.references?.length === 0)
|
|
io.check('费用为本客户明确正数应收列表', null, Array.isArray(detail.fees) && detail.fees.every(f => f.feeDirection && f.feeType))
|
|
const receivables = detail.fees.filter(f => f.feeDirection === 'RECEIVABLE' && f.feeType === 'DELIVERY_FEE' && f.sourceStatus !== 'VOIDED')
|
|
const expectedAmounts = [...input.fees.map(f => `${f.currency}:${cents(f.amount)}`), ...group.externalQuotes.filter(q => cents(q.amount) > 0n).map(q => `${q.currency}:${cents(q.amount)}`)].sort()
|
|
io.check('仅未覆盖货物和原外接报价产生应收,禁止重复或零值', null, stableRows(receivables.map(f => `${f.currency}:${cents(f.amount)}`).sort()) === stableRows(expectedAmounts)
|
|
&& receivables.every(f => cents(f.amount) > 0n && f.sourceStatus === 'ACTIVE' && f.reviewStatus === 'APPROVED'))
|
|
added += input.fees.reduce((n, f) => n + cents(f.amount), 0n)
|
|
snapshots[ref.deliveryNo] = detail.feeCoverage; feeRows[ref.deliveryNo] = stableRows(detail.fees)
|
|
}
|
|
const replayed = await replay(io, `${path}/transfer-delivery`, body, result, '费用确认原键原载荷重放')
|
|
io.check('费用安排重放明确恢复原成功命令', null, replayed.replayed === true)
|
|
for (const ref of arranged.deliveries) {
|
|
const detail = await io.get(`/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(ref.deliveryNo)}?customerId=${encodeURIComponent(id(ref.customerId))}`, '原键重放后回查每客户费用')
|
|
io.check('重放不改确认快照也不新增应收', null, JSON.stringify(detail.feeCoverage) === JSON.stringify(snapshots[ref.deliveryNo]) && stableRows(detail.fees) === feeRows[ref.deliveryNo])
|
|
}
|
|
const after = await io.get(`${path}/delivery-arrangement`, '回查原费用来源未被确认改变')
|
|
const currentFees = after.groups.flatMap(g => g.feeSources.map(f => ({ groupKey: g.groupKey, ...feeIdentity(f) })))
|
|
io.check('原费用版本金额状态保持不变', null, stableRows(currentFees) === stableRows(proof.originalFees))
|
|
proof.snapshots = snapshots; proof.newReceivable = Number(added) / 100; proof.status = 'VERIFIED'; await context.checkpoint?.(); return proof
|
|
} catch (error) { proof.status = proof.outcomeUnknown ? 'OUTCOME_UNKNOWN' : 'UNPROVEN'; proof.error = error.message; await context.checkpoint?.(); throw error }
|
|
}
|