Insurer & product catalogue (insurers, products, plans, IRA registry)
Scope
Six modules that together define the counterparty + product surface every referral submission gates against:
insurers/—Insurermodel: onboarding → active → suspended state machine, IRA licence tracking (registrationNo), contact fields, bordereau cadence.insurer-products/—InsurerProduct: an insurer's specific product filing (e.g. "Sanlam Motor Third-Party 2024"), IRA filing fields (iraFilingReference,iraFilingApprovedAt,iraFilingExpiresAt), commission applicability, active flag.insurer-subresources/—InsurerContact(PII-ciphered viaPiiCipher) +InsurerIntegrationSetting(webhook secret, API endpoint config, secret valuesSecretsCipher-encrypted).product-categories/— platform-level taxonomy (motor, life, medical, property, business, marine, agriculture, travel, other).product-plans/—ProductPlan(specific plan variant under anInsurerProduct) +ProductPlanVersion(append-only rate- card snapshot for historical policy recomputation) + per-plan required-document catalogue.ira-registry/—IraLicensedAgentlookup table: cache of IRA-approved licence holders for public verification (throttled public endpoint) + internal enrichment.
Together they gate every POST /referrals at creation and every
POST /referrals/:id/submit at PII transmission.
Compliance envelope
- IRA Insurance Act 2017 §12 — a policy may only be sold on a product with a valid IRA filing under a validly-licenced insurer. Filing + licence must be re-checked at PII-transmission time; a filing that expired between draft + submit is not §12-compliant.
- IRA Insurance Act 2017 §35 — rate-card filing. Rating factors in force at the moment of issue must be reconstructable from the ledger; ad-hoc rate changes retro-applied to a live book are §35 non-compliant.
- IRA Insurance Act 2017 §102 — quarterly insurer returns (already covered by the regulator-reports review) — this surface is the upstream ledger that feeds those returns.
- AML Act 2013 §6 — CDD on the insurer counterparty. Insurers are payment counterparties (premium collections + claim payouts); a sanctioned insurer in the platform is a §6 finding.
- DPPA 2019 §21 — insurer contact PII (contact person's email
/ phone). The sub-resource
InsurerContactciphers correctly; the parentInsurerrow'scontactEmail/contactPhonecolumns are still plaintext. - BOU Cybersecurity Guidelines §5.4 — status transitions chain-audited + cascaded to downstream state.
Current state (2026-07-18)
Module footprint
src/modules/insurers/insurers.module.ts — 259 LOC
src/modules/insurer-products/insurer-products.module.ts — 466 LOC
src/modules/insurer-subresources/insurer-subresources.module.ts — 410 LOC
src/modules/product-categories/product-categories.module.ts — 180 LOC
src/modules/product-plans/product-plans.module.ts — 450 LOC
src/modules/ira-registry/ira-registry.module.ts — 129 LOC
Prisma models: Insurer, InsurerContact, InsurerIntegrationSetting,
ProductCategory, InsurerProduct, ProductPlan,
ProductPlanVersion, IraLicensedAgent.
What works today
- Rate-card versioning is shipped:
ProductPlanVersionis an append-only snapshot table;resolveAt(planId, at)returns the version in force at a given timestamp so a policy issued 12 months ago still computes against its issue-time rate (IRA §35 compliant). - Bordereau cadence is honoured — the
BordereauScheduleJobreadsInsurer.bordereauCadenceand generates the IRA §102 quarterly-return input on schedule. - Insurer-contact PII cipher is shipped:
InsurerContact.email.phonearePiiCipher.encrypt-ed at rest with HMAC-SHA256 hash siblings for dedup, gated behind theInsurerContactPiipermission on read.
- Integration secrets are
SecretsCipher-encrypted at rest; list / get endpoints mask the value. - Own-scope enforcement on
list()+findById(): insurer- staff callers are restricted touser.insurerIdviaObjectAuthorizationService.assertInsurerAccess(). - Public IRA lookup —
GET /public/ira-registry/:iraNumberis throttled to 30/min per IP; used by client-portal / onboarding flows to verify an agent's IRA number without an authenticated session. - Category taxonomy soft-delete —
DELETE /product-categories/:idsoft-deletes when the category is still referenced by an active product; hard-deletes when unused. - Audit trail — every insurer / product / plan / category / contact / integration state transition emits a structured audit event with before / after snapshots + reason (on suspend).
Gaps
All 12 gaps closed. See shipped-notes below.
Release insurer-p1-20260718-0700. Closes gaps 1–4:
- gap 1 —
IraRegistrySyncJobruns weekly (Mon 04:00 UTC) and emitsira_registry.syncaudit + flags stale when the newestIraLicensedAgent.importedAtexceedsIRA_REGISTRY_SYNC_STALE_DAYS(default 30). - gap 2 —
ReferralsService.create()(viaensureAgentAndProduct) andsubmit()both callassertProductIraFilingValid(); refusals raiseINVARIANT_VIOLATIONandsubmit()emitsreferral.product.ira_filing_invalidaudit event. - gap 3 —
SUBJECT_TYPESgainedinsurer;InsurersService.create()runsscreenForPayout('insurer', …)and on block suspends the insurer + emitsinsurer.sanctions_block. - gap 4 — Migration
20260718090000_insurer_phase1addscontactEmailHash,contactPhoneHash,piiEncryptedAttoinsurers+ indexes;InsurersService.create/updatecipher + hash-sibling every write, legacy rows round-trip viaPiiCipher.decryptfallback.
Locked in by src/modules/insurers/insurer-phase1.spec.ts
(7 cases). Test suite: 478/478 (68 suites).
Release insurer-p2-20260718-0716. Closes gaps 5–8:
- gap 5 —
InsurersService.suspend / deactivate / archiveeach callcascadeInFlightReferralsOnSuspend()which flips referrals insent_to_insurer,quotation_pending,quotation_received,quotation_shared,client_accepted,premium_payment_pendingtoon_holdwithmetadata.heldReason='insurer_suspended'. Emits aggregateinsurer.suspend.in_flight_referralsaudit event. - gap 6 — new
POST /insurers/:id/deactivate+POST /insurers/:id/archiveendpoints, gated by newinsurer:deactivate+insurer:archivepermissions (added to super_admin default). Emitinsurer.deactivate/insurer.archive. Deactivating an alreadyarchivedinsurer refuses. - gap 7 —
InsurerProductsService.create / update / bulkImportfunneliraFiling*throughparseAndValidateIraFiling(), which refuses approvedAt in the future, expiresAt in the past, and expires ≤ approved. Bulk-import surfaces filing failures asinsurer_product.bulk_import.ira_validation_failed. CSV column set now includesiraFilingReference,iraFilingApprovedAt,iraFilingExpiresAt. - gap 8 —
ProductPlansService.deactivatequeriesReferralfor the plan with a non-terminal status; on hit, refuses with a sample of affected reference numbers and emitsproduct_plan.deactivate.blocked_by_referrals.
Locked in by src/modules/insurers/insurer-phase2.spec.ts
(6 cases). Test suite: 484/484 (69 suites).
Release insurer-p3-20260718-0835. Closes gaps 9–12:
- gap 9 —
InsurersService,InsurerProductsService,ProductPlansServiceeach expose acreateIdempotent()wrapper keyed onIdempotency-Keyheader via the sharedIdempotencyService(per-endpoint scope, 24 h TTL). Same body + key replays the memoised response; missing header falls through to the plain create path. - gap 10 —
@Throttleon every mutation across insurers + insurer-products + product-plans: creates 10/min, updates + activate / deactivate / suspend / archive 20/min, bulk-import 3/min, adminPOST /ira-registry/sync3/min. Excess → 429. - gap 11 — new
data_sharing.insurer_catalogue_retention_dayspolicy (default 3650).RetentionPurgeJobnow scrubs archived insurers past the window: contact PII +contactEmailHash/contactPhoneHash+ address are erased, InsurerContact rows scrubbed in-place, andInsurer.piiScrubbedAtis stamped. Emitsinsurer.retention.pii_scrubbedaudit event. - gap 12 — new
IraRegistrySyncRunledger table;IraRegistrySyncJob.tick()opens a row at start + closes it with counts + stale flag at end. New endpoints:POST /ira-registry/sync(throttled 3/min, gated byira_registry:syncpermission granted to super_admin) invokes the job manually withinitiatedType='admin'+initiatedBy=user.id, andGET /ira-registry/sync-runslists the ledger.
Migration: 20260718100000_insurer_phase3 — creates
ira_registry_sync_runs + adds Insurer.piiScrubbedAt + index.
Locked in by src/modules/insurers/insurer-phase3.spec.ts
(4 cases). Test suite: 488/488 (70 suites).
All 12 gaps closed. Insurer & product catalogue module is audit-clean against IRA §12 / §35, AML §6 / §14, and DPPA §11 / §21.
Phased implementation plan
Complete — every phase shipped.