98 lines
7.2 KiB
JavaScript
98 lines
7.2 KiB
JavaScript
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
|
|
}
|
|
}
|