同じ請求なのに、担当者ごとに支払額が違った
一言でいうと
保障の判定は、請求額から自己負担金・共同負担率・1件あたりの限度・年間限度を決められた順序で削っていく計算であり、その順序と丸めと「何を単位に数えるか」が結果を変えます。
なぜ必要なのか
保険金の計算は、見た目には掛け算と引き算を数回行うだけです。ところが現場に入ると、同じ請求を2つのシステムが違うように計算している場面を、ほぼ毎回目にします。請求審査システムと会計システムが違い、支払担当者のスプレッドシートがまた違います。3つとも約款を読んで作ったのに、結果が違います。
原因はたいてい、約款が言葉で書かれていて、順序が明示されていないからです。「自己負担金を控除した金額の80%を支払うが、1回30万ウォン、年間300万ウォンを限度とする」という文では、自己負担金を先に引くのか80%を先に掛けるのか、30万ウォンの限度を掛ける前に見るのか後に見るのか、自己負担金が請求1件ごとなのか事故1件ごとなのかを、文は決めていません。各チームは自分の解釈で作り、その解釈がコードの中に散らばって隠れます。
このずれは、エラーとしては現れません。2つのシステムとも正常終了し、ログには何も残りません。現れるのは、顧客からの問い合わせと決算の差です。そのため、FDEがこの判定を作り直すときにすることは、ルールをデータとして引き出し、どのルールがいくら削ったかを請求ごとに残すことです。判定が合っているかどうか以前に、判定を説明できなければなりません。
どう動くのか
判定器は、請求1件を受け取って金額を順に削ります。このラボで使う順序は次のとおりです。これはこのラボの仮定であり、実際の商品の約款は商品ごとに異なります。保険契約の基本的な骨格は商法第4編(保険)にありますが、自己負担金や限度のような数値は、法ではなく約款が定めます。
청구액
├─ 1. 자기부담금(공제액) 사고 하나에 한 번 → 남은 금액
├─ 2. 공동부담률 비율로 깎음 → 남은 금액
├─ 3. 건당 한도 이 청구의 상한 → 남은 금액
└─ 4. 연간 한도 남은 연간 여유 → 지급액
順序がなぜ結果を変えるのか。請求100万ウォン、自己負担金30万ウォン、共同負担20%だとします。控除を先に行うと、(100 - 30) × 0.8 = 56万ウォンです。共同負担を先に行うと、100 × 0.8 - 30 = 50万ウォンです。6万ウォンの差が出て、この差は請求の数だけ掛け算されます。2つの結果とも「約款どおり」と主張できることが、問題の核心です。
何を単位に数えるかも結果を変えます。自己負担金が請求1件ごとなら、同じ事故で外来・薬剤・検査を別々に請求した人は、自己負担金を3回払います。事故1件ごとなら1回です。年間限度も同様で、「事故日基準の年度」なのか「受付日基準の年度」なのかによって、年末の請求の判定が分かれます。
免責期間(待機期間)は、契約日と事故日の間の距離です。契約開始後、一定期間内に起きた事故は保障しません。ここでよくある間違いは、事故日ではなく受付日で測ることです。事故は期間のあとに起きたのに、受付が遅れて支払いが拒否されたり、その逆になったりします。日付の計算は、文字列比較から始めると必ず問題が起きるので、Pythonならdatetimeのdate.fromisoformatとtimedeltaで扱い、SQLの中で扱う必要があるならSQLiteの日付関数を使います。
丸めはポリシーです。共同負担率を掛けると、ウォン単位より下が残ります。Pythonの組み込みround()は、ちょうど半分の値を偶数側に寄せます(銀行家の丸め)。decimalモジュールのデフォルトの丸め方式もROUND_HALF_EVENです。約款が四捨五入を述べているならROUND_HALF_UPを明示する必要があり、その差は1件あたり1ウォンですが、数十万件なら決算で目に見えます。さらに重要なのは、2つのシステムが別々のモードを使っているときに、差がランダムに散らばって原因を探しにくくなるという点です。
現場での姿
1つ目に、ルールの数値がコードの中に埋め込まれています。if plan == "BASIC": deductible = 20000のような行が複数のファイルに散らばっていて、商品が1つ増えるたびにすべてのファイルを直さなければなりません。ルールを表に出すと、商品の追加がデータ1行になり、何よりも過去の判定を、そのときの表で再現できます。
2つ目に、判定結果だけがあって、根拠がありません。支払額12万ウォンだけが保存されていると、顧客が「なぜ12万ウォンなのか」と聞いたときに、誰も答えられません。ルールごとに削った金額を配列で残せば、それ自体が顧客向けの案内文になり、回帰テストの期待値にもなります。ラボの判定器がstepsを出す理由です。
3つ目に、累積限度を請求単位でしか見ていません。年間限度は、その契約の先行する請求がいくら使ったかに左右されます。請求1件だけを見て計算すると、限度を超えて支払ったり、順序が変わると別の結果が出たりします。判定は決められた順序(通常は受付日)で処理する必要があり、遅れて入った請求が先の判定を覆さないかも決めておく必要があります。
4つ目に、拒否理由が1つだけです。免責期間内に起きた事故で、かつ保障除外の項目でもある請求があります。理由を1つしか残さないと、顧客が1つを説明したあとで、再び拒否されます。理由は一覧として残します。
実務で本当に大切なこと
- 順序を文書に固定し、コードがその順序どおりに読めるようにします。順序はコメントではなく、コードの形で現れなければなりません。
- ルールの数値は表に、計算はコードに置きます。商品が増えても、コードはそのままでなければなりません。
- 判定の根拠を、請求ごとに残します。合計だけを合わせると、どこでずれたのかを永遠に知ることができません。
- 丸めモードと、限度の基準年度をポリシーとして書きます。決めなければ2つのシステムが別々の答えを出し、その差はエラーには見えません。
次のラボですること
合成約款表と請求141件を自分で作り、適用順序の2通りが合計をどれだけ広げるかを、数字で見ます。そのあと判定器を作り、控除・共同負担・1件あたりの限度を順に適用し、免責期間と保障除外で拒否し、同じ事故の複数請求が自己負担金を分け合うようにし、年間限度を使い切らせます。採点ツールは毎回異なる約款の数値で判定器を実際に実行し、支払額と判定根拠を突き合わせ、ちょうど半分の位置の丸めまで確認します。