File size: 6,811 Bytes
069dc30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
"""
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