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
191
site/src/components/intake/steps/ReviewStep.astro
Normal file
191
site/src/components/intake/steps/ReviewStep.astro
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
---
|
||||
// ReviewStep — summary panel, server-side validation hook, price display.
|
||||
export interface Props { service_slug: string; }
|
||||
const { service_slug } = Astro.props;
|
||||
import { SERVICE_META, formatUSD } from "../../../lib/intake_manifest";
|
||||
const meta = SERVICE_META[service_slug];
|
||||
---
|
||||
|
||||
<div class="pw-step" data-slug={service_slug}>
|
||||
<h2>Review</h2>
|
||||
<p class="pw-help">
|
||||
Review what we'll send to the FCC/USAC on your behalf. Click Finish
|
||||
to run validation + continue to payment.
|
||||
</p>
|
||||
|
||||
<div class="pw-summary" id="pw-review-pane">
|
||||
<h3>Service</h3>
|
||||
<div class="pw-summary-row">
|
||||
<span>{meta?.name ?? service_slug}</span>
|
||||
<strong>{meta ? formatUSD(meta.price_cents) : "—"}</strong>
|
||||
</div>
|
||||
|
||||
<h3>Carrier</h3>
|
||||
<div id="pw-summary-entity" class="pw-summary-sec"></div>
|
||||
|
||||
<h3>Order details</h3>
|
||||
<div id="pw-summary-intake" class="pw-summary-sec"></div>
|
||||
</div>
|
||||
|
||||
<div id="pw-review-errors" class="pw-err" hidden></div>
|
||||
<div id="pw-review-warnings" class="pw-warn" 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-summary { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 1rem 1.25rem; }
|
||||
.pw-summary h3 { color: #1a2744; margin: 1rem 0 0.4rem; font-size: 1rem; }
|
||||
.pw-summary h3:first-child { margin-top: 0; }
|
||||
.pw-summary-row { display: flex; justify-content: space-between; padding: 0.3rem 0; font-size: 0.95rem; border-bottom: 1px solid #e2e8f0; }
|
||||
.pw-summary-row:last-child { border: 0; }
|
||||
.pw-summary-sec { font-size: 0.9rem; color: #475569; padding: 0.3rem 0; }
|
||||
.pw-summary-sec pre { font-family: ui-monospace, SFMono-Regular, monospace; font-size: 0.8rem; background: #fff; padding: 0.5rem; border-radius: 4px; overflow-x: auto; }
|
||||
.pw-err { color: #b91c1c; margin-top: 0.75rem; font-size: 0.9rem; background: #fee2e2; padding: 0.5rem 0.75rem; border-radius: 6px; }
|
||||
.pw-warn { color: #92400e; margin-top: 0.75rem; font-size: 0.9rem; background: #fef3c7; padding: 0.5rem 0.75rem; border-radius: 6px; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const slug = document.querySelector(".pw-step[data-slug]")!.getAttribute("data-slug")!;
|
||||
const entDiv = document.getElementById("pw-summary-entity")!;
|
||||
const intakeDiv = document.getElementById("pw-summary-intake")!;
|
||||
const errDiv = document.getElementById("pw-review-errors") as HTMLDivElement;
|
||||
const warnDiv = document.getElementById("pw-review-warnings") as HTMLDivElement;
|
||||
|
||||
function renderReview() {
|
||||
const s = (window as any).PWIntake.get();
|
||||
const e = s.entity || {};
|
||||
entDiv.innerHTML = `
|
||||
<div><strong>${e.legal_name || "(name pending)"}</strong>
|
||||
${e.dba_name ? ` d/b/a ${e.dba_name}` : ""}</div>
|
||||
<div>${e.frn ? `FRN ${e.frn}` : "<em>FRN pending (we'll register it if needed)</em>"}
|
||||
${e.filer_id_499 ? ` · Filer ID ${e.filer_id_499}` : ""}</div>
|
||||
<div>${[e.address_street, e.address_city,
|
||||
(e.address_state && e.address_zip) ? `${e.address_state} ${e.address_zip}` : ""].filter(Boolean).join(", ") || "(address pending)"}</div>
|
||||
<div>${e.carrier_category || ""}</div>
|
||||
`;
|
||||
intakeDiv.innerHTML = `<pre>${JSON.stringify(s.intake_data || {}, null, 2)}</pre>`;
|
||||
}
|
||||
|
||||
async function runValidate(): Promise<{ ok: boolean; missing: string[]; soft: string[] }> {
|
||||
// /validate expects the order to exist — we create a draft here if
|
||||
// we haven't yet. Draft orders are created with payment_status=pending_payment.
|
||||
const state = (window as any).PWIntake.get();
|
||||
// Step 1: ensure we have an order_number for this session
|
||||
let orderNumber = state.order_number;
|
||||
if (!orderNumber) {
|
||||
const createResp = await fetch("/api/v1/compliance-orders", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
service_slug: slug,
|
||||
customer_email: state.email,
|
||||
customer_name: state.name,
|
||||
telecom_entity_id: state.telecom_entity_id,
|
||||
intake_data: state.intake_data,
|
||||
// Filing mode + de minimis election (migrations 058/059)
|
||||
filing_mode: (state as any).filing_mode || "current",
|
||||
form_year_override: (state as any).form_year_override || null,
|
||||
revises_order_number: (state as any).revises_order_number || null,
|
||||
revised_reason: (state as any).revised_reason || null,
|
||||
waive_deminimis_exemption: (state as any).waive_deminimis_exemption === true,
|
||||
waive_deminimis_reason: (state as any).waive_deminimis_reason || null,
|
||||
}),
|
||||
});
|
||||
if (!createResp.ok) throw new Error(`order create HTTP ${createResp.status}`);
|
||||
const created = await createResp.json();
|
||||
orderNumber = created.order_number;
|
||||
(window as any).PWIntake.set({ ...state, order_number: orderNumber });
|
||||
}
|
||||
// Step 2: validate
|
||||
const vResp = await fetch(
|
||||
`/api/v1/compliance-orders/${orderNumber}/validate`,
|
||||
{ method: "POST" },
|
||||
);
|
||||
const data = await vResp.json();
|
||||
return {
|
||||
ok: data.ok === true,
|
||||
missing: data.missing || [],
|
||||
rejections: data.rejections || [],
|
||||
soft: data.soft_warnings || [],
|
||||
calculations: data.calculations || {},
|
||||
};
|
||||
}
|
||||
|
||||
function renderDeMinimis(w: any): string {
|
||||
if (!w) return "";
|
||||
const dollars = (c: number) => "$" + (c / 100).toLocaleString("en-US", { minimumFractionDigits: 2 });
|
||||
return `
|
||||
<div class="pw-deminimis">
|
||||
<strong>Appendix A de minimis calculation (${w.form_year}):</strong>
|
||||
<div>Contribution base: ${dollars(w.line_9_contribution_base_cents)}</div>
|
||||
<div>× factor ${w.line_10_factor} = estimated contribution ${dollars(w.line_11_estimated_contrib_cents)}</div>
|
||||
<div style="font-weight:600; color:${w.is_de_minimis ? "#065f46" : "#991b1b"};">
|
||||
${w.is_de_minimis ? "✓ DE MINIMIS (exempt from USF)" : "NOT de minimis — you must contribute to USF"}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderCalcsInfo(calcs: any): string {
|
||||
if (!calcs) return "";
|
||||
const parts: string[] = [];
|
||||
if (calcs.de_minimis) parts.push(renderDeMinimis(calcs.de_minimis));
|
||||
if (calcs.lnpa_sums) {
|
||||
parts.push(`<div>LNPA sums: Block 3 ${calcs.lnpa_sums.block_3_pct.toFixed(2)}% / Block 4 ${calcs.lnpa_sums.block_4_pct.toFixed(2)}%</div>`);
|
||||
}
|
||||
if (calcs.trs_base) {
|
||||
parts.push(`<div>TRS base (Line 512): $${(calcs.trs_base.line_512/100).toLocaleString("en-US",{minimumFractionDigits:2})}</div>`);
|
||||
}
|
||||
return parts.length ? `<section class="pw-block"><h3>Computed totals</h3>${parts.join("")}</section>` : "";
|
||||
}
|
||||
|
||||
window.addEventListener("pw:step-shown", (evt: any) => {
|
||||
if (evt.detail.step !== "review") return;
|
||||
renderReview();
|
||||
errDiv.hidden = true; warnDiv.hidden = true;
|
||||
});
|
||||
|
||||
window.addEventListener("pw:step-next", async (evt: any) => {
|
||||
const PW = (window as any).PWIntake;
|
||||
if (PW.steps[PW.get().step_index] !== "review") return;
|
||||
evt.preventDefault(); // handle async flow
|
||||
try {
|
||||
const result = await runValidate();
|
||||
// Append computed totals (de minimis, LNPA sums, TRS base) to the review pane
|
||||
const calcsHtml = renderCalcsInfo(result.calculations);
|
||||
if (calcsHtml) {
|
||||
const host = document.getElementById("pw-review-pane");
|
||||
if (host) host.insertAdjacentHTML("beforeend", calcsHtml);
|
||||
}
|
||||
if (result.soft.length > 0) {
|
||||
warnDiv.hidden = false;
|
||||
warnDiv.textContent = "Soft warnings: " + result.soft.join(", ") + ". You can still proceed.";
|
||||
} else { warnDiv.hidden = true; }
|
||||
if (!result.ok) {
|
||||
errDiv.hidden = false;
|
||||
const msgs: string[] = [];
|
||||
if (result.missing.length) msgs.push("Missing: " + result.missing.join(", "));
|
||||
if (result.rejections && result.rejections.length) msgs.push(result.rejections.join(" | "));
|
||||
errDiv.textContent = msgs.join(" — ");
|
||||
return;
|
||||
}
|
||||
errDiv.hidden = true;
|
||||
// advance to the payment step
|
||||
const state = PW.get();
|
||||
PW.set({ ...state, step_index: state.step_index + 1 });
|
||||
document.querySelectorAll<HTMLElement>(".pw-wizard-body > [data-step]").forEach((el) => {
|
||||
el.hidden = el.getAttribute("data-step") !== PW.steps[state.step_index + 1];
|
||||
});
|
||||
document.querySelectorAll<HTMLElement>(".pw-step-chip").forEach((chip, i) => {
|
||||
chip.setAttribute("data-active", String(i === state.step_index + 1));
|
||||
chip.setAttribute("data-done", String(i < state.step_index + 1));
|
||||
});
|
||||
window.dispatchEvent(new CustomEvent("pw:step-shown",
|
||||
{ detail: { step: PW.steps[state.step_index + 1], idx: state.step_index + 1 } }));
|
||||
} catch (err: any) {
|
||||
errDiv.hidden = false;
|
||||
errDiv.textContent = "Could not validate order: " + err.message;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue