admin: inline filing screenshots + atomic approve transaction

- Documents now flag is_image and the drawer renders screenshots / confirmation
  images as inline clickable thumbnails (click to open full size); PDFs keep the
  View link. Evidence keys are labeled (Filing confirmation screenshot, etc.),
  the worker-temp screenshot_path (not a MinIO key) is dropped in favor of the
  durable evidence copy, and non-file evidence (fax_log_id) is skipped.
- Wrap approve's status-update + audit-insert in a transaction so a failure can
  no longer leave an order out of ready_to_file without dispatching (the earlier
  audit CHECK violation did exactly that to Paul's UCR; it has been reset).
This commit is contained in:
justin 2026-06-16 02:57:24 -05:00
parent 73c27c75b1
commit 326aee7714
2 changed files with 58 additions and 18 deletions

View file

@ -499,21 +499,36 @@ router.post("/api/v1/admin/compliance-orders/:order_number/approve", requireAdmi
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)
|| (order.intake_data_validated
? "Approved + dispatched for government submission"
: "Approved + dispatched (intake-incomplete override)")],
);
// Flip status + write the audit row atomically. Previously these were two
// separate pool.query calls, so a failure on the audit insert (e.g. an
// order_type CHECK violation) left the status changed but un-dispatched and
// returned a 500 -- the order silently fell out of ready_to_file without
// being filed. A transaction keeps them all-or-nothing.
const client = await pool.connect();
try {
await client.query("BEGIN");
await client.query(
`UPDATE compliance_orders
SET fulfillment_status = 'authorization_signed', fulfillment_status_at = now(), updated_at = now()
WHERE order_number = $1`,
[id],
);
await client.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)
|| (order.intake_data_validated
? "Approved + dispatched for government submission"
: "Approved + dispatched (intake-incomplete override)")],
);
await client.query("COMMIT");
} catch (txErr) {
await client.query("ROLLBACK").catch(() => {});
throw txErr;
} finally {
client.release();
}
const workerUrl = process.env.WORKER_URL || "http://workers:8090";
let dispatched = false;
@ -671,10 +686,19 @@ async function collectOrderDocuments(orderNumber: string): Promise<Array<{ label
add("Prepared filing PDF", fs.pdf_minio_path);
add("Attested PDF (faxed)", fs.attested_pdf);
}
add("Confirmation screenshot", fs.screenshot_path);
// Submission evidence: durable MinIO copies written by the worker after a
// government submission. NOTE: fs.screenshot_path is a worker-local temp
// path (not a MinIO key) -- the durable copy lives in fs.evidence, so we use
// that. Skip evidence entries that aren't object keys (e.g. fax_log_id).
if (fs.evidence && typeof fs.evidence === "object") {
const EVIDENCE_LABELS: Record<string, string> = {
confirmation_screenshot: "Filing confirmation screenshot",
pre_submit_screenshot: "Pre-submission screenshot",
attested_pdf_minio: "Attested filing PDF",
};
for (const [k, v] of Object.entries(fs.evidence as Record<string, any>)) {
add("Evidence: " + k.replace(/_/g, " "), v);
if (k === "fax_log_id") continue; // an ID, not a file
add(EVIDENCE_LABELS[k] || ("Evidence: " + k.replace(/_/g, " ")), v);
}
}
add("Engagement letter", o.engagement_letter_minio_key);
@ -721,7 +745,9 @@ router.get("/api/v1/admin/compliance-orders/:order_number/documents", requireAdm
present = probe.ok; // 200 or 206
}
} catch { /* treat as missing */ }
return { ...d, exists: present };
const lower = d.key.toLowerCase();
const isImage = lower.endsWith(".png") || lower.endsWith(".jpg") || lower.endsWith(".jpeg") || lower.endsWith(".gif") || lower.endsWith(".webp");
return { ...d, exists: present, is_image: isImage };
}));
const available = checked.filter((d) => d.exists);
res.json({ order_number: req.params.order_number, documents: available });