一笔理赔到底赔多少
目标
把条款中的保障条件转成代码,编写一个能输出单笔索赔的赔付金额及其依据的判定器。按固定顺序应用免赔额、共付比例、单笔限额和年度限额,按免责期和除外责任拒付,合并同一次事故的多笔索赔,然后对全部 141 笔索赔做判定,输出汇总和报告。
为什么重要
条款是用文字书写的,没有明确顺序。先扣免赔额还是先乘比例,免赔额按每笔索赔还是按每次事故计算,年度限额按事故日期还是受理日期计数,这些都会让同一笔索赔的赔付金额不同。而且哪一种都不会报错。 所以这类判定首先要解决的不是值对不对,而是能不能解释。把规则数值提取成表,并为每笔索赔记录哪条规则扣了多少,这份记录既是给客户的说明,也是回归测试的期望值。 本实验的条款数值(免赔额 1 万到 3 万韩元,共付 10% 到 20%,单笔限额 30 万到 100 万韩元,年度限额 100 万到 500 万韩元,免责期 30、90、180 天)并不是真实产品,而是为本实验虚构的合成表。 评分器不会相信你写的判定文字。它会在临时目录中搭建自己生成的条款、合同和索赔,每次用不同的数值实际运行你的判定器,并把赔付金额和判定依据与评分器另行计算的值进行核对。
步骤
- 创建并运行 /root/coverage/gen_claims.py,生成 /root/coverage/claims.db。包含 3 种条款、12 份合同、5 行除外责任、141 笔索赔。
- 比较两种应用顺序的合计,并在 /root/coverage/order_compare.json 中写入 claims、order_a_total、order_b_total、difference。
- 创建 /root/coverage/adjudicate.py,按免赔额 → 共付 → 单笔限额的顺序扣减,并把扣减的金额保留在 steps 中。
- 拒付保障期间之外、免责期之内以及属于除外责任项目的索赔。decision 为 denied,payable 为 0,reasons 中写入全部原因。
- 让免赔额每次事故只使用一次。同一次事故的第二笔索赔只扣减剩余的免赔额。
- 加入年度限额。按每份合同的事故年度累计,按受理日期顺序用尽,换年后重新恢复。
- 只从表中读取条款数值,并把共付计算的舍入固定为四舍五入(ROUND_HALF_UP)。
- 用 /root/coverage/make_payout.py 判定全部 141 笔索赔,生成 /root/coverage/payout.json 和 /root/coverage/coverage_report.md。
参考
- 运行约定:
python3 /root/coverage/adjudicate.py --db <DB> --claim <청구번호>会把一段判定 JSON 输出到标准输出,并以 0 退出。索赔编号不存在时退出码为 2。--all会逐行输出所有索赔的判定,每行一笔。 - 判定 JSON:
{"claim_id": …, "decision": "paid"|"denied", "billed": 정수, "payable": 정수, "reasons": [문자열], "steps": [{"rule": …, "amount": 깎은 금액, "after": 적용 뒤 금액}]} - steps 中的 rule 名称为
deductible、coinsurance、per_claim_cap、annual_cap,按实际应用的顺序放入。即使扣减金额为 0,也要保留该步骤。最后一个步骤的 after 就是 payable。 - 被拒付的索赔,payable 为 0,steps 为空列表,reasons 中放入
out_of_period、waiting_period、excluded中所有适用的项。 - 判定顺序:索赔按受理日期(filed_date)处理,同一天则按索赔编号顺序处理。累计限额取决于这个顺序。
- 免责期按事故日期与合同开始日期之间的间隔计算,而不是受理日期。
- 常见错误:按每笔索赔扣减免赔额,先乘比例,只看单笔索赔就判断年度限额,把舍入交给 Python 默认值。
- 不要把条款数值写在代码里。评分器会替换表中的值,检查判定是否随之变化。
创建条款表和索赔快照
创建并运行 /root/coverage/gen_claims.py,生成 /root/coverage/claims.db。包含 3 种条款、12 份合同、5 行除外责任、141 笔索赔(其中多笔索赔的事故 31 起)。
先创建 /root/coverage,然后在其中用 python3 创建 sqlite 数据库。表有 plan、policy、exclusion、claim 四张。索赔对应 100 起事故,其中一次事故对应两笔以上索赔的有 31 起。
应用顺序会使合计相差多少
在 /root/coverage/order_compare.json 中写入 claims、order_a_total、order_b_total、difference。A 是先扣免赔额再应用共付比例所得值的合计,B 是先应用共付比例再扣免赔额所得值的合计,两者都针对全部 141 笔索赔,负数按 0 处理。
这一步不是判定,而是比较。不要考虑限额、免责期和除外责任。A 是从 max(0, 索赔金额 - 免赔额) 中减去共付部分所得的值,B 是从索赔金额中减去共付部分,再减去免赔额,并在 0 处截断所得的值。共付部分按四舍五入取整到韩元的整数。
依次扣除免赔额、共付和单笔限额
创建 /root/coverage/adjudicate.py,按免赔额 → 共付 → 单笔限额的顺序,计算通过 --claim 传入的索赔的赔付金额,并把每条规则扣减的金额保留在 steps 中。
条款数值从 plan 表中读取。免赔额可能大于索赔金额,所以要用 min 截断;单笔限额只扣减超出的部分。steps 的每个元素有 rule、amount、after 三个键,最后一个 after 就是 payable。
按保障期间、免责期和除外项目拒付
拒付保障期间之外的事故、免责期之内的事故以及属于除外责任项目的索赔。decision 为 denied,payable 为 0,steps 为空列表,reasons 中放入 out_of_period、waiting_period、excluded 中所有适用的项。
免责期是事故日期与合同开始日期之间的间隔,而不是受理日期。日期请用 date.fromisoformat 和 timedelta 处理。保障期间之外的事故不必再判断免责期,但除外项目可以与之无关地同时记录。
免赔额每次事故只扣一次
让同一 accident_id 的索赔共用免赔额。第一笔索赔用完后,第二笔索赔的 deductible 步骤为 0;如果第一笔索赔小于免赔额,剩余的部分由下一笔索赔接着使用。
按事故记录已经用掉的免赔额,每次扣减剩余部分与索赔金额中较小的一个。索赔必须按受理日期顺序处理才能得到同样的答案——顺序一变,由哪笔索赔承担免赔额就会不同。
用尽年度限额
把年度限额作为第四条规则加入。按每份合同的事故年度累计,只扣减超出剩余额度的部分,换年后限额重新恢复。steps 的最后一项是 annual_cap。
年度限额取决于该合同之前的索赔已经用掉了多少。按受理日期顺序遍历判定,并按(合同、事故年度)累计已用金额。限额用尽后的索赔,赔付金额为 0,但并不是拒付——decision 仍是 paid,由 annual_cap 扣减全部金额。
数值放在表中,舍入作为策略
只从 plan 表中读取条款数值(不要把值写在代码里),并把共付计算的舍入固定为四舍五入(ROUND_HALF_UP)。
Python 的 round() 和 decimal 的默认值会把恰好处于一半的值向偶数方向取整。如果条款规定的是四舍五入,就必须在 Decimal 的 quantize 中明确指定 rounding。评分器会替换表中的数值,检查判定是否随之变化,以及恰好处于一半的位置是否会进位。
全部判定与赔付报告
用 /root/coverage/make_payout.py 判定全部 141 笔索赔,生成 /root/coverage/payout.json(claims、paid、denied、billed_total、payable_total、denied_by_reason、cut_by_rule)和 /root/coverage/coverage_report.md(## 무엇을 판정했나 ## 어떤 규칙이 얼마를 깎았나 ## 거절한 건과 이유 ## 남은 위험)。
不要重新实现判定器,而是汇总 --all 的输出。cut_by_rule 是按规则累加 steps 中 amount 得到的值,denied_by_reason 是按原因统计 reasons 的次数。有两个原因的索赔,在两个原因下各计一次。报告中请用数字写出赔付合计。