Add Finish button handler + intake save API endpoint
- Wizard "Finish" button now submits intake data to the API - New PUT /api/v1/compliance-orders/:id/intake endpoint saves intake data, updates entity, re-dispatches worker, and unpauses batch siblings - Shows success screen with portal link after submission Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3614284a78
commit
7b650179e4
2 changed files with 202 additions and 1 deletions
|
|
@ -1534,6 +1534,139 @@ router.get("/api/v1/compliance-orders/my-orders", async (req, res) => {
|
|||
* Customer confirms they've completed USAC E-File delegation.
|
||||
* Updates order status and notifies the team to begin filing.
|
||||
*/
|
||||
/**
|
||||
* PUT /api/v1/compliance-orders/:id/intake
|
||||
* Save intake data collected from the wizard. Dispatches the worker
|
||||
* to process the filing now that intake is complete.
|
||||
*/
|
||||
router.put("/api/v1/compliance-orders/:id/intake", async (req, res) => {
|
||||
const id = req.params.id;
|
||||
const { intake_data, entity, officers } = req.body ?? {};
|
||||
|
||||
if (!intake_data) {
|
||||
res.status(400).json({ error: "intake_data required" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Merge new intake data with existing (don't overwrite source/frn)
|
||||
const existing = await pool.query(
|
||||
`SELECT intake_data, batch_id, service_slug, payment_status FROM compliance_orders WHERE order_number = $1`,
|
||||
[id],
|
||||
);
|
||||
if (existing.rows.length === 0) {
|
||||
res.status(404).json({ error: "Order not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const order = existing.rows[0] as Record<string, unknown>;
|
||||
const oldIntake = (typeof order.intake_data === "string"
|
||||
? JSON.parse(order.intake_data as string)
|
||||
: order.intake_data) || {};
|
||||
const merged = { ...oldIntake, ...intake_data };
|
||||
|
||||
// Update the order with merged intake data
|
||||
await pool.query(
|
||||
`UPDATE compliance_orders
|
||||
SET intake_data = $1,
|
||||
payment_status = CASE WHEN payment_status = 'pending_intake' THEN 'paid' ELSE payment_status END,
|
||||
intake_data_validated = TRUE
|
||||
WHERE order_number = $2`,
|
||||
[JSON.stringify(merged), id],
|
||||
);
|
||||
|
||||
// If entity data was provided, update the telecom_entity too
|
||||
if (entity && entity.frn) {
|
||||
try {
|
||||
await pool.query(
|
||||
`UPDATE telecom_entities SET
|
||||
legal_name = COALESCE(NULLIF($1, ''), legal_name),
|
||||
dba_name = COALESCE(NULLIF($2, ''), dba_name),
|
||||
contact_name = COALESCE(NULLIF($3, ''), contact_name),
|
||||
contact_email = COALESCE(NULLIF($4, ''), contact_email),
|
||||
contact_phone = COALESCE(NULLIF($5, ''), contact_phone),
|
||||
address_street = COALESCE(NULLIF($6, ''), address_street),
|
||||
address_city = COALESCE(NULLIF($7, ''), address_city),
|
||||
address_state = COALESCE(NULLIF($8, ''), address_state),
|
||||
address_zip = COALESCE(NULLIF($9, ''), address_zip)
|
||||
WHERE frn = $10`,
|
||||
[
|
||||
entity.legal_name || "", entity.dba_name || "",
|
||||
entity.contact_name || "", entity.contact_email || "",
|
||||
entity.contact_phone || "",
|
||||
entity.address_street || "", entity.address_city || "",
|
||||
entity.address_state || "", entity.address_zip || "",
|
||||
entity.frn,
|
||||
],
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Re-dispatch the worker to process this order now that intake is complete
|
||||
const workerUrl = process.env.WORKER_URL || "http://workers:8090";
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
await fetch(`${workerUrl}/jobs`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
action: "process_compliance_service",
|
||||
order_name: id,
|
||||
order_number: id,
|
||||
service_slug: order.service_slug as string,
|
||||
}),
|
||||
});
|
||||
console.log(`[compliance-orders] Worker re-dispatched after intake: ${id}`);
|
||||
} catch (err) {
|
||||
console.warn(`[compliance-orders] Worker dispatch failed for ${id}:`, err);
|
||||
}
|
||||
});
|
||||
|
||||
// If this is a batch order, check if all orders in the batch have intake
|
||||
// and dispatch any that were also pending
|
||||
if (order.batch_id) {
|
||||
try {
|
||||
const pending = await pool.query(
|
||||
`SELECT order_number, service_slug FROM compliance_orders
|
||||
WHERE batch_id = $1 AND payment_status = 'pending_intake' AND order_number != $2`,
|
||||
[order.batch_id, id],
|
||||
);
|
||||
for (const po of pending.rows as any[]) {
|
||||
// Update their intake data too (same entity info)
|
||||
await pool.query(
|
||||
`UPDATE compliance_orders
|
||||
SET intake_data = jsonb_set(COALESCE(intake_data::jsonb, '{}'::jsonb), '{entity_filled}', 'true'::jsonb),
|
||||
payment_status = 'paid'
|
||||
WHERE order_number = $1 AND payment_status = 'pending_intake'`,
|
||||
[po.order_number],
|
||||
);
|
||||
// Dispatch worker
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
await fetch(`${workerUrl}/jobs`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
action: "process_compliance_service",
|
||||
order_name: po.order_number,
|
||||
order_number: po.order_number,
|
||||
service_slug: po.service_slug,
|
||||
}),
|
||||
});
|
||||
console.log(`[compliance-orders] Batch sibling dispatched: ${po.order_number}`);
|
||||
} catch {}
|
||||
});
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
res.json({ ok: true, order_number: id });
|
||||
} catch (err) {
|
||||
console.error("[compliance-orders] Intake save error:", err);
|
||||
res.status(500).json({ error: "Failed to save intake data" });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/api/v1/compliance-orders/:id/usac-delegation", async (req, res) => {
|
||||
const id = req.params.id;
|
||||
|
||||
|
|
|
|||
|
|
@ -404,9 +404,77 @@ const STEP_LABELS: Record<string, string> = {
|
|||
window.dispatchEvent(cancelEvt);
|
||||
// Step components call evt.preventDefault() to block.
|
||||
if (cancelEvt.defaultPrevented) return;
|
||||
if (s.step_index < steps.length - 1) renderStep(s.step_index + 1);
|
||||
if (s.step_index < steps.length - 1) {
|
||||
renderStep(s.step_index + 1);
|
||||
} else {
|
||||
// Last step — submit the intake data
|
||||
submitIntake(s);
|
||||
}
|
||||
});
|
||||
|
||||
async function submitIntake(state: IntakeState) {
|
||||
const nextBtn = document.getElementById("pw-next") as HTMLButtonElement;
|
||||
nextBtn.disabled = true;
|
||||
nextBtn.textContent = "Submitting...";
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const token = params.get("token");
|
||||
const orderNumber = state.intake_data?.order_number || "";
|
||||
const API = (window as any).__PW_API || "";
|
||||
|
||||
if (!orderNumber && !token) {
|
||||
// No order context — this is a standalone order, go to payment
|
||||
// (shouldn't happen since payment step was removed for token orders)
|
||||
nextBtn.disabled = false;
|
||||
nextBtn.textContent = "Finish";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Save intake data to the compliance order
|
||||
const saveResp = await fetch(`${API}/api/v1/compliance-orders/${orderNumber}/intake`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(token ? { "Authorization": `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
intake_data: state.intake_data,
|
||||
entity: state.entity,
|
||||
officers: state.officers,
|
||||
}),
|
||||
});
|
||||
|
||||
if (saveResp.ok) {
|
||||
// Clear session storage
|
||||
sessionStorage.removeItem(`pw-intake-${slug}`);
|
||||
// Show success
|
||||
const body = document.querySelector(".pw-wizard-body") as HTMLElement;
|
||||
body.innerHTML = `
|
||||
<div style="text-align:center;padding:3rem 1rem;">
|
||||
<div style="width:64px;height:64px;margin:0 auto 1rem;background:#dcfce7;border-radius:50%;display:flex;align-items:center;justify-content:center;">
|
||||
<svg style="width:32px;height:32px;color:#16a34a" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
|
||||
</div>
|
||||
<h2 style="font-size:1.3rem;font-weight:700;color:#111827;margin-bottom:0.5rem;">Intake Complete</h2>
|
||||
<p style="color:#6b7280;font-size:0.95rem;margin-bottom:1.5rem;">Your information has been submitted. We'll begin processing your filing and email you with updates.</p>
|
||||
<a href="https://portal.performancewest.net" style="display:inline-block;background:#1a2744;color:#fff;padding:10px 24px;border-radius:8px;text-decoration:none;font-weight:600;">Go to Client Portal</a>
|
||||
</div>
|
||||
`;
|
||||
document.querySelector(".pw-wizard-stepbar")?.remove();
|
||||
document.querySelector(".pw-wizard-footer")?.remove();
|
||||
} else {
|
||||
const err = await saveResp.json().catch(() => ({}));
|
||||
alert(err.error || "Failed to submit. Please try again.");
|
||||
nextBtn.disabled = false;
|
||||
nextBtn.textContent = "Finish";
|
||||
}
|
||||
} catch (e) {
|
||||
alert("Network error. Please try again.");
|
||||
nextBtn.disabled = false;
|
||||
nextBtn.textContent = "Finish";
|
||||
}
|
||||
}
|
||||
|
||||
// Kick off
|
||||
renderStep(loadState().step_index || 0);
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue