Commit graph

20 commits

Author SHA1 Message Date
justin
1acae2f20c healthcare: fix 4 bugs in segment-assignment + free-check email
Found during a bug-review pass of the one-email-per-provider work:

1. assign_all overwrite bug: an email on MULTIPLE rows (shared practice inbox /
   multiple NPIs -- 2,592 such emails, 299 with mixed status) was assigned by
   the LAST row, so a less-urgent row could clobber an urgent one (overdue ->
   free check). Now keeps the most-urgent (lowest-priority) assignment.

2. warm_segment double-import + wrong-row render: all of an email's rows passed
   the candidate filter, so it could be imported twice (over-counting the slice)
   and attribs_for could render a sibling row's blank due-date in the overdue
   email. Now requires row_matches(seg) for the specific row AND dedupes by
   email (one row per email).

3. free-check email rendered broken text ('last updated on  -- about  years
   ago', 'Last updated  . ~ yrs ago') for any provider whose NPPES date isn't
   cached yet (the free check goes to everyone, and the fill is gradual). Wrapped
   the example sentence + official-record card in listmonk {{ if
   .nppes_last_updated }}...{{ else }}...{{ end }}; added a date-free else
   branch. altbody keeps the conditionals (listmonk evaluates body+altbody), and
   the test/preview renderer gained a minimal {{ if/else/end }} evaluator so
   previews match real sends. Verified both branches render with zero unfilled
   tokens.

4. cross-cron double-send: pw-hc-campaign (warmup file) and pw-hc-nppes (63k
   file) share state but tracked imports per-segment; 312 emails overlap both
   files, so a provider could get an urgent email from one cron AND the free
   check from the other. Added load_all_imported() global guard (union of all
   segment state) so each provider gets exactly one healthcare email overall.

All verified: assignment regression test (10 cases) + new dup-email/guard checks
pass; all 6 templates render clean.
2026-06-20 16:14:44 -05:00
justin
0320dc17ba healthcare: one-email-per-provider by urgency priority + free check as default
Make the free NPI compliance check the catch-all for ALL verified institutional
providers, but route anyone with a more important/time-sensitive issue to THAT
email instead -- each provider gets exactly one email, their most urgent.

- SEGMENTS gain a 'priority' (lower=more urgent): reactivation 10, revalidation
  overdue 20, due-soon 30, bundle 45, free-NPI-check 100 (catch-all).
- assign_segment()/assign_all(): route each provider to the single
  highest-priority active segment whose selector matches; warm_segment() takes
  the assignment map and only claims its assigned providers (disjoint pools, no
  double-mailing). main() now splits the daily slice by priority order, serving
  urgent segments fully before the broad free-check consumes the remainder.
- nppes_outdated selector -> 'institutional_default' (every verified, non-
  deactivated row), since the free check's value no longer depends on staleness;
  list/campaign renamed 'HC Warmup - Free NPI Check'.
- FIX latent bug: reactivation selector treated 'not on CMS reval list' as
  deactivated -- false for org NPIs (would mis-tell active practices they're
  deactivated). Now uses the REAL nppes_deactivated flag (or OIG/SAM exclusion).
- Drop blanket oig_screening from the default rotation: it matched every row and
  would starve the catch-all, and the free check already screens OIG/SAM and
  routes to the paid fix on a hit. Still runnable via --segments.
- Add scripts/test_segment_assignment.py (10 cases incl. 'overdue AND stale ->
  overdue wins'); all pass.
2026-06-20 16:01:23 -05:00
justin
4ed1498ef3 healthcare: reframe NPPES email as a FREE NPI compliance check
Pivot the weakest healthcare email from an 'your record is out of date -> buy an
update' sell into a free, value-first compliance check (the funnel already
exists: /tools/npi-compliance-check + /api/v1/npi/lookup run 5 live gov checks --
NPI status, Medicare revalidation, OIG/SAM exclusions, NPPES freshness -- and
deep-link to the right paid fix).

- Subject: 'A free compliance check for your NPI' (was 'may be out of date').
- Header: 'Free NPI Compliance Check' covering NPPES/revalidation/exclusions/NPI.
- Body: keep the REAL last_updated date as a credibility hook ('we pulled your
  public records'), but frame it honestly ('that's usually fine') and pivot to
  the broader free check. Adds a 4-item 'your free check covers' card.
- CTA now -> /tools/npi-compliance-check?npi={npi} (prefills + auto-runs their
  own check on landing) with @TrackLink + UTM; dropped the straight-to-order
  NPPES CTA and the redundant 'look up on NPPES' button.
- Reassurance reframed to free-first ('the check is completely free; a fix is
  optional, flat-fee'). cta_path updated in the segment registry.
- Verified: render + plaintext + headless screenshot, CTA tracked, no stray
  order link, zero unfilled tokens.
2026-06-20 15:46:26 -05:00
justin
9e155d214c healthcare: cite REAL NPPES last_updated date in 'outdated' email
The NPPES 'may be out of date' email previously asserted staleness with no
per-record evidence (softened earlier to a generic 'periodic review required').
NPPES is fully public and every record carries basic.last_updated, so we now
cite the actual government date the provider can verify on the registry.

- enrich_nppes_last_updated.py: joins real basic.last_updated /
  enumeration_date / deactivated onto the institutional list via a cached,
  resumable per-NPI crawl (no batch endpoint exists). Adds nppes_last_updated,
  nppes_enumeration, nppes_years_stale, nppes_deactivated.
- cron: new 'nppes_stale' selector mails ONLY records >= 3yrs stale (env
  HC_NPPES_STALE_MIN_YEARS) and excludes deactivated NPIs; empty date => no
  match, so we never claim staleness without the government date to back it.
- template: headline + official-record card now show the real last_updated
  date and ~N-years-ago, sourced to npiregistry.cms.hhs.gov.
- attribs + test SAMPLE expose the new fields; verified render + plaintext.
2026-06-20 15:21:15 -05:00
justin
d8e3e40dda healthcare emails: remove prices, fix click tracking, de-risk claims
Diagnosing zero healthcare sales (11k sent, 5479 opens, 0 clicks, 0 orders).
Root cause of clicks=0: Listmonk only registers a link for tracking when the
href ends with the literal @TrackLink marker; all 10 hc templates lacked it
(trucking/CRTC have it). So the entire funnel was unmeasurable below 'open'.

Changes:
- Click tracking: append @TrackLink + UTM to every /order/ CTA across all 10
  templates (external gov self-verify links left untracked on purpose).
- Remove all service prices from emails (99/49/49/99yr/9mo). Price is
  now revealed on the order page after value is established; catalog
  (api/src/service-catalog.ts) stays source of truth. Kept the 0,000 OIG
  penalty stat (regulatory fact, not our price). Added a neutral 'flat fee shown
  up front' reassurance block where the fee table used to be.
- Compliance/honesty: the nppes_outdated email asserted a per-record
  'FLAGGED OUT OF DATE / detected' status, but its selector only checks
  deliverability and the data has no NPPES last-updated field -> unsubstantiated
  for every recipient. Reframed to a generally-true periodic-attestation message
  ('PERIODIC REVIEW REQUIRED', 'most practices drift out of date'). Same hedging
  applied to npi_reactivation ('may be deactivated ... confirm on official
  sources'). Substantiated reval 'past due' claims (backed by the public CMS
  Revalidation list) were kept.
- Fixed stale $299 OIG metadata in build script -> $79/mo (reference only).

Docs: docs/healthcare-competitive-pricing.md (benchmark research) and
docs/healthcare-email-compliance-review.md (CAN-SPAM / FTC / impersonation pass;
flags SOC2/HIPAA/PCI badge claims for owner confirmation).

Verified headless: all 10 render with 0 JS errors, exactly 1 tracked CTA each,
no price leaks.
2026-06-20 09:37:02 -05:00
justin
b73edadb89 hc: unlock the full 62k verified institutional pool for broad offers
The OIG-screening + NPPES-update segments were effectively limited to ~1,437
providers because the warmup 'any' selector excluded not-on-reval-list rows as a
deliverability proxy -- but that excludes almost the ENTIRE institutional list
(org NPIs aren't individual Medicare enrollees). Since we already SMTP-verified
all 63k inboxes, add an 'institutional_verified' selector that trusts our own
verification instead of reval-list presence. Result: OIG + NPPES-update now
address 62,422 (43x more), giving multiple broad offers to test engagement on.

- enrich_institutional_revalidation.py: fast local join of the institutional
  list to the CMS Revalidation Due Date List bulk file (revalidation_base.csv)
  by NPI -> adds reval_due_date/days_overdue/reval_status. ~1,437 are genuine
  Medicare enrollees (197 overdue / 164 due-soon) -> flagship $599 reval pitch.
- npi_reactivation stays on leie_or_deactivated (only REAL deactivations -- no
  false 'your NPI is deactivated' claims to active orgs).
2026-06-14 01:07:40 -05:00
justin
d1a9260854 hc: consistent striped official-record card + wire past-due overdue variant
- Upgrade the plain teal record banner to the authoritative barber-pole 'Official
  record' banner in the personal/turnover/overdue-personal templates (the switch
  to personal templates had dropped the striped look from live revalidation sends).
- nppes_outdated: replace plain info table with the striped 'Official record -
  NPPES NPI Registry' card (status honestly labeled as our compliance flag).
- Wire revalidation_overdue -> hc_revalidation_overdue_personal.html with a direct
  past-due subject ('Your Medicare revalidation is past due - let's get it filed')
  and PAST DUE status + days-overdue in the record card; due_soon stays warm.
- Striped card now on all 7 templates that show a real record; oig_screening and
  compliance_bundle correctly omit it (no specific record to display).
2026-06-13 21:55:50 -05:00
justin
16f3dd67e4 can-spam: add full street address to ALL email templates + wire HC personal variant
CAN-SPAM requires a valid physical postal address in every commercial email.
All 8 HC campaign templates and the FCC campaign_template.html only had
'Cheyenne, WY' (no street) -- added the full
'525 Randall Ave Ste 100-1195, Cheyenne, WY 82001' to match the (already-correct)
trucking templates. Audited every Listmonk source/sent campaign + wrapper
templates: all active sends carry address + unsubscribe.

Also: revalidation segments now use hc_revalidation_personal.html with subject
'Let's make sure your Medicare revalidation is handled in time'.
2026-06-13 21:27:16 -05:00
justin
c8c9a04c1d 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.
2026-06-12 19:33:49 -05:00
justin
0b0ff9d311 hc campaigns: make the HTML templates the single source of truth
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.
2026-06-08 02:57:49 -05:00
justin
3da7794a85 hc email: add 'verify every detail for accuracy' (core strength) so the filing isn't rejected 2026-06-06 16:49:36 -05:00
justin
4233c90a4f hc email: reframe value-add to 'No 2FA. No government portals.' (we have a portal; the pain is CMS 2FA/identity-proofing); cron creates fresh dated campaign when prior is finished; add hc bounce watcher (Postfix->listmonk-hc webhook, hard/complaint->blocklist) 2026-06-06 16:47:12 -05:00
justin
2aa9e770c9 healthcare email: add color variety - amber warning box (urgency) + red overdue date + green price/CTA, breaking up the all-teal look into a problem->relief->action color story 2026-06-06 04:58:44 -05:00
justin
8eea9a694f healthcare email: add 'About Performance West' explainer (regulatory compliance consulting firm) after the no-logins relief copy 2026-06-06 04:57:42 -05:00
justin
0d212787ef healthcare email: add 'No logins. No portals. No headaches.' value-add (sells the relief, hides the mechanics); research doc on verified no-login third-party submission paths 2026-06-06 04:53:26 -05:00
justin
53ec011198 email trust signals: add data-safety + guarantee + social-proof strip to HC, telecom (campaign_template), and trucking (6 source + active campaigns via injector). Vertical accents: teal/blue/orange 2026-06-06 04:13:16 -05:00
justin
2d3bccd31e healthcare email: white logo for teal header (was dark navy, invisible); drop NPPES-source footer line 2026-06-06 03:48:04 -05:00
justin
29c7a421e9 healthcare email: teal gradient header (matches site hero) + standalone CSV MX/SMTP verifier (binds .72 non-sending IP); gitignore PII warmup lists 2026-06-06 03:39:19 -05:00
justin
5129ebec5c healthcare email: add List-Unsubscribe/List-Id/Date/Precedence bulk headers to improve inbox placement on the cold hc IPs 2026-06-06 03:31:22 -05:00
justin
3859557506 healthcare: +$200 across all 6 provider services; add segmented marketing email builder (5 compliance-problem campaigns) + rendered HTML 2026-06-06 02:33:46 -05:00