190 lines
8.6 KiB
Text
190 lines
8.6 KiB
Text
---
|
||
// 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>
|