优化
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url))
|
||||
const source = fs.readFileSync(path.join(here, 'finance-demo-fixtures.mjs'), 'utf8')
|
||||
|
||||
test('演示数据只消费本轮固定 marker 和客户 10001', () => {
|
||||
assert.match(source, /const MARKER = 'FIN-DEMO-20260812'/)
|
||||
assert.match(source, /fixture\.marker !== runMarker/)
|
||||
assert.match(source, /fixture\.customerId !== '10001'/)
|
||||
assert.match(source, /const runMarker = `\$\{MARKER\}-/)
|
||||
assert.match(source, /禁止借历史来源/)
|
||||
assert.doesNotMatch(source, /DELETE|cleanup|remove/i)
|
||||
})
|
||||
|
||||
test('覆盖费用、对账单、AR 和 AP 页面分层状态', () => {
|
||||
for (const value of ['pendingReview', 'approvedUnbilled', 'draft', 'arUnpaid', 'arPartial', 'arPaid',
|
||||
'apUnpaid', 'apPartial', 'apPaid']) assert.match(source, new RegExp(value))
|
||||
for (const status of ['DRAFT', 'PENDING_REVIEW', 'ISSUED']) assert.match(source, new RegExp(`'${status}'`))
|
||||
assert.match(source, /reviewStatus === 'PENDING'/)
|
||||
assert.match(source, /reviewStatus === 'APPROVED'/)
|
||||
assert.match(source, /settlementIncluded === true/)
|
||||
})
|
||||
|
||||
test('平台列表默认全量查询不带 customerId,未部署时登记 blocker', () => {
|
||||
assert.match(source, /\['statements', 'receivable-settlements', 'payable-settlements', 'receipts', 'cost-entries'\]/)
|
||||
assert.match(source, /context\.request\('GET', `\$\{BASE\}\/\$\{path\}`\)/)
|
||||
assert.match(source, /finance-unfiltered-list-not-deployed/)
|
||||
})
|
||||
|
||||
test('所有财务写操作使用 marker 幂等键并上传真实凭证', () => {
|
||||
assert.match(source, /'Idempotency-Key': key\(context, operation\)/)
|
||||
assert.match(source, /fixture\.voucherFileId/)
|
||||
assert.match(source, /finance-full-fixtures\.mjs/)
|
||||
assert.match(source, /context\.createdResources\.financeDemo = matrix/)
|
||||
assert.match(source, /`source-\$\{role\}-\$\{source\.feeSourceId\}-include`/)
|
||||
assert.match(source, /`ap-\$\{role\}-\$\{source\.feeSourceId\}-create`/)
|
||||
})
|
||||
|
||||
test('额外应付审核路径先追加 feeId 再追加 accountId 查询参数', () => {
|
||||
assert.match(source, /`\$\{basePath\}\/\$\{created\.id\}\/approve\?accountId=10001`/)
|
||||
assert.doesNotMatch(source, /fee-details\?accountId=10001\/\$\{created\.id\}/)
|
||||
})
|
||||
|
||||
test('费用来源使用 operationVersion,应付供应商长整型保持字符串精度', () => {
|
||||
assert.match(source, /expectedVersion: operationVersion\(current\.value\)/)
|
||||
assert.match(source, /supplierId: id\(source\.supplierId\)/)
|
||||
assert.doesNotMatch(source, /supplierId: Number\(source\.supplierId\)/)
|
||||
})
|
||||
|
||||
test('恢复模式只续补固定报告来源并校验 AR AP 金额状态', () => {
|
||||
assert.match(source, /process\.env\.FINANCE_DEMO_RECOVERY_REPORT/)
|
||||
assert.match(source, /恢复报告不是 FIN-DEMO-20260812/)
|
||||
assert.match(source, /拒绝借历史数据/)
|
||||
assert.match(source, /supplierId: id\(fixture\.supplier\?\.supplierId\)/)
|
||||
assert.match(source, /const payableIds = \['307', '310'\]/)
|
||||
assert.match(source, /finance-container-partner-not-deployed/)
|
||||
assert.match(source, /findPayableBySource/)
|
||||
assert.match(source, /approved\?\.statementGenerated === true/)
|
||||
assert.match(source, /finance-settlement-payment-status-not-deployed/)
|
||||
assert.match(source, /\['arUnpaid', 'UNPAID'\]/)
|
||||
assert.match(source, /\['arPartial', 'PARTIALLY_PAID'\]/)
|
||||
assert.match(source, /\['arPaid', 'PAID'\]/)
|
||||
})
|
||||
@@ -0,0 +1,360 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
|
||||
const MODULE = 'finance-demo-fixtures'
|
||||
const MARKER = 'FIN-DEMO-20260812'
|
||||
const BASE = '/api/platform/finance/v1'
|
||||
const OPTIONS = { scenario: 'finance-demo-layered-data', deploymentBlocker: true }
|
||||
|
||||
const id = (value) => value == null ? '' : String(value)
|
||||
const rows = (value) => Array.isArray(value) ? value : Array.isArray(value?.records) ? value.records : []
|
||||
const data = (response) => response?.body?.data ?? null
|
||||
const ok = (response) => response?.http === 200 && String(response?.body?.code) === '200'
|
||||
const statementId = (value) => id(value?.statementId ?? value?.id)
|
||||
const settlementId = (value) => id(value?.settlementId ?? value?.id)
|
||||
const outstanding = (value) => Number(value?.outstandingAmount ?? 0)
|
||||
const operationVersion = (value) => Number(value?.operationVersion ?? 0)
|
||||
const version = (value) => Number(value?.version ?? 0)
|
||||
const key = (context, operation) => `${context.marker}-${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) })
|
||||
|
||||
function record(context, name, response, condition, evidence) {
|
||||
return context.assert(MODULE, name, response, condition, evidence, OPTIONS)
|
||||
}
|
||||
|
||||
function blocker(context, name, response, evidence, category) {
|
||||
context.addStep(MODULE, name, response, false, evidence, OPTIONS)
|
||||
context.addCoverageGap(MODULE, OPTIONS.scenario, name, evidence, category, {
|
||||
marker: context.marker, http: response?.http ?? null, code: response?.body?.code ?? null,
|
||||
requestId: response?.requestId ?? null, traceId: response?.traceId ?? null,
|
||||
})
|
||||
}
|
||||
|
||||
async function waitForSources(context, sourceFeeIds) {
|
||||
let pool = []
|
||||
for (let attempt = 0; attempt < 30; attempt += 1) {
|
||||
const response = await context.request('GET', `${BASE}/fee-pool?customerId=10001&direction=ALL`)
|
||||
pool = rows(data(response)).concat(rows(data(response)?.items))
|
||||
const visible = new Set(pool.map((item) => id(item.sourceId)))
|
||||
if (sourceFeeIds.every((sourceId) => visible.has(sourceId))) return pool
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
}
|
||||
return pool
|
||||
}
|
||||
|
||||
function financeId(poolItem) {
|
||||
const value = id(poolItem?.poolItemId)
|
||||
return value.includes(':') ? value.slice(value.indexOf(':') + 1) : id(poolItem?.feeSourceId ?? poolItem?.id)
|
||||
}
|
||||
|
||||
async function sourceDetail(context, feeSourceId) {
|
||||
const response = await context.request('GET', `${BASE}/fee-sources/${feeSourceId}`)
|
||||
return { response, value: data(response) }
|
||||
}
|
||||
|
||||
async function approveAndInclude(context, source, role) {
|
||||
let current = await sourceDetail(context, source.feeSourceId)
|
||||
if (current.value?.reviewStatus === 'PENDING') {
|
||||
const response = await write(context, 'POST', `${BASE}/fee-sources/${source.feeSourceId}/review`,
|
||||
{ action: 'APPROVE', reason: `${MARKER}-${role}`, expectedVersion: operationVersion(current.value) }, `source-${role}-${source.feeSourceId}-approve`)
|
||||
current = { response, value: data(response) }
|
||||
}
|
||||
if (current.value?.settlementIncluded !== true) {
|
||||
const response = await write(context, 'PUT', `${BASE}/fee-sources/${source.feeSourceId}/settlement-inclusion`,
|
||||
{ included: true, reason: `${MARKER}-${role}`, expectedVersion: operationVersion(current.value) }, `source-${role}-${source.feeSourceId}-include`)
|
||||
current = { response, value: data(response) }
|
||||
}
|
||||
record(context, `${role} 来源已审核并纳入结算`, current.response,
|
||||
current.value?.reviewStatus === 'APPROVED' && current.value?.settlementIncluded === true
|
||||
&& current.value?.sourceStatus === 'AVAILABLE',
|
||||
`feeSourceId=${source.feeSourceId}, review=${current.value?.reviewStatus}, included=${current.value?.settlementIncluded}`)
|
||||
return current.value
|
||||
}
|
||||
|
||||
async function createExtraPayable(context, fixture, index) {
|
||||
const itemResponse = await context.request('GET', '/api/platform/v1/fee-items?page=1&pageSize=200')
|
||||
const feeItem = rows(data(itemResponse)).find((item) => item.status === 'ACTIVE' && item.warehousePayable === true)
|
||||
const inboundId = id(fixture.orders.at(-1)?.inboundId)
|
||||
const partnerId = id(fixture.supplier?.partnerId ?? fixture.supplier?.supplierId)
|
||||
if (!feeItem || !inboundId || !partnerId) return null
|
||||
const body = { feeItemId: Number(feeItem.id), direction: 'PAYABLE', businessDate: new Date().toISOString().slice(0, 10),
|
||||
unitPrice: 90 + index * 10, amount: 90 + index * 10, currency: 'CNY', settlementIncluded: true,
|
||||
partnerId: Number(partnerId), partnerName: fixture.supplier?.supplierName || null, remark: `${MARKER}-AP-${index}` }
|
||||
const basePath = `/api/platform/v1/inbounds/${inboundId}/fee-details`
|
||||
const createdResponse = await write(context, 'POST', `${basePath}?accountId=10001`, body, `extra-payable-${index}`)
|
||||
const created = data(createdResponse)
|
||||
if (!ok(createdResponse) || !created?.id) return null
|
||||
const approveResponse = await write(context, 'POST', `${basePath}/${created.id}/approve?accountId=10001`, undefined, `extra-payable-${index}-approve`)
|
||||
record(context, `创建第 ${index} 条额外应付来源`, approveResponse, ok(approveResponse),
|
||||
`sourceFeeId=${created.id}, inboundId=${inboundId}, partnerId=${partnerId}`)
|
||||
return { sourceFeeId: id(created.id), direction: 'PAYABLE', businessDate: body.businessDate,
|
||||
supplierId: partnerId, supplierName: body.partnerName }
|
||||
}
|
||||
|
||||
async function createStatement(context, source, role, targetStatus) {
|
||||
const body = { customerId: '10001', currency: 'CNY', generationMode: 'SELECTED',
|
||||
feeSourceIds: [source.feeSourceId], periodStart: source.businessDate, periodEnd: source.businessDate,
|
||||
remark: `${MARKER}-${role}` }
|
||||
const response = await write(context, 'POST', `${BASE}/statements`, body, `statement-${role}-create`)
|
||||
let statement = data(response)
|
||||
if (!ok(response) || !statementId(statement)) return { response, value: null }
|
||||
for (const [action, status] of [['SUBMIT', 'PENDING_REVIEW'], ['APPROVE', 'APPROVED'], ['ISSUE', 'ISSUED']]) {
|
||||
if (targetStatus === 'DRAFT' || statement.status === targetStatus) break
|
||||
const actionResponse = await write(context, 'POST', `${BASE}/reconciliation-statements/${statementId(statement)}/actions`,
|
||||
{ action, expectedVersion: version(statement) }, `statement-${role}-${action.toLowerCase()}`)
|
||||
statement = data(actionResponse)
|
||||
if (!ok(actionResponse) || statement?.status !== status) return { response: actionResponse, value: null }
|
||||
}
|
||||
record(context, `${role} 对账单保留 ${targetStatus}`, response, statement?.status === targetStatus,
|
||||
`statementId=${statementId(statement)}, status=${statement?.status}`)
|
||||
return { response, value: statement }
|
||||
}
|
||||
|
||||
async function createAr(context, statement, role) {
|
||||
const response = await write(context, 'POST', `${BASE}/receivable-settlements`,
|
||||
{ reconciliationStatementId: Number(statementId(statement)), currency: 'CNY' }, `ar-${role}-create`)
|
||||
const value = data(response)
|
||||
record(context, `${role} 创建应收结算单`, response, ok(response) && settlementId(value),
|
||||
`settlementId=${settlementId(value)}, status=${value?.paymentStatus}`)
|
||||
return value
|
||||
}
|
||||
|
||||
async function receive(context, fixture, settlement, amount, role) {
|
||||
const response = await write(context, 'POST', `${BASE}/receipts`, {
|
||||
customerId: 10001, receivedDate: new Date().toISOString().slice(0, 10), amount, currency: 'CNY',
|
||||
paymentMethod: 'BANK_TRANSFER', voucherFileIds: [fixture.voucherFileId], remark: `${MARKER}-${role}`,
|
||||
allocations: [{ billId: Number(settlement.reconciliationStatementId), amount }],
|
||||
}, `receipt-${role}`)
|
||||
const detailResponse = await context.request('GET', `${BASE}/settlements/${settlementId(settlement)}`)
|
||||
return { response, value: data(detailResponse) }
|
||||
}
|
||||
|
||||
async function createAp(context, fixture, source, role) {
|
||||
const response = await write(context, 'POST', `${BASE}/payable-settlements`, {
|
||||
customerId: 10001, supplierId: id(source.supplierId), feeSourceIds: [Number(source.feeSourceId)],
|
||||
dueDate: new Date().toISOString().slice(0, 10), currency: 'CNY',
|
||||
}, `ap-${role}-${source.feeSourceId}-create`)
|
||||
const value = data(response)
|
||||
record(context, `${role} 创建应付结算单`, response, ok(response) && settlementId(value),
|
||||
`settlementId=${settlementId(value)}, status=${value?.paymentStatus}`)
|
||||
return value
|
||||
}
|
||||
|
||||
async function findPayableBySource(context, feeSourceId) {
|
||||
const listResponse = await context.request('GET', `${BASE}/payable-settlements?customerId=10001`)
|
||||
for (const item of rows(data(listResponse))) {
|
||||
const detailResponse = await context.request('GET', `${BASE}/settlements/${settlementId(item)}`)
|
||||
const value = data(detailResponse)
|
||||
if (value?.direction === 'PAYABLE' && value?.lines?.some((line) => id(line.feeSourceId) === id(feeSourceId))) {
|
||||
return { response: detailResponse, value }
|
||||
}
|
||||
}
|
||||
return { response: listResponse, value: null }
|
||||
}
|
||||
|
||||
function verifySettlementState(context, direction, role, response, value, expectedStatus) {
|
||||
const remaining = outstanding(value)
|
||||
const status = value?.paymentStatus ?? value?.status
|
||||
const amountMatches = role.endsWith('Unpaid') ? remaining > 0 && Number(value?.paidAmount ?? 0) === 0
|
||||
: role.endsWith('Partial') ? remaining > 0 && Number(value?.paidAmount ?? 0) > 0
|
||||
: remaining === 0 && Number(value?.paidAmount ?? 0) > 0
|
||||
const expected = amountMatches && status === expectedStatus
|
||||
if (expected) record(context, `${role} 金额和状态一致`, response, true,
|
||||
`settlementId=${settlementId(value)}, paid=${value?.paidAmount}, outstanding=${remaining}, status=${status}`)
|
||||
else blocker(context, `${role} 金额和状态一致`, response,
|
||||
`settlementId=${settlementId(value)}, direction=${direction}, paid=${value?.paidAmount}, outstanding=${remaining}, expectedStatus=${expectedStatus}, actualStatus=${status}`,
|
||||
'finance-settlement-payment-status-not-deployed')
|
||||
}
|
||||
|
||||
async function recoverExistingDemo(context, reportPath) {
|
||||
const report = JSON.parse(await readFile(reportPath, 'utf8'))
|
||||
const fixture = report?.createdResources?.financeFullFixtures
|
||||
const matrix = structuredClone(report?.createdResources?.financeDemo ?? {})
|
||||
if (fixture?.marker !== matrix.runMarker || matrix.marker !== MARKER || matrix.customerId !== '10001') {
|
||||
throw new Error('恢复报告不是 FIN-DEMO-20260812 / 客户 10001 的同一轮 fixture')
|
||||
}
|
||||
context.marker = matrix.runMarker
|
||||
await verifyUnfilteredLists(context)
|
||||
|
||||
const containerPayable = await sourceDetail(context, '308')
|
||||
if (!id(containerPayable.value?.supplierId)) blocker(context, '柜次费用携带供应商快照', containerPayable.response,
|
||||
`feeSourceId=308, sourceFeeId=${containerPayable.value?.sourceFeeId ?? 'missing'}, supplierId=missing;需部署柜次 partner 新契约后补跑`,
|
||||
'finance-container-partner-not-deployed')
|
||||
|
||||
const payableIds = ['307', '310']
|
||||
const roles = ['apUnpaid', 'apPartial']
|
||||
for (let index = 0; index < roles.length; index += 1) {
|
||||
const role = roles[index]
|
||||
const detail = await sourceDetail(context, payableIds[index])
|
||||
const source = detail.value
|
||||
if (!ok(detail.response) || source?.direction !== 'PAYABLE' || source?.customerId !== '10001'
|
||||
|| !id(source?.sourceFeeId)) {
|
||||
blocker(context, `${role} 恢复来源属于本轮 fixture`, detail.response,
|
||||
`feeSourceId=${payableIds[index]}, sourceFeeId=${source?.sourceFeeId ?? 'missing'}`,
|
||||
'invalid-finance-demo-recovery-source')
|
||||
continue
|
||||
}
|
||||
const produced = fixture.feeSources.some((item) => id(item.feeSourceId) === id(source.sourceFeeId))
|
||||
|| report.steps?.some((step) => step.name === `创建第 ${index + 1} 条额外应付来源`
|
||||
&& step.passed && step.evidence?.includes(`sourceFeeId=${source.sourceFeeId}`))
|
||||
if (!produced) {
|
||||
blocker(context, `${role} 恢复来源属于本轮 fixture`, detail.response,
|
||||
`Finance feeSourceId=${payableIds[index]} 不属于 runMarker=${matrix.runMarker} 的来源,拒绝借历史数据`,
|
||||
'invalid-finance-demo-recovery-source')
|
||||
continue
|
||||
}
|
||||
const approved = await approveAndInclude(context, { feeSourceId: payableIds[index] }, role)
|
||||
const existing = approved?.statementGenerated === true ? await findPayableBySource(context, payableIds[index]) : null
|
||||
const settlement = existing?.value ?? await createAp(context, fixture, {
|
||||
feeSourceId: payableIds[index], supplierId: id(fixture.supplier?.supplierId),
|
||||
}, role)
|
||||
if (existing?.value) record(context, `${role} 回读已有应付结算单`, existing.response, true,
|
||||
`settlementId=${settlementId(existing.value)}, feeSourceId=${payableIds[index]}`)
|
||||
if (!settlementId(settlement)) continue
|
||||
if (role === 'apUnpaid') matrix.payableSettlements[role] = settlement
|
||||
else {
|
||||
const amount = Math.max(0.01, Math.floor(outstanding(settlement) * 50) / 100)
|
||||
matrix.payableSettlements[role] = (await pay(context, fixture, settlement, amount, role)).value
|
||||
}
|
||||
verifySettlementState(context, 'PAYABLE', role, detail.response, matrix.payableSettlements[role],
|
||||
role === 'apUnpaid' ? 'UNPAID' : 'PARTIALLY_PAID')
|
||||
}
|
||||
|
||||
for (const [role, expectedStatus] of [['arUnpaid', 'UNPAID'], ['arPartial', 'PARTIALLY_PAID'], ['arPaid', 'PAID']]) {
|
||||
const current = matrix.receivableSettlements?.[role]
|
||||
if (!settlementId(current)) continue
|
||||
const response = await context.request('GET', `${BASE}/settlements/${settlementId(current)}`)
|
||||
const value = data(response)
|
||||
matrix.receivableSettlements[role] = value
|
||||
verifySettlementState(context, 'RECEIVABLE', role, response, value, expectedStatus)
|
||||
}
|
||||
matrix.feeSources.approvedUnbilled = await approveAndInclude(context, { feeSourceId: '300' }, 'approved-unbilled')
|
||||
context.createdResources.financeFullFixtures = fixture
|
||||
context.createdResources.financeDemo = matrix
|
||||
}
|
||||
|
||||
async function pay(context, fixture, settlement, amount, role) {
|
||||
const response = await write(context, 'POST', `${BASE}/settlements/${settlementId(settlement)}/transactions`, {
|
||||
transactionDate: new Date().toISOString().slice(0, 10), amount, currency: 'CNY', method: 'BANK_TRANSFER',
|
||||
referenceNo: `${MARKER}-${role}`, voucherFileIds: [fixture.voucherFileId],
|
||||
expectedVersion: version(settlement), remark: `${MARKER}-${role}`,
|
||||
}, `payment-${role}`)
|
||||
return { response, value: data(response) }
|
||||
}
|
||||
|
||||
async function verifyUnfilteredLists(context) {
|
||||
for (const path of ['statements', 'receivable-settlements', 'payable-settlements', 'receipts', 'cost-entries']) {
|
||||
const response = await context.request('GET', `${BASE}/${path}`)
|
||||
if (!ok(response)) blocker(context, `平台 ${path} 默认查询全部客户`, response,
|
||||
`GET ${BASE}/${path} 未部署无 customerId 全量查询;HTTP=${response.http}, code=${response.body?.code ?? 'missing'}, requestId=${response.requestId ?? 'null'}, traceId=${response.traceId ?? 'null'}`,
|
||||
'finance-unfiltered-list-not-deployed')
|
||||
else record(context, `平台 ${path} 默认查询全部客户`, response, Array.isArray(data(response)) || Array.isArray(data(response)?.records),
|
||||
`records=${rows(data(response)).length}`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function run(context) {
|
||||
const start = context.steps.length
|
||||
const recoveryReport = process.env.FINANCE_DEMO_RECOVERY_REPORT
|
||||
if (recoveryReport) {
|
||||
await recoverExistingDemo(context, recoveryReport)
|
||||
const steps = context.steps.slice(start)
|
||||
context.scenarios.push({ name: OPTIONS.scenario, module: MODULE,
|
||||
success: steps.every((step) => step.passed), deploymentBlocked: steps.some((step) => step.deploymentBlocker),
|
||||
passed: steps.filter((step) => step.passed).length, failed: steps.filter((step) => !step.passed).length })
|
||||
return
|
||||
}
|
||||
const runMarker = `${MARKER}-${new Date().toISOString().replace(/\D/g, '').slice(8, 14)}`
|
||||
context.marker = runMarker
|
||||
await verifyUnfilteredLists(context)
|
||||
|
||||
const producer = await import('./finance-full-fixtures.mjs')
|
||||
await producer.run(context)
|
||||
const fixture = context.createdResources.financeFullFixtures
|
||||
if (!fixture || fixture.marker !== runMarker || fixture.customerId !== '10001') {
|
||||
blocker(context, '取得演示企业真实 Finance fixture', null,
|
||||
'finance-full-fixtures 未返回 marker=FIN-DEMO-20260812、customerId=10001 的独立资源,禁止借历史来源',
|
||||
'missing-finance-demo-fixture')
|
||||
return
|
||||
}
|
||||
|
||||
const extraPayables = []
|
||||
for (let index = 1; index <= 2; index += 1) {
|
||||
const value = await createExtraPayable(context, fixture, index)
|
||||
if (value) extraPayables.push(value)
|
||||
}
|
||||
const produced = [...fixture.feeSources.map((item) => ({ ...item, sourceFeeId: id(item.feeSourceId) })), ...extraPayables]
|
||||
const pool = await waitForSources(context, produced.map((item) => item.sourceFeeId).filter(Boolean))
|
||||
const poolBySource = new Map(pool.map((item) => [id(item.sourceId), item]))
|
||||
const sources = produced.map((source) => ({ ...source,
|
||||
feeSourceId: financeId(poolBySource.get(source.sourceFeeId)), marker: runMarker,
|
||||
supplierId: id(source.supplierId ?? source.partnerId ?? fixture.supplier?.supplierId) }))
|
||||
.filter((source) => source.feeSourceId)
|
||||
const receivables = sources.filter((source) => source.direction === 'RECEIVABLE')
|
||||
const payables = sources.filter((source) => source.direction === 'PAYABLE' && source.supplierId)
|
||||
if (receivables.length < 7 || payables.length < 3) {
|
||||
blocker(context, '分层状态来源数量门禁', null,
|
||||
`需要至少 7 条本轮应收和 3 条本轮应付;actual AR sources=${receivables.length}, AP sources=${payables.length}`,
|
||||
'insufficient-demo-sources')
|
||||
return
|
||||
}
|
||||
|
||||
const matrix = { marker: MARKER, runMarker, customerId: '10001', customerLabel: '一期演示企业 10001',
|
||||
feeSources: {}, statements: {}, receivableSettlements: {}, payableSettlements: {},
|
||||
voucherFileId: id(fixture.voucherFileId), container: fixture.container, allocations: fixture.allocations }
|
||||
|
||||
const pending = await sourceDetail(context, receivables[0].feeSourceId)
|
||||
record(context, '保留费用待审核', pending.response,
|
||||
pending.value?.reviewStatus === 'PENDING' && pending.value?.settlementIncluded === false,
|
||||
`feeSourceId=${receivables[0].feeSourceId}, review=${pending.value?.reviewStatus}`)
|
||||
matrix.feeSources.pendingReview = pending.value
|
||||
matrix.feeSources.approvedUnbilled = await approveAndInclude(context, receivables[1], 'approved-unbilled')
|
||||
|
||||
const statementRoles = [
|
||||
['draft', 'DRAFT'], ['pendingReview', 'PENDING_REVIEW'], ['arUnpaid', 'ISSUED'],
|
||||
['arPartial', 'ISSUED'], ['arPaid', 'ISSUED'],
|
||||
]
|
||||
for (let index = 0; index < statementRoles.length; index += 1) {
|
||||
const [role, status] = statementRoles[index]
|
||||
await approveAndInclude(context, receivables[index + 2], role)
|
||||
const statement = await createStatement(context, receivables[index + 2], role, status)
|
||||
if (statement.value) matrix.statements[role] = statement.value
|
||||
}
|
||||
|
||||
for (const role of ['arUnpaid', 'arPartial', 'arPaid']) {
|
||||
const statement = matrix.statements[role]
|
||||
if (!statement) continue
|
||||
const settlement = await createAr(context, statement, role)
|
||||
if (!settlement) continue
|
||||
if (role === 'arUnpaid') matrix.receivableSettlements[role] = settlement
|
||||
if (role === 'arPartial') {
|
||||
const amount = Math.max(0.01, Math.floor(outstanding(settlement) * 50) / 100)
|
||||
matrix.receivableSettlements[role] = (await receive(context, fixture, settlement, amount, role)).value
|
||||
}
|
||||
if (role === 'arPaid') matrix.receivableSettlements[role] = (await receive(context, fixture, settlement,
|
||||
outstanding(settlement), role)).value
|
||||
verifySettlementState(context, 'RECEIVABLE', role, settlement, matrix.receivableSettlements[role],
|
||||
role === 'arUnpaid' ? 'UNPAID' : role === 'arPartial' ? 'PARTIALLY_PAID' : 'PAID')
|
||||
}
|
||||
|
||||
for (const [index, role] of ['apUnpaid', 'apPartial', 'apPaid'].entries()) {
|
||||
await approveAndInclude(context, payables[index], role)
|
||||
const settlement = await createAp(context, fixture, payables[index], role)
|
||||
if (!settlement) continue
|
||||
if (role === 'apUnpaid') matrix.payableSettlements[role] = settlement
|
||||
if (role === 'apPartial') {
|
||||
const amount = Math.max(0.01, Math.floor(outstanding(settlement) * 50) / 100)
|
||||
matrix.payableSettlements[role] = (await pay(context, fixture, settlement, amount, role)).value
|
||||
}
|
||||
if (role === 'apPaid') matrix.payableSettlements[role] = (await pay(context, fixture, settlement,
|
||||
outstanding(settlement), role)).value
|
||||
verifySettlementState(context, 'PAYABLE', role, settlement, matrix.payableSettlements[role],
|
||||
role === 'apUnpaid' ? 'UNPAID' : role === 'apPartial' ? 'PARTIALLY_PAID' : 'PAID')
|
||||
}
|
||||
|
||||
context.createdResources.financeDemo = matrix
|
||||
const steps = context.steps.slice(start)
|
||||
context.scenarios.push({ name: OPTIONS.scenario, module: MODULE,
|
||||
success: steps.every((step) => step.passed), deploymentBlocked: steps.some((step) => step.deploymentBlocker),
|
||||
passed: steps.filter((step) => step.passed).length, failed: steps.filter((step) => !step.passed).length })
|
||||
}
|
||||
Reference in New Issue
Block a user