Aller au contenu

Client Profile Schema (CPS) — contrat canonique partagé

Module : profile-schema-svc (micro-service léger, source de vérité du contrat). Voir ADR-026 — Intégration Form Designer ↔ Risk Matrix via CPS, ADR-007 (Form Designer), ADR-025 (Risk Matrix).

Le Client Profile Schema (CPS) est le contrat canonique qui déclare, pour chaque tenant, les variables observables par les modules VitaKYC — typiquement client.profession, client.country, session.channel, screening.ofacMatch. Il est produit par les sources (Form Designer, connecteurs, modules) et consommé principalement par le Risk Matrix.

Ce document spécifie sa structure, son cycle de vie, son API, ses contraintes de cohérence, et les événements cross-module qu’il pilote.


Sans CPS, deux modes de défaillance silencieuse classiques :

Sans CPSScénarioConséquence
Champ fantômeRisk Matrix référence client.homeowner_status qui n’existe nulle partRègle passe la validation syntaxique mais ne matche jamais. Zéro true positive. Détectable seulement via audit approfondi.
Règle orphelineForm Designer supprime le champ profession utilisé par 3 règlesRègles deviennent muettes en silence. Perte de détection PEP et professions à risque.

Avec CPS :

  • Le Form Designer déclare ce qu’il collecte → CPS enregistre avec origine + model card
  • Le Risk Matrix editor autocomplete depuis le CPS, refus serveur si symbole inexistant
  • Form Designer refuse la suppression d’un champ référencé par policy ACTIVE / SHADOW
  • Notifications cross-module à chaque changement structurant

interface ClientProfileSchema {
tenantId: string; // "TN-BANQUEX"
version: string; // ISO-8601 timestamp de dernière modification
hash: string; // SHA-256 du contenu pour intégrité
variables: Variable[];
}
interface Variable {
/** Chemin dot-notation canonique, ex : "client.profession" */
path: string;
/** Type de la variable */
type: "string" | "integer" | "decimal" | "boolean"
| "enum" | "iso3166-alpha2" | "iso4217" // country code, currency
| "date" | "timestamp" | "email" | "phone";
/** Si type=enum, nom du catalogue de valeurs autorisées */
enumCatalog?: string; // ex: "BCT_PROFESSION_CODES"
/** Module ou source qui émet cette variable */
source: string; // "form:FORM_KYC_INDIVIDUAL@v2.7" | "screening:worldcheck" | ...
/** Obligatoire à l'évaluation ? (sinon fallback défini dans policy) */
required: boolean;
/** Classification sensibilité */
sensitivity: "PUBLIC" | "INTERNAL" | "PII" | "SPI"; // SPI = Sensitive PII
/** Field deprecated (bientôt retiré) */
deprecated: boolean;
deprecatedAt?: string; // ISO-8601
deprecatedReason?: string;
/** Liste des consommateurs (références back) */
usedBy: UsedByRef[];
/** Model card locale au champ (YAML inline pour audit) */
modelCard?: string;
}
interface UsedByRef {
kind: "risk.rule" | "risk.override" | "aml.rule" | "report.field";
ref: string; // "policy:TN-BANQUEX@2.1#dimension=client#rule=3"
}
{
"tenant_id": "TN-BANQUEX",
"version": "2026-04-23T11:42:00Z",
"hash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"variables": [
{
"path": "client.profession",
"type": "enum",
"enum_catalog": "BCT_PROFESSION_CODES",
"source": "form:FORM_KYC_INDIVIDUAL@v2.7",
"required": false,
"sensitivity": "PII",
"deprecated": false,
"used_by": [
{ "kind": "risk.rule", "ref": "policy:TN-BANQUEX@2.1#dimension=client#rule=3" },
{ "kind": "risk.rule", "ref": "policy:TN-BANQUEX@2.1#dimension=client#rule=5" },
{ "kind": "aml.rule", "ref": "txmon-policy:TN-BANQUEX@1.0#rule=cash_intensive_profession" }
]
},
{
"path": "client.country",
"type": "iso3166-alpha2",
"source": "form:FORM_KYC_INDIVIDUAL@v2.7",
"required": true,
"sensitivity": "PII",
"deprecated": false,
"used_by": [
{ "kind": "risk.rule", "ref": "policy:TN-BANQUEX@2.1#dimension=geo" },
{ "kind": "risk.override", "ref": "policy:TN-BANQUEX@2.1#mustProhibit=2" }
]
},
{
"path": "screening.ofacMatch",
"type": "enum",
"enum_catalog": "SCREENING_OUTCOME",
"source": "screening:worldcheck+opensanctions",
"required": false,
"sensitivity": "INTERNAL",
"deprecated": false,
"used_by": [
{ "kind": "risk.override", "ref": "policy:TN-BANQUEX@2.1#mustProhibit=1" }
]
},
{
"path": "client.homeowner_status",
"type": "boolean",
"source": "form:FORM_KYC_INDIVIDUAL@v2.6",
"required": false,
"sensitivity": "PII",
"deprecated": true,
"deprecated_at": "2026-03-15T00:00:00Z",
"deprecated_reason": "Remplacé par client.housing_type (plus granulaire). Retrait prévu 2026-06-15.",
"used_by": []
}
]
}

ComposantChoixRationale
ServiceKotlin Spring BootCohérent avec le stack VitaKYC
StockagePostgreSQL JSONB append-onlyVersioning naturel, requêtes riches, compatible on-prem Oracle via couche d’abstraction
MessagingKafka topics cps.source-update.{tenantId} (in) + cps.updated.{tenantId} (out)Réutilise infra existante, garantit livraison, ordering par tenant
Cache consommateurCaffeine TTL 30s + invalidation sur event KafkaLecture locale hot path Risk Matrix, pas de DB hit par évaluation
ValidationJSON Schema 2020-12 (version minimale) + extensions customStandard, outillé, bibliothèque Kotlin stable
APIREST + JSONSimple, cacheable via CDN interne, compatible dev tools
SécuritémTLS interne + Row-Level Security PostgreSQLADR-002

4.2 Scénario protégé : suppression d’un champ utilisé

Section intitulée « 4.2 Scénario protégé : suppression d’un champ utilisé »

4.3 Scénario bloqué : règle référant un symbole inexistant

Section intitulée « 4.3 Scénario bloqué : règle référant un symbole inexistant »
POST /v1/risk/policies/{id}/publish
{ rules: [ { score: 80, predicate: "client.homeowner_status > 0" } ] }
→ HTTP 400
{
"error": "CPS_SYMBOL_UNKNOWN",
"message": "La variable 'client.homeowner_status' n'existe pas dans le CPS du tenant TN-BANQUEX.",
"suggestions": ["client.housing_type", "client.home_owner"],
"hint": "Un champ similaire a été déprécié le 2026-03-15. Utilisez client.housing_type."
}
  • ACTIVE : lisible, proposable, référençable
  • DEPRECATED : toujours lisible mais plus proposé dans autocomplete, warning dans Risk Matrix editor
  • RETIRED : plus lisible, rupture — mais uniquement après fenêtre + vérif consommateurs

GET /v1/cps/tenant/TN-BANQUEX
Authorization: Bearer <service-token>
→ 200 OK
{
"tenant_id": "TN-BANQUEX",
"version": "2026-04-23T11:42:00Z",
"hash": "sha256:e3b0...",
"variables": [ ... ]
}
GET /v1/cps/tenant/TN-BANQUEX/variable/client.profession
→ 200 OK
{
"path": "client.profession",
"type": "enum",
"enum_catalog": "BCT_PROFESSION_CODES",
"source": "form:FORM_KYC_INDIVIDUAL@v2.7",
"deprecated": false,
"used_by": [ ... ]
}
GET /v1/cps/tenant/TN-BANQUEX/variable/client.profession/history
→ 200 OK
{
"path": "client.profession",
"timeline": [
{ "version": "2026-01-15T...", "change": "created", "source": "form:FORM_KYC_INDIVIDUAL@v2.5" },
{ "version": "2026-03-10T...", "change": "type_changed", "from": "string", "to": "enum" },
{ "version": "2026-04-23T...", "change": "used_by_added", "ref": "policy:..." }
]
}

5.4 Vérifier l’existence d’un symbole (batch)

Section intitulée « 5.4 Vérifier l’existence d’un symbole (batch) »
POST /v1/cps/tenant/TN-BANQUEX/validate-symbols
{ "symbols": ["client.profession", "client.foo_bar"] }
200 OK
{
"valid": ["client.profession"],
"invalid": [
{ "symbol": "client.foo_bar", "suggestions": ["client.foo_type", "client.bar_flag"] }
]
}
PATCH /v1/cps/tenant/TN-BANQUEX/variable/client.homeowner_status
Authorization: Bearer <admin-token>
{ "deprecated": true, "deprecated_reason": "Remplacé par client.housing_type", "window_days": 90 }
200 OK
POST /v1/cps/internal/declare
Authorization: mTLS client cert
X-Source-Service: form-designer
{
"tenant_id": "TN-BANQUEX",
"source": "form:FORM_KYC_INDIVIDUAL@v2.7",
"variables": [
{ "path": "client.profession", "type": "enum", "enum_catalog": "BCT_PROFESSION_CODES", ... }
]
}
202 Accepted (traité de manière asynchrone)

6.1 Événements consommés (cps.source-update.{tenantId})

Section intitulée « 6.1 Événements consommés (cps.source-update.{tenantId}) »
EventÉmetteurPayload clé
form.publishedForm DesignerformId, version, addedFields[], removedFields[], renamedFields[]
screening.enabledModule screeningproviderName, variablesDeclared[]
rne.connectedConnecteur RNEvariablesDeclared[] (ex: entity.ubo.anyPep)
txmon.enabledAML TxMonvariablesDeclared[] (ex: transactional.velocity_30d)
product-catalog.updatedBackend banqueproductCodes[], variablesDeclared[]
EventPayload
cps.variable.addedpath, type, source, sensitivity
cps.variable.deprecatedpath, deprecatedAt, windowDays, reason
cps.variable.retiredpath, retiredAt
cps.variable.used_by_changedpath, addedConsumers[], removedConsumers[]
cps.schema.publishedversion, hash, addedCount, deprecatedCount, retiredCount

Les consommateurs (Risk Matrix editor, AML TxMon editor, reporting) écoutent ces events pour invalider leur cache et notifier leurs users.


RègleEnforcement
Un chemin (path) est unique dans le schéma d’un tenantContrainte unique PostgreSQL
sensitivity=PII ou SPI → jamais loggué en clairIntercepteur Logback + regex masking
Pas de cross-tenant referenceRow-Level Security PostgreSQL, tenant_id propagé en MDC
Deprecate → window 90j minimum avant retirementValidation applicative
Retirement → interdit si usedBy non videValidation applicative
Rename (path change)Non autorisé : crée new_path + deprecate old_path — l’admin doit migrer manuellement les consommateurs
Type changeAutorisé uniquement si backward-compatible (ex: enum → enum plus large). Sinon : nouveau path + deprecate.
Source unique par variableUne variable a une seule source émettrice (pas de race condition)

MétriqueTargetComment atteint
GET /v1/cps/tenant/{id} p99≤ 10 msCache L1 Caffeine côté consommateur + CDN interne
Propagation source → consommateur≤ 2 sKafka + invalidation Caffeine async
Taille CPS / tenant~ 50 KB typique, max 500 KBPetit contrat, peu de variables (30-150 typique)
Throughput writes10 req/s par tenantLes sources ne publient pas à haute vélocité (form publish, rare)
Stockage PostgreSQL< 1 GB / 100 tenants / 5 ansJSONB compressé + partitionnement par tenant

  • Pas de PII dans le CPS : seulement des métadonnées (nom de variable, type, source, sensibilité). Jamais de valeurs.
  • mTLS interne entre les services (déclaration + lecture privilégiée)
  • RBAC côté API publique :
    • cps.read : tous les services du tenant
    • cps.admin : compliance officer senior + DSI
    • cps.deprecate : compliance officer senior + DSI
    • cps.source.declare : service-to-service (mTLS cert whitelist)
  • Row-Level Security : tenant_id propagé systématiquement
  • Audit WORM : chaque changement (ajout, deprecate, retire) archivé 10 ans avec hash chain SHA-256
  • Intégrité : chaque version du schéma a un hash SHA-256 + chaîne avec version précédente → vérification au chargement par chaque consommateur

  • Déclare les champs à la publication (form.published event)
  • Affiche le badge “Utilisé par N règles de risque” en consultant GET /v1/cps/tenant/{id}/variable/{path}
  • Refuse la suppression d’un champ avec usedBy.length > 0
  • Propose la déprécation en cliquant “Deprecate 90j”
  • Consomme le CPS au chargement de l’éditeur de policy (cache L1)
  • Autocomplete les chemins à partir du CPS (client., screening., etc.)
  • Valide à la sauvegarde / publication : refus serveur HTTP 400 si symbole inexistant
  • Affiche “source originelle : form:FORM_KYC_INDIVIDUAL@v2.7” sur chaque règle
  • Écoute cps.variable.deprecated → warning dans policies utilisatrices
  • Bloque publication d’une policy référant un champ RETIRED
  • Déclare les variables transactionnelles (transactional.velocity_30d, transactional.cash_intensity)
  • Consomme comme Risk Matrix pour ses règles temps réel
  • Utilise le CPS comme field picker dans les builders de rapports
  • Attention sensitivity : masque automatiquement PII / SPI sauf rôle spécifique

Pour un tenant déjà en prod sans CPS, migration en 3 étapes :

  1. Discovery — inspecte les policies Risk Matrix existantes et les forms Form Designer, produit un CPS initial.
  2. Validation manuelle — compliance officer et admin produit valident le CPS reconstitué (fenêtre 30j).
  3. Go-live — active les validations strictes (refus HTTP 400 si symbole inexistant). Rollback possible pendant 90j.

  • profile-schema-svc déployé (staging + prod)
  • Kafka topics créés par tenant (cps.source-update.{id}, cps.updated.{id})
  • Form Designer wire events form.published
  • Risk Matrix UI wire autocomplete depuis CPS
  • Risk Matrix API wire validation stricte
  • Cache Caffeine configuré côté consommateurs
  • Audit WORM configuré (retention 10 ans)
  • Monitoring : latence p99 API, lag Kafka, drift schéma
  • Alertes : symbole inexistant référencé, tentative retire avec consommateurs
  • Training compliance officers (1h) : concept CPS + workflow dépréciation
  • Training admin tenants (30 min) : mapping CPS obligatoire sur Form Designer
  • Exercise incident : suppression champ utilisé → vérification blocage + workflow correct
  • Export audit pour BCT : export du CPS au format signé JSON