new-site/scripts/document_gen/templates/batch_cover_sheet.py
justin 138fec17e9 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).
2026-06-07 00:30:01 -05:00

151 lines
4.6 KiB
Python

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