Initial commit — Performance West telecom compliance platform
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>
This commit is contained in:
commit
f8cd37ac8c
1823 changed files with 145167 additions and 0 deletions
41
site/src/pages/order/bdc-broadband.astro
Normal file
41
site/src/pages/order/bdc-broadband.astro
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import Wizard from "../../components/intake/Wizard.astro";
|
||||
import TaxDeductibilityNotice from "../../components/TaxDeductibilityNotice.astro";
|
||||
import { INTAKE_MANIFEST, SERVICE_META, formatUSD } from "../../lib/intake_manifest";
|
||||
|
||||
const slug = "bdc-broadband";
|
||||
const steps = INTAKE_MANIFEST[slug];
|
||||
const meta = SERVICE_META[slug];
|
||||
const title = meta ? `Order ${meta.name}` : "Order";
|
||||
const description = "Availability data only (no voice subscription). For broadband-focused ISPs and fixed wireless providers.";
|
||||
---
|
||||
|
||||
<Base title={title} description={description}>
|
||||
<main>
|
||||
<section class="pw-order-intro">
|
||||
<h1>{meta?.name}</h1>
|
||||
<p class="pw-price" id="pw-price">{formatUSD(meta?.price_cents ?? 0)}</p>
|
||||
<div id="pw-tax-notice"><TaxDeductibilityNotice /></div>
|
||||
<p class="pw-desc">{description}</p>
|
||||
</section>
|
||||
|
||||
<Wizard service_slug={slug} steps={steps ?? ["entity", "review", "payment"]} title={meta?.name ?? slug} />
|
||||
</main>
|
||||
<script>
|
||||
if (new URLSearchParams(window.location.search).has("token")) {
|
||||
const p = document.getElementById("pw-price");
|
||||
const t = document.getElementById("pw-tax-notice");
|
||||
if (p) p.style.display = "none";
|
||||
if (t) t.style.display = "none";
|
||||
}
|
||||
</script>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
||||
.pw-order-intro { margin-bottom: 1.5rem; }
|
||||
.pw-order-intro h1 { margin: 0 0 0.25rem; color: var(--pw-navy, #1a2744); }
|
||||
.pw-price { font-size: 1.5rem; font-weight: 700; color: var(--pw-green, #059669); margin: 0 0 0.75rem; }
|
||||
.pw-desc { color: var(--pw-muted, #64748b); max-width: 48rem; }
|
||||
</style>
|
||||
41
site/src/pages/order/bdc-filing.astro
Normal file
41
site/src/pages/order/bdc-filing.astro
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import Wizard from "../../components/intake/Wizard.astro";
|
||||
import TaxDeductibilityNotice from "../../components/TaxDeductibilityNotice.astro";
|
||||
import { INTAKE_MANIFEST, SERVICE_META, formatUSD } from "../../lib/intake_manifest";
|
||||
|
||||
const slug = "bdc-filing";
|
||||
const steps = INTAKE_MANIFEST[slug];
|
||||
const meta = SERVICE_META[slug];
|
||||
const title = meta ? `Order ${meta.name}` : "Order";
|
||||
const description = "Both BDC blocks in one order — broadband deployment + voice subscription. For carriers with both.";
|
||||
---
|
||||
|
||||
<Base title={title} description={description}>
|
||||
<main>
|
||||
<section class="pw-order-intro">
|
||||
<h1>{meta?.name}</h1>
|
||||
<p class="pw-price" id="pw-price">{formatUSD(meta?.price_cents ?? 0)}</p>
|
||||
<div id="pw-tax-notice"><TaxDeductibilityNotice /></div>
|
||||
<p class="pw-desc">{description}</p>
|
||||
</section>
|
||||
|
||||
<Wizard service_slug={slug} steps={steps ?? ["entity", "review", "payment"]} title={meta?.name ?? slug} />
|
||||
</main>
|
||||
<script>
|
||||
if (new URLSearchParams(window.location.search).has("token")) {
|
||||
const p = document.getElementById("pw-price");
|
||||
const t = document.getElementById("pw-tax-notice");
|
||||
if (p) p.style.display = "none";
|
||||
if (t) t.style.display = "none";
|
||||
}
|
||||
</script>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
||||
.pw-order-intro { margin-bottom: 1.5rem; }
|
||||
.pw-order-intro h1 { margin: 0 0 0.25rem; color: var(--pw-navy, #1a2744); }
|
||||
.pw-price { font-size: 1.5rem; font-weight: 700; color: var(--pw-green, #059669); margin: 0 0 0.75rem; }
|
||||
.pw-desc { color: var(--pw-muted, #64748b); max-width: 48rem; }
|
||||
</style>
|
||||
41
site/src/pages/order/bdc-voice.astro
Normal file
41
site/src/pages/order/bdc-voice.astro
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import Wizard from "../../components/intake/Wizard.astro";
|
||||
import TaxDeductibilityNotice from "../../components/TaxDeductibilityNotice.astro";
|
||||
import { INTAKE_MANIFEST, SERVICE_META, formatUSD } from "../../lib/intake_manifest";
|
||||
|
||||
const slug = "bdc-voice";
|
||||
const steps = INTAKE_MANIFEST[slug];
|
||||
const meta = SERVICE_META[slug];
|
||||
const title = meta ? `Order ${meta.name}` : "Order";
|
||||
const description = "Voice subscriber counts only — the part of the legacy Form 477 that now files through BDC. For VoIP resellers + CLECs.";
|
||||
---
|
||||
|
||||
<Base title={title} description={description}>
|
||||
<main>
|
||||
<section class="pw-order-intro">
|
||||
<h1>{meta?.name}</h1>
|
||||
<p class="pw-price" id="pw-price">{formatUSD(meta?.price_cents ?? 0)}</p>
|
||||
<div id="pw-tax-notice"><TaxDeductibilityNotice /></div>
|
||||
<p class="pw-desc">{description}</p>
|
||||
</section>
|
||||
|
||||
<Wizard service_slug={slug} steps={steps ?? ["entity", "review", "payment"]} title={meta?.name ?? slug} />
|
||||
</main>
|
||||
<script>
|
||||
if (new URLSearchParams(window.location.search).has("token")) {
|
||||
const p = document.getElementById("pw-price");
|
||||
const t = document.getElementById("pw-tax-notice");
|
||||
if (p) p.style.display = "none";
|
||||
if (t) t.style.display = "none";
|
||||
}
|
||||
</script>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
||||
.pw-order-intro { margin-bottom: 1.5rem; }
|
||||
.pw-order-intro h1 { margin: 0 0 0.25rem; color: var(--pw-navy, #1a2744); }
|
||||
.pw-price { font-size: 1.5rem; font-weight: 700; color: var(--pw-green, #059669); margin: 0 0 0.75rem; }
|
||||
.pw-desc { color: var(--pw-muted, #64748b); max-width: 48rem; }
|
||||
</style>
|
||||
41
site/src/pages/order/calea-ssi.astro
Normal file
41
site/src/pages/order/calea-ssi.astro
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import Wizard from "../../components/intake/Wizard.astro";
|
||||
import TaxDeductibilityNotice from "../../components/TaxDeductibilityNotice.astro";
|
||||
import { INTAKE_MANIFEST, SERVICE_META, formatUSD } from "../../lib/intake_manifest";
|
||||
|
||||
const slug = "calea-ssi";
|
||||
const steps = INTAKE_MANIFEST[slug];
|
||||
const meta = SERVICE_META[slug];
|
||||
const title = meta ? `Order ${meta.name}` : "Order";
|
||||
const description = "System Security and Integrity plan required of every common carrier under 47 USC § 229 / 47 CFR § 1.20003. Kept internally, provided to DOJ on subpoena.";
|
||||
---
|
||||
|
||||
<Base title={title} description={description}>
|
||||
<main>
|
||||
<section class="pw-order-intro">
|
||||
<h1>{meta?.name}</h1>
|
||||
<p class="pw-price" id="pw-price">{formatUSD(meta?.price_cents ?? 0)}</p>
|
||||
<div id="pw-tax-notice"><TaxDeductibilityNotice /></div>
|
||||
<p class="pw-desc">{description}</p>
|
||||
</section>
|
||||
|
||||
<Wizard service_slug={slug} steps={steps ?? ["entity", "review", "payment"]} title={meta?.name ?? slug} />
|
||||
</main>
|
||||
<script>
|
||||
if (new URLSearchParams(window.location.search).has("token")) {
|
||||
const p = document.getElementById("pw-price");
|
||||
const t = document.getElementById("pw-tax-notice");
|
||||
if (p) p.style.display = "none";
|
||||
if (t) t.style.display = "none";
|
||||
}
|
||||
</script>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
||||
.pw-order-intro { margin-bottom: 1.5rem; }
|
||||
.pw-order-intro h1 { margin: 0 0 0.25rem; color: var(--pw-navy, #1a2744); }
|
||||
.pw-price { font-size: 1.5rem; font-weight: 700; color: var(--pw-green, #059669); margin: 0 0 0.75rem; }
|
||||
.pw-desc { color: var(--pw-muted, #64748b); max-width: 48rem; }
|
||||
</style>
|
||||
41
site/src/pages/order/cdr-analysis.astro
Normal file
41
site/src/pages/order/cdr-analysis.astro
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import Wizard from "../../components/intake/Wizard.astro";
|
||||
import TaxDeductibilityNotice from "../../components/TaxDeductibilityNotice.astro";
|
||||
import { INTAKE_MANIFEST, SERVICE_META, formatUSD } from "../../lib/intake_manifest";
|
||||
|
||||
const slug = "cdr-analysis";
|
||||
const steps = INTAKE_MANIFEST[slug];
|
||||
const meta = SERVICE_META[slug];
|
||||
const title = meta ? `Order ${meta.name}` : "Order";
|
||||
const description = "Classified traffic study from your CDRs — feeds the 499-A with actual interstate/international percentages instead of the 64.9% safe harbor.";
|
||||
---
|
||||
|
||||
<Base title={title} description={description}>
|
||||
<main>
|
||||
<section class="pw-order-intro">
|
||||
<h1>{meta?.name}</h1>
|
||||
<p class="pw-price" id="pw-price">{formatUSD(meta?.price_cents ?? 0)}</p>
|
||||
<div id="pw-tax-notice"><TaxDeductibilityNotice /></div>
|
||||
<p class="pw-desc">{description}</p>
|
||||
</section>
|
||||
|
||||
<Wizard service_slug={slug} steps={steps ?? ["entity", "review", "payment"]} title={meta?.name ?? slug} />
|
||||
</main>
|
||||
<script>
|
||||
if (new URLSearchParams(window.location.search).has("token")) {
|
||||
const p = document.getElementById("pw-price");
|
||||
const t = document.getElementById("pw-tax-notice");
|
||||
if (p) p.style.display = "none";
|
||||
if (t) t.style.display = "none";
|
||||
}
|
||||
</script>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
||||
.pw-order-intro { margin-bottom: 1.5rem; }
|
||||
.pw-order-intro h1 { margin: 0 0 0.25rem; color: var(--pw-navy, #1a2744); }
|
||||
.pw-price { font-size: 1.5rem; font-weight: 700; color: var(--pw-green, #059669); margin: 0 0 0.75rem; }
|
||||
.pw-desc { color: var(--pw-muted, #64748b); max-width: 48rem; }
|
||||
</style>
|
||||
41
site/src/pages/order/cores-frn-registration.astro
Normal file
41
site/src/pages/order/cores-frn-registration.astro
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import Wizard from "../../components/intake/Wizard.astro";
|
||||
import TaxDeductibilityNotice from "../../components/TaxDeductibilityNotice.astro";
|
||||
import { INTAKE_MANIFEST, SERVICE_META, formatUSD } from "../../lib/intake_manifest";
|
||||
|
||||
const slug = "cores-frn-registration";
|
||||
const steps = INTAKE_MANIFEST[slug];
|
||||
const meta = SERVICE_META[slug];
|
||||
const title = meta ? `Order ${meta.name}` : "Order";
|
||||
const description = "Register your carrier in FCC CORES and obtain your FRN. Required before any other FCC filing.";
|
||||
---
|
||||
|
||||
<Base title={title} description={description}>
|
||||
<main>
|
||||
<section class="pw-order-intro">
|
||||
<h1>{meta?.name}</h1>
|
||||
<p class="pw-price" id="pw-price">{formatUSD(meta?.price_cents ?? 0)}</p>
|
||||
<div id="pw-tax-notice"><TaxDeductibilityNotice /></div>
|
||||
<p class="pw-desc">{description}</p>
|
||||
</section>
|
||||
|
||||
<Wizard service_slug={slug} steps={steps ?? ["entity", "review", "payment"]} title={meta?.name ?? slug} />
|
||||
</main>
|
||||
<script>
|
||||
if (new URLSearchParams(window.location.search).has("token")) {
|
||||
const p = document.getElementById("pw-price");
|
||||
const t = document.getElementById("pw-tax-notice");
|
||||
if (p) p.style.display = "none";
|
||||
if (t) t.style.display = "none";
|
||||
}
|
||||
</script>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
||||
.pw-order-intro { margin-bottom: 1.5rem; }
|
||||
.pw-order-intro h1 { margin: 0 0 0.25rem; color: var(--pw-navy, #1a2744); }
|
||||
.pw-price { font-size: 1.5rem; font-weight: 700; color: var(--pw-green, #059669); margin: 0 0 0.75rem; }
|
||||
.pw-desc { color: var(--pw-muted, #64748b); max-width: 48rem; }
|
||||
</style>
|
||||
41
site/src/pages/order/cpni-certification.astro
Normal file
41
site/src/pages/order/cpni-certification.astro
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import Wizard from "../../components/intake/Wizard.astro";
|
||||
import TaxDeductibilityNotice from "../../components/TaxDeductibilityNotice.astro";
|
||||
import { INTAKE_MANIFEST, SERVICE_META, formatUSD } from "../../lib/intake_manifest";
|
||||
|
||||
const slug = "cpni-certification";
|
||||
const steps = INTAKE_MANIFEST[slug];
|
||||
const meta = SERVICE_META[slug];
|
||||
const title = meta ? `Order ${meta.name}` : "Order";
|
||||
const description = "47 CFR § 64.2009 annual CPNI certification filed at FCC ECFS docket 06-36. Due March 1.";
|
||||
---
|
||||
|
||||
<Base title={title} description={description}>
|
||||
<main>
|
||||
<section class="pw-order-intro">
|
||||
<h1>{meta?.name}</h1>
|
||||
<p class="pw-price" id="pw-price">{formatUSD(meta?.price_cents ?? 0)}</p>
|
||||
<div id="pw-tax-notice"><TaxDeductibilityNotice /></div>
|
||||
<p class="pw-desc">{description}</p>
|
||||
</section>
|
||||
|
||||
<Wizard service_slug={slug} steps={steps ?? ["entity", "review", "payment"]} title={meta?.name ?? slug} />
|
||||
</main>
|
||||
<script>
|
||||
// Always hide price + tax on intake pages — if the user is here, they've
|
||||
// already paid via the order page or batch checkout.
|
||||
const price = document.getElementById("pw-price");
|
||||
const tax = document.getElementById("pw-tax-notice");
|
||||
if (price) price.style.display = "none";
|
||||
if (tax) tax.style.display = "none";
|
||||
</script>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
||||
.pw-order-intro { margin-bottom: 1.5rem; }
|
||||
.pw-order-intro h1 { margin: 0 0 0.25rem; color: var(--pw-navy, #1a2744); }
|
||||
.pw-price { font-size: 1.5rem; font-weight: 700; color: var(--pw-green, #059669); margin: 0 0 0.75rem; }
|
||||
.pw-desc { color: var(--pw-muted, #64748b); max-width: 48rem; }
|
||||
</style>
|
||||
41
site/src/pages/order/dc-agent.astro
Normal file
41
site/src/pages/order/dc-agent.astro
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import Wizard from "../../components/intake/Wizard.astro";
|
||||
import TaxDeductibilityNotice from "../../components/TaxDeductibilityNotice.astro";
|
||||
import { INTAKE_MANIFEST, SERVICE_META, formatUSD } from "../../lib/intake_manifest";
|
||||
|
||||
const slug = "dc-agent";
|
||||
const steps = INTAKE_MANIFEST[slug];
|
||||
const meta = SERVICE_META[slug];
|
||||
const title = meta ? `Order ${meta.name}` : "Order";
|
||||
const description = "Your required D.C. registered agent for service of process (47 CFR § 413). Uses Northwest Registered Agent wholesale.";
|
||||
---
|
||||
|
||||
<Base title={title} description={description}>
|
||||
<main>
|
||||
<section class="pw-order-intro">
|
||||
<h1>{meta?.name}</h1>
|
||||
<p class="pw-price" id="pw-price">{formatUSD(meta?.price_cents ?? 0)}</p>
|
||||
<div id="pw-tax-notice"><TaxDeductibilityNotice /></div>
|
||||
<p class="pw-desc">{description}</p>
|
||||
</section>
|
||||
|
||||
<Wizard service_slug={slug} steps={steps ?? ["entity", "review", "payment"]} title={meta?.name ?? slug} />
|
||||
</main>
|
||||
<script>
|
||||
if (new URLSearchParams(window.location.search).has("token")) {
|
||||
const p = document.getElementById("pw-price");
|
||||
const t = document.getElementById("pw-tax-notice");
|
||||
if (p) p.style.display = "none";
|
||||
if (t) t.style.display = "none";
|
||||
}
|
||||
</script>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
||||
.pw-order-intro { margin-bottom: 1.5rem; }
|
||||
.pw-order-intro h1 { margin: 0 0 0.25rem; color: var(--pw-navy, #1a2744); }
|
||||
.pw-price { font-size: 1.5rem; font-weight: 700; color: var(--pw-green, #059669); margin: 0 0 0.75rem; }
|
||||
.pw-desc { color: var(--pw-muted, #64748b); max-width: 48rem; }
|
||||
</style>
|
||||
41
site/src/pages/order/fcc-499-initial.astro
Normal file
41
site/src/pages/order/fcc-499-initial.astro
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import Wizard from "../../components/intake/Wizard.astro";
|
||||
import TaxDeductibilityNotice from "../../components/TaxDeductibilityNotice.astro";
|
||||
import { INTAKE_MANIFEST, SERVICE_META, formatUSD } from "../../lib/intake_manifest";
|
||||
|
||||
const slug = "fcc-499-initial";
|
||||
const steps = INTAKE_MANIFEST[slug];
|
||||
const meta = SERVICE_META[slug];
|
||||
const title = meta ? `Order ${meta.name}` : "Order";
|
||||
const description = "New carrier registration with USAC — obtain your Filer ID (812xxx) and unlock all future 499-A filings.";
|
||||
---
|
||||
|
||||
<Base title={title} description={description}>
|
||||
<main>
|
||||
<section class="pw-order-intro">
|
||||
<h1>{meta?.name}</h1>
|
||||
<p class="pw-price" id="pw-price">{formatUSD(meta?.price_cents ?? 0)}</p>
|
||||
<div id="pw-tax-notice"><TaxDeductibilityNotice /></div>
|
||||
<p class="pw-desc">{description}</p>
|
||||
</section>
|
||||
|
||||
<Wizard service_slug={slug} steps={steps ?? ["entity", "review", "payment"]} title={meta?.name ?? slug} />
|
||||
</main>
|
||||
<script>
|
||||
if (new URLSearchParams(window.location.search).has("token")) {
|
||||
const p = document.getElementById("pw-price");
|
||||
const t = document.getElementById("pw-tax-notice");
|
||||
if (p) p.style.display = "none";
|
||||
if (t) t.style.display = "none";
|
||||
}
|
||||
</script>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
||||
.pw-order-intro { margin-bottom: 1.5rem; }
|
||||
.pw-order-intro h1 { margin: 0 0 0.25rem; color: var(--pw-navy, #1a2744); }
|
||||
.pw-price { font-size: 1.5rem; font-weight: 700; color: var(--pw-green, #059669); margin: 0 0 0.75rem; }
|
||||
.pw-desc { color: var(--pw-muted, #64748b); max-width: 48rem; }
|
||||
</style>
|
||||
41
site/src/pages/order/fcc-499a-499q.astro
Normal file
41
site/src/pages/order/fcc-499a-499q.astro
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import Wizard from "../../components/intake/Wizard.astro";
|
||||
import TaxDeductibilityNotice from "../../components/TaxDeductibilityNotice.astro";
|
||||
import { INTAKE_MANIFEST, SERVICE_META, formatUSD } from "../../lib/intake_manifest";
|
||||
|
||||
const slug = "fcc-499a-499q";
|
||||
const steps = INTAKE_MANIFEST[slug];
|
||||
const meta = SERVICE_META[slug];
|
||||
const title = meta ? `Order ${meta.name}` : "Order";
|
||||
const description = "Annual 499-A plus the four quarterly 499-Q filings — one bundle, full year covered.";
|
||||
---
|
||||
|
||||
<Base title={title} description={description}>
|
||||
<main>
|
||||
<section class="pw-order-intro">
|
||||
<h1>{meta?.name}</h1>
|
||||
<p class="pw-price" id="pw-price">{formatUSD(meta?.price_cents ?? 0)}</p>
|
||||
<div id="pw-tax-notice"><TaxDeductibilityNotice /></div>
|
||||
<p class="pw-desc">{description}</p>
|
||||
</section>
|
||||
|
||||
<Wizard service_slug={slug} steps={steps ?? ["entity", "review", "payment"]} title={meta?.name ?? slug} />
|
||||
</main>
|
||||
<script>
|
||||
if (new URLSearchParams(window.location.search).has("token")) {
|
||||
const p = document.getElementById("pw-price");
|
||||
const t = document.getElementById("pw-tax-notice");
|
||||
if (p) p.style.display = "none";
|
||||
if (t) t.style.display = "none";
|
||||
}
|
||||
</script>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
||||
.pw-order-intro { margin-bottom: 1.5rem; }
|
||||
.pw-order-intro h1 { margin: 0 0 0.25rem; color: var(--pw-navy, #1a2744); }
|
||||
.pw-price { font-size: 1.5rem; font-weight: 700; color: var(--pw-green, #059669); margin: 0 0 0.75rem; }
|
||||
.pw-desc { color: var(--pw-muted, #64748b); max-width: 48rem; }
|
||||
</style>
|
||||
41
site/src/pages/order/fcc-499a.astro
Normal file
41
site/src/pages/order/fcc-499a.astro
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import Wizard from "../../components/intake/Wizard.astro";
|
||||
import TaxDeductibilityNotice from "../../components/TaxDeductibilityNotice.astro";
|
||||
import { INTAKE_MANIFEST, SERVICE_META, formatUSD } from "../../lib/intake_manifest";
|
||||
|
||||
const slug = "fcc-499a";
|
||||
const steps = INTAKE_MANIFEST[slug];
|
||||
const meta = SERVICE_META[slug];
|
||||
const title = meta ? `Order ${meta.name}` : "Order";
|
||||
const description = "Annual Telecommunications Reporting Worksheet. Due April 1 for the prior year's revenue.";
|
||||
---
|
||||
|
||||
<Base title={title} description={description}>
|
||||
<main>
|
||||
<section class="pw-order-intro">
|
||||
<h1>{meta?.name}</h1>
|
||||
<p class="pw-price" id="pw-price">{formatUSD(meta?.price_cents ?? 0)}</p>
|
||||
<div id="pw-tax-notice"><TaxDeductibilityNotice /></div>
|
||||
<p class="pw-desc">{description}</p>
|
||||
</section>
|
||||
|
||||
<Wizard service_slug={slug} steps={steps ?? ["entity", "review", "payment"]} title={meta?.name ?? slug} />
|
||||
</main>
|
||||
<script>
|
||||
if (new URLSearchParams(window.location.search).has("token")) {
|
||||
const p = document.getElementById("pw-price");
|
||||
const t = document.getElementById("pw-tax-notice");
|
||||
if (p) p.style.display = "none";
|
||||
if (t) t.style.display = "none";
|
||||
}
|
||||
</script>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
||||
.pw-order-intro { margin-bottom: 1.5rem; }
|
||||
.pw-order-intro h1 { margin: 0 0 0.25rem; color: var(--pw-navy, #1a2744); }
|
||||
.pw-price { font-size: 1.5rem; font-weight: 700; color: var(--pw-green, #059669); margin: 0 0 0.75rem; }
|
||||
.pw-desc { color: var(--pw-muted, #64748b); max-width: 48rem; }
|
||||
</style>
|
||||
41
site/src/pages/order/fcc-63-11-notification.astro
Normal file
41
site/src/pages/order/fcc-63-11-notification.astro
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import Wizard from "../../components/intake/Wizard.astro";
|
||||
import TaxDeductibilityNotice from "../../components/TaxDeductibilityNotice.astro";
|
||||
import { INTAKE_MANIFEST, SERVICE_META, formatUSD } from "../../lib/intake_manifest";
|
||||
|
||||
const slug = "fcc-63-11-notification";
|
||||
const steps = INTAKE_MANIFEST[slug];
|
||||
const meta = SERVICE_META[slug];
|
||||
const title = meta ? `Order ${meta.name}` : "Order";
|
||||
const description = "47 CFR § 63.11 notification filed with the FCC International Bureau. Required when U.S. carriers are affiliated with a foreign carrier serving the same route.";
|
||||
---
|
||||
|
||||
<Base title={title} description={description}>
|
||||
<main>
|
||||
<section class="pw-order-intro">
|
||||
<h1>{meta?.name}</h1>
|
||||
<p class="pw-price" id="pw-price">{formatUSD(meta?.price_cents ?? 0)}</p>
|
||||
<div id="pw-tax-notice"><TaxDeductibilityNotice /></div>
|
||||
<p class="pw-desc">{description}</p>
|
||||
</section>
|
||||
|
||||
<Wizard service_slug={slug} steps={steps ?? ["entity", "review", "payment"]} title={meta?.name ?? slug} />
|
||||
</main>
|
||||
<script>
|
||||
if (new URLSearchParams(window.location.search).has("token")) {
|
||||
const p = document.getElementById("pw-price");
|
||||
const t = document.getElementById("pw-tax-notice");
|
||||
if (p) p.style.display = "none";
|
||||
if (t) t.style.display = "none";
|
||||
}
|
||||
</script>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
||||
.pw-order-intro { margin-bottom: 1.5rem; }
|
||||
.pw-order-intro h1 { margin: 0 0 0.25rem; color: var(--pw-navy, #1a2744); }
|
||||
.pw-price { font-size: 1.5rem; font-weight: 700; color: var(--pw-green, #059669); margin: 0 0 0.75rem; }
|
||||
.pw-desc { color: var(--pw-muted, #64748b); max-width: 48rem; }
|
||||
</style>
|
||||
41
site/src/pages/order/fcc-compliance-checkup.astro
Normal file
41
site/src/pages/order/fcc-compliance-checkup.astro
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import Wizard from "../../components/intake/Wizard.astro";
|
||||
import TaxDeductibilityNotice from "../../components/TaxDeductibilityNotice.astro";
|
||||
import { INTAKE_MANIFEST, SERVICE_META, formatUSD } from "../../lib/intake_manifest";
|
||||
|
||||
const slug = "fcc-compliance-checkup";
|
||||
const steps = INTAKE_MANIFEST[slug];
|
||||
const meta = SERVICE_META[slug];
|
||||
const title = meta ? `Order ${meta.name}` : "Order";
|
||||
const description = "Diagnostic check — CORES, RMD, STIR/SHAKEN, CPNI, 499-A status. No filing, just a report.";
|
||||
---
|
||||
|
||||
<Base title={title} description={description}>
|
||||
<main>
|
||||
<section class="pw-order-intro">
|
||||
<h1>{meta?.name}</h1>
|
||||
<p class="pw-price" id="pw-price">{formatUSD(meta?.price_cents ?? 0)}</p>
|
||||
<div id="pw-tax-notice"><TaxDeductibilityNotice /></div>
|
||||
<p class="pw-desc">{description}</p>
|
||||
</section>
|
||||
|
||||
<Wizard service_slug={slug} steps={steps ?? ["entity", "review", "payment"]} title={meta?.name ?? slug} />
|
||||
</main>
|
||||
<script>
|
||||
if (new URLSearchParams(window.location.search).has("token")) {
|
||||
const p = document.getElementById("pw-price");
|
||||
const t = document.getElementById("pw-tax-notice");
|
||||
if (p) p.style.display = "none";
|
||||
if (t) t.style.display = "none";
|
||||
}
|
||||
</script>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
||||
.pw-order-intro { margin-bottom: 1.5rem; }
|
||||
.pw-order-intro h1 { margin: 0 0 0.25rem; color: var(--pw-navy, #1a2744); }
|
||||
.pw-price { font-size: 1.5rem; font-weight: 700; color: var(--pw-green, #059669); margin: 0 0 0.75rem; }
|
||||
.pw-desc { color: var(--pw-muted, #64748b); max-width: 48rem; }
|
||||
</style>
|
||||
41
site/src/pages/order/fcc-full-compliance.astro
Normal file
41
site/src/pages/order/fcc-full-compliance.astro
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import Wizard from "../../components/intake/Wizard.astro";
|
||||
import TaxDeductibilityNotice from "../../components/TaxDeductibilityNotice.astro";
|
||||
import { INTAKE_MANIFEST, SERVICE_META, formatUSD } from "../../lib/intake_manifest";
|
||||
|
||||
const slug = "fcc-full-compliance";
|
||||
const steps = INTAKE_MANIFEST[slug];
|
||||
const meta = SERVICE_META[slug];
|
||||
const title = meta ? `Order ${meta.name}` : "Order";
|
||||
const description = "RMD + CPNI + STIR/SHAKEN + 499-A + 499-Q in one order. Every annual filing, 15% bundle discount.";
|
||||
---
|
||||
|
||||
<Base title={title} description={description}>
|
||||
<main>
|
||||
<section class="pw-order-intro">
|
||||
<h1>{meta?.name}</h1>
|
||||
<p class="pw-price" id="pw-price">{formatUSD(meta?.price_cents ?? 0)}</p>
|
||||
<div id="pw-tax-notice"><TaxDeductibilityNotice /></div>
|
||||
<p class="pw-desc">{description}</p>
|
||||
</section>
|
||||
|
||||
<Wizard service_slug={slug} steps={steps ?? ["entity", "review", "payment"]} title={meta?.name ?? slug} />
|
||||
</main>
|
||||
<script>
|
||||
if (new URLSearchParams(window.location.search).has("token")) {
|
||||
const p = document.getElementById("pw-price");
|
||||
const t = document.getElementById("pw-tax-notice");
|
||||
if (p) p.style.display = "none";
|
||||
if (t) t.style.display = "none";
|
||||
}
|
||||
</script>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
||||
.pw-order-intro { margin-bottom: 1.5rem; }
|
||||
.pw-order-intro h1 { margin: 0 0 0.25rem; color: var(--pw-navy, #1a2744); }
|
||||
.pw-price { font-size: 1.5rem; font-weight: 700; color: var(--pw-green, #059669); margin: 0 0 0.75rem; }
|
||||
.pw-desc { color: var(--pw-muted, #64748b); max-width: 48rem; }
|
||||
</style>
|
||||
70
site/src/pages/order/foreign-qualification.astro
Normal file
70
site/src/pages/order/foreign-qualification.astro
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
---
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import Wizard from "../../components/intake/Wizard.astro";
|
||||
import TaxDeductibilityNotice from "../../components/TaxDeductibilityNotice.astro";
|
||||
import { INTAKE_MANIFEST, SERVICE_META, formatUSD } from "../../lib/intake_manifest";
|
||||
|
||||
// URL param ?multi=1 selects the multi-state discounted SKU.
|
||||
const isMulti = Astro.url.searchParams.get("multi") === "1";
|
||||
const slug = isMulti ? "foreign-qualification-multi" : "foreign-qualification-single";
|
||||
const steps = INTAKE_MANIFEST[slug];
|
||||
const meta = SERVICE_META[slug];
|
||||
const title = "Foreign Qualification (Certificate of Authority)";
|
||||
const description =
|
||||
"Register your existing LLC or corporation to do business in additional " +
|
||||
"US states. Required when expanding operations, for FCC carriers serving " +
|
||||
"multiple states, or for establishing nexus in a new jurisdiction.";
|
||||
---
|
||||
|
||||
<Base title={title} description={description}>
|
||||
<main>
|
||||
<section class="pw-order-intro">
|
||||
<h1>{title}</h1>
|
||||
<p class="pw-price" id="pw-price">
|
||||
Service fee: {formatUSD(meta?.price_cents ?? 0)}
|
||||
{isMulti ? " per state" : ""}
|
||||
<span class="pw-price-note"> + state filing fees + registered agent</span>
|
||||
</p>
|
||||
<div id="pw-tax-notice"><TaxDeductibilityNotice /></div>
|
||||
<p class="pw-desc">{description}</p>
|
||||
|
||||
{!isMulti && (
|
||||
<p class="pw-multi-cta">
|
||||
Registering in multiple states?
|
||||
<a href="/order/foreign-qualification?multi=1">
|
||||
Switch to multi-state pricing ($99/state)
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
{isMulti && (
|
||||
<p class="pw-multi-cta">
|
||||
Only need one state?
|
||||
<a href="/order/foreign-qualification">
|
||||
Switch to single-state pricing ($149)
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<Wizard service_slug={slug} steps={steps ?? ["entity", "foreign_qual", "review", "payment"]} title={title} />
|
||||
</main>
|
||||
<script>
|
||||
if (new URLSearchParams(window.location.search).has("token")) {
|
||||
const p = document.getElementById("pw-price");
|
||||
const t = document.getElementById("pw-tax-notice");
|
||||
if (p) p.style.display = "none";
|
||||
if (t) t.style.display = "none";
|
||||
}
|
||||
</script>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
||||
.pw-order-intro { margin-bottom: 1.5rem; }
|
||||
.pw-order-intro h1 { margin: 0 0 0.25rem; color: var(--pw-navy, #1a2744); }
|
||||
.pw-price { font-size: 1.5rem; font-weight: 700; color: var(--pw-green, #059669); margin: 0 0 0.5rem; }
|
||||
.pw-price-note { font-size: 0.9rem; font-weight: 400; color: #64748b; }
|
||||
.pw-desc { color: var(--pw-muted, #64748b); max-width: 48rem; }
|
||||
.pw-multi-cta { font-size: 0.9rem; color: #475569; margin-top: 0.5rem; }
|
||||
.pw-multi-cta a { color: #2563eb; text-decoration: underline; }
|
||||
</style>
|
||||
41
site/src/pages/order/new-carrier-bundle.astro
Normal file
41
site/src/pages/order/new-carrier-bundle.astro
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import Wizard from "../../components/intake/Wizard.astro";
|
||||
import TaxDeductibilityNotice from "../../components/TaxDeductibilityNotice.astro";
|
||||
import { INTAKE_MANIFEST, SERVICE_META, formatUSD } from "../../lib/intake_manifest";
|
||||
|
||||
const slug = "new-carrier-bundle";
|
||||
const steps = INTAKE_MANIFEST[slug];
|
||||
const meta = SERVICE_META[slug];
|
||||
const title = meta ? `Order ${meta.name}` : "Order";
|
||||
const description = "Start-to-finish for a brand-new VoIP carrier: FRN + 499 Initial + RMD + CPNI + CALEA SSI. Everything you need to legally start billing customers.";
|
||||
---
|
||||
|
||||
<Base title={title} description={description}>
|
||||
<main>
|
||||
<section class="pw-order-intro">
|
||||
<h1>{meta?.name}</h1>
|
||||
<p class="pw-price" id="pw-price">{formatUSD(meta?.price_cents ?? 0)}</p>
|
||||
<div id="pw-tax-notice"><TaxDeductibilityNotice /></div>
|
||||
<p class="pw-desc">{description}</p>
|
||||
</section>
|
||||
|
||||
<Wizard service_slug={slug} steps={steps ?? ["entity", "review", "payment"]} title={meta?.name ?? slug} />
|
||||
</main>
|
||||
<script>
|
||||
if (new URLSearchParams(window.location.search).has("token")) {
|
||||
const p = document.getElementById("pw-price");
|
||||
const t = document.getElementById("pw-tax-notice");
|
||||
if (p) p.style.display = "none";
|
||||
if (t) t.style.display = "none";
|
||||
}
|
||||
</script>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
||||
.pw-order-intro { margin-bottom: 1.5rem; }
|
||||
.pw-order-intro h1 { margin: 0 0 0.25rem; color: var(--pw-navy, #1a2744); }
|
||||
.pw-price { font-size: 1.5rem; font-weight: 700; color: var(--pw-green, #059669); margin: 0 0 0.75rem; }
|
||||
.pw-desc { color: var(--pw-muted, #64748b); max-width: 48rem; }
|
||||
</style>
|
||||
41
site/src/pages/order/ocn-registration.astro
Normal file
41
site/src/pages/order/ocn-registration.astro
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import Wizard from "../../components/intake/Wizard.astro";
|
||||
import TaxDeductibilityNotice from "../../components/TaxDeductibilityNotice.astro";
|
||||
import { INTAKE_MANIFEST, SERVICE_META, formatUSD } from "../../lib/intake_manifest";
|
||||
|
||||
const slug = "ocn-registration";
|
||||
const steps = INTAKE_MANIFEST[slug];
|
||||
const meta = SERVICE_META[slug];
|
||||
const title = meta ? `Order ${meta.name}` : "Order";
|
||||
const description = "Obtain an Operating Company Number from NECA. Required for STIR/SHAKEN signing authority and direct numbering access.";
|
||||
---
|
||||
|
||||
<Base title={title} description={description}>
|
||||
<main>
|
||||
<section class="pw-order-intro">
|
||||
<h1>{meta?.name}</h1>
|
||||
<p class="pw-price" id="pw-price">{formatUSD(meta?.price_cents ?? 0)}</p>
|
||||
<div id="pw-tax-notice"><TaxDeductibilityNotice /></div>
|
||||
<p class="pw-desc">{description}</p>
|
||||
</section>
|
||||
|
||||
<Wizard service_slug={slug} steps={steps ?? ["entity", "review", "payment"]} title={meta?.name ?? slug} />
|
||||
</main>
|
||||
<script>
|
||||
if (new URLSearchParams(window.location.search).has("token")) {
|
||||
const p = document.getElementById("pw-price");
|
||||
const t = document.getElementById("pw-tax-notice");
|
||||
if (p) p.style.display = "none";
|
||||
if (t) t.style.display = "none";
|
||||
}
|
||||
</script>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
||||
.pw-order-intro { margin-bottom: 1.5rem; }
|
||||
.pw-order-intro h1 { margin: 0 0 0.25rem; color: var(--pw-navy, #1a2744); }
|
||||
.pw-price { font-size: 1.5rem; font-weight: 700; color: var(--pw-green, #059669); margin: 0 0 0.75rem; }
|
||||
.pw-desc { color: var(--pw-muted, #64748b); max-width: 48rem; }
|
||||
</style>
|
||||
46
site/src/pages/order/rmd-filing.astro
Normal file
46
site/src/pages/order/rmd-filing.astro
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import Wizard from "../../components/intake/Wizard.astro";
|
||||
import TaxDeductibilityNotice from "../../components/TaxDeductibilityNotice.astro";
|
||||
import { INTAKE_MANIFEST, SERVICE_META, formatUSD } from "../../lib/intake_manifest";
|
||||
|
||||
const slug = "rmd-filing";
|
||||
const steps = INTAKE_MANIFEST[slug];
|
||||
const meta = SERVICE_META[slug];
|
||||
const title = meta ? `Order ${meta.name}` : "Order";
|
||||
const description = "Robocall Mitigation Database filing. Annual recertification required March 1 (March 2 in 2026).";
|
||||
---
|
||||
|
||||
<Base title={title} description={description}>
|
||||
<main>
|
||||
<section class="pw-order-intro">
|
||||
<h1>{meta?.name}</h1>
|
||||
<p class="pw-price" id="pw-price">{formatUSD(meta?.price_cents ?? 0)} <span style="font-size:0.85rem;font-weight:400;color:#64748b;">+ $100 FCC filing fee</span></p>
|
||||
<div id="pw-tax-notice"><TaxDeductibilityNotice /></div>
|
||||
<p class="pw-desc">{description}</p>
|
||||
<p style="font-size:0.82rem;color:#92400e;background:#fefce8;border-left:3px solid #f59e0b;padding:0.5rem 0.75rem;border-radius:0 4px 4px 0;margin-top:0.5rem;max-width:48rem;">
|
||||
The FCC charges a $100 filing fee for RMD registrations and recertifications (effective 2025). This fee is passed through at cost and paid directly to the FCC during submission.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<Wizard service_slug={slug} steps={steps ?? ["entity", "review", "payment"]} title={meta?.name ?? slug} />
|
||||
</main>
|
||||
<script>
|
||||
// Always hide price + tax + FCC fee note on intake pages — if the user
|
||||
// is here, they've already paid via the order page or batch checkout.
|
||||
const p = document.getElementById("pw-price");
|
||||
const t = document.getElementById("pw-tax-notice");
|
||||
const f = document.querySelector(".pw-order-intro p[style*='fefce8']") as HTMLElement;
|
||||
if (p) p.style.display = "none";
|
||||
if (t) t.style.display = "none";
|
||||
if (f) f.style.display = "none";
|
||||
</script>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
||||
.pw-order-intro { margin-bottom: 1.5rem; }
|
||||
.pw-order-intro h1 { margin: 0 0 0.25rem; color: var(--pw-navy, #1a2744); }
|
||||
.pw-price { font-size: 1.5rem; font-weight: 700; color: var(--pw-green, #059669); margin: 0 0 0.75rem; }
|
||||
.pw-desc { color: var(--pw-muted, #64748b); max-width: 48rem; }
|
||||
</style>
|
||||
41
site/src/pages/order/stir-shaken.astro
Normal file
41
site/src/pages/order/stir-shaken.astro
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import Wizard from "../../components/intake/Wizard.astro";
|
||||
import TaxDeductibilityNotice from "../../components/TaxDeductibilityNotice.astro";
|
||||
import { INTAKE_MANIFEST, SERVICE_META, formatUSD } from "../../lib/intake_manifest";
|
||||
|
||||
const slug = "stir-shaken";
|
||||
const steps = INTAKE_MANIFEST[slug];
|
||||
const meta = SERVICE_META[slug];
|
||||
const title = meta ? `Order ${meta.name}` : "Order";
|
||||
const description = "Posture update + RMD refresh + STI-CA vendor coordination. For carriers moving from robocall-mitigation to partial implementation.";
|
||||
---
|
||||
|
||||
<Base title={title} description={description}>
|
||||
<main>
|
||||
<section class="pw-order-intro">
|
||||
<h1>{meta?.name}</h1>
|
||||
<p class="pw-price" id="pw-price">{formatUSD(meta?.price_cents ?? 0)}</p>
|
||||
<div id="pw-tax-notice"><TaxDeductibilityNotice /></div>
|
||||
<p class="pw-desc">{description}</p>
|
||||
</section>
|
||||
|
||||
<Wizard service_slug={slug} steps={steps ?? ["entity", "review", "payment"]} title={meta?.name ?? slug} />
|
||||
</main>
|
||||
<script>
|
||||
if (new URLSearchParams(window.location.search).has("token")) {
|
||||
const p = document.getElementById("pw-price");
|
||||
const t = document.getElementById("pw-tax-notice");
|
||||
if (p) p.style.display = "none";
|
||||
if (t) t.style.display = "none";
|
||||
}
|
||||
</script>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
||||
.pw-order-intro { margin-bottom: 1.5rem; }
|
||||
.pw-order-intro h1 { margin: 0 0 0.25rem; color: var(--pw-navy, #1a2744); }
|
||||
.pw-price { font-size: 1.5rem; font-weight: 700; color: var(--pw-green, #059669); margin: 0 0 0.75rem; }
|
||||
.pw-desc { color: var(--pw-muted, #64748b); max-width: 48rem; }
|
||||
</style>
|
||||
309
site/src/pages/services/telecom/499a-traffic-study.astro
Normal file
309
site/src/pages/services/telecom/499a-traffic-study.astro
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
---
|
||||
import Base from "../../../layouts/Base.astro";
|
||||
|
||||
const title = "499-A Traffic Study — Carrier CDR Data Upload & Classification";
|
||||
const description = "Upload your call detail records, get a signed traffic study for your FCC Form 499-A with every jurisdiction classified — interstate, intrastate, international, indeterminate, wholesale vs. retail, and the Block 5 regional breakouts.";
|
||||
---
|
||||
|
||||
<Base title={title} description={description}>
|
||||
<main>
|
||||
<div class="pw-hero">
|
||||
<h1>499-A Traffic Study</h1>
|
||||
<p style="font-size: 1.1rem; max-width: 42rem;">
|
||||
Send us your call records. We classify every call for your FCC Form 499-A
|
||||
and hand you a signed study you can file against — with a 5-year
|
||||
audit trail, automatic regional breakouts, and a pre-filled 499-A worksheet.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section id="why">
|
||||
<h2>Why the numbers on your 499-A matter</h2>
|
||||
<p>
|
||||
USAC assesses Universal Service Fund contributions on the interstate and
|
||||
international portion of your telecom revenue. Get the percentage wrong
|
||||
on your 499-A and you either overpay (and over-charge your customers),
|
||||
underpay (and invite an audit), or get hit with a monetary forfeiture.
|
||||
For 2026, inaccurate filings can carry up to <strong>$251,322 per
|
||||
violation</strong>.
|
||||
</p>
|
||||
<p>
|
||||
Most small carriers default to the 64.9% interconnected-VoIP safe harbor
|
||||
because computing the actual split is a pain: pull CDRs from the switch,
|
||||
look up hundreds of area codes, allocate wholesale vs. retail, run the
|
||||
regional breakouts, and keep the spreadsheet for five years in case of
|
||||
audit. We do all of that for you, on autopilot, every quarter.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="how-data-gets-to-us">
|
||||
<h2>How you send us your data</h2>
|
||||
<p>Pick whichever fits your operation — you can use more than one at the same time:</p>
|
||||
|
||||
<div class="pw-card">
|
||||
<strong>Secure file drop (SFTP / FTPS)</strong>
|
||||
<p style="margin: 0.4rem 0 0;">
|
||||
Enable it in your portal and we generate a username and one-time
|
||||
random password pointed at your own encrypted storage bucket. Works
|
||||
with any standard SFTP or FTPS client or automation script. TLS-only.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="pw-card">
|
||||
<strong>We pull directly from your switch</strong>
|
||||
<p style="margin: 0.4rem 0 0;">
|
||||
Pick your switch from the dropdown and hand us credentials once; we
|
||||
grab new CDRs on the schedule you choose. Supported platforms:
|
||||
</p>
|
||||
<ul style="margin-top: 0.5rem;">
|
||||
<li>NetSapiens</li>
|
||||
<li>Metaswitch (iCM / Provisioning Server)</li>
|
||||
<li>FreeSWITCH</li>
|
||||
<li>Asterisk / FreePBX / AsteriskNOW</li>
|
||||
<li>Ribbon / Sonus SBC (EMA / PSX)</li>
|
||||
<li>Sansay SBC (SSM)</li>
|
||||
<li>Cisco BroadWorks</li>
|
||||
<li>2600Hz Kazoo</li>
|
||||
<li>Grandstream UCM (62xx / 63xx)</li>
|
||||
<li>46Labs (Peering / NOVA)</li>
|
||||
<li>SIP Navigator (Cataleya Orchid One)</li>
|
||||
</ul>
|
||||
<p style="margin: 0.5rem 0 0; color: var(--pw-muted); font-size: 0.9rem;">
|
||||
Don't see yours? Pick <em>Other</em> and configure SFTP / FTP / FTPS /
|
||||
HTTPS / S3 transport manually with any CDR format — we adapt to
|
||||
whatever column layout your switch emits.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="pw-card">
|
||||
<strong>Drag-and-drop upload in your portal</strong>
|
||||
<p style="margin: 0.4rem 0 0;">
|
||||
Quickest path for one-off batches or small carriers. Files go
|
||||
straight to an encrypted bucket — no email, no disks, no
|
||||
PII in flight.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="pw-card">
|
||||
<strong>Real-time webhook</strong>
|
||||
<p style="margin: 0.4rem 0 0;">
|
||||
For platforms that emit per-call records as they happen, point the
|
||||
stream at the webhook URL from your portal. Calls classify within
|
||||
seconds of landing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p style="color: var(--pw-muted); font-size: 0.9rem;">
|
||||
All transport is encrypted. Files are stored in an encrypted bucket
|
||||
scoped to your account. Default retention is 5 years — matches
|
||||
USAC's audit window per 47 CFR § 54.711(a).
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="what-we-classify">
|
||||
<h2>What we classify</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Interstate / intrastate / international</strong> — every
|
||||
call, based on calling and called numbers. We use NANPA area-code
|
||||
records and the FCC's country-code assignments.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Indeterminate</strong> — calls we can't resolve to
|
||||
one state/country. Kept visible so you can see what's unclear
|
||||
rather than silently miscounting. Common causes: anonymized caller,
|
||||
unrouteable DID, switch-side caller-ID strip.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Wholesale vs. retail</strong> — fed by a one-time
|
||||
mapping of your trunk groups and customer account IDs in the portal,
|
||||
or by a per-row tag column if your CDR carries one.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Block 5 regional breakouts</strong> (Lines 503–510) —
|
||||
produced <em>two ways</em>, by the caller's originating state
|
||||
<em>and</em> by your customer's billing-address state. You (or we)
|
||||
pick whichever the FCC has asked for at filing time.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="what-you-get">
|
||||
<h2>What you get back</h2>
|
||||
<ul>
|
||||
<li>A <strong>signed traffic study</strong> (PDF + XLSX) with jurisdictional percentages, both regional breakouts, a methodology statement, sample size, and revenue-vs-minutes cross-check</li>
|
||||
<li>A <strong>pre-filled 499-A workbook</strong> — Block 3, Block 4-A, and Block 5 cells populated from your actual data</li>
|
||||
<li>A <strong>data-quality report</strong> listing anything we couldn't classify so you can fix switch issues before next quarter</li>
|
||||
<li>A <strong>5-year retention guarantee</strong> on the underlying CDRs — ready for a USAC audit on 24 hours' notice</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="revenue-first">
|
||||
<h2>We bill USF by actual revenue when we can</h2>
|
||||
<p>
|
||||
USAC assesses on <strong>revenue</strong>, not minutes. If your CDRs carry
|
||||
a per-call charge (most commercial switches do — Asterisk nibblebill,
|
||||
NetSapiens <code>cost</code>, Ribbon <code>totalCharge</code>, etc.), we
|
||||
compute your percentages from actual dollars. Minutes-weighted numbers
|
||||
appear beside the revenue view as a sanity check.
|
||||
</p>
|
||||
<p>
|
||||
Flat-rate line service (monthly seat pricing with no per-call charge) is
|
||||
the one case where minutes-only estimation is the correct choice. That's
|
||||
an explicit opt-in on your profile — we label the study accordingly
|
||||
so the methodology section is defensible under audit.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="benefits-vs-manual">
|
||||
<h2>vs. doing it yourself</h2>
|
||||
<table class="pw-pricing">
|
||||
<thead>
|
||||
<tr><th>Doing it yourself</th><th>With us</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Pull raw CDRs from the switch, wrestle with format changes</td><td>Automated the moment your switch drops a new file</td></tr>
|
||||
<tr><td>Look up hundreds of area codes by hand, maintain a spreadsheet</td><td>Full NANPA lookup, updated continuously</td></tr>
|
||||
<tr><td>Eyeball “is this interstate?” from phone numbers</td><td>Deterministic rules, same answer every quarter</td></tr>
|
||||
<tr><td>Guess at the wholesale/retail split</td><td>Per-call bucketed from trunk-group mapping you set once</td></tr>
|
||||
<tr><td>Use the 64.9% safe harbor even when your actual % is lower</td><td>Use your actual study; lower USF pass-through when math justifies it</td></tr>
|
||||
<tr><td>Weight by minutes because it's easier</td><td>Weight by actual per-call revenue — the denominator USAC uses</td></tr>
|
||||
<tr><td>No audit trail when USAC asks</td><td>Signed study with methodology + sample size, delivered quarterly</td></tr>
|
||||
<tr><td>One-off Excel with formulas you'll forget in 12 months</td><td>Archived, dated, downloadable from your portal</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section id="mistakes">
|
||||
<h2>Common mistakes we prevent</h2>
|
||||
<ul>
|
||||
<li><strong>Counting failed/abandoned calls as billable minutes</strong> — inflates the denominator; USAC fines follow</li>
|
||||
<li><strong>Attestation confusion</strong> — treating all outbound as interstate when the answering point was actually intrastate</li>
|
||||
<li><strong>Toll-free inversion</strong> — 8XX calls are paid by the called party; Block 5 attribution flips. Customers routinely put these in the wrong region.</li>
|
||||
<li><strong>Time-zone drift</strong> — naive UTC-vs-local splits move calls across midnight boundaries and miscount a day's minutes</li>
|
||||
<li><strong>Indeterminate calls hidden in “interstate”</strong> — bumps your USF contribution for minutes you shouldn't be paying on</li>
|
||||
<li><strong>Stale trunk-group tags</strong> — a customer moves from wholesale to retail and nobody updates the spreadsheet; we re-prompt quarterly</li>
|
||||
<li><strong>Safe harbor when actual interstate % is lower</strong> — overpaying into USF and over-charging end users</li>
|
||||
<li><strong>Minutes-weighting a per-call carrier</strong> — a few long-international calls can swing a minutes ratio sharply away from the real revenue mix</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="cdr-fields">
|
||||
<h2>What CDR fields we need</h2>
|
||||
<p>No custom development on your side. If your CDR carries these fields, you're ready:</p>
|
||||
|
||||
<h3>Required — the system won't classify without these</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Field</th><th>Typical CSV header</th><th>Why we need it</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Call start time</td><td><code>start_time</code>, <code>calldate</code>, <code>start_stamp</code>, <code>initiated</code></td><td>Assigns the call to a reporting period and quarter</td></tr>
|
||||
<tr><td>Calling number (ANI)</td><td><code>src</code>, <code>caller_id_number</code>, <code>orig_number</code>, <code>ani</code></td><td>Derives originating state via NPA</td></tr>
|
||||
<tr><td>Called number (DNIS)</td><td><code>dst</code>, <code>destination_number</code>, <code>term_number</code>, <code>dnis</code></td><td>Derives terminating state</td></tr>
|
||||
<tr><td>Call duration</td><td><code>duration</code>, <code>billsec</code>, <code>duration_ms</code>, <code>talk_time</code></td><td>Minutes-based weighting</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Strongly preferred — makes the study revenue-accurate instead of minutes-estimated</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Field</th><th>Typical CSV header</th><th>What it enables</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Per-call billed amount</td>
|
||||
<td><code>charge</code>, <code>cost</code>, <code>amount_billed</code>, <code>rate_charge</code>, <code>nibble_total_billed</code></td>
|
||||
<td>
|
||||
<strong>The primary revenue signal for the traffic study.</strong>
|
||||
When present, we attribute USF by actual per-call revenue —
|
||||
the denominator USAC actually uses. Without it, we fall back to
|
||||
minutes-only estimation, which must be opted in on your profile.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Helpful — quality of attribution and audit strength improves when present</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Field</th><th>Typical CSV header</th><th>What it enables</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Call direction</td><td><code>direction</code>, <code>call_direction</code>, <code>leg_type</code></td><td>Distinguishes outbound from inbound when ANI alone can't (toll-free inversion, etc.)</td></tr>
|
||||
<tr><td>Disposition / answer indicator</td><td><code>disposition</code>, <code>hangup_cause</code>, <code>answered</code></td><td>Drops failed/abandoned calls so they don't skew minutes</td></tr>
|
||||
<tr><td>Trunk group ID</td><td><code>trunk_group</code>, <code>trunk</code>, <code>carrier_id</code>, <code>channel_name</code> prefix</td><td>Primary key for wholesale/retail bucketing via the portal mapping</td></tr>
|
||||
<tr><td>Customer / account ID</td><td><code>account_code</code>, <code>accountcode</code>, <code>customer_id</code>, <code>sip_account</code></td><td>Secondary bucketing key when trunk group isn't granular enough</td></tr>
|
||||
<tr><td>Answer time + release time</td><td><code>answer_time</code>, <code>end_stamp</code>, <code>release_time</code></td><td>Cross-checks duration, detects zero-duration anomalies</td></tr>
|
||||
<tr><td>Orig / term country</td><td><code>orig_country</code>, <code>term_country</code>, <code>country_code</code></td><td>Classifies international calls with ambiguous NANP derivation (Caribbean jurisdictions etc.)</td></tr>
|
||||
<tr><td>Customer-type tag (if your switch can emit it)</td><td><code>customer_type</code>, <code>market_segment</code></td><td>Optional per-row override — wins over portal mapping</td></tr>
|
||||
<tr><td>Rate center / LATA</td><td><code>rate_center</code>, <code>orig_lata</code>, <code>term_lata</code></td><td>Enables local-vs-intrastate-toll split (not required for 499-A)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="pw-callout">
|
||||
<strong>Bottom line:</strong> if your CDRs carry start time, ANI, DNIS,
|
||||
and duration, you're ready. A per-call charge or cost column turns
|
||||
the study into a revenue-weighted report, which is what USAC uses. Flat-rate
|
||||
line services opt into minutes-only estimation — that's standard
|
||||
for retail VoIP, and we note the methodology on every study.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="pricing">
|
||||
<h2>Storage, processing, and retention</h2>
|
||||
<p>
|
||||
Your 499-A filing service covers <strong>10 GB of compressed CDR
|
||||
storage</strong> and <strong>10 million classified calls per reporting
|
||||
year</strong>, with <strong>5-year retention</strong> — plenty for
|
||||
most small-to-midsize carriers. Larger carriers pick the smallest tier
|
||||
that covers their footprint:
|
||||
</p>
|
||||
<table class="pw-pricing">
|
||||
<thead>
|
||||
<tr><th>Plan</th><th>Storage</th><th>Calls / year</th><th>Annual</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Included with filing</td><td>10 GB</td><td>10 M</td><td>—</td></tr>
|
||||
<tr><td>Storage Tier 1</td><td>50 GB</td><td>50 M</td><td>$99</td></tr>
|
||||
<tr><td>Storage Tier 2</td><td>250 GB</td><td>250 M</td><td>$299</td></tr>
|
||||
<tr><td>Storage Tier 3</td><td>1 TB</td><td>1 B</td><td>$799</td></tr>
|
||||
<tr><td>Enterprise</td><td>Custom</td><td>Custom</td><td>Let's talk</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
Your portal shows a live usage meter. At 80% we email a heads-up; at
|
||||
100% you pick (at setup) whether to keep ingesting and get the next
|
||||
tier billed automatically, pause new uploads until you upgrade, or
|
||||
notify-only. Prior years stay retained while you're on an active
|
||||
plan; if you lapse, we send a 30-day notice before any deletion.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="how-it-works-across-the-year">
|
||||
<h2>How it works across a filing year</h2>
|
||||
<p>
|
||||
Data collection runs continuously — you can send us CDRs the day you
|
||||
sign up and every quarter rolls into the year's dataset
|
||||
automatically. The <strong>classified study for the current reporting
|
||||
year</strong> becomes available once you've signed up for that
|
||||
year's 499-A filing service (or the standalone traffic study).
|
||||
Prior years you've filed with us stay open in your portal forever.
|
||||
</p>
|
||||
<p>
|
||||
Until the current year is paid, you'll still see ingestion health
|
||||
in the portal — upload counts, data-quality warnings, quarantine
|
||||
reviews — you just won't see the final percentages or download
|
||||
the signed study until the filing service is paid for. This is how we
|
||||
keep the service sustainable without hourly billing.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="cta">
|
||||
<div class="pw-callout" style="border-color: var(--pw-green); background: #ecfdf5; text-align: center; padding: 1.5rem;">
|
||||
<h3 style="margin: 0 0 0.5rem; color: var(--pw-navy);">Ready to stop using the safe harbor?</h3>
|
||||
<p style="margin: 0 0 1rem;">Set up a profile, send us a sample CDR, and see your actual interstate percentage — usually in under an hour.</p>
|
||||
<a class="pw-cta" href="/order/fcc-499a">Start your 2026 499-A filing →</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</Base>
|
||||
128
site/src/pages/services/telecom/form-477.astro
Normal file
128
site/src/pages/services/telecom/form-477.astro
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
---
|
||||
import Base from "../../../layouts/Base.astro";
|
||||
const title = "FCC Form 477 Filing — What Replaced It and What You Need Now";
|
||||
const description = "The FCC retired Form 477 in December 2022. Voice subscription data now files through BDC. Here's what you actually need for 2026 and how we file it for you.";
|
||||
---
|
||||
|
||||
<Base title={title} description={description}>
|
||||
<main>
|
||||
<section class="pw-hero">
|
||||
<h1>FCC Form 477 Filing — Where Things Stand in 2026</h1>
|
||||
<p style="font-size: 1.1rem; max-width: 44rem;">
|
||||
The Form 477 you remember doesn't exist anymore. But the
|
||||
<em>filing obligation</em> still does. Here's what changed and
|
||||
what you actually need to file.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>What happened to Form 477</h2>
|
||||
<p>
|
||||
The FCC retired the standalone Form 477 filing system on
|
||||
<strong>December 31, 2022</strong>. All voice subscription data and
|
||||
all broadband deployment data moved into the <strong>Broadband Data
|
||||
Collection (BDC) system</strong>. Same deadlines (March 1 for
|
||||
December-31 snapshots, September 1 for June-30 snapshots), same
|
||||
required data points, different portal.
|
||||
</p>
|
||||
<div class="pw-callout">
|
||||
<strong>Key point:</strong> if someone tells you to file Form 477
|
||||
in 2026, they mean <em>file your voice-subscription data through BDC</em>.
|
||||
The old apps.fcc.gov/form477 login page will redirect you to
|
||||
bdc.fcc.gov — that's on purpose.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Who still has to file</h2>
|
||||
<ul>
|
||||
<li><strong>Facilities-based providers of fixed or mobile voice service</strong> — interconnected VoIP providers, CLECs, and wireline ILECs. File voice subscription counts.</li>
|
||||
<li><strong>Facilities-based broadband ISPs</strong> — file availability data (by serviceable location) and subscription counts.</li>
|
||||
<li><strong>Interconnected VoIP-only resellers</strong> — still required to file voice subscriber counts even though you don't own the underlying infrastructure.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Two ways we can file for you</h2>
|
||||
<div class="pw-card-grid">
|
||||
<a class="pw-card" href="/order/bdc-voice">
|
||||
<h3>BDC Voice Subscription only</h3>
|
||||
<p class="pw-card-price">$149 per filing</p>
|
||||
<p>
|
||||
The voice part of what used to be Form 477. Tell us your voice
|
||||
subscriber count at the snapshot date; we file it.
|
||||
</p>
|
||||
<p class="pw-card-hint">
|
||||
<em>Best for</em>: VoIP resellers, CLECs, competitive carriers
|
||||
that don't offer broadband.
|
||||
</p>
|
||||
</a>
|
||||
<a class="pw-card" href="/order/bdc-broadband">
|
||||
<h3>BDC Broadband Deployment only</h3>
|
||||
<p class="pw-card-price">$199 per filing</p>
|
||||
<p>
|
||||
Availability data — which locations you serve and at what speeds.
|
||||
Upload your availability CSV; we validate and submit.
|
||||
</p>
|
||||
<p class="pw-card-hint">
|
||||
<em>Best for</em>: ISPs, fixed wireless providers, broadband-only
|
||||
companies.
|
||||
</p>
|
||||
</a>
|
||||
<a class="pw-card" href="/order/bdc-filing">
|
||||
<h3>Both — BDC Voice + Broadband</h3>
|
||||
<p class="pw-card-price">$299 per filing</p>
|
||||
<p>
|
||||
One filing covers both blocks. Saves $49 vs. buying them
|
||||
separately.
|
||||
</p>
|
||||
<p class="pw-card-hint">
|
||||
<em>Best for</em>: full-service carriers offering both voice
|
||||
and broadband.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Why you don't want to file this yourself</h2>
|
||||
<ul>
|
||||
<li><strong>BDC portal authentication</strong> — requires an authorized CORES session tied to your FRN; miss a step and you get locked out of your own filing.</li>
|
||||
<li><strong>Availability CSV format is strict</strong> — the BDC system validates every row; a misformatted header or bad location ID rejects the entire upload.</li>
|
||||
<li><strong>Missed filings</strong> — the FCC has begun issuing forfeitures to providers who miss BDC deadlines. Don't be the cautionary tale.</li>
|
||||
<li><strong>Voice-only confusion</strong> — many voice-only VoIP resellers skip BDC because "it's broadband data." Wrong. The voice subscription block is mandatory and lives in BDC now.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Deadlines for 2026</h2>
|
||||
<table>
|
||||
<thead><tr><th>Snapshot date</th><th>Filing deadline</th><th>What to file</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>December 31, 2025</td><td>March 1, 2026</td><td>Voice subscribers + broadband availability + subscriber counts as of Dec 31, 2025</td></tr>
|
||||
<tr><td>June 30, 2026</td><td>September 1, 2026</td><td>Voice subscribers + broadband availability + subscriber counts as of Jun 30, 2026</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section class="pw-cta-section">
|
||||
<a class="pw-cta" href="/order/bdc-voice">File your BDC voice data (formerly Form 477) →</a>
|
||||
</section>
|
||||
</main>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
||||
.pw-hero { padding: 2.5rem 1.5rem; background: linear-gradient(135deg, #1a2744 0%, #2d4e78 100%); color: #fff; border-radius: 14px; margin-bottom: 2rem; }
|
||||
.pw-hero h1 { color: #fff; margin: 0 0 0.5rem; }
|
||||
.pw-callout { border-left: 4px solid #2d4e78; background: #f0f9ff; padding: 0.9rem 1.1rem; border-radius: 0 6px 6px 0; margin: 1rem 0; }
|
||||
.pw-card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; margin: 1rem 0; }
|
||||
.pw-card { display: block; padding: 1.25rem; border: 1px solid #e2e8f0; border-radius: 10px; text-decoration: none; color: inherit; transition: box-shadow 0.15s; }
|
||||
.pw-card:hover { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); border-color: #2d4e78; }
|
||||
.pw-card h3 { margin: 0 0 0.3rem; color: #1a2744; font-size: 1.05rem; }
|
||||
.pw-card-price { font-weight: 700; color: #059669; margin: 0 0 0.5rem; font-size: 1rem; }
|
||||
.pw-card-hint { color: #64748b; font-size: 0.85rem; margin-top: 0.5rem; }
|
||||
.pw-cta-section { margin: 3rem 0; text-align: center; }
|
||||
.pw-cta { display: inline-block; padding: 1rem 2rem; background: #059669; color: #fff; text-decoration: none; font-weight: 600; border-radius: 8px; font-size: 1.05rem; }
|
||||
.pw-cta:hover { background: #047857; }
|
||||
</style>
|
||||
622
site/src/pages/tools/fcc-compliance-check.astro
Normal file
622
site/src/pages/tools/fcc-compliance-check.astro
Normal file
|
|
@ -0,0 +1,622 @@
|
|||
---
|
||||
import Base from "../../layouts/Base.astro";
|
||||
---
|
||||
|
||||
<Base
|
||||
title="FCC Compliance Check — Free Telecom Filing Status"
|
||||
description="Enter your FCC Registration Number (FRN) to check your RMD, CPNI, 499-A, and STIR/SHAKEN compliance status instantly. Free tool from Performance West."
|
||||
>
|
||||
<main class="max-w-3xl mx-auto px-4 py-10">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="text-sm text-gray-500 mb-6" aria-label="Breadcrumb">
|
||||
<a href="/" class="hover:text-pw-600">Home</a>
|
||||
<span class="mx-1">/</span>
|
||||
<a href="/tools" class="hover:text-pw-600">Free Tools</a>
|
||||
<span class="mx-1">/</span>
|
||||
<span class="text-gray-800 font-medium">FCC Compliance Check</span>
|
||||
</nav>
|
||||
|
||||
<!-- Header -->
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">FCC Compliance Check</h1>
|
||||
<p class="text-gray-600 mb-6">
|
||||
Enter a business name or FRN below to instantly check FCC filing and compliance status.
|
||||
</p>
|
||||
|
||||
<!-- Disclaimer -->
|
||||
<div class="bg-gray-100 border border-gray-200 rounded-lg p-4 text-sm text-gray-600 mb-8">
|
||||
This tool queries publicly available FCC data and is for informational purposes only.
|
||||
This is not legal or compliance advice.
|
||||
</div>
|
||||
|
||||
<!-- Search Box -->
|
||||
<div class="bg-white border border-gray-200 rounded-xl p-6 shadow-sm mb-8">
|
||||
<!-- Business Name Search -->
|
||||
<label for="name-input" class="block text-sm font-medium text-gray-700 mb-1">Business Name</label>
|
||||
<div class="flex gap-2 mb-2">
|
||||
<input
|
||||
type="text"
|
||||
id="name-input"
|
||||
placeholder="e.g. Verizon"
|
||||
class="flex-1 border border-gray-300 rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-pw-500"
|
||||
/>
|
||||
<button
|
||||
id="name-search-btn"
|
||||
class="bg-pw-600 hover:bg-pw-700 text-white font-semibold px-5 py-2 rounded-lg text-sm transition"
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
<div id="name-results" class="hidden mt-3"></div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="flex items-center gap-3 my-5">
|
||||
<div class="flex-1 border-t border-gray-200"></div>
|
||||
<span class="text-sm text-gray-400">or enter FRN directly</span>
|
||||
<div class="flex-1 border-t border-gray-200"></div>
|
||||
</div>
|
||||
|
||||
<!-- FRN Search -->
|
||||
<label for="frn-input" class="block text-sm font-medium text-gray-700 mb-1">FCC Registration Number (FRN)</label>
|
||||
<div class="flex gap-2 mb-2">
|
||||
<input
|
||||
type="text"
|
||||
id="frn-input"
|
||||
placeholder="0012345678"
|
||||
maxlength="10"
|
||||
class="flex-1 border border-gray-300 rounded-lg px-4 py-2 font-mono text-lg focus:outline-none focus:ring-2 focus:ring-pw-500"
|
||||
/>
|
||||
<button
|
||||
id="frn-check-btn"
|
||||
class="bg-gray-700 hover:bg-gray-800 text-white font-semibold px-5 py-2 rounded-lg text-sm transition"
|
||||
>
|
||||
Check Compliance
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400">
|
||||
Don't know your FRN?
|
||||
<a href="https://apps.fcc.gov/cores/userLogin.do" target="_blank" rel="noopener" class="text-pw-600 hover:underline">Look it up on FCC CORES</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div id="loading" class="hidden text-center py-10">
|
||||
<svg class="animate-spin h-8 w-8 text-pw-600 mx-auto mb-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path>
|
||||
</svg>
|
||||
<p class="text-gray-500 text-sm">Checking FCC databases…</p>
|
||||
</div>
|
||||
|
||||
<!-- Error -->
|
||||
<div id="error-box" class="hidden bg-red-50 border border-red-200 rounded-lg p-4 text-red-700 text-sm mb-6">
|
||||
<p id="error-message"></p>
|
||||
</div>
|
||||
|
||||
<!-- Results -->
|
||||
<div id="results" class="hidden space-y-6">
|
||||
<!-- Entity Header -->
|
||||
<div class="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
|
||||
<h2 id="entity-name" class="text-xl font-bold text-gray-900"></h2>
|
||||
<p id="entity-dba" class="text-sm text-gray-500 hidden"></p>
|
||||
<p id="entity-address" class="text-sm text-gray-500 mt-1"></p>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
FRN: <span id="entity-frn" class="font-mono font-semibold text-gray-800"></span>
|
||||
</p>
|
||||
<div id="entity-details" class="hidden mt-3 space-y-1 text-sm text-gray-600">
|
||||
<p id="detail-filer-id"></p>
|
||||
<p id="detail-comm-type"></p>
|
||||
<p id="detail-rmd-number"></p>
|
||||
<div id="detail-provider-types" class="flex flex-wrap gap-1 mt-1"></div>
|
||||
<p id="detail-usf-contributor"></p>
|
||||
<p id="detail-other-frns"></p>
|
||||
<p id="detail-previous-names"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Checks -->
|
||||
<div id="checks-container" class="space-y-4"></div>
|
||||
|
||||
<!-- Prior-year catch-up -->
|
||||
<div id="prior-year" class="hidden bg-red-50 border border-red-200 rounded-xl p-5 text-sm text-red-800">
|
||||
<p class="font-semibold mb-1">Prior-Year Catch-Up May Be Required</p>
|
||||
<p>One or more filings appear overdue. In some cases prior-year filings must be completed before the current year can be filed. Contact us for a full assessment.</p>
|
||||
</div>
|
||||
|
||||
<!-- CTA Section -->
|
||||
<div id="cta-section" class="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
|
||||
<div id="cta-content"></div>
|
||||
|
||||
<!-- Payment Icons -->
|
||||
<div id="payment-icons" class="hidden mt-5 flex flex-wrap items-center gap-3 justify-center">
|
||||
<img src="/images/cards/visa.png" alt="Visa" class="h-7 opacity-70" />
|
||||
<img src="/images/cards/mastercard.svg" alt="Mastercard" class="h-7 opacity-70" />
|
||||
<img src="/images/cards/amex.png" alt="Amex" class="h-7 opacity-70" />
|
||||
<img src="/images/cards/discover.svg" alt="Discover" class="h-7 opacity-70" />
|
||||
<img src="/images/cards/paypal.svg" alt="PayPal" class="h-6 opacity-70" />
|
||||
<span class="text-gray-300 mx-1">|</span>
|
||||
<img src="/images/cards/bitcoin.svg" alt="Bitcoin" class="h-6 opacity-60" />
|
||||
<img src="/images/cards/ethereum.svg" alt="Ethereum" class="h-6 opacity-60" />
|
||||
<img src="/images/cards/usdc.svg" alt="USDC" class="h-6 opacity-60" />
|
||||
<img src="/images/cards/usdt.svg" alt="USDT" class="h-6 opacity-60" />
|
||||
<img src="/images/cards/litecoin.svg" alt="Litecoin" class="h-6 opacity-60" />
|
||||
</div>
|
||||
|
||||
<!-- Reminder CTA (shown when all green) -->
|
||||
<div id="reminder-cta" class="hidden mt-5 text-center">
|
||||
<p class="text-sm text-gray-600 mb-3">Want a reminder before your next filing deadline?</p>
|
||||
<button
|
||||
id="subscribe-btn-compliance"
|
||||
class="bg-pw-600 hover:bg-pw-700 text-white font-semibold px-6 py-2 rounded-lg text-sm transition"
|
||||
>
|
||||
Join Reminder List
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Bottom row -->
|
||||
<div class="mt-6 flex flex-wrap gap-3 justify-center text-sm">
|
||||
<a href="/order/new-carrier" class="inline-flex items-center gap-1.5 bg-pw-600 hover:bg-pw-700 text-white font-semibold px-4 py-2 rounded-lg transition">
|
||||
<img src="/images/flags/usa.png" alt="" class="h-4" />
|
||||
Start a New US Carrier
|
||||
</a>
|
||||
<a href="/order/new-carrier-canada" class="inline-flex items-center gap-1.5 bg-red-600 hover:bg-red-700 text-white font-semibold px-4 py-2 rounded-lg transition">
|
||||
<img src="/images/flags/canada.png" alt="" class="h-4" />
|
||||
Start a New Canadian Carrier
|
||||
</a>
|
||||
<a href="/services" class="inline-flex items-center gap-1.5 border border-gray-300 text-gray-700 hover:bg-gray-50 font-semibold px-4 py-2 rounded-lg transition">
|
||||
View All Telecom Services
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timestamp -->
|
||||
<p id="checked-at" class="text-xs text-gray-400 text-center"></p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script type="module" is:inline>
|
||||
const API = window.__PW_API || "https://api.performancewest.net";
|
||||
|
||||
const nameInput = document.getElementById("name-input");
|
||||
const nameSearchBtn = document.getElementById("name-search-btn");
|
||||
const nameResults = document.getElementById("name-results");
|
||||
const frnInput = document.getElementById("frn-input");
|
||||
const frnCheckBtn = document.getElementById("frn-check-btn");
|
||||
const loadingEl = document.getElementById("loading");
|
||||
const errorBox = document.getElementById("error-box");
|
||||
const errorMessage = document.getElementById("error-message");
|
||||
const resultsEl = document.getElementById("results");
|
||||
|
||||
// --- Name search ---
|
||||
nameSearchBtn.addEventListener("click", runNameSearch);
|
||||
nameInput.addEventListener("keydown", (e) => { if (e.key === "Enter") runNameSearch(); });
|
||||
|
||||
async function runNameSearch() {
|
||||
const name = nameInput.value.trim();
|
||||
if (!name) return;
|
||||
|
||||
nameResults.classList.remove("hidden");
|
||||
nameResults.innerHTML = '<p class="text-sm text-gray-500">Searching…</p>';
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/api/v1/fcc/cores-search?name=${encodeURIComponent(name)}`);
|
||||
if (!res.ok) throw new Error("Search failed");
|
||||
const data = await res.json();
|
||||
const items = data.results || data;
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
nameResults.innerHTML = '<p class="text-sm text-gray-500">No results found.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `<p class="text-xs text-gray-400 mb-2">${items.length} result${items.length !== 1 ? "s" : ""}</p>`;
|
||||
html += '<div class="space-y-1 max-h-60 overflow-y-auto">';
|
||||
for (const item of items) {
|
||||
const label = [
|
||||
item.legal_name || "",
|
||||
item.dba ? `DBA: ${item.dba}` : "",
|
||||
item.frn ? `FRN: ${item.frn}` : "",
|
||||
item.filer_id ? `Filer ID: ${item.filer_id}` : "",
|
||||
].filter(Boolean).join(" — ");
|
||||
|
||||
html += `<button
|
||||
data-frn="${item.frn || ""}"
|
||||
class="name-result-btn block w-full text-left px-3 py-2 rounded-lg text-sm hover:bg-pw-50 border border-transparent hover:border-pw-200 transition"
|
||||
>${label}</button>`;
|
||||
}
|
||||
html += "</div>";
|
||||
nameResults.innerHTML = html;
|
||||
|
||||
nameResults.querySelectorAll(".name-result-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const frn = btn.getAttribute("data-frn");
|
||||
if (!frn) {
|
||||
showError("No FRN available for this entity.");
|
||||
return;
|
||||
}
|
||||
frnInput.value = frn;
|
||||
runCheck();
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
nameResults.innerHTML = `<p class="text-sm text-red-600">Search error: ${err.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
// --- FRN compliance check ---
|
||||
frnCheckBtn.addEventListener("click", runCheck);
|
||||
frnInput.addEventListener("keydown", (e) => { if (e.key === "Enter") runCheck(); });
|
||||
|
||||
async function runCheck() {
|
||||
const frn = frnInput.value.trim();
|
||||
if (!frn) return;
|
||||
|
||||
// Update URL
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set("frn", frn);
|
||||
history.replaceState(null, "", url);
|
||||
|
||||
// Reset UI
|
||||
resultsEl.classList.add("hidden");
|
||||
errorBox.classList.add("hidden");
|
||||
loadingEl.classList.remove("hidden");
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/api/v1/fcc/lookup?frn=${frn}`);
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.detail || body.error || `Lookup failed (${res.status})`);
|
||||
}
|
||||
const data = await res.json();
|
||||
renderResults(data);
|
||||
} catch (err) {
|
||||
showError(err.message);
|
||||
} finally {
|
||||
loadingEl.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
errorMessage.textContent = msg;
|
||||
errorBox.classList.remove("hidden");
|
||||
}
|
||||
|
||||
// --- Render results ---
|
||||
let lastData = null;
|
||||
|
||||
function renderResults(data) {
|
||||
lastData = data;
|
||||
errorBox.classList.add("hidden");
|
||||
|
||||
// Entity name
|
||||
document.getElementById("entity-name").textContent = data.entity_name || data.legal_name || "Unknown Entity";
|
||||
document.getElementById("entity-frn").textContent = data.frn || frnInput.value.trim();
|
||||
|
||||
// DBA
|
||||
const dbaEl = document.getElementById("entity-dba");
|
||||
const dbaValues = [
|
||||
data.filer_499?.trade_name,
|
||||
...(data.rmd?.other_dba_names || []),
|
||||
].filter((v) => v && v !== "None" && v.trim() !== "");
|
||||
if (dbaValues.length > 0) {
|
||||
dbaEl.textContent = "DBA: " + [...new Set(dbaValues)].join(", ");
|
||||
dbaEl.classList.remove("hidden");
|
||||
} else {
|
||||
dbaEl.classList.add("hidden");
|
||||
}
|
||||
|
||||
// Address
|
||||
const addrEl = document.getElementById("entity-address");
|
||||
const coresAddr = data.cores_address || data.address;
|
||||
if (coresAddr) {
|
||||
addrEl.textContent = coresAddr;
|
||||
} else if (data.rmd?.business_address) {
|
||||
addrEl.textContent = data.rmd.business_address;
|
||||
} else if (data.filer_499?.state) {
|
||||
addrEl.textContent = data.filer_499.state;
|
||||
} else {
|
||||
addrEl.textContent = "";
|
||||
}
|
||||
|
||||
// Details
|
||||
const detailsEl = document.getElementById("entity-details");
|
||||
let hasDetails = false;
|
||||
|
||||
function setDetail(id, label, value) {
|
||||
const el = document.getElementById(id);
|
||||
if (value && value !== "None" && String(value).trim() !== "") {
|
||||
el.innerHTML = `<span class="font-medium text-gray-700">${label}:</span> ${value}`;
|
||||
el.classList.remove("hidden");
|
||||
hasDetails = true;
|
||||
} else {
|
||||
el.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
setDetail("detail-filer-id", "Filer ID", data.filer_499?.filer_id || data.filer_id);
|
||||
setDetail("detail-comm-type", "Comm Type", data.filer_499?.comm_type || data.comm_type);
|
||||
setDetail("detail-rmd-number", "RMD Number", data.rmd?.rmd_number);
|
||||
|
||||
// USF Contributor
|
||||
const usfRaw = data.filer_499?.usf_contributor ?? data.usf_contributor ?? null;
|
||||
const usfDisplay = usfRaw === true ? "Yes" : usfRaw === false ? "No" : null;
|
||||
setDetail("detail-usf-contributor", "USF Contributor", usfDisplay);
|
||||
|
||||
// Provider types
|
||||
const ptContainer = document.getElementById("detail-provider-types");
|
||||
const providerTypes = data.rmd?.provider_types || [];
|
||||
if (providerTypes.length > 0) {
|
||||
ptContainer.innerHTML = '<span class="font-medium text-gray-700 mr-1">Provider Types:</span>' +
|
||||
providerTypes.map((t) => `<span class="inline-block bg-pw-100 text-pw-800 text-xs font-medium px-2 py-0.5 rounded-full">${t}</span>`).join("");
|
||||
ptContainer.classList.remove("hidden");
|
||||
hasDetails = true;
|
||||
} else {
|
||||
ptContainer.classList.add("hidden");
|
||||
}
|
||||
|
||||
setDetail("detail-other-frns", "Other FRNs", data.other_frns?.join(", "));
|
||||
setDetail("detail-previous-names", "Previous Names", data.previous_names?.join(", "));
|
||||
|
||||
if (hasDetails) {
|
||||
detailsEl.classList.remove("hidden");
|
||||
} else {
|
||||
detailsEl.classList.add("hidden");
|
||||
}
|
||||
|
||||
// Checks
|
||||
const checksContainer = document.getElementById("checks-container");
|
||||
checksContainer.innerHTML = "";
|
||||
const checks = data.checks || [];
|
||||
|
||||
const colorMap = {
|
||||
green: { bg: "bg-green-50", border: "border-green-200", iconColor: "text-green-600", textColor: "text-green-800" },
|
||||
yellow: { bg: "bg-amber-50", border: "border-amber-200", iconColor: "text-amber-600", textColor: "text-amber-800" },
|
||||
red: { bg: "bg-red-50", border: "border-red-200", iconColor: "text-red-600", textColor: "text-red-800" },
|
||||
unknown: { bg: "bg-gray-50", border: "border-gray-200", iconColor: "text-gray-500", textColor: "text-gray-700" },
|
||||
};
|
||||
|
||||
const icons = {
|
||||
green: `<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>`,
|
||||
yellow: `<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01M10.29 3.86l-8.8 15.32A1 1 0 002.36 21h19.28a1 1 0 00.87-1.5l-9.64-16.64a1 1 0 00-1.74 0z"/></svg>`,
|
||||
red: `<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>`,
|
||||
unknown: `<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01"/></svg>`,
|
||||
};
|
||||
|
||||
let hasRed = false;
|
||||
|
||||
checks.forEach((check) => {
|
||||
const status = check.status || "unknown";
|
||||
if (status === "red") hasRed = true;
|
||||
const c = colorMap[status] || colorMap.unknown;
|
||||
const icon = icons[status] || icons.unknown;
|
||||
|
||||
const card = document.createElement("div");
|
||||
card.className = `${c.bg} ${c.border} border rounded-xl p-4 flex items-start gap-3`;
|
||||
card.setAttribute("data-check-id", check.id);
|
||||
|
||||
let inner = `<div class="${c.iconColor} mt-0.5 flex-shrink-0">${icon}</div>`;
|
||||
inner += `<div class="flex-1">`;
|
||||
inner += `<p class="font-semibold ${c.textColor}">${check.label || check.id}</p>`;
|
||||
if (check.due_date) inner += `<p class="text-xs ${c.textColor} opacity-75 mt-0.5">Due: ${check.due_date}</p>`;
|
||||
if (check.detail) inner += `<p class="text-sm ${c.textColor} mt-1">${check.detail}</p>`;
|
||||
|
||||
// BDC special
|
||||
if (check.id === "bdc_filing" && status === "unknown") {
|
||||
inner += `<div class="mt-3">
|
||||
<p class="text-sm font-medium ${c.textColor} mb-2">Do you provide broadband internet access?</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="bdc-yes bg-amber-500 hover:bg-amber-600 text-white text-sm font-semibold px-4 py-1.5 rounded-lg transition">Yes</button>
|
||||
<button class="bdc-no bg-green-600 hover:bg-green-700 text-white text-sm font-semibold px-4 py-1.5 rounded-lg transition">No</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
inner += `</div>`;
|
||||
card.innerHTML = inner;
|
||||
|
||||
// BDC button handlers
|
||||
if (check.id === "bdc_filing" && status === "unknown") {
|
||||
card.querySelector(".bdc-yes")?.addEventListener("click", () => {
|
||||
check.status = "yellow";
|
||||
const cy = colorMap.yellow;
|
||||
card.className = `${cy.bg} ${cy.border} border rounded-xl p-4 flex items-start gap-3`;
|
||||
card.innerHTML = `<div class="${cy.iconColor} mt-0.5 flex-shrink-0">${icons.yellow}</div>
|
||||
<div class="flex-1">
|
||||
<p class="font-semibold ${cy.textColor}">${check.label || check.id}</p>
|
||||
${check.due_date ? `<p class="text-xs ${cy.textColor} opacity-75 mt-0.5">Due: ${check.due_date}</p>` : ""}
|
||||
${check.detail ? `<p class="text-sm ${cy.textColor} mt-1">${check.detail}</p>` : ""}
|
||||
</div>`;
|
||||
renderCta(lastData);
|
||||
});
|
||||
card.querySelector(".bdc-no")?.addEventListener("click", () => {
|
||||
check.status = "green";
|
||||
const cg = colorMap.green;
|
||||
card.className = `${cg.bg} ${cg.border} border rounded-xl p-4 flex items-start gap-3`;
|
||||
card.innerHTML = `<div class="${cg.iconColor} mt-0.5 flex-shrink-0">${icons.green}</div>
|
||||
<div class="flex-1">
|
||||
<p class="font-semibold ${cg.textColor}">${check.label || check.id}</p>
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
checksContainer.appendChild(card);
|
||||
});
|
||||
|
||||
// Prior-year
|
||||
document.getElementById("prior-year").classList.toggle("hidden", !hasRed);
|
||||
|
||||
// CTA
|
||||
renderCta(data);
|
||||
|
||||
// Timestamp
|
||||
document.getElementById("checked-at").textContent = "Checked at " + new Date().toLocaleString();
|
||||
|
||||
// Show results
|
||||
resultsEl.classList.remove("hidden");
|
||||
}
|
||||
|
||||
// --- Render CTA (separate function) ---
|
||||
function renderCta(data) {
|
||||
const checks = data.checks || [];
|
||||
const frn = data.frn || frnInput.value.trim();
|
||||
const services = [];
|
||||
|
||||
for (const check of checks) {
|
||||
const s = check.status || "unknown";
|
||||
if (s === "green") continue;
|
||||
|
||||
switch (check.id) {
|
||||
case "rmd_filing":
|
||||
services.push({ id: "rmd_filing", name: "RMD Registration", desc: "", price: 219 });
|
||||
break;
|
||||
case "stir_shaken":
|
||||
services.push({ id: "stir_shaken", name: "STIR/SHAKEN Implementation", desc: "", price: 499 });
|
||||
break;
|
||||
case "form_499a":
|
||||
services.push({ id: "form_499a", name: "Form 499-A Filing", desc: "", price: 499 });
|
||||
if (s === "red") {
|
||||
services.push({ id: "dc_agent", name: "D.C. Registered Agent", desc: "", price: 99, priceLabel: "$99/yr" });
|
||||
}
|
||||
break;
|
||||
case "form_499q":
|
||||
services.push({ id: "form_499q", name: "Form 499-Q Filing", desc: "included with 499-A", price: 0 });
|
||||
break;
|
||||
case "cpni_certification":
|
||||
services.push({ id: "cpni_certification", name: "CPNI Certification", desc: "", price: 149 });
|
||||
break;
|
||||
case "cores_registration":
|
||||
if (s === "red") {
|
||||
services.push({ id: "cores_registration", name: "FCC CORES Registration", desc: "included", price: 0 });
|
||||
}
|
||||
break;
|
||||
case "bdc_filing":
|
||||
if (s === "yellow") {
|
||||
services.push({ id: "bdc_filing", name: "BDC / Form 477 Filing", desc: "", price: 199 });
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const ctaContent = document.getElementById("cta-content");
|
||||
const paymentIcons = document.getElementById("payment-icons");
|
||||
const reminderCta = document.getElementById("reminder-cta");
|
||||
|
||||
if (services.length > 0) {
|
||||
let html = `<h3 class="text-lg font-bold text-gray-900 mb-4">We can help fix these items</h3>`;
|
||||
html += `<div class="space-y-2" id="service-list">`;
|
||||
|
||||
for (const svc of services) {
|
||||
const checked = svc.price > 0 && svc.id !== "stir_shaken" ? "checked" : "";
|
||||
const priceLabel = svc.priceLabel || (svc.price > 0 ? `$${svc.price}` : svc.desc || "included");
|
||||
html += `<label class="flex items-start gap-3 p-3 rounded-lg hover:bg-gray-50 cursor-pointer transition">
|
||||
<input type="checkbox" class="svc-checkbox mt-1 accent-pw-600" data-id="${svc.id}" data-price="${svc.price}" ${checked} />
|
||||
<div class="flex-1">
|
||||
<span class="font-medium text-gray-900">${svc.name}</span>
|
||||
${svc.desc && svc.price > 0 ? `<span class="text-xs text-gray-500 ml-1">${svc.desc}</span>` : ""}
|
||||
${svc.price === 0 && svc.desc ? `<span class="text-xs text-gray-500 ml-1">(${svc.desc})</span>` : ""}
|
||||
</div>
|
||||
<span class="font-semibold text-gray-700 text-sm whitespace-nowrap">${priceLabel}</span>
|
||||
</label>`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
html += `<div id="total-row" class="mt-4 pt-4 border-t border-gray-200"></div>`;
|
||||
html += `<div class="mt-4 text-center">
|
||||
<a id="get-started-btn" href="#" class="inline-block bg-orange-500 hover:bg-orange-600 text-white font-bold px-8 py-3 rounded-lg shadow-md transition text-sm">
|
||||
Get Started →
|
||||
</a>
|
||||
</div>`;
|
||||
|
||||
ctaContent.innerHTML = html;
|
||||
paymentIcons.classList.remove("hidden");
|
||||
reminderCta.classList.add("hidden");
|
||||
|
||||
// Total calculation
|
||||
function updateTotal() {
|
||||
const boxes = ctaContent.querySelectorAll(".svc-checkbox");
|
||||
let total = 0;
|
||||
let pricedCount = 0;
|
||||
const selectedIds = [];
|
||||
|
||||
boxes.forEach((cb) => {
|
||||
if (cb.checked) {
|
||||
const p = parseInt(cb.dataset.price, 10);
|
||||
selectedIds.push(cb.dataset.id);
|
||||
if (p > 0) {
|
||||
// Exclude dc_agent from discount calc
|
||||
if (cb.dataset.id !== "dc_agent") {
|
||||
pricedCount++;
|
||||
}
|
||||
total += p;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const totalRow = document.getElementById("total-row");
|
||||
let discountHtml = "";
|
||||
|
||||
if (pricedCount >= 2) {
|
||||
const discount = Math.round(total * 0.15);
|
||||
const final_ = total - discount;
|
||||
discountHtml = `<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">Subtotal</span>
|
||||
<span class="text-gray-600">$${total}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm text-green-600 font-medium">
|
||||
<span>15% bundle discount</span>
|
||||
<span>-$${discount}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-lg font-bold mt-1">
|
||||
<span>Total</span>
|
||||
<span>$${final_}</span>
|
||||
</div>`;
|
||||
} else if (pricedCount === 1) {
|
||||
discountHtml = `<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">Total</span>
|
||||
<span class="font-semibold">$${total}</span>
|
||||
</div>
|
||||
<p class="text-xs text-amber-600 mt-1">Add another service for 15% off</p>`;
|
||||
} else {
|
||||
discountHtml = `<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">Total</span>
|
||||
<span class="font-semibold">$${total}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
totalRow.innerHTML = discountHtml;
|
||||
|
||||
// Update link
|
||||
const btn = document.getElementById("get-started-btn");
|
||||
if (btn) {
|
||||
btn.href = `/order/fcc-compliance?services=${selectedIds.join(",")}&frn=${frn}`;
|
||||
}
|
||||
}
|
||||
|
||||
ctaContent.querySelectorAll(".svc-checkbox").forEach((cb) => {
|
||||
cb.addEventListener("change", updateTotal);
|
||||
});
|
||||
|
||||
updateTotal();
|
||||
} else {
|
||||
ctaContent.innerHTML = `<h3 class="text-lg font-bold text-green-800 mb-2">Looking good — all checks passed</h3>
|
||||
<p class="text-sm text-gray-600">Your filings appear current. No action is needed at this time.</p>`;
|
||||
paymentIcons.classList.add("hidden");
|
||||
reminderCta.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Auto-fill from URL ---
|
||||
(function init() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const frn = params.get("frn");
|
||||
const entity = params.get("entity");
|
||||
|
||||
if (frn) {
|
||||
frnInput.value = frn;
|
||||
runCheck();
|
||||
} else if (entity) {
|
||||
nameInput.value = entity;
|
||||
runNameSearch();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</Base>
|
||||
Loading…
Add table
Add a link
Reference in a new issue