diff --git a/data/hc_campaigns/hc_revalidation_due_soon.html b/data/hc_campaigns/hc_revalidation_due_soon.html new file mode 100644 index 0000000..30ca8d2 --- /dev/null +++ b/data/hc_campaigns/hc_revalidation_due_soon.html @@ -0,0 +1,106 @@ + +
+
+ + + + + + + + + + + +
diff --git a/scripts/build_healthcare_campaigns.py b/scripts/build_healthcare_campaigns.py index 4edd491..a898d81 100644 --- a/scripts/build_healthcare_campaigns.py +++ b/scripts/build_healthcare_campaigns.py @@ -59,6 +59,15 @@ SEGMENTS = { "campaign_name": "HC Warmup - Medicare Revalidation", "selector": "reval_overdue", }, + "revalidation_due_soon": { + "subject": "Your Medicare revalidation deadline is approaching", + "template": "hc_revalidation_due_soon.html", + "cta_path": "/order/npi-revalidation", + "price": "$599", + "list_name": "HC Warmup - Revalidation Due Soon", + "campaign_name": "HC Warmup - Revalidation Due Soon", + "selector": "reval_due_soon", + }, "npi_reactivation": { "subject": "Your NPI / Medicare enrollment appears deactivated", "template": "hc_npi_reactivation.html", diff --git a/scripts/build_healthcare_campaigns_cron.py b/scripts/build_healthcare_campaigns_cron.py index 45b5c26..b25ed78 100644 --- a/scripts/build_healthcare_campaigns_cron.py +++ b/scripts/build_healthcare_campaigns_cron.py @@ -74,7 +74,7 @@ def load_suppressed() -> set[str]: # without overwhelming the warming IPs. ACTIVE_SEGMENTS = os.getenv( "HC_SEGMENTS", - "revalidation_overdue,oig_screening,nppes_outdated,npi_reactivation,compliance_bundle", + "revalidation_overdue,revalidation_due_soon,oig_screening,nppes_outdated,npi_reactivation,compliance_bundle", ).split(",") # Warmup deliverability guard: only mail SLIGHTLY-overdue providers. A practice @@ -84,6 +84,11 @@ ACTIVE_SEGMENTS = os.getenv( # warming IP's reputation. Window is inclusive [MIN, MAX] days overdue. WARMUP_OVERDUE_MIN = int(os.getenv("HC_OVERDUE_MIN", "1")) WARMUP_OVERDUE_MAX = int(os.getenv("HC_OVERDUE_MAX", "90")) +# Proactive "revalidation due soon" window (days UNTIL the due date). Mirrors the +# overdue window so we reach providers shortly before AND after their deadline, +# roughly doubling the deliverable warmup pool from the same CMS data source. +WARMUP_DUE_SOON_MIN = int(os.getenv("HC_DUE_SOON_MIN", "1")) +WARMUP_DUE_SOON_MAX = int(os.getenv("HC_DUE_SOON_MAX", "90")) def _overdue_days(r: dict): @@ -245,6 +250,20 @@ def row_matches(seg_key: str, r: dict) -> bool: return False od = _overdue_days(r) return od is not None and WARMUP_OVERDUE_MIN <= od <= WARMUP_OVERDUE_MAX + if sel == "reval_due_soon": + # Proactive: revalidation is UPCOMING within the lookahead window. Pitch + # is "handle it before your deadline" -- taps the same CMS Revalidation + # Due Date List as reval_overdue but the (much larger) not-yet-due slice, + # so it grows warmup supply without touching a new data source. + # days_overdue is negative for upcoming (days until due), so a provider + # due in N days has days_overdue == -N. + if status != "upcoming": + return False + od = _overdue_days(r) + if od is None: + return False + days_until = -od + return WARMUP_DUE_SOON_MIN <= days_until <= WARMUP_DUE_SOON_MAX if sel == "reval_upcoming": return status == "upcoming" if sel == "leie_or_deactivated": # Reactivation targets: flagged excluded, OR no longer on the reval list @@ -265,16 +284,28 @@ def row_matches(seg_key: str, r: dict) -> bool: def attribs_for(r: dict) -> dict: + # days_overdue is positive when past due and negative when upcoming (days + # until the due date). Expose a clean positive "days_until" for the + # due-soon segment's template. + od_raw = (str(r.get("days_overdue", "")) or "").strip() + days_until = "" + try: + od = int(od_raw) + if od < 0: + days_until = str(-od) + except ValueError: + pass return { "npi": r.get("npi", ""), "practice": r.get("name", ""), "specialty": r.get("specialty", ""), "state": r.get("state", ""), # Separate fields so the email's "official CMS record" card can render - # the due date + overdue count cleanly (mirrors the CMS Revalidation Due - # Date List, verified by NPI via the weekly hc_data_refresh). + # the due date + overdue/until count cleanly (mirrors the CMS + # Revalidation Due Date List, verified by NPI via the weekly refresh). "reval_due_date": r.get("reval_due_date", ""), "days_overdue": str(r.get("days_overdue", "")), + "days_until": days_until, "detail": (f"{r.get('reval_due_date','')} ({r.get('days_overdue','')} days overdue)" if r.get("reval_status") == "overdue" else r.get("reval_due_date", "")), }