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 => /^(0|[1-9]\d*)$/.test(String(value)) && Number.isSafeInteger(Number(value)) 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 || !Array.isArray(m.boxes) || m.boxes.length !== m.cartonCount || m.boxes.some(box => ['length', 'width', 'height', 'weight'].some(field => !Number.isFinite(box[field]) || box[field] <= 0) || !Number.isSafeInteger(box.quantity) || box.quantity <= 0) || Math.abs(m.boxes.reduce((sum, box) => sum + box.length * box.width * box.height / 1000000, 0) - m.volumeCbm) > 0.000001 || m.boxes.reduce((sum, box) => sum + box.weight, 0) !== m.grossWeightKg || ['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)}` const forecast = await io('GET', pda, undefined, pdaToken) check('逐箱实收只使用原来货预报箱号和唯一预报SKU', forecast?.transferNo === proof.incomingNo && id(forecast.operatingWarehouseId) === id(tripProof.warehouseId) && forecast.packageCount === cargo.packageCount && forecast.items?.length === 1 && forecast.items[0].skuCode && forecast.cartons?.length === cargo.packageCount && new Set(forecast.cartons.map(box => box.cartonMark)).size === cargo.packageCount && forecast.cartons.every(box => box.cartonMark && box.status === 'EXPECTED' && id(box.transferItemId) === id(forecast.items[0].id))) for (const [index, measurement] of measurements.entries()) { const draft = await io('POST', `${pda}/pallets/drafts`, { 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 === 0 && /^[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 }) } let offset = 0 const boxes = measurements.flatMap((measurement, index) => measurement.boxes.map(input => { const carton = forecast.cartons[offset++] return { cartonMark: carton.cartonMark, transferItemId: id(carton.transferItemId), transferPalletId: id(proof.pallets[index].id), length: input.length, width: input.width, height: input.height, weight: input.weight, items: [{ skuCode: forecast.items[0].skuCode, quantity: input.quantity }] } })) proof.controlledBoxInputs = { syntheticTestMaterials: true, boxes } const counted = await io('POST', `${pda}/cartons/count`, { idempotencyKey: key('count-boxes'), forecastPackageCount: forecast.packageCount, boxes }, pdaToken) check('逐箱尺寸重量SKU实点与实际托盘归属已保存', counted?.transferNo === proof.incomingNo && counted.cartons?.length === boxes.length && boxes.every(input => counted.cartons.some(box => box.cartonMark === input.cartonMark && box.status === 'COUNTED' && id(box.transferPalletId) === input.transferPalletId && ['length', 'width', 'height', 'weight'].every(field => Number(box[field]) === input[field]) && box.items?.length === 1 && box.items[0].skuCode === input.items[0].skuCode && Number(box.items[0].quantity) === input.items[0].quantity)) && proof.pallets.every((p, index) => counted.pallets?.some(row => id(row.id) === id(p.id) && row.cartonCount === p.packages && Number(row.volumeCbm) === measurements[index].volumeCbm && Number(row.grossWeightKg) === measurements[index].grossWeightKg))) 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 } }