admin: compliance-orders dashboard (view, approve-to-file, re-arm intake)

The admin SPA only managed formation_orders; compliance service orders
(telecom/DOT/healthcare) had no admin surface, so you couldn't see what was
paid, what was stuck on intake, or approve a prepared filing for submission.

API (api/src/routes/admin.ts), all requireAdmin:
  GET  /api/v1/admin/compliance-orders            list, grouped by batch, filters
  GET  /api/v1/admin/compliance-orders/stats      queue overview counts
  GET  /api/v1/admin/compliance-orders/:id        full detail + audit log
  POST /api/v1/admin/compliance-orders/:id/approve       approve ready_to_file + dispatch worker
  POST /api/v1/admin/compliance-orders/:id/rearm-intake  clear reminder stamp so daily nudge resumes

UI: new static page /admin/compliance-orders/ (self-contained, CSP-safe inline
CSS, no external JS framework) reusing the existing pw_admin_token session.
Cards group multi-service batches, flag paid+intake-incomplete in red, show
reminder counts, and expose Approve & Re-arm buttons. Linked from the main
/admin top bar. Every approve/re-arm writes an order_audit_log entry.
This commit is contained in:
justin 2026-06-15 23:57:05 -05:00
parent b48d0cb799
commit 2296566e85
3 changed files with 669 additions and 1 deletions

View file

@ -301,4 +301,283 @@ router.get("/api/v1/admin/audit", requireAdmin, async (req, res) => {
}
});
// =====================================================================
// Compliance Orders (telecom / DOT / healthcare service fulfillment)
//
// The legacy admin SPA only manages formation_orders. Compliance service
// orders (compliance_orders) had no admin surface at all -- you couldn't see
// what was paid, what was stuck on intake, or approve a prepared filing for
// submission. These endpoints back the /admin/compliance-orders page.
// =====================================================================
/** GET /api/v1/admin/compliance-orders — list, grouped by batch, with filters. */
router.get("/api/v1/admin/compliance-orders", requireAdmin, async (req, res) => {
try {
const payment = (req.query.payment as string) || "";
const fulfillment = (req.query.fulfillment as string) || "";
const intake = (req.query.intake as string) || ""; // 'incomplete' | 'complete'
const q = ((req.query.q as string) || "").trim().toLowerCase();
const limit = Math.min(parseInt(req.query.limit as string, 10) || 200, 500);
const where: string[] = ["1=1"];
const params: any[] = [];
let i = 1;
if (payment) { where.push(`co.payment_status = $${i++}`); params.push(payment); }
if (fulfillment === "none") { where.push(`co.fulfillment_status IS NULL`); }
else if (fulfillment) { where.push(`co.fulfillment_status = $${i++}`); params.push(fulfillment); }
if (intake === "incomplete") { where.push(`COALESCE(co.intake_data_validated, FALSE) = FALSE`); }
else if (intake === "complete") { where.push(`co.intake_data_validated = TRUE`); }
if (q) {
where.push(`(lower(co.customer_email) LIKE $${i} OR lower(co.customer_name) LIKE $${i} OR lower(co.order_number) LIKE $${i} OR lower(COALESCE(co.batch_id,'')) LIKE $${i})`);
params.push(`%${q}%`); i++;
}
params.push(limit);
const { rows } = await pool.query(
`SELECT co.order_number, co.batch_id, co.service_slug, co.service_name,
co.customer_email, co.customer_name, co.customer_phone,
co.payment_status, co.payment_method, co.paid_at,
co.service_fee_cents, co.gov_fee_cents, co.surcharge_cents, co.discount_cents,
co.fulfillment_status, co.fulfillment_status_at,
COALESCE(co.intake_data_validated, FALSE) AS intake_data_validated,
co.intake_reminder_count, co.intake_reminder_last_at,
co.erpnext_sales_order, co.created_at
FROM compliance_orders co
WHERE ${where.join(" AND ")}
ORDER BY co.created_at DESC
LIMIT $${i}`,
params,
);
// Group multi-service batches (batch_id) into a single card; standalone
// orders (no batch_id) become their own one-item group keyed by order_number.
type Grp = {
group_id: string;
is_batch: boolean;
customer_name: string;
customer_email: string;
customer_phone: string | null;
payment_status: string;
payment_method: string | null;
paid_at: string | null;
created_at: string;
total_cents: number;
intake_all_complete: boolean;
intake_any_incomplete: boolean;
max_reminder_count: number;
last_reminded_at: string | null;
services: any[];
};
const groups = new Map<string, Grp>();
for (const r of rows as any[]) {
const key = r.batch_id || r.order_number;
let g = groups.get(key);
if (!g) {
g = {
group_id: key,
is_batch: !!r.batch_id,
customer_name: r.customer_name,
customer_email: r.customer_email,
customer_phone: r.customer_phone,
payment_status: r.payment_status,
payment_method: r.payment_method,
paid_at: r.paid_at,
created_at: r.created_at,
total_cents: 0,
intake_all_complete: true,
intake_any_incomplete: false,
max_reminder_count: 0,
last_reminded_at: null,
services: [],
};
groups.set(key, g);
}
g.total_cents += Number(r.service_fee_cents || 0) + Number(r.gov_fee_cents || 0)
+ Number(r.surcharge_cents || 0) - Number(r.discount_cents || 0);
if (!r.intake_data_validated) { g.intake_all_complete = false; g.intake_any_incomplete = true; }
g.max_reminder_count = Math.max(g.max_reminder_count, Number(r.intake_reminder_count || 0));
if (r.intake_reminder_last_at && (!g.last_reminded_at || r.intake_reminder_last_at > g.last_reminded_at)) {
g.last_reminded_at = r.intake_reminder_last_at;
}
g.services.push({
order_number: r.order_number,
service_slug: r.service_slug,
service_name: r.service_name || r.service_slug,
fulfillment_status: r.fulfillment_status,
fulfillment_status_at: r.fulfillment_status_at,
intake_data_validated: r.intake_data_validated,
erpnext_sales_order: r.erpnext_sales_order,
ready_to_approve: r.fulfillment_status === "ready_to_file",
});
}
res.json({ groups: Array.from(groups.values()) });
} catch (err) {
console.error("[admin/compliance-orders] Error:", err);
res.status(500).json({ error: "Could not load compliance orders." });
}
});
/** GET /api/v1/admin/compliance-orders/stats — queue overview counts. */
router.get("/api/v1/admin/compliance-orders/stats", requireAdmin, async (_req, res) => {
try {
const { rows } = await pool.query(`
SELECT
COUNT(*) FILTER (WHERE payment_status = 'paid') AS paid,
COUNT(*) FILTER (WHERE payment_status = 'pending_payment') AS pending_payment,
COUNT(*) FILTER (WHERE payment_status = 'paid' AND COALESCE(intake_data_validated, FALSE) = FALSE) AS paid_intake_incomplete,
COUNT(*) FILTER (WHERE fulfillment_status = 'ready_to_file') AS ready_to_file,
COUNT(*) FILTER (WHERE fulfillment_status = 'awaiting_intake') AS awaiting_intake,
COUNT(*) FILTER (WHERE fulfillment_status = 'completed') AS completed,
COUNT(*) AS total
FROM compliance_orders
`);
res.json(rows[0]);
} catch (err) {
console.error("[admin/compliance-orders/stats] Error:", err);
res.status(500).json({ error: "Could not load stats." });
}
});
/** GET /api/v1/admin/compliance-orders/:order_number — single order full detail. */
router.get("/api/v1/admin/compliance-orders/:order_number", requireAdmin, async (req, res) => {
try {
const { rows } = await pool.query(
`SELECT co.*, te.legal_name AS entity_name, te.frn AS entity_frn
FROM compliance_orders co
LEFT JOIN telecom_entities te ON te.id = co.telecom_entity_id
WHERE co.order_number = $1`,
[req.params.order_number],
);
if (rows.length === 0) { res.status(404).json({ error: "Order not found." }); return; }
const order = rows[0];
const audit = await pool.query(
`SELECT * FROM order_audit_log
WHERE order_number = $1 AND order_type IN ('compliance', 'compliance_batch')
ORDER BY created_at DESC`,
[order.order_number],
);
res.json({ order, audit_log: audit.rows });
} catch (err) {
console.error("[admin/compliance-orders/:id] Error:", err);
res.status(500).json({ error: "Could not load order." });
}
});
/**
* POST /api/v1/admin/compliance-orders/:order_number/approve
* Approve a prepared filing held at fulfillment_status='ready_to_file' and
* dispatch the worker to actually submit it to the government system.
*/
router.post("/api/v1/admin/compliance-orders/:order_number/approve", requireAdmin, async (req, res) => {
const id = req.params.order_number;
try {
const { rows } = await pool.query(
`SELECT order_number, service_slug, fulfillment_status
FROM compliance_orders WHERE order_number = $1`,
[id],
);
const order = rows[0];
if (!order) { res.status(404).json({ error: "Order not found." }); return; }
if (order.fulfillment_status !== "ready_to_file") {
res.status(409).json({
error: `Order is not awaiting submission approval (status=${order.fulfillment_status ?? "none"}).`,
});
return;
}
await pool.query(
`UPDATE compliance_orders
SET fulfillment_status = 'authorization_signed', fulfillment_status_at = now(), updated_at = now()
WHERE order_number = $1`,
[id],
);
await pool.query(
`INSERT INTO order_audit_log (order_type, order_id, order_number, action, from_status, to_status, actor_type, actor_id, actor_name, note)
VALUES ('compliance', 0, $1, 'approved_for_submission', 'ready_to_file', 'authorization_signed', 'admin', $2, $3, $4)`,
[id, req.admin!.id, req.admin!.username, (req.body?.note as string) || "Approved + dispatched for government submission"],
);
const workerUrl = process.env.WORKER_URL || "http://workers:8090";
let dispatched = false;
try {
const r = 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,
admin_approved: true,
}),
});
dispatched = r.ok;
} catch (err) {
console.error(`[admin/compliance-orders] approve dispatch failed for ${id}:`, err);
}
res.json({ success: true, order_number: id, dispatched });
} catch (err) {
console.error(`[admin/compliance-orders] approve error for ${id}:`, err);
res.status(500).json({ error: "Approve failed." });
}
});
/**
* POST /api/v1/admin/compliance-orders/:order_number/rearm-intake
* Re-arm the daily intake reminder for a paid+incomplete order whose reminders
* went quiet (clears intake_reminder_last_at so the next daily run nudges it).
* Optionally resets the count back to 0 with { reset_count: true }.
*/
router.post("/api/v1/admin/compliance-orders/:order_number/rearm-intake", requireAdmin, async (req, res) => {
const id = req.params.order_number;
try {
const resetCount = req.body?.reset_count === true;
const { rows } = await pool.query(
`SELECT order_number, batch_id, payment_status,
COALESCE(intake_data_validated, FALSE) AS intake_data_validated
FROM compliance_orders WHERE order_number = $1`,
[id],
);
const order = rows[0];
if (!order) { res.status(404).json({ error: "Order not found." }); return; }
if (order.payment_status !== "paid") {
res.status(409).json({ error: "Only paid orders can be re-armed." });
return;
}
if (order.intake_data_validated) {
res.status(409).json({ error: "Intake is already complete for this order." });
return;
}
// Re-arm the whole batch when this order belongs to one, so a multi-service
// customer gets a single consolidated nudge (matches the worker grouping).
const filter = order.batch_id
? { clause: "batch_id = $1", val: order.batch_id }
: { clause: "order_number = $1", val: id };
const updated = await pool.query(
`UPDATE compliance_orders
SET intake_reminder_last_at = NULL
${resetCount ? ", intake_reminder_count = 0" : ""},
updated_at = now()
WHERE ${filter.clause}
AND payment_status = 'paid'
AND COALESCE(intake_data_validated, FALSE) = FALSE
RETURNING order_number`,
[filter.val],
);
await pool.query(
`INSERT INTO order_audit_log (order_type, order_id, order_number, action, actor_type, actor_id, actor_name, note)
VALUES ('compliance', 0, $1, 'intake_reminder_rearmed', 'admin', $2, $3, $4)`,
[id, req.admin!.id, req.admin!.username,
`Re-armed intake reminder for ${updated.rowCount} order(s)${resetCount ? " (count reset to 0)" : ""}`],
);
res.json({ success: true, rearmed: updated.rowCount });
} catch (err) {
console.error(`[admin/compliance-orders] rearm-intake error for ${id}:`, err);
res.status(500).json({ error: "Re-arm failed." });
}
});
export default router;