import { testPlannedCapacity } from './fleet-booking-reservation-acceptance.mjs' import { bindLooseboardDemand } from './looseboard-pool-binding.mjs' const API = '/api/platform/v1/last-leg' const MODULE = 'phase4-overseas-business' const OPTIONS = { scenario: 'looseboard-pool-preparation', deploymentBlocker: true } const validVersion = value => /^(0|[1-9]\d*)$/.test(String(value)) && Number.isSafeInteger(Number(value)) /** 复用本轮已审核且未扫码车次,发布可核验余位并衔接散板原单。 */ export async function prepareLooseboardPool(context, { demand, tripProof, destinationKey, unitPrice }, bind = bindLooseboardDemand) { if (!context.createdResources?.fleetTrips?.includes(tripProof) || tripProof.preparation !== 'VERIFIED' || !tripProof.tripNo || !tripProof.dispatchNo || tripProof.looseboardOffer || context.createdResources?.phase4OverseasBusiness?.looseboard !== demand || demand?.marker !== context.marker || demand.finalStatus !== 'PENDING_ACCEPTANCE' || demand.poolBinding || !destinationKey || !Number.isFinite(unitPrice) || unitPrice <= 0 || typeof context.checkpoint !== 'function') { throw Error('散板余位准备缺少本轮原单、已审核未占用车次、约定目的地/价格或检查点') } const proof = tripProof.looseboardOffer = { marker: context.marker, status: 'PREPARING', requests: [] } const check = (name, condition) => { context.assert(MODULE, name, null, Boolean(condition), `tripNo=${tripProof.tripNo}`, OPTIONS) if (!condition) throw Error(name) } const request = async (method, path, body) => { const log = { method, path, ...(body ? { body: structuredClone(body) } : {}), status: 'PREPARED' } proof.requests.push(log) await context.checkpoint() let response try { log.status = 'SENDING'; response = await context.request(method, path, body) } catch (error) { log.status = 'UNKNOWN_RESULT'; throw error } Object.assign(log, { http: response?.http, code: response?.body?.code, requestId: response?.requestId, traceId: response?.traceId }) const ok = response?.http === 200 && String(response?.body?.code) === '200' log.status = ok ? 'SUCCEEDED' : !response?.http || response.http >= 500 ? 'UNKNOWN_RESULT' : 'REJECTED' await context.checkpoint() context.apiSuccess(MODULE, `散板余位准备 ${method}`, response, JSON.stringify({ http: log.http, requestId: log.requestId, traceId: log.traceId }), OPTIONS) if (!ok) throw Error('散板余位准备失败;不得换键重造或自动重试') return response.body.data } const tripPath = `${API}/fleet/trips/${encodeURIComponent(tripProof.tripNo)}` const pool = `${API}/fleet/capacity-offers` try { const dispatch = await request('GET', `${API}/dispatches/${encodeURIComponent(tripProof.dispatchNo)}`) check('仅在原排车未扫码未发运时接入散板', dispatch?.dispatchNo === tripProof.dispatchNo && dispatch.status === 'ACTIVE' && ['BEFORE_LOADING', 'LOADING'].includes(dispatch.stage) && Number(dispatch.counts?.pickedPackageCount) === 0 && Number(dispatch.counts?.shippedPackageCount) === 0) let detail = await request('GET', tripPath) check('散板接车复用本轮同仓同车同司机', detail?.trip?.tripNo === tripProof.tripNo && detail.trip.status === 'PLANNED' && String(detail.trip.warehouseId) === String(tripProof.warehouseId) && String(detail.trip.vehicleId) === String(tripProof.vehicleId) && String(detail.trip.driverId) === String(tripProof.driverId) && detail.dispatches?.some(row => row.dispatchNo === tripProof.dispatchNo) && detail.trip.stops?.filter(stop => stop.destinationKey === destinationKey).length === 1) let plan = await request('GET', `${tripPath}/capacity-plan`) check('容量依据来自本轮原车真实货物', Array.isArray(plan?.cargo) && plan.cargo.length > 0 && plan.cargo.every(row => row.dispatchNo === tripProof.dispatchNo) && validVersion(plan.recordVersion) && typeof plan.manifestHash === 'string' && /^[0-9a-f]{64}$/.test(plan.manifestHash)) if (plan.planValid !== true) { const pallets = testPlannedCapacity(plan.cargo, detail.trip.stops) proof.planningBasis = { actual: false, kind: 'CONTROLLED_TEST_PLAN', pallets } plan = await request('POST', `${tripPath}/capacity-plan`, { recordVersion: plan.recordVersion, manifestHash: plan.manifestHash, pallets, reason: '仅本轮真实实托的受控预计占用;不得作为装车实测或回执' }) } check('车次容量计划有效且无阻断', plan?.planValid === true && !plan.blockReason && plan.availableSegments?.length > 0) const loading = await request('GET', `${tripPath}/loading`) check('预计容量未冒充实载确认', loading?.confirmedLoad == null) detail = await request('GET', tripPath) check('发布前车次仍未发车且版本有效', detail?.trip?.status === 'PLANNED' && validVersion(detail.trip.recordVersion)) const draftBody = { tripNo: tripProof.tripNo, tripRecordVersion: detail.trip.recordVersion, cutoffAt: new Date(Date.now() + 1800000).toISOString(), contactName: 'TEST调度', contactPhone: '19900000000', remark: context.marker, idempotencyKey: `${context.marker}-looseboard-offer` } const draft = await request('POST', pool, draftBody) check('余位草稿属于原车且版本完整', draft?.offer?.tripNo === tripProof.tripNo && draft.offer.status === 'DRAFT' && typeof draft.offer.offerNo === 'string' && draft.offer.offerNo.length > 0 && validVersion(draft.offer.tripRecordVersion) && validVersion(draft.offer.recordVersion)) proof.offerNo = draft.offer.offerNo const replay = await request('POST', pool, draftBody) check('余位发布前草稿重放不生成第二份', replay?.replayed === true && JSON.stringify(replay.offer) === JSON.stringify(draft.offer)) const publishBody = { tripRecordVersion: draft.offer.tripRecordVersion, offerRecordVersion: draft.offer.recordVersion, idempotencyKey: `${context.marker}-looseboard-publish` } const publishPath = `${pool}/${encodeURIComponent(proof.offerNo)}/publish` const published = await request('POST', publishPath, publishBody) check('原车余位已开放且可登记承运', published?.offer?.offerNo === proof.offerNo && published.offer.tripNo === tripProof.tripNo && published.offer.status === 'OPEN' && published.offer.capacityKnown && published.offer.canCreateInquiry && published.offer.canConfirm) const publishedAgain = await request('POST', publishPath, publishBody) check('余位发布重放不重复推进版本', publishedAgain?.replayed === true && JSON.stringify(publishedAgain.offer) === JSON.stringify(published.offer)) proof.status = 'OPEN' await context.checkpoint() const binding = await bind(context, { demand, tripProof, offerNo: proof.offerNo, destinationKey, unitPrice }) check('散板原单绑定已确认且指向原车余位', binding?.status === 'CONFIRMED_ORIGINAL_ORDER' && binding.tripNo === tripProof.tripNo && binding.offerNo === proof.offerNo) proof.status = 'BOUND_ORIGINAL_DEMAND' await context.checkpoint() return binding } catch (error) { proof.status = 'FAILED'; proof.failure = error.message await context.checkpoint() throw error } }