Files
baiyue-firstleg/scripts/admin-test-env-modules/fleet-company-cost-acceptance-contract.test.mjs
T

155 lines
12 KiB
JavaScript

import assert from 'node:assert/strict'
import test from 'node:test'
import { run } from './fleet-company-cost-acceptance.mjs'
import { requireReady, READY_COMMIT } from '../fleet-company-cost-test-env.mjs'
const copy = value => value == null ? value : structuredClone(value)
const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aH1kAAAAASUVORK5CYII=', 'base64')
function fixture(fault) {
const marker = 'CONTROLLED-COMPANY-COST', calls = [], vehicles = new Map(), records = new Map(), costs = new Map(), submissions = new Map(), commands = new Map(), files = new Map()
const response = (data, code = '200', http = 200) => ({ http, body: { code, data: copy(data) }, requestId: 'controlled-request', traceId: 'controlled-trace' })
let sequence = 0
const history = row => ({ recordVersion: row.cost.recordVersion, action: row.action, snapshot: copy(row.cost) })
function save(row, action) { row.action = action; row.history.unshift(history(row)) }
function current(row) { const { cost } = row; return copy(cost) }
let supplier
const context = { marker, token: 'offline-test-only', createdResources: {},
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, raw, body, _token, headers) {
calls.push({ method, raw, body: copy(body), headers: copy(headers) }); const url = new URL(raw, 'https://fixture.invalid'), path = url.pathname
if (method === 'POST' && path === '/api/platform/v1/suppliers') { supplier = { id: '8', ...copy(body) }; return response('8') }
if (method === 'GET' && path === '/api/platform/v1/suppliers/8') return response(supplier)
if (method === 'GET' && path.includes('/master-data/exchange-rates/')) return response({ month: path.split('/').at(-1), status: fault === 'inactive exchange' ? 'INACTIVE' : 'ACTIVE', usdToCnyAverage: '7.123456', recordVersion: 0 })
if (path.endsWith('/fee-pool')) return response(fault === 'customer pollution' && costs.size ? [{ poolItemId: 'LEAK', customerId: '9', amount: 27.31, feeItemName: marker }] : [])
if (path.endsWith('/cost-entries')) return response([])
if (path.endsWith('/customer-accounts/9/summary')) return response({ customerId: '9', currency: 'CNY', totalReceivable: 100, receivedAmount: 30, outstandingReceivable: 70,
pendingBilling: 0, totalCost: 0, paidCost: 0, outstandingPayable: 0, asOf: new Date().toISOString() })
if (path === '/api/platform/v1/last-leg/master-data/vehicles' && method === 'POST') {
const row = { id: '71', ...copy(body), recordVersion: 0 }; vehicles.set(row.id, row); return response(row)
}
if (path.startsWith('/api/platform/v1/last-leg/master-data/vehicles/')) {
const row = vehicles.get(path.split('/vehicles/')[1].split('/')[0]); assert.ok(row)
if (method === 'GET') return response(row)
assert.equal(method, 'PATCH'); assert.equal(body.status, 'INACTIVE')
if (fault === 'cleanup failure') return response(null, 'S0503', 503)
row.status = 'INACTIVE'; row.recordVersion++; return response(row)
}
const care = path.match(/^\/api\/platform\/v1\/last-leg\/fleet\/vehicles\/71\/records(?:\/(.*))?$/)
if (care) {
const suffix = care[1]
if (!suffix && method === 'POST') {
const recordNo = `VR-${records.size + 1}`, row = { recordNo, vehicleId: '71', vehiclePlate: vehicles.get('71').plateNumber, recordVersion: 0, details: copy(body.details) }
records.set(recordNo, row); return response(row)
}
if (suffix === 'finance-status') {
return response(body.recordNos.map(recordNo => {
const record = records.get(recordNo), row = costs.get(recordNo); assert.ok(record && row)
if (fault === 'delivery exhausted') return { recordNo, status: 'FINANCE_ATTENTION', sourceVersion: row.cost.source.sourceVersion }
if (fault === 'never delivered') return { recordNo, status: 'PENDING_FINANCE', sourceVersion: row.cost.source.sourceVersion }
const cost = current(row)
if (fault === 'wrong source vehicle') cost.source.vehicleId = 'OTHER'
if (fault === 'wrong source version') cost.source.sourceVersion = 999
if (fault === 'wrong conversion') cost.source.amount = 999
if (fault === 'premature paid') cost.status = 'PAID'
return { recordNo, status: cost.status, sourceVersion: cost.source.sourceVersion, submittedRecordVersion: record.recordVersion, finance: cost }
}))
}
const [recordNo, action] = suffix.split('/'), record = records.get(recordNo); assert.ok(record)
if (method === 'GET') return response(record)
if (method === 'PUT') {
if (fault === 'paid source mutable') { record.details = copy(body.details); record.recordVersion++; return response(record) }
return response(null, 'A0409')
}
assert.equal(action, 'finance')
const key = `${recordNo}:${body.idempotencyKey}`
if (submissions.has(key)) return response({ ...submissions.get(key), replayed: true })
let row = costs.get(recordNo), sourceVersion = row ? row.cost.source.sourceVersion + 1 : 1
if (row && row.cost.status !== 'VOIDED') return response(null, 'A0409')
const incident = record.details.incident, rate = incident.currency === 'CNY' ? 1 : 7.123456
const source = { sourceType: record.details.recordType, sourceNo: recordNo, sourceVersion, vehicleId: '71', vehiclePlate: record.vehiclePlate,
supplierId: body.supplierId, feeName: incident.item, businessDate: body.businessDate, originalCurrency: incident.currency,
originalAmount: incident.reportedCost, exchangeRate: rate, exchangeRateMonth: body.businessDate.slice(0, 7),
amount: Math.round(incident.reportedCost * rate * 100) / 100, remark: record.details.remark }
const cost = { costNo: row?.cost.costNo ?? `VC-${++sequence}`, subjectCode: 'PLATFORM_DEFAULT', currency: 'CNY', status: 'PENDING', paidAmount: 0, outstandingAmount: null,
recordVersion: row ? row.cost.recordVersion + 1 : 0, source }
if (!row) row = { cost, payments: [], history: [] }
else row.cost = cost
save(row, 'RECEIVE'); costs.set(recordNo, row)
const result = { recordNo, sourceVersion, submittedRecordVersion: record.recordVersion, replayed: false, status: 'PENDING_FINANCE' }
submissions.set(key, copy(result)); return response(result)
}
const match = path.match(/^\/api\/platform\/finance\/v1\/vehicle-costs\/([^/]+)(?:\/(.*))?$/)
assert.ok(match, `Unexpected ${method} ${raw}`)
const [, no, action] = match, row = [...costs.values()].find(r => r.cost.costNo === no); assert.ok(row)
if (method === 'GET') {
if (!action) return response(current(row))
const list = action === 'payments' ? row.payments : row.history
return response({ records: list, page: 1, size: 100, total: list.length, pages: list.length ? 1 : 0 })
}
const key = `${no}:${action}:${headers?.['Idempotency-Key']}`
assert.ok(headers?.['Idempotency-Key'], 'financial commands require their stable header key')
if (commands.has(key) && !(fault === 'duplicate payment replay' && action === 'payments')) return response(current(row))
if (Number(body.expectedVersion) !== row.cost.recordVersion && !(fault === 'duplicate payment replay' && action === 'payments')) return response(null, 'FIN_409')
if (action === 'approve') { row.cost.status = 'APPROVED'; row.cost.outstandingAmount = row.cost.source.amount; row.cost.recordVersion++; save(row, 'APPROVE') }
else if (action === 'void') {
if (row.cost.paidAmount > 0 && fault !== 'paid void allowed') return response(null, 'FIN_409')
row.cost.status = 'VOIDED'; row.cost.outstandingAmount = null; row.cost.recordVersion++; save(row, 'VOID')
} else if (action === 'payments') {
if (body.amount > row.cost.outstandingAmount && fault !== 'overpayment allowed') return response(null, 'FIN_409')
row.cost.paidAmount = Math.round((row.cost.paidAmount + body.amount) * 100) / 100
row.cost.outstandingAmount = Math.round((row.cost.source.amount - row.cost.paidAmount) * 100) / 100
row.cost.status = row.cost.outstandingAmount === 0 ? 'PAID' : 'PARTIALLY_PAID'; row.cost.recordVersion++
row.payments.push({ paymentNo: `VP-${++sequence}`, currency: 'CNY', amount: body.amount, voucherFileIds: copy(body.voucherFileIds) }); save(row, 'PAY')
if (fault === 'partial money drift') row.cost.outstandingAmount += 0.01
} else throw new Error(`Unexpected action ${action}`)
commands.set(key, true); return response(current(row))
},
async requestMultipart(method, raw, file) {
calls.push({ method, raw, multipart: true }); assert.ok(Buffer.from(file.content).equals(PNG))
if (fault === 'file outage') return response(null, 'FIN_503', 503)
const fileId = id(files.size + 1); files.set(fileId, raw)
return response({ fileId, ownerScope: 'PLATFORM', tenantId: '0', businessType: 'FINANCE_VEHICLE_VOUCHER', contentType: 'image/png', sizeBytes: PNG.length })
},
async requestBinary(method, raw) { calls.push({ method, raw, binary: true }); return { http: 200, contentType: 'image/png', bytes: fault === 'wrong file bytes' ? Buffer.from('wrong') : PNG } },
}
return { context, calls, vehicles, records, costs }
}
const id = value => String(value)
const options = { pollAttempts: 2, pollDelayMs: 0 }
test('independent runner requires explicit service readiness and only the specified TEST profile', () => {
const env = 'C:/Users/BY-OY/.codex/env/chongchong-global/test-acceptance.env'
assert.throws(() => requireReady([], [`--env-file=${env}`]), /就绪/)
assert.throws(() => requireReady([`--ready-commit=${READY_COMMIT}`], [`--env-file=${env.replace('test-acceptance', 'dev-acceptance')}`]), /授权目录/)
assert.ok(requireReady([`--ready-commit=${READY_COMMIT}`], [`--env-file=${env}`]).endsWith('test-acceptance.env'))
})
test('company cost verifier exercises CNY/USD, void/rebuild, payment replay and customer isolation without direct transport', async () => {
const { context, calls, vehicles, records, costs } = fixture()
await run(context, options)
const proof = context.createdResources.fleetCompanyCostAcceptance
assert.equal(proof.status, 'VERIFIED'); assert.equal(proof.cleanup, 'INACTIVE')
assert.equal(proof.cases.length, 2); assert.equal(records.size, 2)
assert.ok([...costs.values()].every(row => row.cost.status === 'PAID' && row.payments.length === 2))
assert.ok([...vehicles.values()].every(row => row.status === 'INACTIVE'))
assert.ok(calls.every(call => call.raw.startsWith('/api/platform/')))
assert.equal(calls.some(call => /cost-entries|customer-accounts|customerId=/.test(call.raw)), false, 'must not borrow another task customer for evidence')
assert.equal(calls.some(call => call.method === 'DELETE'), false)
await assert.rejects(run(context, options), /本轮已存在/)
})
test('missing independent marker blocks before any write', async () => {
const { context, calls } = fixture(); context.marker = ''
await assert.rejects(run(context, options), /独立marker/); assert.equal(calls.length, 0)
})
for (const fault of ['inactive exchange', 'delivery exhausted', 'never delivered', 'wrong source vehicle', 'wrong source version', 'wrong conversion', 'premature paid',
'duplicate payment replay', 'overpayment allowed', 'partial money drift', 'paid void allowed', 'paid source mutable', 'customer pollution', 'file outage', 'wrong file bytes', 'cleanup failure']) {
test(`company cost verifier rejects ${fault} and preserves failure evidence`, async () => {
const { context, vehicles } = fixture(fault)
await assert.rejects(run(context, options))
const proof = context.createdResources.fleetCompanyCostAcceptance
assert.equal(proof.status, 'FAILED')
assert.ok(proof.error || proof.cleanupError)
if (vehicles.size && fault !== 'cleanup failure') assert.ok([...vehicles.values()].every(row => row.status === 'INACTIVE'))
if (fault === 'cleanup failure') assert.equal(proof.cleanup, 'FAILED')
})
}