Skip to main content

Regulator reports & requests

Phase 1 shipped (2026-07-17)

The four criticals — gaps 1, 2, 3, 5 — landed in release regulator-reports-p1-20260717190705.

  • gap 1: new ClaimPayment.payeeNameHash column (HMAC-SHA256 via PiiCipher.hash) written on recordPayment. FiaThresholdService.generateGoAmlXml() now emits PAYEE-HASH:<suffix> for the claim-payout <first_name> element, matching the PAYER-HASH: pattern already used for inbound collection CTRs — no plaintext claimant name lands in the goAML file.
  • gap 2: IraQuarterlyReport.reportHash + TaxRemittance.reportHash columns hold a SHA-256 hex of the canonical-JSON snapshot of the compute inputs. Recomputed inside markSubmitted(); a mismatch (hand-run SQL UPDATE between compute and submit) raises INTEGRITY_VIOLATION. The XML download endpoints set an x-payload-hash: sha256=<hex> response header so regulator ops can verify the file in transit.
  • gap 3: new RegulatorReportSubmission model — symmetric outbound-disclosure log to RegulatorRecordFulfilment. Auto-emitted by IraReturnsService.markSubmitted() and UraRemittanceService.markSubmitted() on every state change; a single-query answer to "list every disclosure to IRA / URA in period X". FIA hooks in Phase 2 (via the new FiaCtrSweepRun).
  • gap 5: explicit state machine on IRA + URA markSubmitted: draft → submitted → acknowledged → closed. Direct jumps (draft → acknowledged), regressions (acknowledged → submitted), and post-terminal transitions raise ApiError.invariant. computeQuarter / computeMonth refuse to touch a row past draft (compute-freeze) so re-runs can't silently diverge the return content from what was filed.

Schema migration 20260717190000_regulator_reports_phase1 adds ClaimPayment.payeeNameHash, reportHash on both report tables, and the regulator_report_submissions table.

New regulator-reports-phase1.spec locks all 4 contracts (10 tests: hash-tamper detection on both services, submission emission on both state changes, compute-freeze past draft, direct-jump refusal, regression refusal). 53/53 suites, 388/388 tests green.

Phase 2 shipped (2026-07-17)

The operational-hygiene block — gaps 4, 6, 7, 8, 13, 14, 15 — landed in release regulator-reports-p2-20260717193007.

  • gap 4: new FiaCtrSweepRun model captures every FIA sweep run keyed on (windowFrom, windowTo). sweep() upserts, so concurrent same-window calls refresh counts rather than duplicating. New POST /reports/regulator/fia/sweep/:sweepRunId/mark-submitted writes back filedAt + filedReference and emits a RegulatorReportSubmission — closes the outbound-disclosure log loop for FIA that gap 3 opened for IRA / URA. GET /reports/regulator/fia/goaml.xml?sweepRunId=<uuid> binds the rendered XML's SHA-256 to the run row via xmlHash.
  • gap 6: @Throttle decorators across the whole surface. Compute + mark-submitted endpoints: 10/min. XML export endpoints (heavy queries): 5/min. POST /compliance/regulator-requests: 20/min. Read / list endpoints: 30–60/min. Excess → 429.
  • gap 7: new RecordRequestFiltersDto (dateRange, insurerId, productId, agentId, clientId, referralId, policyId, claimId). CreateRecordRequestDto.filters now validates through the DTO; unknown keys or malformed values raise 400 at create time so a compliance officer can't store an unparseable filter blob.
  • gap 8: new IraQuarterlySummary interface + isIraQuarterlySummary() runtime guard. generateXml() calls the guard before serialising — silent schema drift in computeQuarter now raises INVARIANT_VIOLATION instead of emitting malformed XML with IRA.
  • gap 13: UraRemittanceJob now short-circuits on quiet months (zero outstanding WHT payables). Emits ura.remittance.skipped_zero_items audit event instead of creating an empty envelope that ops would have to void manually.
  • gap 14: POST /compliance/regulator-requests accepts Idempotency-Key header; RegulatorRequestsService.createIdempotent memoises through the shared IdempotencyService (24 h TTL, scope regulator_record_request.create). Body-mismatch → 409 IDEMPOTENCY_CONFLICT. Matches invoices / payments.
  • gap 15: FiaThresholdService.onModuleInit() refuses to boot in NODE_ENV=production when FIA_REPORTING_ENTITY_ID is unset (falls back to the 'PENDING' sentinel) unless FIA_ALLOW_PENDING_ENTITY_ID_IN_PROD=true. Mirrors the sanctions + notification provider boot-gates so ops can't accidentally file a goAML with the sentinel id embedded.

Schema migration 20260717193000_regulator_reports_phase2 adds the fia_ctr_sweep_runs table.

New regulator-reports-phase2.spec locks 7 additional contracts (URA zero-item skip + normal path, five schema-guard shapes). 54/54 suites, 395/395 tests green.

Phase 3 shipped (2026-07-17)

The retention + access + resilience block — gaps 9, 10, 11, 12 — landed in release regulator-reports-p3-20260717195149, closing the module at 15 / 15 gaps.

  • gap 9: RetentionPurgeJob extends across the four regulator tables (IraQuarterlyReport, TaxRemittance, RegulatorRecordRequest, RegulatorRecordFulfilment). Driven by new data_sharing.regulator_reports_retention_days policy (default 3650 = 10 y — AML §14 + URA §41 7-y floor plus one rotation cycle). Terminal rows past the window get retentionScrubbedAt stamped and the ephemeral free-text nulled (summary, iraReference, uraReference, goamlXml, filters, denialReason, attachedNote, metadata) — §14 record fields (amounts, counts, timestamps) survive. Each sweep emits an aggregate *_report.retention.scrubbed audit event with the count + retention window.
  • gap 10: FiaThresholdService.resolveThresholds() reads fia.ctr_threshold_ugx + fia.aggregate_threshold_ugx from OperationalPolicy first, falling back to env (FIA_CTR_THRESHOLD_UGX / FIA_AGGREGATE_THRESHOLD_UGX) for bootstrap. BOU-driven rate moves no longer require a redeploy; each FiaCtrSweepRun row stamps the threshold-in-effect on the audit-of-record.
  • gap 11: new SystemPermissions.FinancialReportReadOwn (compliance:financial:report:own) granted to the agent role. GET /reports/regulator/ura/irp5/:agentId/:year accepts either ComplianceFinancialReport (any agent) or FinancialReportReadOwn (own agent-id only, enforced in-service). Agents can now self-serve their annual withholding statement without compliance-officer handholding; cross-agent access from an own-scope caller returns 403.
  • gap 12: XML export endpoints (GET /reports/regulator/fia/goaml.xml, GET /reports/regulator/ira/quarterly/:id/xml) now wrap the render + write in withXmlErrorAudit(). Any failure inside the generation / response-write path emits an xml_export.failed audit event with the endpoint code + error message before the exception filter returns 5xx. Partial-download failures + malformed render failures both leave a trail.

Schema migration 20260717194500_regulator_reports_phase3 adds retentionScrubbedAt to the four regulator tables + supporting indexes.

New regulator-reports-phase3.spec locks 7 additional contracts (policy-first threshold resolution + fallback chain, four boot-gate scenarios). 55/55 suites, 402/402 tests green.

All 15 / 15 gaps closed for the Regulator Reports & Requests module.

Scope

Two modules that together form the entire outbound + inbound regulator surface:

  1. regulator-reports/ — three services co-mounted on /reports/regulator/*:
    • FiaThresholdService — daily-cadence CTR / aggregate-STR sweep of the payments + claim-payments + collections ledgers; renders goAML 4.8 XML for FIA portal upload.
    • IraReturnsService — quarterly compute for the IRA insurer-return (premiums, claims paid, active policies, new / renewed / cancelled) with an XML skeleton export.
    • UraRemittanceService — monthly TaxPayable roll-up envelope by (year, month, taxCode); used by finance to file URA Form URA-04 (VAT) + URA-05 (WHT) and per-agent IRP5-equivalent annual statements.
  2. regulator-requests/ — formal information-request lifecycle: RegulatorRecordRequest (pending → approved → fulfilled | denied → closed) with typed RegulatorRecordFulfilment rows recording exactly which entities were released to which requester.

Together they answer the compliance question "what did we tell whom, when, and with what evidence?".

Compliance envelope

  • AML Act 2013 §6 — CTR (single-transaction ≥ UGX 20 M) + STR (aggregated activity) reporting duty to the Financial Intelligence Authority via goAML 4.8 XML. Threshold amounts are set by FIA and move with policy.
  • AML Act 2013 §12–§14 — record-keeping obligation covering every reportable transaction for 7 y past the last touch; the report itself is a §14 record.
  • IRA Insurance Act 2017 §102 — quarterly and annual insurer returns (premiums written, claims paid, cash + investment position) filed to IRA within 45 days of quarter-end.
  • IRA Insurance Act 2017 §112 — commission-cap enforcement is audited via the same quarterly return; over-cap agents surface here.
  • URA Income Tax Act §80 — monthly VAT + WHT remittance by the 15th of the following month.
  • URA Tax Procedures Act 2014 §41 — 7-year record-keeping floor on every financial document that supports the return.
  • DPPA 2019 §11 — minimum-necessary retention; regulator reports are §14 records inside the AML floor but §11 forbids indefinite retention past the floor.
  • DPPA 2019 §21 — third-party disclosure (which any regulator export is) must be logged with legal basis + recipient + scope; that's what the RegulatorRecordRequest + RegulatorRecordFulfilment pair captures for inbound requests, but nothing captures the same for proactive outbound submissions (FIA CTR, IRA quarterly).
  • BOU Cybersecurity Guidelines §5.4 — outbound file integrity; regulator must be able to verify the file wasn't tampered with in transit.

Current state (2026-07-17)

Module footprint

src/modules/regulator-reports/regulator-reports.module.ts — 1276 LOC
src/modules/regulator-reports/fia-threshold.service.ts — CTR/STR sweep + goAML XML
src/modules/regulator-reports/ira-returns.service.ts — quarterly compute + XML
src/modules/regulator-reports/ura-remittance.service.ts — monthly envelope + IRP5
src/modules/regulator-requests/regulator-requests.module.ts — inbound request lifecycle
src/modules/scheduled-jobs/ura-remittance.job.ts — monthly cron (15th @ 04:00 UTC)

Prisma models: IraQuarterlyReport, TaxPayable, TaxRemittance, RegulatorRecordRequest, RegulatorRecordFulfilment.

What works today

  • Idempotent compute — IRA computeQuarter() upserts on (insurerId, year, quarter); URA computeMonth() upserts on (year, month, taxCode). Re-runs refresh totals rather than duplicating rows.
  • Audit envelope — every service method takes an AuditContext and emits a structured event (regulator.fia.threshold_sweep, ira.quarterly_report.computed, ura.remittance.computed, regulator_record_request.{create,approve,deny,fulfil}). Scheduled jobs mint a synthetic actorType='system' context.
  • Payer PII masked in goAMLPaymentCollection.payerAccountHash (HMAC-SHA256 hex) is used as the <first_name> for inbound collection CTRs, not the plaintext MSISDN. The insurer payee-account is truncated to the first 8 chars of the id.
  • URA envelope covers all sourcesTaxPayable is polymorphic (commissionId | investmentId | invoiceId) and every source module writes into the same envelope so computeMonth sums a single ledger.
  • Typed fulfilment records — every entity released for a RegulatorRecordRequest is captured on RegulatorRecordFulfilment with entityKind + entityId + releasedBy + releasedAt and a compound unique on (requestId, entityKind, entityId) — duplicate attachments are silently ignored on re-fulfil rather than raising.

Gaps

All 15 gaps closed across Phase 1, Phase 2, and Phase 3 (all shipped 2026-07-17). See the shipped-notes above for the exact scope of each phase; every gap is now enforced by locked spec coverage in regulator-reports-phase1.spec / regulator-reports-phase2.spec / regulator-reports-phase3.spec.