This commit is contained in:
2026-08-12 12:35:29 +08:00
parent 0fceece378
commit e4f65dae5f
8 changed files with 434 additions and 41 deletions
@@ -223,29 +223,25 @@ export async function run(context) {
const cutoffDate = [...businessDates].sort()[0]
const cutoffSources = receivables.filter((source) => source.businessDate === cutoffDate)
const poolResponse = await context.request('GET',
`${BASE}/fee-pool?customerId=${encodeURIComponent(fixture.customerId)}&direction=RECEIVABLE`)
const pool = context.apiSuccess(MODULE, 'BY_CUTOFF 前核对客户应收费用池', poolResponse, '', OPTIONS)
const eligible = rows(pool).filter((source) => sourceDate(source) === cutoffDate
&& String(source.currency ?? source.currencyCode) === 'CNY' && source.reviewStatus === 'APPROVED'
&& source.settlementIncluded === true && source.sourceStatus === 'AVAILABLE').map(sourceId)
if (!sameIds(eligible, cutoffSources.map((source) => source.feeSourceId))) {
blocker(context, 'BY_CUTOFF 历史来源隔离门禁',
'指定日期全部可结算来源必须与本轮 fixture 精确相等,禁止锁定历史来源', 'unsafe-cutoff-source-set', poolResponse)
if (cutoffSources.length < 2) {
blocker(context, '日期筛选多行来源门禁',
`最早真实 businessDate=${cutoffDate} 仅有 ${cutoffSources.length} 条来源,无法同时验证日期排除和多行调整`,
'insufficient-same-date-receivable-sources')
finish(context, start)
return
}
const cutoff = await createStatement(context, cutoffSources, 'cutoff', '按真实业务日期生成对账单', 'BY_CUTOFF')
let cutoffStatement = cutoff.statement
context.assert(MODULE, 'BY_CUTOFF 仅纳入真实日期范围内本轮来源', cutoff.response,
cutoffStatement && cutoff.body.periodStart === cutoffDate && cutoff.body.periodEnd === cutoffDate
&& sameIds(lineSourceIds(cutoffStatement), cutoffSources.map((source) => source.feeSourceId)),
`date=${cutoffDate}, expected=${cutoffSources.length}, actual=${lineSourceIds(cutoffStatement).length}`, OPTIONS)
// ReconciliationStatementService explicitly permits VOID from DRAFT and releases LOCKED sources.
cutoffStatement = await act(context, cutoffStatement, 'VOID', 'cutoff-draft-void', `${context.marker}-release-cutoff`)
if (!cutoffStatement) { finish(context, start); return }
const groups = [[receivables[0], receivables[1]], [receivables[2]], receivables.slice(3)]
// Date coverage intentionally uses SELECTED. BY_CUTOFF queries all eligible customer history.
const remainingSources = receivables.filter((source) => source.businessDate !== cutoffDate)
if (remainingSources.length < 2) {
blocker(context, '跨日期三组来源门禁',
`最早 businessDate=${cutoffDate} 之外仅有 ${remainingSources.length} 条本轮来源,无法生成三张非空独立对账单`,
'insufficient-cross-date-receivable-sources')
finish(context, start)
return
}
const groups = [cutoffSources, ...(() => {
return [remainingSources.slice(0, 1), remainingSources.slice(1)]
})()]
const issuedStatements = []
for (let index = 0; index < groups.length; index += 1) {
const created = await createStatement(context, groups[index], `selected-${index + 1}`,
@@ -262,6 +258,15 @@ export async function run(context) {
return
}
issuedStatements.push(issued)
if (index === 0) {
context.assert(MODULE, 'SELECTED 日期范围只包含指定真实日期来源', created.response,
created.body.periodStart === cutoffDate && created.body.periodEnd === cutoffDate
&& sameIds(lineSourceIds(created.statement), cutoffSources.map((source) => source.feeSourceId))
&& lineSourceIds(created.statement).every((source) => !receivables
.filter((item) => item.businessDate !== cutoffDate)
.some((item) => item.feeSourceId === source)),
`date=${cutoffDate}, expected=${cutoffSources.length}, actual=${lineSourceIds(created.statement).length}`, OPTIONS)
}
}
const receivableSettlements = []
@@ -298,8 +303,8 @@ export async function run(context) {
}
const statementRows = [
{ kind: 'BY_CUTOFF', statementId: statementId(cutoffStatement), status: cutoffStatement.status,
version: cutoffStatement.version, feeSourceIds: cutoffSources.map((source) => source.feeSourceId),
{ kind: 'SELECTED_DATE', statementId: statementId(issuedStatements[0]), status: issuedStatements[0].status,
version: issuedStatements[0].version, feeSourceIds: cutoffSources.map((source) => source.feeSourceId),
periodStart: cutoffDate, periodEnd: cutoffDate },
...issuedStatements.map((statement, index) => ({ kind: index === 0 ? 'SELECTED_FULL_CHAIN' : 'SELECTED',
statementId: statementId(statement), status: statement.status, version: statement.version,