new-site/site/src/components/intake/steps/ReviewStep.astro
justin c80f3a408a Fix: generic review step text (was FCC/USAC specific)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-29 00:49:18 -05:00

190 lines
8.6 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
// 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 your order details. Click Finish to continue.
</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>