feat(web-admin): add last-period due amount calc helpers
Pure functions for fq auto-fill of the final installment amount with override/lock gates, covered by unit tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
13ac45d10d
commit
5fd5550e0d
2 files changed
+73
-1
No files matched your search
@@ -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
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -409,3 +409,34 @@ export function applyExpandRemainingToInstallment<T extends Record<string, any>>
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** 分期:最后一期应缴 = 合同 − 前 N−1 期;无法计算返回 null;负值钳为 0 */
|
||||
export function calcLastPeriodDueAmount(
|
||||
contractAmount: number | null | undefined,
|
||||
priorAmounts: Array<number | string | null | undefined>
|
||||
): 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
|
||||
}
|
||||
Reference in new issue
Block a user