462 lines
19 KiB
Text
462 lines
19 KiB
Text
---
|
|
// 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">TIN / EIN</label>
|
|
<input type="text" id="pw-ein" class="pw-input" placeholder="12-3456789 or 123-45-6789" />
|
|
|
|
<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}&quick=1`);
|
|
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 address");
|
|
if (!INPUTS.name.value.trim()) missing.push("your name");
|
|
if (!INPUTS.legal_name.value.trim()) missing.push("carrier legal name");
|
|
if (!INPUTS.entity_structure.value) missing.push("entity structure");
|
|
// TIN/EIN validation: EIN (XX-XXXXXXX) or SSN (XXX-XX-XXXX)
|
|
const ein = INPUTS.ein.value.trim().replace(/[^0-9-]/g, "");
|
|
if (!ein) {
|
|
missing.push("TIN / EIN");
|
|
} else if (!/^\d{2}-?\d{7}$/.test(ein) && !/^\d{3}-?\d{2}-?\d{4}$/.test(ein)) {
|
|
missing.push("valid TIN / EIN (format: 12-3456789 or 123-45-6789)");
|
|
}
|
|
// Address — at minimum need state
|
|
if (!INPUTS.street.value.trim()) missing.push("street address");
|
|
if (!INPUTS.city.value.trim()) missing.push("city");
|
|
if (!INPUTS.state.value.trim()) missing.push("state");
|
|
if (!INPUTS.zip.value.trim()) missing.push("ZIP code");
|
|
// 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>
|