Case 3: Insurance claim adjustment
This case corresponds to Scenario 3: Insurance claim adjustment (production-grade template) in the JQuick-Java repository’s USE-CASES.md, in the insurance domain. It automates claim adjustment for four product types — medical, critical illness, accident, and auto insurance: following the real adjustment pipeline of “waiting-period eligibility check → reimbursement ratio → deductible → settlement → product cap → risk scoring → channel routing”, it outputs the adjustment verdict, payout amount, and handling channel.
Industry scenario
Section titled “Industry scenario”Claim adjustment is the step in an insurer’s claim process that decides “whether to pay, how much to pay, and who pays”. After a customer files a claim, the claim system collects case elements such as product type (medical / critical illness / accident / auto), hospital tier (tier-3A / tier-2A / community), claimed amount, invoice and itemized expense amounts, the policy’s contractual deductible, days insured versus the product’s waiting period, claim counts in the past year, risk-control flag counts (blacklist, duplicate filing, image anomalies, etc.), whether the event was an accident, and whether the documents are complete.
Adjustment starts with an eligibility check: cases whose insured days are fewer than the waiting-period days bear no liability and are rejected outright. Cases passing eligibility enter settlement: the reimbursement ratio is determined by the cross of product type, hospital tier, and accident status (medical: 85% at tier-3A, 75% at tier-2A, 60% at community; critical illness: 100%; accident: 90%; auto: 90% for accidents, 70% otherwise). The actual deductible is adjusted by product (zero for critical illness, halved for accident, and never exceeding the claimed amount). The payout uses the smaller of the claimed amount and the invoice amount as the expense base, subtracts the deductible, then multiplies by the ratio. Finally, per-product caps are applied — 200k for medical, 500k for critical illness, 300k for auto, 100k for accident.
Running in parallel with settlement is anti-fraud risk control: the risk score is built from historical claim counts (10 points each), risk-control flag counts (25 points each), and large-claim bonuses (+20 for claims above 100k, +10 for above 50k), mapping to LOW/MEDIUM/HIGH. Cases with 2 or more risk-control flags, or 1 flag with an amount of at least 50k, must be routed to the investigation desk.
The final output includes the adjustment verdict (APPROVE for normal payout / MANUAL_AUDIT for manual review / REJECT for denial), payout amount, actual deductible, reimbursement ratio, risk score and risk level, whether to route to investigation, whether to use the small-claim fast track, and itemized adjustment notes (waiting-period denial, cap reached, incomplete documents, fast-track admission, high claim frequency, etc.) for claim handlers to act on and explain to customers. Cases with complete documents, payout not exceeding 5,000 yuan, and no claim history enter the small-claim fast track for near-instant payment.
Business pain points
Section titled “Business pain points”- Complex, frequently changing ratio matrices: the cross ratios of 4 product types × 3 hospital tiers × accident status are hard-coded in the claim core system; adding a partner hospital network, filing a new product, or adjusting a ratio all require core code changes and a new release.
- Waiting periods, deductibles, and caps follow policy terms: zero deductible for critical illness, half deductible for accident, and the 200k/500k/300k/100k caps for medical/critical-illness/auto/accident all come from product filing terms; as product lines grow, policies scatter and stale terms are easily misapplied, causing wrong or excess payouts.
- Long, coupled settlement pipeline: waiting-period check, ratio, deductible, min-of-two-amounts, multiplication, capping, and negative-guard — an error in any step directly produces a wrong payout, and when everything is written in one big method it is hard to test or locate per segment.
- Risk routing tangled with settlement lets fraud slip through: conditions like flag-based investigation routing, large-claim review, and small-claim fast-track admission are entangled with settlement logic, creating loopholes where a case with “2 risk flags + an 80k large claim” could still be auto-paid.
- Unexplainable verdicts: with only a payout figure and no adjustment notes, denials for waiting period, reduced payout at the cap, or manual review for incomplete documents are hard to justify in customer complaints and regulatory inspections.
- Aging insurance cores, difficult domestic (Xinchuang) adoption: many life/P&C core systems still run on JDK 8 and domestic JDKs and cannot bear the middleware and database operations cost of a heavy rule platform.
Solution architecture
Section titled “Solution architecture”This case uses the six-file structure of “domain model + rule interface & XML rule script + orchestration service + tests”: waiting period, ratios, deductibles, caps, risk, and routing are all externalized in XML rules, while the Service chains the adjustment pipeline, re-checks caps, and assembles adjustment notes.
| Layer | Responsibility | Files |
|---|---|---|
| Domain model | Claim filing elements, adjustment result | ClaimRequest.java, ClaimDecision.java |
| Rule set & XML | Waiting period, ratio, deductible, settlement, cap, risk, channel routing — all policies externalized | ClaimAuditRules.java, claim-audit.xml |
| Orchestration service | Pipeline chaining, cap re-check, investigation override, adjustment note assembly and amount precision handling | ClaimAuditService.java |
| Tests | 6 groups of adjustment business shapes | ClaimAuditServiceTest.java |
Rule method breakdown
Section titled “Rule method breakdown”The rule interface has 10 methods, arranged strictly along the real adjustment pipeline, each doing exactly one thing:
| Rule method | Policy | Score / output |
|---|---|---|
withinWaitingPeriod |
Insured days fewer than waiting-period days means the policy is in its waiting period | boolean |
reimburseRatio |
Product type × hospital tier × accident status: medical 0.85/0.75/0.6, critical illness 1.0, accident 0.9, auto 0.9/0.7 | 0.6 ~ 1.0 |
actualDeductible |
Zero deductible for critical illness, half for accident, never exceeding the claimed amount | Actual deductible (yuan) |
payableAmount |
(min(claimed amount, invoice amount) − deductible) × ratio, never negative | Payout amount (yuan) |
applyCap |
Caps: 200k medical / 500k critical illness / 300k auto / 100k accident | Capped amount (yuan) |
riskScore |
Historical claims × 10 + risk flags × 25 + large-claim bonus (≥100k +20, ≥50k +10) | Risk score |
riskLevel |
≥60 HIGH / ≥30 MEDIUM / otherwise LOW | LOW / MEDIUM / HIGH |
needInvestigation |
≥2 risk flags, or ≥1 flag with amount ≥50k | boolean |
auditResult |
Waiting period → REJECT; flags → MANUAL_AUDIT; below deductible → REJECT; complete documents → APPROVE, otherwise MANUAL_AUDIT | APPROVE / MANUAL_AUDIT / REJECT |
fastTrack |
Complete documents + payout ≤5000 yuan + no claim history | boolean |
Code implementation (complete & runnable)
Section titled “Code implementation (complete & runnable)”Place the six files into the corresponding src/test directories of a Maven project following the original package structure: model classes ClaimRequest.java and ClaimDecision.java under com.github.paohaijiao.demo.model; the rule interface ClaimAuditRules.java and test class ClaimAuditServiceTest.java under com.github.paohaijiao.demo; the orchestration service ClaimAuditService.java under com.github.paohaijiao.demo.service; and the rule script claim-audit.xml under src/test/resources/demo/. Keep the package names exactly as in the source; the model classes depend on Lombok’s @Data annotation. Run mvn test -Dtest=ClaimAuditServiceTest at the project root to execute all 6 test cases.
ClaimRequest.java
Section titled “ClaimRequest.java”package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;
/** * 理赔申请单(报案要素) */@Data@NoArgsConstructor@AllArgsConstructorpublic class ClaimRequest {
/** * 理赔单号 */ private String claimNo;
/** * 保单号 */ private String policyNo;
/** * 被保险人 */ private String insuredName;
/** * 产品类型:MEDICAL-医疗险 / CRITICAL_ILLNESS-重疾险 / ACCIDENT-意外险 / AUTO-车险 */ private String productType;
/** * 就诊医院等级:THIRD_A-三甲 / SECOND_A-二甲 / COMMUNITY-社区医院 */ private String hospitalLevel;
/** * 申请赔付金额(元) */ private double claimAmount;
/** * 发票/费用清单金额(元) */ private double invoiceAmount;
/** * 保单约定免赔额(元) */ private double deductible;
/** * 已投保天数 */ private int insuredDays;
/** * 产品等待期天数 */ private int waitingPeriodDays;
/** * 近 1 年历史理赔次数 */ private int priorClaimCount;
/** * 风控标记次数(黑名单、重复报案、影像异常等) */ private int fraudFlagCount;
/** * 是否意外出险 */ private boolean isAccident;
/** * 理赔材料是否齐全 */ private boolean documentComplete;}ClaimDecision.java
Section titled “ClaimDecision.java”package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;import java.util.List;
/** * 理赔核赔结果 */@Datapublic class ClaimDecision {
/** * 理赔单号 */ private String claimNo;
/** * 核赔结论:APPROVE-正常赔付 / MANUAL_AUDIT-转人工审核 / REJECT-拒赔 */ private String auditResult;
/** * 赔付金额(元) */ private double payableAmount;
/** * 实际扣减的免赔额(元) */ private double deductibleAmount;
/** * 适用赔付比例 */ private double reimburseRatio;
/** * 理赔风险分 */ private int riskScore;
/** * 理赔风险等级:LOW / MEDIUM / HIGH */ private String riskLevel;
/** * 是否需要转调查岗 */ private boolean needInvestigation;
/** * 是否走小额快赔通道 */ private boolean fastTrack;
/** * 核赔说明,供理赔员与客户解释使用 */ private List<String> auditNotes = new ArrayList<String>();
public void addAuditNote(String note) { this.auditNotes.add(note); }}ClaimAuditRules.java
Section titled “ClaimAuditRules.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.xml.param.Param;
/** * 场景 3(生产级样板):保险理赔核赔规则集 * <p> * 规则按核赔实务拆分为「资格校验 → 比例与免赔额 → 赔付计算 → 封顶 → 风险与通道路由」, * 由 {@code ClaimAuditService} 串联执行,规则脚本见 * {@code src/test/resources/demo/claim-audit.xml}。 */public interface ClaimAuditRules {
/** * 是否处于等待期(等待期内不承担赔付责任) */ boolean withinWaitingPeriod(@Param("insuredDays") int insuredDays, @Param("waitingPeriodDays") int waitingPeriodDays);
/** * 赔付比例:产品类型 × 医院等级 × 是否意外 */ double reimburseRatio(@Param("productType") String productType, @Param("hospitalLevel") String hospitalLevel, @Param("isAccident") boolean isAccident);
/** * 实际扣减的免赔额(重疾免赔、意外减半、不超过申请金额) */ double actualDeductible(@Param("productType") String productType, @Param("deductible") double deductible, @Param("claimAmount") double claimAmount);
/** * 赔付金额 =(实际费用 - 免赔额)× 赔付比例 */ double payableAmount(@Param("claimAmount") double claimAmount, @Param("invoiceAmount") double invoiceAmount, @Param("ratio") double ratio, @Param("deductibleAmount") double deductibleAmount);
/** * 产品封顶线控制 */ double applyCap(@Param("payableAmount") double payableAmount, @Param("productType") String productType);
/** * 理赔风险分:历史理赔次数 + 风控标记 + 大额报案 */ int riskScore(@Param("priorClaimCount") int priorClaimCount, @Param("fraudFlagCount") int fraudFlagCount, @Param("claimAmount") double claimAmount);
/** * 理赔风险等级:LOW / MEDIUM / HIGH */ String riskLevel(@Param("riskScore") int riskScore);
/** * 是否需要转调查岗 */ boolean needInvestigation(@Param("fraudFlagCount") int fraudFlagCount, @Param("claimAmount") double claimAmount);
/** * 核赔结论:APPROVE / MANUAL_AUDIT / REJECT */ String auditResult(@Param("withinWaitingPeriod") boolean withinWaitingPeriod, @Param("fraudFlagCount") int fraudFlagCount, @Param("documentComplete") boolean documentComplete, @Param("payableAmount") double payableAmount);
/** * 是否走小额快赔通道 */ boolean fastTrack(@Param("payableAmount") double payableAmount, @Param("documentComplete") boolean documentComplete, @Param("priorClaimCount") int priorClaimCount);}claim-audit.xml
Section titled “claim-audit.xml”<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE javas PUBLIC "-//PAOHAIJIAO//DTD API JAVA 1.0//EN" "classpath:paohaijiao/dtd/Jquick-java.dtd"><javas namespace="com.github.paohaijiao.demo.ClaimAuditRules">
<!-- 等待期校验:等待期内不承担赔付责任 --> <java name="withinWaitingPeriod" returnClass="boolean"> <![CDATA[ boolean def withinWaitingPeriod(int:insuredDays, int:waitingPeriodDays) { if (insuredDays < waitingPeriodDays) { return true; } else { return false; } } ]]> </java>
<!-- 赔付比例:产品 × 医院等级 × 意外与否 --> <java name="reimburseRatio" returnClass="double"> <![CDATA[ double def reimburseRatio(str:productType, str:hospitalLevel, boolean:isAccident) { double ratio = 0.6; if (productType == "MEDICAL") { if (hospitalLevel == "THIRD_A") { ratio = 0.85; } else if (hospitalLevel == "SECOND_A") { ratio = 0.75; } else { ratio = 0.6; } } else if (productType == "CRITICAL_ILLNESS") { ratio = 1.0; } else if (productType == "ACCIDENT") { ratio = 0.9; } else if (productType == "AUTO") { if (isAccident) { ratio = 0.9; } else { ratio = 0.7; } } return ratio; } ]]> </java>
<!-- 实际免赔额:重疾免赔额为 0,意外险减半,且不超过申请金额 --> <java name="actualDeductible" returnClass="double"> <![CDATA[ double def actualDeductible(str:productType, double:deductible, double:claimAmount) { double actual = deductible; if (productType == "CRITICAL_ILLNESS") { actual = 0.0; } else if (productType == "ACCIDENT") { actual = deductible * 0.5; } if (actual > claimAmount) { actual = claimAmount; } return actual; } ]]> </java>
<!-- 赔付金额计算:以发票金额与申请金额的较小者为费用基数 --> <java name="payableAmount" returnClass="double"> <![CDATA[ double def payableAmount(double:claimAmount, double:invoiceAmount, double:ratio, double:deductibleAmount) { double base = claimAmount; if (invoiceAmount < claimAmount) { base = invoiceAmount; } double payable = (base - deductibleAmount) * ratio; if (payable < 0.0) { payable = 0.0; } return payable; } ]]> </java>
<!-- 产品封顶线 --> <java name="applyCap" returnClass="double"> <![CDATA[ double def applyCap(double:payableAmount, str:productType) { double capped = payableAmount; if (productType == "MEDICAL" && payableAmount > 200000.0) { capped = 200000.0; } else if (productType == "CRITICAL_ILLNESS" && payableAmount > 500000.0) { capped = 500000.0; } else if (productType == "AUTO" && payableAmount > 300000.0) { capped = 300000.0; } else if (productType == "ACCIDENT" && payableAmount > 100000.0) { capped = 100000.0; } return capped; } ]]> </java>
<!-- 理赔风险分 --> <java name="riskScore" returnClass="int"> <![CDATA[ int def riskScore(int:priorClaimCount, int:fraudFlagCount, double:claimAmount) { int score = 0; score = score + priorClaimCount * 10; score = score + fraudFlagCount * 25; if (claimAmount >= 100000.0) { score = score + 20; } else if (claimAmount >= 50000.0) { score = score + 10; } return score; } ]]> </java>
<!-- 理赔风险等级 --> <java name="riskLevel" returnClass="java.lang.String"> <![CDATA[ str def riskLevel(int:riskScore) { if (riskScore >= 60) { return "HIGH"; } else if (riskScore >= 30) { return "MEDIUM"; } else { return "LOW"; } } ]]> </java>
<!-- 是否转调查岗 --> <java name="needInvestigation" returnClass="boolean"> <![CDATA[ boolean def needInvestigation(int:fraudFlagCount, double:claimAmount) { if (fraudFlagCount >= 2) { return true; } else if (fraudFlagCount >= 1 && claimAmount >= 50000.0) { return true; } else { return false; } } ]]> </java>
<!-- 核赔结论 --> <java name="auditResult" returnClass="java.lang.String"> <![CDATA[ str def auditResult(boolean:withinWaitingPeriod, int:fraudFlagCount, boolean:documentComplete, double:payableAmount) { if (withinWaitingPeriod) { return "REJECT"; } else if (fraudFlagCount >= 2) { return "MANUAL_AUDIT"; } else if (payableAmount <= 0.0) { return "REJECT"; } else if (documentComplete) { return "APPROVE"; } else { return "MANUAL_AUDIT"; } } ]]> </java>
<!-- 小额快赔通道 --> <java name="fastTrack" returnClass="boolean"> <![CDATA[ boolean def fastTrack(double:payableAmount, boolean:documentComplete, int:priorClaimCount) { if (documentComplete && payableAmount <= 5000.0 && priorClaimCount == 0) { return true; } else { return false; } } ]]> </java>
</javas>ClaimAuditService.java
Section titled “ClaimAuditService.java”package com.github.paohaijiao.demo.service;
import com.github.paohaijiao.demo.ClaimAuditRules;import com.github.paohaijiao.demo.model.ClaimDecision;import com.github.paohaijiao.demo.model.ClaimRequest;
import java.math.BigDecimal;import java.math.RoundingMode;
/** * 场景 3:保险理赔核赔服务 * <p> * 核赔链路:等待期校验 → 赔付比例 → 免赔额扣减 → 赔付计算 → 封顶 → 风险评分与通道路由。 * 比例、免赔、封顶、路由阈值全部由 XML 规则外置管理,理算逻辑变化时无需发版。 */public class ClaimAuditService {
/** * 小额快赔通道默认额度上限(元),仅用于说明展示 */ private static final double FAST_TRACK_CAP = 5000.0;
private final ClaimAuditRules rules;
public ClaimAuditService(ClaimAuditRules rules) { this.rules = rules; }
/** * 执行理赔核赔 * * @param request 报案要素 * @return 核赔结果 */ public ClaimDecision audit(ClaimRequest request) { ClaimDecision decision = new ClaimDecision(); decision.setClaimNo(request.getClaimNo());
boolean withinWaiting = rules.withinWaitingPeriod(request.getInsuredDays(), request.getWaitingPeriodDays()); double ratio = rules.reimburseRatio(request.getProductType(), request.getHospitalLevel(), request.isAccident()); double deductibleAmount = rules.actualDeductible(request.getProductType(), request.getDeductible(), request.getClaimAmount()); double payable = rules.payableAmount(request.getClaimAmount(), request.getInvoiceAmount(), ratio, deductibleAmount); double capped = rules.applyCap(payable, request.getProductType()); if (capped > request.getClaimAmount()) { capped = request.getClaimAmount(); }
int score = rules.riskScore(request.getPriorClaimCount(), request.getFraudFlagCount(), request.getClaimAmount()); boolean investigation = rules.needInvestigation(request.getFraudFlagCount(), request.getClaimAmount()); String result = rules.auditResult(withinWaiting, request.getFraudFlagCount(), request.isDocumentComplete(), capped);
decision.setReimburseRatio(ratio); decision.setDeductibleAmount(scale(deductibleAmount)); decision.setRiskScore(score); decision.setRiskLevel(rules.riskLevel(score)); decision.setNeedInvestigation(investigation);
if ("REJECT".equals(result)) { decision.setAuditResult(result); decision.setPayableAmount(0.0); decision.setFastTrack(false); if (withinWaiting) { decision.addAuditNote("保单处于等待期(已投保 " + request.getInsuredDays() + " 天 < 等待期 " + request.getWaitingPeriodDays() + " 天),本次不承担赔付责任"); } else { decision.addAuditNote("扣除免赔额后无可赔付金额,本次拒赔"); } return decision; }
if (investigation && "APPROVE".equals(result)) { result = "MANUAL_AUDIT"; decision.addAuditNote("存在风控标记,转调查岗复核"); } decision.setAuditResult(result); decision.setPayableAmount(scale(capped)); decision.setFastTrack(rules.fastTrack(capped, request.isDocumentComplete(), request.getPriorClaimCount()));
if (payable > capped) { decision.addAuditNote("赔付金额触达产品封顶线,按封顶金额赔付"); } if ("MANUAL_AUDIT".equals(result) && !request.isDocumentComplete()) { decision.addAuditNote("理赔材料不齐全,转人工审核"); } if (decision.isFastTrack()) { decision.addAuditNote("赔付金额不超过 " + (int) FAST_TRACK_CAP + " 元且材料齐全,进入小额快赔通道"); } if (request.getPriorClaimCount() >= 3) { decision.addAuditNote("近 1 年理赔 " + request.getPriorClaimCount() + " 次,理赔频次偏高"); } if ("MEDICAL".equals(request.getProductType()) && "COMMUNITY".equals(request.getHospitalLevel())) { decision.addAuditNote("社区医院就诊,赔付比例按 60% 执行"); } return decision; }
private double scale(double value) { return BigDecimal.valueOf(value).setScale(2, RoundingMode.HALF_UP).doubleValue(); }}ClaimAuditServiceTest.java
Section titled “ClaimAuditServiceTest.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.demo.model.ClaimDecision;import com.github.paohaijiao.demo.model.ClaimRequest;import com.github.paohaijiao.demo.service.ClaimAuditService;import com.github.paohaijiao.xml.JQuickJava;import org.junit.Assert;import org.junit.Before;import org.junit.Test;
/** * 场景 3 生产级样板:保险理赔核赔 * <p> * 覆盖 6 组核赔形态:门诊小额快赔、重疾大额封顶、等待期拒赔、风控标记转调查、 * 材料不全转人工、未达免赔额拒赔。 */public class ClaimAuditServiceTest {
private ClaimAuditService service;
@Before public void setUp() { ClaimAuditRules rules = JQuickJava.create() .createApi(ClaimAuditRules.class, "demo/claim-audit.xml"); this.service = new ClaimAuditService(rules); }
/** * 门诊小额:三甲医院医疗险,赔款 2465 元,材料齐全且无历史理赔 → 正常赔付并走快赔通道 */ @Test public void shouldApproveOutpatientClaimWithFastTrack() { ClaimRequest request = new ClaimRequest(); request.setClaimNo("CLM20260826001"); request.setPolicyNo("POL20250101001"); request.setInsuredName("刘洋"); request.setProductType("MEDICAL"); request.setHospitalLevel("THIRD_A"); request.setClaimAmount(3200.0); request.setInvoiceAmount(3200.0); request.setDeductible(300.0); request.setInsuredDays(400); request.setWaitingPeriodDays(30); request.setPriorClaimCount(0); request.setFraudFlagCount(0); request.setAccident(false); request.setDocumentComplete(true);
ClaimDecision decision = service.audit(request);
Assert.assertEquals("APPROVE", decision.getAuditResult()); Assert.assertEquals(2465.0, decision.getPayableAmount(), 0.001); Assert.assertEquals(300.0, decision.getDeductibleAmount(), 0.001); Assert.assertEquals(0.85, decision.getReimburseRatio(), 0.0001); Assert.assertEquals(0, decision.getRiskScore()); Assert.assertEquals("LOW", decision.getRiskLevel()); Assert.assertFalse(decision.isNeedInvestigation()); Assert.assertTrue(decision.isFastTrack()); Assert.assertTrue(decision.getAuditNotes().contains("赔付金额不超过 5000 元且材料齐全,进入小额快赔通道")); }
/** * 重疾大额:赔款触达 50 万封顶线,风险分为 30(MEDIUM) */ @Test public void shouldApplyCapForCriticalIllnessClaim() { ClaimRequest request = new ClaimRequest(); request.setClaimNo("CLM20260826002"); request.setPolicyNo("POL20240101002"); request.setInsuredName("周静"); request.setProductType("CRITICAL_ILLNESS"); request.setHospitalLevel("THIRD_A"); request.setClaimAmount(600000.0); request.setInvoiceAmount(580000.0); request.setDeductible(10000.0); request.setInsuredDays(800); request.setWaitingPeriodDays(90); request.setPriorClaimCount(1); request.setFraudFlagCount(0); request.setAccident(false); request.setDocumentComplete(true);
ClaimDecision decision = service.audit(request);
Assert.assertEquals("APPROVE", decision.getAuditResult()); Assert.assertEquals(500000.0, decision.getPayableAmount(), 0.001); Assert.assertEquals(0.0, decision.getDeductibleAmount(), 0.001); Assert.assertEquals(1.0, decision.getReimburseRatio(), 0.0001); Assert.assertEquals(30, decision.getRiskScore()); Assert.assertEquals("MEDIUM", decision.getRiskLevel()); Assert.assertFalse(decision.isFastTrack()); Assert.assertTrue(decision.getAuditNotes().contains("赔付金额触达产品封顶线,按封顶金额赔付")); }
/** * 等待期内出险:已投保 20 天 < 等待期 30 天 → 拒赔且赔款为 0 */ @Test public void shouldRejectClaimWithinWaitingPeriod() { ClaimRequest request = new ClaimRequest(); request.setClaimNo("CLM20260826003"); request.setPolicyNo("POL20260801003"); request.setInsuredName("吴磊"); request.setProductType("MEDICAL"); request.setHospitalLevel("THIRD_A"); request.setClaimAmount(8000.0); request.setInvoiceAmount(8000.0); request.setDeductible(300.0); request.setInsuredDays(20); request.setWaitingPeriodDays(30); request.setPriorClaimCount(0); request.setFraudFlagCount(0); request.setAccident(false); request.setDocumentComplete(true);
ClaimDecision decision = service.audit(request);
Assert.assertEquals("REJECT", decision.getAuditResult()); Assert.assertEquals(0.0, decision.getPayableAmount(), 0.001); Assert.assertFalse(decision.isFastTrack()); Assert.assertTrue(decision.getAuditNotes().contains( "保单处于等待期(已投保 20 天 < 等待期 30 天),本次不承担赔付责任")); }
/** * 风控标记:2 次标记 + 大额意外报案 → 转调查岗,风险分 80(HIGH) */ @Test public void shouldRouteFlaggedClaimToInvestigation() { ClaimRequest request = new ClaimRequest(); request.setClaimNo("CLM20260826004"); request.setPolicyNo("POL20250101004"); request.setInsuredName("郑凯"); request.setProductType("ACCIDENT"); request.setHospitalLevel("THIRD_A"); request.setClaimAmount(80000.0); request.setInvoiceAmount(90000.0); request.setDeductible(500.0); request.setInsuredDays(300); request.setWaitingPeriodDays(30); request.setPriorClaimCount(2); request.setFraudFlagCount(2); request.setAccident(true); request.setDocumentComplete(true);
ClaimDecision decision = service.audit(request);
Assert.assertEquals("MANUAL_AUDIT", decision.getAuditResult()); Assert.assertEquals(71775.0, decision.getPayableAmount(), 0.001); Assert.assertEquals(250.0, decision.getDeductibleAmount(), 0.001); Assert.assertEquals(0.9, decision.getReimburseRatio(), 0.0001); Assert.assertEquals(80, decision.getRiskScore()); Assert.assertEquals("HIGH", decision.getRiskLevel()); Assert.assertTrue(decision.isNeedInvestigation()); Assert.assertFalse(decision.isFastTrack()); }
/** * 材料不全:车险意外小额,赔款 3600 元但材料缺失 → 转人工审核 */ @Test public void shouldRouteIncompleteDocumentClaimToManualAudit() { ClaimRequest request = new ClaimRequest(); request.setClaimNo("CLM20260826005"); request.setPolicyNo("POL20250201005"); request.setInsuredName("何欣"); request.setProductType("AUTO"); request.setHospitalLevel("THIRD_A"); request.setClaimAmount(4000.0); request.setInvoiceAmount(4000.0); request.setDeductible(0.0); request.setInsuredDays(500); request.setWaitingPeriodDays(30); request.setPriorClaimCount(0); request.setFraudFlagCount(0); request.setAccident(true); request.setDocumentComplete(false);
ClaimDecision decision = service.audit(request);
Assert.assertEquals("MANUAL_AUDIT", decision.getAuditResult()); Assert.assertEquals(3600.0, decision.getPayableAmount(), 0.001); Assert.assertEquals(0.9, decision.getReimburseRatio(), 0.0001); Assert.assertFalse(decision.isFastTrack()); Assert.assertTrue(decision.getAuditNotes().contains("理赔材料不齐全,转人工审核")); }
/** * 未达免赔额:费用 500 元、免赔额 800 元 → 拒赔 */ @Test public void shouldRejectClaimBelowDeductible() { ClaimRequest request = new ClaimRequest(); request.setClaimNo("CLM20260826006"); request.setPolicyNo("POL20250401006"); request.setInsuredName("林小雨"); request.setProductType("MEDICAL"); request.setHospitalLevel("COMMUNITY"); request.setClaimAmount(500.0); request.setInvoiceAmount(500.0); request.setDeductible(800.0); request.setInsuredDays(200); request.setWaitingPeriodDays(30); request.setPriorClaimCount(0); request.setFraudFlagCount(0); request.setAccident(false); request.setDocumentComplete(true);
ClaimDecision decision = service.audit(request);
Assert.assertEquals("REJECT", decision.getAuditResult()); Assert.assertEquals(0.0, decision.getPayableAmount(), 0.001); Assert.assertTrue(decision.getAuditNotes().contains("扣除免赔额后无可赔付金额,本次拒赔")); }}Run & verified results
Section titled “Run & verified results”Run this case with the following command at the project root:
mvn test -Dtest=ClaimAuditServiceTestTest coverage
Section titled “Test coverage”The test class has 6 @Test methods covering six adjustment shapes: small-claim fast track, large-claim capping, waiting-period denial, risk-flag investigation routing, manual review for incomplete documents, and below-deductible denial:
| Test method | Constructed business shape | Key assertions / expected decision |
|---|---|---|
shouldApproveOutpatientClaimWithFastTrack |
Medical insurance at a tier-3A hospital, claim and invoice both 3200 yuan, deductible 300, insured 400 days (past waiting period), complete documents, no claim history | APPROVE, payout 2465.0 ((3200−300)×0.85), deductible 300.0, ratio 0.85, risk score 0 LOW, no investigation, fast track true, notes include “entered the small-claim fast track” |
shouldApplyCapForCriticalIllnessClaim |
Critical illness insurance, claim 600k vs. invoice 580k, contractual deductible 10k, 90-day waiting period passed, 1 historical claim | APPROVE, payout 500000.0 (hits the 500k cap), actual deductible 0.0, ratio 1.0, risk score 30 MEDIUM, not fast track, notes include “hits the product cap” |
shouldRejectClaimWithinWaitingPeriod |
Medical insurance, insured 20 days vs. 30-day waiting period, claim 8000 yuan, complete documents | REJECT, payout 0.0, not fast track, notes include 已投保 20 天 < 等待期 30 天 (policy in waiting period) |
shouldRouteFlaggedClaimToInvestigation |
Accident insurance, claim 80k vs. invoice 90k, deductible 500, 2 historical claims, 2 risk flags, large accident claim | MANUAL_AUDIT, payout 71775.0 ((80000−250)×0.9), deductible 250.0 (halved for accident), ratio 0.9, risk score 80 HIGH, investigation needed, not fast track |
shouldRouteIncompleteDocumentClaimToManualAudit |
Auto accident, claim and invoice both 4000 yuan, zero deductible, incomplete documents | MANUAL_AUDIT, payout 3600.0 (4000×0.9), ratio 0.9, not fast track, notes include “incomplete documents, routed to manual review” |
shouldRejectClaimBelowDeductible |
Medical insurance at a community hospital, expense 500 yuan vs. deductible 800 yuan, complete documents | REJECT, payout 0.0, notes include “no payable amount after deductible, denied” |
Verified output
Section titled “Verified output”Small outpatient medical claim (tier-3A hospital, expense 3200 yuan, deductible 300 yuan, complete documents):
核赔结论: APPROVE 赔付金额: 2465.0 实际免赔额: 300.0赔付比例: 0.85 风险分: 0 (LOW) 快赔通道: trueLarge critical illness claim (claim 600k, invoice 580k, zero deductible for critical illness, 100% ratio):
核赔结论: APPROVE 赔付金额: 500000.0(触达 50 万封顶线)风险分: 30 (MEDIUM) 快赔通道: falseTechnical highlights
Section titled “Technical highlights”- All adjustment policies externalized in XML — no release for settlement changes: the product × hospital tier × accident ratio matrix, zero-deductible-for-critical-illness and half-for-accident rules, the 200k/500k/300k/100k caps, and the 5000-yuan fast-track line all live in
claim-audit.xml; product term updates only require editing the XML and rebuilding the proxy. - Ten pipeline steps, each an independently testable method: waiting period, ratio, deductible, settlement, capping, risk score, risk level, investigation, verdict, and fast track are each an independent rule method — any policy can be tested and audited alone, avoiding the mis-payouts that long tangled pipelines cause.
- Amount guards built into the rule scripts:
payableAmountzeroes out negative settlement results so a deductible larger than the expense can never produce a negative payout;actualDeductibleis capped at the claimed amount so the deductible cannot exceed the expense base. - Service-side double-check and override: after rule capping, the Java side takes the min against the claimed amount to prevent over-claim payouts; when the investigation flag hits, an APPROVE from the rules is overridden to MANUAL_AUDIT; amounts are uniformly rounded to two decimals with
BigDecimal, and adjustment notes are assembled per hit step. - Positive-form returns fit the script syntax: boolean rules like
withinWaitingPeriodandfastTrackare written in the positive formif (...) { return true; } else { return false; }, avoiding reliance on negation operators and respecting the engine script’s grammar constraints. @Paramname binding + proxy reuse + ASM invocation chain: rule methods only take scalars; the claim object is decomposed by the Service; the proxy is created once and reused across all 6 cases; the engine targets Java 8 with no heavy dependencies and embeds into aging insurance cores and domestic (Xinchuang) JDKs, supporting high-frequency online claim settlement.