Commit graph

114 commits

Author SHA1 Message Date
justin
0fe8ce53ac intake: guard hoisted step scripts against absent DOM
Astro hoists multiple step scripts from Wizard even when the step is not
rendered on the current order page. Several steps bound DOM elements at load
time with no presence check, causing null addEventListener errors on unrelated
order pages.

Add step-presence guards to the obvious offenders used by the shared wizard:
IccImportStep, Block6CertStep, WirelessStep, BundledServiceStep, CPNIStep,
and LNPARegionStep.
2026-06-02 13:12:40 -05:00
justin
219507ce74 intake: fix hoisted payment-step null error and success nav cleanup
- Guard PaymentStep DOM bindings so its hoisted script no longer throws on
  pages without a payment step.
- Remove the correct wizard nav element on successful intake submit
  (pw-wizard-nav, not the nonexistent pw-wizard-footer).
2026-06-02 13:08:47 -05:00
justin
d0d39ebcbc intake: validate cold ?dot= orders before checkout (fix 422)
The cold-visitor Finish flow created the order then called Stripe Checkout
directly, which is gated on intake_data_validated=true and returned 422
INTAKE_NOT_VALIDATED. Now call POST /compliance-orders/:n/validate between
create and checkout (matching the token-order flow), and surface any missing
required fields to the user instead of a raw HTTP error.
2026-06-02 13:03:14 -05:00
justin
5c2f32c6f2 order: reframe state trucking fees as money recovered + deductible
Add TruckingValueNotice on the 14 state trucking order pages. Each page now
shows, near the price:
  - 'You already owe this' reframe: the tax/fee is a liability the moment the
    carrier operates in the state; we do not create it.
  - Per-program 'how you protect and recover money' bullets (IFTA fuel-tax
    credits/refunds, IRP apportionment vs trip permits, avoided penalties,
    citations, out-of-service, and protected operating revenue).
  - 'Is this a tax write-off?' section: BOTH our service fee AND the state
    tax/fee paid are deductible business expenses (IRC 162), with the exact
    return line per entity type (Schedule C / 1120 / 1120-S / 1065), mirroring
    the telecom TaxDeductibilityNotice pattern. Fuel taxes noted as part of
    fuel cost.
Not tax advice; disclaimer included.
2026-06-02 13:00:44 -05:00
justin
53ae3ef870 intake: cold ?dot= visitors can finish + correct per-state CTA links
- Wizard Finish button: for visitors with no token/order (e.g. arriving via a
  campaign ?dot= link), create the compliance order from collected intake data
  and redirect to Stripe Checkout, instead of silently doing nothing.
- StateTrucking: Operating States no longer required; single-state/intrastate
  carriers can finish (relabeled 'Other Operating States (if any)').
- build_trucking_campaigns: per-state programs (weight_tax/emissions) now derive
  the CTA landing page from the deficiency flag's state suffix (e.g.
  state_weight_tax_OR -> OR), not the carrier base state, so a GA-based carrier
  flagged for OR weight-mile tax links to the OR page (not a mismatched one).
2026-06-02 12:56:03 -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
3322003da0 feat(order-pages): landing pages for all state/hazmat/emissions slugs
Add /order/{irp-registration,ifta-application,ifta-quarterly,or-weight-mile-tax,
ny-hut-registration,ky-kyu-registration,nm-weight-distance,ct-highway-use-fee,
ca-mcp-carb,state-dot-registration,intrastate-authority,osow-permit,
state-trucking-bundle,hazmat-phmsa,state-emissions,usdot-reactivation}.
Each renders the slug-gated state-trucking intake wizard. Site builds 48 pages,
new routes verified to render correct intake sections.
2026-06-02 03:33:23 -05:00
justin
cadff79bd6 test(fulfillment): consistency + intake-completeness checker
Cross-references every DOT/state/hazmat slug across COMPLIANCE_SERVICES,
REQUIRED_FIELDS, SERVICE_META, INTAKE_MANIFEST, and SERVICE_HANDLERS, and
verifies every required field is collectible by its assigned intake steps.
Caught + fixed missing usdot-reactivation SERVICE_META entry. 24/24 pass.
2026-06-02 03:29:53 -05:00
justin
9c6b8d95e0 feat(fulfillment): state-trucking intake form + hazmat/emissions products
- Add StateTruckingIntakeStep.astro with slug-gated sections (IRP/IFTA,
  emissions, intrastate authority, OSOW, hazmat/PHMSA); wired into Wizard
- Register hazmat-phmsa + state-emissions products & SERVICE_INFO
- Add server-side bundle/mutual-exclusion enforcement + REQUIRED_FIELDS
- State-trucking slugs now collect real intake data (were review-only)
- Surface slug-specific intake fields in admin todo (_summarize_intake)
- Remove state slugs from email ADMIN_ASSISTED set (now get intake links)
2026-06-02 03:27:51 -05:00
justin
3d611e97a4 tawk mobile UX: hide widget on small screens to stop text overlay popups
Adds Tawk_API.onLoad mobile guard (max-width 768px -> hideWidget) in shared
footer snippet and current built pages so mobile browsers no longer get the
proactive text bubble covering content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 12:31:01 -05:00
justin
6def0f6186 collect photo ID for all FMCSA filings that legally require the signer's ID
Gap: dot-registration (new USDOT=MCS-150) routed through intake but never asked
for photo ID; usdot-reactivation, emergency-temporary-authority, carrier-closeout
(final MCS-150 + authority revocation), entity-dissolution, and entity-upgrade-
bundle weren't wired to collect it at all.

- intake_manifest: route usdot-reactivation, ETA, carrier-closeout,
  entity-dissolution through the dot-intake step
- DOTIntakeStep DOT_SECTIONS: add dot-sec-photo-id for dot-registration and all
  the above (operations + photo ID)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 18:00:59 -05:00
justin
479f3dfc45 add entity upgrade bundle service + deploy completion/IMAP crons
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 22:12:11 -05:00
justin
e2313bcc5e add blur detection + Ollama ID validation + corporate check for all carriers
- Client-side: Laplacian variance blur detection in photo quality check
  (very blurry / somewhat blurry / acceptable / good)
- Server-side: async Ollama vision model validates uploaded image is a
  real government ID (minicpm-v:8b), flags non-ID uploads
- Corporate check: sole proprietors now get yellow 'form an LLC' upsell,
  formal entities get annual report/RA reminder
2026-05-30 19:17:31 -05:00
justin
ca7af40ceb update photo ID instructions: suggest QR code first for desktop users 2026-05-30 19:07:13 -05:00
justin
e2c7cc582b increase font sizes in photo ID section for readability (min 14px) 2026-05-30 18:15:22 -05:00
justin
04861ccfe0 Fix QR code: add inline script right at image element for immediate generation
Previous approach relied on the main is:inline script block which
could be blocked by FCC step crashes. New approach: tiny self-contained
script right next to the QR img element, runs immediately, fetches
upload token and generates QR. Falls back to page URL on failure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 17:20:04 -05:00
justin
66bd1306e3 Accept TIFF images for photo ID upload (scanner default format)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 16:28:22 -05:00
justin
d2f9e642d4 Hide QR code on mobile phones, auto-trigger camera on mobile
QR code hidden via CSS on screens < 640px (phones). On mobile,
file input gets capture=environment so tapping the button opens
the camera directly. Tablets still show QR code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 16:25:52 -05:00
justin
1535acb413 Complete phone-to-desktop photo ID upload pipeline
- API: POST /api/v1/id-upload/token generates upload token
- API: POST /api/v1/id-upload/:token receives base64 image, stores in MinIO
- API: GET /api/v1/id-upload/:token/status returns upload status + thumbnail
- Mobile page: sends image as base64 with upload_token
- Desktop intake: requests token, generates QR with upload URL, polls
  every 3s for phone upload, auto-shows thumbnail when detected
- MinIO storage with presigned URLs for thumbnails
- Compliance order intake_data updated with photo_id_uploaded flag

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 16:24:35 -05:00
justin
daf6d1f831 Always show QR code for phone photo upload — no click to reveal
QR code displayed inline below the upload button so truckers can
immediately scan with their phone to take a photo of their ID.
Clear instructions: 'Scan with your phone camera to take a photo'

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 16:17:17 -05:00
justin
73a10d4a0f Simplify photo ID upload: clear instructions for truckers
- Yellow instruction box explains 3 methods in plain English
  (phone photo, computer upload, scanner)
- One big orange "Add Photo of Your ID" button
- Webcam + QR code kept in code but simplified UI
- Accepted formats note

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 16:11:49 -05:00
justin
3423b4914a Relabel photo ID buttons: Upload File/Scan + Use Webcam + scanner instructions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 16:09:47 -05:00
justin
39fb1c9998 Webcam capture for photo ID via getUserMedia
Desktop users can now use their webcam to photograph their ID:
- Click "Use Camera" → browser requests webcam permission
- Live video preview with orange guide rectangle for ID placement
- Capture button takes high-res JPEG (1280x720)
- Cancel button stops webcam and returns to upload options
- Captured image goes through same quality check flow
- Works on Chrome, Firefox, Edge, Safari (desktop + mobile)
- No libraries needed — native WebRTC API

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 16:09:02 -05:00
justin
c40dfb552e Auto-save intake data to server after every step
Intake data now persists to DB after each step completion (non-blocking).
If browser crashes, data is recoverable from compliance_orders.intake_data.

Partial saves (_partial: true) only update intake_data without changing
payment_status or marking intake_data_validated. Final submit still
triggers the full validation + worker dispatch flow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 16:01:20 -05:00
justin
beb23d777e Photo ID quality check after upload
Shows uploaded image at larger size with automated quality checks:
- File size (too small = low quality warning)
- File type validation (JPEG, PNG, PDF, HEIC)
- Resolution check (minimum 400x250 for readable text)
- Aspect ratio check (should look like an ID card)

Green checkmark for passing checks, red X for issues.
Yellow warning box for quality problems with specific guidance.
Accept & Continue button to confirm, Retake to re-upload.
After accept, collapses to small preview with "Change ID" option.

Front of ID only (sufficient for FMCSA MCS-150 filing).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 15:59:27 -05:00
justin
e8769e4d5d Photo ID upload: add QR code for phone + scanner device support
Three upload methods:
- Upload File: standard file picker
- Camera / Scanner: uses capture attribute for camera on mobile
  or TWAIN/WIA scanner devices on desktop
- QR Code: generates QR with current page URL so user can scan
  with phone and take a photo of their ID on mobile

QR generated via api.qrserver.com (no library dependency).
Remove button restores all upload options.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 15:56:00 -05:00
justin
b8a52303b3 DOT intake: use is:inline script to avoid hoisted bundle crash
FCC step scripts crash on DOT pages due to missing elements. By
using is:inline, DOT intake script runs independently, not in the
hoisted bundle. Stripped TypeScript annotations for plain JS compat.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 15:38:04 -05:00
justin
db96af53bf Guard DOT intake script — skip on non-DOT pages
Wraps entire script in element existence check to prevent running
on FCC pages where other step scripts crash from missing elements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 15:35:52 -05:00
justin
989ccaec93 Fix MCS150Step null crash — was breaking ALL DOT intake pages
MCS150Step script compiled into hoisted JS for all order pages.
Non-null assertions on photo ID elements crashed on non-MCS150 pages,
preventing DOTIntakeStep's showRelevantSections from running.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 15:33:56 -05:00
justin
c98ac9ae54 Fix DOT intake crash: null-safe photo ID element refs
Script crashed on 'Cannot read properties of null' because photo ID
elements are inside a hidden section. All element refs now use
optional chaining instead of non-null assertions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 15:31:15 -05:00
justin
d55384975e Fix DOT intake: retry init until wizard found + null-safe PWIntake
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 15:29:42 -05:00
justin
7fe3311921 Fix DOT intake: wait for DOMContentLoaded before showing sections
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 15:27:39 -05:00
justin
897554fa4e Fix DOT intake: hide all sections by default, show on load
Sections were visible by default in HTML. Now all hidden, then
showRelevantSections() runs immediately + on pw:step-shown.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 15:26:41 -05:00
justin
ee4ea70beb Fix DOT intake section visibility — read slug from wizard data attribute
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 15:25:02 -05:00
justin
8ce9e2e118 Unified DOT intake form — one form for all trucking services
Single DOTIntakeStep shows/hides sections based on services ordered:
- Company info + address + signer (always)
- Entity & operations (MCS-150, USDOT, MC Auth, bundles)
- Fleet info (MCS-150, UCR, bundles)
- UCR fleet bracket + base state (UCR)
- Cargo types (MCS-150, bundles)
- D&A program (CDL drivers, DER, consortium)
- BOC-3 docket info (BOC-3)
- Photo ID upload (MCS-150, MC Auth)
- Security/encryption notices

All DOT services now use ["dot-intake", "review"] instead of ["review"].

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 15:23:48 -05:00
justin
b59b266a80 MCS-150 intake: add encryption notice, EIN field, photo ID upload
- Security notice: SSL encryption, encrypted at rest, no third-party sharing
- EIN field added (required for MCS-150 form field 19)
- Photo ID upload with camera capture on mobile
- ID auto-deleted after filing processed
- Preview with remove button

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 13:28:43 -05:00
justin
d2b42cd4b8 Add MCS-150 intake form for biennial update orders
Collects all fields needed for FMCSA Form MCS-150:
- Legal name, DBA, DOT#, MC#
- Principal business address
- Entity type, carrier operation, interstate/intrastate
- Fleet info (power units, drivers, annual miles)
- 29 cargo type checkboxes
- Authorized signer name and title

Filed via fax to FMCSA at 202-366-3477 (VitalPBX).
Previously was review-only with no data collection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 11:29:05 -05:00
justin
78ed1db15a Recalculate bundle pricing + bundle auto-uncheck individual items
- DOT Full Compliance Bundle: $499 → $399 (saves $376 vs $775 individual)
- State Compliance Bundle: $599 → $499 (saves $297 vs $796 individual)
- D&A marked non-discountable (passthrough cost to testing provider)
- Order page: selecting a bundle auto-unchecks its individual components
  via data-bundle attribute listing component slugs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 14:49:59 -05:00
justin
f1027694a3 Add interest field to mailing list subscribe (telecom/trucking/formation)
- Footer subscribe modal: new "I'm interested in" dropdown with 3 options
- Hoisted JS: reads interest field, validates selection, passes to API
- Subscribe API: routes to different Listmonk lists by interest
  (telecom→list 3, trucking→list 8, formation→list 9)
- Interest stored as subscriber attribute for campaign segmentation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 12:50:27 -05:00
justin
33da00fd89 50-state trucking compliance: services, checker, order page, CA landing
- Migration 079: state_trucking_requirements table seeded for all 51 jurisdictions
  (IRP, IFTA, weight-distance taxes, MCP/CARB, intrastate authority, state DOT)
- Migration 080: carrier_operating_states tracking table
- 13 new state trucking services in catalog ($99-$599)
- StateTruckingHandler with state-specific admin todos
- DOT compliance checker: 7 new state-level checks (IRP, IFTA, weight tax,
  MCP/CARB, emissions, intrastate authority, state DOT number)
- New API endpoint: GET /api/v1/dot/state-requirements
- DOT order page: state compliance service cards with auto-preselect
- California trucking landing page (MCP + CARB + IRP + IFTA)
- Fix: DOT checker nav missing Trucking/DOT section
- Fix: All 8 DOT intake pages missing style block (dangling text)
- Fix: DOT confirmation email now says "Order Confirmed" not "Action Required"
- Fix: MCS150/BOC3/StateTrucking handlers missing async process() method
- Fix: StateTruckingHandler connection leak + slug resolution

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 12:46:33 -05:00
justin
c80f3a408a Fix: generic review step text (was FCC/USAC specific)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 00:49:18 -05:00
justin
e67df3c4c3 DOT intake: review-only (no telecom entity step), email verifier,
updated flagger excluding 4+ year stale carriers

- Intake manifest: DOT services use ["review"] only, skipping the
  telecom entity step with FRN/USAC fields
- Flagger: excludes 4+ year overdue carriers from campaign (spam
  trap risk). 18,277 safe targets from 100K records.
- Email verifier: self-hosted MX + SMTP verification tool

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 00:29:28 -05:00
justin
aad45dc827 Add DOT service intake pages and manifest entries
8 new Astro intake pages for DOT services:
mcs150-update, boc3-filing, ucr-registration, dot-registration,
mc-authority, dot-drug-alcohol, dot-audit-prep, dot-full-compliance.

All use entity + review wizard steps (admin-assisted services).
Added to INTAKE_MANIFEST and SERVICE_META with correct pricing.

Fixes 404 on /order/mcs150-update?order=CO-xxx from confirmation emails.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 00:02:47 -05:00
justin
69b70ee96d Add Trucking/DOT to site-wide nav and footer
Nav dropdown: Trucking/DOT section with DOT Compliance Services,
MCS-150/BOC-3/UCR link, and DOT Compliance Check [FREE] badge.
Added to both desktop mega-dropdown and mobile menu.

Footer: DOT/Trucking in services list, DOT Compliance Check in
free tools list.

This updates all Astro-compiled pages site-wide (nav partial).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-28 23:12:27 -05:00
justin
d4c4ae003e Fix 6 bugs from code review
Critical:
- Single-order discount used wrong column names (discount_pct/discount_flat_cents
  → discount_type/discount_value). Discounts were silently $0.
- Single-order discount skipped allowed_emails and expires_at checks
- Free orders now set paid_at = NOW()

High:
- Discount usage now tracked in discount_usage table + current_uses incremented
- Flat discount only replaces bundle when flat >= bundle (was always replacing)

Minor:
- Removed unused CDR profile fetch in EntityStep

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 02:16:38 -05:00
justin
4125b0f09f Auto-fill entity from FRN in order intake_data, not just URL param
When intake loads from ?order=CO-xxx, the FRN is in the order's
intake_data, not the URL. Now checks state.intake_data.frn and
state.entity.frn as fallback sources for auto-fill.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 01:53:44 -05:00
justin
03c72a2525 Pre-fill intake form from order data via ?order=CO-xxx
When the intake page is loaded with ?order=CO-xxx (from the
confirmation email), fetch the order and pre-fill customer name,
email, and FRN from the order record. Previously only worked
with JWT token-based links.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 01:51:12 -05:00
justin
2316168072 Clarify email field is not shared with FCC
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 01:49:53 -05:00