Skip to main content

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 typed ReportData dataclass — no dicts, no Any, all sanitization done here.
  • report_service.py receives the typed ReportData and 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.

DocumentBuilt byOutput
KYB Compliance Reportreport_service.generate_compliance_report10-section PDF (templates/compliance_report.html)
Audit Ledgerreport_service.generate_audit_ledger14-section PDF or JSON (templates/audit_ledger_v2.html)
Regulator case packcase_pack_serviceTamper-evident ZIP + SHA-256 manifest + pack_hash (ADR-0069)
MLRO memo / evidence request / source appendixcompliance_docs_builderPDFs; source appendix carries per-source collected_at + content_hash provenance (PR #171)
SAR/STR reportability assessmentsar_assessmentSealed into the case pack (fail-closed inclusion)
Officer Decision Memorandumdecision_memorandum_serviceSigned 9-section HTML or A4 PDF via WeasyPrint (PR #169)
EU AI Act conformity recordai_act_conformity_serviceGET /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:

MethodPathDescription
GET/cases/{workflow_id}/reportKYB 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

ModulePurpose
report_data_builder.pyData assembly from Temporal/PostgreSQL/MinIO into a typed ReportData
report_service.pyJinja2 + WeasyPrint PDF rendering of the Compliance Report and Audit Ledger
case_analysis.pyFastAPI endpoints exposing the two reports