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;
contentSha256present 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
clientsentity 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. BordereauWebhookServiceHMAC verification on insurer acknowledgement callback.WarehouseExportService.onModuleInitvalidatesWAREHOUSE_ALLOWED_HOSTSboot-gate.warehouse.export.completedaudit summary event.RetentionPurgeJob90-d artefact purge withwarehouse.export.retention.purgedaudit event.
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.consentIdaccepted + persisted onWarehouseExport.consentId. - gap 7 —
TriggerExportDto.businessPurposeaccepted + persisted; ready for CPA §37 pivot queries. - gap 9 —
bordereau.generate()stampsdueAt = periodEnd + 72 h;bordereau.share()stampsfiledAt = sharedAt. SLA-breach queries can now compute (filedAt - dueAt) delta.
Also fully closed:
- gap 1 — New
BordereauSlaEscalationJobruns hourly, flags any bordereau pastdueAtwithoutfiledAt, stampsslaBreachedAtonce (idempotent) + auditbordereau.filing_sla_breached+ compliance-officer fanout. Migration20260721050000_bordereau_sla_breachedadds the column + partial index. - gap 6 —
runExport()writes oneWarehouseExportManifestrow 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 })onPOST /admin/warehouse/exports/runcaps concurrent triggers (5 / hour per caller). - gap 10 —
runExport()stampsWarehouseExport.encryptionKeyKid = SecretsCipher.activeKid()when the export is encrypted; decrypt paths select the correct key after rotation. - gap 4 —
BordereauWebhookService.ingest()accepts + verifies EdDSAsignature+publicKeyKidon the ack payload againstBORDEREAU_ACK_PUBKEY_<INSURERCODE>_<KID>env;BORDEREAU_ACK_REQUIRE_SIGNATURE=truerefuses acks without a valid signature. Verified signature + kid persist onBordereau.insurerAckSignature+insurerAckPublicKeyKidfor §132 dispute defence. - gap 3 —
redactForExport()extends beyond the previous clients-only mask:referralsmasks name / email / phone / nationalId / address;paymentsmasksaccountHolder;agentsmasks name / phone / email / nationalId / address / taxPin / bankAccountNumber. WAREHOUSE_INCLUDE_NAMES=true still opts back in with a DPPA §16 justification. - gap 12 —
TriggerExportDto.scopeInsurerId+scopeOrganizationIdpush 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-entitypii.accessaudit alongside the manifest write (entityKind,rowCount,rowShaChainin metadata) so a subject- rights query can locate exactly which warehouse export shipped a subject's row. - gap 11 —
BordereauWebhookService.verifySignatureWithReason()distinguishesmissing_header/unknown_insurer_secret/signature_mismatchand emitsbordereau.webhook.signature.invalidaudit 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 |
|---|---|
| 1 | bordereau.filing_sla_hours policy (default 72). Overdue bordereau flagged by a new BordereauSlaEscalationJob + emits bordereau.filing_sla_breached. |
| 2 | Migration adds WarehouseExport.consentId FK to DataTransferConsent. TriggerExportDto requires the consentId; refused without valid sms_kenya/whatsapp_us/email_smtp acknowledgement. |
| 3 | redactForExport() extends to policies, claims, commissions, payments, agents. Per-entity mask matrix in a config file. |
| 4 | Migration adds Bordereau.insurerAckSignature + insurerAckPublicKeyKid. BordereauWebhookService.receiveAck requires an EdDSA-signed acknowledgement payload; verify against insurer's registered public key. |
| 5 | runExport() invokes AuditService.recordPiiAccess(ctx, { subjectType: 'warehouse_export', ... }) for each entity shipped. |
| 6 | Migration adds WarehouseExportManifest model — retained past artefact purge for the AML §14 7-y floor — mapping exportId → sha256[] of every row. |
| 7 | TriggerExportDto 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. |
| 9 | Migration adds Bordereau.dueAt + filedAt. Scheduler stamps dueAt on generation; share() stamps filedAt. |
| 10 | WarehouseExport.encryptionKeyKid column recorded per export; rotation ledger integration via SecretRotation. |
| 11 | verifySignature() returns detailed reason + emits bordereau.webhook.signature.invalid audit on failure. |
| 12 | Per-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.