155 lines
12 KiB
JavaScript
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')
|
|
})
|
|
}
|