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