feat(sc-coc): SCDMV Certificate of Compliance PDF filler + correct $25 state fee
SC for-hire PROPERTY carriers (not passenger/HHG/hazwaste) register intrastate
via the SCDMV Certificate of Compliance (COC), not a PSC certificate. This adds:
- sc_coc_pdf_filler.fill_sc_coc(): fills the official SCDMV Form COC from
intake (business name, officers, physical/mailing address, phone), picks
New vs Renewal, and stamps the coverage class (E-L low-value / E-LC).
Field names in the source PDF are auto-generated + offset from their labels;
mapped here by verified on-page geometry. Verified by render.
- suggest_coverage_class(): E-L for low-value cargo (scrap/dump/aggregate),
else E-LC (safer default).
- gov_fee: SC intrastate fee corrected from $0 placeholder to the real $25
COC new-application fee (renewals $0), billed at cost.
The carrier's INSURER files the Form E (liability) + Form H (cargo, E-LC only)
directly with SCDMV; we collect the COC app + $25 and submit it.
This commit is contained in:
parent
01b3e1d234
commit
ad590aab7c
3 changed files with 189 additions and 1 deletions
182
scripts/document_gen/templates/sc_coc_pdf_filler.py
Normal file
182
scripts/document_gen/templates/sc_coc_pdf_filler.py
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
"""SCDMV Certificate of Compliance (COC) PDF filler.
|
||||
|
||||
The Certificate of Compliance is South Carolina's intrastate for-hire motor
|
||||
carrier registration for PROPERTY carriers (everyone except passenger,
|
||||
household-goods, and hazardous-waste-for-disposal carriers — those go to the
|
||||
PSC instead). It is filed on SCDMV Form COC and mailed with a $25 fee to:
|
||||
|
||||
SCDMV, P.O. Box 1498, Blythewood, SC 29016-0027
|
||||
|
||||
Two coverage classes:
|
||||
- E-L : low-value commodities (dump-truck-type, scrap metal, etc.) — NO
|
||||
cargo insurance required (liability Form E only).
|
||||
- E-LC : property properly insured for any cargo — needs Form E + Form H.
|
||||
|
||||
The carrier's INSURANCE COMPANY (not agent) must file a Form E (liability) and,
|
||||
if E-LC, a Form H (cargo) directly with SCDMV. SCDMV does NOT accept an ACORD
|
||||
certificate. We collect the COC application + $25; the insurer files the Form E.
|
||||
|
||||
This module fills the official SCDMV Form COC from intake data. Field names in
|
||||
the source PDF are partly auto-generated ('undefined', '1'..'6'); they are
|
||||
mapped here by their verified on-page positions:
|
||||
undefined -> Class E-L checkbox (8x8 box, y=581)
|
||||
undefined_2 -> Class E-LC checkbox (8x8 box, y=520)
|
||||
'1'..'6' -> officer/partner name+address lines
|
||||
Mailing Address (label is mis-assigned) -> PHYSICAL address line (y=159)
|
||||
undefined_3 -> mailing address line (y=137)
|
||||
Telephone Number / undefined_4 -> phone / fax
|
||||
|
||||
Usage:
|
||||
from scripts.document_gen.templates.sc_coc_pdf_filler import fill_sc_coc
|
||||
path = fill_sc_coc(intake, order_number="CO-...", coverage_class="E-L")
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
LOG = logging.getLogger("document_gen.sc_coc")
|
||||
|
||||
try:
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
except ImportError: # pragma: no cover
|
||||
try:
|
||||
from PyPDF2 import PdfReader, PdfWriter # type: ignore
|
||||
except ImportError:
|
||||
PdfReader = PdfWriter = None # type: ignore
|
||||
|
||||
DOCS_DIR = Path(__file__).resolve().parent.parent.parent.parent / "docs"
|
||||
COC_FORM = DOCS_DIR / "SC COC Form.pdf"
|
||||
OUTPUT_DIR = Path(os.getenv("DOC_OUTPUT_DIR", "/tmp/coc-filings"))
|
||||
|
||||
# Low-value commodities that SCDMV classifies as E-L (no cargo insurance). Scrap
|
||||
# metal, dump-truck aggregates, etc. Used to auto-suggest the coverage class.
|
||||
E_L_COMMODITY_HINTS = (
|
||||
"scrap", "metal", "dump", "aggregate", "gravel", "sand", "dirt", "rock",
|
||||
"debris", "waste" "recycl", "junk", "salvage", "demolition", "asphalt",
|
||||
"concrete", "mulch", "wood chip", "fill",
|
||||
)
|
||||
|
||||
|
||||
def suggest_coverage_class(intake: dict) -> str:
|
||||
"""Best-effort E-L vs E-LC suggestion from the carrier's cargo description.
|
||||
Defaults to E-LC (the safer, fully-insured class) when unknown so we never
|
||||
under-state the insurance requirement."""
|
||||
cargo = " ".join(str(intake.get(k, "")) for k in
|
||||
("cargo_carried", "commodities", "commodity",
|
||||
"cargo", "operation_description", "legal_name")).lower()
|
||||
if any(h in cargo for h in E_L_COMMODITY_HINTS):
|
||||
return "E-L"
|
||||
return "E-LC"
|
||||
|
||||
|
||||
def _is_renewal(intake: dict) -> bool:
|
||||
return str(intake.get("coc_renewal", "")).lower() in ("yes", "true", "1") \
|
||||
or bool(intake.get("existing_coc_number"))
|
||||
|
||||
|
||||
def fill_sc_coc(intake: dict, order_number: str = "",
|
||||
coverage_class: str | None = None) -> str:
|
||||
"""Fill the SCDMV Form COC. Returns the path to the written PDF.
|
||||
|
||||
coverage_class: 'E-L' or 'E-LC'. If None, auto-suggested from cargo.
|
||||
"""
|
||||
if PdfReader is None:
|
||||
raise ImportError("pypdf not installed")
|
||||
if not COC_FORM.exists():
|
||||
raise FileNotFoundError(f"SC COC form not found: {COC_FORM}")
|
||||
|
||||
coverage_class = (coverage_class or suggest_coverage_class(intake)).upper()
|
||||
reader = PdfReader(str(COC_FORM))
|
||||
writer = PdfWriter()
|
||||
writer.clone_document_from_reader(reader)
|
||||
|
||||
legal_name = intake.get("legal_name") or intake.get("entity_name") or ""
|
||||
phys = ", ".join(p for p in [
|
||||
intake.get("address_street", "") or intake.get("phy_street", ""),
|
||||
intake.get("address_city", "") or intake.get("phy_city", ""),
|
||||
f"{intake.get('address_state','') or intake.get('phy_state','')} "
|
||||
f"{intake.get('address_zip','') or intake.get('phy_zip','')}".strip(),
|
||||
] if p and p.strip())
|
||||
mailing = ", ".join(p for p in [
|
||||
intake.get("mailing_street", ""),
|
||||
intake.get("mailing_city", ""),
|
||||
f"{intake.get('mailing_state','')} {intake.get('mailing_zip','')}".strip(),
|
||||
] if p and p.strip()) or phys
|
||||
|
||||
# Officers / partners (up to 6 lines). Accept a list or single signer.
|
||||
officers = intake.get("officers") or []
|
||||
if not officers:
|
||||
signer = intake.get("signer_name") or ""
|
||||
if signer:
|
||||
officers = [f"{signer} - {phys}"]
|
||||
|
||||
field_updates = {
|
||||
# Business identity (the source PDF splits the business-name label across
|
||||
# two text lines, suffixed " 1" / " 2"; put the name on line 1).
|
||||
"Business Name Corporation Partnership Sole Proprietorship With or Without Trade name 1":
|
||||
legal_name,
|
||||
# The form's text fields are auto-named after the label BELOW them, so the
|
||||
# names are offset by one. Mapped here by verified on-page geometry:
|
||||
# "Mailing Address" field (y159, full width) -> PHYSICAL address line
|
||||
# "undefined_3" field (y137) -> MAILING address line
|
||||
# "Fax Number" field (y80, x170) -> TELEPHONE box
|
||||
# "undefined_4" field (y80, x417) -> FAX box
|
||||
"Mailing Address": phys,
|
||||
"undefined_3": mailing,
|
||||
"Fax Number": intake.get("phone", "") or intake.get("telephone", ""),
|
||||
"undefined_4": intake.get("fax", ""),
|
||||
}
|
||||
# Officer/partner lines 1..6
|
||||
for i, line in enumerate(officers[:6], start=1):
|
||||
field_updates[str(i)] = str(line)
|
||||
|
||||
# Checkboxes: new vs renewal, and coverage class.
|
||||
button_updates = {}
|
||||
if _is_renewal(intake):
|
||||
button_updates["I am renewing an existing Certificate of Compliance I understand no fee is required for this submission"] = "/On"
|
||||
else:
|
||||
button_updates["I am applying for a Certificate of Compliance for the first time I understand that a 25 fee is required for initial"] = "/On"
|
||||
|
||||
# Class E-L (undefined) vs E-LC (undefined_2). These are tiny text boxes in
|
||||
# the source, so we stamp an "X" into the right one.
|
||||
if coverage_class == "E-L":
|
||||
field_updates["undefined"] = "X"
|
||||
else:
|
||||
field_updates["undefined_2"] = "X"
|
||||
|
||||
# Apply text fields (tolerant of name mismatches across PDF revisions).
|
||||
for page in writer.pages:
|
||||
try:
|
||||
writer.update_page_form_field_values(page, field_updates, auto_regenerate=False)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
LOG.debug("COC text fill page warning: %s", exc)
|
||||
# Apply radio/checkbox states.
|
||||
for page in writer.pages:
|
||||
try:
|
||||
writer.update_page_form_field_values(page, button_updates, auto_regenerate=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
safe = (order_number or legal_name or "coc").replace("/", "_").replace(" ", "_")
|
||||
out = OUTPUT_DIR / f"SC_COC_{safe}.pdf"
|
||||
with open(out, "wb") as fh:
|
||||
writer.write(fh)
|
||||
LOG.info("[%s] Filled SC COC (%s) -> %s", order_number, coverage_class, out)
|
||||
return str(out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
demo = {
|
||||
"legal_name": "ALLENS SCRAP METAL LLC",
|
||||
"address_street": "3838 DANNY RD", "address_city": "LORIS",
|
||||
"address_state": "SC", "address_zip": "29569",
|
||||
"phone": "8435551234", "signer_name": "Mitchell Allen",
|
||||
"cargo_carried": "scrap metal",
|
||||
}
|
||||
p = fill_sc_coc(demo, order_number="CO-DEMO")
|
||||
print("wrote", p, "class:", suggest_coverage_class(demo))
|
||||
Loading…
Add table
Add a link
Reference in a new issue