build_healthcare_campaigns.py had a divergent inline HTML generator (old teal- header + yellow issue-box layout, missing the official-record card and the per- segment verify-it-yourself blocks) that nobody called -- the live cron reads the hand-tuned data/hc_campaigns/hc_*.html files directly. Removed the dead generator + cmd_render(); render() now READS the canonical template file so the files can't drift from a parallel generator. SEGMENTS is now a metadata registry (subject, template, cta_path, price, list_name, campaign_name, selector) that the multi-segment cron will consume. Verified --list and that send-test still reads the real bodies.
181 lines
8.1 KiB
Python
181 lines
8.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Healthcare (NPI/Medicare) marketing-email SEGMENT REGISTRY + test tooling.
|
|
|
|
SINGLE SOURCE OF TRUTH for the healthcare campaign segments. Each segment maps a
|
|
compliance problem to a real PW service, its order page, price, the listmonk
|
|
list/campaign it warms, and the canonical HTML template under data/hc_campaigns/.
|
|
|
|
The HTML bodies themselves are the hand-tuned, deployed templates in
|
|
data/hc_campaigns/hc_<seg>.html (teal header, per-segment "verify it yourself"
|
|
trust block, official-record card on revalidation, etc.). This module does NOT
|
|
regenerate them -- it READS them, so the files stay the one source of truth and
|
|
can't drift from a parallel generator. (An earlier version of this script kept a
|
|
divergent inline generator; that was removed.)
|
|
|
|
Consumers:
|
|
* build_healthcare_campaigns_cron.py imports SEGMENTS to warm every segment.
|
|
* `--send-test <email>` sends every segment as a real test through the
|
|
healthcare HOT SMTP stream (host :2526 -> hcout1 -> .107) so you see exactly
|
|
what a provider receives. Personalization tokens are filled with sample data.
|
|
|
|
Listmonk personalization tokens used on real sends (filled from subscriber
|
|
attribs by listmonk; filled from SAMPLE here for test sends):
|
|
{{ .Subscriber.Name }} provider / practice name
|
|
{{ .Subscriber.Attribs.npi }} NPI
|
|
{{ .Subscriber.Attribs.practice }} practice / org name
|
|
{{ .Subscriber.Attribs.detail }} segment-specific detail (e.g. due date)
|
|
{{ .Subscriber.Attribs.reval_due_date }} / .days_overdue (revalidation card)
|
|
{{ UnsubscribeURL }} listmonk per-subscriber unsubscribe
|
|
"""
|
|
from __future__ import annotations
|
|
import argparse, os, smtplib, ssl
|
|
from email.mime.multipart import MIMEMultipart
|
|
from email.mime.text import MIMEText
|
|
from email.utils import formataddr, make_msgid
|
|
|
|
SITE = "https://performancewest.net"
|
|
PHONE = "(888) 411-0383"
|
|
FROM_NAME = "Performance West Compliance"
|
|
FROM_EMAIL = "compliance@performancewest.net"
|
|
REPLY_TO = "info@performancewest.net"
|
|
OUT_DIR = os.path.join(os.path.dirname(__file__), "..", "data", "hc_campaigns")
|
|
|
|
# ── Per-segment registry ───────────────────────────────────────────────────
|
|
# Metadata only. The email body lives in OUT_DIR/<template>. Fields:
|
|
# subject listmonk campaign subject line
|
|
# template HTML file under data/hc_campaigns/ (the canonical body)
|
|
# cta_path order page the CTA links to (NPI appended as ?npi=)
|
|
# price headline price (for reference / docs; lives in the template)
|
|
# list_name listmonk-hc list this segment is warmed into
|
|
# campaign_name listmonk-hc campaign name prefix (dated per build)
|
|
# selector which warmup-CSV rows belong to this segment (see cron)
|
|
SEGMENTS = {
|
|
"revalidation_overdue": {
|
|
"subject": "Action needed: your Medicare revalidation is overdue",
|
|
"template": "hc_revalidation_overdue.html",
|
|
"cta_path": "/order/npi-revalidation",
|
|
"price": "$599",
|
|
"list_name": "HC Warmup - Revalidation Overdue",
|
|
"campaign_name": "HC Warmup - Medicare Revalidation",
|
|
"selector": "reval_overdue",
|
|
},
|
|
"npi_reactivation": {
|
|
"subject": "Your NPI / Medicare enrollment appears deactivated",
|
|
"template": "hc_npi_reactivation.html",
|
|
"cta_path": "/order/npi-reactivation",
|
|
"price": "$449",
|
|
"list_name": "HC Warmup - Reactivation",
|
|
"campaign_name": "HC Warmup - NPI Reactivation",
|
|
"selector": "leie_or_deactivated",
|
|
},
|
|
"nppes_outdated": {
|
|
"subject": "Your NPPES record may be out of date",
|
|
"template": "hc_nppes_outdated.html",
|
|
"cta_path": "/order/nppes-update",
|
|
"price": "$349",
|
|
"list_name": "HC Warmup - NPPES Update",
|
|
"campaign_name": "HC Warmup - NPPES Outdated",
|
|
"selector": "reval_upcoming",
|
|
},
|
|
"oig_screening": {
|
|
"subject": "Are you screening for OIG / SAM exclusions?",
|
|
"template": "hc_oig_screening.html",
|
|
"cta_path": "/order/oig-sam-screening",
|
|
"price": "$299",
|
|
"list_name": "HC Warmup - OIG Screening",
|
|
"campaign_name": "HC Warmup - OIG Screening",
|
|
"selector": "any",
|
|
},
|
|
"compliance_bundle": {
|
|
"subject": "Get your provider compliance handled for the year",
|
|
"template": "hc_compliance_bundle.html",
|
|
"cta_path": "/order/provider-compliance-bundle",
|
|
"price": "$899/yr",
|
|
"list_name": "HC Warmup - Compliance Bundle",
|
|
"campaign_name": "HC Warmup - Compliance Bundle",
|
|
"selector": "optout_ending",
|
|
},
|
|
}
|
|
|
|
# Sample values for test sends (real sends use Listmonk subscriber attribs).
|
|
SAMPLE = {
|
|
"name": "Dr. Sample Provider",
|
|
"practice": "Riverbend Family Medicine",
|
|
"npi": "1234567890",
|
|
"detail": "06/30/2024 (706 days overdue)",
|
|
"reval_due_date": "06/30/2024",
|
|
"days_overdue": "706",
|
|
}
|
|
|
|
|
|
def template_path(seg_key: str) -> str:
|
|
return os.path.join(OUT_DIR, SEGMENTS[seg_key]["template"])
|
|
|
|
|
|
def render(seg_key: str, *, test: bool = False) -> tuple[str, str]:
|
|
"""Return (subject, html) for a segment. The html is the canonical
|
|
data/hc_campaigns/<template> file -- the single source of truth. For test
|
|
sends, listmonk tokens are filled with SAMPLE data so the email is viewable
|
|
standalone."""
|
|
s = SEGMENTS[seg_key]
|
|
html = open(template_path(seg_key)).read()
|
|
if test:
|
|
html = (html
|
|
.replace("{{ .Subscriber.Name }}", SAMPLE["name"])
|
|
.replace("{{ .Subscriber.Attribs.npi }}", SAMPLE["npi"])
|
|
.replace("{{ .Subscriber.Attribs.practice }}", SAMPLE["practice"])
|
|
.replace("{{ .Subscriber.Attribs.detail }}", SAMPLE["detail"])
|
|
.replace("{{ .Subscriber.Attribs.reval_due_date }}", SAMPLE["reval_due_date"])
|
|
.replace("{{ .Subscriber.Attribs.days_overdue }}", SAMPLE["days_overdue"])
|
|
.replace("{{ UnsubscribeURL }}", f"{SITE}/unsubscribe?test=1"))
|
|
return s["subject"], html
|
|
|
|
|
|
def cmd_send_test(to_addr: str, host: str, port: int):
|
|
from email.utils import formatdate
|
|
n = 0
|
|
for key in SEGMENTS:
|
|
subj, html = render(key, test=True)
|
|
msg = MIMEMultipart("alternative")
|
|
msg["Subject"] = f"[TEST] {subj}"
|
|
msg["From"] = formataddr((FROM_NAME, FROM_EMAIL))
|
|
msg["To"] = to_addr
|
|
msg["Reply-To"] = REPLY_TO
|
|
msg["Date"] = formatdate(localtime=True)
|
|
msg["Message-ID"] = make_msgid(domain="performancewest.net")
|
|
# Bulk-mail deliverability headers (Gmail/GMX strongly reward these).
|
|
msg["List-Unsubscribe"] = (
|
|
f"<mailto:unsubscribe@performancewest.net?subject=unsubscribe>, "
|
|
f"<{SITE}/unsubscribe?e={to_addr}>")
|
|
msg["List-Unsubscribe-Post"] = "List-Unsubscribe=One-Click"
|
|
msg["List-Id"] = "Performance West Healthcare Compliance <hc.performancewest.net>"
|
|
msg["Precedence"] = "bulk"
|
|
msg["X-Entity-Ref-ID"] = make_msgid(domain="performancewest.net")
|
|
msg.attach(MIMEText("Please view this email in HTML.", "plain"))
|
|
msg.attach(MIMEText(html, "html"))
|
|
with smtplib.SMTP(host, port, timeout=15) as s:
|
|
s.ehlo("hcmta01.performancewest.net")
|
|
s.sendmail(FROM_EMAIL, [to_addr], msg.as_string())
|
|
print(f" sent [{key}] -> {to_addr} ({subj})")
|
|
n += 1
|
|
print(f"done: {n} test emails sent via {host}:{port}")
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--send-test", metavar="EMAIL", help="send all segments as test to EMAIL")
|
|
ap.add_argument("--list", action="store_true", help="list the segment registry")
|
|
ap.add_argument("--smtp-host", default="127.0.0.1")
|
|
ap.add_argument("--smtp-port", type=int, default=2526, help="hc HOT submission port")
|
|
args = ap.parse_args()
|
|
if args.list:
|
|
for k, s in SEGMENTS.items():
|
|
print(f"{k:22} {s['price']:>8} {s['template']:30} -> {s['campaign_name']}")
|
|
if args.send_test:
|
|
cmd_send_test(args.send_test, args.smtp_host, args.smtp_port)
|
|
if not args.send_test and not args.list:
|
|
ap.print_help()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|