Reporting
Report generation pipeline that assembles investigation data into structured, PDF-rendered compliance documents. The two-layer design cleanly separates data assembly from rendering:
ReportDataBuilder(report_data_builder.py) queries Temporal, PostgreSQL, and MinIO and produces a fully typedReportDatadataclass — no dicts, noAny, all sanitization done here.report_service.pyreceives the typedReportDataand renders it to PDF via Jinja2 + WeasyPrint. It does no data assembly and no DB queries.
The typed model lives in the shared trustrelay_models.report package (re-exported through app/models/report.py), comprising dataclasses such as CompanyIdentity, Finding, Discrepancy, DirectorRecord, UBORecord, ConfidenceBreakdown, FollowUpTask, ReportAuditEvent, and BrandingConfig, aggregated into ReportData.
The document family
ReportData is the single source of truth for a whole family of compliance documents,
not just two reports. Every document below assembles from the same ReportData, so the
verdict, findings, and evidence are identical across them (the "one verdict" consistency
fix, PR #140) — a regulator comparing the report, the memo, and the case pack sees one story.
| Document | Built by | Output |
|---|---|---|
| KYB Compliance Report | report_service.generate_compliance_report | 10-section PDF (templates/compliance_report.html) |
| Audit Ledger | report_service.generate_audit_ledger | 14-section PDF or JSON (templates/audit_ledger_v2.html) |
| Regulator case pack | case_pack_service | Tamper-evident ZIP + SHA-256 manifest + pack_hash (ADR-0069) |
| MLRO memo / evidence request / source appendix | compliance_docs_builder | PDFs; source appendix carries per-source collected_at + content_hash provenance (PR #171) |
| SAR/STR reportability assessment | sar_assessment | Sealed into the case pack (fail-closed inclusion) |
| Officer Decision Memorandum | decision_memorandum_service | Signed 9-section HTML or A4 PDF via WeasyPrint (PR #169) |
| EU AI Act conformity record | ai_act_conformity_service | GET /api/conformity/ai-act.pdf (ADR-0072) |
The core report_service functions return PDF bytes; Jinja2 is configured with custom
filters for formatting dates, currency (€ with M/K abbreviation), percentages, booleans,
trend arrows, and RAG/severity color codes. The case-pack, compliance-document, and
decision-memorandum surfaces are documented in full on the
Case-Pack Export & Compliance Documents page.
Honest coverage-gap finding
ReportDataBuilder injects a Finding(category="adverse_media_recall_gap", severity="medium")
whenever the network scan identified related entities or named persons that were
sanctions/PEP-screened but not individually adverse-media-searched. A CLEAR screening
result is therefore never presented as "no adverse media" for the related network — the
report states plainly that adverse media was not assessed for those entities
(ADR-0067 fail-closed "not assessed" contract).
API Endpoints
Exposed through app/api/case_analysis.py:
| Method | Path | Description |
|---|---|---|
| GET | /cases/{workflow_id}/report | KYB Compliance Report as PDF |
| GET | /cases/{workflow_id}/audit-ledger (alias /cases/{workflow_id}/report/audit-ledger) | Audit Ledger as PDF (?format=json returns the raw ReportData) |
Each endpoint resolves the case, builds a ReportData via ReportDataBuilder (passing the Temporal client from app state), renders, and streams the result with a Content-Disposition attachment header.
Components
| Module | Purpose |
|---|---|
report_data_builder.py | Data assembly from Temporal/PostgreSQL/MinIO into a typed ReportData |
report_service.py | Jinja2 + WeasyPrint PDF rendering of the Compliance Report and Audit Ledger |
case_analysis.py | FastAPI endpoints exposing the two reports |