feat: 细胞存储缴费催缴、统一样本信息与协议支付能力增强
汇总近期业务改动:催缴菜单与后端、样本信息快捷登记与进度、运费关联、客户档案扩展、协议优惠/期次与押金划转,并同步 SQL 迁移与管理端页面收敛。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
2d3472f5c4
commit
db31990ebe
235 files changed
+14898
-10196
No files matched your search
@@ -0,0 +1,333 @@
|
||||
// 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
|
||||
} 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 dfj + jfqrz + 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, 6780)
|
||||
assert.equal(arrears, 19100)
|
||||
})
|
||||
})
|
||||
|
||||
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('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
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user