feat(compliance): admin verification gate + durable submission evidence
Per request: after the customer signs but BEFORE we submit to the government, hold the order for a human to verify the prepared filing is correct. - MCS-150 handler (mcs150-update + usdot-reactivation): new admin-verification gate after the signature gate -- if not admin_approved, set fulfillment_status= 'ready_to_file', create a HIGH-priority 'VERIFY before filing' admin todo, and STOP (no FMCSA submission). job_server injects admin_approved from the dispatch payload (mirrors client_approved). - New admin endpoint POST /api/v1/admin/compliance-orders/:id/approve-submit (requireAdmin): verifies status=ready_to_file, re-dispatches the worker with admin_approved=true to proceed to actual submission. - Durable submission EVIDENCE: the web/fax submitters only wrote confirmation screenshots to an ephemeral temp dir. Now _upload_submission_evidence copies the FMCSA confirmation screenshot + attested PDF + fax_log_id to MinIO under filings/<slug>/<order>/evidence/ and records the keys on the order, so we keep proof of every government submission. (state-trucking + the FCC handlers already gate via admin todos / auto_filing.py; this brings MCS-150 to parity and adds evidence retention.)
This commit is contained in:
parent
e87715aee7
commit
058d4d426a
3 changed files with 205 additions and 0 deletions
|
|
@ -13,6 +13,7 @@ import { Router } from "express";
|
|||
import { pool } from "../db.js";
|
||||
import { randomBytes } from "crypto";
|
||||
import { COMPLIANCE_SERVICES } from "../service-catalog.js";
|
||||
import { requireAdmin } from "../middleware/admin-auth.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
|
|
@ -179,6 +180,10 @@ const REQUIRED_FIELDS: Record<string, FieldSpec> = {
|
|||
"foreign-qualification-single": { required: ["legal_name", "home_state_code", "entity_type", "target_states"], soft: ["ein"] },
|
||||
"foreign-qualification-multi": { required: ["legal_name", "home_state_code", "entity_type", "target_states"], soft: ["ein"] },
|
||||
|
||||
// ── Business name reservation (binding hold; admin-assisted filing) ──
|
||||
"name-reservation-tx": { required: ["entity_name", "entity_type"], soft: ["entity_name_alt"] },
|
||||
"name-reservation-nv": { required: ["entity_name", "entity_type"], soft: ["entity_name_alt"] },
|
||||
|
||||
// ── DOT / FMCSA Motor Carrier Services ───────────────────────────────
|
||||
// All collected via the unified dot-intake step (DOTIntakeStep.astro).
|
||||
"mcs150-update": { required: ["dot_number", "legal_name", "address_street", "address_city", "address_state", "address_zip", "phone", "email", "signer_name", "signer_title", "power_units", "drivers", "carrier_operation", "interstate_intrastate", "hazmat"], soft: ["mc_number", "ein", "annual_miles", "cargo_types"] },
|
||||
|
|
@ -1753,5 +1758,60 @@ router.post("/api/v1/compliance-orders/:id/usac-delegation", async (req, res) =>
|
|||
}
|
||||
});
|
||||
|
||||
// ── POST /api/v1/admin/compliance-orders/:id/approve-submit ──────────────────
|
||||
// Admin verification gate: an order that has been prepared + (if needed) signed
|
||||
// is held at fulfillment_status='ready_to_file'. The admin reviews the prepared
|
||||
// filing and calls this to APPROVE it, which re-dispatches the worker with
|
||||
// admin_approved=true so it proceeds to the actual government submission. We
|
||||
// never submit to a government system until an admin has cleared this gate.
|
||||
router.post("/api/v1/admin/compliance-orders/:id/approve-submit", requireAdmin, async (req, res) => {
|
||||
const id = req.params.id;
|
||||
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) {
|
||||
return res.status(404).json({ error: "Order not found" });
|
||||
}
|
||||
if (order.fulfillment_status !== "ready_to_file") {
|
||||
return res.status(409).json({
|
||||
error: `Order is not awaiting submission approval (status=${order.fulfillment_status ?? "none"})`,
|
||||
});
|
||||
}
|
||||
|
||||
await pool.query(
|
||||
`UPDATE compliance_orders SET fulfillment_status = 'authorization_signed',
|
||||
fulfillment_status_at = now() WHERE order_number = $1`,
|
||||
[id],
|
||||
);
|
||||
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(`[compliance-orders] approve-submit dispatch failed for ${id}:`, err);
|
||||
}
|
||||
console.log(`[compliance-orders] Admin approved + dispatched submission for ${id} (${order.service_slug})`);
|
||||
res.json({ success: true, order_number: id, dispatched });
|
||||
} catch (err) {
|
||||
console.error(`[compliance-orders] approve-submit error for ${id}:`, err);
|
||||
res.status(500).json({ error: "Approve-submit failed" });
|
||||
}
|
||||
});
|
||||
|
||||
export { COMPLIANCE_SERVICES, REQUIRED_FIELDS, BUNDLE_COMPONENTS, MUTUALLY_EXCLUSIVE_GROUPS };
|
||||
export default router;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue