Files
baiyue-firstleg/scripts/admin-test-env-modules/fleet-external-dispatch-acceptance.mjs
2026-10-21 14:30:41 +08:00

115 lines
15 KiB
JavaScript

import { DISPATCH_BASE as BASE, id as stringId, version, createBody, candidatePage, readDispatch, replay } from './grouped-dispatch-acceptance.mjs'
import { fleetConsumerIo, readyFleetDispatch, stableRows, verifyFleetFeeCoverage, readAutomaticDelivery } from './fleet-fee-coverage-acceptance.mjs'
const clone = value => structuredClone(value)
const id = value => { if (typeof value === 'number' && !Number.isSafeInteger(value)) throw Error('服务端ID精度无效,必须保留Long字符串'); return stringId(value) }
const money = 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 identity = item => ({ dispatchItemId: id(item.dispatchItemId), sourceKey: item.sourceKey, customerId: id(item.customerId), warehousePalletId: id(item.warehousePalletId), scheduledPackages: Number(item.scheduledPackages), pickedPackages: Number(item.pickedPackages) })
/** 将本轮已实收外接货加入原车,复审及真实装车后读取自动派送。 */
export async function verifyFleetExternalDispatch(context, module, tripProof, { pickerToken, actualMeasurements, coverageCase } = {}) {
const receipt = tripProof?.externalReceipt, cases = context.createdResources?.fleetFeeCoverageCases
if (!context.createdResources?.fleetTrips?.includes(tripProof) || receipt?.status !== 'VERIFIED' || tripProof.externalDispatch)
throw Error('外接只接受本轮已运营确认且尚未装入本车的真实来货')
if (coverageCase && (!cases?.includes(coverageCase) || coverageCase.marker !== context.marker || coverageCase.mode !== 'PARTIAL'
|| coverageCase.dispatchNo !== tripProof.dispatchNo || coverageCase.proof) || !pickerToken || typeof context.checkpoint !== 'function')
throw Error('外接加货前须有同主单未扫码PARTIAL用例及本轮PDA身份;必须先外接后扫码安排')
const proof = tripProof.externalDispatch = { marker: context.marker, dispatchNo: tripProof.dispatchNo, fulfillmentMode: 'AUTOMATIC_DELIVERY', status: 'CHECKING', requests: [] }
const io = fleetConsumerIo(context, module, proof, `EXTERNAL-${tripProof.dispatchNo}`)
const base = `/api/platform/v1/last-leg/fleet/trips/${encodeURIComponent(tripProof.tripNo)}`, path = `${BASE}/${encodeURIComponent(tripProof.dispatchNo)}`
const commandReplay = async (path, body, result, label) => { const again = await replay(io, path, body, result, label); io.check(`${label}明确恢复原结果`, null, again.replayed === true) }
try {
let current = await readDispatch(io, tripProof.dispatchNo)
io.check('只调整本轮同仓零扫码原主单', null, id(current.warehouseId) === id(tripProof.warehouseId) && current.data.status === 'ACTIVE'
&& ['BEFORE_LOADING', 'LOADING'].includes(current.data.stage) && Number(current.data.counts.pickedPackageCount) === 0 && Number(current.data.counts.shippedPackageCount) === 0)
const ownItems = current.items.items.map(identity)
const before = await io.get(`${base}/capacity-plan`, '冻结外接预留与真实原容量计划')
io.check('原容量和外接承诺均完整', null, before?.planValid === true && before.capacityPlan?.pallets?.length > 0 && JSON.stringify(before.availableSegments) === JSON.stringify(receipt.finalSegments))
let list = await io.get(`${base}/bookings`, '核对运营确认后的原booking和原报价')
const booking = list.records?.find(row => row.bookingNo === receipt.bookingNo), accepted = booking?.receipt
io.check('实时booking保持本轮客户仓实收和运营确认', null, booking?.status === 'RECEIVED' && !booking.dispatchNo && booking.incomingNo === receipt.incomingNo
&& id(booking.customerId) === id(receipt.createRequest?.customerId) && accepted?.source?.warehouseId === id(tripProof.warehouseId)
&& accepted.source.incomingNo === receipt.incomingNo && accepted.source.status === 'READY_FOR_DISPATCH' && accepted.source.receiptHash
&& accepted.source.pallets?.length > 0 && Array.isArray(accepted.pallets) && accepted.operatorId && Number.isFinite(Date.parse(accepted.confirmedAt))
&& JSON.stringify(booking.cargo) === JSON.stringify(receipt.finalBooking.cargo) && String(booking.agreedAmount) === String(receipt.finalBooking.agreedAmount))
const actual = accepted.source.pallets, packages = actual.reduce((n, p) => n + Number(p.packages), 0)
io.check('真实来货托盘和箱数守恒', null, new Set(actual.map(p => id(p.warehousePalletId))).size === actual.length && packages === Number(accepted.source.receivedPackages)
&& actual.every(p => p.warehousePalletId && p.palletNo && Number.isSafeInteger(Number(p.packages)) && Number(p.packages) > 0)
&& actual.some(p => id(p.warehousePalletId) === id(receipt.warehousePalletId)))
const page = await candidatePage(io, tripProof.warehouseId, { orderNo: receipt.incomingNo })
const candidates = page.filter(c => c.inboundCode === receipt.incomingNo && id(c.customerId) === id(booking.customerId)
&& id(c.warehouseId) === id(tripProof.warehouseId) && actual.some(p => id(p.warehousePalletId) === id(c.selection?.warehousePalletId)))
io.check('候选仅为本次确认的全部真实外接来源', null, candidates.length === actual.length && candidates.every(c => c.destination.key === booking.cargo.destinationKey
&& actual.some(p => id(p.warehousePalletId) === id(c.selection.warehousePalletId) && p.palletNo === c.palletNo && Number(p.packages) === Number(c.availablePackages))))
const addition = createBody(context.marker, `EXTERNAL-${tripProof.dispatchNo}`, candidates)
if (current.data.stage === 'LOADING') {
proof.returnRequest = { recordVersion: version(current.data.recordVersion), idempotencyKey: io.key('explicit-return'), reason: '本轮零扫码外接实收显式退回装车前,保留同一整车' }
const returned = await io.request('POST', `${path}/return-to-review`, proof.returnRequest, '零扫码明确退回审核')
await commandReplay(`${path}/return-to-review`, proof.returnRequest, returned, '退回审核原键重放')
current = await readDispatch(io, tripProof.dispatchNo)
}
io.check('同一主单已处于装车前可加货', null, current.data.stage === 'BEFORE_LOADING')
proof.addRequest = { recordVersion: version(current.data.recordVersion), idempotencyKey: io.key('add-real-incoming'), cargo: addition.cargo, batchAllocations: addition.batchAllocations }
const added = await io.request('POST', `${path}/items`, proof.addRequest, '真实已运营确认来源ADD_CARGO加入原主单')
io.check('加货命令保持原整车身份', null, added.dispatchNo === tripProof.dispatchNo && added.action === 'ADD_CARGO')
await commandReplay(`${path}/items`, proof.addRequest, added, '同主单加货原键重放')
current = await readDispatch(io, tripProof.dispatchNo)
io.check('加货和重放保留原客户物理行,仅增加本次完整来源', null, current.data.stage === 'BEFORE_LOADING'
&& current.items.items.length === ownItems.length + candidates.length && ownItems.every(i => current.items.items.some(j => JSON.stringify(identity(j)) === JSON.stringify(i)))
&& candidates.every(c => current.items.items.some(i => i.sourceKey === c.sourceKey && i.sourceVersion === c.sourceVersion && id(i.customerId) === id(booking.customerId)
&& id(i.warehousePalletId) === id(c.selection.warehousePalletId) && i.inboundCode === receipt.incomingNo && Number(i.scheduledPackages) === Number(c.availablePackages) && Number(i.pickedPackages) === 0)))
list = await io.get(`${base}/bookings`, '加货后刷新booking与真实车次版本')
const latest = list.records.find(b => b.bookingNo === receipt.bookingNo)
proof.bindRequest = { tripRecordVersion: version(list.tripRecordVersion), recordVersion: version(latest.recordVersion), dispatchNo: tripProof.dispatchNo, idempotencyKey: io.key('bind-same-whole') }
const bound = await io.request('POST', `${base}/bookings/${encodeURIComponent(receipt.bookingNo)}/dispatch`, proof.bindRequest, '原booking绑定本车唯一整车主单')
io.check('实收转关联只指向原主单', null, bound.booking?.status === 'BOUND' && bound.booking.dispatchNo === tripProof.dispatchNo && bound.booking.bookingNo === receipt.bookingNo
&& JSON.stringify(bound.booking.cargo) === JSON.stringify(booking.cargo) && String(bound.booking.agreedAmount) === String(booking.agreedAmount))
await commandReplay(`${base}/bookings/${encodeURIComponent(receipt.bookingNo)}/dispatch`, proof.bindRequest, bound, '同主单booking关联原键重放')
const after = await io.get(`${base}/capacity-plan`, '真实加货和预留转关联后重新核对容量')
io.check('实际新增货物使旧容量计划失效且没有第二张客户排车', null, after.planValid === false
&& after.cargo.every(c => c.dispatchNo === tripProof.dispatchNo) && candidates.every(c => after.cargo.some(i => id(i.warehousePalletId) === id(c.selection.warehousePalletId) && Number(i.packages) === Number(c.availablePackages))))
const pallets = new Map(before.capacityPlan.pallets.map(p => [id(p.warehousePalletId), clone(p)]))
for (const p of actual) {
const measurement = accepted.pallets.find(m => id(m.warehousePalletId) === id(p.warehousePalletId)), index = current.route.stops.findIndex(s => s.destinationKey === booking.cargo.destinationKey)
io.check('新托仅使用实际运营确认托位和来货实测', null, measurement && Number.isSafeInteger(measurement.palletSpaces) && measurement.palletSpaces > 0
&& Number(measurement.weightKg ?? p.weightKg) > 0 && Number(measurement.volumeCbm ?? p.volumeCbm) > 0 && index > 0)
pallets.set(id(p.warehousePalletId), { warehousePalletId: id(p.warehousePalletId), palletSpaces: measurement.palletSpaces,
weightKg: measurement.weightKg ?? p.weightKg, volumeCbm: measurement.volumeCbm ?? p.volumeCbm, deliveryIndex: index })
}
io.check('新容量逐托覆盖全部实际货物且没有额外资源', null, new Set(after.cargo.map(c => id(c.warehousePalletId))).size === pallets.size && after.cargo.every(c => pallets.has(id(c.warehousePalletId))))
proof.capacityRequest = { recordVersion: version(after.recordVersion), manifestHash: after.manifestHash, pallets: [...pallets.values()], reason: '本轮同主单外接实收按实际运营确认和原计划量重新核对' }
const planned = await io.request('POST', `${base}/capacity-plan`, proof.capacityRequest, '重新确认实际货物容量')
io.check('预留转实际后分段占用守恒', null, planned.planValid === true && JSON.stringify(planned.availableSegments) === JSON.stringify(before.availableSegments))
const trip = await io.get(base, '复审前核对资源和唯一整车关联')
io.check('原车辆司机起运仓和唯一排车保持不变', null, trip.dispatches?.length === 1 && trip.dispatches[0].dispatchNo === tripProof.dispatchNo
&& id(trip.trip?.warehouseId) === id(tripProof.warehouseId) && id(trip.trip.vehicleId) === id(tripProof.vehicleId) && id(trip.trip.driverId) === id(tripProof.driverId))
tripProof.stops = trip.trip.stops
for (const action of ['submit-review', 'dispatch-review', 'loading-review']) {
const detail = await io.get(path, `${action}前刷新原主单版本`)
const body = { recordVersion: version(detail.recordVersion), idempotencyKey: io.key(action), ...(action === 'submit-review' ? { remark: '本轮加入外接实物后全车重新提交' } : { decision: 'APPROVED', reason: '本轮外接实收全车重新审核' }) }
const result = await io.request('POST', `${path}/${action}`, body, `整车${action}`)
await commandReplay(`${path}/${action}`, body, result, `整车${action}原键重放`)
}
const ready = await io.get(path, '核对两级审核恢复真实扫码资格')
io.check('同主单两级审核通过且仍零扫码', null, ready.stage === 'LOADING' && ready.reviewState === 'APPROVED' && ready.reviews?.dispatchReview?.status === 'APPROVED'
&& ready.reviews?.loadingReview?.status === 'APPROVED' && Number(ready.counts.pickedPackageCount) === 0)
const execution = coverageCase ? await verifyFleetFeeCoverage(context, module, coverageCase, { pickerToken, actualMeasurements, externalTrip: tripProof })
: await readAutomaticDelivery(io, await readyFleetDispatch(context, module, io, tripProof, { pickerToken, actualMeasurements }), tripProof)
const expected = receipt.finalBooking
const deliveries = execution.deliveries.filter(d => id(d.customerId) === id(booking.customerId) && d.destination?.key === booking.cargo.destinationKey)
io.check('本次外接来源只落入其客户目的地派送', null, deliveries.length === 1 && id(deliveries[0].customerId) === id(booking.customerId))
proof.outboundNo = deliveries[0].outboundNo; proof.deliveryNo = deliveries[0].deliveryNo; proof.deliveries = execution.deliveries
const delivery = await io.get(`/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(proof.deliveryNo)}?customerId=${encodeURIComponent(id(booking.customerId))}`, '回查本客户外接应收来源')
const sources = await io.get(`/api/platform/v1/last-leg/finance-fee-sources?${new URLSearchParams({ customerId: id(booking.customerId), businessType: 'DELIVERY_ORDER', businessNo: proof.deliveryNo })}`, '按本次客户派送业务号回查外接应收来源')
const fees = Array.isArray(sources) ? sources.filter(f => f.direction === 'RECEIVABLE' && f.feeItemCode === 'DELIVERY_FEE' && f.sourceStatus === 'ACTIVE' && f.inboundCode === receipt.incomingNo) : []
const quantity = expected.cargo.priceBasis === 'PALLET' ? expected.cargo.palletSpaces : expected.cargo.priceBasis === 'ORDER' ? 1 : null
io.check('外接只登记本次约定金额应收且来源已审核', null, fees.length === 1 && money(fees[0].originalAmount) === money(expected.agreedAmount)
&& fees[0].originalCurrency === expected.cargo.currency && fees[0].reviewStatus === 'APPROVED' && id(fees[0].customerId) === id(booking.customerId)
&& quantity != null && Number(fees[0].chargeableQuantity) === Number(quantity) && money(fees[0].unitPrice) === money(expected.cargo.unitPrice)
&& fees[0].quantityUnit === (expected.cargo.priceBasis === 'PALLET' ? '托位' : '单')
&& fees[0].businessNo === proof.deliveryNo && delivery.fees.some(f => id(f.id) === id(fees[0].id) && money(f.amount) === money(fees[0].originalAmount) && f.currency === fees[0].originalCurrency))
proof.customerId = id(booking.customerId); proof.sourceFees = fees; proof.loadingRequest = execution.confirmationRequest
proof.finalTripVersion = (await io.get(base, '交接同车资源版本')).trip.recordVersion
proof.financeAcknowledgement = 'NOT_VERIFIED'; proof.status = 'VERIFIED'; await context.checkpoint?.(); return proof
} catch (error) { proof.status = proof.outcomeUnknown || context.createdResources.dispatchWriteBoundary?.outcomeUnknown ? 'OUTCOME_UNKNOWN' : 'UNPROVEN'; proof.error = error.message; await context.checkpoint?.(); throw error }
}