add human estimation conditions

This commit is contained in:
ipu 2025-09-08 18:47:01 +03:00
parent 735ccb757c
commit c8e6474edd
3 changed files with 103 additions and 57 deletions

View file

@ -135,5 +135,5 @@ async def fetch_drug_with_dosage(drug_name: str, dosage: float) -> DrugFull | No
if drug_full: if drug_full:
return drug_full return drug_full
raise Exception(f"Drug {drug_name} with dosage {dosage} not found") return None

View file

@ -102,7 +102,7 @@ def fetch_drug(drug_name: str) -> DrugPriceResponse:
return result return result
def search_drug(drug_name: str) -> str: def search_drug(drug_name: str) -> str | None:
cache_key = f"drug_search:{drug_name}" cache_key = f"drug_search:{drug_name}"
redis_client = get_redis_client() redis_client = get_redis_client()
@ -113,6 +113,7 @@ def search_drug(drug_name: str) -> str:
except Exception: except Exception:
pass pass
try:
client = httpx.Client( client = httpx.Client(
base_url="https://www.drugs.com/api/autocomplete", base_url="https://www.drugs.com/api/autocomplete",
# headers=headers, # headers=headers,
@ -122,6 +123,8 @@ def search_drug(drug_name: str) -> str:
response_json = response.json() 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: try:
redis_client.setex(cache_key, settings.REDIS_CACHE_TTL_DRUGS, result) redis_client.setex(cache_key, settings.REDIS_CACHE_TTL_DRUGS, result)

View file

@ -55,47 +55,48 @@ BASE_TIERS = {
} }
HEIGHT_WEIGHT_TABLE = { HEIGHT_WEIGHT_TABLE = {
(4, 2): (60, 124), (4, 2): (54, 60, 125, 151),
(4, 3): (63, 129), (4, 3): (56, 63, 130, 156),
(4, 4): (65, 135), (4, 4): (59, 65, 136, 162),
(4, 5): (68, 140), (4, 5): (60, 68, 141, 169),
(4, 6): (71, 145), (4, 6): (63, 71, 146, 175),
(4, 7): (73, 151), (4, 7): (65, 73, 152, 182),
(4, 8): (76, 156), (4, 8): (67, 76, 157, 188),
(4, 9): (79, 162), (4, 9): (70, 79, 163, 195),
(4, 10): (81, 167), (4, 10): (72, 81, 168, 202),
(4, 11): (84, 173), (4, 11): (75, 84, 174, 209),
(5, 0): (87, 179), (5, 0): (77, 87, 180, 216),
(5, 1): (90, 185), (5, 1): (80, 90, 186, 223),
(5, 2): (93, 191), (5, 2): (83, 93, 192, 230),
(5, 3): (96, 197), (5, 3): (85, 96, 199, 238),
(5, 4): (99, 204), (5, 4): (88, 99, 205, 245),
(5, 5): (102, 210), (5, 5): (91, 102, 211, 253),
(5, 6): (105, 217), (5, 6): (93, 105, 218, 261),
(5, 7): (109, 223), (5, 7): (96, 109, 224, 269),
(5, 8): (112, 230), (5, 8): (99, 112, 231, 277),
(5, 9): (115, 237), (5, 9): (102, 115, 238, 285),
(5, 10): (118, 244), (5, 10): (105, 118, 245, 293),
(5, 11): (122, 251), (5, 11): (108, 122, 252, 302),
(6, 0): (125, 258), (6, 0): (111, 125, 259, 310),
(6, 1): (129, 265), (6, 1): (114, 129, 266, 319),
(6, 2): (132, 273), (6, 2): (117, 132, 274, 327),
(6, 3): (136, 280), (6, 3): (121, 136, 281, 336),
(6, 4): (140, 288), (6, 4): (124, 140, 289, 345),
(6, 5): (143, 295), (6, 5): (127, 143, 296, 354),
(6, 6): (147, 303), (6, 6): (130, 147, 304, 364),
(6, 7): (151, 311), (6, 7): (134, 151, 312, 374),
(6, 8): (155, 319), (6, 8): (137, 155, 320, 383),
(6, 9): (159, 327), (6, 9): (140, 159, 328, 393),
(6, 10): (163, 335), (6, 10): (144, 163, 336, 403),
(6, 11): (167, 343), (6, 11): (147, 167, 344, 413),
(7, 0): (171, 351), (7, 0): (151, 171, 352, 423),
(7, 1): (175, 360), (7, 1): (155, 175, 361, 433),
(7, 2): (179, 368), (7, 2): (158, 179, 369, 443),
(7, 3): (183, 377), (7, 3): (162, 183, 378, 453),
(7, 4): (187, 386), (7, 4): (166, 187, 387, 464),
} }
UNINSURABLE_MEDICATIONS = [ UNINSURABLE_MEDICATIONS = [
"abacavir", "abacavir",
"abarelix", "abarelix",
@ -348,17 +349,23 @@ class EstimationService:
today = date.today() today = date.today()
return today.year - born.year - ((today.month, today.day) < (born.month, born.day)) 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 rx_spend = 0
review_reasons = []
for medication in phq.medications: for medication in phq.medications:
if medication.applicant != applicant_id: if medication.applicant != applicant_id:
continue continue
try: try:
drug_name = medication.name.lower() drug_name = medication.name.lower()
drug_url = search_drug(drug_name) 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) drug_dosage = float(medication.dosage)
print(f"{drug_name} | {drug_url} | {drug_dosage}")
drug_price = await fetch_drug_with_dosage(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"]: if medication.frequency in ["Once daily", "At bedtime"]:
month_times = 30 month_times = 30
@ -375,13 +382,14 @@ class EstimationService:
elif medication.frequency == "Every other day": elif medication.frequency == "Every other day":
month_times = 15 month_times = 15
else: else:
review_reasons.append(f"Unclear frequency of drug {medication.name}: {medication.frequency}")
month_times = 1 month_times = 1
rx_spend += drug_price.unit_price * month_times rx_spend += drug_price.unit_price * month_times
except Exception as e: except Exception as e:
print(f"Error calculating rx spend for {medication.name}: {e}") print(f"Error calculating rx spend for {medication.name}: {e}")
pass pass
return rx_spend return rx_spend, review_reasons
def get_tier(self, coverage: int, rx_spend: float) -> Optional[Tier]: def get_tier(self, coverage: int, rx_spend: float) -> Optional[Tier]:
tiers = [ tiers = [
@ -431,9 +439,16 @@ class EstimationService:
async def estimate_insurance(self, applicants: list[Applicant], phq: PHQ, plans: list[Plan]): async def estimate_insurance(self, applicants: list[Applicant], phq: PHQ, plans: list[Plan]):
estimation_results = [] estimation_results = []
is_review = False
review_reasons = []
dtq_reasons = []
accept_reasons = []
is_dtq, reason = self.check_dtq(phq) is_dtq, reason = self.check_dtq(phq)
if not is_dtq: if not is_dtq:
is_dtq, reason = await self.check_dtq_ai(phq) 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) max_age = max(self.calculate_age(applicant.dob) for applicant in applicants)
for base_age, tier in BASE_TIERS.items(): for base_age, tier in BASE_TIERS.items():
@ -453,27 +468,42 @@ class EstimationService:
if applicant_age >= 65: if applicant_age >= 65:
is_dtq = True is_dtq = True
reason = "Age is over 65" reason = "Age is over 65"
dtq_reasons.append(reason)
if (applicant.heightFt, applicant.heightIn) in HEIGHT_WEIGHT_TABLE: if (applicant.heightFt, applicant.heightIn) in HEIGHT_WEIGHT_TABLE:
weight_min, weight_max = HEIGHT_WEIGHT_TABLE[(applicant.heightFt, applicant.heightIn)] w1, w2, w3, w4 = HEIGHT_WEIGHT_TABLE[(applicant.heightFt, applicant.heightIn)]
if applicant.weight < weight_min: if applicant.weight < w1:
is_dtq = True is_dtq = True
reason = "Declined due to low BMI of one or more applicants" 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 is_dtq = True
reason = "Declined due to high BMI of one or more applicants" reason = "Declined due to high BMI of one or more applicants"
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_applicant = await self.calculate_rx_spend(phq, applicant_id)
rx_spend += rx_spend_applicant rx_spend += rx_spend_applicant
applicant_new_tier = self.get_tier(plan_coverage, rx_spend_applicant) applicant_new_tier = self.get_tier(plan_coverage, rx_spend_applicant)
if applicant_new_tier is None: if applicant_new_tier is None:
is_dtq = True is_dtq = True
reason = "Declined due to high Rx spend" reason = "Declined due to high Rx spend"
dtq_reasons.append(reason)
if applicant_new_tier and applicant_new_tier > applicant_tier: if applicant_new_tier and applicant_new_tier > applicant_tier:
applicant_tier = applicant_new_tier applicant_tier = applicant_new_tier
reason = f"Rx spend increased tier to {applicant_new_tier}." reason = f"Rx spend increased tier to {applicant_new_tier}."
accept_reasons.append(reason)
estimation_results.append( estimation_results.append(
EstimationResult( EstimationResult(
@ -490,6 +520,7 @@ class EstimationService:
plan_price_id = self.get_plan_price(plans[0], base_tier, plan_coverage) plan_price_id = self.get_plan_price(plans[0], base_tier, plan_coverage)
if is_dtq: if is_dtq:
reason = "\n".join(dtq_reasons)
return EstimationResponse( return EstimationResponse(
status="rejected", status="rejected",
details=EstimationDetails( details=EstimationDetails(
@ -500,6 +531,18 @@ class EstimationService:
results=estimation_results 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) new_tier = self.get_tier(plan_coverage, rx_spend)
if new_tier is None: if new_tier is None: