From c8e6474edd9600e8d7fbcf36534bdb1601957dab Mon Sep 17 00:00:00 2001 From: ipu Date: Mon, 8 Sep 2025 18:47:01 +0300 Subject: [PATCH] add human estimation conditions --- src/cache/drug_cache.py | 2 +- src/cache/redis_cache.py | 21 ++-- src/services/estimation_service_v2.py | 137 +++++++++++++++++--------- 3 files changed, 103 insertions(+), 57 deletions(-) diff --git a/src/cache/drug_cache.py b/src/cache/drug_cache.py index d998eaf..326e078 100644 --- a/src/cache/drug_cache.py +++ b/src/cache/drug_cache.py @@ -135,5 +135,5 @@ async def fetch_drug_with_dosage(drug_name: str, dosage: float) -> DrugFull | No if drug_full: return drug_full - raise Exception(f"Drug {drug_name} with dosage {dosage} not found") + return None diff --git a/src/cache/redis_cache.py b/src/cache/redis_cache.py index 50c8b76..c612799 100644 --- a/src/cache/redis_cache.py +++ b/src/cache/redis_cache.py @@ -102,7 +102,7 @@ def fetch_drug(drug_name: str) -> DrugPriceResponse: return result -def search_drug(drug_name: str) -> str: +def search_drug(drug_name: str) -> str | None: cache_key = f"drug_search:{drug_name}" redis_client = get_redis_client() @@ -113,15 +113,18 @@ def search_drug(drug_name: str) -> str: except Exception: pass - client = httpx.Client( - base_url="https://www.drugs.com/api/autocomplete", - # headers=headers, - timeout=httpx.Timeout(60.0, connect=10.0) - ) - response = client.get(f"/?type=price-guide&s={drug_name}") - response_json = response.json() + try: + client = httpx.Client( + base_url="https://www.drugs.com/api/autocomplete", + # headers=headers, + timeout=httpx.Timeout(60.0, connect=10.0) + ) + response = client.get(f"/?type=price-guide&s={drug_name}") + response_json = response.json() - result = response_json["categories"][0]["results"][0]["url"].replace("/price-guide/", "") + result = response_json["categories"][0]["results"][0]["url"].replace("/price-guide/", "") + except Exception: + return None try: redis_client.setex(cache_key, settings.REDIS_CACHE_TTL_DRUGS, result) diff --git a/src/services/estimation_service_v2.py b/src/services/estimation_service_v2.py index fb4ee13..953aba6 100644 --- a/src/services/estimation_service_v2.py +++ b/src/services/estimation_service_v2.py @@ -55,47 +55,48 @@ BASE_TIERS = { } HEIGHT_WEIGHT_TABLE = { - (4, 2): (60, 124), - (4, 3): (63, 129), - (4, 4): (65, 135), - (4, 5): (68, 140), - (4, 6): (71, 145), - (4, 7): (73, 151), - (4, 8): (76, 156), - (4, 9): (79, 162), - (4, 10): (81, 167), - (4, 11): (84, 173), - (5, 0): (87, 179), - (5, 1): (90, 185), - (5, 2): (93, 191), - (5, 3): (96, 197), - (5, 4): (99, 204), - (5, 5): (102, 210), - (5, 6): (105, 217), - (5, 7): (109, 223), - (5, 8): (112, 230), - (5, 9): (115, 237), - (5, 10): (118, 244), - (5, 11): (122, 251), - (6, 0): (125, 258), - (6, 1): (129, 265), - (6, 2): (132, 273), - (6, 3): (136, 280), - (6, 4): (140, 288), - (6, 5): (143, 295), - (6, 6): (147, 303), - (6, 7): (151, 311), - (6, 8): (155, 319), - (6, 9): (159, 327), - (6, 10): (163, 335), - (6, 11): (167, 343), - (7, 0): (171, 351), - (7, 1): (175, 360), - (7, 2): (179, 368), - (7, 3): (183, 377), - (7, 4): (187, 386), + (4, 2): (54, 60, 125, 151), + (4, 3): (56, 63, 130, 156), + (4, 4): (59, 65, 136, 162), + (4, 5): (60, 68, 141, 169), + (4, 6): (63, 71, 146, 175), + (4, 7): (65, 73, 152, 182), + (4, 8): (67, 76, 157, 188), + (4, 9): (70, 79, 163, 195), + (4, 10): (72, 81, 168, 202), + (4, 11): (75, 84, 174, 209), + (5, 0): (77, 87, 180, 216), + (5, 1): (80, 90, 186, 223), + (5, 2): (83, 93, 192, 230), + (5, 3): (85, 96, 199, 238), + (5, 4): (88, 99, 205, 245), + (5, 5): (91, 102, 211, 253), + (5, 6): (93, 105, 218, 261), + (5, 7): (96, 109, 224, 269), + (5, 8): (99, 112, 231, 277), + (5, 9): (102, 115, 238, 285), + (5, 10): (105, 118, 245, 293), + (5, 11): (108, 122, 252, 302), + (6, 0): (111, 125, 259, 310), + (6, 1): (114, 129, 266, 319), + (6, 2): (117, 132, 274, 327), + (6, 3): (121, 136, 281, 336), + (6, 4): (124, 140, 289, 345), + (6, 5): (127, 143, 296, 354), + (6, 6): (130, 147, 304, 364), + (6, 7): (134, 151, 312, 374), + (6, 8): (137, 155, 320, 383), + (6, 9): (140, 159, 328, 393), + (6, 10): (144, 163, 336, 403), + (6, 11): (147, 167, 344, 413), + (7, 0): (151, 171, 352, 423), + (7, 1): (155, 175, 361, 433), + (7, 2): (158, 179, 369, 443), + (7, 3): (162, 183, 378, 453), + (7, 4): (166, 187, 387, 464), } + UNINSURABLE_MEDICATIONS = [ "abacavir", "abarelix", @@ -348,17 +349,23 @@ class EstimationService: today = date.today() return today.year - born.year - ((today.month, today.day) < (born.month, born.day)) - async def calculate_rx_spend(self, phq: PHQ, applicant_id: int) -> float: + async def calculate_rx_spend(self, phq: PHQ, applicant_id: int) -> (float, list[str]): rx_spend = 0 + review_reasons = [] for medication in phq.medications: if medication.applicant != applicant_id: continue try: drug_name = medication.name.lower() drug_url = search_drug(drug_name) + if not drug_url: + review_reasons.append(f"Drug not found: {drug_name}") + continue drug_dosage = float(medication.dosage) - print(f"{drug_name} | {drug_url} | {drug_dosage}") drug_price = await fetch_drug_with_dosage(drug_url, drug_dosage) + if not drug_price: + review_reasons.append(f"Dosage {drug_dosage} for drug {drug_name} not found") + continue if medication.frequency in ["Once daily", "At bedtime"]: month_times = 30 @@ -375,13 +382,14 @@ class EstimationService: elif medication.frequency == "Every other day": month_times = 15 else: + review_reasons.append(f"Unclear frequency of drug {medication.name}: {medication.frequency}") month_times = 1 rx_spend += drug_price.unit_price * month_times except Exception as e: print(f"Error calculating rx spend for {medication.name}: {e}") pass - return rx_spend + return rx_spend, review_reasons def get_tier(self, coverage: int, rx_spend: float) -> Optional[Tier]: tiers = [ @@ -431,9 +439,16 @@ class EstimationService: async def estimate_insurance(self, applicants: list[Applicant], phq: PHQ, plans: list[Plan]): estimation_results = [] + is_review = False + review_reasons = [] + dtq_reasons = [] + accept_reasons = [] + is_dtq, reason = self.check_dtq(phq) if not is_dtq: is_dtq, reason = await self.check_dtq_ai(phq) + if reason: + dtq_reasons.append(reason) max_age = max(self.calculate_age(applicant.dob) for applicant in applicants) for base_age, tier in BASE_TIERS.items(): @@ -453,27 +468,42 @@ class EstimationService: if applicant_age >= 65: is_dtq = True reason = "Age is over 65" + dtq_reasons.append(reason) if (applicant.heightFt, applicant.heightIn) in HEIGHT_WEIGHT_TABLE: - weight_min, weight_max = HEIGHT_WEIGHT_TABLE[(applicant.heightFt, applicant.heightIn)] - if applicant.weight < weight_min: + w1, w2, w3, w4 = HEIGHT_WEIGHT_TABLE[(applicant.heightFt, applicant.heightIn)] + if applicant.weight < w1: is_dtq = True reason = "Declined due to low BMI of one or more applicants" - elif applicant.weight > weight_max: + dtq_reasons.append(reason) + elif w1 <= applicant.weight < w2: + is_review = True + review_reasons.append("low BMI of one or more applicants") + elif w3 <= applicant.weight < w4: + is_review = True + review_reasons.append("high BMI of one or more applicants") + elif applicant.weight >= w4: is_dtq = True reason = "Declined due to high BMI of one or more applicants" - - rx_spend_applicant = await self.calculate_rx_spend(phq, applicant_id) + dtq_reasons.append(reason) + + rx_spend_applicant, rx_review_reasons = await self.calculate_rx_spend(phq, applicant_id) + if rx_review_reasons: + is_review = True + review_reasons += rx_review_reasons + rx_spend += rx_spend_applicant applicant_new_tier = self.get_tier(plan_coverage, rx_spend_applicant) if applicant_new_tier is None: is_dtq = True reason = "Declined due to high Rx spend" + dtq_reasons.append(reason) if applicant_new_tier and applicant_new_tier > applicant_tier: applicant_tier = applicant_new_tier reason = f"Rx spend increased tier to {applicant_new_tier}." + accept_reasons.append(reason) estimation_results.append( EstimationResult( @@ -490,6 +520,7 @@ class EstimationService: plan_price_id = self.get_plan_price(plans[0], base_tier, plan_coverage) if is_dtq: + reason = "\n".join(dtq_reasons) return EstimationResponse( status="rejected", details=EstimationDetails( @@ -499,6 +530,18 @@ class EstimationService: ), results=estimation_results ) + + if is_review: + reason = "\n".join(review_reasons) + return EstimationResponse( + status="human_review", + details=EstimationDetails( + dtq=is_dtq, + reason=reason, + price_id=plan_price_id, + ), + results=estimation_results + ) new_tier = self.get_tier(plan_coverage, rx_spend)