InsuranceBot / tests /test_premium_reconciliation.py
Claude Code
fix(reco): cap 3, exclude non-extracted policies, unify premium, ask PED
61980e6
Raw
History Blame Contribute Delete
10.4 kB
"""KI-278 (2026-05-16) β€” header "Premium range" chip vs per-settings panel
reconciliation + smoker / family-history wiring + full-profile exhaustiveness.
Pins the bug from Image#7:
For ONE profile the header chip showed β‚Ή6,500–₹26,500/yr while opening the
panel showed β‚Ή16,235–₹21,965 (point β‚Ή19,100). Two contradictory numbers
for the same person.
Root cause: estimate_premium_band() hard-coded sum_insured_default=β‚Ή10L and
IGNORED the profile, while PremiumCalculatorPanel seeded its SI slider from
desired_sum_insured_inr β†’ existing_cover_inr β†’ β‚Ή10L. A user who stated a
β‚Ή25L target therefore had the header priced at β‚Ή10L and the panel at β‚Ή25L.
Fix: estimate_premium_band() resolves SI via resolve_profile_sum_insured()
(the single source of truth shared with the panel/widget), prices the whole
basket at that SI, and reports the p25–p75 INTERQUARTILE of the basket as
the band (directionally rounded) β€” the honest "what similar profiles
typically pay" range. Raw min–max across the heterogeneous basket spans
~4-5x and renders as a useless, broken-looking band (KI broken-band fix);
a specific plan's per-settings panel point may sit inside or just outside
this typical band, which is expected and correct.
"""
from backend.premium_calculator import (
_DEFAULT_BAND_POLICY_IDS,
_ceil_to_500,
_floor_to_500,
_median,
_percentile,
bulk_estimate,
estimate,
estimate_premium_band,
resolve_profile_sum_insured,
)
# ---------------------------------------------------------------------------
# resolve_profile_sum_insured β€” the shared SI contract
# ---------------------------------------------------------------------------
def test_si_precedence_desired_over_existing_over_default():
# desired_sum_insured_inr wins
assert (
resolve_profile_sum_insured(
{"desired_sum_insured_inr": 2_500_000, "existing_cover_inr": 800_000}
)
== 2_500_000
)
# existing_cover_inr next when no desired
assert resolve_profile_sum_insured({"existing_cover_inr": 800_000}) == 800_000
# legacy default when neither present
assert resolve_profile_sum_insured({}) == 1_000_000
assert resolve_profile_sum_insured(None) == 1_000_000
def test_si_unclamped_and_snapped_to_slider_grid():
# SI RATIONALISATION (D2, 2026-05-16) β€” the global β‚Ή5 L / β‚Ή1 Cr clamp was
# REMOVED. The user's actual stated target is now honoured (snapped to the
# β‚Ή50k grid only), not squashed into a synthetic envelope.
# Above the OLD ceiling: prices at the real β‚Ή2 Cr (was clamped to β‚Ή1 Cr).
assert resolve_profile_sum_insured({"desired_sum_insured_inr": 20_000_000}) == 20_000_000
# Below the OLD floor: prices at the real β‚Ή1 L (was clamped to β‚Ή5 L).
assert resolve_profile_sum_insured({"desired_sum_insured_inr": 100_000}) == 100_000
# Off-grid still snaps to nearest β‚Ή50k.
assert resolve_profile_sum_insured({"desired_sum_insured_inr": 1_234_000}) == 1_250_000
def test_si_coerces_garbage_gracefully():
assert resolve_profile_sum_insured({"desired_sum_insured_inr": "2500000"}) == 2_500_000
assert resolve_profile_sum_insured({"desired_sum_insured_inr": None,
"existing_cover_inr": "abc"}) == 1_000_000
assert resolve_profile_sum_insured({"desired_sum_insured_inr": 0}) == 1_000_000
# ---------------------------------------------------------------------------
# Header β‰  panel reconciliation β€” the core defect
# ---------------------------------------------------------------------------
def _panel_points_for(profile: dict) -> list[int]:
"""The per-settings panel = one basket policy priced at the SAME
profile-resolved SI the header band uses. Reproduce every basket member's
point so we can assert each one lies inside the displayed band."""
si = resolve_profile_sum_insured(profile)
rows = bulk_estimate(
list(_DEFAULT_BAND_POLICY_IDS),
profile=profile,
overrides={pid: {"sum_insured_inr": si} for pid in _DEFAULT_BAND_POLICY_IDS},
)
return sorted(r.premium_inr_annual for r in rows.values() if r.premium_inr_annual)
import pytest
@pytest.mark.parametrize(
"profile,label",
[
({"age": 35, "location_tier": "metro", "dependents": "self",
"desired_sum_insured_inr": 2_500_000}, "defect profile (β‚Ή25L stated)"),
({"age": 35, "location_tier": "metro", "dependents": "self"},
"no SI signal (β‚Ή10L default)"),
({"age": 55, "location_tier": "tier2", "dependents": "spouse and 2 kids",
"smoker": True, "family_medical_history": ["cancer"],
"existing_cover_inr": 800_000}, "rich profile"),
({"age": 28, "location_tier": "metro", "dependents": "parents",
"parents_age_max": 72, "parents_has_ped": True}, "parents-on-cover"),
],
)
def test_header_band_is_p25_p75_interquartile(profile, label):
band = estimate_premium_band(dict(profile))
points = _panel_points_for(profile) # sorted basket points
assert points, f"no basket points for {label}"
# New contract: the band edges ARE the directionally-rounded p25 / p75
# of the basket β€” the interquartile "typical range", NOT raw min-max.
assert band["min_inr"] == _floor_to_500(_percentile(points, 25)), label
assert band["max_inr"] == _ceil_to_500(_percentile(points, 75)), label
# min ≀ median ≀ max, all positive.
assert 0 < band["min_inr"] <= band["median_inr"] <= band["max_inr"], label
# The typical (median) plan lies inside the band.
med = _median(points)
assert band["min_inr"] <= med <= band["max_inr"], (
f"{label}: median basket point β‚Ή{med:,} fell outside the typical "
f"band β‚Ή{band['min_inr']:,}–₹{band['max_inr']:,}"
)
# And it is materially TIGHTER than the raw min-max envelope β€” the
# entire point of the fix (the heterogeneous basket's raw spread is
# ~4-5x; the interquartile band must be a strict subset of it).
raw_lo = _floor_to_500(min(points))
raw_hi = _ceil_to_500(max(points))
assert band["min_inr"] >= raw_lo, label
assert band["max_inr"] <= raw_hi, label
assert (band["max_inr"] - band["min_inr"]) <= (raw_hi - raw_lo), label
def test_band_exposes_resolved_si_for_panel_alignment():
band = estimate_premium_band({"desired_sum_insured_inr": 2_500_000})
assert band["sum_insured_used"] == 2_500_000
# And it is the same value the panel/widget would seed its slider with.
assert band["sum_insured_used"] == resolve_profile_sum_insured(
{"desired_sum_insured_inr": 2_500_000}
)
def test_band_moves_with_stated_si_was_the_bug():
"""Pre-fix this returned an IDENTICAL band regardless of stated SI."""
base = {"age": 35, "location_tier": "metro", "dependents": "self"}
b_10l = estimate_premium_band(dict(base))
b_25l = estimate_premium_band({**base, "desired_sum_insured_inr": 2_500_000})
assert b_25l["max_inr"] > b_10l["max_inr"]
assert b_25l["sum_insured_used"] == 2_500_000
assert b_10l["sum_insured_used"] == 1_000_000
# ---------------------------------------------------------------------------
# Smoker + family_medical_history wiring (defect #2)
# ---------------------------------------------------------------------------
def test_smoker_moves_both_band_and_per_policy():
base = {"age": 40, "location_tier": "metro", "dependents": "self"}
b0 = estimate_premium_band(dict(base))
b1 = estimate_premium_band({**base, "smoker": True})
assert b1["max_inr"] > b0["max_inr"], "smoker not reflected in header band"
e0 = estimate(age=40, sum_insured_inr=1_000_000, policy_id="hdfc-ergo__optima-secure")
e1 = estimate(age=40, sum_insured_inr=1_000_000, policy_id="hdfc-ergo__optima-secure",
smoker=True)
assert e1.point_estimate_inr > e0.point_estimate_inr, "smoker not in per-policy estimate"
def test_family_medical_history_moves_both_band_and_per_policy():
base = {"age": 40, "location_tier": "metro", "dependents": "self"}
b0 = estimate_premium_band(dict(base))
b1 = estimate_premium_band({**base, "family_medical_history": ["cancer", "diabetes"]})
assert b1["max_inr"] > b0["max_inr"], "family history not in header band"
e0 = estimate(age=40, sum_insured_inr=1_000_000, policy_id="hdfc-ergo__optima-secure")
e1 = estimate(age=40, sum_insured_inr=1_000_000, policy_id="hdfc-ergo__optima-secure",
family_medical_history=["cancer", "diabetes"])
assert e1.point_estimate_inr > e0.point_estimate_inr, "family history not in estimate"
# ---------------------------------------------------------------------------
# Exhaustiveness β€” every pricing-relevant SLOT_UNION field must move the band
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"delta,field",
[
({"age": 60}, "age"),
({"location_tier": "tier2"}, "location_tier"),
({"dependents": "spouse and 2 kids"}, "dependents/family_size"),
({"smoker": True}, "smoker"),
({"copay_pct": 30}, "copay_pct"),
({"family_medical_history": ["cancer", "heart"]}, "family_medical_history"),
({"health_conditions": ["diabetes"]}, "health_conditions"),
({"existing_cover_inr": 800_000}, "existing_cover_inr"),
({"desired_sum_insured_inr": 2_500_000}, "desired_sum_insured_inr"),
],
)
def test_every_pricing_slot_moves_the_header_band(delta, field):
base = {"age": 35, "location_tier": "metro", "dependents": "self"}
b0 = estimate_premium_band(dict(base))
b1 = estimate_premium_band({**base, **delta})
moved = (
b1["min_inr"] != b0["min_inr"]
or b1["max_inr"] != b0["max_inr"]
or b1["sum_insured_used"] != b0["sum_insured_used"]
)
assert moved, f"{field} dropped on the header-band path (no effect)"
def test_parents_on_cover_moves_band():
base = {"age": 35, "location_tier": "metro", "dependents": "parents"}
b0 = estimate_premium_band(dict(base))
b1 = estimate_premium_band({**base, "parents_age_max": 72})
b2 = estimate_premium_band({**base, "parents_age_max": 72, "parents_has_ped": True})
assert b1["max_inr"] > b0["max_inr"], "parents_age_max dropped"
assert b2["max_inr"] > b1["max_inr"], "parents_has_ped dropped"