Commit graph

108 commits

Author SHA1 Message Date
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
justin
2f635227aa improve name-check: match base name without entity suffix (LLC/Inc/Corp), flag close matches 2026-05-30 23:07:52 -05:00
justin
e0ba8acc90 add pipeline orchestrator, mailbox 1583 flow, EIN + virtual-mailbox services
- Pipeline orchestrator: chains sequential fulfillment for new carrier bundles
  (formation → EIN → USDOT → MC → BOC-3 → MCS-150 → D&A → UCR)
- Mailbox setup: Anytime Mailbox provisioning with USPS 1583 e-sign + online notarization
- New services: ein-application ($79), virtual-mailbox ($149/yr)
- Registered all new handlers in SERVICE_HANDLERS
- Pipeline cron: every 5 minutes
2026-05-30 22:56:54 -05:00
justin
90a0f983ee add order timeline API: step names + estimated business-day completion dates per service
GET /api/v1/order-timeline/:order_id — returns steps with dates:
- Skips weekends + US federal holidays (matches business_days.py)
- Per-service timelines: MCS-150 (2 days), BOC-3 (3 days), MC Auth (15 days), etc
- Entity upgrade bundle shows full sequential pipeline (10 business days)
- ETA emergency shows same-day processing
2026-05-30 22:48:44 -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
ad3d189b2b post-completion flow: survey, referral program, review ask
- Migration 081: referral_codes, referral_uses, exit_surveys tables
- API: POST /api/v1/survey, POST /api/v1/referral/check, GET /api/v1/referral/:email
- Worker: completion_emails.py — sends completion + 24h follow-up (survey + referral)
- Survey page: /survey/?order=X&rating=N — star rating, feedback, Google review ask
- Referral: REF-FIRSTNAME codes, $25 credit per referred order, no limit
- Low ratings (1-3 stars) trigger Telegram alert for admin follow-up
- Cron: every 15 minutes
2026-05-30 21:22:14 -05:00
justin
08e80e11f9 add POST /api/v1/dot/name-check: parallel SOS + FMCSA name availability check
- Checks state SOS for entity name availability (via workers adapter)
- Checks FMCSA census for exact + fuzzy name matches
- Returns: sos_available, fmcsa_in_use, fmcsa_matches, combined 'available' flag
- 20s timeout on SOS lookup, FMCSA query is instant (local DB)
2026-05-30 21:02:32 -05:00
justin
58aa2cf78e map all FMCSA status codes to services: OOS→ETA ($499), Inactive→reactivation, Revoked/Suspended→reinstatement
- OOS (Out of Service): RED, urgent messaging, maps to Emergency Temporary Authority $499
- Inactive: YELLOW, reactivation $149
- Not Authorized: YELLOW, new USDOT + MC authority
- Revoked: RED, new authority application
- Suspended: YELLOW, compliance resolution + reinstatement
- Cancelled: YELLOW, new registration
- Each status has specific actionable messaging with PW CTA
2026-05-30 20:48:47 -05:00
justin
b106a88e90 add USDOT reactivation service ($149) + map inactive status to reactivation CTA
- New service: usdot-reactivation — filed via ask.fmcsa.dot.gov (sub-cat 302)
- Inactive carriers see 'PW can handle reactivation, no Login.gov needed'
- Compliance checker maps inactive operating status to reactivation service
2026-05-30 20:40:56 -05:00
justin
2b8e2cf7ea fix: inactive carriers show yellow warning instead of green, even if FMCSA says 'allowed' 2026-05-30 20:39:28 -05:00
justin
18fb45dba0 fix operating status: translate FMCSA status codes to human-readable labels (A=Active, I=Inactive, etc) 2026-05-30 20:38:22 -05:00
justin
a6c7d2aef8 increase SOS lookup timeout to 20s for slower state portals 2026-05-30 19:23:41 -05:00
justin
e8a98b1130 corporate check: live SOS lookup via state adapters for all 51 states
- Formal entities: queries workers /entity-status endpoint for real-time
  Secretary of State status (ACTIVE/dissolved/revoked/delinquent)
- Green if active, red if not active, yellow if not found or lookup failed
- Sole proprietors: yellow 'form an LLC' upsell
- 12s timeout so compliance check doesn't hang on slow state portals
2026-05-30 19:21:06 -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
e40f359693 fix photo upload: add synchronous /jobs/presign and /jobs/minio-upload endpoints to workers 2026-05-30 19:13:51 -05:00
justin
592b9875da fix TS build: use rawDot not dotNumber in pending filing query 2026-05-30 19:03:15 -05:00
justin
ffc5a16b5c add corporate compliance check (#15) to DOT checker + annual report/RA/reinstatement services
- Check 15: detects LLC/Inc/Corp/LTD/LP in entity name, shows yellow warning
  about annual reports, franchise tax, and registered agent requirements
- New services: annual-report-filing (49), registered-agent (9/yr),
  entity-reinstatement (99)
- Upsell opportunity: 1.31M formal entities in FMCSA database
2026-05-30 19:02:37 -05:00
justin
1f1113d63c add fax filing pipeline: VitalPBX sender, attestation cover page with digital signature, compliance checker pending filing override
- filing_attestation.py: generates cover page attesting PW submitted document
  to recipient with date/time stamp, contact info, and digital signature
- fax_sender.py: sends PDFs via VitalPBX API, polls for delivery, generates
  attested copy for customer records
- dot-lookup.ts: if DOT has pending MCS-150 order, show green 'UPDATE SUBMITTED'
  instead of red 'OVERDUE' in compliance checker
- requirements.txt: add pyhanko + cryptography for PDF digital signatures
2026-05-30 18:32:01 -05:00
justin
e0fc4810d1 show proper error on phone when photo ID upload fails to MinIO 2026-05-30 18:13:18 -05:00
justin
7ef509c247 fix photo ID upload: use workers for MinIO storage + public presigned URLs
- id-upload.ts: replace broken direct minio import with workers presign/upload
- job_server.py: add minio-upload handler for API to store files via workers
- rewrite presigned URLs from internal minio:9000 to public minio.performancewest.net
- fixes: thumbnail not showing after phone upload, base64 fallback storage
2026-05-30 18:12:06 -05:00
justin
f60c5229ab Fix mobile photo upload: resize large camera images + increase body limit
Mobile cameras produce 8-12MB photos. Now:
- Canvas-based resize to max 2000x1500 before upload
- JPEG compression at 0.7-0.85 quality
- Express body limit increased to 5MB for id-upload route
- Falls back to raw upload for small images and PDFs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 17:24:16 -05:00
justin
6b874ea72b Add ts-nocheck to id-upload.ts (minio optional dep)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 16:42:41 -05:00
justin
0ba8730487 Add Telegram notifications for tickets, quotes, and insurance leads
Tickets: 📩 for support, 🏥 for insurance leads, 💰 for quotes
Quotes: 💰 with name, email, company, service, details
All fire-and-forget to Telegram bot — non-blocking.

Previously these only went to ERPNext with no real-time alert.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 16:35:11 -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
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
6f3ad1b686 Customer dashboard: order tracking + portal login
- New page: /portal/dashboard/ — customer can view all orders
- Auth: cookie-based login, shows auth modal if not logged in
- Orders grouped by batch, filtered by DOT/FCC tabs
- Shows service name, amount, discount, status badge, payment method
- Portal API: /api/v1/portal/me now returns compliance_orders

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 14:20:58 -05:00
justin
e82aa0b8c2 Fix PayPal capture for compliance orders + MCS-150 form generator
PayPal capture was defaulting to canada_crtc_orders table for all
non-formation orders. Now properly routes compliance_batch orders
to compliance_orders table with batch_id lookup. Also infers
order type from ID prefix (CB-=batch, CO-=compliance, FO-=formation).

MCS-150 form generator: produces DOCX with fax cover sheet + filled
MCS-150 form for faxing to FMCSA at 202-366-3477.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 11:34:16 -05:00
justin
c47c52e9e8 Remove external action_url links from compliance checker
Don't send users to FMCSA portal or state agency sites — keep them
on our site to order services through us. Removed all action_url
from API responses and "Fix this" / "Learn more" links from frontend.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 16:16:44 -05:00
justin
da00817174 Show DOT# or FRN in Telegram order notifications
Reads dot_number or frn from intake_data and includes in the
notification. DOT orders show DOT#, FCC orders show FRN.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 15:55:41 -05:00
justin
9579d74d7d Fix Telegram notification: show actual paid amount after discount
Was showing service_fee_cents ($69) instead of actual charge ($35.54).
Now subtracts discount_cents and adds surcharge_cents. Also shows
discount line in notification when a promo code was used.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 15:51:34 -05:00
justin
4e7493b088 Mark MC Authority as non-discountable ($300 FMCSA gov fee)
Non-discountable services: BOC-3 ($25 vendor), D&A (~$100 provider),
MC Authority ($300 gov fee). All other DOT services are pure labor.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 15:02:31 -05:00
justin
79fb4722f1 Mark BOC-3 as non-discountable (passthrough cost to Process Agent LLC)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 15:00:46 -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
4aff121c0b Fix interstate detection: use carrier_operation code A, not text match
FMCSA census carrier_operation is single-letter: A=Interstate,
B=Intrastate Hazmat, C=Intrastate Non-Hazmat. Previous code searched
for "interstate" in text which never matched. Now 22,089 interstate
carriers will be properly flagged for IRP/IFTA.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 14:01:18 -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