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>
241 lines
9.7 KiB
Text
241 lines
9.7 KiB
Text
---
|
|
// 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>
|