Critical: - Single-order discount used wrong column names (discount_pct/discount_flat_cents → discount_type/discount_value). Discounts were silently $0. - Single-order discount skipped allowed_emails and expires_at checks - Free orders now set paid_at = NOW() High: - Discount usage now tracked in discount_usage table + current_uses incremented - Flat discount only replaces bundle when flat >= bundle (was always replacing) Minor: - Removed unused CDR profile fetch in EntityStep Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
465 lines
20 KiB
Text
465 lines
20 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">Your email address</label>
|
|
<p class="text-xs text-gray-500 -mt-1 mb-1">This is for communication between you and Performance West only. It will not be included in any FCC filing. We use it to send you filing updates, documents for review, and your completed filings.</p>
|
|
<input type="email" id="pw-email" class="pw-input" required />
|
|
|
|
<label class="pw-field">Your first and last name</label>
|
|
<input type="text" id="pw-name" class="pw-input" required placeholder="e.g. Jane Smith" />
|
|
|
|
<label class="pw-field">Carrier entity legal name</label>
|
|
<input type="text" id="pw-legal-name" class="pw-input" required placeholder="e.g. Acme LLC or Gadgets Inc" />
|
|
|
|
<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>
|
|
<p class="text-xs text-gray-500 -mt-1 mb-1">6-digit number starting with 8 (e.g. 834314). Found on your Form 499-A or at <a href="https://apps.fcc.gov/cgb/form499/499a.cfm" target="_blank" class="text-pw-600 underline">USAC E-File</a>.</p>
|
|
<input type="text" id="pw-filer-id" class="pw-input" placeholder="e.g. 834314" maxlength="6" pattern="8\d{5}" />
|
|
|
|
<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);
|
|
// Look up by FRN in telecom_entities (richer record)
|
|
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, or from order's intake_data)
|
|
const urlFrn = new URLSearchParams(window.location.search).get("frn")
|
|
|| state.intake_data?.frn
|
|
|| state.entity?.frn
|
|
|| "";
|
|
if (urlFrn && !state.entity?.legal_name) {
|
|
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>
|