diff --git a/docs/SC COC Form.pdf b/docs/SC COC Form.pdf new file mode 100644 index 0000000..ed986d1 Binary files /dev/null and b/docs/SC COC Form.pdf differ diff --git a/scripts/document_gen/templates/sc_coc_pdf_filler.py b/scripts/document_gen/templates/sc_coc_pdf_filler.py new file mode 100644 index 0000000..0e4fe0e --- /dev/null +++ b/scripts/document_gen/templates/sc_coc_pdf_filler.py @@ -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)) diff --git a/scripts/workers/services/gov_fee.py b/scripts/workers/services/gov_fee.py index f882064..d0b9d39 100644 --- a/scripts/workers/services/gov_fee.py +++ b/scripts/workers/services/gov_fee.py @@ -43,8 +43,14 @@ IFTA_DECAL_FEE_PER_UNIT_CENTS = int(os.getenv("IFTA_DECAL_FEE_PER_UNIT_CENTS", " # Intrastate authority: state filing fee, fixed per state. Conservative # defaults; refine per state as we confirm exact amounts. Cents. +# +# SC: for-hire PROPERTY carriers (not passenger/HHG/hazwaste) register intrastate +# via the SCDMV Certificate of Compliance (COC), $25 new application / $0 renewal. +# The carrier's INSURER files a Form E (liability) and, for class E-LC, a Form H +# (cargo) directly with SCDMV. The COC class (E-L low-value vs E-LC) is what the +# state fee attaches to, so we collect the exact $25 at cost once class is set. INTRASTATE_AUTHORITY_FEE_CENTS = { - "SC": 0, # SC intrastate handled via federal authority; nominal/none + "SC": 2500, # SCDMV COC new-application fee ($25); renewals are $0 "TX": 10000, # TxDMV intrastate "CA": 0, # MCP handled separately "FL": 5000,