import { verifyDownloadCenterExport } from './download-center.mjs' import { createHash } from 'node:crypto' import { expectedAllocations } from './fleet-trip-cost-acceptance.mjs' /** C's signed-by-hash handover artifact is the only producer accepted by the standalone reader. */ export function operationsFixtureFromHandover(bytes, { sha256, marker, runId }) { const fail = () => { throw new Error('Current C fixture, original DEPART or verified independent cost shares do not match') } if (!/^[a-f0-9]{64}$/.test(sha256 || '') || createHash('sha256').update(bytes).digest('hex') !== sha256 || !marker || !runId) fail() const f = JSON.parse(bytes.toString('utf8')), history = f.immutableDeparture, snapshot = history?.snapshot, load = snapshot?.confirmedLoad, c = f.costs if (f.marker !== marker || f.runId !== runId || f.status !== 'DEPART_AND_HANDOVER_VERIFIED' || !f.handoverNo || !f.tripNo || !f.warehouseId || history?.action !== 'DEPART' || snapshot?.trip?.tripNo !== f.tripNo || String(snapshot.trip.warehouseId) !== String(f.warehouseId) || f.departure?.tripNo !== f.tripNo || f.departure.status !== 'IN_TRANSIT' || !Number.isFinite(Date.parse(f.departure.departedAt)) || snapshot.trip.departedAt !== f.departure.departedAt || !load?.manifestHash || c?.status !== 'VERIFIED' || c.marker !== marker || c.tripNo !== f.tripNo || c.expected?.finalCny !== '100.03' || c.expected?.usd !== '13.07') fail() const positive = value => Number.isSafeInteger(Number(value)) && Number(value) > 0 if (!Array.isArray(load.pallets) || !load.pallets.length || !Array.isArray(load.cargo) || !load.cargo.length || !Array.isArray(c.shares) || !c.shares.length) fail() const stops = snapshot.trip.stops if (!Array.isArray(stops) || load.pallets.some(p => !Number.isSafeInteger(p.deliveryIndex) || p.deliveryIndex <= 0 || p.deliveryIndex >= stops.length || !stops[p.deliveryIndex]?.destinationKey || !load.cargo.some(row => String(row.warehousePalletId) === String(p.warehousePalletId)) || load.cargo.filter(row => String(row.warehousePalletId) === String(p.warehousePalletId)).some(row => row.destinationKey !== stops[p.deliveryIndex].destinationKey))) fail() const sameRoute = new Set(load.pallets.map(p => p.deliveryIndex)).size === 1 if (sameRoute ? c.expected.allocationMethod !== 'SAME_ROUTE_PALLET_SPACES' || c.expected.allocationStatus !== 'AVAILABLE' : c.expected.allocationMethod !== null || c.expected.allocationStatus !== 'UNAVAILABLE' || c.expected.allocationBlockReason !== '各票卸货站不同,暂不可按同路线托位自动分摊') fail() const pallets = new Map() for (const p of load.pallets) { if (!p.warehousePalletId || pallets.has(String(p.warehousePalletId)) || !positive(p.palletSpaces)) fail(); pallets.set(String(p.warehousePalletId), Number(p.palletSpaces)) } const seen = new Set(), shipped = new Map() for (const row of load.cargo) { const item = `${row.dispatchNo}:${row.dispatchItemId}`, key = `${row.dispatchNo}:${row.customerId}:${row.warehousePalletId}` if (!row.dispatchNo || !row.dispatchItemId || !row.customerId || seen.has(item) || !pallets.has(String(row.warehousePalletId)) || !positive(row.packages)) fail() seen.add(item); shipped.set(key, (shipped.get(key) || 0) + Number(row.packages)) } const bookings = new Map(), external = new Map(), money = new Map() for (const b of snapshot.externalBookings || []) { if (['DRAFT', 'CANCELLED'].includes(b.status)) continue if (b.status !== 'BOUND' || !b.bookingNo || bookings.has(b.bookingNo) || !b.dispatchNo || !b.receipt?.source?.pallets?.length || !['CNY', 'USD'].includes(b.cargo?.currency)) fail() bookings.set(b.bookingNo, b) let count = 0 for (const p of b.receipt.source.pallets) { if (!positive(p.packages)) fail(); const key = `${b.dispatchNo}:${b.customerId}:${p.warehousePalletId}`; external.set(key, (external.get(key) || 0) + Number(p.packages)); count += Number(p.packages) } if (count !== Number(b.receipt.source.receivedPackages)) fail() const amount = number(b.agreedAmount); money.set(b.cargo.currency, (money.get(b.cargo.currency) || 0) + Math.round(amount * 100)) } for (const [key, value] of external) if (value > (shipped.get(key) || 0)) fail() const sharePallets = new Set(), businesses = new Set() for (const share of c.shares) { if (!share.businessNo || businesses.has(share.businessNo) || !['OWN', 'EXTERNAL'].includes(share.sourceType) || !share.warehousePalletIds?.length) fail() businesses.add(share.businessNo) let spaces = 0 for (const id of share.warehousePalletIds) { if (!pallets.has(String(id)) || sharePallets.has(String(id)) || !load.cargo.some(row => row.dispatchNo === share.dispatchNo && String(row.warehousePalletId) === String(id))) fail() sharePallets.add(String(id)); spaces += pallets.get(String(id)) } if (spaces !== share.palletSpaces) fail() if (share.sourceType === 'EXTERNAL') { const b = bookings.get(share.businessNo) if (!b || b.dispatchNo !== share.dispatchNo || b.receipt.source.pallets.some(p => !share.warehousePalletIds.map(String).includes(String(p.warehousePalletId)))) fail() } else if (share.businessNo !== share.dispatchNo || [...bookings.values()].some(b => b.dispatchNo === share.dispatchNo)) fail() } if (sharePallets.size !== pallets.size || !c.shares.some(s => s.sourceType === 'OWN') || !c.shares.some(s => s.sourceType === 'EXTERNAL')) fail() const total = [...shipped.values()].reduce((a, b) => a + b, 0), ext = [...external.values()].reduce((a, b) => a + b, 0) if (total !== Number(f.departure.shippedPackages) || ext <= 0 || ext >= total) fail() const costs = [['CNY', '100.03'], ['USD', '13.07']].map(([currency, amount]) => { if (!sameRoute) return { currency, directCost: Number(amount), ownCost: null, externalCost: null } const allocations = expectedAllocations(amount, c.shares) const sum = type => Number(allocations.filter(row => row.sourceType === type).reduce((n, row) => n + BigInt(row.cents), 0n)) / 100 return { currency, directCost: Number(amount), ownCost: sum('OWN'), externalCost: sum('EXTERNAL') } }) return { marker, stage: 'CURRENT_RUN_DEPARTED', tripNo: f.tripNo, warehouseId: String(f.warehouseId), departedAt: f.departure.departedAt, departureVerified: true, immutableDeparture: history, expected: { physicalPallets: pallets.size, palletSpaces: [...pallets.values()].reduce((a, b) => a + b, 0), ownPackages: total - ext, externalPackages: ext, externalAgreedAmounts: [...money].map(([currency, amount]) => ({ currency, amount: amount / 100 })), costs } } } const MODULE = 'fleet-quotes-operations' const QUOTES = '/api/platform/v1/last-leg/fleet/quotes' const OPERATIONS = '/api/platform/v1/last-leg/fleet/operations' const OPTIONS = { scenario: 'fleet-quotes-and-current-departure', deploymentBlocker: true } const id = value => value == null ? '' : String(value) const successful = response => response?.http === 200 && id(response.body?.code) === '200' const encode = encodeURIComponent const query = values => new URLSearchParams(Object.entries(values).filter(([, value]) => value != null).map(([key, value]) => [key, String(value)])).toString() const version = value => BigInt(id(value)) const same = (a, b) => JSON.stringify(a) === JSON.stringify(b) const canonical = value => Array.isArray(value) ? value.map(canonical) : value && typeof value === 'object' ? Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])])) : value function number(value) { if (!['number', 'string'].includes(typeof value) || !/^\d+(?:\.\d{1,2})?$/.test(String(value)) || !Number.isFinite(Number(value))) throw new Error('Missing or invalid numeric fact; never convert unknown to zero') return Number(value) } export const fixtureContract = Object.freeze({ quoteEntry: 'run(context): QUOTES_ONLY; requires only current-run merchant-qualification-fixture, never DEPART', operationsEntry: 'verifyOperations(context,{writeMeasured:false,idempotencyNamespace}): OPERATIONS_ONLY, read-only by default; explicit writeMeasured:true uses a separate stable idempotency namespace', contextKey: 'context.fleetQuotesOperationsFixture', stage: 'CURRENT_RUN_DEPARTED', required: 'marker,tripNo,warehouseId,departedAt,departureVerified:true,expected:{physicalPallets,palletSpaces,ownPackages,externalPackages,externalAgreedAmounts:[{currency,amount}],costs:[{currency,directCost,ownCost,externalCost}]}', prerequisite: 'Operations require a current-run real HTTP departure producer and a trip remark containing its marker. Quotes require their own current-run qualified customer only. These are independent acceptance scopes.', boundary: 'No historical fallback, no invented DEPART, no SQL or deployment. Does not prove the upstream departure transaction or Finance payment. Import exercises the structured rows produced by the CSV UI, not browser file selection.', effects: 'Creates a fresh overseas TRUCKING supplier and isolated purchase/sale quotes; appends measured distance/fuel corrections to the supplied current-run trip. Finally deactivates only own quotes/supplier; retains all rows/history. No DELETE.', }) export function requireCurrentFixture(context) { const fixture = context?.context?.fleetQuotesOperationsFixture if (!context?.marker || !fixture || fixture.marker !== context.marker || fixture.stage !== fixtureContract.stage || fixture.departureVerified !== true || !fixture.tripNo || !fixture.warehouseId || !Number.isFinite(Date.parse(fixture.departedAt))) throw new Error('Requires current-run verified real DEPART fixture; historical samples are forbidden') const expected = fixture.expected if (!expected || ['physicalPallets', 'palletSpaces', 'ownPackages', 'externalPackages'].some(key => !Number.isSafeInteger(expected[key]) || expected[key] < 0) || expected.physicalPallets < 1 || expected.palletSpaces < 1 || expected.ownPackages + expected.externalPackages < 1 || !Array.isArray(expected.externalAgreedAmounts) || !Array.isArray(expected.costs)) throw new Error('Requires independent expected cargo and currency-separated cost facts') amounts(expected.externalAgreedAmounts); costs(expected.costs) return fixture } function requireQuoteCustomer(context) { const customer = context?.createdResources?.merchantQualificationFixture if (!context?.marker || customer?.marker !== context.marker || !customer.tenantId) throw new Error('Requires current-run qualified customer for quote acceptance') return customer } const currency = value => { if (!['CNY', 'USD'].includes(value)) throw new Error('Unsupported or missing currency fact'); return value } const amounts = rows => rows.map(row => [currency(row.currency), number(row.amount)]).sort((a, b) => a[0].localeCompare(b[0])) const costs = rows => rows.map(row => [currency(row.currency), number(row.directCost), row.ownCost == null ? null : number(row.ownCost), row.externalCost == null ? null : number(row.externalCost)]).sort((a, b) => a[0].localeCompare(b[0])) /** Only the registered TEST runner invokes run; importing this module never reads credentials or calls HTTP. */ export async function run(context) { return execute(context, { scope: 'QUOTES_ONLY' }) } export async function verifyOperations(context, { writeMeasured = false, idempotencyNamespace } = {}) { if (writeMeasured && !idempotencyNamespace) throw new Error('Measured corrections require an explicit independent idempotency namespace') return execute(context, { scope: 'OPERATIONS_ONLY', writeMeasured, idempotencyNamespace }) } /** Explicit orchestrator helper. Caller supplies a fresh AcceptanceRun with TEST options; no global env mutation. * Real standalone execution uses fleet-quotes-test-env.mjs, which journals every request before sending. * This return value is a final summary, not a replacement for durable pre-request evidence. */ export async function runQuoteSession(context, { marker = 'FLEET-QUOTE-20261003', prepareCustomer } = {}) { if (context?.options?.environment !== 'test' || context.token || context.refreshToken || Object.keys(context.createdResources || {}).length) throw new Error('Quote session requires a fresh TEST context, never a borrowed session or fixture') context.marker = marker context.options.customerName = `报价专用客户-${marker}` let fatal = null, loggedOut = false try { const response = await context.request('POST', '/api/auth/login', { loginId: context.options.username, password: context.options.password }, '') const login = context.apiSuccess(MODULE, '独立报价验收会话登录', response, '', OPTIONS) if (!successful(response) || !login?.accessToken || !login?.refreshToken) throw new Error('Independent TEST login failed') context.token = login.accessToken; context.refreshToken = login.refreshToken const prepare = prepareCustomer || (await import('./merchant-qualification-fixture.mjs')).run await context.executeModule('merchant-qualification-fixture', () => prepare(context, { publishEnvironment: false, requireDistinctCustomer: false })) if (context.steps.some(step => !step.passed)) throw new Error('Current-run customer preparation failed; quotes not executed') await context.executeModule(MODULE, () => run(context)) } catch (error) { fatal = error instanceof Error ? error.message : String(error) } finally { if (context.token) { const response = await context.request('POST', '/api/auth/logout', { refreshToken: context.refreshToken }) context.apiSuccess(MODULE, '退出独立报价验收会话', response, '', OPTIONS) loggedOut = successful(response) if (!loggedOut) fatal ||= 'Independent TEST session logout failed' } } const { buildAcceptanceSummary } = await import('../admin-test-env-acceptance.mjs') const summary = buildAcceptanceSummary(context.steps, context.modules, context.coverageGaps) const report = { marker, scope: 'QUOTES_ONLY', generatedAt: new Date().toISOString(), success: !fatal && loggedOut && summary.failed === 0, fatal, loggedOut, operationsStatus: 'NOT_TESTED', financeStatus: 'NOT_TESTED', summary, modules: context.modules, steps: context.steps, createdResources: context.createdResources } const serialized = JSON.stringify(report) const secrets = [context.options.password, context.token, context.refreshToken, context.context?.merchantQualificationFixture?.password].filter(value => typeof value === 'string' && value.length >= 8) if (secrets.some(value => serialized.includes(value)) || /"(?:password|accessToken|refreshToken|authorization)"\s*:/i.test(serialized)) throw new Error('Unsafe report rejected: contains credentials') context.token = ''; context.refreshToken = '' return report } async function execute(context, { scope, writeMeasured = false, idempotencyNamespace }) { context.createdResources ||= {} const proofKey = scope === 'QUOTES_ONLY' ? 'fleetQuoteAcceptance' : 'fleetOperationsAcceptance' if (context.createdResources[proofKey]) throw new Error('Existing proof must be retained; do not overwrite failed evidence') const proof = context.createdResources[proofKey] = { marker: context.marker, scope, status: 'CHECKING', quoteStatus: 'NOT_TESTED', operationsStatus: 'NOT_TESTED', measuredStatus: 'NOT_TESTED', requests: [], quotes: [], supplierId: null, cleanup: 'PENDING', upstreamDeparture: 'NOT_TESTED', financePayment: 'NOT_TESTED' } let failure const check = (label, response, condition) => { context.assert(MODULE, label, response, Boolean(condition), '', OPTIONS) if (!condition) throw new Error(label) } const key = suffix => createHash('sha256').update(`${idempotencyNamespace || context.marker}:${suffix}`).digest('hex') const recordResponse = (method, path, response) => proof.requests.push({ method, path, http: response?.http, code: response?.body?.code, requestId: response?.requestId, traceId: response?.traceId }) async function request(method, path, body) { const response = await context.request(method, path, body); recordResponse(method, path, response); return response } async function command(method, path, body, label) { const response = await request(method, path, body) const data = context.apiSuccess(MODULE, label, response, '', OPTIONS) check(`${label}保留链路证据`, response, successful(response) && response.requestId && response.traceId && data != null) return data } async function reject(method, path, body, expectedCode, label) { const response = await request(method, path, body) check(label, response, response.http === 200 && id(response.body?.code) === expectedCode && response.requestId && response.traceId) } async function binary(path) { const response = await context.requestBinary('GET', path); recordResponse('GET', path, response); return response } const exportContext = Object.assign(Object.create(context), { request, requestBinary: (_method, path) => binary(path) }) function retain(row) { check('报价返回稳定业务号', null, typeof row?.quoteNo === 'string' && row.quoteNo.length > 0) if (!proof.quotes.includes(row.quoteNo)) proof.quotes.push(row.quoteNo) return row } try { let fixture, from, to, report if (scope === 'OPERATIONS_ONLY') { fixture = requireCurrentFixture(context) if (fixture.immutableDeparture) { const history = await command('GET', `/api/platform/v1/last-leg/fleet/trips/${encode(fixture.tripNo)}/history?page=1&size=200`, undefined, '核对输入报告的原DEPART不可变快照') const departures = history.records?.filter(row => row.action === 'DEPART') || [] check('当前历史完整且原DEPART与SHA绑定报告一致', null, Number(history.total) === history.records?.length && departures.length === 1 && same(canonical(departures[0]), canonical(fixture.immutableDeparture))) } proof.tripNo = fixture.tripNo const trip = await command('GET', `/api/platform/v1/last-leg/fleet/trips/${encode(fixture.tripNo)}`, undefined, '核对本轮真实发车车次') check('车次属于当前场景并有真实发车时间', null, trip.trip?.tripNo === fixture.tripNo && id(trip.trip.warehouseId) === id(fixture.warehouseId) && (fixture.immutableDeparture || trip.trip.remark?.includes(context.marker)) && Date.parse(trip.trip.departedAt) === Date.parse(fixture.departedAt) && ['IN_TRANSIT', 'COMPLETED'].includes(trip.trip.status)) from = new Date(Date.parse(fixture.departedAt) - 1).toISOString(); to = new Date(Date.parse(fixture.departedAt) + 1000).toISOString() const reportPath = `${OPERATIONS}?${query({ from, to, warehouseId: fixture.warehouseId, keyword: fixture.tripNo, page: 1, size: 20 })}` report = async () => { const result = await command('GET', reportPath, undefined, '读取本轮车次运营与成本汇总') check('同车多客户及目的地只计一次出车', null, Number(result.page?.total) === 1 && result.page.records?.length === 1 && Number(result.summary?.trips) === 1 && result.page.records[0].tripNo === fixture.tripNo) const row = result.page.records[0] for (const field of ['physicalPallets', 'palletSpaces', 'ownPackages', 'externalPackages']) check(`实运${field}与独立fixture一致`, null, number(row[field]) === fixture.expected[field] && number(result.summary[field]) === fixture.expected[field]) check('约定金额分币种核平且不伪造实收', null, same(amounts(row.externalAgreedAmounts), amounts(fixture.expected.externalAgreedAmounts)) && same(amounts(result.summary.externalAgreedAmounts), amounts(fixture.expected.externalAgreedAmounts)) && row.financeStatus === 'NOT_QUERIED') check('成本使用正式批量事实而非缺失补零', null, result.summary.costs?.visible === true && row.costs && same(costs(row.costs.totals), costs(fixture.expected.costs)) && row.costStatus === (fixture.expected.costs.length ? 'AVAILABLE' : 'NOT_RECORDED') && Number(result.summary.costs.unrecordedTrips) === (fixture.expected.costs.length ? 0 : 1)) check('统计图来自同一查询范围', null, result.charts?.physicalPallets?.flatMap(series => series.points).reduce((sum, point) => sum + Number(point.value), 0) === fixture.expected.physicalPallets) return result } await report() } if (scope === 'QUOTES_ONLY') { const customer = requireQuoteCustomer(context) const supplier = await command('POST', '/api/platform/v1/suppliers?tenantId=0', { name: `车辆报价验收-${context.marker}`, category: 'TRUCKING', truckingScope: 'OVERSEAS', carrierCodes: [], status: 'ACTIVE', remark: context.marker, region: 'TEST', legalRepresentative: '测试法人', contactName: '测试联系人', phone: '19900000000', email: 'fleet-acceptance@example.com', settlementPeriod: 'MONTHLY_30', }, '新建本轮海外车队供应商') proof.supplierId = id(supplier.id ?? supplier) check('供应商返回明确标识', null, /^\d+$/.test(proof.supplierId)) const day = new Date().toISOString().slice(0, 10) const details = { kind: 'SALE', partyId: id(customer.tenantId), originCity: `QA-${context.marker}`, destinationCode: 'QA-FLEET', distanceMiles: 10, currency: 'USD', selfPickupPrice: null, fullDeliveryPrice: 0, looseDeliveryPrice: 12.5, validFrom: day, validTo: day, remark: context.marker } const create = { idempotencyKey: key('sale'), details } await reject('POST', QUOTES, { idempotencyKey: key('no-price'), details: { ...details, fullDeliveryPrice: null, looseDeliveryPrice: null } }, 'A0400', '所有价格为空不能成为免费报价') const sale = retain(await command('POST', QUOTES, create, '创建空价与明确零价销售报价')) check('空价与零价严格区分', null, sale.details?.selfPickupPrice == null && sale.details?.fullDeliveryPrice != null && Number(sale.details.fullDeliveryPrice) === 0 && Number(sale.details.looseDeliveryPrice) === 12.5 && id(sale.details.partyId) === id(customer.tenantId)) const replay = await command('POST', QUOTES, create, '重放原报价创建请求') check('创建重放不新增报价或版本', null, replay.quoteNo === sale.quoteNo && id(replay.recordVersion) === id(sale.recordVersion)) await reject('POST', QUOTES, { ...create, details: { ...details, looseDeliveryPrice: 13 } }, 'A0409', '同键异价拒绝') let current = await command('PUT', `${QUOTES}/${encode(sale.quoteNo)}`, { recordVersion: sale.recordVersion, details: { ...details, looseDeliveryPrice: 15 } }, '更正销售报价') check('更正只递增一个版本', null, version(current.recordVersion) === version(sale.recordVersion) + 1n) await reject('PUT', `${QUOTES}/${encode(sale.quoteNo)}`, { recordVersion: sale.recordVersion, details }, 'A0409', '旧版本不能覆盖新报价') const afterConflict = await command('GET', `${QUOTES}/${encode(sale.quoteNo)}`, undefined, '回查版本冲突无写入') check('冲突后价格版本不漂移且空价零价已持久化', null, id(afterConflict.recordVersion) === id(current.recordVersion) && number(afterConflict.details.looseDeliveryPrice) === 15 && afterConflict.details.selfPickupPrice == null && afterConflict.details.fullDeliveryPrice != null && number(afterConflict.details.fullDeliveryPrice) === 0) const historicalReplay = await command('POST', QUOTES, create, '编辑后原创建键仍返回原始快照') check('创建重放不会返回被更正价格冒充原结果', null, id(historicalReplay.recordVersion) === id(sale.recordVersion) && Number(historicalReplay.details.looseDeliveryPrice) === 12.5) const purchase = retain(await command('POST', QUOTES, { idempotencyKey: key('purchase'), details: { ...details, kind: 'PURCHASE', partyId: proof.supplierId } }, '创建独立采购报价')) check('采购销售没有合并成同一记录', null, purchase.quoteNo !== sale.quoteNo && purchase.details.kind === 'PURCHASE' && id(purchase.details.partyId) === proof.supplierId) const importCity = `IMPORT-${context.marker}` const invalidBatch = { idempotencyKey: key('rollback-import'), rows: [{ ...details, originCity: importCity }, { ...details, originCity: importCity }] } await reject('POST', `${QUOTES}/import`, invalidBatch, 'A0409', '重复组合导致整批导入失败') const rollback = await command('GET', `${QUOTES}?${query({ keyword: importCity, kind: 'SALE', page: 1, size: 200 })}`, undefined, '回查失败批次无部分写入') proof.rollbackReturnedQuoteNos = rollback.records.map(row => row.quoteNo) check('CSV结构化导入失败整批回滚', null, Number(rollback.total) === 0 && rollback.records.length === 0) const batch = { idempotencyKey: key('valid-import'), rows: [{ ...details, originCity: importCity }, { ...details, originCity: importCity, destinationCode: 'QA-FLEET-B' }] } const imported = await command('POST', `${QUOTES}/import`, batch, '原子导入两条报价') check('成功导入返回两条业务记录', null, Number(imported.importedRows) === 2 && imported.records?.length === 2) imported.records.forEach(retain) const importedAgain = await command('POST', `${QUOTES}/import`, batch, '重放原批次导入') check('整批重放保持原业务号和版本', null, same(importedAgain.records.map(r => [r.quoteNo, id(r.recordVersion)]), imported.records.map(r => [r.quoteNo, id(r.recordVersion)]))) await reject('POST', `${QUOTES}/import`, { ...batch, rows: batch.rows.slice(0, 1) }, 'A0409', '同批次键不能缩短或改变内容') const confirmedImport = await command('GET', `${QUOTES}?${query({ keyword: importCity, kind: 'SALE', page: 1, size: 200 })}`, undefined, '核对导入重放无重复行') check('导入重放及拒绝后仍恰好两条', null, Number(confirmedImport.total) === 2 && new Set(confirmedImport.records.map(r => r.quoteNo)).size === 2) current = await command('PATCH', `${QUOTES}/${encode(sale.quoteNo)}/status`, { recordVersion: current.recordVersion, status: 'INACTIVE' }, '停用销售报价') const activeSales = await command('GET', `${QUOTES}?${query({ keyword: details.originCity, kind: 'SALE', status: 'ACTIVE', page: 1, size: 200 })}`, undefined, '核对停用及用途筛选隔离') check('停用销售不被启用筛选或采购混入', null, activeSales.records.every(r => r.details.kind === 'SALE' && r.status === 'ACTIVE' && r.quoteNo !== sale.quoteNo)) const stillPurchase = await command('GET', `${QUOTES}/${encode(purchase.quoteNo)}`, undefined, '核对采购报价不受销售停用影响') check('用途独立状态未串改', null, stillPurchase.status === 'ACTIVE' && id(stillPurchase.recordVersion) === id(purchase.recordVersion)) const history = await command('GET', `${QUOTES}/${encode(sale.quoteNo)}/history`, undefined, '读取报价不可变版本历史') check('历史保留原始零价与更正版本', null, history.length === 3 && history.some(h => Number(h.snapshot?.details?.looseDeliveryPrice) === 12.5) && history.some(h => Number(h.snapshot?.details?.looseDeliveryPrice) === 15)) await verifyDownloadCenterExport(exportContext, MODULE, 'FLEET_QUOTE_LIST', { keyword: details.originCity }, ['报价号'], [sale.quoteNo, purchase.quoteNo]) await verifyDownloadCenterExport(exportContext, MODULE, 'FLEET_QUOTE_LIST', { kind: 'INVALID' }, [], [], 'FAILED') proof.quoteStatus = 'VERIFIED' } if (scope === 'OPERATIONS_ONLY') { if (writeMeasured) { const recordPath = `${OPERATIONS}/${encode(fixture.tripNo)}/record` const initial = await command('GET', recordPath, undefined, '核对本轮车次尚无实测记录') check('禁止覆盖已有实测历史样本', null, id(initial.recordVersion) === '0' && initial.details.actualDistanceMiles == null && initial.details.actualFuelLitres == null) const measured = { recordVersion: initial.recordVersion, idempotencyKey: key('measured-zero'), details: { actualDistanceMiles: 0, actualFuelLitres: null, remark: context.marker, evidenceFileIds: [] }, reason: null } const first = await command('PUT', recordPath, measured, '登记明确零里程和未知油耗') check('实测明确零值不变成未填写', null, first.details.actualDistanceMiles != null && Number(first.details.actualDistanceMiles) === 0 && first.details.actualFuelLitres == null) const firstAgain = await command('PUT', recordPath, measured, '重放实测保存') check('实测重放保留原版本', null, id(firstAgain.recordVersion) === id(first.recordVersion)) const corrected = await command('PUT', recordPath, { recordVersion: first.recordVersion, idempotencyKey: key('measured-correct'), details: { actualDistanceMiles: 25.5, actualFuelLitres: 8.2, remark: context.marker, evidenceFileIds: [] }, reason: '核对受控实测' }, '更正实测里程油耗') await reject('PUT', recordPath, { ...measured, idempotencyKey: key('measured-stale') }, 'A0409', '旧实测版本拒绝') const measuredHistory = await command('GET', `${recordPath}/history?page=1&size=20`, undefined, '读取实测更正历史') check('实测重放与冲突没有追加历史', null, Number(measuredHistory.total) === 2 && version(corrected.recordVersion) === version(first.recordVersion) + 1n) proof.measuredStatus = 'VERIFIED' } const finalReport = await report() if (writeMeasured) check('运营列表批量回显最新实测记录', null, Number(finalReport.page.records[0].measured?.actualDistanceMiles) === 25.5 && Number(finalReport.page.records[0].measured?.actualFuelLitres) === 8.2) await verifyDownloadCenterExport(exportContext, MODULE, 'TRIP_OPERATIONS_LIST', { from, to, warehouseId: fixture.warehouseId, keyword: fixture.tripNo }, ['车次号', '实际里程(mile)'], [fixture.tripNo, ...(writeMeasured ? ['25.5'] : [])]) await verifyDownloadCenterExport(exportContext, MODULE, 'TRIP_OPERATIONS_LIST', { from: to, to: from }, [], [], 'FAILED') proof.operationsStatus = 'VERIFIED' } proof.status = 'VERIFIED' } catch (error) { proof.status = 'FAILED'; proof.error = error instanceof Error ? error.message : String(error); failure = error } finally { const cleanupErrors = [] for (const no of context.quoteRequestOutcomeUnknown ? [] : proof.quotes) { try { const row = await command('GET', `${QUOTES}/${encode(no)}`, undefined, '读取本轮报价最终版本') if (row.status === 'ACTIVE') await command('PATCH', `${QUOTES}/${encode(no)}/status`, { recordVersion: row.recordVersion, status: 'INACTIVE' }, '停用本轮专用报价并保留历史') } catch (error) { cleanupErrors.push(error.message) } } if (proof.supplierId && !context.quoteRequestOutcomeUnknown) { try { const path = `/api/platform/v1/suppliers/${encode(proof.supplierId)}` const response = await request('PATCH', `${path}/status?tenantId=0`, { status: 'INACTIVE' }) context.apiSuccess(MODULE, '停用本轮车队供应商并保留证据', response, '', OPTIONS) check('供应商停用命令成功', response, successful(response)) const supplier = await command('GET', `${path}?tenantId=0`, undefined, '回查本轮车队供应商已停用') check('本轮车队供应商最终停用', null, supplier.status === 'INACTIVE' && id(supplier.id) === proof.supplierId) } catch (error) { cleanupErrors.push(error.message) } } proof.cleanup = context.quoteRequestOutcomeUnknown ? 'NOT_ATTEMPTED_UNKNOWN_OUTCOME' : cleanupErrors.length ? 'FAILED' : proof.quotes.length || proof.supplierId ? 'RETAINED_AND_DEACTIVATED' : 'NOTHING_CREATED' if (cleanupErrors.length) { proof.cleanupErrors = cleanupErrors; proof.status = 'FAILED'; failure ||= new Error('本轮报价资源停用失败') } } if (failure) throw failure }