const API = '/api/platform/v1/last-leg' const OPTIONS = { scenario: 'looseboard-pool-receipt', deploymentBlocker: true } const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aH1kAAAAASUVORK5CYII=', 'base64') const validVersion = value => Number.isSafeInteger(value) && value >= 0 const id = value => String(value ?? '') /** 原承运来货经PDA实收及运营确认;测量值由调用方明确给出,不借历史库存。 */ export async function receiveLooseboardPool(context, { demand, tripProof, pdaToken, measurements }) { const binding = demand?.poolBinding, cargo = binding?.createRequest?.cargo if (context.createdResources?.phase4OverseasBusiness?.looseboard !== demand || demand?.marker !== context.marker || !context.createdResources?.fleetTrips?.includes(tripProof) || binding?.tripNo !== tripProof?.tripNo || binding?.status !== 'CONFIRMED_ORIGINAL_ORDER' || demand.receipt || !pdaToken || typeof context.checkpoint !== 'function' || !Array.isArray(measurements) || !measurements.length || measurements.length !== binding.createRequest.expectedPalletCount || measurements.some(m => !Number.isSafeInteger(m.cartonCount) || m.cartonCount <= 0 || ['lengthCm', 'widthCm', 'heightCm', 'volumeCbm', 'grossWeightKg'].some(k => !Number.isFinite(m[k]) || m[k] <= 0)) || measurements.reduce((sum, m) => sum + m.cartonCount, 0) !== cargo?.packageCount) { throw Error('散板实收缺少本轮原单承运、PDA身份或逐托测量与箱数依据') } const proof = demand.receipt = { marker: context.marker, status: 'RECEIVING', bookingNo: binding.bookingNo, requests: [], pallets: [] } const check = (name, condition) => { context.assert('phase4-overseas-business', name, null, Boolean(condition), `orderNo=${demand.orderNo}`, OPTIONS) if (!condition) throw Error(name) } const io = async (method, path, body, token, multipart = false) => { const log = { method, path, status: 'PREPARED', ...(body && !multipart ? { body: structuredClone(body) } : {}) } proof.requests.push(log); await context.checkpoint() let response try { log.status = 'SENDING' response = multipart ? await context.requestMultipart(method, path, body, token) : await context.request(method, path, body, token) } catch (error) { log.status = 'UNKNOWN_RESULT'; throw error } const ok = response?.http === 200 && id(response?.body?.code) === '200' Object.assign(log, { status: ok ? 'SUCCEEDED' : !response?.http || response.http >= 500 ? 'UNKNOWN_RESULT' : 'REJECTED', http: response?.http, code: response?.body?.code, requestId: response?.requestId, traceId: response?.traceId }) await context.checkpoint() context.apiSuccess('phase4-overseas-business', '散板原承运实收', response, JSON.stringify({ http: log.http, requestId: log.requestId, traceId: log.traceId }), OPTIONS) if (!ok) throw Error('散板实收请求失败;未知结果不自动重发') return response.body.data } const key = action => `${context.marker}-looseboard-${action}` const pool = `${API}/fleet/capacity-offers/inquiries/${encodeURIComponent(binding.bookingNo)}` const trip = `${API}/fleet/trips/${encodeURIComponent(tripProof.tripNo)}` const bookingMatches = row => row?.bookingNo === binding.bookingNo && row.tripNo === tripProof.tripNo && id(row.customerId) === id(demand.customerId) try { const list = await io('GET', `${trip}/bookings`) const matches = list?.records?.filter(row => row.bookingNo === binding.bookingNo) || [] const booking = matches[0] check('来货前保持原车原客户唯一承运且未登记来货', matches.length === 1 && bookingMatches(booking) && booking.status === 'CONFIRMED' && !booking.incomingNo && !booking.dispatchNo && validVersion(list.tripRecordVersion) && validVersion(booking.recordVersion) && JSON.stringify(booking.cargo) === JSON.stringify(cargo)) const body = { tripRecordVersion: list.tripRecordVersion, recordVersion: booking.recordVersion, estimatedArrivalAt: new Date(Date.now() + 300000).toISOString(), deliveryInstructions: `${context.marker} TEST自送`, idempotencyKey: key('incoming') } const created = await io('POST', `${pool}/incoming`, body) check('登记来货保留原承运身份', bookingMatches(created?.booking) && created.booking.status === 'CONFIRMED' && typeof created.booking.incomingNo === 'string' && created.booking.incomingNo.length > 0 && !created.booking.receipt) proof.incomingNo = created.booking.incomingNo const replay = await io('POST', `${pool}/incoming`, body) check('来货重放没有产生第二单', replay?.replayed === true && JSON.stringify(replay.booking) === JSON.stringify(created.booking) && replay.tripRecordVersion === created.tripRecordVersion) const initial = (await io('GET', `${pool}/fulfillment`))?.preview?.incoming check('来货范围与初始实收状态正确', initial?.incomingNo === proof.incomingNo && id(initial.warehouseId) === id(tripProof.warehouseId) && initial.destinationKey === cargo.destinationKey && initial.status === 'WAIT_PDA_RECEIVING' && initial.pallets?.length === 0) const pda = `/api/pda/v1/last-leg/warehouse-transfers/${encodeURIComponent(proof.incomingNo)}` for (const [index, measurement] of measurements.entries()) { const draft = await io('POST', `${pda}/pallets/drafts`, { ...measurement, idempotencyKey: key(`measure-${index}`) }, pdaToken) const newPallets = draft?.pallets?.filter(row => !proof.pallets.some(p => id(p.id) === id(row.id))) || [] check('逐托测量只新增本次一个草稿', draft?.transferNo === proof.incomingNo && draft?.pallets?.length === index + 1 && newPallets.length === 1 && newPallets[0].status === 'DRAFT' && newPallets[0].cartonCount === measurement.cartonCount && /^[1-9]\d*$/.test(id(newPallets[0].id))) const palletId = newPallets[0].id, path = `${pda}/pallets/${encodeURIComponent(palletId)}` const generated = await io('POST', `${path}/generate`, { idempotencyKey: key(`generate-${index}`) }, pdaToken) const pallet = generated?.pallets?.find(row => id(row.id) === id(palletId)) check('本托生成真实唯一托盘资源', generated?.transferNo === proof.incomingNo && pallet?.palletNo && /^[1-9]\d*$/.test(id(pallet.warehousePalletId)) && !proof.pallets.some(row => row.palletNo === pallet.palletNo || id(row.warehousePalletId) === id(pallet.warehousePalletId))) await io('POST', `${path}/bind`, { palletCode: pallet.palletNo, idempotencyKey: key(`bind-${index}`) }, pdaToken) proof.pallets.push({ id: palletId, warehousePalletId: id(pallet.warehousePalletId), palletNo: pallet.palletNo, packages: measurement.cartonCount }) } const uploaded = await io('POST', `${pda}/measurement-photos/files`, { name: `${context.marker}-TEST-looseboard.png`, type: 'image/png', content: PNG }, pdaToken, true) check('实收测试图片已正式上传', /^[1-9]\d*$/.test(id(uploaded?.fileId))) const received = await io('POST', `${pda}/palletize`, { idempotencyKey: key('palletize'), photoFileIds: [id(uploaded.fileId)] }, pdaToken) check('本次PDA完成全部原实托', received?.transferNo === proof.incomingNo && received?.status === 'WAIT_CUSTOMER_CONFIRMATION' && received.pallets?.length === proof.pallets.length && proof.pallets.every(p => received.pallets.some(row => id(row.id) === id(p.id) && row.palletNo === p.palletNo && id(row.warehousePalletId) === p.warehousePalletId && row.cartonCount === p.packages && row.status === 'COMPLETED'))) const preview = (await io('GET', `${pool}/fulfillment`))?.preview, source = preview?.incoming check('运营确认依据为同仓同目的地同箱数的实际实收', source?.incomingNo === proof.incomingNo && id(source.warehouseId) === id(tripProof.warehouseId) && source.destinationKey === cargo.destinationKey && source.status === 'WAIT_CUSTOMER_CONFIRMATION' && source.receiptHash && source.receivedPackages === cargo.packageCount && source.pallets?.length === proof.pallets.length && proof.pallets.every(p => source.pallets.some(row => id(row.warehousePalletId) === p.warehousePalletId && row.packages === p.packages)) && validVersion(preview.tripRecordVersion) && validVersion(preview.recordVersion)) const receiptBody = { tripRecordVersion: preview.tripRecordVersion, recordVersion: preview.recordVersion, receiptHash: source.receiptHash, pallets: proof.pallets.map(p => ({ warehousePalletId: p.warehousePalletId, palletSpaces: 1 })), unitPrice: cargo.unitPrice, idempotencyKey: key('receipt') } const accepted = await io('POST', `${pool}/receipt-confirmation`, receiptBody) check('运营确认回写原承运并达到可排车', bookingMatches(accepted?.booking) && accepted.booking.status === 'RECEIVED' && accepted.booking.incomingNo === proof.incomingNo && accepted.booking.receipt?.source?.status === 'READY_FOR_DISPATCH') const again = await io('POST', `${pool}/receipt-confirmation`, receiptBody) check('实收确认重放保持原结果和版本', again?.replayed === true && JSON.stringify(again.booking) === JSON.stringify(accepted.booking) && again.tripRecordVersion === accepted.tripRecordVersion) proof.status = 'RECEIVED_READY_FOR_DISPATCH' proof.acceptedReceipt = accepted.booking.receipt await context.checkpoint() return proof } catch (error) { proof.status = 'FAILED'; proof.failure = error.message await context.checkpoint(); throw error } }