diff --git a/web-admin/src/utils/protocol-sample-payment.test.ts b/web-admin/src/utils/protocol-sample-payment.test.ts index ddae9126..66065e0b 100644 --- a/web-admin/src/utils/protocol-sample-payment.test.ts +++ b/web-admin/src/utils/protocol-sample-payment.test.ts @@ -15,7 +15,9 @@ import { applyConsolidateRemainingToPayment, applyExpandRemainingToInstallment, availableSamplePayModes, - samplePaymentHasConfirmedTransfer + samplePaymentHasConfirmedTransfer, + calcLastPeriodDueAmount, + shouldAutoFillLastPeriodDue } from './protocol-sample-payment' describe('parsePreferentialPrice', () => { @@ -331,3 +333,42 @@ describe('samplePaymentHasConfirmedTransfer', () => { ) }) }) + +describe('calcLastPeriodDueAmount', () => { + it('last = contract − sum(priors), cents remainder on last', () => { + assert.equal(calcLastPeriodDueAmount(100, [33.33, 33.33]), 33.34) + assert.equal(calcLastPeriodDueAmount(20000, [10000, 5000]), 5000) + }) + it('returns null when contract invalid', () => { + assert.equal(calcLastPeriodDueAmount(null, [1000]), null) + assert.equal(calcLastPeriodDueAmount(NaN, [1000]), null) + }) + it('returns null when all priors empty', () => { + assert.equal(calcLastPeriodDueAmount(20000, [null, '', undefined]), null) + }) + it('treats empty slot as 0 when at least one prior filled', () => { + assert.equal(calcLastPeriodDueAmount(20000, [10000, null, '']), 10000) + }) + it('clamps negative result to 0', () => { + assert.equal(calcLastPeriodDueAmount(1000, [800, 800]), 0) + }) +}) + +describe('shouldAutoFillLastPeriodDue', () => { + it('blocks when override or locked', () => { + assert.equal( + shouldAutoFillLastPeriodDue({ lastManualOverride: true, lastLocked: false }), + false + ) + assert.equal( + shouldAutoFillLastPeriodDue({ lastManualOverride: false, lastLocked: true }), + false + ) + }) + it('allows when neither override nor locked', () => { + assert.equal( + shouldAutoFillLastPeriodDue({ lastManualOverride: false, lastLocked: false }), + true + ) + }) +}) diff --git a/web-admin/src/utils/protocol-sample-payment.ts b/web-admin/src/utils/protocol-sample-payment.ts index 795abcdb..d715963c 100644 --- a/web-admin/src/utils/protocol-sample-payment.ts +++ b/web-admin/src/utils/protocol-sample-payment.ts @@ -409,3 +409,34 @@ export function applyExpandRemainingToInstallment> }) } } + +/** 分期:最后一期应缴 = 合同 − 前 N−1 期;无法计算返回 null;负值钳为 0 */ +export function calcLastPeriodDueAmount( + contractAmount: number | null | undefined, + priorAmounts: Array +): number | null { + // Number(null) === 0;须先排除空值,否则会误算尾期 + if (contractAmount == null) return null + const contract = Number(contractAmount) + if (!Number.isFinite(contract)) return null + if (!priorAmounts.length) return null + let anyFilled = false + let sumCents = 0 + for (const raw of priorAmounts) { + if (raw === null || raw === undefined || raw === '') continue + const n = Number(raw) + if (!Number.isFinite(n)) continue + anyFilled = true + sumCents += Math.round(n * 100) + } + if (!anyFilled) return null + const last = Math.round(contract * 100 - sumCents) / 100 + return last < 0 ? 0 : last +} + +export function shouldAutoFillLastPeriodDue(opts: { + lastManualOverride: boolean + lastLocked: boolean +}): boolean { + return !opts.lastManualOverride && !opts.lastLocked +}