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:
parent
73c27c75b1
commit
326aee7714
2 changed files with 58 additions and 18 deletions
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -401,6 +401,20 @@
|
|||
box.innerHTML = documents.map((d) => {
|
||||
const u = API + "/api/v1/admin/compliance-orders/" + encodeURIComponent(orderNumber)
|
||||
+ "/document?key=" + encodeURIComponent(d.key) + "&token=" + tok;
|
||||
// Render screenshots/images as an inline clickable thumbnail so you can
|
||||
// see the filing confirmation at a glance; PDFs stay as a View link.
|
||||
if (d.is_image) {
|
||||
return `<div style="padding:8px 0;border-top:1px solid var(--gray100);">
|
||||
<div class="svc-left" style="margin-bottom:6px;">
|
||||
<span style="font-size:13px;">${esc(d.label)}</span>
|
||||
<span class="small">${esc(d.key.split("/").pop())}</span>
|
||||
</div>
|
||||
<a href="${u}" target="_blank" rel="noopener" title="Open full size">
|
||||
<img src="${u}" alt="${esc(d.label)}" loading="lazy"
|
||||
style="max-width:100%;border:1px solid var(--gray200);border-radius:6px;display:block;" />
|
||||
</a>
|
||||
</div>`;
|
||||
}
|
||||
return `<div class="svc-row"><div class="svc-left">
|
||||
<span style="font-size:13px;">${esc(d.label)}</span>
|
||||
<span class="small">${esc(d.key.split("/").pop())}</span>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue