new-site/scripts/tests/test_cms10114.py
justin e6a630ada1 healthcare: verify CMS-10114 update path, correct NPI Enumerator address, build CMS-10114 filler
Verified firsthand against the live CMS-10114 (Rev. 02/25, OMB 0938-0931):
- Section 1A confirms paper is valid for Change of Information (#2) AND
  Reactivation (#4), not just initial enumeration. Resolves the UNCERTAIN flag.
- Current mailing address is CMS NPI Enumerator Services, Mail Stop DO-01-51,
  7500 Security Blvd, Baltimore MD 21244. The old Fargo PO Box 6059 is retired;
  corrected in mac_routing.NPI_ENUMERATOR + all docs.
- No electronic no-login equivalent exists for CMS (NPI Registry API is
  read-only; PECOS/NPPES-IA require login), unlike FMCSA's ask.fmcsa ticket form.
  So tiers stay: Standard=paper CMS-10114 (no login), Expedited=NPPES surrogate.

New: cms10114_pdf_filler.py fills the flat official form via text overlay
(reason checkbox + NPI + Section 2A identity + Section 4A cert name + signature
anchor); wired into npi_provider._generate_10114_for_signing for nppes-update.
Signed forms route to the NPI Enumerator via the existing daily batch.

Tests: test_cms10114.py 27/27, test_paper_batch.py 15/15, Astro build 58 pages.
2026-06-07 02:04:41 -05:00

128 lines
5 KiB
Python

"""Tests for the CMS-10114 NPI Application/Update filler (Standard no-login path).
Verifies the overlay-based filler:
- selects the correct Reason-for-Submittal checkbox per reason/slug
- places the NPI on the correct line for each reason
- writes the provider name into Section 2A and the Section 4A certification
- produces a signature anchor on the certification page
- surfaces the right "manual completion" notes
- mails to the verified Baltimore NPI Enumerator address (via mac_routing)
Run: python scripts/tests/test_cms10114.py
"""
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "scripts"))
import importlib.util # noqa: E402
import pdfplumber # noqa: E402
from workers import mac_routing as mr # noqa: E402
def _load_module(name, rel_path):
spec = importlib.util.spec_from_file_location(name, ROOT / rel_path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
# Load the filler directly to avoid document_gen/__init__ side-effects (python-docx).
f = _load_module("cms10114_pdf_filler",
"scripts/document_gen/templates/cms10114_pdf_filler.py")
H = 792.0
_fails = 0
def check(name, cond):
global _fails
status = "PASS" if cond else "FAIL"
if not cond:
_fails += 1
print(f" {status} {name}")
def _tokens(pdf_bytes, page_index):
import io
pdf = pdfplumber.open(io.BytesIO(pdf_bytes))
page = pdf.pages[page_index]
return {w["text"]: (w["x0"], H - w["bottom"]) for w in page.extract_words()}
SAMPLE = {
"provider_name": "Jane Q Smith",
"npi": "1234567893",
"practice_state": "CA",
"enumeration_type": "NPI-1",
}
def main():
print("reason normalization")
check("slug nppes-update -> change", f.normalize_reason("nppes-update") == "change")
check("slug npi-reactivation -> reactivation", f.normalize_reason("npi-reactivation") == "reactivation")
check("explicit 'deactivation' kept", f.normalize_reason("deactivation") == "deactivation")
check("unknown -> change (safe default)", f.normalize_reason("zzz") == "change")
print("checkbox + NPI placement per reason (page 2)")
# Expected checkbox X positions (lower-left) per reason, from the form layout.
expect = {
"change": {"X": (48, 582), "NPI": (132, 570)},
"reactivation": {"X": (325, 506), "NPI": (408, 529)},
"deactivation": {"X": (325, 608), "NPI": (408, 599)},
}
for reason, exp in expect.items():
pdf, anchors, missing = f.fill_cms10114(SAMPLE, reason=reason)
toks = _tokens(pdf, f.PAGE_BASIC)
x = toks.get("X")
npi = toks.get("1234567893")
check(f"{reason}: X near {exp['X']}",
x and abs(x[0] - exp["X"][0]) < 6 and abs(x[1] - exp["X"][1]) < 6)
check(f"{reason}: NPI near {exp['NPI']}",
npi and abs(npi[0] - exp["NPI"][0]) < 6 and abs(npi[1] - exp["NPI"][1]) < 6)
print("identity + certification name (Section 2A / 4A)")
pdf, anchors, missing = f.fill_cms10114(SAMPLE, reason="change")
p2 = _tokens(pdf, f.PAGE_BASIC)
p4 = _tokens(pdf, f.PAGE_CERT)
check("Section 2A first name placed", "Jane" in p2)
check("Section 2A last name placed", "Smith" in p2)
check("Section 4A cert first name placed", "Jane" in p4)
check("Section 4A cert last name placed", "Smith" in p4)
print("signature anchor")
check("exactly one signature anchor", len(anchors) == 1)
check("anchor on certification page", anchors and anchors[0]["page"] == f.PAGE_CERT)
check("anchor field is 'signer'", anchors and anchors[0]["field"] == "signer")
print("manual-completion notes")
_, _, m_change = f.fill_cms10114(SAMPLE, reason="change")
_, _, m_react = f.fill_cms10114(SAMPLE, reason="reactivation")
_, _, m_deact = f.fill_cms10114(SAMPLE, reason="deactivation")
check("change asks to mark changing fields", any("changing" in x.lower() for x in m_change))
check("reactivation asks for reason text", any("reactivation reason" in x.lower() for x in m_react))
check("deactivation asks for reason box", any("deactivation reason" in x.lower() for x in m_deact))
_, _, m_noni = f.fill_cms10114({"provider_name": "No NPI"}, reason="change")
check("missing NPI flagged", any("npi is required" in x.lower() for x in m_noni))
_, _, m_org = f.fill_cms10114({**SAMPLE, "enumeration_type": "NPI-2"}, reason="change")
check("org (NPI-2) flagged for 4B path", any("organization" in x.lower() for x in m_org))
print("routing destination = verified Baltimore Enumerator")
addr = " ".join(mr.NPI_ENUMERATOR.address_lines).lower()
check("destination is Baltimore (not Fargo)", "baltimore" in addr and "fargo" not in addr)
check("destination has the Mail Stop", "do-01-51" in addr)
print()
if _fails:
print(f"FAILED: {_fails} checks")
sys.exit(1)
print("all CMS-10114 checks passed")
if __name__ == "__main__":
main()