86 lines
7.3 KiB
JavaScript
86 lines
7.3 KiB
JavaScript
import { DISPATCH_BASE as BASE, id as stringId, dispatchIo, 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 id = value => { if (typeof value === 'number' && !Number.isSafeInteger(value)) throw Error('服务端ID精度无效,必须保留Long字符串'); return stringId(value) }
|
|
|
|
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' && ['LOADING', 'DELIVERING'].includes(current.data.stage))
|
|
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 === 'DELIVERING' && 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
|
|
}
|
|
|
|
/** 装车完成已生成单据并发车;仅读权威单据,不补人工费用或覆盖选择。 */
|
|
export async function readAutomaticDelivery(io, current, trip) {
|
|
const documents = current.data.documents
|
|
const deliveries = documents?.deliveries, outbounds = documents?.outbounds
|
|
io.check('自动派送单据完整且客户箱数守恒', null, current.data.stage === 'DELIVERING'
|
|
&& Array.isArray(deliveries) && deliveries.length > 0 && Array.isArray(outbounds) && outbounds.length > 0
|
|
&& new Set(deliveries.map(d => d.deliveryNo)).size === deliveries.length
|
|
&& new Set(outbounds.map(o => o.outboundNo)).size === outbounds.length
|
|
&& deliveries.every(d => d.deliveryNo && d.status === 'DELIVERING' && outbounds.some(o => o.outboundNo === d.outboundNo && id(o.customerId) === id(d.customerId)))
|
|
&& outbounds.every(o => o.outboundNo && o.status === 'OUTBOUNDED')
|
|
&& deliveries.reduce((sum, d) => sum + Number(d.packageCount), 0) === Number(current.data.counts.packageCount)
|
|
&& current.items.items.every(i => deliveries.some(d => id(d.customerId) === id(i.customerId) && d.destination?.key === i.destination?.key)))
|
|
const snapshots = {}, feeRows = {}
|
|
for (const ref of deliveries) {
|
|
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 === trip.dispatchNo && detail.order.deliveryNo === ref.deliveryNo
|
|
&& id(detail.order.customerId) === id(ref.customerId) && detail.order.outboundNo === ref.outboundNo
|
|
&& Number(detail.order.packageCount) === Number(ref.packageCount) && detail.order.status === 'DELIVERING'
|
|
&& Array.isArray(detail.fees) && new Set(detail.fees.map(f => id(f.id))).size === detail.fees.length)
|
|
snapshots[ref.deliveryNo] = detail.feeCoverage; feeRows[ref.deliveryNo] = stableRows(detail.fees)
|
|
}
|
|
const command = trip.loadingCommand
|
|
io.check('本轮装车成功回执完整', null, command?.body?.idempotencyKey && command.result?.action === 'LOADING_COMPLETE'
|
|
&& command.result.dispatchNo === trip.dispatchNo && command.result.stage === 'DELIVERING')
|
|
return { outbounds, deliveries, snapshots, feeRows, confirmationRequest: command.body, command: command.result }
|
|
}
|
|
|
|
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('需本轮两组不重叠真实排车')
|
|
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, `AUTO-${source.mode}`)
|
|
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) && (!externalTrip || externalTrip === trip))
|
|
const current = await readyFleetDispatch(context, module, io, trip, { pickerToken, actualMeasurements })
|
|
const execution = await readAutomaticDelivery(io, current, trip)
|
|
if (reconcilePersisted) io.check('恢复仅读原生成单据集合', null, stableRows(proof.deliveryNos) === stableRows(execution.deliveries.map(d => d.deliveryNo)))
|
|
Object.assign(proof, execution, { outboundNo: execution.outbounds.find(o => id(o.customerId) === id(source.customerId))?.outboundNo,
|
|
deliveryNos: execution.deliveries.map(d => d.deliveryNo).sort(), feeCoverageSelection: 'NOT_APPLICABLE_AUTOMATIC_DELIVERY', 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 }
|
|
}
|