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
241
site/src/components/intake/steps/ForeignQualStep.astro
Normal file
241
site/src/components/intake/steps/ForeignQualStep.astro
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
---
|
||||
// ForeignQualStep — multi-state COA picker with per-state fee preview.
|
||||
// Calls GET /api/v1/foreign-qualification/jurisdictions to load the
|
||||
// state catalog, then POST /quote to update totals in real-time.
|
||||
---
|
||||
|
||||
<div class="pw-step">
|
||||
<h2>Select target states for foreign qualification</h2>
|
||||
<p class="pw-help">
|
||||
Choose every state where your entity needs authorization to do business.
|
||||
Each state charges its own Certificate of Authority fee. We also provision
|
||||
a registered agent in each target state ($125/yr each).
|
||||
</p>
|
||||
|
||||
<div class="pw-fq-controls">
|
||||
<label class="pw-fq-et">
|
||||
Entity type:
|
||||
<select id="pw-fq-entity-type">
|
||||
<option value="llc">LLC</option>
|
||||
<option value="corporation">Corporation</option>
|
||||
<option value="s_corp">S-Corp</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="pw-fq-home">
|
||||
Home state (where formed):
|
||||
<input type="text" id="pw-fq-home-state" maxlength="2" placeholder="WY" style="width:50px;text-transform:uppercase" />
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" id="pw-fq-ra" checked /> Include registered agent ($125/yr each)
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" id="pw-fq-expedited" /> Expedited processing
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="pw-fq-actions">
|
||||
<button type="button" id="pw-fq-select-all" class="pw-btn-plain pw-btn">Select all</button>
|
||||
<button type="button" id="pw-fq-clear" class="pw-btn-plain pw-btn">Clear</button>
|
||||
<span id="pw-fq-count">0 states selected</span>
|
||||
</div>
|
||||
|
||||
<div id="pw-fq-grid" class="pw-fq-grid"></div>
|
||||
|
||||
<div id="pw-fq-quote" class="pw-fq-quote" hidden>
|
||||
<table id="pw-fq-quote-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>State</th>
|
||||
<th>State fee</th>
|
||||
<th>RA</th>
|
||||
<th>Service</th>
|
||||
<th>Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="pw-fq-quote-body"></tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th colspan="4" style="text-align:right">Grand total:</th>
|
||||
<th id="pw-fq-grand-total">$0</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="pw-fq-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-fq-controls { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; margin-bottom: 1rem; }
|
||||
.pw-fq-controls label { font-size: 0.85rem; color: #475569; display: flex; align-items: center; gap: 0.35rem; }
|
||||
.pw-fq-controls select, .pw-fq-controls input[type="text"] { padding: 0.3rem 0.5rem; border: 1px solid #cbd5e1; border-radius: 4px; }
|
||||
.pw-fq-actions { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.75rem; }
|
||||
.pw-btn { padding: 0.4rem 0.9rem; border: 0; border-radius: 6px; font-size: 0.85rem; cursor: pointer; }
|
||||
.pw-btn-plain { background: #e2e8f0; color: #1f2937; }
|
||||
#pw-fq-count { color: #475569; font-size: 0.85rem; margin-left: 0.5rem; }
|
||||
.pw-fq-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 0.5rem; }
|
||||
.pw-fq-grid label { display: flex; align-items: center; gap: 0.35rem; font-size: 0.85rem;
|
||||
padding: 0.4rem 0.6rem; border: 1px solid #e2e8f0; border-radius: 6px; cursor: pointer; }
|
||||
.pw-fq-grid label:has(input:checked) { background: #e0f2fe; border-color: #38bdf8; }
|
||||
.pw-fq-grid .pw-fq-fee { color: #64748b; font-size: 0.75rem; margin-left: auto; }
|
||||
.pw-fq-quote { margin-top: 1rem; }
|
||||
.pw-fq-quote table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
|
||||
.pw-fq-quote th, .pw-fq-quote td { padding: 0.35rem 0.5rem; border-bottom: 1px solid #e2e8f0; text-align: right; }
|
||||
.pw-fq-quote th:first-child, .pw-fq-quote td:first-child { text-align: left; }
|
||||
.pw-fq-quote thead th { background: #f8fafc; font-size: 0.75rem; text-transform: uppercase; color: #475569; }
|
||||
.pw-fq-quote tfoot th { background: #f1f5f9; font-size: 0.95rem; }
|
||||
.pw-err { color: #b91c1c; margin-top: 0.75rem; font-size: 0.9rem; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const grid = document.getElementById("pw-fq-grid")!;
|
||||
const countEl = document.getElementById("pw-fq-count")!;
|
||||
const quoteDiv = document.getElementById("pw-fq-quote") as HTMLElement;
|
||||
const quoteBody = document.getElementById("pw-fq-quote-body")!;
|
||||
const grandEl = document.getElementById("pw-fq-grand-total")!;
|
||||
const err = document.getElementById("pw-fq-err") as HTMLDivElement;
|
||||
const entityTypeEl = document.getElementById("pw-fq-entity-type") as HTMLSelectElement;
|
||||
const homeStateEl = document.getElementById("pw-fq-home-state") as HTMLInputElement;
|
||||
const raEl = document.getElementById("pw-fq-ra") as HTMLInputElement;
|
||||
const expEl = document.getElementById("pw-fq-expedited") as HTMLInputElement;
|
||||
|
||||
interface JurisdictionRow {
|
||||
code: string; name: string; foreign_llc_fee: number | null;
|
||||
foreign_corp_fee: number | null; publication_required: boolean;
|
||||
typical_processing_days: number | null;
|
||||
}
|
||||
|
||||
let jurisdictions: JurisdictionRow[] = [];
|
||||
|
||||
async function loadJurisdictions() {
|
||||
try {
|
||||
const r = await fetch("/api/v1/foreign-qualification/jurisdictions");
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||
const d = await r.json();
|
||||
jurisdictions = d.jurisdictions || [];
|
||||
renderGrid();
|
||||
} catch (e) {
|
||||
err.hidden = false;
|
||||
err.textContent = `Failed to load jurisdictions: ${(e as Error).message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderGrid() {
|
||||
grid.innerHTML = "";
|
||||
for (const j of jurisdictions) {
|
||||
const et = entityTypeEl.value;
|
||||
const fee = et === "llc" || et === "pllc" ? j.foreign_llc_fee : j.foreign_corp_fee;
|
||||
const lbl = document.createElement("label");
|
||||
lbl.innerHTML =
|
||||
`<input type="checkbox" data-code="${j.code}" />` +
|
||||
`<span>${j.code} — ${j.name}</span>` +
|
||||
`<span class="pw-fq-fee">${fee != null ? "$" + (fee / 100).toFixed(0) : "—"}</span>`;
|
||||
grid.appendChild(lbl);
|
||||
}
|
||||
grid.querySelectorAll("input").forEach(cb => cb.addEventListener("change", updateCount));
|
||||
}
|
||||
|
||||
function getSelected(): string[] {
|
||||
return Array.from(grid.querySelectorAll<HTMLInputElement>("input:checked"))
|
||||
.map(cb => cb.dataset.code!);
|
||||
}
|
||||
|
||||
function updateCount() {
|
||||
const sel = getSelected();
|
||||
countEl.textContent = `${sel.length} state${sel.length === 1 ? "" : "s"} selected`;
|
||||
if (sel.length > 0) fetchQuote(sel);
|
||||
else { quoteDiv.hidden = true; }
|
||||
}
|
||||
|
||||
async function fetchQuote(states: string[]) {
|
||||
try {
|
||||
const r = await fetch("/api/v1/foreign-qualification/quote", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
home_state_code: homeStateEl.value.trim().toUpperCase() || "WY",
|
||||
entity_type: entityTypeEl.value,
|
||||
target_states: states,
|
||||
include_ra_each: raEl.checked,
|
||||
expedited: expEl.checked,
|
||||
}),
|
||||
});
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||
const d = await r.json();
|
||||
renderQuote(d);
|
||||
} catch (e) {
|
||||
err.hidden = false;
|
||||
err.textContent = `Quote failed: ${(e as Error).message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderQuote(d: any) {
|
||||
quoteBody.innerHTML = "";
|
||||
for (const it of (d.items || [])) {
|
||||
if (it.error) continue;
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML =
|
||||
`<td>${it.state_code} — ${it.state_name}</td>` +
|
||||
`<td>$${(it.state_fee_cents / 100).toFixed(0)}</td>` +
|
||||
`<td>$${(it.nwra_ra_fee_cents / 100).toFixed(0)}</td>` +
|
||||
`<td>$${(it.service_fee_cents / 100).toFixed(0)}</td>` +
|
||||
`<td><strong>$${(it.total_cents / 100).toFixed(2)}</strong></td>`;
|
||||
quoteBody.appendChild(tr);
|
||||
}
|
||||
grandEl.textContent = `$${(d.grand_total_cents / 100).toLocaleString("en-US", { minimumFractionDigits: 2 })}`;
|
||||
quoteDiv.hidden = false;
|
||||
err.hidden = true;
|
||||
}
|
||||
|
||||
document.getElementById("pw-fq-select-all")!.addEventListener("click", () => {
|
||||
grid.querySelectorAll<HTMLInputElement>("input").forEach(cb => cb.checked = true);
|
||||
updateCount();
|
||||
});
|
||||
document.getElementById("pw-fq-clear")!.addEventListener("click", () => {
|
||||
grid.querySelectorAll<HTMLInputElement>("input").forEach(cb => cb.checked = false);
|
||||
updateCount();
|
||||
});
|
||||
entityTypeEl.addEventListener("change", () => { renderGrid(); updateCount(); });
|
||||
raEl.addEventListener("change", () => { const s = getSelected(); if (s.length) fetchQuote(s); });
|
||||
expEl.addEventListener("change", () => { const s = getSelected(); if (s.length) fetchQuote(s); });
|
||||
|
||||
// Prefill from wizard state.
|
||||
window.addEventListener("pw:step-shown", (evt: any) => {
|
||||
if (evt.detail.step !== "foreign_qual") return;
|
||||
const s = (window as any).PWIntake.get();
|
||||
if (s.entity?.address_state) homeStateEl.value = s.entity.address_state;
|
||||
if (s.entity?.entity_type) entityTypeEl.value = s.entity.entity_type;
|
||||
if (s.intake_data?.target_states) {
|
||||
const prev = s.intake_data.target_states as string[];
|
||||
grid.querySelectorAll<HTMLInputElement>("input").forEach(cb => {
|
||||
if (prev.includes(cb.dataset.code!)) cb.checked = true;
|
||||
});
|
||||
updateCount();
|
||||
}
|
||||
});
|
||||
|
||||
// Persist into wizard state on Next.
|
||||
window.addEventListener("pw:step-next", (evt: any) => {
|
||||
const PW = (window as any).PWIntake;
|
||||
if (PW.steps[PW.get().step_index] !== "foreign_qual") return;
|
||||
const sel = getSelected();
|
||||
if (sel.length === 0) {
|
||||
err.hidden = false;
|
||||
err.textContent = "Select at least one target state.";
|
||||
evt.preventDefault();
|
||||
return;
|
||||
}
|
||||
err.hidden = true;
|
||||
PW.patchIntakeData({
|
||||
home_state_code: homeStateEl.value.trim().toUpperCase(),
|
||||
entity_type: entityTypeEl.value,
|
||||
target_states: sel,
|
||||
include_ra_each: raEl.checked,
|
||||
expedited: expEl.checked,
|
||||
});
|
||||
});
|
||||
|
||||
loadJurisdictions();
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue