Commit graph

143 commits

Author SHA1 Message Date
justin
e87715aee7 fix(portal): onboarding/login links last 7 days, not 60 min
The rescue onboarding emails hardcoded a 60-minute expiry -- way too short for a
paid customer who hasn't engaged yet (they may not check email for hours/days),
so Paul's and Mitchell's links expired before they used them. Onboarding links
now last 7 days (ONBOARDING_TTL_MINUTES); the standard security password-RESET
window bumped 30min -> 2h. Re-issued fresh 7-day links to all 3 affected
customers (none had set a password yet) via reissue-onboarding-links.mjs, cc'd.
2026-06-09 22:50:09 -05:00
justin
8d301a1ab7 fix(checkout): SO creation falls back to generic item if a catalog Item is missing
Tracing Mitchell's batch, SO creation 404'd on a missing ERPNext Item
(INTRASTATE-AUTHORITY) -- 30 of 70 catalog erpnext_item codes were missing in
ERPNext. Created all 30. Added a safety net: if createResource('Sales Order')
404s on a missing Item, retry once with every line remapped to the generic
COMPLIANCE-SERVICE item so a missing catalog Item never strands a paid order's SO.
2026-06-09 14:50:06 -05:00
justin
e2467752dc chore: export ensureComplianceSalesOrder for rescue/backfill use 2026-06-09 14:44:28 -05:00
justin
68e6b60951 fix: worker emails (localhost:25 -> SMTP relay) + create ERPNext SO on webhook payment
Two bugs found tracing Mitchell Allen's batch CB-95BA6C90 (5 DOT services, card):

1) Worker authorization/signing-link/status emails were sent via
   smtplib.SMTP('localhost', 25), which has no MTA in the workers container ->
   every send failed '[Errno 111] Connection refused', so customers never got
   their e-sign links and orders sat 'awaiting client signature' forever. Routed
   all 9 hardcoded localhost:25 sites (state_trucking, mcs150_update, boc3_filing,
   hazmat_phmsa, mailbox_setup, dot_esign, completion_emails) through the
   authenticated SMTP relay (SMTP_HOST/PORT/STARTTLS/login) + added a shared
   worker_email.send_worker_email helper.

2) The ERPNext Sales Order for compliance/compliance_batch was only created in
   the /checkout/create-session endpoint, but CARD orders confirm via the Stripe
   WEBHOOK -> handlePaymentComplete, which never created the SO. Result: every
   webhook-confirmed order had erpnext_sales_order=NULL and workers logged
   'Sales Order not found 404' then built from PG. Added idempotent
   ensureComplianceSalesOrder() to handlePaymentComplete so ALL payment methods
   (card-webhook, PayPal, crypto) create + link the SO.
2026-06-09 14:40:46 -05:00
justin
3c65dd8748 fix(checkout): pull company from intake_data (compliance has no customer_company col)
compliance_orders stores company in intake_data JSON, not a column; read it from
there (company/legal_name/entity_name) with graceful fallback. Fix e2e test seed
accordingly.
2026-06-09 14:31:42 -05:00
justin
9987b1e30d fix(checkout): create Postgres customers row on order completion (PayPal login bug)
Portal login + forgot-password read the Postgres customers table (bcrypt), NOT
ERPNext. ensureCompliancePortalUser (the common path for Stripe/PayPal/crypto via
handlePaymentComplete) only provisioned the ERPNext customer/website-user and
never created the customers row -- so customers (notably PayPal, who reach this
path directly) had no account to log into or reset a password against. Now upserts
the customers row (no password; ON CONFLICT keeps any existing hash) with name +
company so they can register/reset and log in immediately.

Also: narrowed the placeholder-email skip from 'any synthetic@ or pipeline.com' to
exactly 'synthetic@pipeline.com' (the FMCSA-census placeholder) so real customers
on those real consumer domains aren't wrongly skipped -- which is what bit Paul
Wilson. Added cc support to sendEmail. e2e-paypal-portal-fix.mjs is the regression
test (seeds a compliance order, runs handlePaymentComplete, asserts the customers
row is created). Rescue scripts for the affected customer included.
2026-06-09 14:28:19 -05:00
justin
a308aeed6b fix(checkout): batch SO custom_order_type must be 'compliance' not 'compliance_batch'
ERPNext's PW Order Type select field only allows formation/canada_crtc/bundle/
compliance. A batch is a compliance order (multi-service), so use 'compliance';
the multi-service nature is already captured by the line items + external order
id.
2026-06-09 00:27:17 -05:00
justin
baa40443de fix(checkout): create ERPNext Sales Order for compliance_batch orders
Batch orders (CB-XXXX, used by the trucking new-carrier flow and any multi-
service cart) never created an ERPNext Sales Order -- the SO-creation branch was
gated to order_type 'compliance' only. So those paid orders never reached
ERPNext for fulfillment/accounting (0 of all paid batch orders had an
erpnext_sales_order). Added a compliance_batch branch that creates ONE Sales
Order with a line item per service in the batch (+ government-fee + processing-
fee lines), then stamps the SO name on every batch row. Non-blocking like the
others. Also created the missing ERPNext Items the new slugs reference
(DOT-NEW-CARRIER-BUNDLE, LLC-FORMATION, CORP-FORMATION, NEW-CARRIER-BUNDLE which
was missing too, GOVERNMENT-FILING-FEE).
2026-06-09 00:23:15 -05:00
justin
c6819371d8 fix(checkout): trucking new-carrier ordered the wrong (telecom) product + ACH broke
Two reported bugs, plus two related ones found while tracing:

1. WRONG PRODUCT (Stripe showed 'FCC setup package' for a trucking order): the
   trucking new-carrier form reused the slug 'new-carrier-bundle', which is the
   TELECOM VoIP onboarding bundle (FRN+499+RMD+CPNI+CALEA, $1799). So trucking
   customers were charged the telecom product/price and saw FCC on their receipt.
   Added a distinct 'dot-new-carrier-bundle' (USDOT+MC+BOC-3+MCS-150+Drug&Alcohol,
   $599 + FMCSA gov fees) and pointed the trucking page at it.

2. ACH 500 error: the Stripe session requested the Financial Connections
   'balances' permission, which isn't activated on the account -> Stripe rejected
   the whole session (invalid_request_error). Removed 'balances' (+prefetch); we
   only need 'payment_method' to collect+charge the bank account.

Also fixed (found while tracing):
3. The telecom new-carrier-bundle's BUNDLE_COMPONENTS listed TRUCKING slugs by
   mistake (copy/paste) -- corrected to its real FCC components.
4. The trucking page offered llc-formation / corp-formation / foreign-qual which
   did not exist in the catalog (batch would 400). Added llc-formation +
   corp-formation; remapped foreign-qual -> foreign-qualification-single.

Catalog regenerated (66 -> 69 services), drift-check + tsc clean.
2026-06-08 23:42:36 -05:00
justin
ada9e01321 checkout(healthcare): collapse + reposition surrogate how-to when not granted
The order confirmation asked providers to optionally grant CMS I&A Surrogate
access, then showed the full how-to to everyone high up. Now it respects their
intake answer:
- Granted (surrogate_access=yes): keep the how-to prominent up top so they can
  complete the step they chose.
- Not granted / undecided: top block just confirms 'we're handling it, nothing
  else to do' (no nagging); the speed-it-up how-to moves lower and is collapsed
  behind a 'Show me how' <details> expander (still available, just out of the way).
2026-06-08 00:36:41 -05:00
justin
25cf23dded feat(orders): reduce friction & chargebacks across order flow
1. Email: add a 'Problem with your order? We're here to help' support band to
   the shared htmlEmail() footer, so EVERY transactional email (confirmation,
   portal link, receipts) has a prominent 'Get help with your order' button
   linking to /contact. Less silent frustration -> fewer chargebacks.

2. NPI order form: entering a 10-digit NPI now auto-fills provider name, practice
   state, and specialty from the live NPPES lookup (same API as the free
   compliance-check tool), with a 'Found: <name>' confirmation. Only fills empty
   fields so it never clobbers edits.

3. NPI order form: read ?npi= from the URL so the email 'Start my revalidation'
   click lands with the NPI prefilled and the rest auto-filled (was being
   ignored entirely before).

4. Support FAB: add the floating help button + panel to 27 static public pages
   that were missing it (order, portal, trucking, survey, upload pages), so help
   is one click away everywhere.
2026-06-08 00:24:17 -05:00
justin
483f185861 feat(healthcare): prove revalidation is real via official CMS data + self-verify
Skepticism ("is this even real?") is the top objection. The data IS accurate
(verified our subscribers' NPIs match the official CMS Revalidation Due Date List
exactly), so this is a credibility-presentation fix:

1. Email: replace the plain detail row with an "Official record - CMS Medicare
   Revalidation Due Date List" card (NPI, legal name, due date, days overdue)
   plus a "Verify on CMS.gov" button. Clearly labeled as our presentation of
   public CMS data, not a CMS screenshot (no impersonation).
2. API: npi/lookup now pulls the revalidation due date LIVE from the public CMS
   dataset (data.cms.gov) instead of the empty local table, and returns a
   revalidation{ due_date, source, cms_legal_name, verify_url } proof object.
3. Tool: /tools/npi-compliance-check shows a live "official record" card with a
   self-verify link when CMS returns a due date.

Builder now stores reval_due_date/days_overdue as separate attribs for the card
(existing 194 subscribers backfilled from their detail string).
2026-06-07 23:54:01 -05:00
justin
09e21a6c97 refactor(pricing): single source of truth for the service catalog
Previously two hand-maintained price lists (API COMPLIANCE_SERVICES + site
SERVICE_META) drifted apart -- that is how the healthcare +$200 raise charged
$399 while displaying $599. Eliminate the drift class entirely:

- Move the catalog to api/src/service-catalog.ts (the authority; checkout
  charges from it). compliance-orders.ts imports it.
- scripts/gen-service-catalog.mjs generates site/src/lib/service-catalog.generated.ts
  from the API source. intake_manifest.ts re-exports SERVICE_META from it, so all
  ~60 site pages keep working unchanged.
- deploy.sh regenerates + drift-checks before building (site build context is
  ./site only and cannot read ../api, so generation happens host-side).
- scripts/check-service-catalog-drift.mjs fails the build if the generated file
  ever diverges from the API (verified: passes aligned, fails on mismatch).

To change a price now, edit ONE file: api/src/service-catalog.ts.
2026-06-07 19:11:34 -05:00
justin
e54459776c fix(healthcare): apply the +$200 increase to the API price catalog (was display-only)
The healthcare +$200 raise (commit 3859557) updated the displayed prices
(intake_manifest + service pages) but NOT the API COMPLIANCE_SERVICES catalog,
which is what actually charges. Customers saw $599 but were charged $399.

  npi-revalidation           $399 -> $599
  npi-reactivation           $249 -> $449
  nppes-update               $149 -> $349
  medicare-enrollment        $499 -> $699
  oig-sam-screening           $99 -> $299
  provider-compliance-bundle $699 -> $899

checkout.ts charges order.service_fee_cents (set from this catalog at order
creation), so this makes charged = displayed for all 6 provider services.
2026-06-07 18:57:26 -05:00
justin
e5db147319 esign: make signing copy fully generic - remove all ink references from website/API
Client-facing and website code now describes only a generic per-document signing
authorization; nothing visible to signers or recorded in the website/API code or
DB schema references ink, paper, reproduction, or any fulfillment mechanics.

- rename esign-ink-consent.ts -> esign-sign-consent.ts; INK_CONSENT_TEXT ->
  SIGN_CONSENT_TEXT (generic: 'use my signature to complete and submit this
  single filing', no ink/paper/reproduce language); helpers ink* -> sign*
- portal-esign-generic.ts: API field ink_reproduction -> require_sign_consent,
  ink_consent_text -> sign_consent_text, request field ink_consent -> sign_consent
- signing page (site/public/portal/esign): all ids/vars/comments ink* -> sign*;
  no 'ink' string remains
- npi_provider metadata flag ink_reproduction -> require_sign_consent
- migration 090/092 + live DB column comments rewritten to drop ink/plotter
  wording (DB column names kept as ink_consent* for compat, internal only)
- order-timeline.ts buffer comments neutralized
- tests: 37 checks, consent text asserted to omit ink/plotter/paper/reproduce/etc

DB columns ink_consent* retained (internal, never sent to clients) to avoid a
risky rename of already-applied prod columns.
2026-06-07 05:06:26 -05:00
justin
a4bad723bc esign: ink-reproduction consent gate + patent-risk research
Consent gate (the legal linchpin from the wet-signature memo):
- migration 092 adds ink_consent/ink_consent_at/ink_consent_text to esign_records
- extract pure, unit-tested gate logic into esign-ink-consent.ts (DRY single
  source for route + signing page): isInkReproduction / inkConsentRequired /
  inkConsentSatisfied + verbatim client-safe INK_CONSENT_TEXT
- portal-esign-generic.ts: GET surfaces ink_reproduction + consent text; POST
  gates DRAWN signatures on ink-path docs on explicit consent, stores it
- signing page locks the signature block until consent is checked (drawn only)
- npi_provider marks cms855/cms10114 esign metadata ink_reproduction=true
- 33 unit checks: gate truth table + consent text omits all internal mechanics
  (plotter/machine/CMS/MAC/etc) and keeps required legal reassurances

Patent-risk memo (docs/legal/patent-risk-mechanical-wet-signature.md):
- prior-art-dated risk analysis (autopen 1803/1942, plotters, CNC = public domain
  => low risk on core concept; e-sign workflow space litigious)
- firsthand recent-grant sweep (1.58M USPTO grants 2021-2025, queried via DuckDB):
  ZERO patents on machine-applies-signature-in-ink; e-sign players hold only
  electronic-workflow patents. Not an FTO; flags where attorney search is needed
2026-06-07 04:44:11 -05:00
justin
aafa76df83 Add missing admin_todos migration (091)
The admin_todos table is written by 8 worker handlers (and the new shared
create_admin_todo helper) but had NO CREATE migration anywhere, so every
fulfillment-task insert silently failed in environments without the table.
Define it with the exact column set the handlers use, plus status/priority/
order indexes and operator workflow columns (assigned_to, notes, completed_at).

Applied 076,085,086,088,089,090,091 to the dev DB (all idempotent); verified
admin_todos, esign_records, paper_filing_batches, compliance_orders.
fulfillment_status, and esign_records.signature_vector all exist and accept the
handler insert shape.
2026-06-07 03:22:28 -05:00
justin
28b1af341d Wire fulfillment alerts to Telegram + surface order progress in portal + even out ERPNext sync
Telegram notifications:
- Add shared scripts/workers/telegram_notify.py (send_telegram, notify_fulfillment_todo,
  create_admin_todo) so every worker alerts the operator the same way; fire-and-forget.
- Fire notify_fulfillment_todo after each admin_todos insert across all 8 service
  handlers (9 sites) so no fulfillment task waits unseen.
  (Orders + quotes + tickets already notified via checkout/quotes/tickets routes.)

Client portal order progress:
- order-timeline: derive real per-step status from live signals (payment paid,
  e-signature signed, fulfillment_status) instead of a static template; add
  current_step to the response.
- Extract pure applyLiveStatus into order-timeline-status.ts (DB-free) + unit test
  (api/test/test_timeline_status.ts, 8 cases).
- portal /me now returns compliance_orders.fulfillment_status.
- Dashboard renders a client-safe Progress badge (In progress / Action needed /
  Filed-awaiting-confirmation / Completed); batches show the most actionable status.
  No back-office mechanics exposed.

ERPNext sync parity:
- Create a Sales Order for formation and fcc_carrier_registration orders (previously
  only canada_crtc + compliance synced); write erpnext_sales_order back to each table.
  Non-blocking, matches existing pattern.

Verified: API tsc clean, timeline unit tests 8/8, Astro build 58 pages,
cms10114/ink/paper_batch Python tests still green, no mechanics leaks.
2026-06-07 03:17:46 -05:00
justin
41df4d9553 healthcare: go live with wet-signature filings + 2-day ETA buffer
Adds explicit order timelines for the 5 CMS provider filings (nppes-update,
npi-reactivation, npi-revalidation, medicare-enrollment, provider-compliance-
bundle) and a +2 business-day buffer applied from the signature step onward,
giving us time to produce + mail the original ink signature while the plotter
station is brought online.

- Buffer only affects post-signature steps; pre-signature steps and all
  non-wet-signature services are unchanged (verified with date math).
- Single source of truth: order-timeline route (consumed by the order success
  page), so the buffer flows through to the customer-facing ETA.
- Remove WET_SIGNATURE_BUFFER_DAYS once the ink-signature station is in
  steady-state (see docs/plans/plotter-plan.md).

API tsc clean; buffer compiled into dist.
2026-06-07 02:56:41 -05:00
justin
b0a8563a93 ink-signature: pen-plotter pipeline for original wet-ink CMS signatures
The Standard no-login CMS path needs an ORIGINAL ink signature on paper
(CMS-10114: 'Stamped, faxed or copied signatures will not be accepted'). This
adds a pipeline to redraw the provider's own captured strokes in real ink with a
pen on a CR-10 V2 (or any Marlin/GRBL machine) — original, in ink, never copied.

- migration 090: esign_records.signature_vector (JSONB stroke paths, 0..1).
- signing page now captures normalized stroke paths alongside the PNG; API
  stores a size-bounded vector for drawn signatures.
- ink_signature_plotter.py (hardware-independent): fit strokes to the signature
  anchor box, PDF-pt -> bed-mm via jig offset, emit Marlin/GRBL G-code (Z pen or
  M280 servo/BLTouch), SVG toolpath preview, and render_signature_on_pdf (a
  digital twin that proves the toolpath lands on the cert line). Gated serial
  sender (dry_run default).
- ink_signature_cli.py: end-to-end load-record -> gcode+preview, --test-box jig
  calibration, --plot to stream over USB.
- Corrected CMS-10114 signature anchor to sit inside the Section 4A signing cell
  (above the bottom rule, below the label).
- docs/ink-signature-plotter.md documents the CR-10 retrofit + interpretive risk.

Tests: test_ink_signature.py 30/30, test_cms10114.py 27/27, test_paper_batch.py
15/15, API tsc clean, Astro build 58 pages.
2026-06-07 02:34:17 -05:00
justin
138fec17e9 healthcare: daily batched paper-filing fulfillment
Standard (no-login) CMS filings are mailed in one Priority Mail envelope per
destination agency, batched each postal working-day morning to save postage.

- migration 089: paper_filing_batches table + esign_records.paper_batch_id /
  filing_destination_key (idempotent: a filing is batched at most once).
- batch_cover_sheet.py: per-agency cover sheet (sender/dest/date/manifest) +
  merged print-job PDF (cover + all enclosed signed filings).
- daily_paper_batch.py worker: gather signed+unbatched cms855/cms10114 filings,
  group by destination (MAC by state via mac_routing; Fargo for CMS-10114),
  build cover+merged PDF per agency, persist batch, mark filings batched.
  Self-gates on postal working days (skips weekends + federal/USPS holidays).
  Phase 1 = human prints+mails; phase 2 = wire print-mail API.
- worker-crons: pw-paper-batch systemd timer (Mon-Fri 13:30 UTC, self-gated).
- test_paper_batch.py: 15/15 pass (working-day gating, routing, cover+merge).
2026-06-07 00:30:01 -05:00
justin
258d23bdc6 healthcare: two-tier (standard paper / expedited surrogate) filing model
- Verified Standard(no-login)/Expedited(surrogate) matrix from official CMS-855
  PDFs (docs/healthcare-filing-tiers-verified.md): reactivation+revalidation are
  855I paper-to-MAC reasons, original-signature, routed by state; sig may not be
  delegated; 855B needs PECOS app fee.
- Add scripts/workers/mac_routing.py: state->MAC routing (all 56 jurisdictions,
  12 destinations) for envelope addressing + daily batch grouping. Addresses
  marked VERIFY before live mail.
- npi_provider.py: fix access strings to two-tier framing; NPPES update/reactivation
  no longer 'online-only'; note 855B fee.
- checkout.ts + service pages: strip client-facing mechanics & the paper-vs-tier
  choice; surrogate is the only optional, positively-framed ask (faster, never
  required, never share password).
2026-06-07 00:24:56 -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
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
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
e67db156e8 feat(npi): wire 6 healthcare services into catalog, intake, items, handlers, portal 2026-06-05 01:25:05 -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
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
justin
06e59965cc DOT D&A: instant PDF compliance-program binder (49)
Turn the DOT Drug & Alcohol Compliance Program into an automated
instant-delivery deliverable: when a carrier orders, we generate a
complete, print-ready PDF binder and email it (no admin step).

The binder (dot_da_binder_generator.py) bundles everything a small
carrier needs under 49 CFR Part 382 + Part 40:
  - How to manage the program (DER setup + annual operations)
  - Written drug & alcohol testing policy for employees
  - The six DOT test scenarios + triggers
  - Random testing / consortium (C-TPA) instructions
  - Supervisor reasonable-suspicion training + live/online access
  - Violations, SAP access, return-to-duty / follow-up
  - EAP / rehab / treatment resources (SAMHSA, 988, locator, ODAPC)
  - Recordkeeping retention schedule
  - Ready-to-use forms (acknowledgment, reasonable-suspicion,
    post-accident decision worksheet)
  - Regulation citations
  - Optional state Drug-Free Workplace addendum

Policy-variant selection: FMCSA (Part 382) is the trucking default;
honors an explicit dot_da_mode override for FRA/PHMSA/FTA/FAA/USCG.

New DrugAlcoholProgramHandler returns the binder PDF; slug added to
INSTANT_DELIVERY_SLUGS so job_server emails it automatically. Slug
rerouted from MCS150UpdateHandler (was admin-assisted enrollment) and
re-priced as a discountable own-deliverable (no passthrough cost).

Tests: scripts/tests/test_dot_da_binder.py (FMCSA sections, PHMSA+state
addendum, all-modes render) — passing.
2026-06-02 19:28:58 -05:00
justin
29ad0908ee trucking: pass-through fee disclosure + state fulfillment status machine
Item 2 of the trucking state-authorization plan.

- compliance-orders.ts: populate gov_fee_label for every state-trucking
  service so the variable, billed-at-cost government charges (apportioned
  IRP, IFTA decals, NY HUT, CT HUF, weight-distance, CA MCP+CARB, OS/OW
  permits, bundle) are disclosed at checkout. price_cents stays the flat
  service fee; gov fees pass through at cost.
- migration 086: compliance_orders.fulfillment_status state machine
  (authorization_required -> authorization_signed -> awaiting_customer_
  delegation -> awaiting_secure_credentials -> awaiting_government_fee_
  approval -> awaiting_insurance_filing -> ready_to_file ->
  filed_waiting_state -> completed) + fulfillment_status_at
- state_trucking.py: FULFILLMENT_* constants + _set_fulfillment_status();
  gate sets authorization_required on pause, authorization_signed on
  resume, ready_to_file once the filing todo is queued
- TruckingValueNotice.astro: 'What's included & what's billed at cost'
  disclosure with the authorization/delegation explanation
2026-06-02 16:49:31 -05:00
justin
7ed06780bb trucking: stamp e-signature exactly on form signature lines + state authorization gate
Capture-to-form signature placement so the customer's drawn or typed
signature lands right on the signature rule of the actual form, not in a
sidecar page.

- migration 085: esign_records.signature_anchors (JSONB exact PDF coords,
  lower-left origin, points) + signed_document_minio_key
- signature_stamper.py: signature_box() anchors; anchors_from_acroform()
  pulls the signature field /Rect from a real AcroForm (e.g. MCS-150
  certifySignature); stamp_signature() overlays PNG (auto-trimmed so ink
  rests on the rule) or typed name, scaled to actual page size
- state_trucking_authorization.py: renders the Limited Authorization to
  File PDF and returns (pdf_bytes, anchors)
- esign_stamp.py: stamp_esign_document() downloads unsigned PDF, stamps,
  uploads _signed.pdf, sets signed_document_minio_key (idempotent)
- dot_esign.py: extract certifySignature anchor for MCS-150/closeout forms
  so the federal perjury cert is signed on the line
- state_trucking.py: authorization gate — first run emails signing link
  and PAUSES; resumes with client_approved after signing
- job_server handle_esign_completed: stamp then re-dispatch
- tests: test_signature_placement.py (custom form), and
  test_mcs150_signature_placement.py (official AcroForm) both assert the
  signature lands inside the recorded signature box (verified visually)
2026-06-02 16:44:19 -05:00
justin
345979ed00 Allow multiple referral codes per sales agent
Drop the UNIQUE constraint on sales_agents.email (migration 084) so a single
agent (person/company) can hold several referral codes, each with its own
client discount and commission split. All commission lookups already key on
the unique agent_code, so no lookup logic changes.

Agent-creation endpoint now:
- accepts repeat emails (creates an additional code instead of 409)
- accepts client_discount_value, commission_type, commission_pct per code
- reports existing codes for the email in the response

Both Jay Kordic codes (REF-JKORDIC 7%/12%, REF-JAYK05 5%/15%) now share his
real email jay_kordic@thehorizongroup.biz.
2026-06-02 14:44:22 -05:00
justin
53857574d3 Add referral/discount code to FCC carrier page + REF-JAYK05 agent
Frontend (order/fcc-carrier-registration):
- Add a referral/discount code box on the review step that validates
  against /api/v1/discount/:code and shows the discount line + adjusted
  total. Discount applies to service fee + add-ons, never state filing fees.
- Prefill + auto-apply from ?code= / ?ref= query param (referral links).

Backend (fcc-carrier-registration route):
- Accept discount_code, validate it, store discount_code/discount_cents,
  and subtract from the total. Checkout already reads discount_cents to
  apply the Stripe coupon.
- Create a pending commission when the code belongs to an active sales agent.

Commission fix (agents.createCommission):
- Percent-type agents now earn commission_pct on ALL order types. Previously
  canada_crtc/formation/bundle used flat defaults and ignored percent agents.

Agent: created sales agent Jay Kordic (The Horizon Group) with custom code
REF-JAYK05 -> client gets 5% off discountable services, agent earns 15%.
Idempotent setup script in scripts/create_agent_jaykordic.cjs.
2026-06-02 14:31:22 -05:00
justin
d420c49818 intake: prefill order form from ?dot= campaign CTA links
Campaign CTA buttons link to /order/<slug>?dot=1234567. Add a fast local-only
GET /api/v1/dot/census endpoint (vs the heavy 12s live /dot/lookup) and a ?dot=
branch in the Wizard that seeds intake_data from the carrier's cached FMCSA
census record (name, email, base state, city/street/zip, power units). The
existing StateTrucking step already prefills its inputs from intake_data, so the
form now shows up pre-populated. Best-effort: only fills empty fields, never
blocks the form, never overwrites visitor input.
2026-06-02 12:46:33 -05:00
justin
4010103531 Lower trucking compliance pricing across product + marketing surfaces
Permanent price cuts:
- MCS-150 Biennial Update: $69 -> $39
- UCR Annual Registration: $69 -> $39 (+ gov fee unchanged)
- MC Operating Authority: $349 -> $199 (+ $300 FMCSA fee unchanged)
- State compliance programs (IRP, IFTA, weight-distance/HUT/HUF/KYU,
  intrastate, OSOW, state DOT, state emissions): -> $109
- California MCP + CARB: $349 -> $229

Updated source of truth (compliance-orders.ts, intake_manifest SERVICE_META),
stale dot-lookup recommendation prices, all static trucking landing/marketing
pages (services/trucking/*, order/dot-compliance, pricing), and the email
campaign scripts (setup_trucking_campaigns, create_state_campaigns).
FE/BE price cross-check: all 16 changed slugs consistent. tsc clean,
fulfillment consistency 24/24, site build OK.
2026-06-02 10:45:07 -05:00
justin
b85be726b7 feat(fulfillment): bundle/exclusion enforcement + REQUIRED_FIELDS + intake wiring (Phases 1/1.5/2)
- compliance-orders: hazmat-phmsa/state-emissions products, full REQUIRED_FIELDS
  table for all DOT/state/hazmat slugs, BUNDLE_COMPONENTS dedup + MUTUALLY_EXCLUSIVE
  enforcement on /batch (single source of truth, exported)
- checkout: empty ADMIN_ASSISTED_SLUGS (state/hazmat now get intake links)
- services/__init__: register HazmatPHMSAHandler + state-emissions handler
- state_trucking: _summarize_intake admin-todo enrichment
- Wizard: wire StateTruckingIntakeStep + step labels
2026-06-02 03:51:25 -05:00
justin
fc1a0588f7 feat(advisory): prerequisite-aware DOT lookup + state recommendations
- DOT lookup now returns prerequisite_status {usdot_active, authority_active,
  authority_pending} from live FMCSA data so the order flow can advise
  sequencing BEFORE a customer places an order.
- State-requirements recommendations annotated with prerequisite + label
  (e.g. IRP/IFTA/state taxes need an active USDOT) for UI warnings.
2026-06-02 03:34:40 -05:00
justin
869bcac287 fix batch SO item_code (use erpnext_item) + notification surcharge breakdown
ROOT CAUSE of orders never fulfilling: the batch Sales Order used the service
SLUG as item_code (e.g. 'mcs150-update') but ERPNext items use the catalog
erpnext_item codes ('MCS150-UPDATE'), so SO creation threw 'Item not found' ->
no SO -> no portal -> no fulfillment. Now maps slug -> erpnext_item (falls back
to COMPLIANCE-SERVICE). DOT ERPNext items were also missing — created them.

Notification: show Subtotal / Discount / Card surcharge / Total so totals like
$35.54 (= $34.50 + $1.04 surcharge) are transparent instead of looking wrong.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 20:01:29 -05:00
justin
f4230e1cb1 intake email: DOT services now send a customer intake-form link (auto)
Federal DOT services (MCS-150, BOC-3, UCR, authority, D&A, audit, full-compliance,
reactivation, ETA, closeout) now have customer intake pages, so they get an
intake-form link like FCC services instead of the old 'admin-assisted / we're
working on it' message. Only form-less state-level filings stay admin-assisted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 19:54:43 -05:00
justin
f9c4f6783b fix Telegram order alert: aggregate whole batch (total + all services)
Was reading only updated.rows[0] -> reported a single line item's net as the
'Total' and showed just one service for multi-service batches (e.g. Paul Wilson's
3-service $218 PayPal batch showed as 'mcs150-update $34.50'). Now sums
service_fee - discount + surcharge + gov_fee across all rows and lists every
service.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 19:46:35 -05:00
justin
dcd9fb61d0 migration 083: use CREATE INDEX CONCURRENTLY to avoid locking fmcsa_carriers
The original CREATE INDEX (non-concurrent) on a 2M-row table held a SHARE lock
for ~33 minutes, blocking all 25+ DOT checker queries and causing 'Failed to
fetch' for real users. CONCURRENTLY builds the index without a table lock.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 10:42:47 -05:00
justin
13492af732 dot-lookup: fix hanging FMCSA fetch with AbortController (not AbortSignal.timeout)
AbortSignal.timeout() requires Node 17.3+. The API container likely runs an
older Node version, so timeouts never fired -> fetch hung forever when FMCSA
API is down -> nginx proxy timeout -> 'Failed to fetch' in the browser.

Fix: use AbortController + manual setTimeout() which works on all Node versions.
All 3 external fetch points (fmcsaFetch x2, SOS x2) now actually abort at 5s.

Also: guard final res.json() with !res.headersSent so the 12s deadline fallback
and the normal response path can't double-send.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 10:36:28 -05:00
justin
cebc432af8 dot-lookup: add 12s hard deadline + reduce FMCSA timeout to 5s
If FMCSA live API is slow (can take 2x 10s = 20s when down), the route would
hang until nginx proxy killed the connection -> 'Failed to fetch'. Now:
- fmcsaFetch timeout: 10s -> 5s (two calls max 10s total)
- SOS entity-status timeout: already reduced to 5s
- 12s hard deadline: if any live API hangs past 12s, immediately return
  census-only data with a 'partial=true' flag so the user gets something

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 10:34:04 -05:00
justin
e7afc3002e dot-lookup: revert fmcsaFetch wrapping (already null-safe), keep 5s SOS timeout
The bare catch{} introduced a control-flow issue. fmcsaFetch() already returns
null on all errors and never throws, so the try/catch wrapping was unnecessary.
Keep only the SOS timeout reduction (20s->5s) as the actual fix for the nginx
proxy timeout that caused 'Failed to fetch' on slow DOT lookups.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 10:28:59 -05:00
justin
e0be6468d4 dot-lookup: fix 'Failed to fetch' caused by SOS timeout exceeding nginx proxy limit
Two WORKER_URL/entity-status calls both had 20s timeouts; worst case 40s total
response time exceeds nginx proxy_read_timeout, dropping the connection and
causing the browser to show 'Failed to fetch'. Also wraps fmcsaFetch calls
explicitly so FMCSA API failure still returns full local census data.

- AbortSignal.timeout(20000) -> 5000 on both SOS entity-status calls
- fmcsaFetch carrier + authority calls wrapped in individual try/catch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 10:19:10 -05:00
justin
0b7a35a58e trucking campaigns: daily builder + MX verifier concurrency + tracking column
- build_trucking_campaigns.py: nightly script that creates 8 Listmonk campaigns
  per day (4 TZ x 2 types: MCS-150 overdue 2k/TZ, inactive USDOT 1k/TZ)
  at 4AM ET / 5AM ET (CT) / 6AM ET (MT) / 7AM ET (PT). Deduplicates via
  listmonk_sent_at column.
- migration 083: add listmonk_sent_at + listmonk_campaign_type to fmcsa_carriers
- email_verifier.py: bump max_workers from 5 to 20 for 4x faster throughput
- cron: daily pw-trucking-campaigns at 08:00 UTC (3 AM EST)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 10:07:44 -05:00
justin
c98f48848c entity dissolution: $49 add-on to wrap-up + state fees (was $199)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 01:13:19 -05:00
justin
b25d1f5fd3 trucking wrap-up: close-out becomes a paid order + workflow
- Checker closing mode now pitches a done-for-you 'Trucking Wrap-Up' ($199)
  with a buy button to /order/dot-compliance?services=carrier-closeout, instead
  of a lead form. DIY checklist replaced by what's-included list.
- Entity dissolution offered as a paid add-on with the lawsuits/liens/judgments
  warning before dissolving.
- New catalog services: carrier-closeout ($199), entity-dissolution ($199).
- CarrierCloseoutHandler orchestrates the sequential shutdown workflow
  (final MCS-150 out-of-business, MC revoke, UCR cancel, IFTA/IRP + state
  closures; dissolution branch for the add-on) as admin-tracked tasks.
- Sell-your-trucks: single shared form with quick-cash / marketplace / both;
  name field is now a real first+last name (no corp-name prefill).
- tickets categories: add truck_sale_both, drop business_closeout (now an order).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 01:01:02 -05:00
justin
1e34707258 migration 082: widen tickets.category CHECK for lead-capture categories
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 00:50:32 -05:00
justin
0409167e5a tickets API: allow insurance_lead + business_closeout + truck_sale categories
These lead-capture categories were posted by the DOT checker but missing from
VALID_CATEGORIES, so the API rejected them with 400 (insurance_lead too — it
was referenced in the Telegram code but never allowlisted).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 00:49:18 -05:00