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.
This commit is contained in:
parent
f9c294e962
commit
e6a630ada1
10 changed files with 540 additions and 48 deletions
BIN
docs/CMS-10114 Form.pdf
Normal file
BIN
docs/CMS-10114 Form.pdf
Normal file
Binary file not shown.
|
|
@ -49,22 +49,42 @@ Internal capability map. Confirms how each healthcare service is fulfilled with
|
||||||
- Paper-to-MAC, original signatures, routed by state. "Sign and date section 8
|
- Paper-to-MAC, original signatures, routed by state. "Sign and date section 8
|
||||||
using ink."
|
using ink."
|
||||||
|
|
||||||
### NPPES data update / NPI reactivation (CMS-10114 paper path) [PARTIAL]
|
### NPPES data update / NPI reactivation (CMS-10114 paper path) [CONFIRMED]
|
||||||
- 855I confirms NPI is obtained/changed via NPPES and "you may apply **online** at
|
- **VERIFIED firsthand** against the live CMS-10114 PDF (Rev. 02/25, OMB
|
||||||
NPPES.cms.hhs.gov" — implies the online path; the paper CMS-10114 to the NPI
|
0938-0931, exp. 03/2028) downloaded from
|
||||||
Enumerator (PO Box 6059, Fargo ND 58108-6059) is the documented paper
|
`cms.gov/Medicare/CMS-Forms/CMS-Forms/downloads/CMS10114.pdf`, and the CMS
|
||||||
alternative per the existing `healthcare-no-login-value-add.md` (NPI Enumerator
|
"How to Apply" page which titles it the **"NPI Application/Update Form"**.
|
||||||
800-465-3203).
|
- The form's Section 1A "Reason for Submittal" has four checkboxes:
|
||||||
- **UNCERTAIN (verify before relying on it as the sole standard path):** whether
|
**#1 Initial Application, #2 Change of Information, #3 Deactivation,
|
||||||
CMS still accepts paper CMS-10114 for *updates/reactivation* (vs initial
|
#4 Reactivation** — all completed on the SAME paper form, each requiring a
|
||||||
enumeration only) today. Could not reach the live CMS-10114 instruction from
|
signed + dated certification (Section 4A individual / 4B organization).
|
||||||
this host (search engines blocked, direct URLs 404).
|
Verbatim: *"If changing information, check box #2... then sign and date the
|
||||||
- **Decision:** for NPPES-only services, offer Expedited (surrogate → file in
|
certification... All changes must be reported to the NPI Enumerator within 30
|
||||||
NPPES online) as the primary fast path, and the CMS-10114 paper path as the
|
days."* and *"If you are reactivating the NPI, check box #4... Sign and date
|
||||||
Standard fallback. Confirm the CMS-10114-for-changes acceptance + obtain the
|
the certification."*
|
||||||
current form PDF before building `cms10114_pdf_filler.py`. Until confirmed, the
|
- So paper CMS-10114 is a valid no-login path for **changes AND reactivation**,
|
||||||
honest Standard fallback for a declined-surrogate NPPES update may be "we
|
not just initial enumeration. UNCERTAIN flag resolved.
|
||||||
prepare it and guide the one-step online submission" — TBD on verification.
|
- **Mailing address (current, printed on the form page 5):**
|
||||||
|
```
|
||||||
|
CMS NPI Enumerator Services
|
||||||
|
Mail Stop DO-01-51
|
||||||
|
7500 Security Blvd.
|
||||||
|
Baltimore, MD 21244
|
||||||
|
```
|
||||||
|
The previously-documented **Fargo, ND PO Box 6059 address is RETIRED** — code
|
||||||
|
(`mac_routing.NPI_ENUMERATOR`) and docs updated to the Baltimore address.
|
||||||
|
Enumerator phone for questions: 1-800-465-3203.
|
||||||
|
- **No electronic no-login equivalent exists for CMS.** Unlike FMCSA (whose
|
||||||
|
`ask.fmcsa.dot.gov/app/ticket` accepts an MCS-150 update + attachments with no
|
||||||
|
account), CMS has no public web form to submit an NPPES change without a login.
|
||||||
|
Probed live: the NPI Registry API (`npiregistry.cms.hhs.gov/api`) is
|
||||||
|
**read-only lookup**; PECOS and NPPES I&A both require login. Therefore for
|
||||||
|
NPPES changes the two genuine tiers are: **Standard = paper CMS-10114 to
|
||||||
|
Baltimore (no login)**, **Expedited = surrogate files in NPPES online**.
|
||||||
|
- **Decision (updated):** offer Expedited (surrogate → NPPES online) as the fast
|
||||||
|
path and the CMS-10114 paper-to-Baltimore path as the Standard default. Build
|
||||||
|
`cms10114_pdf_filler.py` to produce the signable form (Reason checkbox driven
|
||||||
|
by service: change vs reactivation vs deactivation).
|
||||||
|
|
||||||
## CMS I&A Surrogate (Expedited path)
|
## CMS I&A Surrogate (Expedited path)
|
||||||
- Provider adds Performance West as a **Surrogate** in CMS I&A; we then file in
|
- Provider adds Performance West as a **Surrogate** in CMS I&A; we then file in
|
||||||
|
|
@ -78,14 +98,14 @@ Internal capability map. Confirms how each healthcare service is fulfilled with
|
||||||
| Medicare revalidation | 855I/B → MAC, client signs | file in PECOS | 855B yes |
|
| Medicare revalidation | 855I/B → MAC, client signs | file in PECOS | 855B yes |
|
||||||
| Medicare enrollment | 855I/B/O → MAC, client signs | file in PECOS | 855B yes |
|
| Medicare enrollment | 855I/B/O → MAC, client signs | file in PECOS | 855B yes |
|
||||||
| NPI reactivation | 855I → MAC (reactivation reason) | PECOS/NPPES | no |
|
| NPI reactivation | 855I → MAC (reactivation reason) | PECOS/NPPES | no |
|
||||||
| NPPES data update | CMS-10114 → Fargo *(verify)* | NPPES online | no |
|
| NPPES data update | CMS-10114 → NPI Enumerator (Baltimore), client signs | NPPES online | no |
|
||||||
| OIG/SAM screening | public DBs, **zero client action** | n/a (no portal) | no |
|
| OIG/SAM screening | public DBs, **zero client action** | n/a (no portal) | no |
|
||||||
| Provider compliance bundle | spawns reval + screening + NPPES | mixed | per-piece |
|
| Provider compliance bundle | spawns reval + screening + NPPES | mixed | per-piece |
|
||||||
|
|
||||||
## Daily batch (Standard-path mailing)
|
## Daily batch (Standard-path mailing)
|
||||||
Each **postal working day morning** (skip weekends + USPS/federal holidays — use
|
Each **postal working day morning** (skip weekends + USPS/federal holidays — use
|
||||||
`scripts/workers/business_days.py` calendar): gather all signed+pending paper
|
`scripts/workers/business_days.py` calendar): gather all signed+pending paper
|
||||||
filings, **group by destination agency** (each MAC; NPI Enumerator Fargo; each
|
filings, **group by destination agency** (each MAC; NPI Enumerator Baltimore; each
|
||||||
state agency), merge each group into ONE print job + a cover sheet (PW sender,
|
state agency), merge each group into ONE print job + a cover sheet (PW sender,
|
||||||
destination, date, enclosed count, per-item order#/provider/NPI/form), one
|
destination, date, enclosed count, per-item order#/provider/NPI/form), one
|
||||||
**Priority Mail** envelope per agency. Mark each order mailed with batch date +
|
**Priority Mail** envelope per agency. Mark each order mailed with batch date +
|
||||||
|
|
@ -100,9 +120,17 @@ Certification. Need a **state → MAC → mailing-address** table to address env
|
||||||
|
|
||||||
## Implementation status (built + validated)
|
## Implementation status (built + validated)
|
||||||
- **mac_routing.py** — state→MAC (56 jurisdictions, 12 destinations) +
|
- **mac_routing.py** — state→MAC (56 jurisdictions, 12 destinations) +
|
||||||
NPI_ENUMERATOR (Fargo). Addresses marked VERIFY before first live mail.
|
NPI_ENUMERATOR (NPI Enumerator Services, Baltimore MD — VERIFIED from CMS-10114
|
||||||
|
Rev. 02/25). MAC addresses still marked VERIFY before first live mail.
|
||||||
- **npi_provider.py** — two-tier `access` strings; NPPES update/reactivation no
|
- **npi_provider.py** — two-tier `access` strings; NPPES update/reactivation no
|
||||||
longer "online-only"; surrogate answer surfaced in the admin todo.
|
longer "online-only"; surrogate answer surfaced in the admin todo. nppes-update
|
||||||
|
now generates the CMS-10114 for e-signature (Standard path) via
|
||||||
|
`_generate_10114_for_signing`.
|
||||||
|
- **cms10114_pdf_filler.py** — fills the official CMS-10114 (flat PDF, text
|
||||||
|
overlay): reason checkbox (change/reactivation/deactivation) + NPI + Section 2A
|
||||||
|
identity + Section 4A certification name, with a signature anchor on the cert
|
||||||
|
line. The signed form joins the daily batch to the NPI Enumerator (Baltimore).
|
||||||
|
`test_cms10114.py` 27/27 pass.
|
||||||
- **checkout.ts + service pages + intake** — client-facing copy stripped of
|
- **checkout.ts + service pages + intake** — client-facing copy stripped of
|
||||||
mechanics; surrogate is the only optional, positively-framed ask (faster,
|
mechanics; surrogate is the only optional, positively-framed ask (faster,
|
||||||
never required, never share password, never mentions paper). Astro build green.
|
never required, never share password, never mentions paper). Astro build green.
|
||||||
|
|
@ -118,9 +146,12 @@ Certification. Need a **state → MAC → mailing-address** table to address env
|
||||||
## TODO before first live mail (manual / verify)
|
## TODO before first live mail (manual / verify)
|
||||||
1. Fill the real MAC provider-enrollment PO Box addresses in `mac_routing.py`
|
1. Fill the real MAC provider-enrollment PO Box addresses in `mac_routing.py`
|
||||||
(marked VERIFY) from each MAC's current enrollment page.
|
(marked VERIFY) from each MAC's current enrollment page.
|
||||||
2. Confirm CMS-10114 paper-for-changes acceptance + obtain the form PDF, then
|
2. ~~Confirm CMS-10114 paper-for-changes acceptance + obtain the form PDF~~
|
||||||
build `cms10114_pdf_filler.py` for the NPPES Standard path (until then
|
~~build `cms10114_pdf_filler.py`~~ **DONE** — verified against CMS-10114
|
||||||
nppes-update falls to surrogate/manual).
|
Rev. 02/25 (address = NPI Enumerator Services, Baltimore MD); filler built +
|
||||||
|
wired into `npi_provider._generate_10114_for_signing`; `test_cms10114.py`
|
||||||
|
27/27 pass. Remaining nicety: org (Entity Type 2 / Section 4B) overlay path
|
||||||
|
(currently flagged for manual completion when NPI-2 detected).
|
||||||
3. Run migration 089 on the DB; confirm the worker picks up a signed test filing
|
3. Run migration 089 on the DB; confirm the worker picks up a signed test filing
|
||||||
and produces the per-agency cover + merged PDF in MinIO.
|
and produces the per-agency cover + merged PDF in MinIO.
|
||||||
4. Phase 2: wire a print-mail API (Lob/Click2Mail) to auto-mail the merged PDF
|
4. Phase 2: wire a print-mail API (Lob/Click2Mail) to auto-mail the merged PDF
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ single paper form (or e-sign), and we file it by mail or as the preparer.
|
||||||
| Service | Paper/no-login path | Provider's only job | Verified source |
|
| Service | Paper/no-login path | Provider's only job | Verified source |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| **Medicare PECOS Revalidation** | CMS-855I/855B/855A **paper** application mailed to the provider's **MAC** (revalidation is a checkbox reason in Section 1A). | **Sign** the form (wet/original signature). We prepare 100% of it. | CMS-855I: "submit your application with **original signatures**… to your designated MAC… by mail." |
|
| **Medicare PECOS Revalidation** | CMS-855I/855B/855A **paper** application mailed to the provider's **MAC** (revalidation is a checkbox reason in Section 1A). | **Sign** the form (wet/original signature). We prepare 100% of it. | CMS-855I: "submit your application with **original signatures**… to your designated MAC… by mail." |
|
||||||
| **NPPES / NPI data update** (address, taxonomy, contact) | **CMS-10114** paper NPI Application/Update mailed to the **NPI Enumerator, PO Box 6059, Fargo, ND 58108-6059**; Enumerator staff key it into NPPES. | Sign the paper form. **No NPPES/I&A login at all.** | NPI Enumerator paper process (PO Box 6059, Fargo ND; 800-465-3203). |
|
| **NPPES / NPI data update** (address, taxonomy, contact) | **CMS-10114** paper NPI Application/Update mailed to the **NPI Enumerator (CMS NPI Enumerator Services, 7500 Security Blvd, Baltimore, MD 21244)**; Enumerator staff key it into NPPES. | Sign the paper form. **No NPPES/I&A login at all.** | NPI Enumerator paper process (Baltimore, MD; 800-465-3203; CMS-10114 Rev. 02/25). |
|
||||||
| **NPI Reactivation / enrollment reactivation** | Same paper-855 path to the MAC (reactivation is a Section 1A reason). | Sign. | CMS-855 instructions. |
|
| **NPI Reactivation / enrollment reactivation** | Same paper-855 path to the MAC (reactivation is a Section 1A reason). | Sign. | CMS-855 instructions. |
|
||||||
| **Medicare Enrollment (new)** | Paper CMS-855I/855B to the MAC with original signature. | Sign + provide info. | CMS-855I. |
|
| **Medicare Enrollment (new)** | Paper CMS-855I/855B to the MAC with original signature. | Sign + provide info. | CMS-855I. |
|
||||||
| **OIG / SAM Exclusion Screening** | We screen against the **public** OIG LEIE + SAM.gov databases by name/NPI. | **Nothing — zero provider involvement.** No login, no signature. | OIG LEIE + SAM.gov are public federal databases. |
|
| **OIG / SAM Exclusion Screening** | We screen against the **public** OIG LEIE + SAM.gov databases by name/NPI. | **Nothing — zero provider involvement.** No login, no signature. | OIG LEIE + SAM.gov are public federal databases. |
|
||||||
|
|
@ -39,7 +39,7 @@ time of navigating CMS portals. Our differentiator:
|
||||||
> website. (For exclusion screening you don't even sign — we just do it.)
|
> website. (For exclusion screening you don't even sign — we just do it.)
|
||||||
|
|
||||||
## Mailing destinations (for credibility in copy)
|
## Mailing destinations (for credibility in copy)
|
||||||
- NPI/NPPES paper: **NPI Enumerator, PO Box 6059, Fargo, ND 58108-6059**
|
- NPI/NPPES paper: **CMS NPI Enumerator Services, Mail Stop DO-01-51, 7500 Security Blvd, Baltimore, MD 21244**
|
||||||
- 855 enrollment/revalidation: the provider's **MAC** (varies by state/jurisdiction;
|
- 855 enrollment/revalidation: the provider's **MAC** (varies by state/jurisdiction;
|
||||||
we determine and route it).
|
we determine and route it).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ the "paper" alternative is NEVER surfaced to the client):
|
||||||
|
|
||||||
- **Standard service (default):** we handle the filing end-to-end. Client signs
|
- **Standard service (default):** we handle the filing end-to-end. Client signs
|
||||||
ONE certification and we submit + track to confirmation. (Internally this is the
|
ONE certification and we submit + track to confirmation. (Internally this is the
|
||||||
paper path — 855 to the MAC, CMS-10114 to the NPI Enumerator in Fargo — but we
|
paper path — 855 to the MAC, CMS-10114 to the NPI Enumerator (Baltimore MD) — but we
|
||||||
never say "paper" to the client; it's just "we file it for you.")
|
never say "paper" to the client; it's just "we file it for you.")
|
||||||
- **Expedited (faster, framed positively at intake):** we ask if they can
|
- **Expedited (faster, framed positively at intake):** we ask if they can
|
||||||
**electronically grant us CMS I&A Surrogate access**. We position this as
|
**electronically grant us CMS I&A Surrogate access**. We position this as
|
||||||
|
|
@ -28,11 +28,11 @@ surrogate as *required* when a paper path exists; paper is always the default.
|
||||||
- `api/src/routes/checkout.ts` (~L2155-2207) — already has the two-tier copy, BUT
|
- `api/src/routes/checkout.ts` (~L2155-2207) — already has the two-tier copy, BUT
|
||||||
treats NPPES update/reactivation as **"online-only, surrogate required"** with
|
treats NPPES update/reactivation as **"online-only, surrogate required"** with
|
||||||
NO standard fallback (`NPPES_ONLY_SLUGS`, `hasNppesOnly`). The directive says:
|
NO standard fallback (`NPPES_ONLY_SLUGS`, `hasNppesOnly`). The directive says:
|
||||||
give those a Standard paper path too (CMS-10114 to Fargo) and make surrogate the
|
give those a Standard paper path too (CMS-10114 to NPI Enumerator (Baltimore MD)) and make surrogate the
|
||||||
*expedited* option, not the only option.
|
*expedited* option, not the only option.
|
||||||
- `scripts/workers/services/npi_provider.py` — `access` strings call NPPES
|
- `scripts/workers/services/npi_provider.py` — `access` strings call NPPES
|
||||||
update/reactivation "NPPES via CMS I&A surrogate access (online-only)". Must
|
update/reactivation "NPPES via CMS I&A surrogate access (online-only)". Must
|
||||||
become "Standard: CMS-10114 paper to NPI Enumerator (Fargo), client signs;
|
become "Standard: CMS-10114 paper to NPI Enumerator (Baltimore MD), client signs;
|
||||||
Expedited: NPPES via I&A surrogate." Also `_STANDARD_FILING_SLUGS` currently
|
Expedited: NPPES via I&A surrogate." Also `_STANDARD_FILING_SLUGS` currently
|
||||||
excludes `nppes-update` (it has no 855) — needs a CMS-10114 paper path instead.
|
excludes `nppes-update` (it has no 855) — needs a CMS-10114 paper path instead.
|
||||||
- `scripts/document_gen/templates/cms855_pdf_filler.py` — working 855 paper path
|
- `scripts/document_gen/templates/cms855_pdf_filler.py` — working 855 paper path
|
||||||
|
|
@ -58,7 +58,7 @@ pricing changes.
|
||||||
## Approach (concrete ordered steps)
|
## Approach (concrete ordered steps)
|
||||||
1. **Confirm the CMS-10114 paper path** for NPPES data updates AND NPI
|
1. **Confirm the CMS-10114 paper path** for NPPES data updates AND NPI
|
||||||
reactivation (not just initial enumeration): paper CMS-10114 mailed to NPI
|
reactivation (not just initial enumeration): paper CMS-10114 mailed to NPI
|
||||||
Enumerator (PO Box 6059, Fargo ND), client signature only, no I&A login.
|
Enumerator (Baltimore MD), client signature only, no I&A login.
|
||||||
Cite the official source. This is what makes NPPES services "Standard, no
|
Cite the official source. This is what makes NPPES services "Standard, no
|
||||||
login" instead of surrogate-required. (We currently have no CMS-10114 PDF.)
|
login" instead of surrogate-required. (We currently have no CMS-10114 PDF.)
|
||||||
2. **Lock the two-tier matrix for the 6 federal services.** For each slug record:
|
2. **Lock the two-tier matrix for the 6 federal services.** For each slug record:
|
||||||
|
|
@ -81,13 +81,13 @@ pricing changes.
|
||||||
paper + e-sign, and the admin todo reflects Standard-default / Expedited-if-
|
paper + e-sign, and the admin todo reflects Standard-default / Expedited-if-
|
||||||
surrogate-granted. Mirror the existing 855 generate→upload→esign flow.
|
surrogate-granted. Mirror the existing 855 generate→upload→esign flow.
|
||||||
5. **MAC + Fargo routing rule.** Document which Standard filings go where
|
5. **MAC + Fargo routing rule.** Document which Standard filings go where
|
||||||
(855 → provider's MAC by state/jurisdiction; CMS-10114 → Fargo). Confirm
|
(855 → provider's MAC by state/jurisdiction; CMS-10114 → NPI Enumerator (Baltimore MD)). Confirm
|
||||||
`practice_state` intake field drives MAC envelope addressing.
|
`practice_state` intake field drives MAC envelope addressing.
|
||||||
7. **Daily batched-mail fulfillment (Standard path).** Design the operational
|
7. **Daily batched-mail fulfillment (Standard path).** Design the operational
|
||||||
flow for paper filings that are signed + pending submission:
|
flow for paper filings that are signed + pending submission:
|
||||||
- On each **postal working day morning** (skip weekends + federal/USPS
|
- On each **postal working day morning** (skip weekends + federal/USPS
|
||||||
holidays), gather ALL signed-and-pending paper filings, **group by
|
holidays), gather ALL signed-and-pending paper filings, **group by
|
||||||
destination agency/address** (each MAC, the NPI Enumerator in Fargo, each
|
destination agency/address** (each MAC, the NPI Enumerator (Baltimore MD), each
|
||||||
state Medicaid/CLIA agency).
|
state Medicaid/CLIA agency).
|
||||||
- For each destination, **merge all that day's documents into one print job**
|
- For each destination, **merge all that day's documents into one print job**
|
||||||
plus a **cover sheet** (Performance West sender, destination agency, date,
|
plus a **cover sheet** (Performance West sender, destination agency, date,
|
||||||
|
|
@ -129,7 +129,7 @@ pricing changes.
|
||||||
and Standard is the default on every surface.
|
and Standard is the default on every surface.
|
||||||
- **Standard path mechanism check:** `cms855_pdf_filler.fill_cms855("855i",...)`
|
- **Standard path mechanism check:** `cms855_pdf_filler.fill_cms855("855i",...)`
|
||||||
still yields filled PDF + cert anchor (read-only smoke render). If CMS-10114
|
still yields filled PDF + cert anchor (read-only smoke render). If CMS-10114
|
||||||
filler is built, same smoke render proves a signable Fargo-bound PDF.
|
filler is built, same smoke render proves a signable Baltimore-bound PDF.
|
||||||
- **Expedited path check:** confirm the surrogate-grant CTA + `ia_surrogacy`
|
- **Expedited path check:** confirm the surrogate-grant CTA + `ia_surrogacy`
|
||||||
success action + admin todo together let a human file in PECOS/NPPES same-day;
|
success action + admin todo together let a human file in PECOS/NPPES same-day;
|
||||||
surrogate scope wording matches what CMS I&A actually grants (PECOS vs NPPES).
|
surrogate scope wording matches what CMS I&A actually grants (PECOS vs NPPES).
|
||||||
|
|
@ -138,11 +138,11 @@ pricing changes.
|
||||||
no "unknown" and no service that's portal-only-but-marketed-as-no-login.
|
no "unknown" and no service that's portal-only-but-marketed-as-no-login.
|
||||||
|
|
||||||
## Open questions / decisions
|
## Open questions / decisions
|
||||||
1. **NPPES update/reactivation Standard path:** is CMS-10114 paper-to-Fargo
|
1. **NPPES update/reactivation Standard path:** ✅ RESOLVED — CMS-10114 paper IS
|
||||||
accepted for *changes/reactivation* today (RESOLVE in step 1)? If yes, both
|
accepted for *changes AND reactivation* (verified against CMS-10114 Rev. 02/25,
|
||||||
become Standard-default + surrogate-expedited. If CMS has gone online-only,
|
Section 1A boxes #2 Change / #4 Reactivation). Mailing address is the NPI
|
||||||
they stay surrogate-required and we say so honestly. (Directive assumes paper
|
Enumerator in **Baltimore MD** (the old Fargo PO Box 6059 is retired). Both
|
||||||
works as Standard; step 1 verifies.)
|
nppes-update and npi-reactivation are Standard-default + surrogate-expedited.
|
||||||
2. **Build `cms10114_pdf_filler.py` this pass?** Needed for a real NPPES Standard
|
2. **Build `cms10114_pdf_filler.py` this pass?** Needed for a real NPPES Standard
|
||||||
path. Recommend: yes if step 1 confirms paper, mirroring the 855 filler;
|
path. Recommend: yes if step 1 confirms paper, mirroring the 855 filler;
|
||||||
otherwise defer.
|
otherwise defer.
|
||||||
|
|
|
||||||
253
scripts/document_gen/templates/cms10114_pdf_filler.py
Normal file
253
scripts/document_gen/templates/cms10114_pdf_filler.py
Normal file
|
|
@ -0,0 +1,253 @@
|
||||||
|
"""Fill the official CMS-10114 NPI Application/Update form (flat PDF, overlay).
|
||||||
|
|
||||||
|
The CMS-10114 is the "NPI Application/Update Form" (Rev. 02/25, OMB 0938-0931).
|
||||||
|
Unlike the CMS-855 forms it is a **flat PDF with no AcroForm fields**, so we
|
||||||
|
fill it by drawing a text overlay at fixed coordinates (reportlab) and merging
|
||||||
|
that over the original page (pypdf), mirroring the stamper approach used by the
|
||||||
|
e-sign pipeline.
|
||||||
|
|
||||||
|
This is the Standard (no-login) path for NPPES data updates, NPI reactivation,
|
||||||
|
and deactivation: the provider signs the certification (Section 4A individual /
|
||||||
|
4B organization) and the signed form is mailed to:
|
||||||
|
|
||||||
|
CMS NPI Enumerator Services
|
||||||
|
Mail Stop DO-01-51
|
||||||
|
7500 Security Blvd.
|
||||||
|
Baltimore, MD 21244
|
||||||
|
|
||||||
|
(verified against the form, page 5; the older Fargo PO Box 6059 is retired).
|
||||||
|
Enumerator staff key the data into NPPES; no NPPES/I&A login is required.
|
||||||
|
|
||||||
|
Section 1A "Reason for Submittal" has four mutually-exclusive checkboxes:
|
||||||
|
1 Initial Application, 2 Change of Information, 3 Deactivation, 4 Reactivation.
|
||||||
|
We drive the checkbox from the service slug / intake reason so the same filler
|
||||||
|
serves nppes-update (change), npi-reactivation, and deactivation.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from scripts.document_gen.templates.cms10114_pdf_filler import fill_cms10114
|
||||||
|
pdf_bytes, anchors, missing = fill_cms10114(intake, reason="change",
|
||||||
|
order_number="CO-1234")
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import io
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
LOG = logging.getLogger("cms10114_pdf_filler")
|
||||||
|
|
||||||
|
# docs/ lives two levels up from scripts/document_gen/templates/
|
||||||
|
DOCS_DIR = Path(__file__).resolve().parents[3] / "docs"
|
||||||
|
FORM_PATH = DOCS_DIR / "CMS-10114 Form.pdf"
|
||||||
|
|
||||||
|
# Page index (0-based) of the actual fill-in application form pages within the
|
||||||
|
# downloaded PDF (the first pages are instructions).
|
||||||
|
PAGE_BASIC = 2 # Section 1 (reason + entity) and Section 2A identity
|
||||||
|
PAGE_CERT = 4 # Section 4 certification / signature
|
||||||
|
|
||||||
|
PAGE_W = 612.0
|
||||||
|
PAGE_H = 792.0
|
||||||
|
|
||||||
|
# Reason-for-submittal checkbox glyph positions (Section 1A). The checkbox sits
|
||||||
|
# just left of each label; coordinates are the lower-left of where we draw an
|
||||||
|
# "X". Verified from pdfplumber word boxes on the form page.
|
||||||
|
REASON_CHECKBOX = {
|
||||||
|
"initial": {"x": 48, "y": 610},
|
||||||
|
"change": {"x": 48, "y": 584},
|
||||||
|
"deactivation": {"x": 325, "y": 610},
|
||||||
|
"reactivation": {"x": 325, "y": 508},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Where to write the NPI for each reason (the "NPI: (Required)" line beside the
|
||||||
|
# chosen checkbox). Change uses the left column NPI line; deactivation/
|
||||||
|
# reactivation use the right column NPI lines.
|
||||||
|
REASON_NPI_POS = {
|
||||||
|
"change": {"x": 132, "y": 570},
|
||||||
|
"deactivation": {"x": 408, "y": 599},
|
||||||
|
"reactivation": {"x": 408, "y": 529},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Section 2A individual identity fields (the blank row under the Prefix/First/
|
||||||
|
# Middle/Last header at pdf_y ~320; we write on the entry row just below it).
|
||||||
|
IDENTITY_POS = {
|
||||||
|
"first": {"x": 168, "y": 305},
|
||||||
|
"middle": {"x": 281, "y": 305},
|
||||||
|
"last": {"x": 370, "y": 305},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Section 4A (individual) printed-name fields under "First*/Middle/Last*" header.
|
||||||
|
CERT_NAME_POS = {
|
||||||
|
"first": {"x": 142, "y": 372},
|
||||||
|
"middle": {"x": 280, "y": 372},
|
||||||
|
"last": {"x": 429, "y": 372},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Signature line for Section 4A (individual). The label "Signature (First,
|
||||||
|
# Middle, Last...)" sits at pdf_y ~458; the blank signing line is the row above
|
||||||
|
# it. The e-sign stamper places the provider's signature here.
|
||||||
|
SIGNATURE_FIELDS = [
|
||||||
|
{"field": "signer", "page": PAGE_CERT,
|
||||||
|
"rect": [60.0, 470.0, 470.0, 490.0], "page_w": PAGE_W, "page_h": PAGE_H},
|
||||||
|
]
|
||||||
|
|
||||||
|
VALID_REASONS = ("initial", "change", "deactivation", "reactivation")
|
||||||
|
|
||||||
|
# Map service slug -> default reason, so callers can pass a slug instead.
|
||||||
|
SLUG_TO_REASON = {
|
||||||
|
"nppes-update": "change",
|
||||||
|
"npi-data-update": "change",
|
||||||
|
"npi-reactivation": "reactivation",
|
||||||
|
"npi-deactivation": "deactivation",
|
||||||
|
"npi-enumeration": "initial",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _split_name(full: str) -> tuple[str, str, str]:
|
||||||
|
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 normalize_reason(reason_or_slug: str) -> str:
|
||||||
|
r = (reason_or_slug or "").strip().lower()
|
||||||
|
if r in VALID_REASONS:
|
||||||
|
return r
|
||||||
|
return SLUG_TO_REASON.get(r, "change")
|
||||||
|
|
||||||
|
|
||||||
|
def _build_overlay(reason: str, values: dict) -> bytes:
|
||||||
|
"""Render a multi-page overlay matching the form's page count."""
|
||||||
|
from reportlab.lib.pagesizes import letter
|
||||||
|
from reportlab.pdfgen import canvas
|
||||||
|
|
||||||
|
buf = io.BytesIO()
|
||||||
|
c = canvas.Canvas(buf, pagesize=letter)
|
||||||
|
|
||||||
|
def draw(x, y, text, size=9):
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
c.setFont("Helvetica", size)
|
||||||
|
c.drawString(x, y, str(text))
|
||||||
|
|
||||||
|
# We only draw on PAGE_BASIC and PAGE_CERT; emit blank pages for the rest so
|
||||||
|
# page indices line up when merging.
|
||||||
|
n_pages = max(PAGE_BASIC, PAGE_CERT) + 1
|
||||||
|
for pi in range(n_pages):
|
||||||
|
if pi == PAGE_BASIC:
|
||||||
|
cb = REASON_CHECKBOX.get(reason)
|
||||||
|
if cb:
|
||||||
|
draw(cb["x"], cb["y"], "X", size=11)
|
||||||
|
npi_pos = REASON_NPI_POS.get(reason)
|
||||||
|
if npi_pos and values.get("npi"):
|
||||||
|
draw(npi_pos["x"], npi_pos["y"], values["npi"])
|
||||||
|
draw(IDENTITY_POS["first"]["x"], IDENTITY_POS["first"]["y"], values.get("first"))
|
||||||
|
draw(IDENTITY_POS["middle"]["x"], IDENTITY_POS["middle"]["y"], values.get("middle"))
|
||||||
|
draw(IDENTITY_POS["last"]["x"], IDENTITY_POS["last"]["y"], values.get("last"))
|
||||||
|
elif pi == PAGE_CERT:
|
||||||
|
draw(CERT_NAME_POS["first"]["x"], CERT_NAME_POS["first"]["y"], values.get("first"))
|
||||||
|
draw(CERT_NAME_POS["middle"]["x"], CERT_NAME_POS["middle"]["y"], values.get("middle"))
|
||||||
|
draw(CERT_NAME_POS["last"]["x"], CERT_NAME_POS["last"]["y"], values.get("last"))
|
||||||
|
c.showPage()
|
||||||
|
c.save()
|
||||||
|
return buf.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def fill_cms10114(intake: dict, reason: str = "change",
|
||||||
|
order_number: str = "") -> tuple[bytes, list[dict], list[str]]:
|
||||||
|
"""Fill the official CMS-10114 form via text overlay.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
intake: dict with provider_name/first_name/last_name, npi, practice_state.
|
||||||
|
reason: one of initial/change/deactivation/reactivation, OR a service
|
||||||
|
slug (nppes-update, npi-reactivation, ...) which is normalized.
|
||||||
|
order_number: for logging/traceability only.
|
||||||
|
|
||||||
|
Returns (pdf_bytes, signature_anchors, missing_notes).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from pypdf import PdfReader, PdfWriter
|
||||||
|
except ImportError as exc: # pragma: no cover
|
||||||
|
raise RuntimeError("pypdf is required to fill CMS-10114") from exc
|
||||||
|
|
||||||
|
if not FORM_PATH.exists():
|
||||||
|
raise FileNotFoundError(f"Official CMS-10114 not found: {FORM_PATH}")
|
||||||
|
|
||||||
|
reason = normalize_reason(reason)
|
||||||
|
|
||||||
|
provider = intake.get("provider_name", "")
|
||||||
|
f, m, l = _split_name(provider)
|
||||||
|
values = {
|
||||||
|
"first": intake.get("first_name", f),
|
||||||
|
"middle": intake.get("middle_initial", m),
|
||||||
|
"last": intake.get("last_name", l),
|
||||||
|
"npi": (intake.get("npi") or "").strip(),
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay_bytes = _build_overlay(reason, values)
|
||||||
|
|
||||||
|
base = PdfReader(str(FORM_PATH))
|
||||||
|
overlay = PdfReader(io.BytesIO(overlay_bytes))
|
||||||
|
writer = PdfWriter()
|
||||||
|
|
||||||
|
for i, page in enumerate(base.pages):
|
||||||
|
if i < len(overlay.pages):
|
||||||
|
page.merge_page(overlay.pages[i])
|
||||||
|
writer.add_page(page)
|
||||||
|
|
||||||
|
buf = io.BytesIO()
|
||||||
|
writer.write(buf)
|
||||||
|
pdf_bytes = buf.getvalue()
|
||||||
|
|
||||||
|
anchors = []
|
||||||
|
for sig in SIGNATURE_FIELDS:
|
||||||
|
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"],
|
||||||
|
})
|
||||||
|
|
||||||
|
missing = []
|
||||||
|
if reason in ("change", "deactivation", "reactivation") and not values["npi"]:
|
||||||
|
missing.append(f"NPI is required for a CMS-10114 {reason} but was not provided.")
|
||||||
|
if not (values["first"] and values["last"]):
|
||||||
|
missing.append("Provider first/last name not collected — required on the certification.")
|
||||||
|
if reason == "deactivation":
|
||||||
|
missing.append("Deactivation reason (Death / Business Dissolved / Other) must be selected by reviewer.")
|
||||||
|
if reason == "reactivation":
|
||||||
|
missing.append("Reactivation reason free-text must be completed by reviewer.")
|
||||||
|
if reason == "change":
|
||||||
|
missing.append("Mark which specific fields are changing in Section 2/3 before mailing.")
|
||||||
|
# The overlay targets the individual (Section 4A / Section 2A) path. Org
|
||||||
|
# filings (Entity Type 2 / Section 4B) need the org variant.
|
||||||
|
if (intake.get("enumeration_type") or "").upper() in ("NPI-2", "2", "ORGANIZATION"):
|
||||||
|
missing.append("Organization (NPI-2) detected — complete Section 2B/4B; this overlay targets the individual path.")
|
||||||
|
|
||||||
|
LOG.info("[cms10114] order=%s reason=%s npi=%s missing=%d",
|
||||||
|
order_number, reason, values["npi"], len(missing))
|
||||||
|
return pdf_bytes, anchors, missing
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": # quick local render for visual verification
|
||||||
|
sample = {
|
||||||
|
"provider_name": "Jane Q Smith",
|
||||||
|
"npi": "1234567893",
|
||||||
|
"practice_state": "CA",
|
||||||
|
"enumeration_type": "NPI-1",
|
||||||
|
}
|
||||||
|
for r in ("change", "reactivation", "deactivation"):
|
||||||
|
pdf, anchors, missing = fill_cms10114(sample, reason=r, order_number="CO-TEST")
|
||||||
|
out = f"/tmp/cms10114_{r}.pdf"
|
||||||
|
with open(out, "wb") as fh:
|
||||||
|
fh.write(pdf)
|
||||||
|
print(f"wrote {out} anchors={len(anchors)} missing={len(missing)}")
|
||||||
128
scripts/tests/test_cms10114.py
Normal file
128
scripts/tests/test_cms10114.py
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
"""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()
|
||||||
|
|
@ -63,7 +63,7 @@ def test_routing():
|
||||||
tx = dpb._destination_for("TX", "cms855b")
|
tx = dpb._destination_for("TX", "cms855b")
|
||||||
check("TX 855B -> Novitas JH", tx and tx[0] == "novitas_jh")
|
check("TX 855B -> Novitas JH", tx and tx[0] == "novitas_jh")
|
||||||
enum = dpb._destination_for("TX", "cms10114")
|
enum = dpb._destination_for("TX", "cms10114")
|
||||||
check("any-state CMS-10114 -> NPI Enumerator (Fargo)", enum and enum[0] == "npi_enumerator")
|
check("any-state CMS-10114 -> NPI Enumerator (Baltimore)", enum and enum[0] == "npi_enumerator")
|
||||||
check("CMS-10114 ignores state for routing", dpb._destination_for("CA", "cms10114")[0] == "npi_enumerator")
|
check("CMS-10114 ignores state for routing", dpb._destination_for("CA", "cms10114")[0] == "npi_enumerator")
|
||||||
check("unknown state -> None (human review)", dpb._destination_for("ZZ", "cms855i") is None)
|
check("unknown state -> None (human review)", dpb._destination_for("ZZ", "cms855i") is None)
|
||||||
check("blank state -> None", dpb._destination_for("", "cms855i") is None)
|
check("blank state -> None", dpb._destination_for("", "cms855i") is None)
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,8 @@ def _is_postal_working_day(d: date) -> bool:
|
||||||
def _destination_for(practice_state: str, document_type: str):
|
def _destination_for(practice_state: str, document_type: str):
|
||||||
"""Return the (key, name, address_lines) destination for a filing.
|
"""Return the (key, name, address_lines) destination for a filing.
|
||||||
|
|
||||||
NPPES/CMS-10114 updates go to the NPI Enumerator (Fargo); CMS-855s go to the
|
NPPES/CMS-10114 updates go to the NPI Enumerator (Baltimore, MD); CMS-855s go
|
||||||
provider's MAC by state.
|
to the provider's MAC by state.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
from scripts.workers import mac_routing as mr
|
from scripts.workers import mac_routing as mr
|
||||||
|
|
|
||||||
|
|
@ -107,9 +107,13 @@ STATE_TO_MAC: dict[str, MAC] = {
|
||||||
|
|
||||||
# NPI Enumerator paper address (NPPES / CMS-10114 paper path) — not a MAC, but a
|
# NPI Enumerator paper address (NPPES / CMS-10114 paper path) — not a MAC, but a
|
||||||
# destination the daily batch groups by, same as a MAC.
|
# destination the daily batch groups by, same as a MAC.
|
||||||
|
# VERIFIED 2025 against CMS-10114 (Rev. 02/25), OMB 0938-0931, page 5 "Or send the
|
||||||
|
# completed signed application to:". The earlier Fargo, ND PO Box 6059 address is
|
||||||
|
# RETIRED; the current address printed on the form is the Baltimore one below.
|
||||||
NPI_ENUMERATOR = MAC(
|
NPI_ENUMERATOR = MAC(
|
||||||
"npi_enumerator", "NPI Enumerator (NPPES / CMS-10114 paper)",
|
"npi_enumerator", "NPI Enumerator (NPPES / CMS-10114 paper)",
|
||||||
("NPI Enumerator", "P.O. Box 6059", "Fargo, ND 58108-6059"),
|
("CMS NPI Enumerator Services", "Mail Stop DO-01-51",
|
||||||
|
"7500 Security Blvd.", "Baltimore, MD 21244"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -126,16 +126,22 @@ _SLUG_META = {
|
||||||
# batched mailing). The bundle's revalidation piece is handled by the dedicated
|
# batched mailing). The bundle's revalidation piece is handled by the dedicated
|
||||||
# revalidation order it spawns, so it is not listed here.
|
# revalidation order it spawns, so it is not listed here.
|
||||||
#
|
#
|
||||||
# nppes-update is intentionally NOT here: its Standard path is the CMS-10114 (NPPES
|
# nppes-update is NOT a CMS-855 filing: its Standard (no-login) path is the
|
||||||
# update mailed to the NPI Enumerator in Fargo), handled by a separate filler when
|
# CMS-10114 NPI Application/Update form, signed by the provider and mailed to the
|
||||||
# built. Until then, nppes-update falls to the admin todo (Expedited if surrogate
|
# NPI Enumerator (CMS NPI Enumerator Services, Baltimore MD). It is handled by
|
||||||
# granted, otherwise manual CMS-10114 prep).
|
# the dedicated CMS-10114 filler below (_STANDARD_10114_SLUGS).
|
||||||
_STANDARD_FILING_SLUGS = {
|
_STANDARD_FILING_SLUGS = {
|
||||||
"npi-revalidation",
|
"npi-revalidation",
|
||||||
"npi-reactivation",
|
"npi-reactivation",
|
||||||
"medicare-enrollment",
|
"medicare-enrollment",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Slugs whose Standard path is the CMS-10114 (NPPES update/reactivation/deactivation
|
||||||
|
# mailed to the NPI Enumerator). The reason checkbox is derived from the slug.
|
||||||
|
_STANDARD_10114_SLUGS = {
|
||||||
|
"nppes-update",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class _BaseNPIHandler:
|
class _BaseNPIHandler:
|
||||||
"""Shared review-staged behaviour for all NPI services."""
|
"""Shared review-staged behaviour for all NPI services."""
|
||||||
|
|
@ -178,6 +184,14 @@ class _BaseNPIHandler:
|
||||||
except Exception as exc: # never block the admin todo on PDF issues
|
except Exception as exc: # never block the admin todo on PDF issues
|
||||||
LOG.error("[%s] CMS-855 generation failed: %s", order_number, exc)
|
LOG.error("[%s] CMS-855 generation failed: %s", order_number, exc)
|
||||||
filing_note = f"CMS-855 auto-generation FAILED ({exc}); prepare the form manually."
|
filing_note = f"CMS-855 auto-generation FAILED ({exc}); prepare the form manually."
|
||||||
|
elif self.SERVICE_SLUG in _STANDARD_10114_SLUGS:
|
||||||
|
try:
|
||||||
|
filing_note = self._generate_10114_for_signing(
|
||||||
|
order_number, intake, provider, customer_email
|
||||||
|
)
|
||||||
|
except Exception as exc: # never block the admin todo on PDF issues
|
||||||
|
LOG.error("[%s] CMS-10114 generation failed: %s", order_number, exc)
|
||||||
|
filing_note = f"CMS-10114 auto-generation FAILED ({exc}); prepare the form manually."
|
||||||
|
|
||||||
surrogate_label = {
|
surrogate_label = {
|
||||||
"yes": "YES — client can grant I&A Surrogate access -> file the EXPEDITED way (online via surrogate).",
|
"yes": "YES — client can grant I&A Surrogate access -> file the EXPEDITED way (online via surrogate).",
|
||||||
|
|
@ -267,16 +281,78 @@ class _BaseNPIHandler:
|
||||||
note_lines.extend(f" - {m}" for m in missing)
|
note_lines.extend(f" - {m}" for m in missing)
|
||||||
return "\n".join(note_lines)
|
return "\n".join(note_lines)
|
||||||
|
|
||||||
|
def _generate_10114_for_signing(self, order_number, intake, provider, customer_email) -> str:
|
||||||
|
"""Generate the official CMS-10114 (NPPES update path), upload it, and
|
||||||
|
request an e-signature.
|
||||||
|
|
||||||
|
Standard (no-login) path for NPPES data updates / NPI reactivation: the
|
||||||
|
provider signs the certification and the signed form is mailed to the NPI
|
||||||
|
Enumerator (CMS NPI Enumerator Services, Baltimore MD) via the daily batch.
|
||||||
|
Returns a human-readable note for the admin todo.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from scripts.document_gen.templates.cms10114_pdf_filler import (
|
||||||
|
fill_cms10114, normalize_reason,
|
||||||
|
)
|
||||||
|
except ImportError:
|
||||||
|
from document_gen.templates.cms10114_pdf_filler import ( # type: ignore
|
||||||
|
fill_cms10114, normalize_reason,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reason comes from the explicit intake reason if present, else the slug.
|
||||||
|
reason = normalize_reason(intake.get("filing_reason") or self.SERVICE_SLUG)
|
||||||
|
pdf_bytes, anchors, missing = fill_cms10114(intake, reason=reason, order_number=order_number)
|
||||||
|
|
||||||
|
document_key = f"compliance/{order_number}/cms10114_unsigned.pdf"
|
||||||
|
try:
|
||||||
|
import tempfile
|
||||||
|
try:
|
||||||
|
from scripts.document_gen.minio_client import MinioStorage
|
||||||
|
except ImportError:
|
||||||
|
from document_gen.minio_client import MinioStorage # type: ignore
|
||||||
|
storage = MinioStorage()
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=True) as tf:
|
||||||
|
tf.write(pdf_bytes)
|
||||||
|
tf.flush()
|
||||||
|
storage.upload(tf.name, document_key, content_type="application/pdf")
|
||||||
|
except Exception as exc:
|
||||||
|
LOG.error("[%s] CMS-10114 upload failed: %s", order_number, exc)
|
||||||
|
return f"CMS-10114 generated but upload FAILED ({exc})."
|
||||||
|
|
||||||
|
signed = self._create_855_esign_record(
|
||||||
|
order_number, intake, provider, customer_email,
|
||||||
|
form_type="10114", document_key=document_key, anchors=anchors,
|
||||||
|
document_type="cms10114",
|
||||||
|
document_title="NPI Application/Update Form (CMS-10114)",
|
||||||
|
)
|
||||||
|
|
||||||
|
note_lines = [
|
||||||
|
f"CMS-10114 generated (reason: {reason}; official form, auto-filled where possible).",
|
||||||
|
f"Unsigned PDF: {document_key}",
|
||||||
|
]
|
||||||
|
if signed and customer_email:
|
||||||
|
note_lines.append(f"E-sign link emailed to {customer_email}. After signing, the form joins the daily mail batch to the NPI Enumerator (standard) or file via NPPES surrogate access (expedited).")
|
||||||
|
else:
|
||||||
|
note_lines.append("No customer email or esign infra — send the form for signature manually.")
|
||||||
|
if missing:
|
||||||
|
note_lines.append("MANUAL COMPLETION NEEDED:")
|
||||||
|
note_lines.extend(f" - {m}" for m in missing)
|
||||||
|
return "\n".join(note_lines)
|
||||||
|
|
||||||
def _create_855_esign_record(self, order_number, intake, provider, customer_email,
|
def _create_855_esign_record(self, order_number, intake, provider, customer_email,
|
||||||
form_type, document_key, anchors) -> bool:
|
form_type, document_key, anchors,
|
||||||
|
document_type=None, document_title=None) -> bool:
|
||||||
"""Create the esign record + email the signing link via the shared helper,
|
"""Create the esign record + email the signing link via the shared helper,
|
||||||
then attach the official signature anchors. Returns True on success.
|
then attach the official signature anchors. Returns True on success.
|
||||||
|
|
||||||
|
Used for both CMS-855 and CMS-10114 (pass document_type/document_title to
|
||||||
|
override the defaults).
|
||||||
"""
|
"""
|
||||||
if not customer_email:
|
if not customer_email:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
document_type = f"cms{form_type}"
|
document_type = document_type or f"cms{form_type}"
|
||||||
document_title = f"Medicare Enrollment Form (CMS-{form_type.upper()})"
|
document_title = document_title or f"Medicare Enrollment Form (CMS-{form_type.upper()})"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue