Files
baiyue-firstleg/scripts/admin-test-env-modules/finance-settlement-full-contract.test.mjs
T
2026-08-12 12:18:59 +08:00

103 lines
6.6 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { run, values } from './finance-settlement-full.mjs'
const response = (data, http = 200, code = '200') => ({ http, body: { code, data } })
function fakeContext(overrides = {}) {
const balances = new Map([['101', 100], ['102', 100], ['103', 100], ['201', 100]])
const settlementToBill = new Map([['101', '11'], ['102', '12'], ['103', '13']])
const billToSettlement = new Map([...settlementToBill].map(([s, b]) => [b, s]))
const fixture = { marker: 'T1', customerId: '9', voucherFileId: '77',
feeSources: [{ feeSourceId: '501', sourceType: 'MANUAL' }],
statements: [
{ kind: 'BY_CUTOFF', statementId: '10', status: 'VOIDED', feeSourceIds: [] },
{ kind: 'SELECTED_FULL_CHAIN', statementId: '11', status: 'ISSUED', feeSourceIds: ['501'] },
{ kind: 'SELECTED', statementId: '12', status: 'ISSUED', feeSourceIds: [] },
{ kind: 'SELECTED', statementId: '13', status: 'ISSUED', feeSourceIds: [] },
],
receivableSettlements: [
{ settlementId: '101', statementId: '11', direction: 'RECEIVABLE', status: 'ISSUED' },
{ settlementId: '102', statementId: '12', direction: 'RECEIVABLE', status: 'ISSUED' },
{ settlementId: '103', statementId: '13', direction: 'RECEIVABLE', status: 'ISSUED' },
],
payableSettlements: [{ settlementId: '201', direction: 'PAYABLE', supplierId: '301', status: 'ISSUED' }],
receivablePartialAmount: 40, payablePartialAmount: 40, multiBillAllocationAmount: 10,
...overrides }
const calls = []; const assertions = []; const gaps = []; const steps = []
return { marker: 'T1', token: 'token', createdResources: { financeFullFixtures: fixture }, calls, assertions, gaps, steps,
addStep(module, name, response, passed, evidence, options) { steps.push({ module, name, passed, evidence, options }) }, addCoverageGap(...args) { gaps.push(args) },
assert(module, name, res, condition) { assertions.push({ name, condition }); assert.equal(condition, true, name) },
async request(method, path, body, token, headers) {
calls.push({ method, path, body, headers })
const match = path.match(/\/settlements\/(\d+)/)
if (method === 'GET' && match) return response({ settlementId: match[1], outstandingAmount: balances.get(match[1]), status: balances.get(match[1]) === 0 ? 'PAID' : 'PARTIALLY_PAID', version: calls.length })
if (path.endsWith('/receipts')) { for (const allocation of body.allocations) balances.set(billToSettlement.get(String(allocation.billId)), balances.get(billToSettlement.get(String(allocation.billId))) - allocation.amount); return response({ receiptId: calls.length, allocations: body.allocations }) }
if (match && path.endsWith('/transactions')) {
const current = balances.get(match[1]); if (body.amount > current || current === 0) return response(null, 409, 'FIN_409')
balances.set(match[1], current - body.amount); return response({ settlementId: match[1], outstandingAmount: current - body.amount, status: current - body.amount === 0 ? 'PAID' : 'PARTIALLY_PAID', version: calls.length })
}
throw new Error(`unexpected ${method} ${path}`)
} }
}
test('values helper supports nested arrays while run consumes frozen top-level arrays', () => {
assert.deepEqual(values([{ id: 1 }]), [{ id: 1 }])
assert.deepEqual(values({ records: [{ id: 2 }] }), [{ id: 2 }])
assert.deepEqual(values({ marker: 'x', selected: { id: 3 } }), [{ id: 3 }])
})
test('run 使用 receipts 完成应收部分、全额及多单分配,使用 transactions 完成 AP', async () => {
const context = fakeContext()
await run(context)
const receiptCalls = context.calls.filter((call) => call.path.endsWith('/receipts'))
assert.equal(receiptCalls.length, 3)
assert.deepEqual(Object.keys(receiptCalls[0].body.allocations[0]).sort(), ['amount', 'billId'])
assert.equal(receiptCalls.find((call) => call.body.allocations.length === 3).body.allocations.length, 3)
const payments = context.calls.filter((call) => call.path.includes('/settlements/201/transactions'))
assert.equal(payments.length, 3)
assert.ok(payments.every((call) => call.body.method === 'BANK_TRANSFER' && call.body.transactionDate && call.body.expectedVersion != null))
assert.ok(context.calls.filter((call) => call.method === 'POST').every((call) => call.headers['Idempotency-Key'].startsWith('T1-')))
assert.ok(context.createdResources.financeFullFixtures.receipts.final)
assert.ok(context.createdResources.financeFullFixtures.payments.final)
assert.equal(context.createdResources.financeFullFixtures.settlementFinalStates.receivable.outstandingAmount, 0)
assert.equal(context.createdResources.financeFullFixtures.settlementFinalStates.payable.outstandingAmount, 0)
})
test('冻结共享输入使用 4 条 statements、3 条 AR 和 payableSettlements 数组', async () => {
const context = fakeContext()
assert.equal(context.createdResources.financeFullFixtures.statements.length, 4)
assert.equal(context.createdResources.financeFullFixtures.receivableSettlements.length, 3)
assert.equal(context.createdResources.financeFullFixtures.payableSettlements.length, 1)
await run(context)
assert.equal(context.gaps.length, 0)
})
test('缺 fixture 或真实 voucher 时记录 blocker 且不写接口', async () => {
const missing = fakeContext(); delete missing.createdResources.financeFullFixtures
await run(missing); assert.equal(missing.gaps.length, 1); assert.equal(missing.calls.length, 0)
assert.equal(missing.steps.length, 1); assert.equal(missing.steps[0].passed, false)
assert.equal(missing.steps[0].options.deploymentBlocker, true)
const noVoucher = fakeContext({ voucherFileId: null })
await run(noVoucher); assert.equal(noVoucher.gaps.length, 1); assert.equal(noVoucher.calls.length, 0)
assert.equal(noVoucher.steps.length, 1); assert.equal(noVoucher.steps[0].passed, false)
assert.equal(noVoucher.steps[0].options.deploymentBlocker, true)
})
test('AR/AP 数组为空或不完整时原子阻断且不查询、不写入', async () => {
const empty = fakeContext({ statements: [], receivableSettlements: [], payableSettlements: [] })
await run(empty)
assert.equal(empty.calls.length, 0)
assert.equal(empty.gaps.length, 1)
assert.equal(empty.steps[0].passed, false)
assert.match(empty.steps[0].evidence, /actual AR=0, AP=0/)
const partial = fakeContext({ receivableSettlements: [
{ settlementId: '101', statementId: '11', direction: 'RECEIVABLE', status: 'ISSUED' },
] })
await run(partial)
assert.equal(partial.calls.length, 0)
assert.equal(partial.gaps.length, 1)
assert.match(partial.steps[0].evidence, /actual AR=1, AP=1/)
})