Commit graph

477 commits

Author SHA1 Message Date
justin
7d61b4aad4 dev: remap api(3002)/site(4323) ports in override to avoid prod collision on shared host 2026-06-05 23:07:22 -05:00
justin
61dac80dc6 hc-email: PTR/FCrDNS for hc IPs (.107-.109 -> hcmta01-03) done + SPF/DKIM/DMARC verified 2026-06-05 23:01:34 -05:00
justin
8c51fa4b99 docs: record dual-stream implementation status + remaining DNS/prod steps 2026-06-05 19:22:02 -05:00
justin
90d8b94f3f feat(email): wire listmonk-hc into deploy + dev override + hc ramp-cap
- deploy.sh/deploy-dev.sh: bring up listmonk-hc (upstream image, excluded from
  build); document the one-time listmonk_hc DB create + --install.
- docker-compose.dev.override.yml: dev-only override (committed) that drops the
  prod host-port bindings and pins dev's own postgres volume (dev-pgdata) via
  compose !override tags. deploy-dev ships it as docker-compose.override.yml so
  syncing the canonical compose to the shared host no longer breaks dev's
  api-postgres (port :5432 clash + volume switch). Discovered + fixed while
  validating listmonk-hc on dev.
- pw-hc-rampcap.sh: healthcare analogue of pw-listmonk-rampcap, ramps the
  listmonk_hc cap 100->1000/h off /etc/postfix/hc-warmup-start, fully
  independent of the trucking ramp/cap.
2026-06-05 19:19:45 -05:00
justin
08d5132459 feat(email): add listmonk-hc second instance for the healthcare HOT stream
Isolated from the trucking listmonk: own DB (listmonk_hc), own uploads volume,
own sliding-window cap. Configured (on dev) with 3 SMTP servers pointing at the
host Postfix hc submission ports 2526/2527/2528 so it round-robins the dedicated
hc IPs .107/.108/.109. Reaches the host MTA via the docker bridge gateway.

Note: the listmonk image needs an explicit one-time '--install --idempotent
--yes' against listmonk_hc (env vars alone do not auto-install this image tag).

Validated on dev: listmonk-hc container (172.19.0.16) -> host :2526 (hcsubmit107)
-> hcout1 (.107) -> real gmail MX; both listmonk instances Up.
2026-06-05 19:18:35 -05:00
justin
70d742df08 feat(mta): healthcare HOT-stream Postfix setup (dedicated hc IPs, isolated)
Adds 3 hc submission ports (2526/2527/2528) in the single Postfix instance,
each content_filter'd onto a dedicated hc transport (hcout1/2/3) binding the
hc IPs .107/.108/.109 with hc HELO identity (hcmta01-03) and hotter concurrency.
listmonk-hc round-robins the 3 ports.

Discovered + documented the constraint that drove this shape: transport_maps
randmap is owned by the shared trivial-rewrite(8) and is global, so neither a
per-smtpd -o transport_maps nor a FILTER randmap:{...} can scope a separate IP
pool (FILTER parses randmap as a literal transport). content_filter=hcoutN:
(empty nexthop) overrides transport_maps and keeps the real recipient domain.

Verified end-to-end on the server: :2527 -> hcout2 (.108) -> real gmail MX;
trucking transport_maps (.94-.96) untouched. Idempotent, postfix-check gated
with auto-rollback.
2026-06-05 19:07:02 -05:00
justin
289c3b91be feat(healthcare): split outreach list into 3 outbound streams
Add scripts/healthcare_email_streams.py as the single source of truth for
classifying NPPES-endpoint emails into institutional (HOT stream) / consumer
(trucking-discipline stream) / direct (DirectTrust, parked), plus an exclude set
for non-prospect giants (va.gov, *.mil, cvshealth, walgreens, walmart).

Rework build_npi_outreach_lists.py to emit one CSV per stream
(npi_healthcare_institutional/consumer + npi_direct_secure), overdue-first
sorted, with companion files (revalidation/leie/optout) now optional.

Verified on May 2026 NPPES endpoint_pfile: 89,557 institutional / 19,366 consumer
/ 242,441 direct rows.
2026-06-05 18:59:44 -05:00
justin
54a342059b docs: lock dual-stream email decisions + verified institutional audience size
Measured against May 2026 NPPES endpoint_pfile (tightened HISP filter):
- 92,592 institutional NPIs across 38,873 practice domains (76% single-provider)
- 19,072 consumer-webmail NPIs (ride trucking discipline)
- 242,441 Direct/HISP rows parked until DirectTrust.
Decisions: single Postfix + class hc transport (:2526), 2nd Listmonk instance
(listmonk-hc, own cap + own listmonk_hc DB), 10k/day institutional ceiling.
2026-06-05 18:57:18 -05:00
justin
40090da1dd docs: plan dual-stream outbound email (healthcare-hot + trucking-trickle)
Today one global Listmonk cap + shared Postfix rotation pool governs all mail,
sized to protect consumer-ISP (Gmail/MS/Yahoo) reputation for trucking cold mail.
Healthcare practice-domain (institutional) mail has an independent deliverability
profile and should run hotter without endangering the warmed trucking IPs.

Plan: isolate two streams sharing one Postfix/Listmonk:
- carve hc-dedicated sending IPs (.107-.109) with their own PTR/SPF + warmup;
- a 2nd Postfix submission service (:2526) bound to the hc pool;
- a 2nd Listmonk instance (or SMTP server) with its own sliding-window cap;
- split the healthcare list into institutional (hot) vs consumer-webmail (rides
  trucking discipline) vs DirectTrust (parked);
- free MX+SMTP verify the institutional list on a non-sending IP first.

Includes mermaid topology, separate hc warmup/cap schedule, validation (isolation/
identity/deliverability/cap proofs), and open decisions for sizing.
2026-06-05 18:51:05 -05:00
justin
4f49fad7f9 deploy: bring up the healthcare proxy-relay sidecar on prod and dev
deploy.sh: include proxy-relay in the default service set; it's an upstream
image (ginuerzh/gost) with no build context, so exclude it from 'compose build'
while keeping it in 'compose up'.

deploy-dev.sh: rsync docker-compose.yml to the dev server (it was never synced,
so new services like proxy-relay never reached dev) and add proxy-relay to the
'compose up --build' set.
2026-06-05 18:41:09 -05:00
justin
a79d6b1906 feat(healthcare): add gost proxy-relay so Chromium can use the residential proxy
Chromium rejects authenticated SOCKS5 ('Browser does not support socks5 proxy
authentication'). Add a gost (ginuerzh/gost:2.11.5) 'proxy-relay' sidecar that
listens unauthenticated on socks5://proxy-relay:11080 and forwards to the
authenticated residential upstream (HEALTHCARE_PROXY_UPSTREAM_URL). Workers point
Playwright at the relay via HEALTHCARE_PROXY_URL=socks5://proxy-relay:11080.

env template: split into HEALTHCARE_PROXY_UPSTREAM_URL (authenticated, password
percent-encoded so '#' -> %23) and HEALTHCARE_PROXY_URL (the relay address).

Validated end-to-end on dev: workers Chromium -> proxy-relay -> residential
egress IP 76.228.206.147; NPPES + PECOS both HTTP 200.
2026-06-05 18:39:26 -05:00
justin
4060fd7562 fix(proxy): parse proxy creds with URL-reserved chars (e.g. '#') correctly
The residential proxy password contains a '#', which urlparse() misreads as a
URL fragment and corrupts the port (ValueError: Port could not be cast...).
Parse scheme://creds@host:port manually and percent-decode user/pass so both
raw ('#') and encoded ('%23') passwords work. Verified against the live
credential.
2026-06-05 18:34:19 -05:00
justin
17318f6e7d feat(healthcare): route NPPES/PECOS Playwright flows through residential SOCKS proxy
CMS healthcare portals (NPPES, PECOS, I&A) block datacenter IPs, so the
healthcare browser automation needs to egress via the residential proxy on
hg409y7ez04.sn.mynetname.net (username 'performancewest').

- undetected_browser: use_proxy now accepts an env-var name, so callers can
  select a domain-specific proxy. _proxy_config(proxy_env) reads it and falls
  back to UNDETECTED_PROXY_URL. Healthcare uses 'HEALTHCARE_PROXY_URL'.
- probe_npi_undetected: launches with use_proxy='HEALTHCARE_PROXY_URL' when set.
- npi_provider: documents that the (future) automated NPPES/PECOS flows must
  use the healthcare proxy.
- Plumb HEALTHCARE_PROXY_URL (+ UNDETECTED_PROXY_URL fallback) through the
  ansible env template and docker-compose workers env.

The credential itself is NOT in the repo. Set the full URL in the ansible
vault as vault_healthcare_proxy_url:
  socks5://performancewest:<password>@hg409y7ez04.sn.mynetname.net:<port>
Verified parsing + Playwright proxy-dict wiring with a unit test.
2026-06-05 14:36:01 -05:00
justin
bd9a70607f fix: maintain Services dropdown header from one canonical source
The site header / Services mega-dropdown was duplicated across two render
systems (Astro pages via Base.astro->nav.html, and ~80 pre-rendered static
public/**/index.html pages each embedding their own copy). They had drifted
into 5 different variants (missing 'New Carrier Setup', misplaced Healthcare
column, NEW vs FREE badges, em-dash encoding differences), so
dev.performancewest.net, the order pages, and the rest of the site disagreed.

- Make site/src/partials/nav.html the single source of truth (adopts the most
  complete variant).
- Add scripts/sync_nav.py to rewrite every static page's <nav> block from
  nav.html (idempotent; --check guards against drift in CI/deploy).
- Run the sync automatically in deploy.sh and scripts/deploy-dev.sh.
- Deprecate scripts/inject_healthcare_nav.py (now delegates to sync_nav.py).
- Neutralize the broken no-op SiteNav.astro component.

All 80 headers + the Astro-built order pages now render the identical dropdown.
2026-06-05 14:27:24 -05:00
justin
695ace207c Reframe healthcare filing as standard vs expedited; e2e test + bug fixes
Copy: drop paper/electronic/fax framing across the revalidation + enrollment
marketing pages and the order-confirmation email; present two service tiers:
- Standard filing  (no CMS account; we prepare CMS-855, you sign, we submit to MAC)
- Expedited filing (CMS I&A surrogate access; same-day PECOS filing + tracking)
Internal worker todos + the _STANDARD_FILING_SLUGS identifier updated to match.

New scripts/test_healthcare_e2e.py validates the whole order line (slug
consistency x6 places, price agreement, intake field collection+enforcement,
worker dispatch, handler execution producing CMS-855 PDF+anchor, free-tool
action_urls). 45 checks.

Bugs found + fixed by the test:
- medicare-enrollment requires practice_state server-side but the wizard never
  enforced it -> orders could be paid then stall. Wizard now requires it.
- determine_form_type defaulted org NPIs to the individual 855I because
  enumeration_type is never collected -> wrong form, CMS rejection. Now does a
  live NPPES lookup (safe 855I fallback).
2026-06-05 03:58:46 -05:00
justin
5cfe9702e2 Add Healthcare/NPI section to nav dropdown across all static pages
The site's pre-rendered public/**/index.html pages each embed their own copy
of the Services mega-dropdown and do not read src/partials/nav.html, so the
earlier nav.html-only edit never appeared. inject_healthcare_nav.py adds the
canonical Healthcare block (Medicare Revalidation, Medicare Enrollment, NPI/
NPPES Services, free NPI Compliance Check) to the desktop Column 3 + mobile
menu of all 80 static pages. Idempotent.
2026-06-05 03:05:19 -05:00
justin
e212f20a34 Add CMS-855 PDF filler + e-sign fulfillment for Medicare revalidation/enrollment
- cms855_pdf_filler.py: fills official CMS-855I/B/O/A AcroForms from intake
  (name, NPI, DOB, cert-page printed name) and records the signature anchor at
  the form's official /Sig box so the e-sign stamper lands on the cert line.
- npi_provider handlers (revalidation/reactivation/enrollment) now generate the
  paper CMS-855, upload it to MinIO, request_esign with anchors, and email the
  signing link. Human completes/verifies + USPS Priority Mails to the MAC.
- scripts/Dockerfile: copy the official CMS-855I/B/O/A forms into the image.
2026-06-05 02:27:11 -05:00
justin
31a53f89a6 feat(npi): offer paper CMS-855 path (e-sign + we mail to MAC) alongside I&A surrogacy
- order-confirmation email presents both filing methods: paper CMS-855 (no
  account needed, client e-signs one page, we print+mail to their MAC) and
  I&A surrogacy (faster, needs CMS account). NPPES-only services note that
  surrogacy is required (web-only).
- npi_provider handlers record the access model per service in admin todos.
- marketing copy leads with the lowest-friction paper option.
2026-06-05 01:53:44 -05:00
justin
e32193352b fix(npi): lenient CSV decoding in companion loader (CMS exports have stray latin-1 bytes) 2026-06-05 01:38:02 -05:00
justin
157c7a2571 test(npi): add slug consistency check across all wiring places 2026-06-05 01:35:04 -05:00
justin
4b0155542e feat(npi): healthcare marketing pages, nav dropdown, NPI lookup API + free tool + companion data migration/loader 2026-06-05 01:33:36 -05:00
justin
f349d519c6 feat(npi): add NpiIntakeStep wizard + 6 healthcare order pages 2026-06-05 01:26:58 -05:00
justin
e67db156e8 feat(npi): wire 6 healthcare services into catalog, intake, items, handlers, portal 2026-06-05 01:25:05 -05:00
justin
8748c0a141 docs: NPI/healthcare products implementation plan 2026-06-05 01:21:58 -05:00
justin
73e09b12a0 feat: NPI outreach list pipeline (120k cold-emailable + 236k DirectTrust-later) + doc 2026-06-05 01:08:26 -05:00
justin
091ebbd7f9 docs: verified free NPI email-append paths (NPPES endpoint file + free SMTP/MX verify) 2026-06-05 01:00:54 -05:00
justin
604ad151c7 docs: add verified NPI services/pricing + companion-db analysis (217k overdue revalidations) 2026-06-05 00:57:09 -05:00
justin
2db2106ad5 docs: add postcard print-and-mail vendor pricing (Lob verified, PostGrid/Click2Mail quote-gated) 2026-06-05 00:38:41 -05:00
justin
5e4e73674a docs: verify NPPES + EPA RCRA field schemas against live files 2026-06-05 00:34:56 -05:00
justin
70d05e0607 docs: new compliance sectors (NPPES/FMC/EPA) + contact channels beyond postal mail 2026-06-05 00:25:51 -05:00
justin
8400e27d12 Add DOT check CTA to trucking deficiency emails 2026-06-04 18:29:01 -05:00
justin
327c4c9790 Use DOT checker hero on trucking order pages 2026-06-04 13:32:17 -05:00
justin
fcb56a4707 Simplify paid intake review flow 2026-06-04 13:05:21 -05:00
justin
74acda7171 Add trucking order page trust header 2026-06-04 13:01:56 -05:00
justin
40d5643116 Fill trucking campaign quotas with sendable subscribers 2026-06-04 12:36:22 -05:00
justin
e5e70b744b Guard bounce watcher against empty queue IDs 2026-06-04 12:33:53 -05:00
justin
c027d49f43 Fix trucking campaign cron send date 2026-06-04 03:19:35 -05:00
justin
b48fc3a406 Retire burned MTA IPs in warmup script 2026-06-03 23:37:27 -05:00
justin
f42833cc9d Set trucking campaign Reply-To header 2026-06-03 23:35:28 -05:00
justin
ef79e85b41 Align trucking source campaign placeholder list 2026-06-03 23:32:53 -05:00
justin
5c35140a22 Configure trucking deficiency campaign cron env 2026-06-03 23:04:41 -05:00
justin
72da37e47d fix: simplify order price banner copy 2026-06-03 14:02:55 -05:00
justin
965b9ce3c8 fix: add tax deductibility notice to trucking orders 2026-06-03 13:48:37 -05:00
justin
ee07064b0f fix: show trucking order prices and payment options 2026-06-03 13:24:40 -05:00
justin
d7de818f39 fix: stagger trucking campaign catchups and subscriber reattach 2026-06-03 13:21:16 -05:00
justin
6d4c323ab6 feat: daily intake-reminder worker for paid orders with incomplete intake
Adds a systemd-timed worker that nudges customers who paid but never completed
their intake form (which stalls fulfillment).

- migration 087: intake_reminder_count + intake_reminder_last_at on
  compliance_orders (makes the daily run idempotent and bounded), plus a
  partial index for the paid-order eligibility scan.
- scripts/workers/intake_reminder.py: each run emails any paid order with
  intake_data_validated != TRUE, capped at 10 reminders/order, at most one
  consolidated email per customer per day (groups a customer's incomplete
  services into one email). Reuses the post-payment intake URL format
  (/order/{slug}?order={n}) and the API's email validation, skipping
  placeholder/invalid addresses (synthetic@, pipeline.com, etc.). Sends via
  smtplib with SMTP_PASS (verified working in the worker container).
- worker-crons: pw-intake-reminder timer, daily ~noon ET (16:00 UTC).
2026-06-03 00:20:37 -05:00
justin
00c960f5b5 build: pin payments to version-15 + stage apps in deploy.sh erpnext
Two build fixes surfaced while shipping the set-password rename:

1. erpnext/Dockerfile cloned frappe/payments unpinned; its default branch now
   requires Python >=3.14 while frappe/erpnext:v15 ships 3.11, so the image
   build failed with 'Package payments requires a different Python'. Pin the
   clone to --branch version-15.

2. deploy.sh built the erpnext image without first staging the custom Frappe
   apps into the build context (erpnext/build.sh). That meant a baked-code
   change could silently ship stale code. Stage apps when erpnext is built.
2026-06-02 23:13:01 -05:00
justin
5c1341e6a1 portal: fix dead set-password link (rename controller to underscore)
Root cause of the 'Link invalid' onboarding link: Frappe's TemplatePage
resolves a www page's Python controller by converting hyphens to underscores
(see frappe/website/page_renderers/template_page.py set_pymodule: it looks for
'set_password.py' next to 'set-password.html'). Our controller was named
'set-password.py' (hyphen), so os.path.exists() missed it, pymodule_name stayed
None, get_context never ran over HTTP, and the template rendered with no
context -> raw {{ email }}, title 'Link invalid', token never verified. (It
worked under bench/in-process only because we called get_context directly.)

Fix: rename www/set-password.py -> www/set_password.py (route stays
/set-password, driven by the .html filename) and update the whitelisted submit
endpoint path in set-password.html to ...www.set_password.submit.

NOTE: the sibling legacy CRTC/CDR admin pages (admin-filings.py,
admin-resellers.py, cdr-*.py) have the same latent hyphen bug; left as-is since
they're outside the compliance portal, but they are silently controller-less.
2026-06-02 23:10:36 -05:00
justin
668fc6783b compose: give ERPNext CUSTOMER_JWT_SECRET + DATABASE_URL (fix portal drift)
The erpnext service was missing both env vars that the portal needs:
- CUSTOMER_JWT_SECRET: verifies /set-password magic-link tokens signed by the
  API. Without it, the set-password page resolved an empty/placeholder secret
  and showed 'Link invalid' for every customer onboarding link.
- DATABASE_URL: lets www/orders.py read compliance_orders from Postgres for the
  portal's Compliance section.

Both were present on api/workers but never wired to erpnext -> drift. Now the
single ERPNext portal can actually verify invites and show compliance orders.
2026-06-02 23:02:58 -05:00
justin
f6419759e6 portal: converge all compliance orders on the single ERPNext portal
Root cause of customers being unable to log in: ERPNext (portal.performancewest.net)
is the intended single portal and already surfaces compliance/trucking orders
(performancewest_erpnext/www/orders.py reads compliance_orders by email). But
only the Stripe checkout path provisioned the ERPNext Website User up-front
(findOrCreateCustomer). PayPal / crypto / remediation-pipeline orders go straight
to handlePaymentComplete, which created NO portal user and never set
portal_user_created -> no login + no set-password invite (exactly what happened
to the Paul Wilson / Compound Technologies PayPal order).

- handlePaymentComplete: add ensureCompliancePortalUser() in the shared
  post-payment path so EVERY paid compliance order (any payment method) gets an
  ERPNext portal account + the set-password invite. Idempotent.
- Guard against placeholder emails (synthetic@/pipeline.com etc): skip portal
  provisioning and the set-password invite for non-deliverable addresses.
- compliance-orders API: validate email format AND reject placeholder addresses
  at order creation (was: presence-only, so synthetic@pipeline.com passed).
- delivery_worker: never email a set-password invite to a placeholder address.

Note: the legacy PG-customers login (api/routes/portal-auth.ts, /account/*) is
CRTC/formation-era and only backfills canada_crtc_orders/orders, never
compliance_orders. ERPNext is now the consistent portal for compliance.
2026-06-02 22:44:34 -05:00