Files
baiyue-firstleg/scripts/admin-test-env-modules/looseboard-pool-binding.mjs

102 lines
8.2 KiB
JavaScript

const BASE = '/api/platform/v1/last-leg/fleet/capacity-offers'
const MODULE = 'phase4-overseas-business'
const OPTIONS = { scenario: 'looseboard-pool-binding', deploymentBlocker: true }
const version = value => /^(0|[1-9]\d*)$/.test(String(value)) && Number.isSafeInteger(Number(value))
/** 将本轮散板原单绑定本轮已发布车次余位,禁止无来源另建商业订单。 */
export async function bindLooseboardDemand(context, { demand, tripProof, offerNo, destinationKey, unitPrice }) {
if (context.createdResources?.phase4OverseasBusiness?.looseboard !== demand || demand?.marker !== context.marker
|| demand.finalStatus !== 'PENDING_ACCEPTANCE' || demand.poolBinding
|| !context.createdResources?.fleetTrips?.includes(tripProof) || !tripProof?.tripNo
|| tripProof.looseboardOffer?.marker !== context.marker || tripProof.looseboardOffer?.offerNo !== offerNo
|| !destinationKey || !Number.isFinite(unitPrice) || unitPrice <= 0 || typeof context.checkpoint !== 'function') {
throw Error('散板匹配缺少本轮原单、车次、发布证据、目的地、约定价格或检查点')
}
const proof = demand.poolBinding = { marker: context.marker, offerNo, tripNo: tripProof.tripNo, status: 'CHECKING', requests: [] }
const check = (name, ok) => {
context.assert(MODULE, name, null, Boolean(ok), `orderNo=${demand.orderNo}`, OPTIONS)
if (!ok) throw Error(name)
}
const request = async (method, path, body) => {
const log = { method, path, status: 'SENDING' }; proof.requests.push(log)
await context.checkpoint()
const response = await context.request(method, path, body)
Object.assign(log, { http: response?.http, code: response?.body?.code, requestId: response?.requestId, traceId: response?.traceId })
const success = response?.http === 200 && String(response?.body?.code) === '200'
log.status = success ? 'SUCCEEDED' : !response?.http || response.http >= 500 ? 'UNKNOWN_RESULT' : 'REJECTED'
await context.checkpoint()
context.apiSuccess(MODULE, `散板原单接车 ${method}`, response, JSON.stringify(log), OPTIONS)
if (!success) throw Error('散板接车请求失败,保留原单与请求证据,不自动重试')
return response.body.data
}
const originalPath = `/api/platform/v1/last-leg/local-looseboard-orders/${encodeURIComponent(demand.orderNo)}?customerId=${encodeURIComponent(demand.customerId)}`
const validateInquiry = (row, status) => check('询价归属本轮原车与原客户', row?.tripNo === tripProof.tripNo && row.offerNo === offerNo
&& String(row.customerId) === String(demand.customerId) && row.businessType === 'LOCAL_LOOSEBOARD'
&& row.status === status && row.bookingNo && (!proof.bookingNo || row.bookingNo === proof.bookingNo)
&& version(row.recordVersion) && row.cargo?.destinationKey === destinationKey)
try {
const original = await request('GET', originalPath)
const summary = original?.summary
check('散板原单仍可接车且无已有承运', summary?.orderNo === demand.orderNo && String(summary.customerId) === String(demand.customerId)
&& summary.status === 'PENDING_ACCEPTANCE' && version(summary.version) && version(demand.version)
&& Number(summary.version) === Number(demand.version)
&& !summary.fleetArrangement && original.remark?.startsWith(context.marker))
const offer = (await request('GET', `${BASE}/${encodeURIComponent(offerNo)}`))?.offer
const destination = offer?.destinations?.find(row => row.destinationKey === destinationKey)
check('余位为本轮同仓开放车次且容量明确', offer?.offerNo === offerNo && offer.tripNo === tripProof.tripNo
&& String(offer.warehouseId) === String(tripProof.warehouseId) && offer.status === 'OPEN'
&& offer.canCreateInquiry && offer.canConfirm && offer.capacityKnown && destination
&& version(offer.tripRecordVersion) && version(offer.recordVersion))
const pallets = Number(summary.estimatedPalletCount), packages = Number(summary.packageCount)
const weight = Number(summary.totalWeightKg), volume = Number(summary.totalVolumeCbm)
check('接车货量沿用原散板单且不超过余位', Number.isSafeInteger(pallets) && pallets > 0 && Number.isSafeInteger(packages) && packages > 0
&& Number.isFinite(weight) && weight > 0 && Number.isFinite(volume) && volume > 0
&& destination.remaining?.palletSpaces >= pallets && destination.remaining?.weightKg >= weight && destination.remaining?.volumeCbm >= volume)
const body = proof.createRequest = { tripRecordVersion: offer.tripRecordVersion, offerRecordVersion: offer.recordVersion,
customerId: demand.customerId, businessType: 'LOCAL_LOOSEBOARD', deliveryMode: 'SELF_DELIVERY',
sourceOrderNo: demand.orderNo, sourceOrderVersion: summary.version, expectedPalletCount: pallets,
estimatedArrivalAt: new Date(Date.now() + 900000).toISOString(), idempotencyKey: `${context.marker}-looseboard-bind`,
cargo: { destinationKey, cargoName: summary.productName, packageCount: packages, palletSpaces: pallets,
weightKg: weight, volumeCbm: volume, priceBasis: 'PALLET', unitPrice, currency: summary.currency,
contactName: summary.originContactName, contactPhone: summary.originContactPhone, remark: context.marker } }
const created = await request('POST', `${BASE}/${encodeURIComponent(offerNo)}/inquiries`, body)
validateInquiry(created?.inquiry, 'DRAFT')
proof.bookingNo = created.inquiry.bookingNo
const replay = await request('POST', `${BASE}/${encodeURIComponent(offerNo)}/inquiries`, body)
check('原单询价重放保持唯一承运与版本', replay.replayed === true
&& JSON.stringify(replay.inquiry) === JSON.stringify(created.inquiry) && JSON.stringify(replay.offer) === JSON.stringify(created.offer))
const path = `${BASE}/inquiries/${encodeURIComponent(proof.bookingNo)}`
const latest = await request('GET', path)
validateInquiry(latest, 'DRAFT')
check('确认承运使用完整即时版本', version(latest.tripRecordVersion) && version(latest.offerRecordVersion))
const confirmBody = proof.confirmRequest = { tripRecordVersion: latest.tripRecordVersion, offerRecordVersion: latest.offerRecordVersion,
recordVersion: latest.recordVersion, idempotencyKey: `${context.marker}-looseboard-confirm`, reason: context.marker }
const confirmed = await request('POST', `${path}/confirm`, confirmBody)
validateInquiry(confirmed?.inquiry, 'CONFIRMED')
check('原单约定货量和运费没有漂移', confirmed.inquiry.cargo?.packageCount === packages
&& confirmed.inquiry.cargo?.palletSpaces === pallets && confirmed.inquiry.cargo?.currency === summary.currency
&& Number(confirmed.inquiry.cargo?.unitPrice) === unitPrice
&& Math.round(Number(confirmed.inquiry.agreedAmount) * 100) === Math.round(unitPrice * pallets * 100))
check('承运复用原散板单且尚未伪造来货或排车', confirmed.inquiry.businessOrderNo === demand.orderNo
&& !confirmed.inquiry.incomingNo && !confirmed.inquiry.dispatchNo)
const confirmReplay = await request('POST', `${path}/confirm`, confirmBody)
check('承运确认重放未生成第二单或重复占位', confirmReplay.replayed === true
&& JSON.stringify(confirmReplay.inquiry) === JSON.stringify(confirmed.inquiry) && JSON.stringify(confirmReplay.offer) === JSON.stringify(confirmed.offer))
const result = await request('GET', originalPath)
const arrangement = result?.summary?.fleetArrangement
check('原散板单回写同一承运并进入待入仓', result?.summary?.orderNo === demand.orderNo
&& String(result.summary.customerId) === String(demand.customerId) && result.summary.status === 'WAITING_INBOUND'
&& arrangement?.businessOrderNo === demand.orderNo && arrangement.bookingNo === proof.bookingNo && arrangement.tripNo === tripProof.tripNo
&& !arrangement.incomingNo && !arrangement.dispatchNo)
Object.assign(demand, { finalStatus: 'WAITING_INBOUND', version: result.summary.version, retained: true })
proof.status = 'CONFIRMED_ORIGINAL_ORDER'
await context.checkpoint()
return proof
} catch (error) {
for (const log of proof.requests) if (log.status === 'SENDING') log.status = 'UNKNOWN_RESULT'
proof.status = 'FAILED'; proof.failure = error.message
await context.checkpoint()
throw error
}
}