Add FCC Carrier/ISP Registration: migration + order page

Phase 1-2 of the new registration product:
- Migration 075: fcc_carrier_registrations table with full pipeline status,
  service wizard answers, entity choice, pricing, idempotency tracking
- Order page with 5-step wizard:
  1. Service wizard (voice/broadband/wholesale + delivery method + infra needs)
  2. Registration checklist (auto-determined + add-ons with dynamic pricing)
  3. Entity choice (existing FRN search OR new formation with nexus guidance)
  4. Contact & officer info
  5. Review & payment with engagement clickwrap

Still needed: API endpoint, checkout integration, worker pipeline handler.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
justin 2026-04-29 08:39:03 -05:00
parent 94ce14dc17
commit 2312edf5df
2 changed files with 882 additions and 0 deletions

View file

@ -0,0 +1,128 @@
-- 075_fcc_carrier_registration.sql
--
-- FCC Carrier / ISP Registration — dedicated order table with
-- CRTC-style multi-step pipeline. Supports optional formation,
-- CORES/FRN, Form 499, state PUC, and compliance filings.
BEGIN;
CREATE TABLE IF NOT EXISTS fcc_carrier_registrations (
id BIGSERIAL PRIMARY KEY,
order_number TEXT NOT NULL UNIQUE, -- FCR-YYYY-XXXXX
-- Customer
customer_email TEXT NOT NULL,
customer_name TEXT NOT NULL,
customer_phone TEXT,
-- Entity choice
entity_source TEXT NOT NULL CHECK (entity_source IN ('existing', 'new_formation')),
telecom_entity_id INTEGER,
formation_order_number TEXT,
-- Entity info (populated from either source)
entity_legal_name TEXT,
entity_type TEXT,
formation_state CHAR(2),
ein TEXT,
frn TEXT,
filer_id_499 TEXT,
-- Contact / Officer
contact_name TEXT,
contact_email TEXT,
contact_phone TEXT,
contact_title TEXT,
address_street TEXT,
address_city TEXT,
address_state CHAR(2),
address_zip TEXT,
-- Service wizard answers (JSONB for flexibility)
service_wizard JSONB NOT NULL DEFAULT '{}'::jsonb,
-- e.g. { service_types: ["voice","broadband"], voice_delivery: "reseller",
-- needs_lcr: true, broadband_type: "facilities_based", operating_states: 5 }
-- Service configuration (derived from wizard + confirmation)
include_formation BOOLEAN NOT NULL DEFAULT FALSE,
include_dc_agent BOOLEAN NOT NULL DEFAULT TRUE,
include_rmd BOOLEAN NOT NULL DEFAULT FALSE,
include_cpni BOOLEAN NOT NULL DEFAULT FALSE,
include_calea BOOLEAN NOT NULL DEFAULT FALSE,
include_bdc BOOLEAN NOT NULL DEFAULT FALSE,
include_stir_shaken BOOLEAN NOT NULL DEFAULT FALSE,
include_ocn BOOLEAN NOT NULL DEFAULT FALSE,
state_puc_states TEXT[] DEFAULT '{}',
-- Pipeline status
status TEXT NOT NULL DEFAULT 'received' CHECK (status IN (
'received',
'awaiting_formation',
'formation_complete',
'cores_registration',
'form_499_initial',
'state_registrations',
'compliance_filings',
'review',
'delivered',
'cancelled'
)),
-- Pricing (cents)
service_fee_cents INTEGER NOT NULL DEFAULT 129900,
formation_fee_cents INTEGER NOT NULL DEFAULT 0,
state_fee_cents INTEGER NOT NULL DEFAULT 0,
puc_fee_cents INTEGER NOT NULL DEFAULT 0,
addon_fee_cents INTEGER NOT NULL DEFAULT 0,
discount_cents INTEGER NOT NULL DEFAULT 0,
discount_code TEXT,
-- Payment
payment_status TEXT NOT NULL DEFAULT 'pending_payment'
CHECK (payment_status IN ('pending_payment','paid','refunded','cancelled')),
payment_method TEXT,
stripe_session_id TEXT,
paid_at TIMESTAMPTZ,
-- ERPNext
erpnext_sales_order TEXT,
-- Pipeline tracking (idempotency timestamps)
formation_completed_at TIMESTAMPTZ,
cores_completed_at TIMESTAMPTZ,
frn_obtained TEXT,
form_499_completed_at TIMESTAMPTZ,
filer_id_obtained TEXT,
dc_agent_completed_at TIMESTAMPTZ,
state_puc_completed_at TIMESTAMPTZ,
rmd_completed_at TIMESTAMPTZ,
cpni_completed_at TIMESTAMPTZ,
calea_completed_at TIMESTAMPTZ,
bdc_completed_at TIMESTAMPTZ,
stir_shaken_completed_at TIMESTAMPTZ,
ocn_completed_at TIMESTAMPTZ,
-- Engagement
engagement_accepted_at TIMESTAMPTZ,
engagement_accepted_ip TEXT,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_fcr_customer_email ON fcc_carrier_registrations(customer_email);
CREATE INDEX IF NOT EXISTS idx_fcr_status ON fcc_carrier_registrations(status) WHERE status NOT IN ('delivered','cancelled');
CREATE INDEX IF NOT EXISTS idx_fcr_formation ON fcc_carrier_registrations(formation_order_number) WHERE formation_order_number IS NOT NULL;
-- Updated_at trigger
CREATE OR REPLACE FUNCTION set_updated_at_fcr() RETURNS trigger AS $$
BEGIN NEW.updated_at = NOW(); RETURN NEW; END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_fcr_updated_at ON fcc_carrier_registrations;
CREATE TRIGGER trg_fcr_updated_at
BEFORE UPDATE ON fcc_carrier_registrations
FOR EACH ROW EXECUTE FUNCTION set_updated_at_fcr();
COMMIT;

View file

@ -0,0 +1,754 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Register your telecom carrier or ISP with the FCC — CORES/FRN, Form 499, state PUC, RMD, CPNI, CALEA. Optional business formation included.">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<title>FCC Carrier / ISP Registration — Performance West Inc.</title>
<script>
window.__PW_API = (function() {
var h = window.location.hostname;
if (h === "localhost" || h === "127.0.0.1") return "http://" + h + ":3001";
if (h === "dev.performancewest.net") return "https://api.dev.performancewest.net";
return "https://api.performancewest.net";
})();
</script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f9fafb;line-height:1.6}
a{color:#1e3a5f;text-decoration:none}
.wrap{max-width:740px;margin:0 auto;padding:2rem 1.25rem 4rem}
h1{font-size:1.65rem;font-weight:700;color:#111827;margin-bottom:.25rem}
.subtitle{font-size:.9rem;color:#6b7280;margin-bottom:1.5rem}
.card{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:1.25rem;margin-bottom:1rem}
.card h2{font-size:1.05rem;font-weight:700;color:#1e3a5f;margin-bottom:.75rem}
.label{display:block;font-size:.82rem;font-weight:600;color:#374151;margin-bottom:.3rem;margin-top:.6rem}
select,input[type=text],input[type=email],input[type=tel],input[type=number]{width:100%;padding:.5rem .7rem;border:1px solid #d1d5db;border-radius:8px;font-size:.88rem;background:#fff}
select:focus,input:focus{outline:none;border-color:#1e3a5f;box-shadow:0 0 0 2px rgba(30,58,95,.15)}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:.4rem;padding:.65rem 1.5rem;border-radius:8px;font-weight:600;font-size:.92rem;cursor:pointer;border:none;transition:all .15s;font-family:inherit}
.btn-primary{background:#1e3a5f;color:#fff}.btn-primary:hover{background:#162e4d}
.btn-primary:disabled{opacity:.5;cursor:not-allowed}
.btn-back{background:#e5e7eb;color:#374151}
.hidden{display:none}
.step-dots{display:flex;gap:.75rem;margin-bottom:1.5rem}
.step-dot{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.72rem;font-weight:700;background:#e5e7eb;color:#6b7280}
.step-dot.active{background:#1e3a5f;color:#fff}
.step-dot.done{background:#22c55e;color:#fff}
.q-card{padding:.65rem .85rem;border:1.5px solid #d1d5db;border-radius:8px;background:#fff;cursor:pointer;transition:all .15s;text-align:left;font-family:inherit;font-size:.88rem;color:#374151;width:100%;display:block;margin-bottom:.4rem}
.q-card:hover{border-color:#1e3a5f;background:#f0f4f8}
.q-card.selected{border-color:#1e3a5f;background:#eff6ff;color:#1e3a5f;font-weight:600}
.q-card.selected::before{content:"\2713 "}
.q-label{font-weight:600;color:#1f2937;font-size:.95rem;margin-bottom:.3rem}
.q-hint{font-size:.82rem;color:#6b7280;margin-bottom:.6rem}
.info-box{padding:.6rem .85rem;background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px;font-size:.82rem;color:#1e40af;margin:.75rem 0}
.warn-box{padding:.6rem .85rem;background:#fef3c7;border:1px solid #fbbf24;border-radius:8px;font-size:.82rem;color:#92400e;margin:.75rem 0}
.svc-row{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;border:1px solid #e5e7eb;border-radius:8px;margin-bottom:.35rem;font-size:.85rem}
.svc-row input{accent-color:#1e3a5f;width:16px;height:16px;flex-shrink:0}
.svc-row .svc-name{flex:1;font-weight:600}
.svc-row .svc-price{color:#059669;font-weight:600;white-space:nowrap}
.svc-row .svc-inc{color:#9ca3af;font-size:.78rem}
.svc-row.disabled{opacity:.5}
.price-box{background:#f0f4f8;border:2px solid #1e3a5f;border-radius:10px;padding:1rem;text-align:center;margin:1rem 0}
.price-box .price{font-size:1.6rem;font-weight:800;color:#1e3a5f}
.price-box .price-detail{font-size:.78rem;color:#6b7280}
.entity-toggle{display:flex;gap:.5rem;margin-bottom:.75rem}
.entity-toggle button{flex:1;padding:.6rem;border:2px solid #d1d5db;border-radius:8px;background:#fff;cursor:pointer;font-weight:600;font-size:.88rem;font-family:inherit;color:#374151;transition:all .15s}
.entity-toggle button.active{border-color:#1e3a5f;background:#eff6ff;color:#1e3a5f}
.recommend-box{padding:.75rem;background:#f0fdf4;border:2px solid #22c55e;border-radius:8px;margin:.75rem 0;font-size:.85rem}
.recommend-box strong{color:#166534}
.err{color:#dc2626;font-size:.82rem;margin-top:.5rem;display:none}
</style>
</head>
<body>
<div class="wrap">
<h1>FCC Carrier / ISP Registration</h1>
<p class="subtitle">Start your telecom carrier or ISP from scratch. We handle all federal and state registrations based on the services you plan to offer.</p>
<div class="step-dots">
<div class="step-dot active" id="dot-1">1</div>
<div class="step-dot" id="dot-2">2</div>
<div class="step-dot" id="dot-3">3</div>
<div class="step-dot" id="dot-4">4</div>
<div class="step-dot" id="dot-5">5</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- STEP 1: Service Wizard -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div id="step-1" class="card">
<h2>What services will you offer?</h2>
<p class="q-hint">Tell us what you plan to do and we'll determine exactly which registrations you need.</p>
<!-- Q1: Service types -->
<div id="q1">
<p class="q-label">What type of service will you offer?</p>
<p class="q-hint">Select all that apply.</p>
<label class="svc-row"><input type="checkbox" data-svc="voice"> <span class="svc-name">Voice / phone service</span> <span class="svc-inc">VoIP, CLEC, or traditional</span></label>
<label class="svc-row"><input type="checkbox" data-svc="broadband"> <span class="svc-name">Broadband internet service</span> <span class="svc-inc">Retail or wholesale ISP</span></label>
<label class="svc-row"><input type="checkbox" data-svc="wholesale"> <span class="svc-name">Wholesale only</span> <span class="svc-inc">Sell to other carriers, not end users</span></label>
</div>
<!-- Q2: Voice delivery (shown if voice selected) -->
<div id="q2" class="hidden" style="margin-top:1rem">
<p class="q-label">How will you deliver voice service?</p>
<button type="button" class="q-card" data-voice="reseller">I'll resell service from a provider like Bandwidth, Telnyx, Twilio, or a UCaaS platform</button>
<button type="button" class="q-card" data-voice="own_switch">I'll run my own switching equipment (softswitch, SBC, PBX) and buy wholesale trunks</button>
<button type="button" class="q-card" data-voice="ucaas">I'll offer a hosted/UCaaS platform and interconnect directly with carriers</button>
<button type="button" class="q-card" data-voice="unsure">Not sure yet — I'm still planning</button>
</div>
<!-- Q3: Infrastructure needs (shown if own_switch or ucaas) -->
<div id="q3" class="hidden" style="margin-top:1rem">
<p class="q-label">Will you need any of these?</p>
<p class="q-hint">Check all that apply. These determine whether you need your own OCN and STIR/SHAKEN certificate.</p>
<label class="svc-row"><input type="checkbox" data-infra="lcr"> <span class="svc-name">Least-cost routing (LCR)</span> <span class="svc-inc">Route across multiple wholesale providers</span></label>
<label class="svc-row"><input type="checkbox" data-infra="own_dids"> <span class="svc-name">Your own phone numbers (DID blocks)</span> <span class="svc-inc">From number providers like Bandwidth, Inteliquent</span></label>
<label class="svc-row"><input type="checkbox" data-infra="interconnect"> <span class="svc-name">Direct carrier interconnection</span> <span class="svc-inc">SIP trunking you sell to other carriers</span></label>
<label class="svc-row"><input type="checkbox" data-infra="stir_sign"> <span class="svc-name">Sign outbound calls with your own identity</span> <span class="svc-inc">STIR/SHAKEN certificate for call authentication</span></label>
<div id="ocn-explain" class="info-box hidden">
<strong>Why do wholesale voice vendors require an OCN?</strong><br>
An Operating Company Number identifies your company in the telecom numbering system. Major voice providers like Bandwidth, Telnyx, Lumen, and Peerless require it to open a wholesale account, port numbers, and assign DID blocks. Without an OCN, you're limited to reseller-tier access.
</div>
</div>
<!-- Q4: Broadband type (shown if broadband selected) -->
<div id="q4" class="hidden" style="margin-top:1rem">
<p class="q-label">How will you provide internet service?</p>
<button type="button" class="q-card" data-bb="reseller">I resell another provider's internet (white-label fiber, cable resale)</button>
<button type="button" class="q-card" data-bb="facilities">I own or lease network infrastructure (fiber, fixed wireless, towers)</button>
<button type="button" class="q-card" data-bb="cloud">I provide cloud services only (SaaS, hosting) — no last-mile internet</button>
<div id="cloud-warn" class="warn-box hidden">
Based on your description, you may not need FCC carrier registration. Cloud and SaaS services are generally not regulated as telecommunications. <a href="/contact" style="font-weight:600;text-decoration:underline">Contact us for a free assessment.</a>
</div>
</div>
<!-- Q5: Operating states -->
<div id="q5" class="hidden" style="margin-top:1rem">
<p class="q-label">Where will you operate?</p>
<button type="button" class="q-card" data-states="1">Just one state</button>
<button type="button" class="q-card" data-states="few">25 states</button>
<button type="button" class="q-card" data-states="nationwide">Nationwide</button>
</div>
<div style="margin-top:1rem;text-align:right">
<button class="btn btn-primary" id="btn-next-1">See What You Need &rarr;</button>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- STEP 2: Registration Checklist -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div id="step-2" class="card hidden">
<h2>Your Registration Package</h2>
<p class="q-hint">Based on your answers, here's what you need. You can add or remove services.</p>
<p style="font-size:.78rem;font-weight:600;color:#059669;text-transform:uppercase;margin-bottom:.5rem">Included in base price ($1,299)</p>
<div id="base-services"></div>
<p style="font-size:.78rem;font-weight:600;color:#6b7280;text-transform:uppercase;margin:.75rem 0 .5rem">Determined by your service type</p>
<div id="wizard-services"></div>
<p style="font-size:.78rem;font-weight:600;color:#d97706;text-transform:uppercase;margin:.75rem 0 .5rem">Optional add-ons</p>
<div id="addon-services"></div>
<div class="price-box">
<div class="price" id="total-price">$1,299</div>
<div class="price-detail" id="price-detail">Base registration package</div>
</div>
<div style="display:flex;gap:.5rem;justify-content:flex-end">
<button class="btn btn-back" id="btn-back-2">&larr; Back</button>
<button class="btn btn-primary" id="btn-next-2">Choose Entity &rarr;</button>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- STEP 3: Entity Choice -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div id="step-3" class="card hidden">
<h2>Your Business Entity</h2>
<p class="q-hint">We'll file registrations under this entity. You can use an existing one or we'll form a new one.</p>
<div class="entity-toggle">
<button type="button" id="entity-existing" class="active">Use Existing Entity</button>
<button type="button" id="entity-new">Form a New Entity</button>
</div>
<!-- Existing entity search -->
<div id="entity-existing-form">
<label class="label">Search by FRN or company name</label>
<input type="text" id="entity-search" placeholder="e.g. 0012345678 or Acme Telecom LLC">
<div id="entity-search-results" style="margin-top:.5rem"></div>
<div id="entity-details" class="hidden" style="margin-top:.75rem;padding:.75rem;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px">
<label class="label">Legal Name</label>
<input type="text" id="ex-legal-name">
<label class="label">EIN / TIN</label>
<input type="text" id="ex-ein" placeholder="12-3456789 or 123-45-6789">
<label class="label">State of Formation</label>
<input type="text" id="ex-formation-state" maxlength="2" placeholder="WY">
</div>
</div>
<!-- New formation -->
<div id="entity-new-form" class="hidden">
<!-- Nexus guidance questions -->
<div id="formation-guide">
<p class="q-label">Where do you live / primary office?</p>
<select id="home-state" style="margin-bottom:.5rem">
<option value="">Select state...</option>
</select>
<p class="q-label">Do you have employees or a physical office there?</p>
<div style="display:flex;gap:.5rem;margin-bottom:.5rem">
<button type="button" class="q-card" data-nexus="yes" style="flex:1">Yes</button>
<button type="button" class="q-card" data-nexus="no" style="flex:1">No — remote/virtual</button>
</div>
<div id="size-q" class="hidden">
<p class="q-label">Expected first-year size?</p>
<button type="button" class="q-card" data-size="small">Small (&lt; $50K, 1-2 people)</button>
<button type="button" class="q-card" data-size="medium">Medium ($50K-$500K, small team)</button>
<button type="button" class="q-card" data-size="large">Large (&gt; $500K, multiple states)</button>
</div>
<div id="state-recommend" class="recommend-box hidden"></div>
</div>
<label class="label">Formation State</label>
<select id="new-formation-state"></select>
<label class="label">Entity Type</label>
<select id="new-entity-type">
<option value="llc">LLC (recommended for most carriers)</option>
<option value="corporation">Corporation</option>
</select>
<label class="label">Entity Name (leave blank for numbered)</label>
<input type="text" id="new-entity-name" placeholder="e.g. Acme Telecom LLC">
<div id="formation-fee-display" style="margin-top:.5rem;font-size:.85rem;color:#6b7280"></div>
</div>
<div style="margin-top:1rem;display:flex;gap:.5rem;justify-content:flex-end">
<button class="btn btn-back" id="btn-back-3">&larr; Back</button>
<button class="btn btn-primary" id="btn-next-3">Contact Info &rarr;</button>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- STEP 4: Contact & Officer -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div id="step-4" class="card hidden">
<h2>Contact & Officer Information</h2>
<p class="q-hint">This information is used on all FCC filings and state registrations.</p>
<label class="label">Contact Name *</label>
<input type="text" id="contact-name">
<label class="label">Contact Email *</label>
<input type="email" id="contact-email">
<label class="label">Contact Phone *</label>
<input type="tel" id="contact-phone">
<label class="label">Title</label>
<input type="text" id="contact-title" value="Chief Executive Officer">
<label class="label" style="margin-top:1rem">Business Address *</label>
<input type="text" id="addr-street" placeholder="Street address">
<div style="display:flex;gap:.5rem;margin-top:.3rem">
<input type="text" id="addr-city" placeholder="City" style="flex:2">
<input type="text" id="addr-state" placeholder="ST" maxlength="2" style="flex:.5">
<input type="text" id="addr-zip" placeholder="ZIP" style="flex:1">
</div>
<div style="margin-top:1rem;display:flex;gap:.5rem;justify-content:flex-end">
<button class="btn btn-back" id="btn-back-4">&larr; Back</button>
<button class="btn btn-primary" id="btn-next-4">Review &rarr;</button>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- STEP 5: Review & Payment -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div id="step-5" class="card hidden">
<h2>Review & Checkout</h2>
<div id="review-content"></div>
<label style="display:flex;align-items:flex-start;gap:.5rem;padding:.65rem;margin-top:.75rem;border:1px solid #e5e7eb;border-radius:8px;cursor:pointer;font-size:.75rem;color:#6b7280;line-height:1.5">
<input type="checkbox" id="engage-check" required style="margin-top:2px;accent-color:#1e3a5f">
<span>I authorize Performance West Inc. to prepare and submit regulatory filings on my behalf as described above. I understand Performance West provides compliance consulting services, not legal advice or legal representation. I confirm the information I provide is accurate to the best of my knowledge. <a href="/terms" target="_blank" style="color:#1e3a5f;text-decoration:underline">Terms of Service</a></span>
</label>
<div style="margin-top:.75rem">
<label class="label">Payment Method</label>
<div style="display:flex;flex-direction:column;gap:.3rem">
<label class="svc-row"><input type="radio" name="pay" value="ach" checked> <span class="svc-name">ACH Bank Transfer</span> <span style="color:#059669;font-size:.78rem">No fee</span></label>
<label class="svc-row"><input type="radio" name="pay" value="card"> <span class="svc-name">Credit / Debit Card</span> <span style="color:#9ca3af;font-size:.78rem">+3%</span></label>
<label class="svc-row"><input type="radio" name="pay" value="paypal"> <span class="svc-name">PayPal</span> <span style="color:#9ca3af;font-size:.78rem">+3%</span></label>
<label class="svc-row"><input type="radio" name="pay" value="crypto"> <span class="svc-name">Cryptocurrency</span> <span style="color:#059669;font-size:.78rem">No fee</span></label>
</div>
</div>
<div style="margin-top:1rem;display:flex;gap:.5rem;justify-content:flex-end">
<button class="btn btn-back" id="btn-back-5">&larr; Back</button>
<button class="btn btn-primary" id="btn-pay">Continue to Payment</button>
</div>
<p class="err" id="checkout-err"></p>
</div>
</div>
<script>
(function() {
var API = window.__PW_API;
// ── State data ──
var STATES = [
['AL','Alabama'],['AK','Alaska'],['AZ','Arizona'],['AR','Arkansas'],['CA','California'],
['CO','Colorado'],['CT','Connecticut'],['DE','Delaware'],['DC','District of Columbia'],['FL','Florida'],
['GA','Georgia'],['HI','Hawaii'],['ID','Idaho'],['IL','Illinois'],['IN','Indiana'],
['IA','Iowa'],['KS','Kansas'],['KY','Kentucky'],['LA','Louisiana'],['ME','Maine'],
['MD','Maryland'],['MA','Massachusetts'],['MI','Michigan'],['MN','Minnesota'],['MS','Mississippi'],
['MO','Missouri'],['MT','Montana'],['NE','Nebraska'],['NV','Nevada'],['NH','New Hampshire'],
['NJ','New Jersey'],['NM','New Mexico'],['NY','New York'],['NC','North Carolina'],['ND','North Dakota'],
['OH','Ohio'],['OK','Oklahoma'],['OR','Oregon'],['PA','Pennsylvania'],['RI','Rhode Island'],
['SC','South Carolina'],['SD','South Dakota'],['TN','Tennessee'],['TX','Texas'],['UT','Utah'],
['VT','Vermont'],['VA','Virginia'],['WA','Washington'],['WV','West Virginia'],['WI','Wisconsin'],['WY','Wyoming']
];
// Populate state dropdowns
['home-state','new-formation-state'].forEach(function(id) {
var sel = document.getElementById(id);
STATES.forEach(function(s) {
var opt = document.createElement('option');
opt.value = s[0]; opt.textContent = s[0] + ' — ' + s[1];
sel.appendChild(opt);
});
});
// ── Wizard state ──
var wizard = {
serviceTypes: [], voiceDelivery: '', infraNeeds: [],
broadbandType: '', operatingStates: '',
// Derived
needsRmd: false, needsCpni: false, needsCalea: false, needsBdc: false,
needsOcn: false, needsStirShaken: false,
// Entity
entitySource: 'existing', formationState: '', entityType: 'llc',
entityName: '', ein: '', frn: '', legalName: '',
// Contact
contactName: '', contactEmail: '', contactPhone: '', contactTitle: 'Chief Executive Officer',
addrStreet: '', addrCity: '', addrState: '', addrZip: '',
// Pricing
baseFee: 129900, formationFee: 0, stateFee: 0, pucFee: 0, addonFee: 0,
};
// ── Step navigation ──
function showStep(n) {
[1,2,3,4,5].forEach(function(i) {
document.getElementById('step-' + i).classList.toggle('hidden', i !== n);
var dot = document.getElementById('dot-' + i);
dot.className = 'step-dot' + (i < n ? ' done' : i === n ? ' active' : '');
});
document.querySelector('.wrap').scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// ── Q1: Service types ──
document.querySelectorAll('[data-svc]').forEach(function(cb) {
cb.addEventListener('change', function() {
wizard.serviceTypes = Array.from(document.querySelectorAll('[data-svc]:checked')).map(function(c) { return c.dataset.svc; });
document.getElementById('q2').classList.toggle('hidden', wizard.serviceTypes.indexOf('voice') < 0);
document.getElementById('q4').classList.toggle('hidden', wizard.serviceTypes.indexOf('broadband') < 0);
document.getElementById('q5').classList.remove('hidden');
});
});
// ── Q2: Voice delivery ──
document.querySelectorAll('[data-voice]').forEach(function(btn) {
btn.addEventListener('click', function() {
document.querySelectorAll('[data-voice]').forEach(function(b) { b.classList.remove('selected'); });
btn.classList.add('selected');
wizard.voiceDelivery = btn.dataset.voice;
var showQ3 = wizard.voiceDelivery === 'own_switch' || wizard.voiceDelivery === 'ucaas';
document.getElementById('q3').classList.toggle('hidden', !showQ3);
});
});
// ── Q3: Infrastructure needs ──
document.querySelectorAll('[data-infra]').forEach(function(cb) {
cb.addEventListener('change', function() {
wizard.infraNeeds = Array.from(document.querySelectorAll('[data-infra]:checked')).map(function(c) { return c.dataset.infra; });
var needsOcn = wizard.infraNeeds.indexOf('lcr') >= 0 || wizard.infraNeeds.indexOf('own_dids') >= 0 ||
wizard.infraNeeds.indexOf('interconnect') >= 0 || wizard.infraNeeds.indexOf('stir_sign') >= 0;
document.getElementById('ocn-explain').classList.toggle('hidden', !needsOcn);
});
});
// ── Q4: Broadband type ──
document.querySelectorAll('[data-bb]').forEach(function(btn) {
btn.addEventListener('click', function() {
document.querySelectorAll('[data-bb]').forEach(function(b) { b.classList.remove('selected'); });
btn.classList.add('selected');
wizard.broadbandType = btn.dataset.bb;
document.getElementById('cloud-warn').classList.toggle('hidden', wizard.broadbandType !== 'cloud');
});
});
// ── Q5: Operating states ──
document.querySelectorAll('[data-states]').forEach(function(btn) {
btn.addEventListener('click', function() {
document.querySelectorAll('[data-states]').forEach(function(b) { b.classList.remove('selected'); });
btn.classList.add('selected');
wizard.operatingStates = btn.dataset.states;
});
});
// ── Step 1 → 2: Derive registrations ──
document.getElementById('btn-next-1').addEventListener('click', function() {
if (wizard.serviceTypes.length === 0) { alert('Please select at least one service type.'); return; }
var hasVoice = wizard.serviceTypes.indexOf('voice') >= 0;
var hasBroadband = wizard.serviceTypes.indexOf('broadband') >= 0;
var isWholesale = wizard.serviceTypes.indexOf('wholesale') >= 0;
wizard.needsRmd = hasVoice;
wizard.needsCpni = hasVoice;
wizard.needsCalea = hasVoice || (hasBroadband && wizard.broadbandType === 'facilities');
wizard.needsBdc = hasBroadband && !isWholesale;
wizard.needsOcn = wizard.infraNeeds.indexOf('lcr') >= 0 || wizard.infraNeeds.indexOf('own_dids') >= 0 ||
wizard.infraNeeds.indexOf('interconnect') >= 0 || wizard.infraNeeds.indexOf('stir_sign') >= 0;
wizard.needsStirShaken = wizard.infraNeeds.indexOf('stir_sign') >= 0 || wizard.needsOcn;
buildChecklist();
showStep(2);
});
// ── Build registration checklist (Step 2) ──
function buildChecklist() {
var base = document.getElementById('base-services');
var derived = document.getElementById('wizard-services');
var addons = document.getElementById('addon-services');
base.innerHTML = ''; derived.innerHTML = ''; addons.innerHTML = '';
function svcRow(name, desc, included, price, key, parent) {
var row = document.createElement('label');
row.className = 'svc-row';
var checked = included ? 'checked' : '';
var priceText = price ? '$' + (price / 100).toLocaleString() : 'Included';
row.innerHTML = '<input type="checkbox" data-reg="' + key + '" ' + checked + '> ' +
'<span class="svc-name">' + name + '</span> ' +
(price ? '<span class="svc-price">+' + priceText + '</span>' : '<span class="svc-inc">' + priceText + '</span>');
row.querySelector('input').addEventListener('change', updatePrice);
parent.appendChild(row);
}
// Base (always included)
svcRow('CORES / FRN Registration', 'FCC Registration Number', true, 0, 'cores', base);
svcRow('Form 499 Initial Registration', 'USAC enrollment', true, 0, 'form499', base);
svcRow('D.C. Registered Agent (Annual)', 'FCC process-of-service', true, 0, 'dc_agent', base);
// Wizard-determined
if (wizard.needsRmd) svcRow('RMD Registration', 'Robocall Mitigation Database', true, 0, 'rmd', derived);
if (wizard.needsCpni) svcRow('CPNI Certification', 'Customer data protection', true, 0, 'cpni', derived);
if (wizard.needsCalea) svcRow('CALEA SSI Plan', 'Lawful intercept compliance', true, 0, 'calea', derived);
if (wizard.needsBdc) svcRow('BDC Filing', 'Broadband Data Collection', true, 0, 'bdc', derived);
// Add-ons
svcRow('STIR/SHAKEN Implementation', 'Call authentication certificate', wizard.needsStirShaken, 49900, 'stir_shaken', addons);
svcRow('NECA OCN Registration', 'Operating Company Number + sponsoring CLEC', wizard.needsOcn, 265000, 'ocn', addons);
svcRow('State PUC Registration (per state)', 'State utility commission filing', false, 39900, 'state_puc', addons);
updatePrice();
}
function updatePrice() {
var total = wizard.baseFee;
var details = ['Base: $1,299'];
document.querySelectorAll('[data-reg]').forEach(function(cb) {
if (!cb.checked) return;
var key = cb.dataset.reg;
if (key === 'stir_shaken') { total += 49900; details.push('STIR/SHAKEN: +$499'); }
if (key === 'ocn') { total += 265000; details.push('OCN: +$2,650'); }
if (key === 'state_puc') { total += 39900; details.push('State PUC: +$399'); }
});
total += wizard.formationFee + wizard.stateFee;
if (wizard.formationFee) details.push('Formation: +$' + ((wizard.formationFee + wizard.stateFee) / 100).toLocaleString());
total -= wizard.addonFee; // discount placeholder
document.getElementById('total-price').textContent = '$' + (total / 100).toLocaleString();
document.getElementById('price-detail').textContent = details.join(' · ');
wizard.addonFee = total - wizard.baseFee - wizard.formationFee - wizard.stateFee;
}
// ── Step 2 → 3 ──
document.getElementById('btn-next-2').addEventListener('click', function() { showStep(3); });
document.getElementById('btn-back-2').addEventListener('click', function() { showStep(1); });
// ── Step 3: Entity toggle ──
document.getElementById('entity-existing').addEventListener('click', function() {
wizard.entitySource = 'existing';
this.classList.add('active');
document.getElementById('entity-new').classList.remove('active');
document.getElementById('entity-existing-form').classList.remove('hidden');
document.getElementById('entity-new-form').classList.add('hidden');
wizard.formationFee = 0; wizard.stateFee = 0;
updatePrice();
});
document.getElementById('entity-new').addEventListener('click', function() {
wizard.entitySource = 'new_formation';
this.classList.add('active');
document.getElementById('entity-existing').classList.remove('active');
document.getElementById('entity-new-form').classList.remove('hidden');
document.getElementById('entity-existing-form').classList.add('hidden');
});
// ── Formation state guidance ──
document.querySelectorAll('[data-nexus]').forEach(function(btn) {
btn.addEventListener('click', function() {
document.querySelectorAll('[data-nexus]').forEach(function(b) { b.classList.remove('selected'); });
btn.classList.add('selected');
if (btn.dataset.nexus === 'yes') {
var st = document.getElementById('home-state').value;
showRecommendation(st, 'Your home state (' + st + ') is recommended since you already have business presence there. This avoids foreign qualification fees.');
document.getElementById('size-q').classList.add('hidden');
} else {
document.getElementById('size-q').classList.remove('hidden');
}
});
});
document.querySelectorAll('[data-size]').forEach(function(btn) {
btn.addEventListener('click', function() {
document.querySelectorAll('[data-size]').forEach(function(b) { b.classList.remove('selected'); });
btn.classList.add('selected');
var home = document.getElementById('home-state').value;
if (btn.dataset.size === 'small') {
showRecommendation('WY', 'Wyoming is recommended for small operations — $100 formation fee, no state income tax, $60/yr annual report, strong privacy. You\'ll need foreign qualification if you expand to other states.');
} else if (btn.dataset.size === 'medium') {
showRecommendation(home || 'DE', (home ? home : 'Delaware') + ' is recommended. ' + (home ? 'Forming in your home state avoids foreign qualification fees.' : 'Delaware is investor-friendly with a specialized business court.'));
} else {
showRecommendation(home || 'WY', 'Forming in your home state is recommended for larger operations — you\'ll need foreign qualifications in other states regardless.');
}
});
});
function showRecommendation(state, reason) {
var box = document.getElementById('state-recommend');
box.innerHTML = '<strong>Recommended: ' + state + '</strong><br>' + reason +
'<br><button type="button" onclick="document.getElementById(\'new-formation-state\').value=\'' + state + '\';updateFormationFee()" style="margin-top:.5rem;font-size:.82rem;font-weight:600;color:#1e3a5f;background:none;border:none;cursor:pointer;text-decoration:underline">Use ' + state + '</button>';
box.classList.remove('hidden');
document.getElementById('new-formation-state').value = state;
updateFormationFee();
}
// ── Formation fee lookup ──
var stateFeesCache = null;
window.updateFormationFee = function() {
var state = document.getElementById('new-formation-state').value;
var type = document.getElementById('new-entity-type').value;
if (!state) return;
if (stateFeesCache) {
applyFormationFee(state, type);
} else {
fetch(API + '/api/v1/states').then(function(r) { return r.json(); }).then(function(d) {
stateFeesCache = d;
applyFormationFee(state, type);
}).catch(function() {});
}
};
function applyFormationFee(state, type) {
if (!stateFeesCache) return;
var fees = (stateFeesCache.states || stateFeesCache).find(function(s) { return s.state_code === state; });
if (!fees) return;
var fee = type === 'corporation' ? (fees.corp_formation_fee || fees.llc_formation_fee) : fees.llc_formation_fee;
wizard.stateFee = fee || 0;
wizard.formationFee = 2500; // $25 filing markup
document.getElementById('formation-fee-display').textContent =
'State filing fee: $' + ((wizard.stateFee) / 100).toLocaleString() + ' + $25 filing service = $' + ((wizard.stateFee + 2500) / 100).toLocaleString();
updatePrice();
}
document.getElementById('new-formation-state').addEventListener('change', function() { updateFormationFee(); });
document.getElementById('new-entity-type').addEventListener('change', function() { updateFormationFee(); });
// ── Step 3 → 4 ──
document.getElementById('btn-next-3').addEventListener('click', function() { showStep(4); });
document.getElementById('btn-back-3').addEventListener('click', function() { showStep(2); });
// ── Step 4 → 5 ──
document.getElementById('btn-next-4').addEventListener('click', function() {
var name = document.getElementById('contact-name').value.trim();
var email = document.getElementById('contact-email').value.trim();
var phone = document.getElementById('contact-phone').value.trim();
if (!name || !email || !phone) { alert('Please fill in contact name, email, and phone.'); return; }
wizard.contactName = name; wizard.contactEmail = email; wizard.contactPhone = phone;
wizard.contactTitle = document.getElementById('contact-title').value.trim();
wizard.addrStreet = document.getElementById('addr-street').value.trim();
wizard.addrCity = document.getElementById('addr-city').value.trim();
wizard.addrState = document.getElementById('addr-state').value.trim().toUpperCase();
wizard.addrZip = document.getElementById('addr-zip').value.trim();
buildReview();
showStep(5);
});
document.getElementById('btn-back-4').addEventListener('click', function() { showStep(3); });
document.getElementById('btn-back-5').addEventListener('click', function() { showStep(4); });
// ── Build review ──
function buildReview() {
var services = [];
document.querySelectorAll('[data-reg]:checked').forEach(function(cb) { services.push(cb.dataset.reg); });
var html = '<table style="width:100%;border-collapse:collapse;font-size:.85rem;margin-bottom:1rem">';
html += '<tr style="border-bottom:2px solid #e5e7eb"><th style="text-align:left;padding:.4rem">Item</th><th style="text-align:right;padding:.4rem">Price</th></tr>';
var items = [
{ name: 'Base Registration (CORES/FRN + Form 499 + DC Agent)', price: wizard.baseFee },
];
if (services.indexOf('rmd') >= 0) items.push({ name: 'RMD Registration', price: 0 });
if (services.indexOf('cpni') >= 0) items.push({ name: 'CPNI Certification', price: 0 });
if (services.indexOf('calea') >= 0) items.push({ name: 'CALEA SSI Plan', price: 0 });
if (services.indexOf('bdc') >= 0) items.push({ name: 'BDC Filing', price: 0 });
if (services.indexOf('stir_shaken') >= 0) items.push({ name: 'STIR/SHAKEN Implementation', price: 49900 });
if (services.indexOf('ocn') >= 0) items.push({ name: 'NECA OCN Registration', price: 265000 });
if (services.indexOf('state_puc') >= 0) items.push({ name: 'State PUC Registration', price: 39900 });
if (wizard.entitySource === 'new_formation') {
items.push({ name: 'Business Formation (' + (document.getElementById('new-formation-state').value || '?') + ' ' + document.getElementById('new-entity-type').value.toUpperCase() + ')', price: wizard.formationFee + wizard.stateFee });
}
var total = 0;
items.forEach(function(item) {
total += item.price;
html += '<tr style="border-bottom:1px solid #f3f4f6"><td style="padding:.35rem">' + item.name + '</td>';
html += '<td style="text-align:right;padding:.35rem">' + (item.price ? '$' + (item.price / 100).toLocaleString() : 'Included') + '</td></tr>';
});
html += '<tr style="border-top:2px solid #e5e7eb;font-weight:700"><td style="padding:.5rem">Total</td><td style="text-align:right;padding:.5rem">$' + (total / 100).toLocaleString() + '</td></tr>';
html += '</table>';
html += '<div style="font-size:.85rem;color:#6b7280;margin-bottom:.5rem">';
html += '<strong>Entity:</strong> ' + (wizard.entitySource === 'existing' ? (document.getElementById('ex-legal-name').value || 'Existing entity') : 'New ' + document.getElementById('new-entity-type').value.toUpperCase() + ' in ' + (document.getElementById('new-formation-state').value || '?'));
html += '<br><strong>Contact:</strong> ' + wizard.contactName + ' (' + wizard.contactEmail + ')';
html += '</div>';
document.getElementById('review-content').innerHTML = html;
}
// ── Checkout ──
document.getElementById('btn-pay').addEventListener('click', async function() {
var btn = this;
var errEl = document.getElementById('checkout-err');
errEl.style.display = 'none';
if (!document.getElementById('engage-check').checked) { errEl.textContent = 'Please accept the authorization terms.'; errEl.style.display = 'block'; return; }
btn.disabled = true; btn.textContent = 'Creating order...';
var services = [];
document.querySelectorAll('[data-reg]:checked').forEach(function(cb) { services.push(cb.dataset.reg); });
var payMethod = document.querySelector('input[name=pay]:checked').value;
try {
var orderResp = await fetch(API + '/api/v1/fcc-carrier-registration', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
customer_name: wizard.contactName,
customer_email: wizard.contactEmail,
customer_phone: wizard.contactPhone,
entity_source: wizard.entitySource,
entity_legal_name: wizard.entitySource === 'existing' ? document.getElementById('ex-legal-name').value.trim() : document.getElementById('new-entity-name').value.trim(),
ein: wizard.entitySource === 'existing' ? document.getElementById('ex-ein').value.trim() : '',
formation_state: wizard.entitySource === 'existing' ? document.getElementById('ex-formation-state').value.trim() : document.getElementById('new-formation-state').value,
entity_type: wizard.entitySource === 'new_formation' ? document.getElementById('new-entity-type').value : '',
frn: wizard.frn || '',
contact_name: wizard.contactName,
contact_email: wizard.contactEmail,
contact_phone: wizard.contactPhone,
contact_title: wizard.contactTitle,
address_street: wizard.addrStreet,
address_city: wizard.addrCity,
address_state: wizard.addrState,
address_zip: wizard.addrZip,
service_wizard: {
service_types: wizard.serviceTypes,
voice_delivery: wizard.voiceDelivery,
infra_needs: wizard.infraNeeds,
broadband_type: wizard.broadbandType,
operating_states: wizard.operatingStates,
},
services: services,
engagement_accepted: true,
}),
});
var orderData = await orderResp.json();
if (!orderResp.ok) throw new Error(orderData.error || 'Order creation failed');
btn.textContent = 'Redirecting to payment...';
var sessionResp = await fetch(API + '/api/v1/checkout/create-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order_id: orderData.order_number, order_type: 'fcc_carrier_registration', payment_method: payMethod }),
});
var sessionData = await sessionResp.json();
if (!sessionResp.ok) throw new Error(sessionData.error || 'Checkout failed');
if (sessionData.checkout_url) window.location.href = sessionData.checkout_url;
else throw new Error('No checkout URL');
} catch (err) {
errEl.textContent = err.message || 'Something went wrong.';
errEl.style.display = 'block';
btn.disabled = false; btn.textContent = 'Continue to Payment';
}
});
// ── Entity search ──
var searchTimeout;
document.getElementById('entity-search').addEventListener('input', function() {
clearTimeout(searchTimeout);
var q = this.value.trim();
if (q.length < 3) return;
searchTimeout = setTimeout(function() {
var isDigits = /^\d+$/.test(q);
var params = isDigits && q.length === 10 ? 'frn=' + q : 'q=' + encodeURIComponent(q);
fetch(API + '/api/v1/fcc/search?' + params).then(function(r) { return r.json(); }).then(function(d) {
var results = d.results || [];
var container = document.getElementById('entity-search-results');
container.innerHTML = '';
if (results.length === 0) { container.innerHTML = '<p style="font-size:.82rem;color:#9ca3af">No results found.</p>'; return; }
results.forEach(function(r) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'q-card';
btn.innerHTML = '<strong>' + (r.business_name || 'Unknown') + '</strong>' +
(r.frn ? ' <span style="font-size:.78rem;color:#6b7280">FRN: ' + r.frn + '</span>' : '') +
(r.city ? ' <span style="font-size:.78rem;color:#9ca3af">' + r.city + ', ' + (r.state || '') + '</span>' : '');
btn.addEventListener('click', function() {
wizard.frn = r.frn || '';
document.getElementById('ex-legal-name').value = r.business_name || '';
document.getElementById('entity-details').classList.remove('hidden');
// Auto-fill from FCC lookup
if (r.frn) {
fetch(API + '/api/v1/fcc/lookup?frn=' + r.frn + '&quick=1').then(function(resp) { return resp.json(); }).then(function(fcc) {
if (fcc.cores) {
if (fcc.cores.address && !/LLC|Inc|Corp/i.test(fcc.cores.address)) wizard.addrStreet = fcc.cores.address;
if (fcc.cores.city) wizard.addrCity = fcc.cores.city;
if (fcc.cores.state) { wizard.addrState = fcc.cores.state; document.getElementById('ex-formation-state').value = fcc.cores.state; }
if (fcc.cores.zip) wizard.addrZip = fcc.cores.zip;
document.getElementById('addr-street').value = wizard.addrStreet;
document.getElementById('addr-city').value = wizard.addrCity;
document.getElementById('addr-state').value = wizard.addrState;
document.getElementById('addr-zip').value = wizard.addrZip;
}
if (fcc.rmd && fcc.rmd.contact_name) {
wizard.contactName = fcc.rmd.contact_name;
document.getElementById('contact-name').value = fcc.rmd.contact_name;
}
if (fcc.entity_name) document.getElementById('ex-legal-name').value = fcc.entity_name;
}).catch(function() {});
}
});
container.appendChild(btn);
});
}).catch(function() {});
}, 500);
});
})();
</script>
</body>
</html>