import { exportWorkbook } from './download-center-fixture.mjs' import { parseXlsx } from './finance-downloads.mjs' import assert from 'node:assert/strict' import test from 'node:test' import { run, runQuoteSession, verifyOperations, requireCurrentFixture, fixtureContract } from './fleet-quotes-operations.mjs' import { AcceptanceRun } from '../admin-test-env-acceptance.mjs' const copy = value => structuredClone(value) test('fixed quote supplier is reused without creation or deactivation', async () => { const { context } = harness(), request = context.request.bind(context), writes = [] context.options = { fixedCustomerId: '101' } const supplier = { id: '901', name: 'Existing', category: 'TRUCKING', truckingScope: 'OVERSEAS', status: 'ACTIVE' } context.request = async (method, path, ...args) => { if (path.includes('/suppliers')) { if (method !== 'GET') writes.push({ method, path }) return { http: 200, body: { code: '200', data: path.includes('/901') ? supplier : { records: [supplier], total: 1 } } } } return request(method, path, ...args) } await run(context) assert.equal(context.createdResources.fleetQuoteAcceptance.supplierReused, true) assert.deepEqual(writes, []) }) function harness(fault) { const marker = 'FLEET-CURRENT-RUN', tripNo = 'TR-CURRENT', departedAt = '2026-10-03T01:00:00Z' const rows = new Map(), histories = new Map(), commands = new Map(), measuredKeys = new Map(), calls = [] let next = 0, supplierStatus = 'ACTIVE' let measured = { tripNo, recordVersion: '0', details: { actualDistanceMiles: null, actualFuelLitres: null, remark: null, evidenceFiles: [] } } const measuredHistory = [] const exportTasks = new Map() const expected = { physicalPallets: 1, palletSpaces: 2, ownPackages: 11, externalPackages: 4, externalAgreedAmounts: [{ currency: 'USD', amount: 0 }], costs: [] } const response = (data, code = '200', http = 200) => ({ http, body: { code, data: copy(data) }, requestId: 'offline-request', traceId: 'offline-trace' }) function snapshot(row, action) { histories.get(row.quoteNo).unshift({ action, recordVersion: row.recordVersion, snapshot: copy(row) }) } const natural = d => JSON.stringify([d.kind, d.partyId, d.originCity, d.destinationCode, d.distanceMiles, d.currency, d.validFrom, d.validTo]) function create(request, batchHash = '') { const hash = batchHash + JSON.stringify(request.details), prior = commands.get(request.idempotencyKey) if (prior) return prior.hash === hash ? response(fault === 'mutable creation replay' ? rows.get(prior.row.quoteNo) : prior.row) : response(null, 'A0409') const d = request.details if ([d.selfPickupPrice, d.fullDeliveryPrice, d.looseDeliveryPrice].every(v => v == null)) return response(null, 'A0400') if ([...rows.values()].some(r => natural(r.details) === natural(d))) return response(null, 'A0409') const row = { quoteNo: `FQ-${++next}`, recordVersion: '0', details: copy(d), partyName: '本轮往来单位', status: 'ACTIVE' } if (fault === 'null price becomes zero') row.details.selfPickupPrice = 0 rows.set(row.quoteNo, row); histories.set(row.quoteNo, []); snapshot(row, 'CREATE'); commands.set(request.idempotencyKey, { hash, row: copy(row) }); return response(row) } const context = { marker, context: { fleetQuotesOperationsFixture: { marker, stage: fixtureContract.stage, departureVerified: true, tripNo, warehouseId: '92', departedAt, expected } }, createdResources: { merchantQualificationFixture: { marker, tenantId: '101' } }, assert(_module, label, _response, condition) { assert.ok(condition, label) }, apiSuccess(_module, label, result) { assert.equal(result.http, 200, label); assert.equal(result.body.code, '200', label); return result.body.data }, async request(method, path, body) { calls.push({ method, path, body: copy(body) }); const u = new URL(path, 'http://offline.invalid'), p = u.pathname if (p === '/api/platform/v1/files/export-tasks' && method === 'POST') { const taskId = String(exportTasks.size + 1) exportTasks.set(taskId, body) return response({ id: taskId, status: 'PENDING' }) } if (p.startsWith('/api/platform/v1/files/export-tasks/')) { const task = exportTasks.get(p.split('/').at(-1)) const failed = task.parameters.kind === 'INVALID' || (task.parameters.from && Date.parse(task.parameters.from) > Date.parse(task.parameters.to)) return response({ id: p.split('/').at(-1), status: failed ? 'FAILED' : 'SUCCEEDED', failureMessage: failed ? '筛选参数无效' : null }) } if (p === '/api/platform/v1/last-leg/fleet/trips/TR-CURRENT') return response({ trip: { tripNo, warehouseId: '92', departedAt, status: 'IN_TRANSIT', remark: fault === 'historical trip' ? 'OLD' : marker } }) if (p === '/api/platform/v1/suppliers' && method === 'POST') { for (const field of ['name', 'region', 'legalRepresentative', 'contactName', 'phone', 'settlementPeriod']) assert.ok(body[field], field) assert.equal(body.truckingScope, 'OVERSEAS'); return response('901') } if (p === '/api/platform/v1/suppliers/901/status') { supplierStatus = body.status; return response(null) } if (p === '/api/platform/v1/suppliers/901') return response({ id: '901', status: supplierStatus }) if (p === '/api/platform/v1/last-leg/fleet/quotes/import') { const backup = { rows: copy(rows), histories: copy(histories), commands: copy(commands) }, result = [] for (let index = 0; index < body.rows.length; index++) { const outcome = create({ idempotencyKey: `${body.idempotencyKey}:${index + 1}`, details: body.rows[index] }, JSON.stringify(body.rows)) if (outcome.body.code !== '200') { if (fault !== 'partial import leak') { rows.clear(); histories.clear(); commands.clear(); backup.rows.forEach((v, k) => rows.set(k, v)); backup.histories.forEach((v, k) => histories.set(k, v)); backup.commands.forEach((v, k) => commands.set(k, v)) } return outcome } result.push(outcome.body.data) } return response({ importedRows: result.length, records: result }) } if (p === '/api/platform/v1/last-leg/fleet/quotes') { if (method === 'POST') return create(body) let records = [...rows.values()] for (const field of ['kind', 'status']) if (u.searchParams.get(field)) records = records.filter(r => (field === 'kind' ? r.details.kind : r.status) === u.searchParams.get(field)) const keyword = u.searchParams.get('keyword') || ''; records = records.filter(r => [r.details.originCity, r.details.destinationCode, r.quoteNo].some(v => v.includes(keyword))) if (fault === 'inactive leaks into active list' && u.searchParams.get('status') === 'ACTIVE') records.push([...rows.values()].find(r => r.status === 'INACTIVE')) return response({ records, total: String(records.length), page: '1', size: '200', pages: '1' }) } const quote = p.match(/\/quotes\/(FQ-\d+)(?:\/(history|status))?$/) if (quote) { const row = rows.get(quote[1]); assert.ok(row) if (method === 'GET') return response(quote[2] === 'history' ? histories.get(row.quoteNo) : row) if (String(body.recordVersion) !== row.recordVersion) { if (fault === 'stale write accepted' && method === 'PUT') { row.details = copy(body.details); return response(row) } return response(null, 'A0409') } if (method === 'PUT') row.details = copy(body.details); else row.status = body.status row.recordVersion = String(Number(row.recordVersion) + 1); snapshot(row, method === 'PUT' ? 'UPDATE' : body.status); return response(row) } if (p === '/api/platform/v1/last-leg/fleet/operations/TR-CURRENT/record/history') return response({ records: measuredHistory, total: String(measuredHistory.length), page: '1', size: '20', pages: '1' }) if (p === '/api/platform/v1/last-leg/fleet/operations/TR-CURRENT/record') { if (method === 'GET') return response(measured) const prior = measuredKeys.get(body.idempotencyKey) if (prior) { if (fault === 'measured replay adds history') measuredHistory.push(copy(measured)); return response(prior) } if (String(body.recordVersion) !== measured.recordVersion) return response(null, 'A0409') assert.deepEqual(body.details.evidenceFileIds, []); assert.equal(Object.hasOwn(body.details, 'evidenceFiles'), false) const { evidenceFileIds, ...values } = body.details measured = { tripNo, recordVersion: String(Number(measured.recordVersion) + 1), details: { ...copy(values), evidenceFiles: [] } } measuredKeys.set(body.idempotencyKey, copy(measured)); measuredHistory.unshift(copy(measured)); return response(measured) } if (p === '/api/platform/v1/last-leg/fleet/operations') { assert.equal(u.searchParams.get('keyword'), tripNo); assert.equal(u.searchParams.get('warehouseId'), '92') const metrics = { physicalPallets: 1, palletSpaces: 2, ownPackages: 11, externalPackages: 4, externalAgreedAmounts: [{ currency: 'USD', amount: '0.00' }] } const row = { tripNo, ...metrics, measured: measured.details, financeStatus: 'NOT_QUERIED', costs: { totals: [] }, costStatus: fault === 'unknown cost becomes zero' ? 'AVAILABLE' : 'NOT_RECORDED' } return response({ page: { records: [row], total: '1' }, summary: { trips: fault === 'counts destinations as trips' ? '2' : '1', ...metrics, costs: { visible: true, unrecordedTrips: 1 } }, charts: { physicalPallets: [{ points: [{ value: 1 }] }] } }) } throw new Error(`Unexpected offline HTTP: ${method} ${path}`) }, async requestBinary(method, path) { calls.push({ method, path }); const u = new URL(path, 'http://offline.invalid') assert.match(u.pathname, /files\/export-tasks\/[^/]+\/download$/) if (fault === 'json export masquerades as csv') return { http: 200, contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', bytes: Buffer.from('{"code":"A0400"}') } const task = exportTasks.get(u.pathname.split('/').at(-2)) const bytes = task.exportType === 'FLEET_QUOTE_LIST' ? exportWorkbook(['报价号'], [[...rows.keys()].join(' / ')]) : exportWorkbook(['车次号', '实际里程(mile)'], [tripNo, '25.5']) return { http: 200, body: null, bytes, contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', requestId: 'offline-request', traceId: 'offline-trace' } }, } return { context, rows, calls, supplierStatus: () => supplierStatus } } test('offline full flow checks quotes, atomic import, current departure and measured history without live network', async () => { const { context, rows, calls, supplierStatus } = harness(); await run(context); await verifyOperations(context, { writeMeasured: true, idempotencyNamespace: 'OPERATIONS-INDEPENDENT-KEY' }) const proof = context.createdResources.fleetQuoteAcceptance assert.equal(proof.status, 'VERIFIED'); assert.equal(proof.financePayment, 'NOT_TESTED'); assert.equal(proof.upstreamDeparture, 'NOT_TESTED') assert.equal(proof.operationsStatus, 'NOT_TESTED'); assert.equal(proof.quoteStatus, 'VERIFIED') assert.equal(context.createdResources.fleetOperationsAcceptance.operationsStatus, 'VERIFIED'); assert.equal(context.createdResources.fleetOperationsAcceptance.quoteStatus, 'NOT_TESTED') assert.equal(rows.size, 4); assert.ok([...rows.values()].every(r => r.status === 'INACTIVE')); assert.equal(supplierStatus(), 'INACTIVE') assert.ok(!calls.some(call => call.method === 'DELETE')); assert.ok(!JSON.stringify(proof).includes('password')) }) for (const fault of ['null price becomes zero', 'mutable creation replay', 'partial import leak', 'inactive leaks into active list', 'stale write accepted', 'counts destinations as trips', 'unknown cost becomes zero', 'measured replay adds history', 'json export masquerades as csv', 'historical trip']) { test(`verifier fails closed for ${fault}`, async () => { const { context } = harness(fault) const operations = ['counts destinations as trips', 'unknown cost becomes zero', 'measured replay adds history', 'historical trip'].includes(fault) await assert.rejects(operations ? verifyOperations(context, { writeMeasured: true, idempotencyNamespace: 'INDEPENDENT-KEY' }) : run(context)) assert.equal(context.createdResources[operations ? 'fleetOperationsAcceptance' : 'fleetQuoteAcceptance'].status, 'FAILED') }) } test('missing or stale fixture fails before any HTTP and cannot be treated as skipped success', async () => { for (const alter of [c => delete c.context.fleetQuotesOperationsFixture, c => { c.context.fleetQuotesOperationsFixture.marker = 'OLD' }, c => { c.context.fleetQuotesOperationsFixture.departureVerified = false }]) { const { context, calls } = harness(); alter(context); await assert.rejects(verifyOperations(context), /current-run/); assert.equal(calls.length, 0); assert.equal(context.createdResources.fleetOperationsAcceptance.status, 'FAILED') } const { context, calls } = harness(); context.createdResources.merchantQualificationFixture.marker = 'OLD' await assert.rejects(run(context), /current-run/); assert.equal(calls.length, 0) }) test('proof cannot be overwritten on rerun and CSV detector rejects misleading success content', async () => { const { context } = harness(); await run(context); const proof = context.createdResources.fleetQuoteAcceptance await assert.rejects(run(context), /Existing proof/); assert.equal(context.createdResources.fleetQuoteAcceptance, proof) assert.throws(() => parseXlsx(Buffer.from('{"code":"A0403"}')), /ZIP/) assert.throws(() => requireCurrentFixture({ marker: 'x' }), /current-run/) }) test('unknown expected money is rejected before HTTP instead of comparing as zero', async () => { const { context, calls } = harness(); context.context.fleetQuotesOperationsFixture.expected.externalAgreedAmounts[0].amount = null await assert.rejects(verifyOperations(context), /never convert unknown to zero/); assert.equal(calls.length, 0) }) test('quote acceptance is independent of DEPART and never requests operations, inventory or finance', async () => { const { context, calls } = harness(); delete context.context.fleetQuotesOperationsFixture await run(context) assert.equal(context.createdResources.fleetQuoteAcceptance.scope, 'QUOTES_ONLY') assert.equal(context.createdResources.fleetQuoteAcceptance.operationsStatus, 'NOT_TESTED') assert.ok(calls.every(call => !/operations|inventory|finance|\/trips\//.test(call.path))) }) test('operations only reads business data and creates download tasks, never customer, supplier or quote records', async () => { const { context, calls, rows } = harness(); delete context.createdResources.merchantQualificationFixture await verifyOperations(context) assert.ok(calls.every(call => call.method === 'GET' || (call.method === 'POST' && call.path === '/api/platform/v1/files/export-tasks' && call.body.exportType === 'TRIP_OPERATIONS_LIST'))); assert.equal(rows.size, 0) assert.equal(context.createdResources.fleetOperationsAcceptance.measuredStatus, 'NOT_TESTED') }) test('measured writes require an explicit independent idempotency namespace', async () => { const { context, calls } = harness(); await assert.rejects(verifyOperations(context, { writeMeasured: true }), /independent idempotency/) assert.equal(calls.length, 0) }) test('independent quote session logs out after a failed quote scenario and persists scoped failure without secrets', async () => { const offline = harness('null price becomes zero') const context = new AcceptanceRun({ environment: 'test', gateway: 'http://offline.invalid', username: 'test-user', password: 'synthetic-password', quiet: true }) const authCalls = [] context.request = async (method, path, body) => { if (path.startsWith('/api/auth/')) { authCalls.push(path) if (path.endsWith('/logout')) { assert.equal(body.refreshToken, 'synthetic-refresh'); return { http: 200, body: { code: '200', data: null } } } return { http: 200, body: { code: '200', data: { accessToken: 'synthetic-access', refreshToken: 'synthetic-refresh' } } } } return offline.context.request(method, path, body) } context.requestBinary = offline.context.requestBinary const report = await runQuoteSession(context, { marker: 'FLEET-QUOTE-20261003', prepareCustomer: async (current, options) => { assert.equal(options.publishEnvironment, false) current.createdResources.merchantQualificationFixture = { marker: current.marker, tenantId: '101' } } }) assert.deepEqual(authCalls, ['/api/auth/login', '/api/auth/logout']) assert.equal(report.success, false); assert.equal(report.loggedOut, true); assert.equal(report.operationsStatus, 'NOT_TESTED') assert.equal(report.createdResources.fleetQuoteAcceptance.status, 'FAILED') assert.equal(context.token, ''); assert.equal(context.refreshToken, '') assert.ok(!/synthetic-password|synthetic-access|synthetic-refresh/.test(JSON.stringify(report))) }) test('session helper rejects a borrowed login before touching HTTP', async () => { const context = new AcceptanceRun({ environment: 'test', quiet: true }); context.token = 'already-logged-in' context.request = () => { throw new Error('must not run') } await assert.rejects(runQuoteSession(context), /fresh TEST context/) })