Files
zkxw/web-admin/src/utils/protocol-sample-payment.test.ts
T
zlwuandCursor 5dad1bb0e3 fix: 协议子资源单事务保存,并补齐样本编码/OA/质检等近期能力
锁定缴费校验失败时整单回滚,避免样本半成功落库;前端禁止删除锁定缴费样本,改码走更正编码。同步落地样本更正、采集微信通知、OA提及未读、细胞产品套餐与相关迁移文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-26 11:02:25 +08:00

445 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// web-admin/src/utils/protocol-sample-payment.test.ts
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import {
parsePreferentialPrice,
resolveSampleContractAmount,
resolvePerSamplePaymentDefaults,
shouldReplaceContractWithPreferentialPrice,
sampleScheduleRequiredFieldsHint,
sampleScheduleDuesMismatchHint,
sumSampleContractAmounts,
schedulePaidArrears,
planFqToFgdMerge,
applyFqToFgdMergeToPayment,
applyConsolidateRemainingToPayment,
applyExpandRemainingToInstallment,
availableSamplePayModes,
samplePaymentHasConfirmedTransfer,
isSamplePaymentTransferLocked,
samplePaymentHasLockedTransfer,
calcLastPeriodDueAmount,
shouldAutoFillLastPeriodDue,
applyLumpSumDeadlineToLastDueDate
} from './protocol-sample-payment'
describe('parsePreferentialPrice', () => {
it('reads sample.price as preferential amount', () => {
assert.equal(parsePreferentialPrice({ price: 20000 }), 20000)
assert.equal(parsePreferentialPrice({ price: '22000' }), 22000)
})
it('null/empty → null', () => {
assert.equal(parsePreferentialPrice({ price: null }), null)
assert.equal(parsePreferentialPrice({ price: '' }), null)
assert.equal(parsePreferentialPrice({}), null)
assert.equal(parsePreferentialPrice(null), null)
})
})
describe('resolveSampleContractAmount', () => {
it('uses preferential price when set', () => {
assert.equal(resolveSampleContractAmount({ price: 20000 }), 20000)
assert.equal(resolveSampleContractAmount({ price: 22000 }), 22000)
})
it('no fallback to hardcoded price table', () => {
assert.equal(resolveSampleContractAmount({}), null)
assert.equal(resolveSampleContractAmount({ price: null }), null)
})
})
describe('resolvePerSamplePaymentDefaults', () => {
it('dj defaults contract to preferential price', () => {
const d = resolvePerSamplePaymentDefaults({ price: 20000 }, 'dj')
assert.equal(d.contractAmount, 20000)
assert.equal(d.installmentCount, undefined)
})
it('fq has structural count but empty installment amounts', () => {
const d = resolvePerSamplePaymentDefaults({ price: 20000 }, 'fq')
assert.equal(d.contractAmount, 20000)
assert.equal(d.installmentCount, 6)
assert.deepEqual(d.installmentAmounts, [])
})
})
describe('shouldReplaceContractWithPreferentialPrice', () => {
it('only replaces null/empty contract', () => {
assert.equal(shouldReplaceContractWithPreferentialPrice(null, 20000), true)
assert.equal(shouldReplaceContractWithPreferentialPrice(22880, 20000), false)
assert.equal(shouldReplaceContractWithPreferentialPrice(18000, 20000), false)
})
})
describe('sampleScheduleRequiredFieldsHint', () => {
it('requires amount and dueDate for each fq period', () => {
const hint = sampleScheduleRequiredFieldsHint({
sampleCode: 'A1',
payMode: 'fq',
contractAmount: 20000,
schedules: [
{ dueAmount: null, dueDate: '20261001', scheduleStatus: 'open' },
{ dueAmount: 10000, dueDate: '', scheduleStatus: 'open' }
]
})
assert.ok(hint)
assert.match(hint!, /应缴金额/)
})
it('ok when all periods filled', () => {
assert.equal(
sampleScheduleRequiredFieldsHint({
payMode: 'fq',
contractAmount: 20000,
schedules: [
{ dueAmount: 10000, dueDate: '20261001' },
{ dueAmount: 10000, dueDate: '20261101' }
]
}),
null
)
})
})
describe('sampleScheduleDuesMismatchHint', () => {
it('sum must equal contract', () => {
const hint = sampleScheduleDuesMismatchHint({
payMode: 'fq',
contractAmount: 20000,
schedules: [
{ dueAmount: 10000, dueDate: '20261001' },
{ dueAmount: 5000, dueDate: '20261101' }
]
})
assert.ok(hint)
})
})
describe('sumSampleContractAmounts', () => {
it('two samples 20000+22000 → 42000', () => {
const payments = [
{ contractAmount: resolveSampleContractAmount({ price: 20000 }) },
{ contractAmount: resolveSampleContractAmount({ price: 22000 }) }
]
assert.equal(sumSampleContractAmounts(payments), 42000)
})
})
describe('schedulePaidArrears', () => {
it('display paid includes only yjf', () => {
const { paid, arrears } = schedulePaidArrears(25880, [
{ payStatus: 'dfj', transferAmount: 480 },
{ payStatus: 'dfj', transferAmount: 6000 },
{ payStatus: 'jfqrz', transferAmount: 100 },
{ payStatus: 'yjf', transferAmount: 200 },
{ payStatus: 'ytf', transferAmount: 50 }
])
assert.equal(paid, 200)
assert.equal(arrears, 25680)
})
})
describe('planFqToFgdMerge', () => {
it('keeps paid periods and merges remaining into first unpaid', () => {
const schedules = [
{
id: 's1',
scheduleNo: 1,
dueAmount: 5000,
scheduleStatus: 'cleared',
transfers: [{ payStatus: 'yjf', transferAmount: 5000 }]
},
{
id: 's2',
scheduleNo: 2,
dueAmount: 5000,
scheduleStatus: 'cleared',
transfers: [{ payStatus: 'yjf', transferAmount: 5000 }]
},
{ id: 's3', scheduleNo: 3, dueAmount: 5000, scheduleStatus: 'open', transfers: [] },
{ id: 's4', scheduleNo: 4, dueAmount: 5000, scheduleStatus: 'open', transfers: [] },
{ id: 's5', scheduleNo: 5, dueAmount: 5000, scheduleStatus: 'open', transfers: [] },
{ id: 's6', scheduleNo: 6, dueAmount: 4800, scheduleStatus: 'open', transfers: [] }
]
const plan = planFqToFgdMerge(schedules)
assert.equal(plan.targetScheduleId, 's3')
assert.equal(plan.targetScheduleNo, 3)
assert.equal(plan.newDueAmount, 19800)
assert.deepEqual(plan.softDeleteScheduleIds, ['s4', 's5', 's6'])
assert.equal(plan.newInstallmentCount, 3)
})
it('rejects when a later period already has confirmed transfer', () => {
assert.throws(
() =>
planFqToFgdMerge([
{ id: 's1', scheduleNo: 1, dueAmount: 1000, transfers: [] },
{
id: 's2',
scheduleNo: 2,
dueAmount: 1000,
transfers: [{ payStatus: 'yjf', transferAmount: 1000 }]
}
]),
/后续期/
)
})
})
describe('applyFqToFgdMergeToPayment', () => {
it('updates payMode/count and drops later unpaid schedules', () => {
const next = applyFqToFgdMergeToPayment({
payMode: 'fq',
installmentCount: 6,
contractAmount: 29800,
schedules: [
{
id: 's1',
scheduleNo: 1,
dueAmount: 5000,
scheduleStatus: 'cleared',
transfers: [{ payStatus: 'yjf', transferAmount: 5000 }]
},
{
id: 's2',
scheduleNo: 2,
dueAmount: 5000,
scheduleStatus: 'cleared',
transfers: [{ payStatus: 'yjf', transferAmount: 5000 }]
},
{ id: 's3', scheduleNo: 3, dueAmount: 5000, scheduleStatus: 'open', transfers: [] },
{ id: 's4', scheduleNo: 4, dueAmount: 5000, scheduleStatus: 'open', transfers: [] },
{ id: 's5', scheduleNo: 5, dueAmount: 5000, scheduleStatus: 'open', transfers: [] },
{ id: 's6', scheduleNo: 6, dueAmount: 4800, scheduleStatus: 'open', transfers: [] }
]
})
assert.equal(next.payMode, 'fgd')
assert.equal(next.installmentCount, 3)
assert.equal(next.schedules.length, 3)
assert.equal(next.schedules[2].dueAmount, 19800)
assert.deepEqual(
next.schedules.map((s: { scheduleNo: number }) => s.scheduleNo),
[1, 2, 3]
)
})
})
describe('availableSamplePayModes', () => {
it('fq → dj/fgd allowed', () => {
assert.deepEqual(availableSamplePayModes({ payMode: 'fq' }), ['fq', 'dj', 'fgd'])
})
it('dj cannot switch to fgd', () => {
assert.deepEqual(availableSamplePayModes({ payMode: 'dj' }), ['dj', 'fq'])
})
it('fgd cannot switch to dj', () => {
assert.deepEqual(availableSamplePayModes({ payMode: 'fgd' }), ['fgd', 'fq'])
})
})
describe('samplePaymentHasLockedTransfer', () => {
it('locks jfqrz / yjf / tfqrz / ytf like backend', () => {
for (const payStatus of ['jfqrz', 'yjf', 'tfqrz', 'ytf']) {
assert.equal(isSamplePaymentTransferLocked(payStatus), true)
assert.equal(
samplePaymentHasLockedTransfer({
schedules: [{ transfers: [{ payStatus }] }]
}),
true
)
}
})
it('dfj / empty are not locked', () => {
assert.equal(isSamplePaymentTransferLocked('dfj'), false)
assert.equal(isSamplePaymentTransferLocked(''), false)
assert.equal(
samplePaymentHasLockedTransfer({
schedules: [{ transfers: [{ payStatus: 'dfj' }] }]
}),
false
)
})
})
describe('applyConsolidateRemainingToPayment', () => {
it('fq→dj keeps paid and consolidates remaining', () => {
const next = applyConsolidateRemainingToPayment(
{
payMode: 'fq',
installmentCount: 6,
contractAmount: 29800,
schedules: [
{
id: 's1',
scheduleNo: 1,
dueAmount: 5000,
transfers: [{ payStatus: 'yjf', transferAmount: 5000 }]
},
{
id: 's2',
scheduleNo: 2,
dueAmount: 5000,
transfers: [{ payStatus: 'yjf', transferAmount: 5000 }]
},
{ id: 's3', scheduleNo: 3, dueAmount: 5000, transfers: [] },
{ id: 's4', scheduleNo: 4, dueAmount: 5000, transfers: [] },
{ id: 's5', scheduleNo: 5, dueAmount: 5000, transfers: [] },
{ id: 's6', scheduleNo: 6, dueAmount: 4800, transfers: [] }
]
},
'dj'
)
assert.equal(next.payMode, 'dj')
assert.equal(next.installmentCount, 3)
assert.equal(next.schedules.length, 3)
assert.equal(next.schedules[2].dueAmount, 19800)
})
})
describe('applyExpandRemainingToInstallment', () => {
it('dj partial paid → keep paid period and split remaining', () => {
const next = applyExpandRemainingToInstallment(
{
payMode: 'dj',
contractAmount: 12000,
schedules: [
{
id: 's1',
scheduleNo: 1,
dueAmount: 12000,
transfers: [{ payStatus: 'yjf', transferAmount: 4000 }]
}
]
},
{
installmentCount: 4,
installmentIntervalMonths: 1,
installmentStartDate: '20261001',
installmentAdvanceDays: 0
}
)
assert.equal(next.payMode, 'fq')
assert.equal(next.schedules[0].dueAmount, 4000)
assert.equal(next.schedules.length, 4) // 1 kept + 3 new (4-1)
const newSum = next.schedules.slice(1).reduce((s, x) => s + Number(x.dueAmount), 0)
assert.equal(newSum, 8000)
assert.equal(next.schedules[1].dueDate, '20261001')
})
it('rejects when already cleared', () => {
assert.throws(
() =>
applyExpandRemainingToInstallment(
{
payMode: 'dj',
contractAmount: 1000,
schedules: [
{
scheduleNo: 1,
dueAmount: 1000,
transfers: [{ payStatus: 'yjf', transferAmount: 1000 }]
}
]
},
{ installmentCount: 6, installmentIntervalMonths: 1, installmentStartDate: '20261001' }
),
/已缴清/
)
})
})
describe('samplePaymentHasConfirmedTransfer', () => {
it('detects yjf', () => {
assert.equal(
samplePaymentHasConfirmedTransfer({
schedules: [{ transfers: [{ payStatus: 'dfj' }] }]
}),
false
)
assert.equal(
samplePaymentHasConfirmedTransfer({
schedules: [{ transfers: [{ payStatus: 'yjf', transferAmount: 1 }] }]
}),
true
)
})
})
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
)
})
})
describe('applyLumpSumDeadlineToLastDueDate', () => {
it('writes deadline to last unlocked schedule', () => {
const next = applyLumpSumDeadlineToLastDueDate(
[
{ scheduleNo: 1, dueDate: '', scheduleStatus: 'open' },
{ scheduleNo: 2, dueDate: '20260101', scheduleStatus: 'open' }
],
'20261231',
(s) => false
)
assert.equal(next[0].dueDate, '')
assert.equal(next[1].dueDate, '20261231')
})
it('clears dueDate when deadline empty', () => {
const next = applyLumpSumDeadlineToLastDueDate(
[{ scheduleNo: 1, dueDate: '20261231', scheduleStatus: 'open' }],
'',
() => false
)
assert.equal(next[0].dueDate, '')
})
it('skips when last schedule locked', () => {
const next = applyLumpSumDeadlineToLastDueDate(
[{ scheduleNo: 1, dueDate: '20260101', scheduleStatus: 'open' }],
'20261231',
() => true
)
assert.equal(next[0].dueDate, '20260101')
})
it('ignores void schedules when picking last', () => {
const next = applyLumpSumDeadlineToLastDueDate(
[
{ scheduleNo: 1, dueDate: '', scheduleStatus: 'open' },
{ scheduleNo: 2, dueDate: 'old', scheduleStatus: 'void' }
],
'20261231',
() => false
)
assert.equal(next[0].dueDate, '20261231')
assert.equal(next[1].dueDate, 'old')
})
})