提交
This commit is contained in:
1 parent
cdaa0ac273
commit
b7cd657f32
32 files changed
+2885
-1936
No files matched your search
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 采集协议单 · 缴费模式切换(dj ↔ fq)
|
||||
* 采集协议单 · 缴费模式切换(dj / fq / djfq)
|
||||
* 规则见 docs/superpowers/specs/2026-06-04-protocol-pay-mode-switch-design.md
|
||||
*/
|
||||
|
||||
@@ -8,8 +8,9 @@ import {
|
||||
DEFAULT_INSTALLMENT_ADVANCE_DAYS,
|
||||
defaultInstallmentAmounts
|
||||
} from '@/utils/protocol-payment-installment'
|
||||
import { type PayMode, isInstallmentPayMode } from '@/utils/protocol-payment-presets'
|
||||
|
||||
export type PayMode = 'dj' | 'fq'
|
||||
export type { PayMode }
|
||||
export type DetailLike = Record<string, any>
|
||||
|
||||
const LOCKED_STATUSES = new Set(['jfqrz', 'yjf', 'tfqrz', 'ytf'])
|
||||
@@ -69,6 +70,35 @@ function countByStatus(details: DetailLike[], status: string): number {
|
||||
return details.filter((d) => String(d.payStatus || 'dfj') === status).length
|
||||
}
|
||||
|
||||
function buildInstallmentDetails(
|
||||
ctx: PayModeSwitchContext,
|
||||
rows: DetailLike[]
|
||||
): DetailLike[] {
|
||||
const count = Number(ctx.installmentCount) || 6
|
||||
const interval = Number(ctx.installmentIntervalMonths) || 6
|
||||
const startDate = ctx.installmentStartDate || ''
|
||||
const advanceDays = Number(ctx.installmentAdvanceDays ?? DEFAULT_INSTALLMENT_ADVANCE_DAYS)
|
||||
const amounts =
|
||||
ctx.installmentRatios?.length && sumPaidAmount(rows) === 0
|
||||
? ctx.installmentRatios.map((v) => Number(v || 0))
|
||||
: defaultInstallmentAmounts(ctx.contractAmount, count)
|
||||
const result: DetailLike[] = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
result.push(
|
||||
createPaymentDetailRow({
|
||||
dueAmount: amounts[i] ?? null,
|
||||
installmentNo: i + 1,
|
||||
installmentDueDate:
|
||||
startDate && interval
|
||||
? calcFqInstallmentDueDate(startDate, i, interval, advanceDays)
|
||||
: '',
|
||||
expanded: i === 0
|
||||
})
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function createPaymentDetailRow(options: {
|
||||
dueAmount: number | null
|
||||
installmentDueDate?: string
|
||||
@@ -110,7 +140,7 @@ export function evaluatePayModeSwitch(
|
||||
}
|
||||
}
|
||||
|
||||
if (fromMode === 'dj' && toMode === 'fq') {
|
||||
if (fromMode === 'dj' && isInstallmentPayMode(toMode)) {
|
||||
if (rows.some((d) => isPayDetailLocked(d))) {
|
||||
return {
|
||||
action: 'blocked',
|
||||
@@ -118,13 +148,14 @@ export function evaluatePayModeSwitch(
|
||||
}
|
||||
}
|
||||
const count = ctx.installmentCount || 6
|
||||
const label = toMode === 'djfq' ? '趸交+分期' : '分期'
|
||||
return {
|
||||
action: 'confirm',
|
||||
summary: `将清除当前趸交计划,按 ${count} 期分期重新生成缴费明细。是否继续?`
|
||||
summary: `将清除当前趸交计划,按 ${count} 期${label}重新生成缴费明细。是否继续?`
|
||||
}
|
||||
}
|
||||
|
||||
if (fromMode === 'fq' && toMode === 'dj') {
|
||||
if (isInstallmentPayMode(fromMode) && toMode === 'dj') {
|
||||
const dfjCount = countByStatus(rows, 'dfj')
|
||||
const yjfCount = countByStatus(rows, 'yjf')
|
||||
|
||||
@@ -156,6 +187,21 @@ export function evaluatePayModeSwitch(
|
||||
}
|
||||
}
|
||||
|
||||
if (isInstallmentPayMode(fromMode) && isInstallmentPayMode(toMode) && fromMode !== toMode) {
|
||||
if (rows.some((d) => isPayDetailLocked(d))) {
|
||||
return {
|
||||
action: 'blocked',
|
||||
reason: '分期已有缴费记录,无法切换分期类型。'
|
||||
}
|
||||
}
|
||||
const count = ctx.installmentCount || 6
|
||||
const label = toMode === 'djfq' ? '趸交+分期' : '分期'
|
||||
return {
|
||||
action: 'confirm',
|
||||
summary: `将按 ${count} 期${label}重新生成缴费明细。是否继续?`
|
||||
}
|
||||
}
|
||||
|
||||
return { action: 'noop' }
|
||||
}
|
||||
|
||||
@@ -167,33 +213,11 @@ export function applyPayModeSwitch(
|
||||
): DetailLike[] {
|
||||
const rows = activeDetails(details)
|
||||
|
||||
if (fromMode === 'dj' && toMode === 'fq') {
|
||||
const count = Number(ctx.installmentCount) || 6
|
||||
const interval = Number(ctx.installmentIntervalMonths) || 6
|
||||
const startDate = ctx.installmentStartDate || ''
|
||||
const advanceDays = Number(ctx.installmentAdvanceDays ?? DEFAULT_INSTALLMENT_ADVANCE_DAYS)
|
||||
const amounts =
|
||||
ctx.installmentRatios?.length && sumPaidAmount(rows) === 0
|
||||
? ctx.installmentRatios.map((v) => Number(v || 0))
|
||||
: defaultInstallmentAmounts(ctx.contractAmount, count)
|
||||
const result: DetailLike[] = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
result.push(
|
||||
createPaymentDetailRow({
|
||||
dueAmount: amounts[i] ?? null,
|
||||
installmentNo: i + 1,
|
||||
installmentDueDate:
|
||||
startDate && interval
|
||||
? calcFqInstallmentDueDate(startDate, i, interval, advanceDays)
|
||||
: '',
|
||||
expanded: i === 0
|
||||
})
|
||||
)
|
||||
}
|
||||
return result
|
||||
if (fromMode === 'dj' && isInstallmentPayMode(toMode)) {
|
||||
return buildInstallmentDetails(ctx, rows)
|
||||
}
|
||||
|
||||
if (fromMode === 'fq' && toMode === 'dj') {
|
||||
if (isInstallmentPayMode(fromMode) && toMode === 'dj') {
|
||||
const yjfRows = rows
|
||||
.filter((d) => String(d.payStatus) === 'yjf')
|
||||
.map((d) => ({ ...d, _payModeLocked: true }))
|
||||
@@ -209,14 +233,14 @@ export function applyPayModeSwitch(
|
||||
return [...yjfRows, newDjRow]
|
||||
}
|
||||
|
||||
if (isInstallmentPayMode(fromMode) && isInstallmentPayMode(toMode) && fromMode !== toMode) {
|
||||
return buildInstallmentDetails(ctx, rows)
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
/** 趸交模式下是否还可添加待缴明细 */
|
||||
export function canAddPaymentDetail(payMode: string, details: DetailLike[]): boolean {
|
||||
if (payMode === 'fq') return true
|
||||
const dfjCount = details.filter(
|
||||
(d) => !isPayDetailLocked(d) && String(d.payStatus || 'dfj') === 'dfj'
|
||||
).length
|
||||
return dfjCount < 1
|
||||
/** 趸交与分期均允许多条缴费明细(合计须等于合同金额) */
|
||||
export function canAddPaymentDetail(_payMode: string, _details: DetailLike[]): boolean {
|
||||
return true
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
/** 分期缴费:期数对齐、应缴日/金额计算(与 ProtocolFormDialog 逻辑一致) */
|
||||
|
||||
import { isInstallmentPayMode } from '@/utils/protocol-payment-presets'
|
||||
|
||||
const DAYS_PER_MONTH = 30
|
||||
|
||||
export const DEFAULT_INSTALLMENT_ADVANCE_DAYS = 30
|
||||
@@ -311,7 +313,7 @@ export function alignFqPaymentDetailsForDisplay(
|
||||
payment: PaymentLike,
|
||||
details: DetailLike[]
|
||||
): DetailLike[] {
|
||||
if (!payment || payment.payMode !== 'fq') return details
|
||||
if (!payment || !isInstallmentPayMode(String(payment.payMode || ''))) return details
|
||||
const count = Number(payment.installmentCount)
|
||||
if (!count || count < 1) return details
|
||||
|
||||
@@ -396,7 +398,7 @@ export function syncFqPaymentDetailRowCount(
|
||||
},
|
||||
paymentDetails: DetailLike[]
|
||||
): void {
|
||||
if (payment.payMode !== 'fq') return
|
||||
if (!isInstallmentPayMode(payment.payMode)) return
|
||||
const count = Number(payment.installmentCount)
|
||||
if (!count || count < 1) return
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/** 采集协议单 · 缴费默认定价表(样本数 × 缴费模式) */
|
||||
|
||||
export type PayMode = 'dj' | 'fq' | 'djfq'
|
||||
|
||||
export type PaymentPreset = {
|
||||
contractAmount: number
|
||||
installmentCount?: number
|
||||
installmentAmounts?: number[]
|
||||
}
|
||||
|
||||
const PRESETS: Record<1 | 2, Partial<Record<PayMode, PaymentPreset>>> = {
|
||||
1: {
|
||||
dj: { contractAmount: 22880 },
|
||||
fq: {
|
||||
contractAmount: 25880,
|
||||
installmentCount: 6,
|
||||
installmentAmounts: [6480, 3880, 3880, 3880, 3880, 3880]
|
||||
}
|
||||
},
|
||||
2: {
|
||||
dj: { contractAmount: 37880 },
|
||||
djfq: {
|
||||
contractAmount: 41000,
|
||||
installmentCount: 4,
|
||||
installmentAmounts: [29360, 3880, 3880, 3880]
|
||||
},
|
||||
fq: {
|
||||
contractAmount: 44000,
|
||||
installmentCount: 5,
|
||||
installmentAmounts: [12960, 7760, 7760, 7760, 7760]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getActiveSampleCount(samples: Array<{ sampleStatus?: string }>): 1 | 2 {
|
||||
const n = samples.filter((s) => String(s.sampleStatus || '') !== 'yzf').length
|
||||
return (n >= 2 ? 2 : 1) as 1 | 2
|
||||
}
|
||||
|
||||
export function resolvePaymentPreset(
|
||||
sampleCount: 1 | 2,
|
||||
payMode: PayMode
|
||||
): PaymentPreset | null {
|
||||
return PRESETS[sampleCount]?.[payMode] ?? null
|
||||
}
|
||||
|
||||
export function isInstallmentPayMode(payMode: string): boolean {
|
||||
return payMode === 'fq' || payMode === 'djfq'
|
||||
}
|
||||
|
||||
export function availablePayModes(sampleCount: 1 | 2): PayMode[] {
|
||||
return sampleCount === 2 ? ['dj', 'djfq', 'fq'] : ['dj', 'fq']
|
||||
}
|
||||
|
||||
/** 校验缴费模式与样本数是否匹配(如 djfq 仅双样本) */
|
||||
export function isPayModeValidForSampleCount(payMode: PayMode, sampleCount: 1 | 2): boolean {
|
||||
return availablePayModes(sampleCount).includes(payMode)
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export type ProtocolStandardImportParsed = {
|
||||
medicalStaff?: string
|
||||
hospitalName?: string
|
||||
salesUserName?: string
|
||||
payMode: 'dj' | 'fq'
|
||||
payMode: 'dj' | 'fq' | 'djfq'
|
||||
contractAmount?: number | null
|
||||
transferTypeLabel?: string
|
||||
paymentTransfers: ProtocolPaymentTransferHint[]
|
||||
@@ -193,12 +193,13 @@ function earliestCollectTime(hints: ProtocolSampleImportHint[]): string | undefi
|
||||
return times.sort((a, b) => a.localeCompare(b))[0]
|
||||
}
|
||||
|
||||
/** 缴费方式:仅「分期」为 fq,其余(含微信/趸交)均为 dj */
|
||||
/** 缴费方式:趸交+分期 → djfq;仅「分期」→ fq;其余 → dj */
|
||||
export function parsePayModeFromText(value: string): {
|
||||
payMode: 'dj' | 'fq'
|
||||
payMode: 'dj' | 'fq' | 'djfq' | 'djfq'
|
||||
transferLabel?: string
|
||||
} {
|
||||
const v = (value || '').trim()
|
||||
if (/趸交\s*[++]\s*分期|趸交.*分期/.test(v)) return { payMode: 'djfq' }
|
||||
if (/分期/.test(v)) return { payMode: 'fq' }
|
||||
if (/微信|支付宝|银行卡|银行|现金/.test(v)) {
|
||||
return { payMode: 'dj', transferLabel: v }
|
||||
|
||||
@@ -117,7 +117,8 @@ export const auditStatusTagType = createTagTypeGetter(auditStatus)
|
||||
|
||||
export const payMode: StatusRegistry = {
|
||||
dj: { label: '趸交', tagType: 'primary' },
|
||||
fq: { label: '分期', tagType: 'warning' }
|
||||
fq: { label: '分期', tagType: 'warning' },
|
||||
djfq: { label: '趸交+分期', tagType: 'warning' }
|
||||
}
|
||||
export const payModeLabel = createLabelGetter(payMode)
|
||||
export const payModeTagType = createTagTypeGetter(payMode)
|
||||
|
||||
Reference in new issue
Block a user