secureshield-backend / tests /test_decision_engine.py
Sharma7's picture
feat: SecureShield Agentic Insurance Engine — full prototype
0fe30af
Raw
History Blame Contribute Delete
15.4 kB
"""
Comprehensive tests for the Deterministic Decision Engine.
Tests cover: room rent caps, co-payments, exclusions, sub-limits,
deductibles, waiting periods, and complex multi-rule scenarios.
These tests use NO LLM calls — they validate pure deterministic logic.
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import pytest
from engine.decision_engine import evaluate
from models.case import CaseFacts, RoomType, AdmissionType, CityTier
from models.verdict import VerdictStatus, RuleMatchStatus
# --- Helper ---
def make_case(**overrides) -> CaseFacts:
"""Create a CaseFacts with sensible defaults."""
defaults = {
"room_type": RoomType.SINGLE_AC,
"room_cost_per_day": 5000,
"stay_duration_days": 3,
"admission_type": AdmissionType.PLANNED,
"procedure": "Appendectomy",
"procedure_cost": 60000,
"total_claimed_amount": 75000, # 5000*3 + 60000
"city_tier": CityTier.TIER_1,
}
defaults.update(overrides)
return CaseFacts(**defaults)
# ============================================================
# TEST 1: Simple approved claim (no deductions)
# ============================================================
class TestApprovedClaim:
"""When no rules are violated, claim should be fully approved."""
def test_no_rules_full_approval(self):
"""Empty rules list → 100% coverage."""
case = make_case()
verdict = evaluate(rules=[], facts=case, sum_insured=1000000)
assert verdict.overall_verdict == VerdictStatus.APPROVED
assert verdict.total_eligible == 75000
assert verdict.total_denied == 0
assert verdict.coverage_percentage == 100.0
def test_room_within_limit(self):
"""Room cost within cap → no deduction."""
rules = [{
"category": "room_rent",
"condition": "Room rent capped at 1% of sum insured per day",
"limit_type": "percentage",
"limit_value": 1.0,
"clause_reference": "Section 4.2",
"applies_to": "all"
}]
# 1% of 10L = ₹10,000/day, room is ₹5,000/day → within limit
case = make_case(room_cost_per_day=5000)
verdict = evaluate(rules, case, sum_insured=1000000)
assert verdict.overall_verdict == VerdictStatus.APPROVED
room_match = [m for m in verdict.matched_rules if m.rule_category == "room_rent"][0]
assert room_match.status == RuleMatchStatus.PASSED
assert room_match.shortfall == 0
# ============================================================
# TEST 2: Room rent capping
# ============================================================
class TestRoomRentCapping:
"""Room rent cap scenarios — the most common deduction in Indian health insurance."""
def test_percentage_cap_exceeded(self):
"""Room rent exceeds % cap → shortfall deducted."""
rules = [{
"category": "room_rent",
"condition": "Room rent capped at 1% of sum insured per day",
"limit_type": "percentage",
"limit_value": 1.0,
"clause_reference": "Section 4.2(a)",
"applies_to": "all"
}]
# SI=5L, 1% = ₹5,000/day, room=₹8,000/day → ₹3,000 shortfall/day × 3 days = ₹9,000
case = make_case(room_cost_per_day=8000, total_claimed_amount=84000)
verdict = evaluate(rules, case, sum_insured=500000)
room_match = [m for m in verdict.matched_rules if m.rule_category == "room_rent"][0]
assert room_match.status == RuleMatchStatus.CAPPED
assert room_match.shortfall == 9000 # (8000-5000) × 3
def test_absolute_cap(self):
"""Absolute room rent cap (e.g., ₹4,000/day)."""
rules = [{
"category": "room_rent",
"condition": "Room rent capped at ₹4,000 per day",
"limit_type": "absolute",
"limit_value": 4000,
"clause_reference": "Section 4.2(b)",
"applies_to": "all"
}]
# Room=₹6,000/day, cap=₹4,000 → ₹2,000 shortfall/day × 3 = ₹6,000
case = make_case(room_cost_per_day=6000, total_claimed_amount=78000)
verdict = evaluate(rules, case, sum_insured=500000)
room_match = [m for m in verdict.matched_rules if m.rule_category == "room_rent"][0]
assert room_match.status == RuleMatchStatus.CAPPED
assert room_match.shortfall == 6000
# ============================================================
# TEST 3: Co-payment
# ============================================================
class TestCopayment:
"""Co-payment deductions (applied last on remaining eligible amount)."""
def test_flat_copay(self):
"""20% co-payment on entire claim."""
rules = [{
"category": "copay",
"condition": "20% co-payment on all claims",
"limit_type": "copay",
"limit_value": 20.0,
"clause_reference": "Section 6.1",
"applies_to": "all"
}]
case = make_case(total_claimed_amount=100000)
verdict = evaluate(rules, case, sum_insured=500000)
copay_match = [m for m in verdict.matched_rules if m.rule_category == "copay"][0]
assert copay_match.status == RuleMatchStatus.CAPPED
assert copay_match.shortfall == 20000 # 20% of 100000
assert verdict.total_eligible == 80000
def test_copay_combined_with_room_cap(self):
"""Room rent cap + co-pay applied sequentially."""
rules = [
{
"category": "room_rent",
"condition": "Room rent capped at 1% of SI per day",
"limit_type": "percentage",
"limit_value": 1.0,
"clause_reference": "Section 4.2",
"applies_to": "all"
},
{
"category": "copay",
"condition": "20% co-payment",
"limit_type": "copay",
"limit_value": 20.0,
"clause_reference": "Section 6.1",
"applies_to": "all"
}
]
# SI=5L, room=₹8000/day×3=₹24000, cap=₹5000/day×3=₹15000, shortfall=₹9000
# Eligible after room cap: 74000-9000=65000
# Copay 20% of 65000 = 13000 → final eligible = 52000
case = make_case(room_cost_per_day=8000, total_claimed_amount=74000)
verdict = evaluate(rules, case, sum_insured=500000)
assert verdict.total_eligible == 52000
assert verdict.overall_verdict == VerdictStatus.PARTIAL
# ============================================================
# TEST 4: Exclusions
# ============================================================
class TestExclusions:
"""Exclusion rules — full denial of specific procedures/conditions."""
def test_procedure_exclusion(self):
"""Excluded procedure → full denial."""
rules = [{
"category": "exclusion_permanent",
"condition": "Cosmetic surgery is permanently excluded",
"limit_type": "exclusion",
"limit_value": None,
"clause_reference": "Section 8.1(c)",
"applies_to": "cosmetic surgery"
}]
case = make_case(procedure="Cosmetic Surgery - Rhinoplasty")
verdict = evaluate(rules, case, sum_insured=500000)
assert verdict.overall_verdict == VerdictStatus.DENIED
assert verdict.total_eligible == 0
assert verdict.coverage_percentage == 0.0
def test_non_matching_exclusion(self):
"""Exclusion that doesn't apply → no effect."""
rules = [{
"category": "exclusion_permanent",
"condition": "Cosmetic surgery is excluded",
"limit_type": "exclusion",
"limit_value": None,
"clause_reference": "Section 8.1(c)",
"applies_to": "cosmetic surgery"
}]
case = make_case(procedure="Appendectomy")
verdict = evaluate(rules, case, sum_insured=500000)
# Exclusion shouldn't apply
exclusion_matches = [m for m in verdict.matched_rules
if m.rule_category == "exclusion" and m.status == RuleMatchStatus.DENIED]
assert len(exclusion_matches) == 0
# ============================================================
# TEST 5: Deductibles
# ============================================================
class TestDeductibles:
"""Deductible amounts subtracted from eligible amount."""
def test_deductible_applied(self):
"""₹10,000 deductible reduces eligible amount."""
rules = [{
"category": "deductible",
"condition": "Compulsory deductible of ₹10,000",
"limit_type": "deductible",
"limit_value": 10000,
"clause_reference": "Section 7.1",
"applies_to": "all"
}]
case = make_case(total_claimed_amount=50000)
verdict = evaluate(rules, case, sum_insured=500000)
assert verdict.total_eligible == 40000
deductible_match = [m for m in verdict.matched_rules if m.rule_category == "deductible"][0]
assert deductible_match.shortfall == 10000
# ============================================================
# TEST 6: Complex real-world scenario
# ============================================================
class TestRealWorldScenarios:
"""End-to-end tests modeling real Indian health insurance scenarios."""
def test_star_health_scenario(self):
"""
Scenario: Patient at private hospital in Mumbai
Policy: Star Health, SI ₹5,00,000
Rules: Room cap 1% SI, 20% copay age>60, ₹2,000 ambulance cap
Patient: 45 years, Single AC room ₹8,000/day, 4 days, Knee surgery ₹2,50,000
"""
rules = [
{
"category": "room_rent",
"condition": "Room rent capped at 1% of SI per day",
"limit_type": "percentage",
"limit_value": 1.0,
"clause_reference": "Section 4.2",
"applies_to": "all"
},
{
"category": "copay",
"condition": "20% co-pay for insured person aged above 60 years",
"limit_type": "copay",
"limit_value": 20.0,
"clause_reference": "Section 6.3",
"applies_to": "age > 60"
},
{
"category": "ambulance",
"condition": "Ambulance charges limited to ₹2,000 per hospitalization",
"limit_type": "absolute",
"limit_value": 2000,
"clause_reference": "Section 5.7",
"applies_to": "ambulance"
}
]
# Room: ₹8,000/day × 4 = ₹32,000 | Cap: ₹5,000/day × 4 = ₹20,000 | Shortfall: ₹12,000
# Surgery: ₹2,50,000
# Total: ₹2,82,000
# Age=45, so copay doesn't apply
# After room cap: ₹2,82,000 - ₹12,000 = ₹2,70,000
case = make_case(
room_cost_per_day=8000,
stay_duration_days=4,
procedure="Total Knee Replacement",
procedure_cost=250000,
total_claimed_amount=282000,
patient_age=45,
)
verdict = evaluate(rules, case, sum_insured=500000)
# Copay does NOT apply (patient age 45 < 60)
# Only room rent cap applies: ₹12,000 shortfall
# ₹2,82,000 - ₹12,000 = ₹2,70,000 → 95.7% → APPROVED (≥95% threshold)
assert verdict.overall_verdict == VerdictStatus.APPROVED
assert verdict.total_eligible == 270000
assert verdict.total_denied == 12000
def test_full_denial_exclusion_stops_processing(self):
"""When exclusion triggers, no further rules should be processed."""
rules = [
{
"category": "exclusion_permanent",
"condition": "Weight management surgery is excluded",
"limit_type": "exclusion",
"limit_value": None,
"clause_reference": "Section 8.2",
"applies_to": "weight management"
},
{
"category": "room_rent",
"condition": "Room cap 1%",
"limit_type": "percentage",
"limit_value": 1.0,
"clause_reference": "Section 4.2",
"applies_to": "all"
}
]
case = make_case(procedure="Bariatric Weight Management Surgery")
verdict = evaluate(rules, case, sum_insured=500000)
assert verdict.overall_verdict == VerdictStatus.DENIED
assert verdict.total_eligible == 0
# Room rent rule should NOT be processed after exclusion
room_matches = [m for m in verdict.matched_rules if m.rule_category == "room_rent"]
assert len(room_matches) == 0
def test_zero_claim_amount(self):
"""Edge case: zero claim amount."""
case = make_case(
room_cost_per_day=0,
procedure_cost=0,
total_claimed_amount=0,
)
# Should not crash
verdict = evaluate(rules=[], facts=case, sum_insured=500000)
assert verdict.coverage_percentage == 0.0 or verdict.total_claimed == 0
def test_very_high_claim(self):
"""Claim amount exceeding sum insured."""
rules = [{
"category": "room_rent",
"condition": "Room rent capped at 1% of SI",
"limit_type": "percentage",
"limit_value": 1.0,
"clause_reference": "Section 4.2",
"applies_to": "all"
}]
case = make_case(
room_cost_per_day=50000,
stay_duration_days=10,
procedure_cost=500000,
total_claimed_amount=1000000,
)
verdict = evaluate(rules, case, sum_insured=500000)
# Should run without error even if claim > SI
assert verdict.total_claimed == 1000000
# ============================================================
# TEST 7: Verdict classification
# ============================================================
class TestVerdictClassification:
"""Test that verdicts are classified correctly."""
def test_approved_threshold(self):
"""Coverage >= 95% → APPROVED."""
# No rules → 100% coverage
case = make_case(total_claimed_amount=100000)
verdict = evaluate([], case, sum_insured=1000000)
assert verdict.overall_verdict == VerdictStatus.APPROVED
def test_partial_verdict(self):
"""Coverage between 0% and 95% → PARTIAL."""
rules = [{
"category": "copay",
"condition": "10% copay",
"limit_type": "copay",
"limit_value": 10.0,
"clause_reference": "Sec 6",
"applies_to": "all"
}]
case = make_case(total_claimed_amount=100000)
verdict = evaluate(rules, case, sum_insured=500000)
assert verdict.overall_verdict == VerdictStatus.PARTIAL
assert verdict.coverage_percentage == 90.0
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])