diff --git a/docs/superpowers/specs/2026-06-30-late-fee-start-and-daily-recalculation-design.md b/docs/superpowers/specs/2026-06-30-late-fee-start-and-daily-recalculation-design.md new file mode 100644 index 0000000..8735b7e --- /dev/null +++ b/docs/superpowers/specs/2026-06-30-late-fee-start-and-daily-recalculation-design.md @@ -0,0 +1,278 @@ +# 违约金起算与每日重算设计 + +日期:2026-06-30 + +## 背景 + +本设计用于明确违约金四种起算方式、逾期天数、金额计算、每日任务重算和历史账单处理口径。 + +本次范围同时覆盖: + +- 开账时违约金起算日计算与固化 +- 每日任务按未结清账单重算当前应收违约金 +- 当前未缴本金、已收违约金、已生效减免金额的扣减口径 +- 小数、封顶、配置非法值处理 + +本设计不改变违约金减免对象本身的提交/审批流程。注意区分: + +- `CALC_LATE_FEE_TYPE`:系统参数,表示违约金起算方式,取值 `1/2/3/4` +- `lateFeeType`:账务调整/违约金减免字段,表示减免方式,例如按金额、按日期 + +## 相关配置 + +核心起算配置: + +- `CALC_LATE_FEE_TYPE`:违约金起算方式 + - `1`:账单账务年月当月某日 + - `2`:开账后若干天 + - `3`:账单账务年月下月某日 + - `4`:账单账务年月下下月某日 +- `CALC_LATE_FEE_SETTING`:起算参数值 + - `type=1/3/4` 时表示日号 + - `type=2` 时表示宽限天数 + +金额相关配置: + +- `LATE_FEE_DECIMAL`:违约金保留小数位数 +- `LATE_FEE_MORE_PRINCIPAL`:违约金封顶倍数 +- `CostComponentDO.penaltyCoefficient`:费用项违约金系数 + +账单字段: + +- `ChargeDO.billMonth`:账单账务年月,格式 `YYYYMM` +- `ChargeDO.billDate`:开账时间 +- `ChargeDO.lateFeeBeginDate`:实际开始计算违约金的日期 +- `ChargeDO.lateFee`:当前应收违约金 + +## 开账起算日规则 + +开账时根据当时系统参数计算并固化 `ChargeDO.lateFeeBeginDate`。系统参数先计算出“宽限截止日”,宽限截止日当天不计违约金,次日才是第 1 个违约金计费日。 + +### 月份基准 + +`CALC_LATE_FEE_TYPE=1/3/4` 的月份最终以账单 `billMonth` 字段对应自然月为准。 + +- `type=1`:`billMonth` 当月某日为宽限截止日 +- `type=3`:`billMonth` 下月某日为宽限截止日 +- `type=4`:`billMonth` 下下月某日为宽限截止日 + +`BILLINGMONTH_STARTDAY` 负责账务年月生成和解释。本次违约金起算计算不在每日任务中重新按运行日期推导账务周期,而是使用账单已固化的 `billMonth`。 + +### 四种起算方式 + +`type=1/3/4`: + +1. 读取 `CALC_LATE_FEE_SETTING` 作为目标日号。 +2. 根据 `billMonth` 和 `type` 得到目标月份。 +3. 若目标月份没有该日号,例如 2 月没有 30/31 号,则目标日取该月最后一天。 +4. 目标日为宽限截止日。 + +`type=2`: + +1. 读取 `CALC_LATE_FEE_SETTING` 作为宽限天数 `N`。 +2. 开账日不算第 1 天。 +3. 宽限截止日为 `billDate.toLocalDate() + N 天`。 +4. `N=0` 表示开账日就是宽限截止日,开账次日开始计违约金。 + +### 实际起算日 + +实际违约金起算日为: + +```text +lateFeeBeginDate = max(宽限截止日 + 1 天, billDate.toLocalDate() + 1 天) +``` + +因此,如果配置算出的宽限截止日早于开账日,也不能在开账日前产生违约金。 + +### 配置非法处理 + +`CALC_LATE_FEE_SETTING` 的处理规则: + +- `type=1/3/4`:有效日号范围为 `1-31` +- `type=2`:有效天数范围为 `>=0` +- `settingValue` 非法时回退 `settingDefault` +- `settingDefault` 仍非法时,不生成 `lateFeeBeginDate`,并记录配置异常 +- `type=1/3/4` 中有效日号超过目标月份最大天数时,不视为非法,取目标月最后一天 + +`billDate` 为空时: + +- 不计算 `lateFeeBeginDate` +- 记录数据异常 +- 不使用当前日期或 `createTime` 兜底 + +## 每日重算规则 + +每日任务只处理未结清账单: + +- 未缴账单:参与重算 +- 部分缴费账单:按当前未缴本金参与重算 +- 已结清账单:不再重算,避免覆盖历史违约金 + +每日任务必须幂等。每次根据当前账单状态重新计算并覆盖当前应收违约金,重复执行、失败重试、补跑都不得重复累加。 + +每日任务默认使用开账时固化的 `lateFeeBeginDate`。后续系统参数变化只影响新开账账单,不自动改变历史未结清账单的起算日。 + +历史未结清账单如果 `lateFeeBeginDate` 为空: + +- 不使用当前系统参数临时推导 +- 不计算违约金 +- 记录异常或告警,等待数据修正 + +### 逾期天数 + +- 当前日期早于 `lateFeeBeginDate`:逾期天数为 `0` +- 当前日期达到或晚于 `lateFeeBeginDate`:逾期天数包含当前日期 + +公式: + +```text +overdueDays = 当前日期 - lateFeeBeginDate.toLocalDate() + 1 +``` + +## 金额计算规则 + +本次不判断通知状态。违约金产生的计算前提为: + +- 账单未结清 +- 存在当前未缴本金 +- 账单存在有效 `lateFeeBeginDate` +- 当前日期达到或晚于 `lateFeeBeginDate` +- 费用项 `penaltyCoefficient > 0` + +### 本金基数 + +违约金本金基数使用当前未缴本金: + +- 已缴本金不再继续产生违约金 +- 部分缴费后,剩余未缴本金继续按完整逾期天数计算 +- 优先使用费用项级剩余未缴本金 +- 如果系统没有费用项级剩余未缴本金,则按各费用项原始本金比例分摊账单剩余未缴本金 +- 分摊后的费用项未缴本金之和必须等于账单剩余未缴本金 +- 分摊尾差落到最后一个参与项或金额最大的参与项 + +### 逐费用项计算 + +继续采用逐费用项计算后汇总的口径: + +```text +单项理论违约金 = 单项未缴本金 × 单项违约金系数 × 逾期天数 +``` + +费用项违约金系数来源: + +```text +ChargeDetailDO.costComponentCode -> CostComponentDO.code -> CostComponentDO.penaltyCoefficient +``` + +每个费用项先按 `LATE_FEE_DECIMAL` 四舍五入,再汇总到账单理论违约金。封顶在账单理论违约金汇总后执行,封顶结果再按 `LATE_FEE_DECIMAL` 规范化一次。 + +### 小数位 + +`LATE_FEE_DECIMAL` 控制违约金保留小数位数: + +- 优先使用 `settingValue` +- `settingValue` 为空、非数字、负数时回退 `settingDefault` +- `settingDefault` 仍非法时按 `0` +- 取整方式固定四舍五入 +- `COUNTER_DECIMAL_WAY` 不参与本次违约金计算 + +### 封顶 + +`LATE_FEE_MORE_PRINCIPAL` 控制违约金封顶: + +- 空值、非数字、负数按 `1` +- `=0` 表示不设上限 +- `>0` 表示理论违约金不得超过当前未缴本金乘以配置倍数 + +处理顺序: + +1. 逐费用项计算原始违约金 +2. 每个费用项先按 `LATE_FEE_DECIMAL` 四舍五入 +3. 汇总得到账单理论违约金 +4. 按 `LATE_FEE_MORE_PRINCIPAL` 对账单理论违约金封顶 +5. 封顶结果再按 `LATE_FEE_DECIMAL` 四舍五入后作为可扣减前理论违约金 + +## 已收与减免扣减 + +每日任务覆盖账单当前应收违约金时,必须扣减已收违约金和已审批生效的违约金减免金额。 + +公式: + +```text +当前应收违约金 = max(0, 理论违约金 - 已收违约金 - 已生效违约金减免金额) +``` + +已收违约金: + +- 从成功且未被撤销/红冲的收费或支付明细汇总 +- 只汇总实际违约金金额,例如 `late_fee_amount` +- 不把本金支付误算为违约金支付 + +已生效违约金减免金额: + +- 只统计审批通过且已生效的违约金减免记录 +- 待审批、驳回、撤销的减免记录不参与扣减 + +## 主要影响范围 + +后端实现预计涉及: + +- 开账服务:生成账单时计算并保存 `lateFeeBeginDate` +- 系统参数读取:读取 `CALC_LATE_FEE_TYPE`、`CALC_LATE_FEE_SETTING`、`LATE_FEE_DECIMAL`、`LATE_FEE_MORE_PRINCIPAL` +- 违约金计算器:统一封装起算日和金额计算 +- 每日任务:扫描未结清账单,幂等重算并覆盖当前应收违约金 +- 账单明细读取:获取费用项本金、费用项编码和违约金系数 +- 支付明细汇总:汇总已收违约金 +- 违约金减免汇总:汇总已审批生效减免金额 +- 异常记录:配置异常、`billDate` 缺失、`lateFeeBeginDate` 缺失等 + +## 测试与验收点 + +开账起算日: + +- `type=1` 当月某日起算 +- `type=2` 开账后 N 天,开账日不算第 1 天 +- `type=2 setting=0` +- `type=3` 下月某日起算 +- `type=4` 下下月某日起算 +- 配置日为 29/30/31 时遇到 2 月、4 月等短月 +- 开账日晚于宽限截止日时,起算日取 `billDate + 1` +- `billDate` 为空时不生成起算日并记录异常 +- `CALC_LATE_FEE_SETTING` 非法时回退默认值 +- `CALC_LATE_FEE_SETTING` 默认值仍非法时不生成起算日并记录异常 + +每日重算: + +- 当前日期早于起算日,逾期天数为 0 +- 当前日期等于起算日,逾期天数为 1 +- 当前日期晚于起算日,逾期天数包含当前日期 +- 未缴账单参与重算 +- 部分缴费账单按当前未缴本金重算 +- 已结清账单不重算 +- `lateFeeBeginDate` 为空的历史未结清账单不计算并记录异常 +- 重复执行任务结果不变 + +金额计算: + +- 逐费用项按不同 `penaltyCoefficient` 计算并汇总 +- 没有费用项级剩余本金时按比例分摊账单剩余未缴本金 +- 分摊尾差不造成汇总不平 +- `LATE_FEE_DECIMAL` 控制逐项舍入和最终结果 +- `LATE_FEE_DECIMAL` 非法时回退默认值,默认值非法时按 0 +- `LATE_FEE_MORE_PRINCIPAL` 空值按 1 +- `LATE_FEE_MORE_PRINCIPAL=0` 不封顶 +- `LATE_FEE_MORE_PRINCIPAL>0` 按当前未缴本金倍数封顶 +- 逐项舍入、汇总、封顶、最终舍入的顺序固定 +- 已收违约金扣减 +- 已审批生效违约金减免扣减 +- 扣减后结果不小于 0 + +## 非目标 + +本次不覆盖: + +- 违约金减免流程重构 +- 通知状态判断 +- 已结清账单历史金额重算 +- 参数变更后自动追溯修改历史未结清账单起算日 +- 查询实时计算替代落库金额