healthcare: daily batched paper-filing fulfillment
Standard (no-login) CMS filings are mailed in one Priority Mail envelope per destination agency, batched each postal working-day morning to save postage. - migration 089: paper_filing_batches table + esign_records.paper_batch_id / filing_destination_key (idempotent: a filing is batched at most once). - batch_cover_sheet.py: per-agency cover sheet (sender/dest/date/manifest) + merged print-job PDF (cover + all enclosed signed filings). - daily_paper_batch.py worker: gather signed+unbatched cms855/cms10114 filings, group by destination (MAC by state via mac_routing; Fargo for CMS-10114), build cover+merged PDF per agency, persist batch, mark filings batched. Self-gates on postal working days (skips weekends + federal/USPS holidays). Phase 1 = human prints+mails; phase 2 = wire print-mail API. - worker-crons: pw-paper-batch systemd timer (Mon-Fri 13:30 UTC, self-gated). - test_paper_batch.py: 15/15 pass (working-day gating, routing, cover+merge).
This commit is contained in:
parent
258d23bdc6
commit
138fec17e9
5 changed files with 542 additions and 0 deletions
151
scripts/document_gen/templates/batch_cover_sheet.py
Normal file
151
scripts/document_gen/templates/batch_cover_sheet.py
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
"""Daily paper-filing batch cover sheet + merged print job.
|
||||
|
||||
For the Standard (no-login) CMS filing path we batch all signed, not-yet-mailed
|
||||
filings each postal working day and mail one Priority Mail envelope per
|
||||
destination agency (the provider's MAC, or the NPI Enumerator in Fargo). This
|
||||
module builds, for one destination/day:
|
||||
|
||||
1. a **cover sheet** PDF — sender (Performance West), destination agency +
|
||||
address, batch date, enclosed count, and a per-item manifest
|
||||
(order# / provider / NPI / form type); and
|
||||
2. a **merged print job** PDF — the cover sheet followed by every enclosed
|
||||
signed filing, ready to print and drop in one envelope.
|
||||
|
||||
Only reportlab + pypdf are required (already used across document_gen).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import logging
|
||||
from datetime import date
|
||||
|
||||
LOG = logging.getLogger("document_gen.batch_cover_sheet")
|
||||
|
||||
PW_SENDER = (
|
||||
"Performance West Inc.",
|
||||
"Provider Enrollment Filings",
|
||||
"filings@performancewest.net | (888) 411-0383",
|
||||
)
|
||||
|
||||
|
||||
def build_cover_sheet(
|
||||
*,
|
||||
destination_name: str,
|
||||
destination_address_lines: tuple[str, ...] | list[str],
|
||||
batch_date: date,
|
||||
items: list[dict],
|
||||
) -> bytes:
|
||||
"""Render the batch cover sheet PDF.
|
||||
|
||||
``items`` = list of dicts with keys: order_number, provider, npi, form.
|
||||
Returns PDF bytes.
|
||||
"""
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.lib.colors import HexColor
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.utils import simpleSplit
|
||||
|
||||
buf = io.BytesIO()
|
||||
c = canvas.Canvas(buf, pagesize=letter)
|
||||
w, h = letter
|
||||
teal = HexColor("#0f766e")
|
||||
gray = HexColor("#444444")
|
||||
x = inch
|
||||
y = h - inch
|
||||
|
||||
# Header band
|
||||
c.setFillColor(teal)
|
||||
c.rect(0, h - 0.9 * inch, w, 0.9 * inch, fill=1, stroke=0)
|
||||
c.setFillColor(HexColor("#ffffff"))
|
||||
c.setFont("Helvetica-Bold", 18)
|
||||
c.drawString(x, h - 0.6 * inch, "Provider Enrollment Filing Transmittal")
|
||||
y = h - 1.3 * inch
|
||||
|
||||
# Sender
|
||||
c.setFillColor(gray)
|
||||
c.setFont("Helvetica-Bold", 10)
|
||||
c.drawString(x, y, "FROM")
|
||||
y -= 14
|
||||
c.setFont("Helvetica", 10)
|
||||
for line in PW_SENDER:
|
||||
c.drawString(x, y, line)
|
||||
y -= 13
|
||||
y -= 8
|
||||
|
||||
# Destination
|
||||
c.setFont("Helvetica-Bold", 10)
|
||||
c.drawString(x, y, "TO")
|
||||
y -= 14
|
||||
c.setFont("Helvetica", 10)
|
||||
c.drawString(x, y, destination_name)
|
||||
y -= 13
|
||||
for line in destination_address_lines:
|
||||
c.drawString(x, y, line)
|
||||
y -= 13
|
||||
y -= 8
|
||||
|
||||
# Batch meta
|
||||
c.setFont("Helvetica-Bold", 10)
|
||||
c.drawString(x, y, f"Date: {batch_date.isoformat()}")
|
||||
c.drawString(x + 3 * inch, y, f"Enclosed filings: {len(items)}")
|
||||
y -= 22
|
||||
|
||||
# Divider
|
||||
c.setStrokeColor(HexColor("#cccccc"))
|
||||
c.line(x, y, w - inch, y)
|
||||
y -= 18
|
||||
|
||||
# Manifest header
|
||||
c.setFont("Helvetica-Bold", 9)
|
||||
cols = [(x, "ORDER"), (x + 1.4 * inch, "PROVIDER"),
|
||||
(x + 4.0 * inch, "NPI"), (x + 5.3 * inch, "FORM")]
|
||||
for cx, label in cols:
|
||||
c.drawString(cx, y, label)
|
||||
y -= 4
|
||||
c.line(x, y, w - inch, y)
|
||||
y -= 14
|
||||
|
||||
c.setFont("Helvetica", 9)
|
||||
for it in items:
|
||||
if y < inch: # new page
|
||||
c.showPage()
|
||||
y = h - inch
|
||||
c.setFont("Helvetica", 9)
|
||||
provider = (it.get("provider") or "")[:34]
|
||||
c.drawString(cols[0][0], y, str(it.get("order_number") or ""))
|
||||
c.drawString(cols[1][0], y, provider)
|
||||
c.drawString(cols[2][0], y, str(it.get("npi") or ""))
|
||||
c.drawString(cols[3][0], y, str(it.get("form") or "").upper())
|
||||
y -= 13
|
||||
|
||||
# Footer note
|
||||
y = max(y - 20, 0.7 * inch)
|
||||
c.setFont("Helvetica-Oblique", 8)
|
||||
c.setFillColor(gray)
|
||||
note = ("This transmittal accompanies the signed provider enrollment "
|
||||
"applications enclosed. Each application bears the provider's "
|
||||
"certification signature. Questions: filings@performancewest.net.")
|
||||
for line in simpleSplit(note, "Helvetica-Oblique", 8, w - 2 * inch):
|
||||
c.drawString(x, y, line)
|
||||
y -= 11
|
||||
|
||||
c.showPage()
|
||||
c.save()
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def merge_batch_pdf(cover_sheet: bytes, filing_pdfs: list[bytes]) -> bytes:
|
||||
"""Merge the cover sheet + all enclosed signed filings into one print job."""
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
|
||||
writer = PdfWriter()
|
||||
writer.append(PdfReader(io.BytesIO(cover_sheet)))
|
||||
for pdf in filing_pdfs:
|
||||
try:
|
||||
writer.append(PdfReader(io.BytesIO(pdf)))
|
||||
except Exception as exc: # one bad PDF shouldn't sink the batch
|
||||
LOG.error("[batch] skipping unreadable filing PDF: %s", exc)
|
||||
out = io.BytesIO()
|
||||
writer.write(out)
|
||||
return out.getvalue()
|
||||
Loading…
Add table
Add a link
Reference in a new issue