Includes: API (Express/TypeScript), Astro site, Python workers, document generators, FCC compliance tools, Canada CRTC formation, Ansible infrastructure, and deployment scripts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
309 lines
12 KiB
Text
309 lines
12 KiB
Text
---
|
|
// ClassificationWizard — Guided Q&A to determine FCC Line 105 carrier category.
|
|
//
|
|
// Replaces the raw Line 105 multi-select with plain-English questions.
|
|
// Outputs the same data: line_105_primary + line_105_categories.
|
|
---
|
|
|
|
<div class="pw-step">
|
|
<h2>What type of carrier are you?</h2>
|
|
<p class="pw-help">
|
|
Answer a few questions about your services and we'll determine the correct FCC classification.
|
|
This affects your 499-A filing categories, USF obligations, and compliance requirements.
|
|
</p>
|
|
|
|
<div id="cw-questions"></div>
|
|
|
|
<div id="cw-result" style="display:none;margin-top:1.5rem">
|
|
<div style="padding:1rem;background:#f0fdf4;border:1px solid #86efac;border-radius:8px">
|
|
<p style="margin:0 0 .5rem;font-size:.85rem;font-weight:700;color:#166534">Your classification:</p>
|
|
<p id="cw-primary" style="margin:0 0 .25rem;font-size:1rem;font-weight:700;color:#111"></p>
|
|
<p id="cw-reason" style="margin:0;font-size:.8rem;color:#166534"></p>
|
|
<div id="cw-secondary" style="margin-top:.5rem;font-size:.8rem;color:#374151"></div>
|
|
</div>
|
|
<p style="font-size:.75rem;color:#94a3b8;margin-top:.5rem">
|
|
This is our recommendation based on your answers. You can change it on the next step if needed.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.pw-step h2 { margin: 0 0 0.5rem; color: #1a2744; }
|
|
.pw-help { color: #64748b; font-size: 0.9rem; margin-bottom: 1.25rem; }
|
|
.cw-q { margin-bottom: 1.25rem; animation: fadeIn 0.2s ease-in; }
|
|
.cw-q-text { font-size: .95rem; font-weight: 600; color: #1a2744; margin-bottom: .5rem; }
|
|
.cw-q-hint { font-size: .8rem; color: #64748b; margin-bottom: .5rem; }
|
|
.cw-opts { display: flex; flex-wrap: wrap; gap: .5rem; }
|
|
.cw-opt { padding: .5rem 1rem; border: 2px solid #d1d5db; border-radius: 8px; cursor: pointer;
|
|
font-size: .85rem; font-weight: 500; color: #374151; background: #fff; transition: all .15s; }
|
|
.cw-opt:hover { border-color: #1e3a5f; background: #f0f4f8; }
|
|
.cw-opt.selected { border-color: #059669; background: #f0fdf4; color: #166534; font-weight: 700; }
|
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
|
</style>
|
|
|
|
<script>
|
|
// Decision tree for carrier classification
|
|
const QUESTIONS = [
|
|
{
|
|
id: "voice",
|
|
text: "Do you provide voice telephone service (VoIP, landline, or wireless)?",
|
|
hint: "This includes any service that connects calls to the public telephone network (PSTN).",
|
|
options: [
|
|
{ label: "Yes, over the internet (VoIP)", value: "voip" },
|
|
{ label: "Yes, over our own network (copper/fiber/wireless)", value: "facilities" },
|
|
{ label: "Yes, we resell another carrier's voice service", value: "reseller" },
|
|
{ label: "No, we don't provide voice service", value: "no" },
|
|
],
|
|
},
|
|
{
|
|
id: "broadband",
|
|
text: "Do you provide broadband internet access to end users?",
|
|
hint: "Cable, fiber, fixed wireless, DSL, or satellite internet.",
|
|
options: [
|
|
{ label: "Yes, over our own infrastructure", value: "facilities" },
|
|
{ label: "Yes, we resell another provider's broadband", value: "resale" },
|
|
{ label: "No", value: "no" },
|
|
],
|
|
},
|
|
{
|
|
id: "switching",
|
|
text: "Do you own or operate telephone switching equipment?",
|
|
hint: "Softswitch, SBC, Class 5 switch, or media gateway that routes calls.",
|
|
options: [
|
|
{ label: "Yes", value: "yes" },
|
|
{ label: "No, we use a platform provider (UCaaS, hosted PBX)", value: "no" },
|
|
],
|
|
showIf: (a: any) => a.voice !== "no",
|
|
},
|
|
{
|
|
id: "local_exchange",
|
|
text: "Do you provide local telephone service in a specific geographic area?",
|
|
hint: "Local dial tone, local phone numbers assigned to your customers.",
|
|
options: [
|
|
{ label: "Yes", value: "yes" },
|
|
{ label: "No", value: "no" },
|
|
],
|
|
showIf: (a: any) => a.voice === "facilities" || (a.voice === "voip" && a.switching === "yes"),
|
|
},
|
|
{
|
|
id: "long_distance",
|
|
text: "Do you provide long-distance or interstate calling?",
|
|
hint: "Toll calls between states, or international calling.",
|
|
options: [
|
|
{ label: "Yes", value: "yes" },
|
|
{ label: "No", value: "no" },
|
|
],
|
|
showIf: (a: any) => a.voice !== "no",
|
|
},
|
|
{
|
|
id: "wireless",
|
|
text: "Do you provide wireless (cellular) service?",
|
|
options: [
|
|
{ label: "Yes, we operate our own wireless network", value: "facilities" },
|
|
{ label: "Yes, as an MVNO (reselling another carrier's network)", value: "mvno" },
|
|
{ label: "No", value: "no" },
|
|
],
|
|
showIf: (a: any) => a.voice === "facilities",
|
|
},
|
|
{
|
|
id: "wholesale",
|
|
text: "Do you sell services primarily to other carriers (wholesale)?",
|
|
options: [
|
|
{ label: "Yes, mostly wholesale", value: "yes" },
|
|
{ label: "No, mostly retail (end users)", value: "no" },
|
|
{ label: "Both", value: "both" },
|
|
],
|
|
showIf: (a: any) => a.voice !== "no",
|
|
},
|
|
{
|
|
id: "toll_free",
|
|
text: "Do you provide toll-free numbers (800, 888, etc.) to customers?",
|
|
options: [
|
|
{ label: "Yes", value: "yes" },
|
|
{ label: "No", value: "no" },
|
|
],
|
|
showIf: (a: any) => a.voice !== "no",
|
|
},
|
|
];
|
|
|
|
function classify(answers: Record<string, string>): { primary: string; categories: string[]; reason: string } {
|
|
const cats: string[] = [];
|
|
let primary = "";
|
|
let reason = "";
|
|
|
|
// No voice service
|
|
if (answers.voice === "no") {
|
|
if (answers.broadband === "facilities") {
|
|
primary = "broadband_isp";
|
|
reason = "You provide broadband over your own infrastructure without voice. You are primarily an ISP.";
|
|
cats.push("broadband_isp");
|
|
} else if (answers.broadband === "resale") {
|
|
primary = "broadband_reseller";
|
|
reason = "You resell broadband without providing voice service. You may not need to file 499-A.";
|
|
cats.push("broadband_reseller");
|
|
} else {
|
|
primary = "other";
|
|
reason = "Based on your answers, you don't appear to be a telecommunications carrier. Contact us if you're unsure.";
|
|
}
|
|
return { primary, categories: cats, reason };
|
|
}
|
|
|
|
// VoIP provider
|
|
if (answers.voice === "voip") {
|
|
primary = "interconnected_voip";
|
|
reason = "You provide VoIP service connected to the PSTN. This is the most common category for modern voice carriers.";
|
|
cats.push("interconnected_voip");
|
|
if (answers.switching === "yes" && answers.local_exchange === "yes") {
|
|
cats.push("clec");
|
|
reason += " You also provide local exchange service with your own switching, making you a CLEC as well.";
|
|
}
|
|
}
|
|
|
|
// Facilities-based voice
|
|
if (answers.voice === "facilities") {
|
|
if (answers.wireless === "facilities") {
|
|
primary = "cmrs";
|
|
reason = "You operate your own wireless network. You are a Commercial Mobile Radio Service (CMRS) provider.";
|
|
cats.push("cmrs");
|
|
} else if (answers.wireless === "mvno") {
|
|
primary = "cmrs_reseller";
|
|
reason = "You resell wireless service as an MVNO.";
|
|
cats.push("cmrs_reseller");
|
|
} else if (answers.local_exchange === "yes") {
|
|
primary = "clec";
|
|
reason = "You provide local telephone service over your own facilities. You are a Competitive Local Exchange Carrier (CLEC).";
|
|
cats.push("clec");
|
|
} else {
|
|
primary = "interconnected_voip";
|
|
reason = "You provide voice service over your own facilities.";
|
|
cats.push("interconnected_voip");
|
|
}
|
|
}
|
|
|
|
// Voice reseller
|
|
if (answers.voice === "reseller") {
|
|
primary = "voip_reseller";
|
|
reason = "You resell another carrier's voice service. You are a VoIP or voice reseller.";
|
|
cats.push("voip_reseller");
|
|
}
|
|
|
|
// IXC if long distance
|
|
if (answers.long_distance === "yes" && !cats.includes("cmrs")) {
|
|
cats.push("ixc");
|
|
if (!primary || primary === "interconnected_voip") {
|
|
// Only make IXC primary if they're primarily long-distance
|
|
}
|
|
}
|
|
|
|
// Toll-free
|
|
if (answers.toll_free === "yes") {
|
|
cats.push("toll_free_provider");
|
|
}
|
|
|
|
// Broadband
|
|
if (answers.broadband === "facilities") {
|
|
cats.push("broadband_isp");
|
|
}
|
|
|
|
// Wholesale
|
|
if (answers.wholesale === "yes" || answers.wholesale === "both") {
|
|
cats.push("wholesale");
|
|
}
|
|
|
|
if (!primary) primary = cats[0] || "other";
|
|
return { primary, categories: [...new Set(cats)], reason };
|
|
}
|
|
|
|
// Render Q&A
|
|
const container = document.getElementById("cw-questions")!;
|
|
const answers: Record<string, string> = {};
|
|
let questionIndex = 0;
|
|
|
|
function renderQuestion(idx: number) {
|
|
// Find the next applicable question
|
|
while (idx < QUESTIONS.length) {
|
|
const q = QUESTIONS[idx];
|
|
if (!q.showIf || q.showIf(answers)) break;
|
|
idx++;
|
|
}
|
|
if (idx >= QUESTIONS.length) {
|
|
showResult();
|
|
return;
|
|
}
|
|
|
|
questionIndex = idx;
|
|
const q = QUESTIONS[idx];
|
|
const div = document.createElement("div");
|
|
div.className = "cw-q";
|
|
div.innerHTML = `
|
|
<p class="cw-q-text">${q.text}</p>
|
|
${q.hint ? `<p class="cw-q-hint">${q.hint}</p>` : ""}
|
|
<div class="cw-opts">
|
|
${q.options.map((o) => `<button type="button" class="cw-opt" data-value="${o.value}">${o.label}</button>`).join("")}
|
|
</div>
|
|
`;
|
|
container.appendChild(div);
|
|
|
|
div.querySelectorAll(".cw-opt").forEach((btn) => {
|
|
btn.addEventListener("click", () => {
|
|
answers[q.id] = (btn as HTMLElement).dataset.value!;
|
|
// Highlight selected
|
|
div.querySelectorAll(".cw-opt").forEach((b) => b.classList.remove("selected"));
|
|
btn.classList.add("selected");
|
|
// Next question after brief delay
|
|
setTimeout(() => renderQuestion(idx + 1), 300);
|
|
});
|
|
});
|
|
}
|
|
|
|
function showResult() {
|
|
const result = classify(answers);
|
|
document.getElementById("cw-result")!.style.display = "block";
|
|
|
|
const labels: Record<string, string> = {
|
|
interconnected_voip: "Interconnected VoIP Provider",
|
|
clec: "Competitive Local Exchange Carrier (CLEC)",
|
|
ixc: "Interexchange Carrier (IXC)",
|
|
cmrs: "Commercial Mobile Radio Service (CMRS)",
|
|
cmrs_reseller: "CMRS Reseller (MVNO)",
|
|
voip_reseller: "VoIP Reseller",
|
|
broadband_isp: "Broadband Internet Service Provider (ISP)",
|
|
broadband_reseller: "Broadband Reseller",
|
|
toll_free_provider: "Toll-Free Provider",
|
|
wholesale: "Wholesale Provider",
|
|
other: "Other / Not Classified",
|
|
};
|
|
|
|
document.getElementById("cw-primary")!.textContent = labels[result.primary] || result.primary;
|
|
document.getElementById("cw-reason")!.textContent = result.reason;
|
|
|
|
const secondary = result.categories.filter((c) => c !== result.primary);
|
|
const secEl = document.getElementById("cw-secondary")!;
|
|
if (secondary.length) {
|
|
secEl.innerHTML = "<strong>Additional categories:</strong> " + secondary.map((c) => labels[c] || c).join(", ");
|
|
}
|
|
|
|
// Save to intake data
|
|
const PW = (window as any).PWIntake;
|
|
if (PW) {
|
|
PW.patchIntakeData({
|
|
carrier_classification_answers: answers,
|
|
line_105_primary: result.primary,
|
|
line_105_categories: result.categories,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Start
|
|
window.addEventListener("pw:step-shown", (evt: any) => {
|
|
if (evt.detail.step !== "classification") return;
|
|
// Reset if re-entering
|
|
container.innerHTML = "";
|
|
Object.keys(answers).forEach((k) => delete answers[k]);
|
|
document.getElementById("cw-result")!.style.display = "none";
|
|
renderQuestion(0);
|
|
});
|
|
|
|
// Also start immediately if this is the current step
|
|
renderQuestion(0);
|
|
</script>
|