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

132 lines
9.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const MODULE = 'finance-settlement-full'
const OPTIONS = { scenario: MODULE, deploymentBlocker: true }
const BASE = '/api/platform/finance/v1'
const id = (value) => value == null ? '' : String(value)
const data = (response) => response?.body?.data ?? response?.body
const ok = (response) => response?.http === 200 && (!response?.body?.code || String(response.body.code) === '200')
const key = (context, operation) => `${context.marker}-finance-settlement-${operation}`.replace(/[^A-Za-z0-9_.:-]/g, '-').slice(0, 128)
const write = (context, method, path, body, operation) => context.request(method, path, body, context.token, { 'Idempotency-Key': key(context, operation) })
const settlementId = (value) => id(value?.settlementId ?? value?.summary?.settlementId ?? value?.id)
const statementId = (value) => id(value?.statementId ?? value?.reconciliationStatementId ?? value?.id)
const outstanding = (value) => Number(value?.outstandingAmount ?? value?.summary?.outstandingAmount ?? 0)
const version = (value) => Number(value?.version ?? value?.summary?.version ?? 0)
export function values(value) {
if (Array.isArray(value)) return value.filter(Boolean)
if (!value || typeof value !== 'object') return []
if (Array.isArray(value.records)) return value.records.filter(Boolean)
return Object.entries(value).filter(([name, item]) => name !== 'marker' && item && typeof item === 'object').map(([, item]) => item)
}
function gap(context, name, scope, reason, category = 'finance-settlement-fixture-unavailable') {
context.addStep?.(MODULE, name, null, false, reason, OPTIONS)
context.addCoverageGap?.(MODULE, OPTIONS.scenario, scope, reason, category, { marker: context.marker })
}
function receiptBody(context, customerId, voucherFileId, allocations) {
return { customerId: Number(customerId), receivedDate: new Date().toISOString().slice(0, 10),
amount: allocations.reduce((sum, item) => sum + item.amount, 0), currency: 'CNY',
paymentMethod: 'BANK_TRANSFER', voucherFileIds: [voucherFileId], remark: context.marker, allocations }
}
async function detail(context, targetId) {
const response = await context.request('GET', `${BASE}/settlements/${targetId}`)
return { response, value: data(response) }
}
async function receive(context, fixtures, item, requestedAmount, operation, label) {
const targetId = settlementId(item)
const billId = statementId(item) || id(item.reconciliationStatementId)
const before = await detail(context, targetId)
const applied = Math.min(Number(requestedAmount), outstanding(before.value))
const response = await write(context, 'POST', `${BASE}/receipts`, receiptBody(context, fixtures.customerId,
fixtures.voucherFileId, [{ billId: Number(billId), amount: applied }]), operation)
const after = await detail(context, targetId)
context.assert(MODULE, label, response, ok(response) && ok(after.response)
&& outstanding(after.value) === outstanding(before.value) - applied,
`before=${outstanding(before.value)}, applied=${applied}, after=${outstanding(after.value)}`, OPTIONS)
return { receipt: data(response), settlement: after.value }
}
export async function run(context) {
const fixtures = context.createdResources?.financeFullFixtures
if (!fixtures || fixtures.marker !== context.marker) {
gap(context, '取得本轮财务 Fixture', 'financeFullFixtures', '缺少匹配 marker 的 financeFullFixtures,禁止借历史单据')
return
}
const statements = Array.isArray(fixtures.statements) ? fixtures.statements.filter(Boolean) : []
const receivables = Array.isArray(fixtures.receivableSettlements) ? fixtures.receivableSettlements.filter(Boolean) : []
const payables = Array.isArray(fixtures.payableSettlements) ? fixtures.payableSettlements.filter(Boolean) : []
const customerId = id(fixtures.customerId)
const voucherFileId = id(fixtures.voucherFileId ?? fixtures.voucher?.fileId ?? fixtures.voucher?.id)
if (!customerId || !voucherFileId) {
gap(context, '取得真实财务凭证', 'voucher-file', '前置 Fixture 未提供 File 服务创建且归属本客户的 FINANCE_VOUCHER fileId,收付款必须 fail-closed', 'missing-finance-voucher')
return
}
const issuedStatementIds = new Set(statements.filter((item) => item.status === 'ISSUED').map(statementId).filter(Boolean))
const validReceivables = receivables.filter((item) => item.direction === 'RECEIVABLE'
&& settlementId(item) && statementId(item) && issuedStatementIds.has(statementId(item)))
const validPayables = payables.filter((item) => item.direction === 'PAYABLE' && settlementId(item) && item.supplierId)
const arSettlementIds = new Set(validReceivables.map(settlementId))
const arStatementIds = new Set(validReceivables.map(statementId))
if (validReceivables.length !== 3 || arSettlementIds.size !== 3 || arStatementIds.size !== 3 || validPayables.length !== 1) {
gap(context, '取得完整收付结算 Fixture', 'three AR and one AP settlements',
`需要 3 张关联本轮 ISSUED 对账单的独立 AR 和 1 张带 supplierId 的 AP;actual AR=${validReceivables.length}, AP=${validPayables.length}`,
'incomplete-settlement-fixture')
return
}
const manualIds = values(fixtures.feeSources).filter((fee) => fee.sourceType === 'MANUAL').map((fee) => id(fee.feeSourceId))
const usedIds = new Set(statements.flatMap((statement) => values(statement.feeSourceIds).map(id)))
context.assert(MODULE, '手动上传费用出账后来源完结', null,
manualIds.length > 0 && manualIds.every((feeId) => usedIds.has(feeId)),
`manual=${manualIds.length}, settled=${manualIds.filter((feeId) => usedIds.has(feeId)).length}`, OPTIONS)
const multiTargets = validReceivables
if (multiTargets.length < 3) gap(context, '取得三张应收结算单', 'multi receipt allocation', '本轮没有三张独占应收单,禁止借历史单据')
else {
const details = await Promise.all(multiTargets.map((item) => detail(context, settlementId(item))))
const allocations = multiTargets.map((item, index) => ({ billId: Number(statementId(item)), amount: Math.min(Number(fixtures.multiBillAllocationAmount || 1), outstanding(details[index].value)) }))
const response = await write(context, 'POST', `${BASE}/receipts`, receiptBody(context, customerId, voucherFileId, allocations), 'multi-bill-receipt')
const after = await Promise.all(multiTargets.map((item) => detail(context, settlementId(item))))
context.assert(MODULE, '一笔收款分配多张应收单', response, ok(response) && after.every((item, index) =>
outstanding(item.value) === outstanding(details[index].value) - allocations[index].amount), `allocations=${allocations.length}`, OPTIONS)
fixtures.receipts = { ...(fixtures.receipts || {}), multi: data(response) }
}
const primary = validReceivables[0]
if (!primary) gap(context, '取得应收结算单', 'receivable partial/full', 'receivableSettlements 未提供 settlementId 与 statementId')
else {
const initial = await detail(context, settlementId(primary))
const partialAmount = Number(fixtures.receivablePartialAmount || Math.max(0.01, Math.floor(outstanding(initial.value) * 50) / 100))
const partial = await receive(context, fixtures, primary, partialAmount, 'ar-partial-receipt', '应收部分收款更新余额和状态')
context.assert(MODULE, '应收部分收款状态正确', null,
['PARTIALLY_PAID', 'PAID'].includes(id(partial.settlement?.status ?? partial.settlement?.paymentStatus)),
`status=${id(partial.settlement?.status ?? partial.settlement?.paymentStatus)}`, OPTIONS)
const final = await receive(context, fixtures, primary, outstanding(partial.settlement), 'ar-final-receipt', '应收全额收款完结')
fixtures.receipts = { ...(fixtures.receipts || {}), partial: partial.receipt, final: final.receipt }
fixtures.settlementFinalStates = { ...(fixtures.settlementFinalStates || {}), receivable: final.settlement }
}
const payable = validPayables[0]
const payableId = settlementId(payable)
if (!payableId) gap(context, '取得应付结算单', 'payable partial/full/overpayment', '前置 Fixture 的 payableSettlements 数组为空或缺少 PAYABLE 结算单')
else {
let current = (await detail(context, payableId)).value
const payment = (payAmount, expectedVersion) => ({ transactionDate: new Date().toISOString().slice(0, 10), amount: payAmount,
currency: 'CNY', method: 'BANK_TRANSFER', referenceNo: `${context.marker}-AP`, voucherFileIds: [voucherFileId], expectedVersion, remark: context.marker })
const firstAmount = Number(fixtures.payablePartialAmount || Math.max(0.01, Math.floor(outstanding(current) * 50) / 100))
const partialResponse = await write(context, 'POST', `${BASE}/settlements/${payableId}/transactions`, payment(firstAmount, version(current)), 'ap-partial-payment')
current = data(partialResponse)
context.assert(MODULE, '应付部分付款更新余额和状态', partialResponse, ok(partialResponse) && outstanding(current) >= 0, `outstanding=${outstanding(current)}`, OPTIONS)
const finalResponse = await write(context, 'POST', `${BASE}/settlements/${payableId}/transactions`, payment(outstanding(current), version(current)), 'ap-final-payment')
current = data(finalResponse)
context.assert(MODULE, '应付付清完结', finalResponse, ok(finalResponse) && outstanding(current) === 0, `status=${id(current?.status)}`, OPTIONS)
const overResponse = await write(context, 'POST', `${BASE}/settlements/${payableId}/transactions`, payment(0.01, version(current)), 'ap-overpayment')
context.assert(MODULE, '应付超额付款拦截', overResponse, !ok(overResponse), `HTTP=${overResponse.http}, code=${id(overResponse.body?.code)}`, OPTIONS)
fixtures.payments = { ...(fixtures.payments || {}), partial: data(partialResponse), final: current, overpayment: { http: overResponse.http, code: overResponse.body?.code } }
fixtures.settlementFinalStates = { ...(fixtures.settlementFinalStates || {}), payable: current }
}
}