From c46efe5730165f247deb162a7314ef302790a167 Mon Sep 17 00:00:00 2001 From: justin Date: Tue, 16 Jun 2026 09:15:55 -0500 Subject: [PATCH] feat(sc-coc): SC intrastate Certificate of Compliance flow (insurance gate -> $25 fee -> file) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- api/src/routes/compliance-orders.ts | 112 +++++++++++++- scripts/workers/services/sc_coc_filing.py | 164 +++++++++++++++++++++ scripts/workers/services/state_trucking.py | 160 ++++++++++++++++---- site/public/order/sc-insurance/index.html | 120 +++++++++++++++ 4 files changed, 526 insertions(+), 30 deletions(-) create mode 100644 scripts/workers/services/sc_coc_filing.py create mode 100644 site/public/order/sc-insurance/index.html diff --git a/api/src/routes/compliance-orders.ts b/api/src/routes/compliance-orders.ts index eb26750..8968dec 100644 --- a/api/src/routes/compliance-orders.ts +++ b/api/src/routes/compliance-orders.ts @@ -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 diff --git a/scripts/workers/services/sc_coc_filing.py b/scripts/workers/services/sc_coc_filing.py new file mode 100644 index 0000000..203a1bb --- /dev/null +++ b/scripts/workers/services/sc_coc_filing.py @@ -0,0 +1,164 @@ +"""South Carolina intrastate Certificate of Compliance (COC) flow. + +For-hire PROPERTY carriers based in / operating intrastate in SC register via the +SCDMV Certificate of Compliance (COC) — NOT a PSC certificate (which only covers +passenger, household-goods, and hazardous-waste-for-disposal carriers). + +Compliance steps we automate for an SC intrastate property carrier: + 1. Confirm the carrier's liability insurance is (or will be) filed with SCDMV + on a Form E by their INSURANCE COMPANY (SCDMV does not accept an ACORD cert). + We email a simple yes/no question with a one-click response page. + - YES -> proceed to bill the $25 COC fee + file the COC application. + - NO -> open a broker-referral ticket so we connect them with an insurer + that writes SC intrastate liability and can file the Form E. + 2. Bill the exact $25 SCDMV COC new-application fee at cost (gov-fee child), + reusing the standard payment-link flow. + 3. Fill + submit the SCDMV Form COC (mail w/ $25 check to Blythewood, or fax), + and confirm the Form E is on record. + +Coverage class (drives whether cargo insurance / Form H is needed, and is the +basis for the state fee determination): + - E-L : low-value commodities (scrap metal, dump-truck aggregates) — Form E + liability only, no cargo insurance. + - E-LC : property properly insured for any cargo — Form E + Form H. + +Insurance minimums (intrastate, non-hazmat liability): + - GVWR >= 10,000 lbs: $750,000 + - GVWR < 10,000 lbs: $300,000 +""" + +from __future__ import annotations + +import json +import logging +import os +import smtplib +from email.mime.text import MIMEText + +LOG = logging.getLogger("workers.services.sc_coc_filing") + +SITE = os.getenv("PUBLIC_SITE_URL", "https://performancewest.net").rstrip("/") +SMTP_HOST = os.getenv("SMTP_HOST", "co.carrierone.com") +SMTP_PORT = int(os.getenv("SMTP_PORT", "587")) +SMTP_USER = os.getenv("SMTP_USER", "") +SMTP_PASS = os.getenv("SMTP_PASS", "") +SMTP_FROM = os.getenv("SMTP_FROM", "Performance West ") +FROM_ADDR = "noreply@performancewest.net" + +# SCDMV COC submission coordinates (from SCDMV Form COC, rev 11/2025). +SCDMV_COC = { + "mail": "SCDMV, P.O. Box 1498, Blythewood, SC 29016-0027", + "fax": "(803) 896-2698", + "phone": "(803) 896-3870", + "new_fee_usd": 25, +} + + +def liability_minimum_usd(intake: dict) -> int: + """SC intrastate non-hazmat liability minimum by GVWR bracket.""" + bracket = (intake.get("gross_weight_bracket") or "").lower() + # Treat unknown / 26k / 80k brackets as the >=10k bucket (the common case). + if bracket in ("under_10k", "lt_10k"): + return 300_000 + return 750_000 + + +def insurance_response_url(order_number: str, answer: str) -> str: + """One-click yes/no link the customer clicks from the email.""" + return f"{SITE}/order/sc-insurance?order={order_number}&have={answer}" + + +def send_insurance_question_email(order_number: str, customer_email: str, + customer_name: str, entity_name: str, + intake: dict) -> bool: + """Email the carrier a simple yes/no: do they have SC intrastate liability + insurance their insurer can file on a Form E with SCDMV?""" + if not customer_email: + return False + minimum = liability_minimum_usd(intake) + yes_url = insurance_response_url(order_number, "yes") + no_url = insurance_response_url(order_number, "no") + body = ( + f"Hi {customer_name or 'there'},\n\n" + f"We're getting your South Carolina intrastate authority set up for " + f"{entity_name}. In SC, a for-hire carrier like yours registers with the " + f"SCDMV (Certificate of Compliance) — and the one thing the state requires " + f"from your insurance company is a liability filing called a \"Form E.\"\n\n" + f"Quick question so we know how to proceed:\n\n" + f"Does your current insurance company have (or can they file) a Form E " + f"showing at least ${minimum:,} in liability coverage for South Carolina " + f"intrastate operation?\n\n" + f"(Note: this must be a Form E filed by your insurance COMPANY directly " + f"with SCDMV — a regular ACORD certificate of insurance is not accepted.)\n\n" + f"✅ YES — my insurer can file the Form E:\n{yes_url}\n\n" + f"❌ NO / NOT SURE — I need help getting the right insurance:\n{no_url}\n\n" + f"If you click NO, no problem — we'll connect you with a broker who writes " + f"SC intrastate trucking liability and can file the Form E for you.\n\n" + f"Once your Form E is on file, we complete your SCDMV Certificate of " + f"Compliance and you're cleared to operate.\n\n" + f"Order: {order_number}\n" + f"Questions? Just reply here or call (888) 411-0383.\n\n" + f"Performance West Inc.\nDOT / State Motor Carrier Compliance\n" + ) + try: + msg = MIMEText(body) + msg["Subject"] = f"Quick question about your SC insurance — {entity_name}" + msg["From"] = SMTP_FROM + msg["To"] = customer_email + with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=30) as s: + s.starttls() + if SMTP_USER and SMTP_PASS: + s.login(SMTP_USER, SMTP_PASS) + s.sendmail(FROM_ADDR, [customer_email], msg.as_string()) + LOG.info("[%s] SC insurance question emailed to %s", order_number, customer_email) + return True + except Exception as exc: # noqa: BLE001 + LOG.error("[%s] Failed to send SC insurance question: %s", order_number, exc) + return False + + +def record_insurance_answer(order_number: str, have_insurance: bool) -> bool: + """Persist the carrier's yes/no into intake_data.sc_coc_insurance. Returns + True on success. Idempotent.""" + try: + import psycopg2 + conn = psycopg2.connect(os.environ.get("DATABASE_URL", "")) + with conn.cursor() as cur: + cur.execute( + "SELECT intake_data FROM compliance_orders WHERE order_number = %s", + (order_number,), + ) + row = cur.fetchone() + if not row: + conn.close() + return False + intake = row[0] or {} + if isinstance(intake, str): + intake = json.loads(intake) + intake["sc_coc_insurance"] = "yes" if have_insurance else "no" + from datetime import datetime, timezone + intake["sc_coc_insurance_at"] = datetime.now(timezone.utc).isoformat() + cur.execute( + "UPDATE compliance_orders SET intake_data = %s, updated_at = now() " + "WHERE order_number = %s", + (json.dumps(intake), order_number), + ) + conn.commit() + conn.close() + LOG.info("[%s] Recorded SC COC insurance answer: %s", order_number, + "yes" if have_insurance else "no") + return True + except Exception as exc: # noqa: BLE001 + LOG.error("[%s] Failed to record insurance answer: %s", order_number, exc) + return False + + +def build_coc_package(order_number: str, intake: dict, + coverage_class: str | None = None) -> str | None: + """Fill the SCDMV Form COC for this order. Returns the filled PDF path.""" + try: + from scripts.document_gen.templates.sc_coc_pdf_filler import fill_sc_coc + return fill_sc_coc(intake, order_number=order_number, coverage_class=coverage_class) + except Exception as exc: # noqa: BLE001 + LOG.error("[%s] Failed to build COC package: %s", order_number, exc) + return None diff --git a/scripts/workers/services/state_trucking.py b/scripts/workers/services/state_trucking.py index 8a83c2b..a4937a7 100644 --- a/scripts/workers/services/state_trucking.py +++ b/scripts/workers/services/state_trucking.py @@ -713,6 +713,94 @@ class StateTruckingHandler: LOG.warning("[%s] Could not check gov-fee settlement: %s", order_number, exc) return False + def _handle_sc_coc_gate(self, order_number, entity_name, customer_email, + customer_phone, intake) -> bool: + """SC intrastate Certificate of Compliance insurance gate. + + SC for-hire property carriers register via the SCDMV COC, which requires + their INSURER to file a Form E (liability). We ask the carrier a one-click + yes/no first: + - no answer yet -> email the question, HOLD (return True) + - answered 'no' -> broker referral is in progress, HOLD (return True) + - answered 'yes' -> insurance confirmed, PROCEED to bill the $25 COC + fee (return False so the caller falls through to fee billing) + """ + answer = str(intake.get("sc_coc_insurance", "")).lower() + + if answer == "yes": + LOG.info("[%s] SC COC: insurance confirmed — proceeding to $25 COC fee", order_number) + return False # proceed to bill the COC fee + + if answer == "no": + # Broker referral opened (by the response endpoint). Hold until ops + # gets them covered, then they re-answer yes. + LOG.info("[%s] SC COC: carrier needs insurance — broker referral, holding", order_number) + try: + notify_fulfillment_todo( + title=f"SC COC — carrier needs insurance broker — {entity_name}", + order_number=order_number, + service_slug="intrastate-authority", + priority="normal", + description=(f"{entity_name} answered NO to the SC intrastate Form E " + f"insurance question. A broker-referral ticket was opened.\n" + f"Connect them with an SC intrastate liability insurer who can " + f"file a Form E with SCDMV, then they re-confirm and we file the " + f"$25 Certificate of Compliance.\nCustomer: {customer_email}"), + ) + except Exception: + pass + return True # hold + + # No answer yet: send the yes/no insurance question (once). + try: + from scripts.workers.services.sc_coc_filing import send_insurance_question_email + except Exception as exc: # noqa: BLE001 + LOG.error("[%s] sc_coc_filing import failed: %s", order_number, exc) + return True # hold safely rather than mis-bill + already = str(intake.get("sc_coc_insurance_asked", "")).lower() in ("1", "true", "yes") + if not already: + send_insurance_question_email(order_number, customer_email, + intake.get("customer_name", ""), entity_name, intake) + self._mark_intake_flag(order_number, "sc_coc_insurance_asked", "true") + try: + notify_fulfillment_todo( + title=f"SC COC — insurance question sent — {entity_name}", + order_number=order_number, + service_slug="intrastate-authority", + priority="low", + description=(f"Asked {entity_name} whether their insurer can file a Form E " + f"for SC intrastate. When they confirm YES we bill the $25 SCDMV " + f"Certificate of Compliance fee and file; if NO we open a broker " + f"referral.\nCustomer: {customer_email}"), + ) + except Exception: + pass + return True # hold pending their answer + + def _mark_intake_flag(self, order_number: str, key: str, value: str) -> None: + """Best-effort set intake_data[key]=value on the order (no schema change).""" + try: + import json as _json + import psycopg2 + conn = psycopg2.connect(os.environ.get("DATABASE_URL", "")) + with conn.cursor() as cur: + cur.execute("SELECT intake_data FROM compliance_orders WHERE order_number = %s", + (order_number,)) + row = cur.fetchone() + if not row: + conn.close() + return + data = row[0] or {} + if isinstance(data, str): + data = _json.loads(data) + data[key] = value + cur.execute("UPDATE compliance_orders SET intake_data = %s, updated_at = now() " + "WHERE order_number = %s", (_json.dumps(data), order_number)) + conn.commit() + conn.close() + except Exception as exc: # noqa: BLE001 + LOG.warning("[%s] Could not set intake flag %s: %s", order_number, key, exc) + def _request_gov_fee_payment(self, order_number, service_slug, service_name, entity_name, customer_email, customer_phone, intake, signed_auth_key=None) -> bool: @@ -751,39 +839,53 @@ class StateTruckingHandler: pass return sent - # ── Intrastate authority: email the PSC/PUC, wait for the fee invoice ── - # (Falls through to manual todo when the state has no email submission - # path — handled by returning False below.) + # ── Intrastate authority ────────────────────────────────────────────── + # Per-state: figure out what the carrier actually needs and route to it. + # SC for-hire PROPERTY carriers register via the SCDMV Certificate of + # Compliance (COC) — gated on a Form E liability filing by their insurer. + # We email a yes/no insurance question first; once they confirm (or we've + # referred them to a broker) we bill the exact $25 COC fee + file. Other + # states fall through to the PSC/PUC email path or a manual todo. if service_slug == "intrastate-authority": - try: - from scripts.workers.services.intrastate_filing import ( - send_intrastate_submission, state_isa_contact, - ) - except Exception as exc: # noqa: BLE001 - LOG.error("[%s] intrastate_filing import failed: %s", order_number, exc) - return False base_state = (intake.get("base_state") or intake.get("address_state") or "").upper() - if not state_isa_contact(base_state): - LOG.info("[%s] No intrastate email contact for %s — manual todo", order_number, base_state) - return False - sent = send_intrastate_submission(order_number, entity_name, - intake.get("dot_number", ""), base_state, intake, - signed_auth_key=signed_auth_key) - if sent: + + if base_state == "SC": + if self._handle_sc_coc_gate(order_number, entity_name, customer_email, + customer_phone, intake): + return True # hold pending insurance answer / broker referral + # else: insurance confirmed -> fall through to bill the $25 COC fee + + else: + # Non-SC: email the state PSC/PUC if we have a submission contact; + # otherwise fall back to a manual todo. try: - notify_fulfillment_todo( - title=f"Intrastate authority submitted to {base_state} PSC — awaiting fee — {entity_name}", - order_number=order_number, - service_slug=service_slug, - priority="normal", - description=(f"Intrastate authority application emailed to the {base_state} " - f"PSC/PUC with the signed POA.\nWaiting on their requirements + " - f"fee invoice; when it arrives we auto-bill the customer the exact " - f"amount and you'll get a Telegram alert.\nCustomer: {customer_email}"), + from scripts.workers.services.intrastate_filing import ( + send_intrastate_submission, state_isa_contact, ) - except Exception: - pass - return sent + except Exception as exc: # noqa: BLE001 + LOG.error("[%s] intrastate_filing import failed: %s", order_number, exc) + return False + if not state_isa_contact(base_state): + LOG.info("[%s] No intrastate email contact for %s — manual todo", order_number, base_state) + return False + sent = send_intrastate_submission(order_number, entity_name, + intake.get("dot_number", ""), base_state, intake, + signed_auth_key=signed_auth_key) + if sent: + try: + notify_fulfillment_todo( + title=f"Intrastate authority submitted to {base_state} PSC — awaiting fee — {entity_name}", + order_number=order_number, + service_slug=service_slug, + priority="normal", + description=(f"Intrastate authority application emailed to the {base_state} " + f"PSC/PUC with the signed POA.\nWaiting on their requirements + " + f"fee invoice; when it arrives we auto-bill the customer the exact " + f"amount and you'll get a Telegram alert.\nCustomer: {customer_email}"), + ) + except Exception: + pass + return sent # ── IFTA / intrastate: published fee, bill the estimate now ─────────── try: from scripts.workers.services.gov_fee import ( diff --git a/site/public/order/sc-insurance/index.html b/site/public/order/sc-insurance/index.html new file mode 100644 index 0000000..46ad52d --- /dev/null +++ b/site/public/order/sc-insurance/index.html @@ -0,0 +1,120 @@ + + + + + + + South Carolina insurance | Performance West + + + + + + +
+
+
+ +

South Carolina intrastate insurance

+

Loading…

+
+
+
+

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 Form E.

+

Does your insurance company have (or can they file) a Form E showing + at least $750,000 in liability coverage for South + Carolina intrastate operation?

+ + +
A Form E must be filed by your insurance company + 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.
+ +
+ + +
+
+
+ + + +