diff --git a/api/src/index.ts b/api/src/index.ts index 3bea7ed..f5a51dd 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -44,6 +44,7 @@ import foreignQualRouter from "./routes/foreign-qualification.js"; import corpStatusRouter from "./routes/corp-status.js"; import portalRmdReviewRouter from "./routes/portal-rmd-review.js"; import pucRouter from "./routes/puc.js"; +import fccCarrierRegRouter from "./routes/fcc-carrier-registration.js"; const app = express(); @@ -112,6 +113,7 @@ app.use(lnpaRegionsRouter); app.use(fccFilingsRouter); app.use(foreignQualRouter); app.use(pucRouter); +app.use(fccCarrierRegRouter); app.use(adminCryptoRouter); // Note: identityRouter mounted above express.json() for webhook route, // but also handles non-webhook routes (create-session, poll) which work fine with json() diff --git a/api/src/routes/checkout.ts b/api/src/routes/checkout.ts index 71a2546..e4b4431 100644 --- a/api/src/routes/checkout.ts +++ b/api/src/routes/checkout.ts @@ -76,7 +76,7 @@ const GATEWAY_LABELS: Record = { const CreateSessionSchema = z.object({ order_id: z.string().min(1), - order_type: z.enum(["canada_crtc", "formation", "bundle", "compliance", "compliance_batch"]), + order_type: z.enum(["canada_crtc", "formation", "bundle", "compliance", "compliance_batch", "fcc_carrier_registration"]), payment_method: z.enum(["card", "ach", "paypal", "klarna", "crypto"]), }); @@ -246,6 +246,48 @@ async function fetchOrderData( } | null> { // ── Canada CRTC ───────────────────────────────────────────────────────── + if (order_type === "fcc_carrier_registration") { + const { rows } = await pool.query( + `SELECT * FROM fcc_carrier_registrations WHERE order_number = $1`, + [order_id], + ); + if (!rows.length) return null; + const order = rows[0] as Record; + if (order.payment_status !== "pending_payment") return null; + + const baseCents = (order.service_fee_cents as number) || 129900; + const formCents = ((order.formation_fee_cents as number) || 0) + ((order.state_fee_cents as number) || 0); + const addonCents = (order.addon_fee_cents as number) || 0; + const pucCents = (order.puc_fee_cents as number) || 0; + const totalCents = baseCents + formCents + addonCents + pucCents; + + const lineItems: Array<{price_data: {currency: "usd"; product_data: {name: string}; unit_amount: number}; quantity: number}> = [ + { price_data: { currency: "usd", product_data: { name: "FCC Carrier / ISP Registration" }, unit_amount: baseCents }, quantity: 1 }, + ]; + if (formCents > 0) { + lineItems.push({ price_data: { currency: "usd", product_data: { name: `Business Formation (${order.formation_state || "?"} ${((order.entity_type as string) || "LLC").toUpperCase()})` }, unit_amount: formCents }, quantity: 1 }); + } + if ((order.include_stir_shaken as boolean)) { + lineItems.push({ price_data: { currency: "usd", product_data: { name: "STIR/SHAKEN Implementation" }, unit_amount: 49900 }, quantity: 1 }); + } + if ((order.include_ocn as boolean)) { + lineItems.push({ price_data: { currency: "usd", product_data: { name: "NECA OCN Registration" }, unit_amount: 265000 }, quantity: 1 }); + } + if (pucCents > 0) { + const stateCount = ((order.state_puc_states as string[]) || []).length; + lineItems.push({ price_data: { currency: "usd", product_data: { name: `State PUC Registration (${stateCount} state${stateCount !== 1 ? "s" : ""})` }, unit_amount: pucCents }, quantity: 1 }); + } + + return { + order, + stripeLineItems: lineItems as Stripe.Checkout.SessionCreateParams.LineItem[], + service_cents: totalCents, + base_cents: totalCents, + customer_email: (order.customer_email as string) || "", + customer_name: (order.customer_name as string) || "", + }; + } + if (order_type === "canada_crtc") { const { rows } = await pool.query( `SELECT * FROM canada_crtc_orders WHERE order_number = $1`, @@ -1106,6 +1148,7 @@ export async function handlePaymentComplete( bundle: "bundle_orders", compliance: "compliance_orders", compliance_batch: "compliance_orders", + fcc_carrier_registration: "fcc_carrier_registrations", }; const table = tableMap[order_type]; if (!table) return; @@ -1200,6 +1243,48 @@ export async function handlePaymentComplete( } // ── Advance compliance batch orders (fan out worker dispatch per service) ── + // ── FCC Carrier Registration — dispatch pipeline worker ────────────────── + if (order_type === "fcc_carrier_registration") { + const workerUrl = process.env.WORKER_URL || "http://workers:8090"; + setImmediate(async () => { + try { + const dispatchRes = await fetch(`${workerUrl}/jobs`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "process_fcc_carrier_registration", + order_number: order_id, + }), + }); + if (dispatchRes.ok) { + console.log(`[checkout] FCC carrier registration dispatched: ${order_id}`); + } else { + console.error(`[checkout] FCC carrier reg dispatch failed: HTTP ${dispatchRes.status}`); + } + } catch (err) { + console.error(`[checkout] FCC carrier reg dispatch error:`, err); + } + }); + + // Send confirmation email + try { + const { sendEmail } = await import("../email.js"); + const custEmail = (order.customer_email as string) || ""; + const custName = (order.customer_name as string) || ""; + const firstName = custName.split(" ")[0] || custName; + await sendEmail({ + to: custEmail, + subject: `FCC Carrier Registration — Order ${order_id} Confirmed`, + html: `

Your FCC Carrier Registration is Underway

+

Hi ${firstName},

+

Thank you for your order. We're now processing your carrier/ISP registration package.

+

You'll receive email updates as each step completes. If we need any additional information, we'll reach out.

+

Order: ${order_id}

+

Performance West Inc. | 525 Randall Ave Ste 100-1195, Cheyenne, WY 82001 | 1-888-411-0383

`, + }); + } catch {} + } + if (order_type === "compliance_batch") { // A batch order is a set of compliance_orders sharing a batch_id. // Create ERPNext Sales Order + Invoice, then dispatch each sub-order. diff --git a/api/src/routes/fcc-carrier-registration.ts b/api/src/routes/fcc-carrier-registration.ts new file mode 100644 index 0000000..79dd605 --- /dev/null +++ b/api/src/routes/fcc-carrier-registration.ts @@ -0,0 +1,250 @@ +/** + * FCC Carrier / ISP Registration — order creation & status API. + * + * POST /api/v1/fcc-carrier-registration — create order + * GET /api/v1/fcc-carrier-registration/:id — get order status + * GET /api/v1/fcc-carrier-registration/state-fees — formation fees per state + */ + +import { Router, type Request, type Response } from "express"; +import { pool } from "../db.js"; +import { randomBytes } from "crypto"; + +const router = Router(); + +const BASE_FEE_CENTS = 129900; // $1,299 +const FORMATION_MARKUP_CENTS = 2500; // $25 filing service +const STIR_SHAKEN_FEE_CENTS = 49900; +const OCN_FEE_CENTS = 265000; +const STATE_PUC_FEE_CENTS = 39900; // per state + +function generateOrderNumber(): string { + const year = new Date().getFullYear(); + const id = randomBytes(4).toString("hex").toUpperCase(); + return `FCR-${year}-${id}`; +} + +// ── GET /api/v1/fcc-carrier-registration/state-fees ───────────────────────── + +router.get("/api/v1/fcc-carrier-registration/state-fees", async (_req: Request, res: Response) => { + try { + const { rows } = await pool.query( + `SELECT state_code, state_name, llc_formation_fee, corp_formation_fee, + expedited_fee, typical_processing_days + FROM state_filing_fees ORDER BY state_name`, + ); + res.json({ states: rows, markup_cents: FORMATION_MARKUP_CENTS }); + } catch (err) { + console.error("[fcc-carrier-reg] state-fees error:", err); + res.status(500).json({ error: "Could not load state fees" }); + } +}); + +// ── POST /api/v1/fcc-carrier-registration ─────────────────────────────────── + +router.post("/api/v1/fcc-carrier-registration", async (req: Request, res: Response) => { + try { + const { + customer_name, customer_email, customer_phone, + entity_source, entity_legal_name, ein, formation_state, entity_type, frn, + contact_name, contact_email, contact_phone, contact_title, + address_street, address_city, address_state, address_zip, + service_wizard, services, + engagement_accepted, + } = req.body ?? {}; + + // Validate required fields + if (!customer_email || !customer_name) { + res.status(400).json({ error: "customer_name and customer_email are required." }); + return; + } + if (!entity_source || !["existing", "new_formation"].includes(entity_source)) { + res.status(400).json({ error: "entity_source must be 'existing' or 'new_formation'." }); + return; + } + if (!contact_name || !contact_email) { + res.status(400).json({ error: "contact_name and contact_email are required." }); + return; + } + + // Determine included services from wizard + checklist + const svcList: string[] = Array.isArray(services) ? services : []; + const wizardData = service_wizard || {}; + + const includeFormation = entity_source === "new_formation"; + const includeRmd = true; // always included in base + const includeCpni = true; + const includeCalea = true; + const includeBdc = true; + const includeDcAgent = true; + const includeStirShaken = svcList.includes("stir_shaken"); + const includeOcn = svcList.includes("ocn"); + const statePucStates = svcList.includes("state_puc") ? (wizardData.puc_states || []) : []; + + // Calculate pricing + let formationFeeCents = 0; + let stateFeeCents = 0; + if (includeFormation && formation_state) { + // Look up state filing fee + const feeCol = (entity_type === "corporation") ? "corp_formation_fee" : "llc_formation_fee"; + try { + const feeResult = await pool.query( + `SELECT ${feeCol} AS fee FROM state_filing_fees WHERE state_code = $1`, + [formation_state.toUpperCase()], + ); + if (feeResult.rows.length > 0 && feeResult.rows[0].fee) { + stateFeeCents = Number(feeResult.rows[0].fee); + } + } catch {} + formationFeeCents = FORMATION_MARKUP_CENTS; + } + + let addonFeeCents = 0; + if (includeStirShaken) addonFeeCents += STIR_SHAKEN_FEE_CENTS; + if (includeOcn) addonFeeCents += OCN_FEE_CENTS; + const pucFeeCents = statePucStates.length * STATE_PUC_FEE_CENTS; + + const orderNumber = generateOrderNumber(); + + const result = await pool.query( + `INSERT INTO fcc_carrier_registrations ( + order_number, customer_email, customer_name, customer_phone, + entity_source, entity_legal_name, entity_type, formation_state, ein, frn, + contact_name, contact_email, contact_phone, contact_title, + address_street, address_city, address_state, address_zip, + service_wizard, + include_formation, include_dc_agent, include_rmd, include_cpni, + include_calea, include_bdc, include_stir_shaken, include_ocn, + state_puc_states, + service_fee_cents, formation_fee_cents, state_fee_cents, + puc_fee_cents, addon_fee_cents, + engagement_accepted_at, engagement_accepted_ip + ) VALUES ( + $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18, + $19::jsonb,$20,$21,$22,$23,$24,$25,$26,$27,$28::text[], + $29,$30,$31,$32,$33,$34,$35 + ) RETURNING *`, + [ + orderNumber, + customer_email.toLowerCase().trim(), + customer_name.trim(), + customer_phone || null, + entity_source, + entity_legal_name || null, + entity_type || null, + formation_state ? formation_state.toUpperCase() : null, + ein || null, + frn || null, + contact_name.trim(), + contact_email.toLowerCase().trim(), + contact_phone || null, + contact_title || null, + address_street || null, + address_city || null, + address_state ? address_state.toUpperCase() : null, + address_zip || null, + JSON.stringify(wizardData), + includeFormation, + includeDcAgent, + includeRmd, + includeCpni, + includeCalea, + includeBdc, + includeStirShaken, + includeOcn, + statePucStates, + BASE_FEE_CENTS, + formationFeeCents, + stateFeeCents, + pucFeeCents, + addonFeeCents, + engagement_accepted ? new Date().toISOString() : null, + engagement_accepted ? (req.ip || req.headers["x-forwarded-for"] || null) : null, + ], + ); + + const order = result.rows[0]; + + // If formation needed, create a formation_orders row + if (includeFormation && formation_state) { + try { + const formationOrderNumber = `FO-${new Date().getFullYear()}-${randomBytes(3).toString("hex").toUpperCase()}`; + await pool.query( + `INSERT INTO formation_orders ( + order_number, customer_name, customer_email, customer_phone, + state_code, entity_type, entity_name, + principal_address, principal_city, principal_state, principal_zip, + service_fee_cents, state_fee_cents, payment_status + ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,'paid')`, + [ + formationOrderNumber, + customer_name.trim(), + customer_email.toLowerCase().trim(), + customer_phone || null, + formation_state.toUpperCase(), + entity_type || "llc", + entity_legal_name || null, + address_street || null, + address_city || null, + address_state ? address_state.toUpperCase() : null, + address_zip || null, + FORMATION_MARKUP_CENTS, + stateFeeCents, + ], + ); + // Link formation order to carrier registration + await pool.query( + `UPDATE fcc_carrier_registrations SET formation_order_number = $1 WHERE order_number = $2`, + [formationOrderNumber, orderNumber], + ); + } catch (formErr) { + console.warn("[fcc-carrier-reg] Formation order creation failed (non-fatal):", formErr); + } + } + + const totalCents = BASE_FEE_CENTS + formationFeeCents + stateFeeCents + pucFeeCents + addonFeeCents; + + console.log( + `[fcc-carrier-reg] Created ${orderNumber}: ${entity_source} for ${customer_email} — $${(totalCents / 100).toFixed(2)}`, + ); + + res.json({ + success: true, + order_number: orderNumber, + order_id: orderNumber, + order_type: "fcc_carrier_registration", + total_cents: totalCents, + pricing: { + base: BASE_FEE_CENTS, + formation: formationFeeCents + stateFeeCents, + addons: addonFeeCents, + puc: pucFeeCents, + total: totalCents, + }, + }); + } catch (err) { + console.error("[fcc-carrier-reg] Create error:", err); + res.status(500).json({ error: "Could not create order." }); + } +}); + +// ── GET /api/v1/fcc-carrier-registration/:id ──────────────────────────────── + +router.get("/api/v1/fcc-carrier-registration/:id", async (req: Request, res: Response) => { + try { + const { rows } = await pool.query( + `SELECT * FROM fcc_carrier_registrations WHERE order_number = $1`, + [req.params.id], + ); + if (rows.length === 0) { + res.status(404).json({ error: "Order not found" }); + return; + } + res.json(rows[0]); + } catch (err) { + console.error("[fcc-carrier-reg] Get error:", err); + res.status(500).json({ error: "Could not load order." }); + } +}); + +export default router; diff --git a/scripts/workers/job_server.py b/scripts/workers/job_server.py index af75a99..40bc1bc 100644 --- a/scripts/workers/job_server.py +++ b/scripts/workers/job_server.py @@ -1681,6 +1681,24 @@ def handle_import_499a(payload: dict) -> dict: return {"success": False, "error": str(exc)} +def handle_process_fcc_carrier_registration(payload: dict) -> dict: + """Process an FCC Carrier / ISP Registration pipeline order.""" + import asyncio + from scripts.workers.services.fcc_carrier_registration import FCCCarrierRegistrationHandler + + order_number = payload.get("order_number", "") + LOG.info("Processing FCC carrier registration: %s", order_number) + + handler = FCCCarrierRegistrationHandler() + loop = asyncio.new_event_loop() + try: + files = loop.run_until_complete(handler.process({"order_number": order_number})) + finally: + loop.close() + + return {"action": "process_fcc_carrier_registration", "files": files} + + JOB_HANDLERS = { "name_search": handle_name_search, "file_entity": handle_file_entity, @@ -1689,6 +1707,7 @@ JOB_HANDLERS = { "deliver": handle_deliver, "send_to_attorney": handle_send_to_attorney, "process_compliance_service": handle_process_compliance_service, + "process_fcc_carrier_registration": handle_process_fcc_carrier_registration, # Canada CRTC pipeline actions (dispatched by ERPNext webhooks) "register_ca_domain": handle_register_ca_domain, "register_awaiting_funds": handle_register_awaiting_funds, diff --git a/scripts/workers/services/__init__.py b/scripts/workers/services/__init__.py index 50353d5..f9a417a 100644 --- a/scripts/workers/services/__init__.py +++ b/scripts/workers/services/__init__.py @@ -41,6 +41,8 @@ from .new_carrier_bundle import NewCarrierBundleHandler from .foreign_qualification import ForeignQualificationHandler # State PUC/PSC registration across US states from .state_puc_filing import StatePucFilingHandler +# FCC Carrier / ISP Registration pipeline +from .fcc_carrier_registration import FCCCarrierRegistrationHandler SERVICE_HANDLERS: dict[str, type] = { "flsa-audit": FLSAAuditHandler, diff --git a/scripts/workers/services/fcc_carrier_registration.py b/scripts/workers/services/fcc_carrier_registration.py new file mode 100644 index 0000000..cdd6b6f --- /dev/null +++ b/scripts/workers/services/fcc_carrier_registration.py @@ -0,0 +1,317 @@ +"""FCC Carrier / ISP Registration pipeline handler. + +CRTC-style multi-step pipeline that orchestrates: + 1. Formation (optional — creates formation_order, waits for completion) + 2. CORES/FRN Registration + 3. Form 499 Initial + 4. D.C. Registered Agent + 5. State PUC Registrations (per selected state) + 6. RMD, CPNI, CALEA, BDC (as applicable) + 7. STIR/SHAKEN + OCN (if add-ons selected) + 8. Final review + client notification + +Each step checks its idempotency timestamp before running. +Reuses existing service handler logic where possible. +""" + +from __future__ import annotations + +import logging +import os +from datetime import datetime +from typing import Optional + +import psycopg2 +import psycopg2.extras + +logger = logging.getLogger(__name__) + +DATABASE_URL = os.environ.get("DATABASE_URL", "") + + +class FCCCarrierRegistrationHandler: + """Pipeline handler for FCC Carrier / ISP Registration orders.""" + + async def process(self, order_data: dict) -> list[str]: + order_number = order_data.get("order_number", "") + logger.info("FCCCarrierRegHandler: starting pipeline for %s", order_number) + + # Load order from PG + conn = psycopg2.connect(DATABASE_URL) + try: + with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: + cur.execute( + "SELECT * FROM fcc_carrier_registrations WHERE order_number = %s", + (order_number,), + ) + order = cur.fetchone() + finally: + conn.close() + + if not order: + logger.error("FCCCarrierRegHandler: order %s not found", order_number) + return [] + + generated: list[str] = [] + + # ── Step 1: Formation (if needed) ───────────────────────────────── + if order["include_formation"] and not order.get("formation_completed_at"): + formation_order = order.get("formation_order_number") + if formation_order: + # Check if formation is complete + conn = psycopg2.connect(DATABASE_URL) + try: + with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: + cur.execute( + "SELECT automation_status, entity_name FROM formation_orders WHERE order_number = %s", + (formation_order,), + ) + fo = cur.fetchone() + finally: + conn.close() + + if fo and fo.get("automation_status") in ("Delivered", "delivered"): + self._update_step(order_number, "formation_completed_at") + self._update_status(order_number, "formation_complete") + logger.info("FCCCarrierRegHandler: formation complete for %s", order_number) + else: + # Formation still in progress — create admin todo and pause + self._update_status(order_number, "awaiting_formation") + self._create_todo( + order_number, + f"FCC Carrier Registration {order_number} is waiting for formation " + f"order {formation_order} to complete. Dispatch the formation " + f"pipeline if not already running.", + ) + logger.info( + "FCCCarrierRegHandler: %s waiting for formation %s", + order_number, formation_order, + ) + return [] + else: + logger.warning( + "FCCCarrierRegHandler: %s needs formation but no formation_order_number", + order_number, + ) + self._create_todo( + order_number, + f"FCC Carrier Registration {order_number} needs formation but " + f"formation_order_number is missing. Create one manually.", + ) + return [] + + # ── Step 2: CORES/FRN ───────────────────────────────────────────── + if not order.get("cores_completed_at"): + self._update_status(order_number, "cores_registration") + self._create_todo( + order_number, + f"FCC Carrier Registration {order_number}\n\n" + f"Step 2: Register with FCC CORES and obtain FRN.\n" + f"Entity: {order.get('entity_legal_name', '?')}\n" + f"EIN: {order.get('ein', 'N/A')}\n" + f"Contact: {order.get('contact_name', '')} ({order.get('contact_email', '')})\n" + f"Address: {order.get('address_street', '')}, {order.get('address_city', '')} " + f"{order.get('address_state', '')} {order.get('address_zip', '')}\n\n" + f"After obtaining FRN, update the order:\n" + f" UPDATE fcc_carrier_registrations SET frn_obtained = 'XXXXXXXXXX', " + f"cores_completed_at = NOW() WHERE order_number = '{order_number}'", + ) + logger.info("FCCCarrierRegHandler: %s queued for CORES registration", order_number) + return generated + + # ── Step 3: Form 499 Initial ────────────────────────────────────── + if not order.get("form_499_completed_at"): + self._update_status(order_number, "form_499_initial") + self._create_todo( + order_number, + f"FCC Carrier Registration {order_number}\n\n" + f"Step 3: File Form 499 Initial Registration at USAC E-File.\n" + f"FRN: {order.get('frn_obtained') or order.get('frn', 'PENDING')}\n" + f"Entity: {order.get('entity_legal_name', '?')}\n\n" + f"After obtaining Filer ID, update the order:\n" + f" UPDATE fcc_carrier_registrations SET filer_id_obtained = 'XXXXXX', " + f"form_499_completed_at = NOW() WHERE order_number = '{order_number}'", + ) + return generated + + # ── Step 4: D.C. Registered Agent ───────────────────────────────── + if order["include_dc_agent"] and not order.get("dc_agent_completed_at"): + self._create_todo( + order_number, + f"FCC Carrier Registration {order_number}\n\n" + f"Step 4: Place D.C. Registered Agent wholesale order with Northwest.\n" + f"Entity: {order.get('entity_legal_name', '?')}\n" + f"FRN: {order.get('frn_obtained') or order.get('frn', '')}\n\n" + f"After placing NW order, update:\n" + f" UPDATE fcc_carrier_registrations SET dc_agent_completed_at = NOW() " + f"WHERE order_number = '{order_number}'", + ) + self._update_step(order_number, "dc_agent_completed_at") + + # ── Step 5: State PUC Registrations ─────────────────────────────── + puc_states = order.get("state_puc_states") or [] + if puc_states and not order.get("state_puc_completed_at"): + self._update_status(order_number, "state_registrations") + self._create_todo( + order_number, + f"FCC Carrier Registration {order_number}\n\n" + f"Step 5: State PUC registrations for: {', '.join(puc_states)}\n" + f"Entity: {order.get('entity_legal_name', '?')}\n\n" + f"Create individual state_puc_registrations rows or handle manually.\n" + f"After completing all states, update:\n" + f" UPDATE fcc_carrier_registrations SET state_puc_completed_at = NOW() " + f"WHERE order_number = '{order_number}'", + ) + return generated + + # ── Step 6: Compliance filings (RMD, CPNI, CALEA, BDC) ─────────── + self._update_status(order_number, "compliance_filings") + compliance_todos = [] + + if order["include_rmd"] and not order.get("rmd_completed_at"): + compliance_todos.append("RMD Registration") + if order["include_cpni"] and not order.get("cpni_completed_at"): + compliance_todos.append("CPNI Certification") + if order["include_calea"] and not order.get("calea_completed_at"): + compliance_todos.append("CALEA SSI Plan") + if order["include_bdc"] and not order.get("bdc_completed_at"): + compliance_todos.append("BDC Filing") + + if compliance_todos: + self._create_todo( + order_number, + f"FCC Carrier Registration {order_number}\n\n" + f"Step 6: Compliance filings needed:\n" + + "\n".join(f" - {t}" for t in compliance_todos) + + f"\n\nEntity: {order.get('entity_legal_name', '?')}\n" + f"FRN: {order.get('frn_obtained') or order.get('frn', '')}\n" + f"Filer ID: {order.get('filer_id_obtained') or order.get('filer_id_499', '')}\n\n" + f"Process each filing, then update the corresponding *_completed_at timestamp.", + ) + + # ── Step 7: Optional add-ons ────────────────────────────────────── + if order["include_stir_shaken"] and not order.get("stir_shaken_completed_at"): + self._create_todo( + order_number, + f"FCC Carrier Registration {order_number}\n\n" + f"Step 7a: STIR/SHAKEN Implementation\n" + f"Entity: {order.get('entity_legal_name', '?')}\n" + f"FRN: {order.get('frn_obtained') or order.get('frn', '')}", + ) + + if order["include_ocn"] and not order.get("ocn_completed_at"): + self._create_todo( + order_number, + f"FCC Carrier Registration {order_number}\n\n" + f"Step 7b: NECA OCN Registration\n" + f"Entity: {order.get('entity_legal_name', '?')}\n" + f"FRN: {order.get('frn_obtained') or order.get('frn', '')}", + ) + + # ── Step 8: Final review ────────────────────────────────────────── + all_done = ( + (not order["include_rmd"] or order.get("rmd_completed_at")) + and (not order["include_cpni"] or order.get("cpni_completed_at")) + and (not order["include_calea"] or order.get("calea_completed_at")) + and (not order["include_bdc"] or order.get("bdc_completed_at")) + and (not order["include_stir_shaken"] or order.get("stir_shaken_completed_at")) + and (not order["include_ocn"] or order.get("ocn_completed_at")) + ) + + if all_done: + self._update_status(order_number, "review") + self._create_todo( + order_number, + f"FCC Carrier Registration {order_number} — ALL STEPS COMPLETE\n\n" + f"Entity: {order.get('entity_legal_name', '?')}\n" + f"FRN: {order.get('frn_obtained') or order.get('frn', '')}\n" + f"Filer ID: {order.get('filer_id_obtained') or order.get('filer_id_499', '')}\n\n" + f"Review all filings and send the client a completion summary.\n" + f"Then mark as delivered:\n" + f" UPDATE fcc_carrier_registrations SET status = 'delivered' " + f"WHERE order_number = '{order_number}'", + priority="High", + ) + # Send client notification + self._send_status_email(order, "Your FCC carrier registration is nearing completion. Our team is doing a final review.") + + return generated + + # ── Helpers ─────────────────────────────────────────────────────────── + + def _update_status(self, order_number: str, status: str) -> None: + try: + conn = psycopg2.connect(DATABASE_URL) + with conn.cursor() as cur: + cur.execute( + "UPDATE fcc_carrier_registrations SET status = %s WHERE order_number = %s", + (status, order_number), + ) + conn.commit() + conn.close() + except Exception as exc: + logger.warning("Could not update status for %s: %s", order_number, exc) + + def _update_step(self, order_number: str, column: str) -> None: + try: + conn = psycopg2.connect(DATABASE_URL) + with conn.cursor() as cur: + cur.execute( + f"UPDATE fcc_carrier_registrations SET {column} = NOW() WHERE order_number = %s", + (order_number,), + ) + conn.commit() + conn.close() + except Exception as exc: + logger.warning("Could not update step %s for %s: %s", column, order_number, exc) + + def _create_todo(self, order_number: str, description: str, priority: str = "Medium") -> None: + try: + from scripts.workers.erpnext_client import ERPNextClient + ERPNextClient().create_resource("ToDo", { + "description": f"[fcc-carrier-reg] {order_number}\n\n{description}", + "priority": priority, + "role": "Accounting Advisor", + }) + except Exception as exc: + logger.error("Could not create admin ToDo: %s", exc) + + def _send_status_email(self, order: dict, message: str) -> None: + try: + import smtplib + from email.mime.text import MIMEText + from email.mime.multipart import MIMEMultipart + + email = order.get("customer_email", "") + name = order.get("customer_name", "") + if not email: + return + + first = name.split(" ")[0] if name else "there" + subject = f"FCC Carrier Registration Update — {order.get('order_number', '')}" + body = ( + f"

Registration Update

" + f"

Hi {first},

" + f"

{message}

" + f"

Entity: {order.get('entity_legal_name', '')}

" + f"

Order: {order.get('order_number', '')}

" + f"

Performance West Inc. | 1-888-411-0383

" + ) + + msg = MIMEMultipart("alternative") + msg["Subject"] = subject + msg["From"] = os.environ.get("SMTP_FROM", "Performance West ") + msg["To"] = email + msg.attach(MIMEText(body, "html")) + + smtp_host = os.environ.get("SMTP_HOST", "co.carrierone.com") + smtp_port = int(os.environ.get("SMTP_PORT", "587")) + smtp_user = os.environ.get("SMTP_USER", "") + smtp_pass = os.environ.get("SMTP_PASS", "") + with smtplib.SMTP(smtp_host, smtp_port) as server: + server.starttls() + if smtp_user and smtp_pass: + server.login(smtp_user, smtp_pass) + server.send_message(msg) + except Exception as exc: + logger.warning("Could not send status email: %s", exc) diff --git a/site/public/services/telecom/ipes-isp/index.html b/site/public/services/telecom/ipes-isp/index.html index f4e5969..d9b63af 100644 --- a/site/public/services/telecom/ipes-isp/index.html +++ b/site/public/services/telecom/ipes-isp/index.html @@ -17,7 +17,7 @@ Sign out $1299 — Per Registration Package Turnaround: 2-3 weeks

Whether you operate a VoIP phone service (known to the FCC as an IPES — Internet Protocol Enabled Service), a broadband ISP, or both, you need to register with multiple federal and state agencies before you can legally offer service. The FCC, USAC, state public utility commissions, and industry databases all require separate filings and ongoing compliance. -Performance West handles the complete registration process for VoIP carriers, broadband ISPs, CLECs, and other telecom providers. We register you with the FCC's CORES system, obtain your FCC Registration Number (FRN), file your initial Form 499, handle state PUC/PSC registrations, and set up your NECA and industry database entries. We also manage ongoing compliance requirements including annual certifications, RMD filings, and CPNI obligations.

Risk if non-compliant

Operating without proper registration can result in FCC enforcement action, state regulatory penalties, and inability to obtain numbering resources or interconnection agreements.

Potential penalties

  • FCC enforcement actions and fines
  • State regulatory penalties
  • Denial of numbering resources
  • Loss of interconnection agreements
  • Ineligibility for USF support

What we deliver

  • Register with FCC CORES and obtain FRN
  • File FCC Form 477 broadband data collection
  • Complete state PUC/PSC registrations
  • Register with NECA and industry databases
  • Obtain required numbering resources
  • Handle annual certification renewals

Frequently asked questions

What registrations do I need as a VoIP provider?

VoIP providers typically need FCC registration (FRN), state registrations, NECA membership, and various database entries. We assess your specific requirements.

Do I need to register in every state I serve?

Generally yes. Most states require telecommunications providers to register with their public utility commission, even for VoIP-only services.

How long do registrations take?

Federal registrations are typically processed within 1-2 weeks. State registrations vary from 2 weeks to 3 months depending on the state.

Do you handle renewals?

Yes, we track all renewal dates and handle annual certifications and updates for all your registrations.

Ready to get started?

Contact us to discuss your compliance needs or request a quote.

Risk if non-compliant

Operating without proper registration can result in FCC enforcement action, state regulatory penalties, and inability to obtain numbering resources or interconnection agreements.

Potential penalties

  • FCC enforcement actions and fines
  • State regulatory penalties
  • Denial of numbering resources
  • Loss of interconnection agreements
  • Ineligibility for USF support

What we deliver

  • Register with FCC CORES and obtain FRN
  • File FCC Form 477 broadband data collection
  • Complete state PUC/PSC registrations
  • Register with NECA and industry databases
  • Obtain required numbering resources
  • Handle annual certification renewals

Frequently asked questions

What registrations do I need as a VoIP provider?

VoIP providers typically need FCC registration (FRN), state registrations, NECA membership, and various database entries. We assess your specific requirements.

Do I need to register in every state I serve?

Generally yes. Most states require telecommunications providers to register with their public utility commission, even for VoIP-only services.

How long do registrations take?

Federal registrations are typically processed within 1-2 weeks. State registrations vary from 2 weeks to 3 months depending on the state.

Do you handle renewals?

Yes, we track all renewal dates and handle annual certifications and updates for all your registrations.