"""Fill an official CMS-855 enrollment PDF for Medicare revalidation/enrollment. We fill the *official* CMS form (downloaded into ``docs/CMS-855X Form.pdf``) using its AcroForm fields, mirroring ``mcs150_pdf_filler.py``. The handful of fields we can reliably populate from intake are mapped here; the rest are left for a human to complete during review before the form is e-signed and mailed. After filling we record a signature anchor at the form's official signature box (the ``/Sig`` annotation on the certification page) so the e-sign stamper lands the provider's signature exactly on the certification line. The signed PDF is then printed and mailed USPS Priority Mail to the provider's MAC. Supported forms (by slug -> form type): npi-revalidation / medicare-enrollment (individual) -> 855I medicare-enrollment (group/supplier) -> 855B medicare-enrollment (ordering/referring only) -> 855O Usage: from scripts.document_gen.templates.cms855_pdf_filler import fill_cms855 pdf_bytes, anchors, missing = fill_cms855("855i", intake, order_number) """ from __future__ import annotations import io import logging import re from pathlib import Path LOG = logging.getLogger("cms855_pdf_filler") # docs/ lives two levels up from scripts/document_gen/templates/ DOCS_DIR = Path(__file__).resolve().parents[3] / "docs" FORM_PATHS = { "855i": DOCS_DIR / "CMS-855I Form.pdf", "855b": DOCS_DIR / "CMS-855B Form.pdf", "855o": DOCS_DIR / "CMS-855O Form.pdf", "855a": DOCS_DIR / "CMS-855A Form.pdf", } # Map intake keys -> AcroForm field names, per form. Field names are taken from # the official PDF's AcroForm (verified against each form's /TU tooltips). Only # the reliably-derivable identity fields are mapped; a human completes the rest. FIELD_MAPS = { # 855I — individual practitioner (most common for revalidation) "855i": { # Section 2A: individual personal identifying info "first_name": "5-1", "middle_init": "5-2", "last_name": "5-3", "npi": "5-13", "dob": "5-11", # Section 15: certification — printed signer name "sign_first": "24-1", "sign_middle": "24-2", "sign_last": "24-3", }, # 855B — clinic/group practice "855b": { # Org NPI appears in section 4 area; org name fields vary, so we map the # ones we can verify and leave the rest for human completion. "org_npi": "11-4", }, # 855O — ordering/referring (no billing). Minimal identity fields. "855o": {}, "855a": {}, } # Official signature annotation(s) on the certification page, by form. Recorded # from the PDF's /Sig field rects (page index is 0-based). These let the e-sign # stamper place the provider's signature on the exact certification line. # rect = [llx, lly, urx, ury] in points; page_w/page_h = mediabox dims. SIGNATURE_FIELDS = { "855i": [ {"field": "signer", "page": 24, "rect": [44.9, 522.8, 370.7, 541.2], "page_w": 612.0, "page_h": 792.0}, ], # 855b/855o/855a signature rects are populated when those flows go live; the # filler still works without them (human places signature during review). "855b": [], "855o": [], "855a": [], } def _split_name(full: str) -> tuple[str, str, str]: """Split 'First Middle Last' into (first, middle_initial, last).""" parts = (full or "").split() if not parts: return "", "", "" if len(parts) == 1: return parts[0], "", "" if len(parts) == 2: return parts[0], "", parts[1] return parts[0], parts[1][0], " ".join(parts[2:]) def _lookup_enumeration_type(npi: str) -> str: """Best-effort NPPES lookup of an NPI's enumeration_type (NPI-1 / NPI-2). Returns "" if the NPI is missing, malformed, or the lookup fails — callers fall back to the individual form (855I), the safe default. """ npi = (npi or "").strip() if not re.fullmatch(r"\d{10}", npi): return "" try: import json as _json from urllib.request import urlopen url = f"https://npiregistry.cms.hhs.gov/api/?version=2.1&number={npi}" with urlopen(url, timeout=8) as resp: # nosec - public, read-only data = _json.loads(resp.read().decode("utf-8")) results = data.get("results") or [] if results: return (results[0].get("enumeration_type") or "").upper() except Exception: LOG.warning("NPPES enumeration_type lookup failed for %s", npi) return "" def determine_form_type(slug: str, intake: dict) -> str: """Pick which 855 form applies for the order. Org NPIs (Type 2 / NPI-2) revalidate/enroll on 855B; individuals on 855I. The wizard does not collect enumeration_type, so when it is absent we look it up live from NPPES rather than silently defaulting an organization to the wrong (individual) form, which CMS would reject. """ if slug not in ("npi-revalidation", "medicare-enrollment"): return "855i" enum_type = (intake.get("enumeration_type") or "").upper() if not enum_type: enum_type = _lookup_enumeration_type(intake.get("npi", "")) return "855b" if enum_type in ("NPI-2", "2", "ORGANIZATION") else "855i" def fill_cms855(form_type: str, intake: dict, order_number: str = "") -> tuple[bytes, list[dict], list[str]]: """Fill the official CMS-855 form. Returns (pdf_bytes, signature_anchors, unmapped_fields_note). ``signature_anchors`` is the list the e-sign stamper consumes. ``unmapped_fields_note`` lists data we could NOT auto-place, for the human reviewer to complete before mailing. """ try: from pypdf import PdfReader, PdfWriter except ImportError as exc: # pragma: no cover raise RuntimeError("pypdf is required to fill CMS-855 forms") from exc form_type = (form_type or "855i").lower() form_path = FORM_PATHS.get(form_type) if not form_path or not form_path.exists(): raise FileNotFoundError(f"Official form not found for {form_type}: {form_path}") reader = PdfReader(str(form_path)) writer = PdfWriter() writer.append(reader) fmap = FIELD_MAPS.get(form_type, {}) # Derive values from intake. provider = intake.get("provider_name", "") first, mid, last = _split_name(provider) npi = intake.get("npi", "") dob = intake.get("dob", "") # MMDDYYYY if provided values = { "first_name": intake.get("first_name", first), "middle_init": intake.get("middle_initial", mid), "last_name": intake.get("last_name", last), "npi": npi, "dob": dob, "sign_first": intake.get("first_name", first), "sign_middle": intake.get("middle_initial", mid), "sign_last": intake.get("last_name", last), "org_npi": npi, } field_updates = {} for intake_key, pdf_field in fmap.items(): v = values.get(intake_key, "") if v: field_updates[pdf_field] = str(v) # Apply to every page that contains these fields. for page in writer.pages: try: writer.update_page_form_field_values(page, field_updates) except Exception: # update only the fields that exist on this page; ignore the rest pass # Make filled values render in all viewers. try: writer.set_need_appearances_writer(True) except Exception: pass buf = io.BytesIO() writer.write(buf) pdf_bytes = buf.getvalue() # Build signature anchors from the official /Sig rects. anchors = [] for sig in SIGNATURE_FIELDS.get(form_type, []): llx, lly, urx, ury = sig["rect"] anchors.append({ "field": sig["field"], "page": sig["page"], "x": float(llx) + 4, "y": float(lly) + 1, "w": float(urx - llx) - 8, "h": max(float(ury - lly), 18.0), "page_w": sig["page_w"], "page_h": sig["page_h"], }) # What we could not auto-fill (so the human reviewer knows to complete it). missing = [] if not anchors: missing.append(f"No signature anchor mapped for {form_type} — place signature manually before mailing.") if form_type in ("855b", "855a") or (form_type == "855o"): missing.append(f"{form_type.upper()} is partially mapped — verify all sections before signing/mailing.") if not intake.get("dob"): missing.append("Date of birth not collected — required on the 855; confirm with provider.") if not intake.get("practice_state"): missing.append("Practice/MAC routing state not collected — needed to address the USPS envelope to the correct MAC.") return pdf_bytes, anchors, missing if __name__ == "__main__": # quick local render for visual verification sample = { "provider_name": "Jane Q Smith", "npi": "1234567893", "dob": "01011980", "practice_state": "CA", "enumeration_type": "NPI-1", } pdf, anchors, missing = fill_cms855("855i", sample, "CO-TEST1234") with open("/tmp/cms855i_filled.pdf", "wb") as f: f.write(pdf) print("wrote /tmp/cms855i_filled.pdf") print("anchors:", anchors) print("missing:", missing)