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:
justin 2026-04-28 18:26:20 -05:00
parent 3614284a78
commit 7b650179e4
2 changed files with 202 additions and 1 deletions

View file

@ -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;