new-site/site/src/components/intake/steps/EntityStep.astro
justin ab7a2d7dc0 Rename EIN to TIN/EIN, accept SSN format (XXX-XX-XXXX)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 18:53:24 -05:00

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>