Import ClientFlow production v4928.1.5.132.4

This commit is contained in:
plx
2026-07-29 13:11:01 +00:00
parent 6445044ac6
commit 261d342057
405 changed files with 48373 additions and 1401 deletions

113
app/pdf_format_guard.py Normal file
View File

@@ -0,0 +1,113 @@
"""PDF format guardrails for customer-facing document delivery.
ClientFlow normally receives Jasmin PDFs as opaque bytes. For customer-facing
invoice sends we still need one cheap safety check: invoices should be printable
A4, not a narrow receipt-like page returned by a wrong Jasmin print layout.
The parser is deliberately dependency-free. It reads common uncompressed page
boxes such as /MediaBox and /CropBox, which are present in PDFs returned by the
Jasmin print endpoints observed in production.
"""
from __future__ import annotations
import hashlib
import re
from typing import Any, Dict, List
A4_PORTRAIT_PT = (595.0, 842.0)
POINT_TO_MM = 25.4 / 72.0
def _looks_a4(width_pt: float, height_pt: float, tolerance_pt: float = 20.0) -> bool:
pairs = [(width_pt, height_pt), (height_pt, width_pt)]
return any(
abs(w - A4_PORTRAIT_PT[0]) <= tolerance_pt and abs(h - A4_PORTRAIT_PT[1]) <= tolerance_pt
for w, h in pairs
)
def extract_pdf_boxes(data: bytes) -> List[Dict[str, Any]]:
latin = data.decode("latin-1", errors="ignore")
pattern = re.compile(
r"/(MediaBox|CropBox)\s*\[\s*([-+0-9.]+)\s+([-+0-9.]+)\s+([-+0-9.]+)\s+([-+0-9.]+)\s*\]",
re.I,
)
boxes: List[Dict[str, Any]] = []
seen: set[tuple[str, float, float, float, float]] = set()
for m in pattern.finditer(latin):
kind = m.group(1)
x0, y0, x1, y1 = (float(m.group(i)) for i in range(2, 6))
key = (kind.lower(), x0, y0, x1, y1)
if key in seen:
continue
seen.add(key)
w = abs(x1 - x0)
h = abs(y1 - y0)
boxes.append({
"kind": kind,
"width_pt": round(w, 2),
"height_pt": round(h, 2),
"width_mm": round(w * POINT_TO_MM, 1),
"height_mm": round(h * POINT_TO_MM, 1),
"orientation": "landscape" if w > h else "portrait",
"looks_a4": _looks_a4(w, h),
})
return boxes
def analyze_pdf_format(data: bytes, content_type: str = "application/pdf") -> Dict[str, Any]:
info: Dict[str, Any] = {
"content_type": content_type,
"bytes": len(data or b""),
"sha256_12": hashlib.sha256(data or b"").hexdigest()[:12] if data else "",
"is_pdf_header": bool(data and data[:5] == b"%PDF-"),
"page_count_hint": None,
"boxes": [],
"format_warning": "",
}
if not data:
info["format_warning"] = "empty_response"
return info
if not info["is_pdf_header"]:
info["format_warning"] = "not_pdf_header"
return info
latin = data.decode("latin-1", errors="ignore")
page_count = len(re.findall(r"/Type\s*/Page(?!s)\b", latin))
info["page_count_hint"] = page_count or None
boxes = extract_pdf_boxes(data)
info["boxes"] = boxes
media_boxes = [b for b in boxes if str(b.get("kind") or "").lower() == "mediabox"]
if media_boxes and not any(bool(b.get("looks_a4")) for b in media_boxes):
info["format_warning"] = "non_a4_or_unexpected_mediabox"
elif len(data) < 15_000:
info["format_warning"] = "very_small_pdf"
elif len(data) > 2_500_000:
info["format_warning"] = "very_large_pdf"
return info
def first_box_label(info: Dict[str, Any]) -> str:
boxes = info.get("boxes") if isinstance(info, dict) else []
if not boxes:
return "sem MediaBox/CropBox legível"
b = boxes[0]
return f"{b.get('kind') or 'Box'} {b.get('width_mm')}x{b.get('height_mm')}mm a4={b.get('looks_a4')}"
def is_customer_send_blocking_pdf_warning(info: Dict[str, Any]) -> bool:
return str((info or {}).get("format_warning") or "") in {"not_pdf_header", "empty_response", "non_a4_or_unexpected_mediabox"}
def invoice_pdf_block_message(document_number: str, info: Dict[str, Any]) -> str:
warning = str((info or {}).get("format_warning") or "")
box = first_box_label(info)
if warning == "non_a4_or_unexpected_mediabox":
return (
f"PDF da fatura {document_number or ''} bloqueado: o Jasmin devolveu formato não-A4 "
f"({box}). Verificar layout/template de impressão da série/tipo FA no Jasmin antes de enviar ao cliente."
).strip()
if warning == "not_pdf_header":
return f"PDF da fatura {document_number or ''} bloqueado: o endpoint Jasmin não devolveu um PDF válido.".strip()
if warning == "empty_response":
return f"PDF da fatura {document_number or ''} bloqueado: resposta vazia do Jasmin.".strip()
return f"PDF da fatura {document_number or ''} bloqueado: {warning or 'formato inesperado'}.".strip()