""" ClaimSense — Agent 8: Coverage Verification Agent =================================================== Verifies whether the claimed incident is covered under the policy. - Works for both linked (submission_id present) and standalone flows - Checks incident type vs coverage type eligibility matrix - Checks deductible, limits, exclusions - No ML — pure rules engine """ import logging from datetime import datetime, date log = logging.getLogger(__name__) # ── Coverage eligibility matrix ─────────────────────────────── # Maps coverage_type → set of covered incident types COVERAGE_MATRIX = { 'HO-3': { 'FIRE', 'WIND', 'HAIL', 'THEFT', 'VANDALISM', 'LIABILITY', 'VEHICLE_IMPACT', 'WATER', 'STRUCTURAL', 'OTHER' }, 'HO-5': { 'FIRE', 'WIND', 'HAIL', 'THEFT', 'VANDALISM', 'LIABILITY', 'VEHICLE_IMPACT', 'WATER', 'STRUCTURAL', 'EARTHQUAKE', 'OTHER' }, 'HO-4': { # Renters 'FIRE', 'THEFT', 'VANDALISM', 'LIABILITY', 'WATER', 'OTHER' }, 'HO-6': { # Condo 'FIRE', 'THEFT', 'VANDALISM', 'LIABILITY', 'WATER', 'STRUCTURAL', 'OTHER' }, 'DP-3': { # Dwelling 'FIRE', 'WIND', 'HAIL', 'VANDALISM', 'VEHICLE_IMPACT', 'OTHER' }, 'WC-3': { 'LIABILITY', 'OTHER' }, } # Incident types typically excluded (require endorsement) FLOOD_EXCLUDED = {'HO-3', 'HO-4', 'HO-5', 'HO-6', 'DP-3'} EARTHQUAKE_EXCLUDED = {'HO-3', 'HO-4', 'HO-6', 'DP-3'} def run_coverage_verify_agent(fnol_result: dict, submission: dict) -> dict: """ Agent 8 — Coverage Verification. Args: fnol_result: Output from Agent 7 (FNOL Intake) submission: Submission/policy data dict (may be empty for standalone) Returns: result dict with coverage_status, applicable_limit, deductible, exclusions """ claim_id = fnol_result.get('claim_id', '') incident_type = fnol_result.get('incident_type', 'OTHER') policy_number = fnol_result.get('policy_number', '') log.info(f"[COVERAGE] Agent 8 running for {claim_id}") # ── Pull policy details ─────────────────────────────────── # Works for both linked (from DB via submission) and standalone coverage_type = ( submission.get('_coverage_type_code') or submission.get('coverage_type_code') or (submission.get('policy_request') or {}).get('coverage_type') or 'HO-3' # safe default ) coverage_limit = float( submission.get('_requested_coverage_limit') or (submission.get('policy_request') or {}).get('requested_coverage_limit') or 300000 ) deductible = float( submission.get('_requested_deductible') or (submission.get('policy_request') or {}).get('requested_deductible') or 2500 ) estimated_damage = float( fnol_result.get('normalised_fields', {}).get('estimated_damage') or 0 ) coverage_type = str(coverage_type).upper().strip() exclusions = [] partial_notes = [] # ── Check eligibility matrix ────────────────────────────── covered_perils = COVERAGE_MATRIX.get(coverage_type, COVERAGE_MATRIX['HO-3']) if incident_type == 'FLOOD' and coverage_type in FLOOD_EXCLUDED: exclusions.append( f"FLOOD damage is excluded under {coverage_type}. " "Separate flood insurance (NFIP) required." ) elif incident_type == 'EARTHQUAKE' and coverage_type in EARTHQUAKE_EXCLUDED: exclusions.append( f"EARTHQUAKE is excluded under {coverage_type}. " "Earthquake endorsement required." ) elif incident_type not in covered_perils: exclusions.append( f"Incident type {incident_type} is not a covered peril under {coverage_type}." ) # ── Limit checks ───────────────────────────────────────── applicable_limit = coverage_limit if estimated_damage > coverage_limit: partial_notes.append( f"Estimated damage ${estimated_damage:,.0f} exceeds " f"coverage limit ${coverage_limit:,.0f}. " f"Maximum payable: ${coverage_limit:,.0f}." ) # ── Deductible check ────────────────────────────────────── net_payable = max(0, min(estimated_damage, coverage_limit) - deductible) if estimated_damage > 0 and estimated_damage <= deductible: partial_notes.append( f"Claimed amount ${estimated_damage:,.0f} does not exceed " f"deductible ${deductible:,.0f}. Claim may not result in payment." ) # ── Final coverage status ───────────────────────────────── if exclusions: coverage_status = 'EXCLUDED' elif partial_notes: coverage_status = 'PARTIAL' else: coverage_status = 'COVERED' # ── Summary ─────────────────────────────────────────────── if coverage_status == 'COVERED': summary = ( f"Incident type {incident_type} is fully covered under {coverage_type}. " f"Coverage limit: ${coverage_limit:,.0f}. " f"Deductible: ${deductible:,.0f}. " f"Estimated net payable: ${net_payable:,.0f}." ) elif coverage_status == 'PARTIAL': summary = ( f"Incident {incident_type} is covered under {coverage_type} " f"but with limitations. " + " ".join(partial_notes) ) else: summary = ( f"Incident {incident_type} is NOT covered under {coverage_type}. " + " ".join(exclusions) ) result = { 'claim_id': claim_id, 'status': f'COVERAGE_{coverage_status}', 'coverage_status': coverage_status, 'coverage_type': coverage_type, 'policy_number': policy_number, 'applicable_limit': applicable_limit, 'deductible': deductible, 'estimated_damage': estimated_damage, 'net_payable_est': net_payable, 'exclusions': exclusions, 'partial_notes': partial_notes, 'summary': summary, } log.info( f"[COVERAGE] {claim_id}: {coverage_status} | " f"type={coverage_type} | limit=${coverage_limit:,.0f} | " f"exclusions={len(exclusions)}" ) return result