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 }