This commit is contained in:
2026-08-13 14:03:54 +08:00
parent 18cebd3fdd
commit bc62650ca9
11 changed files with 349 additions and 18 deletions
@@ -9,7 +9,9 @@ const write = (context, method, path, body, operation) => context.request(method
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 paid = (value) => Number(value?.paidAmount ?? value?.summary?.paidAmount ?? 0)
const version = (value) => Number(value?.version ?? value?.summary?.version ?? 0)
const status = (value) => id(value?.status ?? value?.summary?.status ?? value?.paymentStatus)
export function values(value) {
if (Array.isArray(value)) return value.filter(Boolean)
@@ -34,6 +36,61 @@ async function detail(context, targetId) {
return { response, value: data(response) }
}
async function verifyVoidReopen(context, item) {
const targetId = settlementId(item)
const before = await detail(context, targetId)
if (!ok(before.response) || status(before.value) !== 'ISSUED' || outstanding(before.value) <= 0 || paid(before.value) !== 0) {
gap(context, '取得未收付款 ISSUED 结算单', 'settlement VOID/REOPEN',
`本轮 settlementId=${targetId || 'missing'} 不满足 ISSUED 且 paidAmount=0;status=${status(before.value)}, paid=${paid(before.value)}`,
'settlement-action-fixture-unavailable')
return null
}
const path = `${BASE}/settlements/${targetId}/actions`
const voidBody = { action: 'VOID', reason: context.marker, expectedVersion: version(before.value) }
const voidedResponse = await write(context, 'POST', path, voidBody, 'settlement-void')
const voided = data(voidedResponse)
context.assert(MODULE, '未收付款结算单作废', voidedResponse, ok(voidedResponse)
&& status(voided) === 'VOIDED' && version(voided) === version(before.value) + 1,
`settlementId=${targetId}, status=${status(voided)}, version=${version(voided)}`, OPTIONS)
const replayResponse = await write(context, 'POST', path, voidBody, 'settlement-void')
const replayed = data(replayResponse)
context.assert(MODULE, '结算单作废同键同参幂等重放', replayResponse, ok(replayResponse)
&& status(replayed) === 'VOIDED' && version(replayed) === version(voided),
`settlementId=${targetId}, version=${version(replayed)}`, OPTIONS)
const conflictResponse = await write(context, 'POST', path,
{ action: 'REOPEN', reason: `${context.marker}-conflict`, expectedVersion: version(voided) }, 'settlement-void')
context.assert(MODULE, '结算单动作同键异参冲突', conflictResponse, !ok(conflictResponse),
`HTTP=${conflictResponse.http}, code=${id(conflictResponse.body?.code)}`, OPTIONS)
const reopenBody = { action: 'REOPEN', reason: context.marker, expectedVersion: version(voided) }
const reopenedResponse = await write(context, 'POST', path, reopenBody, 'settlement-reopen')
const reopened = data(reopenedResponse)
context.assert(MODULE, '已作废结算单重开', reopenedResponse, ok(reopenedResponse)
&& status(reopened) === 'ISSUED' && version(reopened) === version(voided) + 1,
`settlementId=${targetId}, status=${status(reopened)}, version=${version(reopened)}`, OPTIONS)
return reopened
}
async function verifyPaidVoidRejected(context, item) {
const targetId = settlementId(item)
const before = await detail(context, targetId)
if (!ok(before.response) || paid(before.value) <= 0) {
gap(context, '取得已发生收付款结算单', 'paid settlement VOID rejection',
`本轮 settlementId=${targetId || 'missing'} 没有有效收付款;paid=${paid(before.value)}`,
'paid-settlement-fixture-unavailable')
return
}
const response = await write(context, 'POST', `${BASE}/settlements/${targetId}/actions`,
{ action: 'VOID', reason: context.marker, expectedVersion: version(before.value) }, 'paid-settlement-void')
const after = await detail(context, targetId)
context.assert(MODULE, '已收付款结算单拒绝作废且状态金额不变', response, !ok(response) && ok(after.response)
&& status(after.value) === status(before.value) && version(after.value) === version(before.value)
&& paid(after.value) === paid(before.value) && outstanding(after.value) === outstanding(before.value),
`settlementId=${targetId}, status=${status(after.value)}, paid=${paid(after.value)}, outstanding=${outstanding(after.value)}, version=${version(after.value)}`, OPTIONS)
}
async function receive(context, fixtures, item, requestedAmount, operation, label) {
const targetId = settlementId(item)
const billId = statementId(item) || id(item.reconciliationStatementId)
@@ -87,6 +144,11 @@ export async function run(context) {
manualIds.length > 0 && manualIds.every((feeId) => usedIds.has(feeId)),
`manual=${manualIds.length}, settled=${manualIds.filter((feeId) => usedIds.has(feeId)).length}`, OPTIONS)
const actionTarget = validReceivables.find((item) => item.status === 'ISSUED')
?? validPayables.find((item) => item.status === 'ISSUED')
if (!actionTarget) gap(context, '取得结算单动作 Fixture', 'settlement VOID/REOPEN', '本轮没有未收付款且状态 ISSUED 的 AR/AP,禁止借历史单据')
else await verifyVoidReopen(context, actionTarget)
const multiTargets = validReceivables
if (multiTargets.length < 3) gap(context, '取得三张应收结算单', 'multi receipt allocation', '本轮没有三张独占应收单,禁止借历史单据')
else {
@@ -111,6 +173,7 @@ export async function run(context) {
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 }
await verifyPaidVoidRejected(context, primary)
}
const payable = validPayables[0]