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
120
site/public/order/sc-insurance/index.html
Normal file
120
site/public/order/sc-insurance/index.html
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<title>South Carolina insurance | Performance West</title>
|
||||
<script>
|
||||
window.__PW_API = (function () {
|
||||
var h = window.location.hostname;
|
||||
if (h === "localhost" || h === "127.0.0.1") return "http://" + h + ":3001";
|
||||
if (h === "dev.performancewest.net") return "https://api.dev.performancewest.net";
|
||||
return "https://api.performancewest.net";
|
||||
})();
|
||||
</script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
:root { --pw600:#3a6192; --pw700:#2d4e78; --pw800:#213b5c; --pw900:#182c45;
|
||||
--g100:#f3f4f6; --g200:#e5e7eb; --g300:#d1d5db; --g500:#6b7280; --g700:#374151; --g900:#111827; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin:0; font-family:'Inter',system-ui,sans-serif; background:#f7f8fa; color:var(--g700); }
|
||||
.wrap { max-width:560px; margin:0 auto; padding:32px 16px; }
|
||||
.card { background:#fff; border:1px solid var(--g200); border-radius:14px; overflow:hidden; box-shadow:0 1px 3px rgba(0,0,0,.06); }
|
||||
.head { background:var(--pw900); color:#fff; padding:20px 24px; }
|
||||
.head h1 { margin:6px 0 0; font-size:18px; font-weight:700; }
|
||||
.head p { margin:4px 0 0; font-size:13px; color:#b8cde5; }
|
||||
.logo { font-weight:700; color:#fff; font-size:15px; }
|
||||
.body { padding:24px; }
|
||||
.muted { color:var(--g500); font-size:13px; line-height:1.5; }
|
||||
.q { font-size:15px; font-weight:600; color:var(--g900); margin:14px 0; line-height:1.45; }
|
||||
.btn { display:block; width:100%; margin-top:12px; padding:14px; border:none; border-radius:10px; font-size:15px; font-weight:600; cursor:pointer; text-align:center; }
|
||||
.btn-yes { background:#166534; color:#fff; } .btn-yes:hover { background:#14532d; }
|
||||
.btn-no { background:#fff; color:var(--g700); border:1px solid var(--g300); } .btn-no:hover { background:var(--g100); }
|
||||
.btn:disabled { opacity:.55; cursor:not-allowed; }
|
||||
.note { margin-top:16px; font-size:12px; color:var(--g500); background:var(--g100); border-radius:8px; padding:10px 12px; }
|
||||
.err { margin-top:12px; font-size:13px; color:#b91c1c; background:#fef2f2; border:1px solid #fecaca; border-radius:8px; padding:10px 12px; }
|
||||
.ok { margin-top:12px; font-size:14px; color:#166534; background:#dcfce7; border:1px solid #bbf7d0; border-radius:8px; padding:14px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="card">
|
||||
<div class="head">
|
||||
<div class="logo">Performance West</div>
|
||||
<h1>South Carolina intrastate insurance</h1>
|
||||
<p id="subtitle">Loading…</p>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div id="content">
|
||||
<p class="muted">In South Carolina, a for-hire carrier registers with the SCDMV
|
||||
(Certificate of Compliance). The one thing the state needs from your insurance
|
||||
company is a liability filing called a <strong>Form E</strong>.</p>
|
||||
<p class="q">Does your insurance company have (or can they file) a Form E showing
|
||||
at least <span id="minimum">$750,000</span> in liability coverage for South
|
||||
Carolina intrastate operation?</p>
|
||||
<button id="yes" class="btn btn-yes">✅ Yes — my insurer can file the Form E</button>
|
||||
<button id="no" class="btn btn-no">❌ No / not sure — I need the right insurance</button>
|
||||
<div class="note">A Form E must be filed by your insurance <strong>company</strong>
|
||||
directly with SCDMV. A regular ACORD certificate of insurance is not accepted.
|
||||
If you choose "No," we'll connect you with a broker who can write SC intrastate
|
||||
liability and file the Form E for you.</div>
|
||||
<div id="err" class="err" style="display:none;"></div>
|
||||
</div>
|
||||
<div id="done" class="ok" style="display:none;"></div>
|
||||
<div id="notfound" class="muted" style="display:none;">This link is invalid or the order was already handled.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API = window.__PW_API;
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const params = new URLSearchParams(location.search);
|
||||
const orderId = params.get("order") || "";
|
||||
const preset = (params.get("have") || "").toLowerCase();
|
||||
|
||||
async function load() {
|
||||
if (!orderId) { $("content").style.display = "none"; $("notfound").style.display = "block"; return; }
|
||||
try {
|
||||
const r = await fetch(API + "/api/v1/compliance-orders/" + encodeURIComponent(orderId));
|
||||
if (!r.ok) throw new Error("nf");
|
||||
const order = await r.json();
|
||||
const intake = (order.intake_data && typeof order.intake_data === "object") ? order.intake_data : {};
|
||||
const name = intake.entity_name || order.customer_name || "";
|
||||
$("subtitle").textContent = `${name} · ${order.order_number}`;
|
||||
const bracket = (intake.gross_weight_bracket || "").toLowerCase();
|
||||
$("minimum").textContent = (bracket === "under_10k" || bracket === "lt_10k") ? "$300,000" : "$750,000";
|
||||
} catch (e) {
|
||||
// Still allow answering even if lookup fails.
|
||||
$("subtitle").textContent = orderId;
|
||||
}
|
||||
// If the email linked directly to a yes/no, auto-submit it.
|
||||
if (preset === "yes" || preset === "no") submit(preset);
|
||||
}
|
||||
|
||||
async function submit(answer) {
|
||||
$("err").style.display = "none";
|
||||
$("yes").disabled = true; $("no").disabled = true;
|
||||
try {
|
||||
const r = await fetch(API + "/api/v1/compliance-orders/" + encodeURIComponent(orderId) + "/sc-insurance?have=" + answer, {
|
||||
method: "POST", headers: { "Content-Type": "application/json" }, body: "{}",
|
||||
});
|
||||
const d = await r.json();
|
||||
if (!r.ok) throw new Error(d.error || "Could not record your answer");
|
||||
$("content").style.display = "none";
|
||||
$("done").style.display = "block";
|
||||
$("done").textContent = d.message || "Thank you — we've recorded your answer.";
|
||||
} catch (e) {
|
||||
$("err").textContent = e.message; $("err").style.display = "block";
|
||||
$("yes").disabled = false; $("no").disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
$("yes").addEventListener("click", () => submit("yes"));
|
||||
$("no").addEventListener("click", () => submit("no"));
|
||||
load();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue