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;
|
const id = req.params.id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Support both batch ID (CB-) and order number (CO-)
|
|
||||||
const whereCol = id.startsWith("CB-") ? "batch_id" : "order_number";
|
const whereCol = id.startsWith("CB-") ? "batch_id" : "order_number";
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
`UPDATE compliance_orders
|
`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 ──────────────────
|
// ── POST /api/v1/admin/compliance-orders/:id/approve-submit ──────────────────
|
||||||
// Admin verification gate: an order that has been prepared + (if needed) signed
|
// 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
|
// is held at fulfillment_status='ready_to_file'. The admin reviews the prepared
|
||||||
|
|
|
||||||
164
scripts/workers/services/sc_coc_filing.py
Normal file
164
scripts/workers/services/sc_coc_filing.py
Normal file
|
|
@ -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 <noreply@performancewest.net>")
|
||||||
|
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
|
||||||
|
|
@ -713,6 +713,94 @@ class StateTruckingHandler:
|
||||||
LOG.warning("[%s] Could not check gov-fee settlement: %s", order_number, exc)
|
LOG.warning("[%s] Could not check gov-fee settlement: %s", order_number, exc)
|
||||||
return False
|
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,
|
def _request_gov_fee_payment(self, order_number, service_slug, service_name,
|
||||||
entity_name, customer_email, customer_phone, intake,
|
entity_name, customer_email, customer_phone, intake,
|
||||||
signed_auth_key=None) -> bool:
|
signed_auth_key=None) -> bool:
|
||||||
|
|
@ -751,39 +839,53 @@ class StateTruckingHandler:
|
||||||
pass
|
pass
|
||||||
return sent
|
return sent
|
||||||
|
|
||||||
# ── Intrastate authority: email the PSC/PUC, wait for the fee invoice ──
|
# ── Intrastate authority ──────────────────────────────────────────────
|
||||||
# (Falls through to manual todo when the state has no email submission
|
# Per-state: figure out what the carrier actually needs and route to it.
|
||||||
# path — handled by returning False below.)
|
# 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":
|
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()
|
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)
|
if base_state == "SC":
|
||||||
return False
|
if self._handle_sc_coc_gate(order_number, entity_name, customer_email,
|
||||||
sent = send_intrastate_submission(order_number, entity_name,
|
customer_phone, intake):
|
||||||
intake.get("dot_number", ""), base_state, intake,
|
return True # hold pending insurance answer / broker referral
|
||||||
signed_auth_key=signed_auth_key)
|
# else: insurance confirmed -> fall through to bill the $25 COC fee
|
||||||
if sent:
|
|
||||||
|
else:
|
||||||
|
# Non-SC: email the state PSC/PUC if we have a submission contact;
|
||||||
|
# otherwise fall back to a manual todo.
|
||||||
try:
|
try:
|
||||||
notify_fulfillment_todo(
|
from scripts.workers.services.intrastate_filing import (
|
||||||
title=f"Intrastate authority submitted to {base_state} PSC — awaiting fee — {entity_name}",
|
send_intrastate_submission, state_isa_contact,
|
||||||
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:
|
except Exception as exc: # noqa: BLE001
|
||||||
pass
|
LOG.error("[%s] intrastate_filing import failed: %s", order_number, exc)
|
||||||
return sent
|
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 ───────────
|
# ── IFTA / intrastate: published fee, bill the estimate now ───────────
|
||||||
try:
|
try:
|
||||||
from scripts.workers.services.gov_fee import (
|
from scripts.workers.services.gov_fee import (
|
||||||
|
|
|
||||||
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