new-site/scripts/document_gen/templates/cms855_pdf_filler.py
justin 695ace207c Reframe healthcare filing as standard vs expedited; e2e test + bug fixes
Copy: drop paper/electronic/fax framing across the revalidation + enrollment
marketing pages and the order-confirmation email; present two service tiers:
- Standard filing  (no CMS account; we prepare CMS-855, you sign, we submit to MAC)
- Expedited filing (CMS I&A surrogate access; same-day PECOS filing + tracking)
Internal worker todos + the _STANDARD_FILING_SLUGS identifier updated to match.

New scripts/test_healthcare_e2e.py validates the whole order line (slug
consistency x6 places, price agreement, intake field collection+enforcement,
worker dispatch, handler execution producing CMS-855 PDF+anchor, free-tool
action_urls). 45 checks.

Bugs found + fixed by the test:
- medicare-enrollment requires practice_state server-side but the wizard never
  enforced it -> orders could be paid then stall. Wizard now requires it.
- determine_form_type defaulted org NPIs to the individual 855I because
  enumeration_type is never collected -> wrong form, CMS rejection. Now does a
  live NPPES lookup (safe 855I fallback).
2026-06-05 03:58:46 -05:00

247 lines
9.1 KiB
Python

"""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)