102 lines
8.2 KiB
JavaScript
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
|
|
}
|
|
}
|