import { randomUUID } from 'node:crypto' const OPTIONS = { scenario: 'fleet-external-actual-receipt', deploymentBlocker: true } const id = value => value == null ? '' : String(value) const key = () => `fleet-receipt-${randomUUID()}` const ok = value => value?.http === 200 && id(value?.body?.code) === '200' const conflict = value => value?.http === 409 || (value?.http === 200 && id(value?.body?.code) === 'A0409') const snapshot = rows => JSON.stringify(rows.map(row => JSON.stringify(row)).sort()) /** Dedicated external receipt case; retains RECEIVED goods for subsequent dispatch. Never runs SQL or bypasses PDA. */ export async function verifyFleetExternalReceipt(context, module, tripProof, pdaToken, { reconcilePalletized = false, reconcileConfirmed = false } = {}) { const reconciling = reconcilePalletized || reconcileConfirmed if (!context.createdResources?.fleetTrips?.includes(tripProof) || (tripProof.externalReceipt && !reconciling) || !pdaToken) throw new Error('外接实收验证须使用本轮车次及已绑定起运仓的PDA账号,失败后保留原证据') if (reconciling && (tripProof.externalReceipt?.status !== 'CHECKING' || !tripProof.externalReceipt.palletizeRequest?.idempotencyKey || !tripProof.externalReceipt.warehousePalletId || (reconcileConfirmed ? !tripProof.externalReceipt.receiptRequest?.idempotencyKey : tripProof.externalReceipt.receiptRequest))) throw new Error('仅核对已入库的原来货与原完成命令,确认阶段须与保存的请求一致') const proof = reconciling ? tripProof.externalReceipt : tripProof.externalReceipt = { status: 'CHECKING', marker: context.marker, requests: [] } const base = `/api/platform/v1/last-leg/fleet/trips/${encodeURIComponent(tripProof.tripNo)}` async function check(name, passed, response) { context.assert(module, name, response, Boolean(passed), '', OPTIONS) if (!passed) throw new Error(`${module}: ${name}`) } async function record(method, path, response, label, expectedMessage) { proof.requests.push({ method, path, label, http: response?.http, code: response?.body?.code, requestId: response?.requestId, traceId: response?.traceId }) await check(label, response?.requestId && response?.traceId && (expectedMessage ? conflict(response) && expectedMessage.test(id(response?.body?.message)) : ok(response)), response) return response.body?.data } const request = async (method, path, body, label, token, expectedMessage) => record(method, path, await context.request(method, path, body, token), label, expectedMessage) let plan = await request('GET', `${base}/capacity-plan`, undefined, '读取本轮真实自有货及可用容量') await check('外接实收前已有有效自有货计划', plan?.planValid === true && !plan.blockReason && plan.cargo?.length > 0) const customerId = id(plan.cargo[0].customerId), destinationKey = plan.cargo[0].destinationKey const destinationIndex = tripProof.stops.findIndex(stop => stop.destinationKey === destinationKey) if (reconciling) { const baseline = proof.initialSegments || tripProof.reservation?.finalSegments const original = reconcileConfirmed ? { ...proof.createRequest?.cargo, palletSpaces: 1, weightKg: 100, volumeCbm: 1 } : proof.createRequest?.cargo await check('原实收来源及原预留基线可追溯', baseline?.length > 0 && proof.createRequest.customerId === customerId && original?.destinationKey === destinationKey) const reserved = baseline.map(segment => segment.fromIndex >= destinationIndex ? segment : { ...segment, occupied: { palletSpaces: Number(segment.occupied.palletSpaces) + Number(original.palletSpaces), weightKg: Number(segment.occupied.weightKg) + Number(original.weightKg), volumeCbm: Number(segment.occupied.volumeCbm) + Number(original.volumeCbm) }, remaining: { palletSpaces: Number(segment.remaining.palletSpaces) - Number(original.palletSpaces), weightKg: Number(segment.remaining.weightKg) - Number(original.weightKg), volumeCbm: Number(segment.remaining.volumeCbm) - Number(original.volumeCbm) } }) await check('已收货未确认期间仍保留原承运预留', snapshot(plan.availableSegments) === snapshot(reserved)) plan = { ...plan, availableSegments: baseline } } else proof.initialSegments = plan.availableSegments await check('使用本轮客户和已约定FBA卸货站且容量足够', /^[1-9]\d*$/.test(customerId) && destinationKey.startsWith('FBA:') && destinationIndex > 0 && plan.availableSegments?.filter(row => row.fromIndex < destinationIndex).length === destinationIndex && plan.availableSegments.filter(row => row.fromIndex < destinationIndex).every(row => Number(row.remaining.palletSpaces) >= 2 && Number(row.remaining.weightKg) >= 100 && Number(row.remaining.volumeCbm) >= 1)) let bookingPath if (!reconciling) { const draft = { tripRecordVersion: plan.recordVersion, customerId, idempotencyKey: key(), cargo: { destinationKey, cargoName: `${context.marker} 外接实收测试货`, packageCount: 1, palletSpaces: 2, weightKg: 50, volumeCbm: 0.5, priceBasis: 'PALLET', unitPrice: 25, currency: 'CNY', remark: '受控测试:计划2托位,PDA实测1托位100kg/1m³,运营重新确认30元/托' } } proof.createRequest = draft let result = await request('POST', `${base}/bookings`, draft, '登记本轮专用外接实收预订') await check('来货由本轮询价产生', result?.booking?.status === 'DRAFT' && result.booking.customerId === customerId && result.booking.bookingNo) proof.bookingNo = result.booking.bookingNo bookingPath = `${base}/bookings/${encodeURIComponent(proof.bookingNo)}` proof.confirmRequest = { tripRecordVersion: result.tripRecordVersion, recordVersion: result.booking.recordVersion, idempotencyKey: key() } result = await request('POST', `${bookingPath}/confirm`, proof.confirmRequest, '确认来货前承运预留') await check('预留报价为2托位乘25元', result?.booking?.status === 'CONFIRMED' && Number(result.booking.agreedAmount) === 50) proof.incomingRequest = { tripRecordVersion: result.tripRecordVersion, recordVersion: result.booking.recordVersion, estimatedArrivalAt: new Date(Date.now() + 1800000).toISOString(), deliveryInstructions: `${context.marker} 客户自送起运仓`, idempotencyKey: key() } result = await request('POST', `${bookingPath}/incoming`, proof.incomingRequest, '创建真实外接来货并进入PDA待收货') proof.incomingNo = result?.booking?.incomingNo await check('创建来货不提前标记实收', proof.incomingNo && result.booking.status === 'CONFIRMED' && !result.booking.receipt) const incomingReplay = await request('POST', `${bookingPath}/incoming`, proof.incomingRequest, '原请求重放来货登记') await check('来货重放复用同一单号和版本', incomingReplay?.replayed === true && incomingReplay.booking?.incomingNo === proof.incomingNo && id(incomingReplay.tripRecordVersion) === id(result.tripRecordVersion) && id(incomingReplay.booking.recordVersion) === id(result.booking.recordVersion)) } bookingPath ||= `${base}/bookings/${encodeURIComponent(proof.bookingNo)}` const incoming = await request('GET', `${bookingPath}/incoming`, undefined, '读取运营侧来货及实收证据') await check(reconciling ? '原来货实收阶段与检查点一致' : '实际来货为待PDA收货且没有完成托盘', incoming?.incoming?.status === (reconcileConfirmed ? 'READY_FOR_DISPATCH' : reconcilePalletized ? 'WAIT_CUSTOMER_CONFIRMATION' : 'WAIT_PDA_RECEIVING') && incoming.incoming.warehouseId === id(tripProof.warehouseId) && incoming.incoming.destinationKey === destinationKey && incoming.incoming.inboundCode && incoming.incoming.pallets?.length === (reconciling ? 1 : 0)) proof.inboundCode = incoming.incoming.inboundCode const filter = new URLSearchParams({ customerId, overseasWarehouseId: id(tripProof.warehouseId), inboundCode: proof.inboundCode, page: '1', size: '200' }) async function inventory(label) { const balances = await request('GET', `/api/platform/v1/last-leg/inventory/balances?${filter}`, undefined, `${label}读取本票库存`) const ledger = await request('GET', `/api/platform/v1/last-leg/inventory/ledger?${filter}&businessOrderNo=${encodeURIComponent(proof.incomingNo)}`, undefined, `${label}读取本票库存流水`) for (const [kind, page] of [['库存', balances], ['流水', ledger]]) await check(`${label}${kind}精确范围且无漏页`, Array.isArray(page?.records) && Number(page.total) === page.records.length && page.records.every(row => id(row.customerId) === customerId && id(row.overseasWarehouseId) === id(tripProof.warehouseId) && (kind === '库存' || row.inboundCode === proof.inboundCode))) const exactBalances = [], exactPositions = [] for (const group of balances.records) { await check(`${label}分组有可回查的余额引用`, /^[1-9]\d*$/.test(id(group.id))) const balancePath = `/api/platform/v1/last-leg/inventory/balances/${encodeURIComponent(id(group.id))}` const balance = await request('GET', `${balancePath}?customerId=${encodeURIComponent(customerId)}`, undefined, `${label}回查实际余额来源`) const positions = await request('GET', `${balancePath}/positions?customerId=${encodeURIComponent(customerId)}`, undefined, `${label}回查实际余额的实物位置`) await check(`${label}余额与位置确属本票实际实托`, id(balance?.id) === id(group.id) && id(balance.customerId) === customerId && id(balance.overseasWarehouseId) === id(tripProof.warehouseId) && balance.inboundCode === proof.inboundCode && Array.isArray(positions) && positions.every(row => id(row.balanceId) === id(balance.id) && id(row.customerId) === customerId && id(row.overseasWarehouseId) === id(tripProof.warehouseId) && row.inboundCode === proof.inboundCode && id(row.palletId) === proof.warehousePalletId && row.palletNo === proof.palletNo) && ['availablePackages', 'holdPackages', 'allocatedPackages', 'pickedPackages', 'receivingPackages', 'inTransitPackages', 'procurementPackages'] .every(field => Number(balance[field]) === Number(group[field]))) exactBalances.push(balance); exactPositions.push(...positions) } return { balances: exactBalances, positions: exactPositions, ledger: ledger.records } } const before = reconciling ? proof.beforeInventory : await inventory('PDA完成前') await check('登记来货未伪造库存或入库流水', before.balances.every(row => Number(row.availablePackages) === 0) && before.ledger.length === 0) proof.beforeInventory = before const pdaBase = `/api/pda/v1/last-leg/warehouse-transfers/${encodeURIComponent(proof.incomingNo)}` const task = await request('GET', pdaBase, undefined, 'PDA按绑定仓读取本轮来货', pdaToken) await check('PDA真实任务与来货、客户来源及仓库一致', task?.transferNo === proof.incomingNo && task.inboundCode === proof.inboundCode && id(task.operatingWarehouseId) === id(tripProof.warehouseId) && task.status === (reconcileConfirmed ? 'READY_FOR_DISPATCH' : reconcilePalletized ? 'WAIT_CUSTOMER_CONFIRMATION' : 'WAIT_PDA_RECEIVING') && task.pallets?.length === (reconciling ? 1 : 0)) let completed if (reconciling) completed = task else { const early = { tripRecordVersion: incoming.tripRecordVersion, recordVersion: incoming.recordVersion, receiptHash: incoming.incoming.receiptHash, pallets: [{ warehousePalletId: id(plan.cargo[0].warehousePalletId), palletSpaces: 1 }], unitPrice: 30, idempotencyKey: key() } await request('POST', `${bookingPath}/receipt-confirmation`, early, '未完成PDA测量禁止运营提前确认实收', undefined, /仓库尚未完成实收及测量/) proof.palletDraftRequest = { idempotencyKey: key(), cartonCount: 1, lengthCm: 100, widthCm: 100, heightCm: 100, volumeCbm: 1, grossWeightKg: 100 } const measured = await request('POST', `${pdaBase}/pallets/drafts`, proof.palletDraftRequest, 'PDA登记本轮受控实物测量', pdaToken) await check('测量仅产生待生成实托草稿', measured?.pallets?.length === 1 && measured.pallets[0].status === 'DRAFT' && measured.pallets[0].id) const draftReplay = await request('POST', `${pdaBase}/pallets/drafts`, proof.palletDraftRequest, '测量草稿原键重放', pdaToken) await check('测量重放不增加托盘', snapshot(draftReplay.pallets) === snapshot(measured.pallets)) const palletPath = `${pdaBase}/pallets/${encodeURIComponent(id(measured.pallets[0].id))}` const generated = await request('POST', `${palletPath}/generate`, { idempotencyKey: key() }, '生成仓库权威实物托盘', pdaToken) const pallet = generated?.pallets?.[0] await check('生成真实托盘且未入库', generated.pallets.length === 1 && pallet?.status === 'GENERATED' && pallet.warehousePalletId && pallet.palletNo) proof.warehousePalletId = id(pallet.warehousePalletId); proof.palletNo = pallet.palletNo const bound = await request('POST', `${palletPath}/bind`, { palletCode: pallet.palletNo, idempotencyKey: key() }, '按真实托盘码完成PDA绑定', pdaToken) await check('PDA绑定保留同一实托和实测数量', bound?.pallets?.length === 1 && bound.pallets[0].status === 'BOUND' && id(bound.pallets[0].warehousePalletId) === proof.warehousePalletId && Number(bound.pallets[0].cartonCount) === 1) const photoPath = `${pdaBase}/measurement-photos/files` const uploaded = await record('POST', photoPath, await context.requestMultipart('POST', photoPath, { name: `${context.marker}-TEST-MEASUREMENT.png`, type: 'image/png', content: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aH1kAAAAASUVORK5CYII=', 'base64'), }, pdaToken), '上传明确标记为测试的测量凭证') await check('测量凭证有真实文件引用', uploaded?.fileId && Number(uploaded.sizeBytes) > 0) proof.photoFileId = id(uploaded.fileId) proof.palletizeRequest = { idempotencyKey: key(), photoFileIds: [proof.photoFileId] } completed = await request('POST', `${pdaBase}/palletize`, proof.palletizeRequest, 'PDA正式完成外接收货及库存入账', pdaToken) } await check('PDA已完成且保留原实托', completed?.status === (reconcileConfirmed ? 'READY_FOR_DISPATCH' : 'WAIT_CUSTOMER_CONFIRMATION') && completed.pallets?.length === 1 && completed.pallets[0].status === 'COMPLETED' && id(completed.pallets[0].warehousePalletId) === proof.warehousePalletId) const receivedStock = await inventory('首次完成收货后') await check('本票真实可用库存及不可变入库流水只增加1箱', receivedStock.balances.reduce((n, row) => n + Number(row.availablePackages), 0) === 1 && receivedStock.positions.length === 1 && Number(receivedStock.positions[0].availablePackages) === 1 && receivedStock.ledger.length > 0 && receivedStock.ledger.every(row => row.businessOrderNo === proof.incomingNo && row.palletNo === proof.palletNo && Number(row.beforePackages) === 0 && Number(row.deltaPackages) === 1 && Number(row.afterPackages) === 1) && receivedStock.ledger.reduce((n, row) => n + Number(row.deltaPackages), 0) === 1) proof.receivedInventory = receivedStock if (!reconcileConfirmed) { const completedReplay = await request('POST', `${pdaBase}/palletize`, proof.palletizeRequest, 'PDA完成原请求重放', pdaToken) const replayedStock = await inventory('PDA完成重放后') await check('PDA重放不重复写入库存与流水', snapshot(replayedStock.balances) === snapshot(receivedStock.balances) && snapshot(replayedStock.positions) === snapshot(receivedStock.positions) && snapshot(replayedStock.ledger) === snapshot(receivedStock.ledger) && snapshot(completedReplay.pallets) === snapshot(completed.pallets)) } const measuredIncoming = await request('GET', `${bookingPath}/incoming`, undefined, '运营读取PDA完成后的实测摘要') await check('运营读取真实实收数量而非原预估', measuredIncoming?.incoming?.status === (reconcileConfirmed ? 'READY_FOR_DISPATCH' : 'WAIT_CUSTOMER_CONFIRMATION') && measuredIncoming.incoming.pallets?.length === 1 && id(measuredIncoming.incoming.pallets[0].warehousePalletId) === proof.warehousePalletId && Number(measuredIncoming.incoming.receivedPackages) === 1) if (!reconcileConfirmed) proof.receiptRequest = { tripRecordVersion: measuredIncoming.tripRecordVersion, recordVersion: measuredIncoming.recordVersion, receiptHash: measuredIncoming.incoming.receiptHash, pallets: [{ warehousePalletId: proof.warehousePalletId, palletSpaces: 1 }], unitPrice: 30, idempotencyKey: key() } const accepted = await request('POST', `${bookingPath}/receipt-confirmation`, proof.receiptRequest, '运营按实际1托位和新单价30确认实收') proof.acceptedReceipt = accepted await check('实收确认冻结新货量和价格且形成可排车来源', accepted?.booking?.status === 'RECEIVED' && accepted.booking.receipt?.source?.status === 'READY_FOR_DISPATCH' && Number(accepted.booking.agreedAmount) === 30 && Number(accepted.booking.cargo?.palletSpaces) === 1 && Number(accepted.booking.cargo?.weightKg) === 100 && Number(accepted.booking.cargo?.volumeCbm) === 1 && Number(accepted.booking.cargo?.unitPrice) === 30) const persisted = await request('GET', `${base}/bookings`, undefined, '冻结已持久化实收记录以核对重放') const persistedBooking = persisted?.records?.find(row => row.bookingNo === proof.bookingNo) const { updatedAt: responseTime, ...acceptedFacts } = accepted.booking const { updatedAt: storedTime, ...persistedFacts } = persistedBooking || {} await check('持久化保留全部实收业务字段与版本', snapshot([acceptedFacts]) === snapshot([persistedFacts]) && id(persisted.tripRecordVersion) === id(accepted.tripRecordVersion)) const acceptedReplay = await request('POST', `${bookingPath}/receipt-confirmation`, proof.receiptRequest, '运营实收确认原键重放') await check('实收重放不改价格或版本', acceptedReplay?.replayed === true && snapshot([acceptedReplay.booking]) === snapshot([persistedBooking]) && id(acceptedReplay.tripRecordVersion) === id(accepted.tripRecordVersion)) const finalStock = await inventory('运营确认及重放后') await check('运营确认不会再次入库', snapshot(finalStock.balances) === snapshot(receivedStock.balances) && snapshot(finalStock.positions) === snapshot(receivedStock.positions) && snapshot(finalStock.ledger) === snapshot(receivedStock.ledger)) const finalPlan = await request('GET', `${base}/capacity-plan`, undefined, '实收确认后回查分段容量') const expected = plan.availableSegments.map(segment => segment.fromIndex >= destinationIndex ? segment : { ...segment, occupied: { palletSpaces: Number(segment.occupied.palletSpaces) + 1, weightKg: Number(segment.occupied.weightKg) + 100, volumeCbm: Number(segment.occupied.volumeCbm) + 1 }, remaining: { palletSpaces: Number(segment.remaining.palletSpaces) - 1, weightKg: Number(segment.remaining.weightKg) - 100, volumeCbm: Number(segment.remaining.volumeCbm) - 1 } }) await check('实收替换原预留而非重复占用且仅影响约定站段', finalPlan?.planValid === true && snapshot(finalPlan.availableSegments) === snapshot(expected)) proof.finalBooking = accepted.booking; proof.finalTripVersion = accepted.tripRecordVersion proof.finalInventory = finalStock; proof.finalSegments = finalPlan.availableSegments proof.status = 'VERIFIED' return proof }