117 lines
11 KiB
JavaScript
117 lines
11 KiB
JavaScript
import { randomUUID } from 'node:crypto'
|
|||
|
|
|
||
|
|
const OPTIONS = { scenario: 'fleet-external-dispatch-price', deploymentBlocker: true }
|
||
|
|
const id = value => value == null ? '' : String(value)
|
||
|
|
const key = () => `fleet-dispatch-${randomUUID()}`
|
||
|
|
const stable = rows => JSON.stringify(rows)
|
||
|
|
|
||
|
|
/** Continues a current-run VERIFIED external receipt. Leaves both dispatches ready for the caller's real PDA picking. */
|
||
|
|
export async function verifyFleetExternalDispatch(context, module, tripProof) {
|
||
|
|
const receipt = tripProof.externalReceipt
|
||
|
|
if (!context.createdResources?.fleetTrips?.includes(tripProof) || receipt?.status !== 'VERIFIED' || tripProof.externalDispatch)
|
||
|
|
throw new Error('外接关联只接受本轮已验证实收且尚未执行关联的车次')
|
||
|
|
const proof = tripProof.externalDispatch = { status: 'CHECKING', marker: context.marker, requests: [] }
|
||
|
|
const customerId = id(receipt.createRequest.customerId)
|
||
|
|
const base = `/api/platform/v1/last-leg/fleet/trips/${encodeURIComponent(tripProof.tripNo)}`
|
||
|
|
const dispatchBase = '/api/platform/v1/last-leg/dispatches'
|
||
|
|
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({ method, path, label, http: response?.http, code: response?.body?.code,
|
||
|
|
requestId: response?.requestId, traceId: response?.traceId })
|
||
|
|
const conflict = response?.http === 409 || (response?.http === 200 && id(response?.body?.code) === 'A0409')
|
||
|
|
await check(label, response?.requestId && response?.traceId && (expectedMessage
|
||
|
|
? conflict && expectedMessage.test(id(response?.body?.message)) : response?.http === 200 && id(response?.body?.code) === '200'), response)
|
||
|
|
return response.body?.data
|
||
|
|
}
|
||
|
|
const before = await request('GET', `${base}/capacity-plan`, undefined, '关联外接前冻结有效计划与预留')
|
||
|
|
await check('实际外接仍计入承诺容量', before?.planValid && before.capacityPlan?.pallets?.length > 0
|
||
|
|
&& stable(before.availableSegments) === stable(receipt.finalSegments))
|
||
|
|
const candidates = await request('GET', `${dispatchBase}/candidates?${new URLSearchParams({ sourceStage: 'RECEIVED', orderNo: receipt.incomingNo })}`,
|
||
|
|
undefined, '查询本轮真实外接来货的可排车实托')
|
||
|
|
await check('候选精确匹配本轮客户起运仓来货及实托', Array.isArray(candidates) && candidates.length === 1
|
||
|
|
&& id(candidates[0].customerId) === customerId && id(candidates[0].overseasWarehouseId) === id(tripProof.warehouseId)
|
||
|
|
&& candidates[0].inboundCode === receipt.incomingNo && id(candidates[0].warehousePalletId) === receipt.warehousePalletId
|
||
|
|
&& candidates[0].palletNo === receipt.palletNo && Number(candidates[0].packageCount) === 1
|
||
|
|
&& candidates[0].sourceStage === 'RECEIVED' && candidates[0].planPalletId)
|
||
|
|
const candidate = candidates[0]
|
||
|
|
proof.draftToken = key()
|
||
|
|
const container = await request('POST', `${dispatchBase}/drafts/containers`, { draftToken: proof.draftToken, capacity: 30 }, '建立本轮外接排车草稿')
|
||
|
|
await check('草稿返回明确容器引用', container?.id)
|
||
|
|
proof.draftContainerId = id(container.id)
|
||
|
|
await request('POST', `${dispatchBase}/draft-locks/lock`, { planPalletId: id(candidate.planPalletId),
|
||
|
|
draftToken: proof.draftToken, draftContainerId: proof.draftContainerId }, '正式锁定本轮外接实托')
|
||
|
|
proof.preArrangeRequest = { planPalletIds: [id(candidate.planPalletId)], draftToken: proof.draftToken }
|
||
|
|
const arranged = await request('POST', `${dispatchBase}/pre-arrange`, proof.preArrangeRequest, '从真实已收货来源生成外接客户排车')
|
||
|
|
await check('外接排车只包含本票实收货物', arranged?.dispatchNo && arranged.dispatchNo !== tripProof.dispatchNo
|
||
|
|
&& arranged.items?.length === 1 && id(arranged.items[0].warehousePalletId) === receipt.warehousePalletId
|
||
|
|
&& Number(arranged.packageCount) === 1 && Number(arranged.pickedPackageCount) === 0 && Number(arranged.shippedPackageCount) === 0)
|
||
|
|
proof.dispatchNo = arranged.dispatchNo
|
||
|
|
const replay = await request('POST', `${dispatchBase}/pre-arrange`, proof.preArrangeRequest, '重放外接预排不生成第二张排车')
|
||
|
|
await check('预排重放保持排车与实托', replay?.dispatchNo === proof.dispatchNo && stable(replay.items) === stable(arranged.items))
|
||
|
|
const bookingPath = `${base}/bookings/${encodeURIComponent(receipt.bookingNo)}`
|
||
|
|
const bookingList = await request('GET', `${base}/bookings`, undefined, '读取关联前最新预订及车次版本')
|
||
|
|
const booking = bookingList?.records?.find(row => row.bookingNo === receipt.bookingNo)
|
||
|
|
await check('本票仍为实收完成且未关联', booking?.status === 'RECEIVED' && booking.incomingNo === receipt.incomingNo && !booking.dispatchNo)
|
||
|
|
proof.bindRequest = { tripRecordVersion: bookingList.tripRecordVersion, recordVersion: booking.recordVersion,
|
||
|
|
dispatchNo: proof.dispatchNo, idempotencyKey: key() }
|
||
|
|
const linked = await request('POST', `${bookingPath}/dispatch`, proof.bindRequest, '将本票外接真实排车关联到承诺车次')
|
||
|
|
await check('关联后预订绑定同一排车', linked?.booking?.status === 'BOUND' && linked.booking.dispatchNo === proof.dispatchNo)
|
||
|
|
const linkedReplay = await request('POST', `${bookingPath}/dispatch`, proof.bindRequest, '重放外接排车关联')
|
||
|
|
await check('关联重放不增加版本或第二次占位', linkedReplay?.replayed === true
|
||
|
|
&& id(linkedReplay.tripRecordVersion) === id(linked.tripRecordVersion) && stable(linkedReplay.booking) === stable(linked.booking))
|
||
|
|
const after = await request('GET', `${base}/capacity-plan`, undefined, '读取新增真实货物后的计划容量')
|
||
|
|
await check('新增外接货物使旧容量确认待重核', after?.planValid === false && after.cargo?.some(row => row.dispatchNo === proof.dispatchNo
|
||
|
|
&& id(row.warehousePalletId) === receipt.warehousePalletId && Number(row.packages) === 1))
|
||
|
|
const pallets = new Map(before.capacityPlan.pallets.map(row => [id(row.warehousePalletId), row]))
|
||
|
|
const actual = receipt.finalBooking.receipt.source.pallets[0]
|
||
|
|
pallets.set(receipt.warehousePalletId, { warehousePalletId: receipt.warehousePalletId, palletSpaces: 1,
|
||
|
|
weightKg: Number(actual.weightKg), volumeCbm: Number(actual.volumeCbm),
|
||
|
|
deliveryIndex: tripProof.stops.findIndex(stop => stop.destinationKey === receipt.finalBooking.cargo.destinationKey) })
|
||
|
|
await check('新容量只引用原计划和已核实外接实测', after.cargo.every(row => pallets.has(id(row.warehousePalletId)))
|
||
|
|
&& new Set(after.cargo.map(row => id(row.warehousePalletId))).size === pallets.size)
|
||
|
|
const replanned = await request('POST', `${base}/capacity-plan`, { recordVersion: after.recordVersion, manifestHash: after.manifestHash,
|
||
|
|
pallets: [...pallets.values()], reason: '外接预订转实际发运后按本轮实测重新确认,不重复计算原预留' }, '重新核对包含外接实物的全部货物容量')
|
||
|
|
await check('从预留转实际关联后分段总占用守恒', replanned?.planValid === true && stable(replanned.availableSegments) === stable(before.availableSegments))
|
||
|
|
let detail = await request('GET', base, undefined, '读取本车所有客户排车待审核情况')
|
||
|
|
await check('自有货与外接客户排车均保留', detail?.dispatches?.some(row => row.dispatchNo === tripProof.dispatchNo)
|
||
|
|
&& detail.dispatches.some(row => row.dispatchNo === proof.dispatchNo))
|
||
|
|
for (const binding of detail.dispatches) detail = await request('POST', `${base}/dispatches/${encodeURIComponent(binding.dispatchNo)}/review`,
|
||
|
|
{ recordVersion: detail.trip.recordVersion, decision: 'APPROVED' }, '关联变化后逐客户通过调度审核')
|
||
|
|
await check('每张客户执行单都有通过事实', detail.dispatches.every(row => row.review?.status === 'APPROVED'))
|
||
|
|
const dispatchPath = `${dispatchBase}/${encodeURIComponent(proof.dispatchNo)}`
|
||
|
|
const freight = await request('GET', `${dispatchPath}/external-freight`, undefined, '读取本票实收约定运费')
|
||
|
|
await check('外接报价精确对应本票实收和确认单价', freight?.requiresDeliveryFee === false && freight.quotes?.length === 1
|
||
|
|
&& freight.quotes[0].bookingNo === receipt.bookingNo && freight.quotes[0].incomingNo === receipt.incomingNo
|
||
|
|
&& freight.quotes[0].currency === 'CNY' && Number(freight.quotes[0].amount) === 30
|
||
|
|
&& Number(freight.quotes[0].quantity) === 1 && Number(freight.quotes[0].unitPrice) === 30)
|
||
|
|
proof.transferRequest = { idempotencyKey: key(), plannedDeliveryAt: new Date(Date.now() + 21600000).toISOString().slice(0, 19),
|
||
|
|
deliveryMethod: 'TRUCK_PALLET', pickupMethod: 'WAREHOUSE_PICKUP', loadingType: 'PALLET', vehicleType: 'TRUCK',
|
||
|
|
vehicleSize: Number(detail.trip.maxPalletSpaces) <= 15 ? '15托' : '30托', vehiclePlate: detail.trip.vehiclePlate,
|
||
|
|
driverName: detail.trip.driverName, driverPhone: detail.trip.driverPhone, appointmentAccount: 'SELF',
|
||
|
|
remark: `${context.marker} 外接实收报价验收`, externalFreightHash: freight.snapshotHash, fees: [] }
|
||
|
|
await request('POST', `${dispatchPath}/transfer-delivery`, { ...proof.transferRequest,
|
||
|
|
idempotencyKey: key(), fees: [{ feeType: 'DELIVERY_FEE', feeName: '重复派送费测试', direction: 'RECEIVABLE', currency: 'CNY', amount: 30 }] },
|
||
|
|
'全部外接已有约定运费时拒绝重复派送应收', /重复|再次/)
|
||
|
|
const transferred = await request('POST', `${dispatchPath}/transfer-delivery`, proof.transferRequest, '按实收报价建立本票派送及外接应收来源')
|
||
|
|
await check('外接只生成本目的地一张派送', transferred?.dispatchNo === proof.dispatchNo && transferred.outboundNo
|
||
|
|
&& transferred.deliveries?.length === 1 && Number(transferred.packageCount) === 1)
|
||
|
|
proof.outboundNo = transferred.outboundNo; proof.deliveryNo = transferred.deliveries[0].deliveryNo
|
||
|
|
const deliveryPath = `/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(proof.deliveryNo)}?customerId=${encodeURIComponent(customerId)}`
|
||
|
|
const delivery = await request('GET', deliveryPath, undefined, '回查本票派送费用来源')
|
||
|
|
const fees = delivery?.fees?.filter(row => row.feeDirection === 'RECEIVABLE' && row.feeType === 'DELIVERY_FEE')
|
||
|
|
await check('本票只登记约定30元应收来源且没有伪零费用', fees?.length === 1 && Number(fees[0].amount) === 30 && fees[0].currency === 'CNY'
|
||
|
|
&& fees[0].sourceStatus === 'ACTIVE' && fees[0].reviewStatus === 'APPROVED' && delivery.order?.dispatchNo === proof.dispatchNo)
|
||
|
|
const transferReplay = await request('POST', `${dispatchPath}/transfer-delivery`, proof.transferRequest, '外接转派送原请求重放')
|
||
|
|
const replayedDelivery = await request('GET', deliveryPath, undefined, '重放后回查外接应收未重复')
|
||
|
|
await check('外接重放保持派送及费用来源', transferReplay?.replayed === true && transferReplay.outboundNo === proof.outboundNo
|
||
|
|
&& stable(replayedDelivery.fees) === stable(delivery.fees))
|
||
|
|
proof.customerId = customerId; proof.finalTripVersion = detail.trip.recordVersion
|
||
|
|
proof.sourceFees = fees; proof.financeAcknowledgement = 'NOT_VERIFIED'
|
||
|
|
proof.status = 'VERIFIED'
|
||
|
|
return proof
|
||
|
|
}
|