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
449
site/src/components/intake/steps/EntityStep.astro
Normal file
449
site/src/components/intake/steps/EntityStep.astro
Normal file
|
|
@ -0,0 +1,449 @@
|
|||
---
|
||||
// EntityStep — collect or select the carrier identity. Also shown at
|
||||
// the top: filing-mode selector (current / past-due / revise prior).
|
||||
// On mount: fetches GET /api/v1/entities/telecom?email=... to offer pre-fill.
|
||||
---
|
||||
|
||||
<div class="pw-step pw-step-entity">
|
||||
<h2>Your carrier</h2>
|
||||
<p class="pw-help">
|
||||
Tell us about the entity we'll be filing for. If you've filed with us
|
||||
before, pick your existing carrier from the list.
|
||||
</p>
|
||||
|
||||
<fieldset class="pw-fieldset" id="pw-filing-mode-fs" hidden>
|
||||
<legend>What are you filing?</legend>
|
||||
<label class="pw-radio">
|
||||
<input type="radio" name="pw-filing-mode" value="current" checked />
|
||||
Current-year 499-A (annual filing, due April 1)
|
||||
</label>
|
||||
<label class="pw-radio">
|
||||
<input type="radio" name="pw-filing-mode" value="past_due" />
|
||||
Past-due filing for a prior year (late filing — expect penalties)
|
||||
</label>
|
||||
<label class="pw-radio">
|
||||
<input type="radio" name="pw-filing-mode" value="revised" />
|
||||
Revise a previously-filed 499-A (amendment)
|
||||
</label>
|
||||
|
||||
<div id="pw-past-due-wrap" hidden>
|
||||
<label class="pw-field">Reporting year (past-due)</label>
|
||||
<input type="number" id="pw-past-due-year" min="2015" max="2035" class="pw-input" />
|
||||
<div class="pw-help" id="pw-past-due-estimate"></div>
|
||||
</div>
|
||||
|
||||
<div id="pw-revised-wrap" hidden>
|
||||
<label class="pw-field">Which prior filing do you want to revise?</label>
|
||||
<select id="pw-revised-prior" class="pw-input">
|
||||
<option value="">— pick a prior filing —</option>
|
||||
</select>
|
||||
<label class="pw-field">What are you changing?</label>
|
||||
<select id="pw-revised-reason" class="pw-input">
|
||||
<option value="revenue">Revenue information</option>
|
||||
<option value="registration">Registration / contact info</option>
|
||||
<option value="both">Both</option>
|
||||
</select>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div id="pw-entity-picker" hidden>
|
||||
<label class="pw-field">Existing carrier on file</label>
|
||||
<select id="pw-entity-select" class="pw-input">
|
||||
<option value="">— New carrier —</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<label class="pw-field">Email address (yours)</label>
|
||||
<input type="email" id="pw-email" class="pw-input" required />
|
||||
|
||||
<label class="pw-field">Your name</label>
|
||||
<input type="text" id="pw-name" class="pw-input" required />
|
||||
|
||||
<label class="pw-field">Carrier legal name</label>
|
||||
<input type="text" id="pw-legal-name" class="pw-input" required />
|
||||
|
||||
<label class="pw-field">DBA (if different)</label>
|
||||
<input type="text" id="pw-dba" class="pw-input" />
|
||||
|
||||
<label class="pw-field">EIN</label>
|
||||
<input type="text" id="pw-ein" class="pw-input" placeholder="12-3456789" />
|
||||
|
||||
<label class="pw-field">FCC Registration Number (FRN)</label>
|
||||
<input type="text" id="pw-frn" class="pw-input" placeholder="10-digit (leave blank if applying via this order)" />
|
||||
|
||||
<label class="pw-field">USAC Filer ID (499)</label>
|
||||
<input type="text" id="pw-filer-id" class="pw-input" />
|
||||
|
||||
<div class="pw-row">
|
||||
<div>
|
||||
<label class="pw-field">Entity structure</label>
|
||||
<select id="pw-entity-structure" class="pw-input">
|
||||
<option value="">Select…</option>
|
||||
<option value="corp">Corporation (inc.)</option>
|
||||
<option value="llc">LLC</option>
|
||||
<option value="partnership">Partnership</option>
|
||||
<option value="sole_prop">Sole proprietorship</option>
|
||||
<option value="gov">Government entity</option>
|
||||
<option value="nonprofit">Nonprofit / 501(c)</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="display:none">
|
||||
<label class="pw-field">Carrier category</label>
|
||||
<select id="pw-carrier-category" class="pw-input">
|
||||
<option value="">Select…</option>
|
||||
<option value="interconnected_voip">Interconnected VoIP</option>
|
||||
<option value="non_interconnected_voip">Non-Interconnected VoIP</option>
|
||||
<option value="clec">CLEC</option>
|
||||
<option value="ixc">Interexchange Carrier</option>
|
||||
<option value="cmrs">CMRS / Wireless</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<fieldset class="pw-fieldset">
|
||||
<legend>Affiliated filer (Line 106)</legend>
|
||||
<p class="pw-help">If your company has affiliated filers (shared holding company), give the common name + holding company EIN. Must match Form 499-A Line 106 on every affiliate's filing.</p>
|
||||
<div class="pw-row">
|
||||
<div><label class="pw-field">Affiliated filer name (Line 106.1)</label>
|
||||
<input type="text" id="pw-aff-name" class="pw-input" placeholder="(leave blank if no affiliates)" /></div>
|
||||
<div><label class="pw-field">Holding company EIN (Line 106.2)</label>
|
||||
<input type="text" id="pw-aff-ein" class="pw-input" placeholder="12-3456789" /></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<label class="pw-field">Trade names / DBAs used in past 3 years (Line 112 — one per line)</label>
|
||||
<textarea id="pw-trade-names" class="pw-input" rows="2" placeholder="Leave blank if only the legal name + DBA above"></textarea>
|
||||
|
||||
<fieldset class="pw-fieldset">
|
||||
<legend>Principal address</legend>
|
||||
<label class="pw-field">Street</label>
|
||||
<input type="text" id="pw-addr-street" class="pw-input" />
|
||||
<div class="pw-row">
|
||||
<div><label class="pw-field">City</label>
|
||||
<input type="text" id="pw-addr-city" class="pw-input" /></div>
|
||||
<div><label class="pw-field">State</label>
|
||||
<input type="text" id="pw-addr-state" class="pw-input" maxlength="2" /></div>
|
||||
<div><label class="pw-field">ZIP</label>
|
||||
<input type="text" id="pw-addr-zip" class="pw-input" maxlength="10" /></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div id="pw-entity-err" class="pw-err" hidden></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.pw-step h2 { margin: 0 0 0.5rem; color: #1a2744; }
|
||||
.pw-help { color: #64748b; font-size: 0.9rem; margin-bottom: 1rem; }
|
||||
.pw-field { display: block; font-weight: 600; color: #1f2937; margin: 1rem 0 0.25rem; font-size: 0.9rem; }
|
||||
.pw-input { width: 100%; padding: 0.55rem 0.7rem; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 0.95rem; }
|
||||
.pw-row { display: flex; gap: 1rem; flex-wrap: wrap; }
|
||||
.pw-row > * { flex: 1 1 180px; }
|
||||
.pw-fieldset { border: 1px solid #e2e8f0; border-radius: 8px; padding: 0.75rem 1rem 1rem; margin-top: 1.25rem; }
|
||||
.pw-fieldset legend { font-weight: 600; color: #1a2744; padding: 0 0.5rem; }
|
||||
.pw-err { color: #b91c1c; margin-top: 0.75rem; font-size: 0.9rem; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const get = <T extends HTMLElement>(id: string) => document.getElementById(id) as T;
|
||||
const INPUTS = {
|
||||
email: get<HTMLInputElement>("pw-email"),
|
||||
name: get<HTMLInputElement>("pw-name"),
|
||||
legal_name: get<HTMLInputElement>("pw-legal-name"),
|
||||
dba_name: get<HTMLInputElement>("pw-dba"),
|
||||
ein: get<HTMLInputElement>("pw-ein"),
|
||||
frn: get<HTMLInputElement>("pw-frn"),
|
||||
filer_id_499: get<HTMLInputElement>("pw-filer-id"),
|
||||
carrier_category: get<HTMLSelectElement>("pw-carrier-category"),
|
||||
entity_structure: get<HTMLSelectElement>("pw-entity-structure"),
|
||||
aff_name: get<HTMLInputElement>("pw-aff-name"),
|
||||
aff_ein: get<HTMLInputElement>("pw-aff-ein"),
|
||||
trade_names: get<HTMLTextAreaElement>("pw-trade-names"),
|
||||
street: get<HTMLInputElement>("pw-addr-street"),
|
||||
city: get<HTMLInputElement>("pw-addr-city"),
|
||||
state: get<HTMLInputElement>("pw-addr-state"),
|
||||
zip: get<HTMLInputElement>("pw-addr-zip"),
|
||||
};
|
||||
const picker = get<HTMLSelectElement>("pw-entity-select");
|
||||
const pickerWrap = get<HTMLDivElement>("pw-entity-picker");
|
||||
const err = get<HTMLDivElement>("pw-entity-err");
|
||||
const fmFs = get<HTMLFieldSetElement>("pw-filing-mode-fs");
|
||||
const pastDueWrap = get<HTMLDivElement>("pw-past-due-wrap");
|
||||
const revisedWrap = get<HTMLDivElement>("pw-revised-wrap");
|
||||
const pastDueYear = get<HTMLInputElement>("pw-past-due-year");
|
||||
const pastDueEst = get<HTMLElement>("pw-past-due-estimate");
|
||||
const revisedPrior = get<HTMLSelectElement>("pw-revised-prior");
|
||||
const revisedReason = get<HTMLSelectElement>("pw-revised-reason");
|
||||
|
||||
// Show filing-mode fieldset only for 499-A family slugs
|
||||
const FILING_MODE_SLUGS = new Set([
|
||||
"fcc-499a", "fcc-499a-499q", "fcc-full-compliance",
|
||||
]);
|
||||
if (FILING_MODE_SLUGS.has((window as any).PWIntake?.slug || "")) {
|
||||
fmFs.hidden = false;
|
||||
}
|
||||
|
||||
function currentMode(): string {
|
||||
const el = document.querySelector<HTMLInputElement>(
|
||||
'input[name="pw-filing-mode"]:checked',
|
||||
);
|
||||
return el?.value || "current";
|
||||
}
|
||||
|
||||
function syncFilingModeVisibility() {
|
||||
const m = currentMode();
|
||||
pastDueWrap.hidden = m !== "past_due";
|
||||
revisedWrap.hidden = m !== "revised";
|
||||
}
|
||||
|
||||
document.querySelectorAll<HTMLInputElement>('input[name="pw-filing-mode"]')
|
||||
.forEach((r) => r.addEventListener("change", () => {
|
||||
syncFilingModeVisibility();
|
||||
if (currentMode() === "revised") loadPriorFilings();
|
||||
}));
|
||||
|
||||
async function loadPriorFilings() {
|
||||
const state = (window as any).PWIntake.get();
|
||||
if (!state.telecom_entity_id) {
|
||||
revisedPrior.innerHTML =
|
||||
'<option value="">— save entity first, then revise —</option>';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const r = await fetch(`/api/v1/fcc/filings/entity/${state.telecom_entity_id}`);
|
||||
if (!r.ok) return;
|
||||
const data = await r.json();
|
||||
revisedPrior.innerHTML = '<option value="">— pick a prior filing —</option>';
|
||||
for (const f of (data.filings || [])) {
|
||||
const yr = f.form_year_declared || new Date(f.created_at).getUTCFullYear();
|
||||
const opt = document.createElement("option");
|
||||
opt.value = f.order_number;
|
||||
opt.textContent = `${f.order_number} (${f.service_slug}, year ${yr})`;
|
||||
revisedPrior.appendChild(opt);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function refreshPastDueEstimate() {
|
||||
const year = Number(pastDueYear.value);
|
||||
const state = (window as any).PWIntake.get();
|
||||
const totalRev = Number(state.intake_data?.total_revenue_cents) || 0;
|
||||
const interPct = Number(state.intake_data?.interstate_pct) || 0;
|
||||
if (!year || !totalRev) {
|
||||
pastDueEst.textContent = "";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const r = await fetch(
|
||||
`/api/v1/fcc/late-filing-estimate?year=${year}&total_revenue_cents=${totalRev}&interstate_pct=${interPct}`,
|
||||
);
|
||||
if (!r.ok) {
|
||||
const err = await r.json();
|
||||
pastDueEst.textContent = `Estimator unavailable: ${err.error || r.status}`;
|
||||
return;
|
||||
}
|
||||
const d = await r.json();
|
||||
pastDueEst.innerHTML =
|
||||
`Estimated retroactive USF owed for ${year}: <strong>$${(d.estimated_usf_cents/100).toLocaleString("en-US",{minimumFractionDigits:2})}</strong>` +
|
||||
(d.estimated_interest_cents > 0 ? ` + ~$${(d.estimated_interest_cents/100).toLocaleString("en-US",{minimumFractionDigits:2})} interest (est.)` : "") +
|
||||
`. USAC also may assess forfeitures separately.`;
|
||||
} catch {
|
||||
pastDueEst.textContent = "";
|
||||
}
|
||||
}
|
||||
pastDueYear.addEventListener("change", refreshPastDueEstimate);
|
||||
|
||||
function intoInputs(entity: any) {
|
||||
INPUTS.legal_name.value = entity?.legal_name || "";
|
||||
INPUTS.dba_name.value = entity?.dba_name || "";
|
||||
INPUTS.ein.value = entity?.ein || "";
|
||||
INPUTS.frn.value = entity?.frn || "";
|
||||
INPUTS.filer_id_499.value = entity?.filer_id_499 || "";
|
||||
INPUTS.carrier_category.value = entity?.carrier_category || "";
|
||||
INPUTS.entity_structure.value = entity?.entity_structure || "";
|
||||
INPUTS.aff_name.value = entity?.affiliated_filer_name || "";
|
||||
INPUTS.aff_ein.value = entity?.affiliated_filer_ein || "";
|
||||
INPUTS.trade_names.value = (entity?.trade_names || []).join("\n");
|
||||
INPUTS.street.value = entity?.address_street || "";
|
||||
INPUTS.city.value = entity?.address_city || "";
|
||||
INPUTS.state.value = entity?.address_state || "";
|
||||
INPUTS.zip.value = entity?.address_zip || "";
|
||||
}
|
||||
|
||||
async function loadExisting() {
|
||||
const email = INPUTS.email.value.trim();
|
||||
if (!email) return;
|
||||
try {
|
||||
const resp = await fetch(`/api/v1/entities/telecom?email=${encodeURIComponent(email)}`);
|
||||
const data = await resp.json();
|
||||
const entities = data.entities || [];
|
||||
picker.innerHTML = '<option value="">— New carrier —</option>';
|
||||
for (const e of entities) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = String(e.id);
|
||||
opt.textContent = `${e.legal_name} ${e.frn ? "(FRN " + e.frn + ")" : ""}`;
|
||||
picker.appendChild(opt);
|
||||
}
|
||||
pickerWrap.hidden = entities.length === 0;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
picker.addEventListener("change", async () => {
|
||||
const id = picker.value;
|
||||
const PW = (window as any).PWIntake;
|
||||
if (!id) {
|
||||
PW.set({ telecom_entity_id: null });
|
||||
intoInputs({});
|
||||
return;
|
||||
}
|
||||
const email = INPUTS.email.value.trim();
|
||||
const resp = await fetch(
|
||||
`/api/v1/entities/telecom/${id}?email=${encodeURIComponent(email)}`,
|
||||
);
|
||||
const data = await resp.json();
|
||||
intoInputs(data);
|
||||
PW.set({ telecom_entity_id: Number(id), entity: data });
|
||||
});
|
||||
|
||||
INPUTS.email.addEventListener("blur", loadExisting);
|
||||
|
||||
// When this step is shown, re-hydrate form from state
|
||||
// Auto-fill entity from FRN query param (from compliance check tool)
|
||||
async function autoFillFromFRN(frn: string) {
|
||||
try {
|
||||
const API = (window as any).__PW_API || "";
|
||||
const r = await fetch(`${API}/api/v1/fcc/lookup?frn=${frn}`);
|
||||
if (!r.ok) return;
|
||||
const d = await r.json();
|
||||
const entity: any = {};
|
||||
// Map FCC lookup data to entity fields
|
||||
entity.frn = d.frn || frn;
|
||||
entity.legal_name = d.entity_name || "";
|
||||
if (d.cores) {
|
||||
entity.address_street = d.cores.address || "";
|
||||
entity.address_city = d.cores.city || "";
|
||||
entity.address_state = d.cores.state || "";
|
||||
entity.address_zip = d.cores.zip || "";
|
||||
}
|
||||
if (d.filer_499) {
|
||||
entity.filer_id_499 = d.filer_499.filer_id || "";
|
||||
entity.dba_name = d.filer_499.trade_name || "";
|
||||
if (!entity.legal_name) entity.legal_name = d.filer_499.legal_name || "";
|
||||
}
|
||||
if (d.rmd?.contact_name) {
|
||||
entity.ceo_name = d.rmd.contact_name;
|
||||
entity.contact_name = d.rmd.contact_name;
|
||||
}
|
||||
intoInputs(entity);
|
||||
// Also try loading from our DB in case we have a richer record
|
||||
const dbResp = await fetch(`${API}/api/v1/cdr/profile/by-entity/${frn}`).catch(() => null);
|
||||
// Look up by FRN in telecom_entities
|
||||
try {
|
||||
const teResp = await fetch(`${API}/api/v1/entities/telecom?frn=${frn}`);
|
||||
if (teResp.ok) {
|
||||
const teData = await teResp.json();
|
||||
const entities = teData.entities || [];
|
||||
if (entities.length > 0) {
|
||||
intoInputs(entities[0]); // DB record is richer, overwrite
|
||||
const PW = (window as any).PWIntake;
|
||||
PW.set({ telecom_entity_id: entities[0].id, entity: entities[0] });
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch (e) {
|
||||
console.warn("FRN auto-fill failed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("pw:step-shown", (evt: any) => {
|
||||
if (evt.detail.step !== "entity") return;
|
||||
const state = (window as any).PWIntake.get();
|
||||
INPUTS.email.value = state.email || "";
|
||||
INPUTS.name.value = state.name || "";
|
||||
intoInputs(state.entity || {});
|
||||
if (state.email) loadExisting();
|
||||
|
||||
// Auto-fill from ?frn= URL param (first load only)
|
||||
const urlFrn = new URLSearchParams(window.location.search).get("frn");
|
||||
if (urlFrn && !state.entity?.frn) {
|
||||
autoFillFromFRN(urlFrn);
|
||||
}
|
||||
// Restore filing mode
|
||||
const mode = (state as any).filing_mode || "current";
|
||||
const rb = document.querySelector<HTMLInputElement>(
|
||||
`input[name="pw-filing-mode"][value="${mode}"]`,
|
||||
);
|
||||
if (rb) rb.checked = true;
|
||||
syncFilingModeVisibility();
|
||||
if ((state as any).form_year_override) {
|
||||
pastDueYear.value = String((state as any).form_year_override);
|
||||
refreshPastDueEstimate();
|
||||
}
|
||||
if (mode === "revised") loadPriorFilings();
|
||||
});
|
||||
|
||||
// Gate advancing until required fields are filled
|
||||
window.addEventListener("pw:step-next", (evt: any) => {
|
||||
const active = (window as any).PWIntake.get();
|
||||
if (active.step_index !== (window as any).PWIntake.steps.indexOf("entity")) return;
|
||||
const missing: string[] = [];
|
||||
if (!INPUTS.email.value.trim()) missing.push("email");
|
||||
if (!INPUTS.name.value.trim()) missing.push("your name");
|
||||
if (!INPUTS.legal_name.value.trim()) missing.push("legal name");
|
||||
// Affiliated filer: name+EIN required together
|
||||
const affName = INPUTS.aff_name.value.trim();
|
||||
const affEin = INPUTS.aff_ein.value.trim();
|
||||
if (affName && !affEin) {
|
||||
missing.push("holding company EIN (required if affiliated filer name is given)");
|
||||
}
|
||||
if (missing.length) {
|
||||
err.hidden = false;
|
||||
err.textContent = `Required: ${missing.join(", ")}`;
|
||||
evt.preventDefault();
|
||||
return;
|
||||
}
|
||||
// Validate filing-mode fields
|
||||
const mode = currentMode();
|
||||
if (mode === "past_due" && !pastDueYear.value) {
|
||||
missing.push("past-due reporting year");
|
||||
}
|
||||
if (mode === "revised" && !revisedPrior.value) {
|
||||
missing.push("prior filing to revise");
|
||||
}
|
||||
if (missing.length) {
|
||||
err.hidden = false;
|
||||
err.textContent = `Required: ${missing.join(", ")}`;
|
||||
evt.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
err.hidden = true;
|
||||
const tradeNames = INPUTS.trade_names.value.split("\n")
|
||||
.map((s) => s.trim()).filter(Boolean);
|
||||
(window as any).PWIntake.set({
|
||||
email: INPUTS.email.value.trim().toLowerCase(),
|
||||
name: INPUTS.name.value.trim(),
|
||||
filing_mode: mode,
|
||||
form_year_override: mode === "past_due" ? Number(pastDueYear.value) : null,
|
||||
revises_order_number: mode === "revised" ? revisedPrior.value : null,
|
||||
revised_reason: mode === "revised" ? revisedReason.value : null,
|
||||
entity: {
|
||||
legal_name: INPUTS.legal_name.value.trim(),
|
||||
dba_name: INPUTS.dba_name.value.trim(),
|
||||
ein: INPUTS.ein.value.trim(),
|
||||
frn: INPUTS.frn.value.trim(),
|
||||
filer_id_499: INPUTS.filer_id_499.value.trim(),
|
||||
carrier_category: INPUTS.carrier_category.value,
|
||||
entity_structure: INPUTS.entity_structure.value,
|
||||
affiliated_filer_name: affName || null,
|
||||
affiliated_filer_ein: affEin || null,
|
||||
trade_names: tradeNames,
|
||||
address_street: INPUTS.street.value.trim(),
|
||||
address_city: INPUTS.city.value.trim(),
|
||||
address_state: INPUTS.state.value.trim().toUpperCase(),
|
||||
address_zip: INPUTS.zip.value.trim(),
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue