Regulator reports & requests
The four criticals — gaps 1, 2, 3, 5 — landed in release
regulator-reports-p1-20260717190705.
- gap 1: new
ClaimPayment.payeeNameHashcolumn (HMAC-SHA256 viaPiiCipher.hash) written onrecordPayment.FiaThresholdService.generateGoAmlXml()now emitsPAYEE-HASH:<suffix>for the claim-payout<first_name>element, matching thePAYER-HASH:pattern already used for inbound collection CTRs — no plaintext claimant name lands in the goAML file. - gap 2:
IraQuarterlyReport.reportHash+TaxRemittance.reportHashcolumns hold a SHA-256 hex of the canonical-JSON snapshot of the compute inputs. Recomputed insidemarkSubmitted(); a mismatch (hand-run SQL UPDATE between compute and submit) raisesINTEGRITY_VIOLATION. The XML download endpoints set anx-payload-hash: sha256=<hex>response header so regulator ops can verify the file in transit. - gap 3: new
RegulatorReportSubmissionmodel — symmetric outbound-disclosure log toRegulatorRecordFulfilment. Auto-emitted byIraReturnsService.markSubmitted()andUraRemittanceService.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 newFiaCtrSweepRun). - gap 5: explicit state machine on IRA + URA
markSubmitted:draft → submitted → acknowledged → closed. Direct jumps (draft → acknowledged), regressions (acknowledged → submitted), and post-terminal transitions raiseApiError.invariant.computeQuarter/computeMonthrefuse to touch a row pastdraft(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.
The operational-hygiene block — gaps 4, 6, 7, 8,
13, 14, 15 — landed in release
regulator-reports-p2-20260717193007.
- gap 4: new
FiaCtrSweepRunmodel captures every FIA sweep run keyed on(windowFrom, windowTo).sweep()upserts, so concurrent same-window calls refresh counts rather than duplicating. NewPOST /reports/regulator/fia/sweep/:sweepRunId/mark-submittedwrites backfiledAt+filedReferenceand emits aRegulatorReportSubmission— 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 viaxmlHash. - gap 6:
@Throttledecorators 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.filtersnow 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
IraQuarterlySummaryinterface +isIraQuarterlySummary()runtime guard.generateXml()calls the guard before serialising — silent schema drift incomputeQuarternow raisesINVARIANT_VIOLATIONinstead of emitting malformed XML with IRA. - gap 13:
UraRemittanceJobnow short-circuits on quiet months (zero outstanding WHT payables). Emitsura.remittance.skipped_zero_itemsaudit event instead of creating an empty envelope that ops would have to void manually. - gap 14:
POST /compliance/regulator-requestsacceptsIdempotency-Keyheader;RegulatorRequestsService.createIdempotentmemoises through the sharedIdempotencyService(24 h TTL, scoperegulator_record_request.create). Body-mismatch → 409IDEMPOTENCY_CONFLICT. Matches invoices / payments. - gap 15:
FiaThresholdService.onModuleInit()refuses to boot inNODE_ENV=productionwhenFIA_REPORTING_ENTITY_IDis unset (falls back to the'PENDING'sentinel) unlessFIA_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.
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:
RetentionPurgeJobextends across the four regulator tables (IraQuarterlyReport,TaxRemittance,RegulatorRecordRequest,RegulatorRecordFulfilment). Driven by newdata_sharing.regulator_reports_retention_dayspolicy (default 3650 = 10 y — AML §14 + URA §41 7-y floor plus one rotation cycle). Terminal rows past the window getretentionScrubbedAtstamped 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.scrubbedaudit event with the count + retention window. - gap 10:
FiaThresholdService.resolveThresholds()readsfia.ctr_threshold_ugx+fia.aggregate_threshold_ugxfromOperationalPolicyfirst, falling back to env (FIA_CTR_THRESHOLD_UGX/FIA_AGGREGATE_THRESHOLD_UGX) for bootstrap. BOU-driven rate moves no longer require a redeploy; eachFiaCtrSweepRunrow 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/:yearaccepts eitherComplianceFinancialReport(any agent) orFinancialReportReadOwn(own agent-id only, enforced in-service). Agents can now self-serve their annual withholding statement without compliance-officer handholding; cross-agent access from anown-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 inwithXmlErrorAudit(). Any failure inside the generation / response-write path emits anxml_export.failedaudit 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:
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— monthlyTaxPayableroll-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.
regulator-requests/— formal information-request lifecycle:RegulatorRecordRequest(pending → approved → fulfilled | denied → closed) with typedRegulatorRecordFulfilmentrows 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+RegulatorRecordFulfilmentpair 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); URAcomputeMonth()upserts on(year, month, taxCode). Re-runs refresh totals rather than duplicating rows. - Audit envelope — every service method takes an
AuditContextand 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 syntheticactorType='system'context. - Payer PII masked in goAML —
PaymentCollection.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 sources —
TaxPayableis polymorphic (commissionId | investmentId | invoiceId) and every source module writes into the same envelope socomputeMonthsums a single ledger. - Typed fulfilment records — every entity released for a
RegulatorRecordRequestis captured onRegulatorRecordFulfilmentwithentityKind + entityId + releasedBy + releasedAtand 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.