feat: 细胞存储缴费催缴、统一样本信息与协议支付能力增强

汇总近期业务改动:催缴菜单与后端、样本信息快捷登记与进度、运费关联、客户档案扩展、协议优惠/期次与押金划转,并同步 SQL 迁移与管理端页面收敛。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-10-10 21:20:37 +08:00
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
)
})
})