diff --git a/scripts/build_healthcare_campaigns_cron.py b/scripts/build_healthcare_campaigns_cron.py index 2a4bfa6..21051af 100644 --- a/scripts/build_healthcare_campaigns_cron.py +++ b/scripts/build_healthcare_campaigns_cron.py @@ -301,20 +301,23 @@ def add_subscriber(list_id: int, email: str, name: str, attribs: dict) -> bool: return False -def ensure_campaign(seg_key: str, list_id: int) -> int: - # Reuse an existing warmup campaign for this segment only if it's still - # ACTIVE (draft / running / paused / scheduled). A finished/cancelled one - # can't accept new subscribers or restart, so we create a fresh dated one -- - # that also picks up the latest email template (the canonical hc_*.html). +def ensure_campaign(seg_key: str, list_id: int, send_list_name: str | None = None) -> int: + # One campaign per (segment, day): each day's campaign is bound to that day's + # dedicated SEND list, so it mails ONLY that day's newly-imported slice. We + # therefore reuse a campaign only when it's the SAME day's campaign (matched by + # the dated name) and still active; we never reuse a prior day's campaign, + # because that would re-target an old slice. A finished/cancelled same-day + # campaign can't restart, so a fresh one is created (also picking up the latest + # canonical hc_*.html template). from datetime import date seg = SEGMENTS[seg_key] ACTIVE = {"draft", "running", "paused", "scheduled"} + dated = f"{seg['campaign_name']} - {date.today():%b %d %Y}" res = lm("/campaigns?per_page=100") for c in res.get("data", {}).get("results", []): - if c["name"].startswith(seg["campaign_name"]) and c.get("status") in ACTIVE: + if c["name"] == dated and c.get("status") in ACTIVE: return c["id"] body = open(template_path(seg_key)).read() - dated = f"{seg['campaign_name']} - {date.today():%b %d %Y}" payload = { "name": dated, "subject": seg["subject"], "lists": [list_id], "from_email": FROM_EMAIL, "type": "regular", "content_type": "richtext", @@ -552,13 +555,29 @@ def warm_segment(seg_key: str, rows: list[dict], slice_n: int, return 0 list_id = get_or_create_list(seg["list_name"], ["healthcare", "warmup", seg_key]) + # Per-day SEND list: the campaign targets ONLY today's newly-imported slice, + # never the whole accumulated segment list. Without this, each day's campaign + # re-mailed every prior subscriber (the cumulative list), so the cohort + # imported on day 1 received the identical email every subsequent day -- a + # 5-6x repeat that burned reputation and blocklisted ~12% of the list. The + # persistent `list_id` is still used for dedup/records; the dated send list is + # what the campaign actually sends to, so every provider gets exactly one send. + from datetime import date + send_list_name = f"{seg['list_name']} - SEND {date.today():%Y-%m-%d}" + send_list_id = get_or_create_list(send_list_name, + ["healthcare", "warmup", seg_key, "daily-send"]) n_ok = 0 for r in todo: email = r["email"].strip().lower() - if add_subscriber(list_id, email, r.get("name") or "", attribs_for(r)): + # Add to BOTH the persistent segment list (dedup/records) and today's + # send list (the campaign's only audience). + ok = add_subscriber(list_id, email, r.get("name") or "", attribs_for(r)) + add_subscriber(send_list_id, email, r.get("name") or "", attribs_for(r)) + if ok: imported.add(email); n_ok += 1 save_imported(seg_key, imported) - cid = ensure_campaign(seg_key, list_id) + # Campaign targets the per-day send list, NOT the cumulative segment list. + cid = ensure_campaign(seg_key, send_list_id, send_list_name) if start_campaign: try: lm(f"/campaigns/{cid}/status", {"status": "running"}, "PUT") @@ -566,7 +585,8 @@ def warm_segment(seg_key: str, rows: list[dict], slice_n: int, # Already running raises; that's fine. if "already" not in str(e).lower(): print(f"[hc-cron] {seg_key}: start warning: {e}") - print(f"[hc-cron] {seg_key}: imported {n_ok} into list {list_id}; campaign={cid}") + print(f"[hc-cron] {seg_key}: imported {n_ok} into list {list_id} " + f"(send list {send_list_id}); campaign={cid}") return n_ok