Commit graph

185 commits

Author SHA1 Message Date
justin
618fafe1d5 order: payment-first express checkout + fix dead Tawk chat widget
Conversion fix for the checkout drop-off (54 sessions reached an /order/ page
over 3 days, 0 advanced to payment). Root cause was friction, not a bug: every
order page dropped a cold email-click straight into a 28-field intake Wizard
before showing any payment option.

- New ExpressCheckout.astro: payment-first entry. Shows price + the minimal
  fields the API needs (prefilled from public records: ?dot= FMCSA census for
  trucking, ?npi= NPPES lookup for healthcare) + Continue to payment. Creates a
  single-service batch-of-one (POST /compliance-orders/batch, which does NOT
  gate Stripe on intake_data_validated) then create-session -> Stripe. Full
  intake is collected AFTER payment via the per-service 'Complete Your Intake
  Form' email the webhook already sends (links to /order/<slug>?order=CO-xxx,
  which re-enters the Wizard in paid-intake mode).

- New OrderFlow.astro: single source of truth replacing ~50 near-identical thin
  Wizard wrappers. Trucking + healthcare default to payment-first (express on
  top, marketing hero moved BELOW the CTA). Telecom + corporate keep Wizard-first
  (rich pre-payment FCC/499 intake, no public-records prefill). Paid-intake
  re-entry (?order=/?token=) always renders the full Wizard.

- Rewrote all 50 /order/*.astro pages to use OrderFlow (foreign-qualification
  keeps its multi-state toggle via slotted content).

- Fixed the dead Tawk.to live-chat widget site-wide: the snippet set an invalid
  crossorigin='*' attribute, forcing the browser into anonymous CORS mode and
  blocking the script (0 chat requests fired anywhere). Removed it to match
  Tawk's official snippet (footer partial + 73 static public/*.html files).

Verified: build clean; express on top with hero below; ?dot=/?npi= prefill;
paid-intake re-entry swaps to Wizard; telecom stays wizard-first; batch-of-one
-> live Stripe URL; both POST endpoints allow the prod origin via CORS.
2026-06-25 11:32:48 -05:00
justin
9a9b0b9130 Add Ontario alongside BC on CRTC page
- New 'Choose your province: BC or Ontario' comparison card (entity name,
  registered office city, fees, annual return, portal, area codes, corp tax)
  inserted above the carriers banner. Previously Ontario was only mentioned
  in a buried FAQ; BC outnumbered ON 53:12.
- Tax-comparison H2 + collapse-menu label now read 'British Columbia / Ontario'
  and the key-takeaway notes ON is ~12.2% (within ~1pt of BC).
- Made hero chip, 'what we deliver' (registered office + file corporation),
  and banking copy province-aware (BC or Ontario) instead of BC-only.
- Verified headless: province card renders, H2 visible (not auto-collapsed),
  13 accordions + proof expander intact, 28 Ontario mentions, no new JS errors.
2026-06-20 06:40:18 -05:00
justin
b1629160d5 crtc collapse: skip card-wrapped headings whose body isn't a sibling (fixes empty US-wholesale + banking expanders; vendor directory left inline) 2026-06-20 01:30:26 -05:00
justin
345c22e561 crtc page: add 'is this real?' proof expander (public RMD/499 stats + named CA operators, defamation-safe); keep standalone expanders out of auto-collapse 2026-06-20 01:12:10 -05:00
justin
0562fd2bd3 crtc page: add 5-min-read TL;DR + collapsible deep-detail sections (non-destructive JS/CSS enhancement) 2026-06-20 01:08:41 -05:00
justin
f481a1d13c analytics: filter email-scanner / headless traffic out of Umami stats
Email security gateways (Microsoft Defender Safe Links / ATP, Proofpoint,
Mimecast, Barracuda, etc.) auto-fetch and often render every link in a
campaign email to scan for malware. The advanced ones drive a real headless
browser, execute JS, and fire Umami pageviews/clicks that masquerade as human
visits -- inflating campaign click-through.

New site/public/js/pw-bot-filter.js queries multiple real-browser signals and
gates Umami via its official data-before-send hook (umamiBeforeSend), dropping
all events when the visitor is a bot. Signals (from empirical chromium probing):
  decisive: navigator.webdriver, HeadlessChrome UA, known scanner UAs, zero/
            collapsed screen|viewport|outer geometry, window LARGER than the
            physical screen (impossible on real HW; uses outerW/H so page zoom
            does not false-positive), software GPU rasterizer (SwiftShader/
            llvmpipe/swrast via WebGL UNMASKED_RENDERER), zero logical CPUs.
  soft (>=2 to trip): tiny screen, inner>screen, low color depth, empty
            navigator.languages, no input device (no fine/coarse pointer + no
            hover + 0 touch), no WebGL on a desktop UA.
Designed to FAIL OPEN: only strong/corroborated evidence suppresses, so real
visitors (incl. zoomed, privacy-tooled, remote-desktop, kiosk) still count.

Wired before the Umami tag in Base.astro (Astro pages) and all 86 static
public/**/*.html pages; both load with defer so order is guaranteed and the
hook is defined before Umami reads it.

Tested end-to-end with chromium (site/tests/bot-filter.test.sh, 4/4):
default headless-new, spoofed-Windows-UA + normal 1366x768 window, and
spoofed-UA + 1x1 window are all caught; hook returns null to drop the event.
2026-06-18 02:02:34 -05:00
justin
c36ef07310 crtc site: defensible framing + 'who this is for' compliance posture
Reduce evasion optics that would draw FCC enforcement attention while keeping the
real value props:
- 'What they avoid by being Canadian' -> 'What the Canadian structure changes'
- Drop 'No US telecom taxes on invoices (15-40% saved)' -> Canadian tax treatment
  on the Canadian entity's billing; 'No US FCC regulatory fees on the Canadian entity'
- '...avoid this by routing US traffic...' -> '...instead route US traffic through
  US intermediaries who carry the 499-A obligation...'
- Add prominent 'Who this is for - and who it isn't' section: legitimate
  conversational voice (UCaaS/PBX/business/residential/live-agent) yes;
  short-duration/dialer/robocall-evasion no. States upstreams are fully
  STIR/SHAKEN compliant and we don't onboard traffic designed to evade
  caller-ID auth; notes Canadian carriers police ASR/ACD more strictly than
  anywhere (a feature). HTML validated balanced.
2026-06-18 00:22:58 -05:00
justin
e379e2b10f CRTC: ERPNext as portal source of truth + harden discount expiry + carrier guide PDF
- checkout.ts: generalize ensureCompliancePortalUser -> ensurePortalUser and
  call it in the CRTC post-payment path so PayPal/crypto/webhook-confirmed CRTC
  orders always get an ERPNext Customer + Website User (the single source of
  truth for portal login/password), matching the compliance fix from the
  PayPal incident. Also flip portal_user_created for canada_crtc/formation.
- canada-crtc.ts: enforce discount active+start/expiry windows, global usage
  limit and applies_to scope server-side at checkout (was active-only), so a
  promo like CANADA200 actually stops working after its expiry.
- scripts/generate_canada_carrier_guide_pdf.py: render the public Canadian
  wholesale carrier/vendor guide PDF (reuses the canonical VENDORS list) to
  site/public/guides/canada-carrier-guide.pdf for the CRTC campaign lead magnet.
2026-06-17 23:34:13 -05:00
justin
c46efe5730 feat(sc-coc): SC intrastate Certificate of Compliance flow (insurance gate -> $25 fee -> file)
Routes SC intrastate-authority orders to the real SCDMV COC product instead of a
PSC certificate (which doesn't apply to property carriers):

  - sc_coc_filing.py: emails the carrier a one-click yes/no — does your insurer
    have / can they file a Form E (SC intrastate liability, $750k or $300k by
    GVWR) with SCDMV? Records the answer; builds the filled COC package.
  - state_trucking._handle_sc_coc_gate: SC intrastate gate —
      no answer  -> email the question once, HOLD
      answered no -> broker referral opened, HOLD (ops todo)
      answered yes-> proceed to bill the exact $25 SCDMV COC fee (at cost) + file
  - API POST /compliance-orders/:id/sc-insurance: records yes/no in intake_data
    (no schema change); NO opens an insurance_lead broker-referral ticket +
    Telegram; YES re-dispatches the worker to bill the $25 + file.
  - site/order/sc-insurance: customer one-click yes/no page (auto-submits when
    the email links straight to ?have=yes|no).

Non-SC intrastate still uses the PSC/PUC email path or a manual todo.
2026-06-16 09:15:55 -05:00
justin
1d6693adb9 govfee: itemize the estimate in the email + add a 'fix my fee' dispute path
The gov-fee email now lists exactly what the amount covers (full breakdown) so
the customer can check it for accuracy, with two clear actions: a  pay link and
a  'something looks wrong' link to /order/dispute.

New /order/dispute page shows the fee breakdown and lets the customer describe
what's wrong; it opens an 'issue' support ticket pre-tagged with the order
(amount + label + their note) via /api/v1/tickets, so ops corrects the fee
before any payment is taken. The /order/pay page also shows the itemized
breakdown and a dispute link.
2026-06-16 05:00:31 -05:00
justin
861f2fbfd4 feat(govfee): auto-quote + collect state fees for at-cost trucking services
At-cost services (IRP/IFTA/intrastate) only collected our service fee at
checkout; the variable state fee was never billed, so orders stalled at
authorization_signed and the filing card would have had to front large IRP fees.

New end-to-end, hands-off flow (you only approve the final filing):
  1. After authorization is signed, state_trucking auto-estimates the gov fee
     from intake (base/op states, power units, weight) via gov_fee.estimate_gov_fee.
  2. Creates a CHILD compliance order (CG-..., service_fee=0, gov_fee=estimate,
     parent_order_number set, migration 099) that flows through the EXISTING
     checkout/payment/webhook machinery.
  3. Emails the customer a payment link to /order/pay (new self-contained page)
     showing every method with correct surcharges — ACH 0% (Stripe 0.8%/ cap
     absorbed, no GoCardless needed), card/PayPal 3%, Klarna 6%, crypto 0%.
  4. Order holds at awaiting_government_fee_approval until paid.
  5. On payment, handlePaymentComplete detects the child (parent_order_number)
     and re-dispatches the PARENT with gov_fee_paid=true, which proceeds to
     prepare + queue the filing and stops at ready_to_file for your approval.

IRP fees are estimates billed at cost (refund overage / rebill shortfall); IFTA
decals + most intrastate fees are near-exact. Tunable via env.
2026-06-16 04:35:45 -05:00
justin
bf69960e8c admin: mark-filed action to advance manual/admin-assisted orders to completed
Admin-assisted services (UCR, MC authority, etc.) have no automated submission,
so approving them only flips to authorization_signed and then sits there -- there
was no way to advance to completed. Add POST /mark-filed (filed_waiting_state |
completed, optional confirmation #, transactional + audit-logged) and drawer
buttons 'Mark as filed (waiting on agency)' / 'Mark completed' shown for orders in
authorization_signed / ready_to_file / filed_waiting_state. Confirmation number
is recorded into intake_data.filing_status.manual_confirmation.
2026-06-16 03:12:57 -05:00
justin
326aee7714 admin: inline filing screenshots + atomic approve transaction
- Documents now flag is_image and the drawer renders screenshots / confirmation
  images as inline clickable thumbnails (click to open full size); PDFs keep the
  View link. Evidence keys are labeled (Filing confirmation screenshot, etc.),
  the worker-temp screenshot_path (not a MinIO key) is dropped in favor of the
  durable evidence copy, and non-file evidence (fax_log_id) is skipped.
- Wrap approve's status-update + audit-insert in a transaction so a failure can
  no longer leave an order out of ready_to_file without dispatching (the earlier
  audit CHECK violation did exactly that to Paul's UCR; it has been reset).
2026-06-16 02:57:24 -05:00
justin
8e1e2f16bf admin docs: only list objects that actually exist (drop dead/phantom rows)
Filter the documents list to objects that exist in storage, so stray keys (a
template pdf_minio_path, or a phantom mcs150 esign_records row on a UCR order
from the shared remediation pipeline) no longer surface as dead rows. The UI
drops the now-unreachable 'not generated yet' branch.
2026-06-16 02:37:33 -05:00
justin
c8e0065729 admin docs: hide phantom prepared-filing PDF for non-form services
The dot-compliance-remediation pipeline seeds filing_status.pdf_minio_path on
every order in a batch, but only MCS-150-producing slugs (mcs150-update,
dot-registration, usdot-reactivation, dot-full-compliance) ever generate it.
For admin-assisted services like UCR it was a phantom 'Prepared filing PDF /
not generated yet' row. Gate the prepared-filing artifacts on FORM_PRODUCING_SLUGS
(mirrors the worker's MCS150_FORM_SLUGS) and give the empty state a clearer
explanation.
2026-06-16 02:35:29 -05:00
justin
d18de006d8 admin approve: block filing when intake incomplete (force override + warning)
Paul Wilson's UCR (CO-FE07212A) sat at fulfillment_status=ready_to_file with
intake_data_validated=false, so the Approve & File button would have dispatched
it for government submission with incomplete intake and no document to review.

Backend: /approve now refuses an order whose intake_data_validated is false
unless {force:true} is passed (409 code=intake_incomplete); the override is
recorded in order_audit_log. The fulfillment_status=ready_to_file requirement
is unchanged, so awaiting_intake orders (e.g. Mitchell's MCS-150s) still 409.

UI: the drawer shows an amber 'intake not complete' warning above the approve
button, and approving an intake-incomplete order triggers an explicit
override confirmation before sending force=true.
2026-06-16 00:33:22 -05:00
justin
1f3b36b29e admin docs: verify object existence, mark dead links, cleaner 404
The DB can record a pdf_minio_path before the object is uploaded (e.g. a
prepared-filing path written for an order whose prep never completed -- Paul
Wilson / Mark Adams MCS-150s). The documents list now HEAD-checks each key and
returns an exists flag; the UI shows 'not generated yet' instead of a dead View
button, and the stream endpoint returns a clean 404 for a missing object.
2026-06-16 00:22:35 -05:00
justin
bce5db4a09 admin: view order PDFs from MinIO (signed forms, prepared filings, evidence)
Adds a Documents section to the compliance-order detail drawer so you can
review the actual filing PDFs before approving an order:
  GET /api/v1/admin/compliance-orders/:id/documents  list viewable objects
  GET /api/v1/admin/compliance-orders/:id/document?key=&token=  stream one

Key discovery pulls from esign_records (unsigned + signed docs per order),
intake_data.filing_status (pdf_minio_path, attested_pdf, evidence/*), and the
order's engagement_letter / rmd_packet columns.

Rather than hand out presigned URLs (MinIO's public host is IP-allowlisted to a
few office IPs, so links break elsewhere), the API streams the object through
itself from internal minio:9000, gated by the admin JWT. The stream endpoint
accepts the token via ?token= (new middleware requireAdminQueryOrHeader) so a
PDF opens in a new tab, and refuses any key that isn't one of the order's own
documents.
2026-06-16 00:20:15 -05:00
justin
2296566e85 admin: compliance-orders dashboard (view, approve-to-file, re-arm intake)
The admin SPA only managed formation_orders; compliance service orders
(telecom/DOT/healthcare) had no admin surface, so you couldn't see what was
paid, what was stuck on intake, or approve a prepared filing for submission.

API (api/src/routes/admin.ts), all requireAdmin:
  GET  /api/v1/admin/compliance-orders            list, grouped by batch, filters
  GET  /api/v1/admin/compliance-orders/stats      queue overview counts
  GET  /api/v1/admin/compliance-orders/:id        full detail + audit log
  POST /api/v1/admin/compliance-orders/:id/approve       approve ready_to_file + dispatch worker
  POST /api/v1/admin/compliance-orders/:id/rearm-intake  clear reminder stamp so daily nudge resumes

UI: new static page /admin/compliance-orders/ (self-contained, CSP-safe inline
CSS, no external JS framework) reusing the existing pw_admin_token session.
Cards group multi-service batches, flag paid+intake-incomplete in red, show
reminder counts, and expose Approve & Re-arm buttons. Linked from the main
/admin top bar. Every approve/re-arm writes an order_audit_log entry.
2026-06-15 23:57:05 -05:00
justin
0dc208ef65 hc: version signature filename (v2) to defeat email/CDN image caching 2026-06-13 21:13:36 -05:00
justin
9d78783258 hc: resize signature to 300x81 (cropped+optimized, 106KB->22KB) for email 2026-06-13 21:12:11 -05:00
justin
84a521d388 hc: use higher-quality Justin Hannah signature in personal email variant 2026-06-13 21:11:45 -05:00
justin
c7c83499d7 hc: personal founder-guarantee revalidation variant (photo + signature)
Adds hc_revalidation_personal.html: the turnover safety-net email plus a
personal guarantee card from Justin Hannah -- round headshot (links to /about so
readers can confirm a real person stands behind it), an italic satisfaction-
guarantee quote ('I will personally make it right... that is my promise'), a
rendered 'Justin Hannah' signature (Dancing Script, SIL OFL), and his title
(Founder & Principal Consultant). Signature image generated via PIL and added to
site/public/images/justin-signature.png. Test sent to justin@.
2026-06-13 21:06:30 -05:00
justin
a04146da2b crtc: remove Canadian accountant/accounting-setup service (no longer offered)
We no longer offer Canadian accountant/accounting setup. Removed all
service-offering content:
- Marketing page (services/telecom/canada-crtc): the 'Set Up Canadian
  Accounting (we help)' next-steps card, the '3 hours of complimentary
  accounting consultation' deliverable bullet, and the whole 'Accounting
  Support' section (assigned accountant, portal chat, $75/hr, 3 complimentary
  hours).
- Order page (order/canada-crtc): the '3 hrs Canadian accounting support'
  included-feature bullet and the 'Preferred accounting software'
  (Xero/QuickBooks) form field + its accounting-hours helper text.
- Fulfillment (canada_crtc.py): dropped the bank-setup email line offering
  '3 hours of Canadian accounting consultation'.

Kept factual GST/HST tax advisories and the bank's QuickBooks/Xero
transaction-sync feature (third-party bank capability, not our service).
2026-06-10 16:51:33 -05:00
justin
b5b2e6e6c3 site: add DEXIT corporate-services page + readiness assessment + cited filings
New page /services/corporate/dexit-reincorporation (matches CRTC service-page
structure): explains DEXIT, the DE franchise-tax dollar driver (real Oracle Health
proxy: $23,600 -> ~$1,000), NV/TX/FL destination guidance, 6-step how-it-works,
3 cited real SEC reincorporation filings (Oracle Health, FG Financial, LogicMark)
with verbatim quotes + EDGAR links, honesty callout, and a lead-gen CTA ('Get my
DEXIT estimate' -> /contact?topic=dexit, NOT a buy-now checkout). Linked from the
corporate services index (new card) + the global Services dropdown across the site.

docs/dexit-cited-filings.md: the filing excerpts + verified gov/statute links.
docs/dexit-readiness-assessment.md: HONEST e2e readiness -- new NV/TX formation is
built (checkout order_type=formation -> formation_orders -> ERPNext SO ->
formation_worker -> TX/NV adapters) but unverified e2e; the 'move a company'
(conversion/domestication) flow + corporate annual-report automation are NOT built;
EIN is kept on a conversion (our ein_worker does NEW EINs only). Page stays lead-gen
until the generic entity-conversion SKU + admin-assisted handler are built+tested.
2026-06-09 07:35:12 -05:00
justin
90bccfda32 fix(checkout): route dot-new-carrier-bundle on success page + worker pipeline
Follow-on to the trucking new-carrier slug fix:
- success page: add dot-new-carrier-bundle to DOT_SLUGS + NEW_CARRIER_SLUGS so
  the order-confirmation 'what to expect' messaging classifies it as trucking.
- pipeline_orchestrator: the trucking onboarding PIPELINE was keyed under the
  bare 'new-carrier-bundle' slug, which is the TELECOM bundle's slug (also a
  collision at the worker layer). Re-keyed to 'dot-new-carrier-bundle' so a
  trucking bundle never runs the telecom pipeline (and vice versa).
2026-06-08 23:48:56 -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
42c6b9607f home: add healthcare to meta description + hero vertical lists
The homepage meta description (description/og/twitter) and hero paragraph listed
'trucking, telecom, data privacy, TCPA, and corporate' but omitted healthcare,
even though healthcare is now a first-class vertical (5-areas strip, full
service pages, active email program). Added healthcare to both vertical lists
and a healthcare-specifics clause (Medicare revalidation, NPI/NPPES, enrollment,
OIG/SAM) mirroring the existing DOT/FMCSA clause.
2026-06-08 02:58:49 -05:00
justin
978b36ad92 home: replace TCPA with Healthcare in the five-areas strip, Healthcare first
Per priority shift to the healthcare vertical: drop the TCPA card from the
'Five areas of compliance expertise' strip and add Healthcare as the first
card (teal accent, links to /services/healthcare). Still five cards.
2026-06-08 00:27:56 -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
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
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
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
0a40e4874e fix(site): bug audit fixes - dot-compliance cart prices now match SERVICE_META (12); /pricing/bundles wrong category links (4); fcc tool order CTAs (new-carrier->fcc-carrier-registration, canada-crtc); reset-password logo; nav duplicate id (137 pages via sync_nav); +3 DOT services in SERVICE_META; classification IntakeStep type 2026-06-06 22:29:38 -05:00
justin
7399211271 trust/security: DMARC p=reject; MTA-STS cert+HTTPS policy live; cookie consent banner (CSP-safe); /accessibility page; footer legal links (Security/Accessibility) on all pages; scope TrustedSite to /order payment pages only 2026-06-06 21:01:36 -05:00
justin
eea1d02045 trust/security: add RFC 9116 /.well-known/security.txt + /security trust page (verified TLS A+/SOC2/HIPAA-PCI, security practices, vuln disclosure) 2026-06-06 19:34:18 -05:00
justin
a56ee01d96 fix(pricing): Trucking/DOT header was invisible (bg-orange-500 purged from CSS) -> inline orange background 2026-06-06 18:19:54 -05:00
justin
3d4269a053 pricing: add Healthcare section at top (6 services: revalidation $599, enrollment $699, reactivation $449, NPPES $349, OIG/SAM $299/yr, bundle $899/yr) with teal header 2026-06-06 18:08:49 -05:00
justin
6c9956d14c about: real founder headshot (face-detected crop from suit photo) replacing placeholder 2026-06-06 18:07:01 -05:00
justin
9eceb18f3e about: add color - gradient hero (navy->teal) + uppercase pill, teal-fade 'What we do', teal/blue founder section, color-varied approach icons 2026-06-06 16:58:36 -05:00
justin
ea0937f723 about: remove Cherry Voice reference from founder bio (company was sold) 2026-06-06 16:53:39 -05:00
justin
971eb8d9d0 about: rewrite founder bio with Justin Hannah's real background (20+ yrs telecom/VoIP, President of Carrier One since 2001, Asterisk/FreeSWITCH pioneer, payments) from LinkedIn 2026-06-06 16:53:11 -05:00
justin
0b51d400f9 about page: add healthcare to firm story + 'What we do' (Medicare/PECOS/NPI, accuracy-verified filings); add founder bio for Justin Hannah with photo placeholder + LinkedIn link 2026-06-06 16:52:09 -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
af0b1d2306 feat(site): wire TrustStrip site-wide (Base layout); fix homepage trust strip (SecurityHeaders A not A+) + add SOC2/HIPAA-PCI badges 2026-06-06 00:25:59 -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
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
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
1584a6692b Pivot CRTC offering on FCC carrier page for A-Z wholesale carriers
The international (Q6) CRTC block was framed as a generic upsell (Expand
2026-06-02 13:26:39 -05:00