import { randomUUID } from 'node:crypto' import { setTimeout as delay } from 'node:timers/promises' const MODULE = 'fleet-company-cost-acceptance' const OPTIONS = { scenario: 'company-vehicle-cost-payment', deploymentBlocker: true } const FINANCE = '/api/platform/finance/v1' const MASTER = '/api/platform/v1/last-leg/master-data' const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aH1kAAAAASUVORK5CYII=', 'base64') const id = value => value == null ? '' : String(value) const ok = response => response?.http === 200 && id(response?.body?.code) === '200' function scaled(value, scale) { if (!new RegExp(`^\\d+(\\.\\d{1,${scale}})?$`).test(id(value))) throw new Error('验收金额或汇率格式无效') const [whole, fraction = ''] = id(value).split('.') return BigInt(whole) * 10n ** BigInt(scale) + BigInt(fraction.padEnd(scale, '0')) } const cents = value => scaled(value, 2) const amount = value => Number(`${value / 100n}.${id(value % 100n).padStart(2, '0')}`) function canonical(value) { if (Array.isArray(value)) return value.map(canonical).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))) if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])])) return value } const same = (a, b) => JSON.stringify(canonical(a)) === JSON.stringify(canonical(b)) /** Internal authorized TEST module; entrypoint owns registration, credentials and execution. No direct fetch, SQL or historical fixtures. */ export async function run(context, { pollAttempts = 60, pollDelayMs = 1000 } = {}) { if (!context.marker || !context.createdResources) throw new Error('公司车辆成本验收须有独立marker和本轮资源上下文') const resources = context.options?.fixedCompanyCostResources if (!resources || !['supplierId', 'vehicleId'].every(key => typeof resources[key] === 'string' && /^[1-9]\d*$/.test(resources[key]))) throw new Error('公司成本验收必须配置既有供应商和车辆,禁止自动创建主档') if (context.createdResources.fleetCompanyCostAcceptance) throw new Error('本轮已存在公司成本验收记录;保留原证据,不自动重新造数') if (!Number.isInteger(pollAttempts) || pollAttempts < 1 || pollAttempts > 120 || !Number.isFinite(pollDelayMs) || pollDelayMs < 0 || pollDelayMs > 5000) throw new Error('公司成本轮询范围无效') const proof = { marker: context.marker, status: 'CHECKING', cases: [], requests: [], cleanup: 'NOT_CREATED', isolationScope: 'Only marker-owned company fees must be absent from the customer fee pool. Customer-specific cost/account records and unrelated concurrent changes are not inspected or compared.' } context.createdResources.fleetCompanyCostAcceptance = proof function check(label, condition, response = null) { context.assert(MODULE, label, response, Boolean(condition), '', OPTIONS) if (!condition) throw new Error(`${MODULE}: ${label}`) } function evidence(label, method, path, response) { proof.requests.push({ label, method, path, http: response?.http, code: response?.body?.code, requestId: response?.requestId, traceId: response?.traceId }) } async function request(method, path, body, label, { key, reject } = {}) { const planned = { label, method, path, requestBody: body, idempotencyKey: key, state: 'PLANNED' } proof.requests.push(planned) await context.checkpoint?.() const response = await context.request(method, path, body, context.token, key ? { 'Idempotency-Key': key } : {}) Object.assign(planned, { state: 'RETURNED', http: response?.http, code: response?.body?.code, requestId: response?.requestId, traceId: response?.traceId }) await context.checkpoint?.() if (reject) { check(label, response?.http === 200 && reject.includes(id(response?.body?.code)), response) return response.body?.data } const data = context.apiSuccess(MODULE, label, response, '', OPTIONS) check(label, ok(response) && data != null, response) return data } async function snapshotCustomer(label) { const pool = await request('GET', `${FINANCE}/fee-pool?direction=ALL`, undefined, `${label}只读核对公司来源未进入客户费用池`) check('客户费用池隔离查询形状完整', Array.isArray(pool)) // Do not retain or operate other tasks' customer records. Only count this run's unique marker. return { companyFeePoolRows: pool.filter(row => JSON.stringify(row).includes(context.marker)).length } } async function page(path, label) { const result = await request('GET', `${path}?page=1&size=100`, undefined, label) check(`${label}分页完整`, Array.isArray(result.records) && Number(result.page) === 1 && Number(result.size) === 100 && Number(result.total) === result.records.length && Number(result.total) <= 100) return result.records } let failure try { const businessDate = new Date().toISOString().slice(0, 10), month = businessDate.slice(0, 7) const exchange = await request('GET', `${MASTER}/exchange-rates/${month}`, undefined, '读取本月有效USD汇率,不修改共享汇率') check('USD汇率月份有效', exchange.month === month && exchange.status === 'ACTIVE' && scaled(exchange.usdToCnyAverage, 6) > 0n) proof.exchangeRate = { month, usdToCnyAverage: id(exchange.usdToCnyAverage), recordVersion: id(exchange.recordVersion) } proof.customerBefore = await snapshotCustomer('操作前') check('本轮marker尚无客户费用污染,禁止覆盖旧验收', proof.customerBefore.companyFeePoolRows === 0) proof.supplierId = id(resources.supplierId) const supplier = await request('GET', `/api/platform/v1/suppliers/${encodeURIComponent(proof.supplierId)}?tenantId=0`, undefined, '核对已配置的既有供应商') check('复用供应商为平台启用海外拖车供应商', id(supplier.id) === proof.supplierId && id(supplier.tenantId) === '0' && supplier.status === 'ACTIVE' && supplier.category === 'TRUCKING' && supplier.truckingScope === 'OVERSEAS') const vehicle = await request('GET', `${MASTER}/vehicles/${encodeURIComponent(id(resources.vehicleId))}`, undefined, '核对已配置的既有公司车辆') check('复用车辆为海外自有在役车辆', id(vehicle.id) === id(resources.vehicleId) && Boolean(vehicle.plateNumber) && vehicle.region === 'OVERSEAS' && vehicle.ownership === 'OWNED' && vehicle.status === 'IN_SERVICE') proof.vehicleId = id(vehicle.id); proof.plateNumber = vehicle.plateNumber proof.reusedMasterData = true; proof.cleanup = 'REUSED_UNCHANGED' const care = `/api/platform/v1/last-leg/fleet/vehicles/${encodeURIComponent(proof.vehicleId)}/records` for (const [recordType, currency, originalAmount] of [['REPAIR', 'CNY', 27.31], ['RESCUE', 'USD', 10]]) { const scenario = { recordType, status: 'CHECKING', commands: [] }; proof.cases.push(scenario) const details = { recordType, occurredAt: new Date(Date.now() - 1000).toISOString(), distanceUnit: null, odometer: null, lastServiceOdometer: null, nextServiceOdometer: null, fuelQuantity: null, fuelUnit: null, verificationStatus: null, remark: context.marker, incident: { description: `${context.marker} 受控验收事件`, item: `${context.marker} ${recordType}公司费用`, place: '受控测试地点', handler: '验收操作人', processingStatus: 'COMPLETED', exceptionType: null, reportedCost: originalAmount, currency } } const creation = { idempotencyKey: randomUUID(), details }; scenario.creation = creation const record = await request('POST', care, creation, `创建本轮${recordType}记录`) check('仅操作当前车辆当前运行记录', id(record.vehicleId) === proof.vehicleId && record.details?.remark === context.marker && record.details?.recordType === recordType && record.recordNo) scenario.recordNo = record.recordNo const recordPath = `${care}/${encodeURIComponent(record.recordNo)}` const rate = currency === 'CNY' ? 1000000n : scaled(exchange.usdToCnyAverage, 6) const total = (cents(originalAmount) * rate + 500000n) / 1000000n check('受控金额可分为两笔付款', total > 1n && total <= BigInt(Number.MAX_SAFE_INTEGER)) let submission async function submit() { const command = { recordVersion: record.recordVersion, supplierId: proof.supplierId, businessDate, idempotencyKey: randomUUID() } scenario.commands.push({ action: 'SUBMIT', request: command }) submission = await request('POST', `${recordPath}/finance`, command, '提交真实公司费用来源') check('提交不冒充入账或付款', submission.recordNo === record.recordNo && submission.status === 'PENDING_FINANCE' && BigInt(submission.sourceVersion) > 0n) const replay = await request('POST', `${recordPath}/finance`, command, '原请求重放提交') check('重放保持同一来源版本', replay.replayed === true && id(replay.sourceVersion) === id(submission.sourceVersion)) } async function awaitFinance() { for (let attempt = 0; attempt < pollAttempts; attempt++) { const rows = await request('POST', `${care}/finance-status`, { recordNos: [record.recordNo] }, '轮询本条已提交Finance来源') check('来源状态严格按本车本记录返回', Array.isArray(rows) && rows.length === 1 && rows[0].recordNo === record.recordNo) const state = rows[0] check('投递未进入需人工处理状态', state.status !== 'FINANCE_ATTENTION') if (state.finance) { const cost = state.finance, source = cost.source check('Finance来源与提交版本、车辆、供应商及费用完全匹配', source?.sourceNo === record.recordNo && source.sourceType === recordType && id(source.sourceVersion) === id(submission.sourceVersion) && id(source.vehicleId) === proof.vehicleId && id(source.supplierId) === proof.supplierId && source.businessDate === businessDate && source.exchangeRateMonth === month && source.originalCurrency === currency && cents(source.originalAmount) === cents(originalAmount) && scaled(source.exchangeRate, 6) === rate && cents(source.amount) === total && source.remark === context.marker && cost.subjectCode === 'PLATFORM_DEFAULT' && cost.currency === 'CNY' && Boolean(cost.costNo)) check('首次财务接收仍未入账付款', state.status === 'PENDING' && cost.status === 'PENDING' && cents(cost.paidAmount) === 0n && cost.outstandingAmount == null) return cost } check('未接收时明确等待,不接受假成功', state.status === 'PENDING_FINANCE') if (attempt + 1 < pollAttempts) await delay(pollDelayMs) } throw new Error('Finance来源轮询超时,不能认定已接收或完成验收') } await submit(); let cost = await awaitFinance(); scenario.costNo = cost.costNo const costPath = `${FINANCE}/vehicle-costs/${encodeURIComponent(cost.costNo)}` async function write(action, body, label) { const key = randomUUID(); scenario.commands.push({ action, key, request: body }) const result = await request('POST', `${costPath}/${action}`, body, label, { key }) const replay = await request('POST', `${costPath}/${action}`, body, `${label}原键重放`, { key }) check(`${label}重放不重复推进`, same(result, replay)) return result } async function approve() { cost = await write('approve', { expectedVersion: cost.recordVersion, reason: '受控验收入账' }, '确认公司成本入账') check('入账只形成未付应付', cost.status === 'APPROVED' && cents(cost.paidAmount) === 0n && cents(cost.outstandingAmount) === total) } await approve() if (recordType === 'REPAIR') { cost = await write('void', { expectedVersion: cost.recordVersion, reason: '受控验证未付款作废后重建' }, '未付款费用作废') check('作废保留原金额且未付款', cost.status === 'VOIDED' && cents(cost.paidAmount) === 0n && cost.outstandingAmount == null) const previousVersion = BigInt(submission.sourceVersion) await submit(); cost = await awaitFinance() check('作废重建保留费用号并增加来源版本', cost.costNo === scenario.costNo && BigInt(submission.sourceVersion) === previousVersion + 1n) await approve() } const uploadPath = `${costPath}/vouchers` const uploaded = await context.requestMultipart('POST', uploadPath, { name: `${recordType.toLowerCase()}-controlled-payment.png`, type: 'image/png', content: PNG }) evidence('上传合成测试付款凭证', 'POST', uploadPath, uploaded) const voucher = context.apiSuccess(MODULE, '上传合成测试付款凭证', uploaded, '', OPTIONS) check('付款凭证为公司专属真实文件', ok(uploaded) && voucher?.fileId && voucher.ownerScope === 'PLATFORM' && id(voucher.tenantId) === '0' && voucher.businessType === 'FINANCE_VEHICLE_VOUCHER' && voucher.contentType === 'image/png' && Number(voucher.sizeBytes) === PNG.length, uploaded) scenario.voucherFileId = id(voucher.fileId) const binary = await context.requestBinary('GET', `${costPath}/vouchers/${encodeURIComponent(voucher.fileId)}/content`) check('付款凭证下载字节与本轮合成图片一致', binary?.http === 200 && id(binary.contentType).startsWith('image/png') && binary.bytes instanceof Uint8Array && Buffer.from(binary.bytes).equals(PNG), binary) const partial = total / 2n const payment = { expectedVersion: cost.recordVersion, amount: amount(partial), paidAt: new Date(Date.now() - 1000).toISOString(), method: 'BANK_TRANSFER', referenceNo: `${context.marker}-${recordType}`, voucherFileIds: [id(voucher.fileId)], remark: '受控验收模拟线下付款凭证,不发起银行转账' } cost = await write('payments', payment, '登记部分付款') check('部分付款金额守恒', cost.status === 'PARTIALLY_PAID' && cents(cost.paidAmount) === partial && cents(cost.outstandingAmount) === total - partial) await request('POST', `${costPath}/payments`, { ...payment, expectedVersion: cost.recordVersion, amount: amount(total - partial + 1n) }, '拒绝超过未付余额的付款', { key: randomUUID(), reject: ['FIN_409'] }) const unchanged = await request('GET', costPath, undefined, '超付失败后回查余额及版本') check('超付拒绝没有改变财务事实', same(unchanged, cost)) cost = await write('payments', { ...payment, expectedVersion: cost.recordVersion, amount: amount(total - partial) }, '登记剩余实际付款') check('全额付款终态金额守恒', cost.status === 'PAID' && cents(cost.paidAmount) === total && cents(cost.outstandingAmount) === 0n) await request('POST', `${costPath}/void`, { expectedVersion: cost.recordVersion, reason: '负向验证已付款不可作废' }, '已付款作废被拒绝', { key: randomUUID(), reject: ['FIN_409'] }) await request('PUT', recordPath, { recordVersion: record.recordVersion, reason: '负向验证已付款不可改金额', details: { ...details, incident: { ...details.incident, reportedCost: originalAmount + 1 } } }, '已付款来源修改被拒绝', { reject: ['A0409'] }) check('已付款负向动作不改变费用', same(await request('GET', costPath, undefined, '负向动作后回查已付费用'), cost)) const careAfter = await request('GET', recordPath, undefined, '回查运维申报金额未被覆盖') check('负向修改不改变运维版本或金额', id(careAfter.recordVersion) === id(record.recordVersion) && cents(careAfter.details?.incident?.reportedCost) === cents(originalAmount)) const paid = await page(`${costPath}/payments`, '核对实际付款流水') check('两次付款重放仅留下两条不同流水及原凭证', paid.length === 2 && new Set(paid.map(row => row.paymentNo)).size === 2 && paid.reduce((sum, row) => sum + cents(row.amount), 0n) === total && paid.every(row => row.currency === 'CNY' && same(row.voucherFileIds.map(id), [id(voucher.fileId)]))) const history = await page(`${costPath}/history`, '核对不可变财务操作历史') check('历史保留接收、入账、两次付款及作废过程', history.filter(row => row.action === 'PAY').length === 2 && history.some(row => row.action === 'RECEIVE') && history.some(row => row.action === 'APPROVE') && (recordType !== 'REPAIR' || history.some(row => row.action === 'VOID'))) scenario.finance = cost; scenario.payments = paid; scenario.history = history; scenario.status = 'VERIFIED' check('本轮公司费用未进入客户费用池', same(proof.customerBefore, await snapshotCustomer(`${recordType}完成后`))) } proof.status = 'VERIFIED' } catch (error) { failure = error; proof.status = 'FAILED'; proof.error = error instanceof Error ? error.message : String(error) } if (failure) throw failure }