feat(sc-coc): SC intrastate Certificate of Compliance flow (insurance gate -> $25 fee -> file)
Routes SC intrastate-authority orders to the real SCDMV COC product instead of a
PSC certificate (which doesn't apply to property carriers):
- sc_coc_filing.py: emails the carrier a one-click yes/no — does your insurer
have / can they file a Form E (SC intrastate liability, $750k or $300k by
GVWR) with SCDMV? Records the answer; builds the filled COC package.
- state_trucking._handle_sc_coc_gate: SC intrastate gate —
no answer -> email the question once, HOLD
answered no -> broker referral opened, HOLD (ops todo)
answered yes-> proceed to bill the exact $25 SCDMV COC fee (at cost) + file
- API POST /compliance-orders/:id/sc-insurance: records yes/no in intake_data
(no schema change); NO opens an insurance_lead broker-referral ticket +
Telegram; YES re-dispatches the worker to bill the $25 + file.
- site/order/sc-insurance: customer one-click yes/no page (auto-submits when
the email links straight to ?have=yes|no).
Non-SC intrastate still uses the PSC/PUC email path or a manual todo.
This commit is contained in:
parent
dae9603808
commit
c46efe5730
4 changed files with 526 additions and 30 deletions
|
|
@ -1726,7 +1726,6 @@ router.post("/api/v1/compliance-orders/:id/usac-delegation", async (req, res) =>
|
|||
const id = req.params.id;
|
||||
|
||||
try {
|
||||
// Support both batch ID (CB-) and order number (CO-)
|
||||
const whereCol = id.startsWith("CB-") ? "batch_id" : "order_number";
|
||||
const result = await pool.query(
|
||||
`UPDATE compliance_orders
|
||||
|
|
@ -1760,6 +1759,117 @@ router.post("/api/v1/compliance-orders/:id/usac-delegation", async (req, res) =>
|
|||
}
|
||||
});
|
||||
|
||||
// ── POST /api/v1/compliance-orders/:id/sc-insurance ──────────────────────────
|
||||
// SC intrastate Certificate of Compliance (COC) flow. The carrier answers a
|
||||
// one-click yes/no from their email: does their insurer have / can they file a
|
||||
// Form E (SC intrastate liability) with SCDMV?
|
||||
// have=yes -> record it; the worker proceeds to bill the $25 COC fee + file.
|
||||
// have=no -> record it + open a broker-referral ticket so we connect them
|
||||
// with an insurer that writes SC intrastate liability.
|
||||
// Stored in intake_data.sc_coc_insurance so no schema change is needed.
|
||||
router.post("/api/v1/compliance-orders/:id/sc-insurance", async (req, res) => {
|
||||
const id = req.params.id;
|
||||
const have = String((req.query.have ?? req.body?.have ?? "")).toLowerCase();
|
||||
if (have !== "yes" && have !== "no") {
|
||||
res.status(400).json({ error: "have must be 'yes' or 'no'." });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
`UPDATE compliance_orders
|
||||
SET intake_data = jsonb_set(
|
||||
jsonb_set(COALESCE(intake_data, '{}'::jsonb),
|
||||
'{sc_coc_insurance}', to_jsonb($2::text)),
|
||||
'{sc_coc_insurance_at}', to_jsonb(now()::text)),
|
||||
updated_at = NOW()
|
||||
WHERE order_number = $1
|
||||
RETURNING order_number, service_slug, customer_email, customer_name,
|
||||
intake_data->>'entity_name' AS entity_name`,
|
||||
[id, have],
|
||||
);
|
||||
if (rows.length === 0) {
|
||||
res.status(404).json({ error: "Order not found." });
|
||||
return;
|
||||
}
|
||||
const o = rows[0];
|
||||
console.log(`[compliance-orders] SC COC insurance answer for ${id}: ${have}`);
|
||||
|
||||
// NO -> open a broker-referral support ticket so ops connects them.
|
||||
if (have === "no") {
|
||||
try {
|
||||
await pool.query(
|
||||
`INSERT INTO tickets (category, subject, message, email, name, page, ip_address)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
[
|
||||
"insurance_lead",
|
||||
`SC intrastate insurance referral — ${o.entity_name || o.customer_name || o.order_number}`,
|
||||
`Carrier needs an insurer that writes SC intrastate liability and can file a Form E with SCDMV.\n\n`
|
||||
+ `Order: ${o.order_number}\nService: ${o.service_slug}\n`
|
||||
+ `Customer: ${o.customer_name || ""} <${o.customer_email || ""}>\n\n`
|
||||
+ `They answered NO to "does your insurer have/can file a Form E for SC intrastate?" `
|
||||
+ `Connect them with a broker for SC intrastate trucking liability, then resume the COC filing.`,
|
||||
o.customer_email || null,
|
||||
o.customer_name || null,
|
||||
`sc-insurance:${o.order_number}`,
|
||||
(req as any).clientIp || req.ip || "",
|
||||
],
|
||||
);
|
||||
} catch (tErr) {
|
||||
console.error("[compliance-orders] SC insurance referral ticket failed:", tErr);
|
||||
}
|
||||
// Telegram alert (best-effort)
|
||||
try {
|
||||
const botToken = process.env.TELEGRAM_BOT_TOKEN;
|
||||
const chatId = process.env.TELEGRAM_CHAT_ID;
|
||||
if (botToken && chatId) {
|
||||
fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
chat_id: chatId,
|
||||
text: `🛟 SC intrastate insurance referral needed\n${o.entity_name || o.customer_name || ""} (${o.order_number})\nCustomer has no Form E — connect them with a broker.`,
|
||||
}),
|
||||
}).catch(() => {});
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
have,
|
||||
message:
|
||||
have === "yes"
|
||||
? "Thank you! We'll complete your SCDMV Certificate of Compliance and confirm your Form E is on file. You'll get a payment link for the $25 state fee shortly."
|
||||
: "No problem — we'll connect you with an insurance broker who can write SC intrastate liability and file your Form E. We'll be in touch within one business day.",
|
||||
});
|
||||
|
||||
// YES -> re-dispatch the worker so it proceeds to bill the $25 COC fee + file.
|
||||
if (have === "yes") {
|
||||
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: o.order_number,
|
||||
order_number: o.order_number,
|
||||
service_slug: o.service_slug,
|
||||
}),
|
||||
});
|
||||
console.log(`[compliance-orders] Worker re-dispatched after SC insurance YES: ${o.order_number}`);
|
||||
} catch (err) {
|
||||
console.warn(`[compliance-orders] Worker dispatch failed for ${o.order_number}:`, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[compliance-orders] SC insurance answer error:", err);
|
||||
res.status(500).json({ error: "Could not record your answer. Please try again." });
|
||||
}
|
||||
});
|
||||
|
||||
// ── 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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue