Commit e9254ee5 by jianan

保费到期提醒8

parent f8230861
package com.yd.csf.service.utils;
import com.yd.csf.feign.enums.PaymentFrequencyEnum;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Objects;
/**
* 保单续期保费到期日计算器。
*
* <p>规则对齐现有 Excel 模板「保费到期日」公式:</p>
* <ul>
* <li>年缴每 12 个月、季缴每 3 个月、月缴每 1 个月为一个缴费日,以生效日为锚点;</li>
* <li>取第一个严格晚于今天的缴费日;</li>
* <li>缴费期满日 = 生效日 + 缴费年期年数 - 1 天,下一缴费日晚于期满日表示已缴完;</li>
* <li>闰日(2/29)在非闰年由 LocalDate.plusMonths/plusYears 自动钳为 2/28。</li>
* </ul>
*/
public final class RenewalDueDateCalculator {
private RenewalDueDateCalculator() {
}
/**
* 计算单张保单的下一期保费到期日。
*
* @param effectiveDate 生效日
* @param frequency 缴费频率(PaymentFrequencyEnum 的 itemValue:YEAR/SEASON/MONTH/FULL_PAYMENT)
* @param issueNumber 缴费年期(年),库中类型不规整,可能为 Integer/String/BigDecimal
* @param today 当前日期(参数化便于测试)
* @return 计算结果;不纳入时 dueDate 为空并带跳过原因
*/
public static Result calculate(LocalDate effectiveDate, String frequency, Object issueNumber, LocalDate today) {
String freq = StringUtils.trimToEmpty(frequency);
if (PaymentFrequencyEnum.FULL_PAYMENT.getItemValue().equals(freq)) {
return Result.skip("整付保单无续期保费", false);
}
int stepMonths;
if (PaymentFrequencyEnum.YEAR.getItemValue().equals(freq)) {
stepMonths = 12;
} else if (PaymentFrequencyEnum.SEASON.getItemValue().equals(freq)) {
stepMonths = 3;
} else if (PaymentFrequencyEnum.MONTH.getItemValue().equals(freq)) {
stepMonths = 1;
} else if (StringUtils.isBlank(freq)) {
return Result.skip("缴费频率为空", true);
} else {
return Result.skip("无法识别的缴费频率: " + freq, true);
}
if (Objects.isNull(effectiveDate)) {
return Result.skip("生效日为空", true);
}
Integer years = parseIssueNumber(issueNumber);
if (years == null) {
return Result.skip("缴费年期为空或无法解析: " + issueNumber, true);
}
if (years <= 0) {
return Result.skip("缴费年期非法: " + years, true);
}
LocalDate endDate = effectiveDate.plusYears(years).minusDays(1);
LocalDate candidate = effectiveDate;
while (!candidate.isAfter(today)) {
candidate = candidate.plusMonths(stepMonths);
if (candidate.isAfter(endDate)) {
return Result.skip("保单缴费期已满", false);
}
}
return Result.included(candidate);
}
/**
* 解析缴费年期为整数年,兼容 "5"、"5.0"、5、5.0(BigDecimal) 等存储形态。
*/
private static Integer parseIssueNumber(Object issueNumber) {
if (Objects.isNull(issueNumber)) {
return null;
}
if (issueNumber instanceof Number) {
BigDecimal bd = new BigDecimal(issueNumber.toString());
return bd.signum() > 0 ? bd.intValue() : null;
}
String s = StringUtils.trimToNull(issueNumber.toString());
if (s == null) {
return null;
}
try {
BigDecimal bd = new BigDecimal(s);
return bd.signum() > 0 ? bd.intValue() : null;
} catch (NumberFormatException e) {
return null;
}
}
/**
* 计算结果:dueDate 非空表示纳入提醒,为空时 skipReason 说明原因。
*/
@Getter
public static class Result {
private final LocalDate dueDate;
private final String skipReason;
/**
* 是否为异常数据跳过(缺字段/无法解析);整付、缴完等正常排除为 false
*/
private final boolean abnormal;
private Result(LocalDate dueDate, String skipReason, boolean abnormal) {
this.dueDate = dueDate;
this.skipReason = skipReason;
this.abnormal = abnormal;
}
public static Result included(LocalDate dueDate) {
return new Result(dueDate, null, false);
}
public static Result skip(String reason, boolean abnormal) {
return new Result(null, reason, abnormal);
}
public boolean isIncluded() {
return dueDate != null;
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment