hc: add 'revalidation due soon' warmup segment (proactive, grows supply)

The HC warmup pool is supply-constrained (~400 verified providers, all fed by
the same narrow 'revalidation 1-90 days OVERDUE' slice). This adds a mirror-image
proactive segment that targets providers whose Medicare revalidation is UPCOMING
within the next 1-90 days, drawn from the same CMS Revalidation Due Date List --
no new data source needed. 'Handle it before your deadline' is a strong pitch and
roughly doubles the deliverable pool.

- New selector reval_due_soon (status=upcoming, days_until in [HC_DUE_SOON_MIN,
  HC_DUE_SOON_MAX] default 1-90).
- New segment revalidation_due_soon reusing the existing /order/npi-revalidation
  service ($599) with template hc_revalidation_due_soon.html.
- attribs_for now exposes days_until (positive days to due date).
- Added to ACTIVE_SEGMENTS.
This commit is contained in:
justin 2026-06-12 19:33:49 -05:00
parent 773c443079
commit c8c9a04c1d
3 changed files with 149 additions and 3 deletions

View file

@ -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",

View file

@ -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", "")),
}