import { prepareLooseboardPool } from './looseboard-pool-preparation.mjs' import { receiveLooseboardPool } from './looseboard-pool-receipt.mjs' import { arrangeLooseboardPool } from './looseboard-pool-dispatch.mjs' import { dispatchIo, version } from './grouped-dispatch-acceptance.mjs' /** 默认主链复用原散板原车;测量仅为明确TEST样本,不代表生产实测。 */ export async function attachLooseboardToTrip(context, tripProof, pdaToken, helpers = { prepare: prepareLooseboardPool, receive: receiveLooseboardPool, arrange: arrangeLooseboardPool }) { const demand = context.createdResources?.phase4OverseasBusiness?.looseboard if (!demand || demand.marker !== context.marker || demand.finalStatus !== 'PENDING_ACCEPTANCE' || !context.createdResources?.fleetTrips?.includes(tripProof) || !pdaToken || demand.fulfillment) throw Error('默认散板履约缺少本轮待接车原单或已执行') const proof = demand.fulfillment = { marker: context.marker, status: 'ATTACHING', requests: [] } const io = dispatchIo(context, 'phase5-overseas-fulfillment', proof, 'LOOSEBOARD-MAIN') const check = (name, ok) => io.check(name, null, Boolean(ok)) try { const original = await io.command('GET', `/api/platform/v1/last-leg/local-looseboard-orders/${encodeURIComponent(demand.orderNo)}?customerId=${encodeURIComponent(demand.customerId)}`, undefined, '读取原散板货量') const row = original?.summary check('原散板身份和货量可供受控实收', row?.orderNo === demand.orderNo && String(row.customerId) === String(demand.customerId) && row.status === 'PENDING_ACCEPTANCE' && Number.isSafeInteger(row.estimatedPalletCount) && row.estimatedPalletCount > 0 && Number.isSafeInteger(row.packageCount) && row.packageCount >= row.estimatedPalletCount) const destinationKey = tripProof.stops?.[1]?.destinationKey check('原车存在明确卸货目的地', typeof destinationKey === 'string' && destinationKey.length > 0) // 明确的两托三箱TEST样本,逐箱输入,不把整托数值或箱数推算成实际箱规。 check('受控箱规仅适用于本轮两托三箱样本', row.estimatedPalletCount === 2 && row.packageCount === 3) const measurements = [ { cartonCount: 2, lengthCm: 100, widthCm: 100, heightCm: 90, volumeCbm: 0.9, grossWeightKg: 40, boxes: [{ length: 100, width: 100, height: 45, weight: 20, quantity: 1 }, { length: 100, width: 100, height: 45, weight: 20, quantity: 1 }] }, { cartonCount: 1, lengthCm: 100, widthCm: 100, heightCm: 90, volumeCbm: 0.9, grossWeightKg: 40, boxes: [{ length: 100, width: 100, height: 90, weight: 40, quantity: 1 }] }, ] proof.measurements = { kind: 'CONTROLLED_TEST_MEASUREMENT', measurements } await helpers.prepare(context, { demand, tripProof, destinationKey, unitPrice: 25 }) await helpers.receive(context, { demand, tripProof, pdaToken, measurements }) const ledger = await io.command('GET', `/api/platform/v1/last-leg/inventory/ledger?${new URLSearchParams({ customerId: String(demand.customerId), overseasWarehouseId: String(tripProof.warehouseId), businessOrderNo: demand.receipt.incomingNo, businessType: 'STOCK_IN', page: '1', size: '200', })}`, undefined, '独立核验原散板来货入库流水') check('散板实收仅产生足量且唯一的本票入库流水', Array.isArray(ledger?.records) && ledger.records.length > 0 && Number(ledger.total) === ledger.records.length && new Set(ledger.records.map(r => String(r.id))).size === ledger.records.length && ledger.records.every(r => /^[1-9]\d*$/.test(String(r.id)) && r.businessOrderNo === demand.receipt.incomingNo && r.businessType === 'STOCK_IN' && String(r.customerId) === String(demand.customerId) && String(r.overseasWarehouseId) === String(tripProof.warehouseId) && Number.isSafeInteger(Number(r.deltaPackages)) && Number(r.deltaPackages) > 0) && ledger.records.reduce((sum, r) => sum + Number(r.deltaPackages), 0) === row.packageCount) proof.stockInLedger = ledger.records const arranged = await helpers.arrange(context, { demand, tripProof }) check('原散板全部实收货物已经加入本车', arranged?.status === 'BOUND_AWAITING_REVIEW' && arranged.dispatchNo === tripProof.dispatchNo && arranged.addedItems?.length === measurements.length && new Set(arranged.addedItems.map(r => r.sourceKey)).size === measurements.length) const path = `/api/platform/v1/last-leg/dispatches/${encodeURIComponent(tripProof.dispatchNo)}` let current = await io.command('GET', path, undefined, '散板加入后重新审核原车') check('加货后仅在原车装车前重新审核', current?.dispatchNo === tripProof.dispatchNo && current.stage === 'BEFORE_LOADING' && Number(current.counts?.pickedPackageCount) === 0) for (const action of ['submit-review', 'dispatch-review', 'loading-review']) { current = await io.command('POST', `${path}/${action}`, { recordVersion: version(current.recordVersion), idempotencyKey: io.key(action), ...(action === 'submit-review' ? { remark: context.marker } : { decision: 'APPROVED', reason: context.marker }) }, `散板加货后${action}`) } const reviewed = await io.command('GET', path, undefined, '核对加货后两级审核及原车资源') check('加货审核后原车资源不变且进入装车', reviewed?.stage === 'LOADING' && reviewed.reviewState === 'APPROVED' && reviewed.reviews?.dispatchReview?.status === 'APPROVED' && reviewed.reviews?.loadingReview?.status === 'APPROVED' && String(reviewed.vehicleInfo?.vehicle?.vehicleId) === String(tripProof.vehicleId) && String(reviewed.vehicleInfo?.driver?.driverId) === String(tripProof.driverId)) proof.status = 'READY_FOR_SHARED_LOADING' proof.sourceKeys = arranged.addedItems.map(row => row.sourceKey) proof.incomingNo = demand.receipt.incomingNo await context.checkpoint() return { proof, detail: reviewed } } catch (error) { proof.status = 'FAILED'; proof.failure = error.message; await context.checkpoint(); throw error } } /** 同车正常签收后回查原散板商业单,不能用整车状态代替原单履约。 */ export async function verifyLooseboardDelivered(context, tripProof, deliveries) { const demand = context.createdResources?.phase4OverseasBusiness?.looseboard, proof = demand?.fulfillment if (demand?.marker !== context.marker || proof?.status !== 'READY_FOR_SHARED_LOADING') throw Error('缺少本轮原散板装车前证据') const io = dispatchIo(context, 'phase5-overseas-fulfillment', proof, 'LOOSEBOARD-COMPLETE') const detail = await io.command('GET', `/api/platform/v1/last-leg/local-looseboard-orders/${encodeURIComponent(demand.orderNo)}?customerId=${encodeURIComponent(demand.customerId)}`, undefined, '签收后回查原散板终态') const row = detail?.summary, arrangement = row?.fleetArrangement const passed = row?.orderNo === demand.orderNo && String(row.customerId) === String(demand.customerId) && row.status === 'COMPLETED' && arrangement?.fulfillmentStage === 'COMPLETED' && arrangement.bookingNo === demand.poolBinding.bookingNo && arrangement.tripNo === tripProof.tripNo && arrangement.dispatchNo === tripProof.dispatchNo && arrangement.incomingNo === demand.receipt.incomingNo && Number(arrangement.signedPackages) === demand.poolBinding.createRequest.cargo.packageCount && deliveries.some(d => d.deliveryNo === arrangement.deliveryNo && String(d.customerId) === String(demand.customerId)) io.check('原散板通过真实同车签收完成且数量守恒', null, Boolean(passed)) Object.assign(proof, { status: 'COMPLETED_HTTP_VERIFIED', deliveryNo: arrangement.deliveryNo }) demand.finalStatus = 'COMPLETED' await context.checkpoint() }