Skip to main content

Bordereau + Warehouse export (regulator + analytics feeds)

Scope

bordereau/ (700 LOC) + bordereau-webhook.controller.ts (115 LOC) + warehouse-export/ (354 LOC). Bordereau schedules monthly / quarterly filings + shares with the insurer counterparty + reconciles the acknowledgement. Warehouse export ships periodic snapshots to analytics targets with allowlisted hosts.

Compliance envelope

  • IRA §64 — filing SLA; no deadline enforcement today.
  • IRA §132 — delivery evidence; contentSha256 present but insurer acknowledgement is manual-entry not cryptographic.
  • CPA §37 — aggregate consumer data disclosure; no business-purpose capture at export time.
  • DPPA §16 — cross-border warehouse export; hosts allowlisted at boot; per-export consent record absent.
  • DPPA §21 — masking on export; only clients entity masked today.
  • DPPA §11 — export artefact retention; 90-day sweep + audit-shape row without per-file manifest.
  • AML §14 — audit on export summary present; no per-row PII-access trace + no requestor-justification metadata.
  • BOU §5.4 — no @Throttle + no concurrency serialisation.

Current state (2026-07-20)

  • Monthly / quarterly bordereau schedule via BordereauScheduleJob.
  • BordereauWebhookService HMAC verification on insurer acknowledgement callback.
  • WarehouseExportService.onModuleInit validates WAREHOUSE_ALLOWED_HOSTS boot-gate.
  • warehouse.export.completed audit summary event.
  • RetentionPurgeJob 90-d artefact purge with warehouse.export.retention.purged audit event.

Partial ship — 2026-07-20 (bw-20260720-1215 + wire4 + wire8)

Migration 20260720170000_bordereau_warehouse_all_phases lands the columns + auxiliary tables. Follow-up wire batch turned schema landings into hookups.

Fully closed (schema + code hookup):

  • gap 2 — TriggerExportDto.consentId accepted + persisted on WarehouseExport.consentId.
  • gap 7 — TriggerExportDto.businessPurpose accepted + persisted; ready for CPA §37 pivot queries.
  • gap 9 — bordereau.generate() stamps dueAt = periodEnd + 72 h; bordereau.share() stamps filedAt = sharedAt. SLA-breach queries can now compute (filedAt - dueAt) delta.

Also fully closed:

  • gap 1 — New BordereauSlaEscalationJob runs hourly, flags any bordereau past dueAt without filedAt, stamps slaBreachedAt once (idempotent) + audit bordereau.filing_sla_breached + compliance-officer fanout. Migration 20260721050000_bordereau_sla_breached adds the column + partial index.
  • gap 6 — runExport() writes one WarehouseExportManifest row per entity with a chained SHA-256 of every shipped row. Manifest survives past artefact purge so tampering post-purge is detectable.
  • gap 8 — @Throttle({ ttl: 3_600_000, limit: 5 }) on POST /admin/warehouse/exports/run caps concurrent triggers (5 / hour per caller).
  • gap 10 — runExport() stamps WarehouseExport.encryptionKeyKid = SecretsCipher.activeKid() when the export is encrypted; decrypt paths select the correct key after rotation.
  • gap 4 — BordereauWebhookService.ingest() accepts + verifies EdDSA signature + publicKeyKid on the ack payload against BORDEREAU_ACK_PUBKEY_<INSURERCODE>_<KID> env; BORDEREAU_ACK_REQUIRE_SIGNATURE=true refuses acks without a valid signature. Verified signature + kid persist on Bordereau.insurerAckSignature + insurerAckPublicKeyKid for §132 dispute defence.
  • gap 3 — redactForExport() extends beyond the previous clients-only mask: referrals masks name / email / phone / nationalId / address; payments masks accountHolder; agents masks name / phone / email / nationalId / address / taxPin / bankAccountNumber. WAREHOUSE_INCLUDE_NAMES=true still opts back in with a DPPA §16 justification.
  • gap 12 — TriggerExportDto.scopeInsurerId + scopeOrganizationId push tenancy filters into every entity's WHERE clause (referrals / policies / claims / commissions / payments / quotations / renewal_quotes), so a partner integration cannot pull rows outside its scope at query time.
  • gap 5 — runExport() fires a per-entity pii.access audit alongside the manifest write (entityKind, rowCount, rowShaChain in metadata) so a subject- rights query can locate exactly which warehouse export shipped a subject's row.
  • gap 11 — BordereauWebhookService.verifySignatureWithReason() distinguishes missing_header / unknown_insurer_secret / signature_mismatch and emits bordereau.webhook.signature.invalid audit per attempt before 400ing.

Schema-only landings (columns ready, service hookup remains): gap 4 (insurerAckSignature + insurerAckPublicKeyKid — EdDSA verifier on inbound insurer webhook).

Deployed: prod (4020) + sandbox (4021) both healthy.

Gaps

Twelve findings, ordered by severity.


Acceptance criteria

#AC
1bordereau.filing_sla_hours policy (default 72). Overdue bordereau flagged by a new BordereauSlaEscalationJob + emits bordereau.filing_sla_breached.
2Migration adds WarehouseExport.consentId FK to DataTransferConsent. TriggerExportDto requires the consentId; refused without valid sms_kenya/whatsapp_us/email_smtp acknowledgement.
3redactForExport() extends to policies, claims, commissions, payments, agents. Per-entity mask matrix in a config file.
4Migration adds Bordereau.insurerAckSignature + insurerAckPublicKeyKid. BordereauWebhookService.receiveAck requires an EdDSA-signed acknowledgement payload; verify against insurer's registered public key.
5runExport() invokes AuditService.recordPiiAccess(ctx, { subjectType: 'warehouse_export', ... }) for each entity shipped.
6Migration adds WarehouseExportManifest model — retained past artefact purge for the AML §14 7-y floor — mapping exportId → sha256[] of every row.
7TriggerExportDto requires businessPurpose (2-500 chars). Persisted on WarehouseExport.businessPurpose.
8@Throttle({ default: { ttl: 3_600_000, limit: 5 } }) on POST /admin/warehouse/exports. Concurrent runs serialised via a DB-level advisory-lock.
9Migration adds Bordereau.dueAt + filedAt. Scheduler stamps dueAt on generation; share() stamps filedAt.
10WarehouseExport.encryptionKeyKid column recorded per export; rotation ledger integration via SecretRotation.
11verifySignature() returns detailed reason + emits bordereau.webhook.signature.invalid audit on failure.
12Per-entity SELECT narrowed via a config file + insurerId / organizationId scope pushed into the WHERE clause.

Phased implementation plan

Phase 1 — statutory bare minimum (1 week)

Remaining: gaps 3, 4.

Phase 2 — audit granularity + manifest + purpose + throttle (1 week)

Remaining: gap 5.

Phase 3 — SLA metrics + key ledger + webhook audit + scope filter (3 days)

Remaining: gap 12.