POC · Risk Engine (Kotlin)
Valide : ADR-025 (modèle de risque client), ADR-004 (DSL Kotlin). Statut : 16/16 tests passent, démo CLI fonctionnelle, prêt à être industrialisé dans
risk-svc.
Ce POC Kotlin démontre le cœur du Risk Engine VitaKYC : un DSL type-safe pour encoder une politique de risque multi-dimensionnelle, un évaluateur qui produit un JSON d’explainability auditable, et 16 tests couvrant les profils BCT typiques.
Source code : /poc-risk-engine/ dans le repo VitaKYC.
1. Lancer le POC
Section intitulée « 1. Lancer le POC »cd poc-risk-engine./gradlew test # suite complète 16 tests./gradlew run # évalue 5 profils BCT types, dump JSON explain2. Ce que le POC démontre
Section intitulée « 2. Ce que le POC démontre »2.1 DSL Kotlin type-safe
Section intitulée « 2.1 DSL Kotlin type-safe »val policy = riskPolicy("TN-BANQUEX", version = "2.1") {
metadata { owner = "compliance-officer@banquex.tn" description = "Politique LCB-FT 2026 — conforme BCT Circ. 2017-08 + 6AMLD" effectiveFrom = LocalDate.of(2026, 5, 1) reviewBy = LocalDate.of(2027, 5, 1) }
dimension("client", weight = 0.30) { score(100, "client.pep == OWN") whenMatch { it.client.pep == PepStatus.OWN } score(75, "PEP family or close assoc") whenMatch { it.client.pep in setOf(PepStatus.FAMILY, PepStatus.CLOSE_ASSOC) } score(50, "profession Wolfsberg high-risk") whenMatch { it.client.profession in Lists.HIGH_RISK_PROFESSIONS } score(30, "non-resident") whenMatch { it.client.residentStatus == ResidentStatus.NON_RESIDENT } score(0, "default").otherwise() }
// ... geo, product, channel, aml_screening
threshold(RiskLevel.LOW, RiskLevel.STANDARD, at = 25) threshold(RiskLevel.STANDARD, RiskLevel.HIGH, at = 50) threshold(RiskLevel.HIGH, RiskLevel.PROHIBITED, at = 80)
mustProhibit whenAny { predicate("OFAC true positive") { it.screening.ofacMatch == ScreeningOutcome.TRUE_POSITIVE } predicate("KP/IR") { it.client.country in setOf("KP", "IR") } predicate("minor for adult product") { it.client.minor && !it.product.allowsMinor } }
mustHigh whenAny { predicate("PEP OWN") { it.client.pep == PepStatus.OWN } predicate("legal rep with UBO PEP") { it.client.isLegalRep && it.entity.uboAnyPep } }}2.2 Invariants enforcés à la construction
Section intitulée « 2.2 Invariants enforcés à la construction »| Invariant | Mécanisme |
|---|---|
| Somme des poids = 1.0 (± 0.001) | require dans PolicyBuilder.build() |
| Thresholds strictement monotones | lowStd < stdHigh < highProh vérifié |
| Chaque dimension a au moins une règle | require dans DimensionBuilder.build() |
| Évaluation cross-tenant refusée | require(ctx.tenantId == policy.tenantId) dans l’evaluator |
2.3 Sortie auditable (RiskEvaluation)
Section intitulée « 2.3 Sortie auditable (RiskEvaluation) »{ "evaluation_id": "revl_5f06292a-904", "tenant_id": "TN-BANQUEX", "subject_id": "cli_03", "subject_kind": "CLIENT_INDIVIDUAL", "policy_version": "TN-BANQUEX@2.1", "evaluator_version": "risk-engine@1.4.2", "evaluated_at": "2026-04-23T12:57:24.194690941Z", "decision": "HIGH", "global_score": 59.0, "level": "HIGH", "path_taken": "score-based", "dimensions": [ { "name": "client", "score": 75.0, "weight": 0.30, "contribution": 22.5, "reasons": ["PEP family or close assoc"] }, { "name": "geo", "score": 70.0, "weight": 0.25, "contribution": 17.5, "reasons": ["client.country in FATF_GREYLIST (BCT 2025-10)"] }, { "name": "product", "score": 25.0, "weight": 0.20, "contribution": 5.0, "reasons": ["other products"] }, { "name": "channel", "score": 60.0, "weight": 0.15, "contribution": 9.0, "reasons": ["REMOTE_SELFIE_ONLY (no QES)"] }, { "name": "aml_screening", "score": 50.0, "weight": 0.10, "contribution": 5.0, "reasons": ["PEP confirmed by screening"] } ], "overrides_triggered": [], "recommended_action": "EDD", "required_mitigations": [ "Demander justificatif origine des fonds", "Entretien visio VideoKYC obligatoire", "Approbation 4-eyes (agent + superviseur)", "Revue annuelle", "SLA STR J+5" ], "list_refs": { "fatf_greylist_version": "2025-10", "ofac_version": "2026-04-22", "transparency_cpi_year": "2025" }}3. Suite de tests (16 tests passants)
Section intitulée « 3. Suite de tests (16 tests passants) »Risk Engine — profils BCT typiques ✓ Résident TN, salarié, compte courant, face-à-face → LOW (auto-approve) ✓ Non-résident FR, compte devise remote selfie → STANDARD ✓ PEP family + greylist + lawyer + remote → HIGH (EDD requise) ✓ mustHigh override — client PEP OWN → HIGH même si score bas ✓ OFAC match → PROHIBITED (override, path=override:mustProhibit) ✓ Country KP (Corée du Nord) → PROHIBITED ✓ Mineur pour produit non-mineur → PROHIBITED via override ✓ Mineur pour COMPTE_JEUNE (allowsMinor=true) → LOW ✓ L'évaluation produit toutes les 5 dimensions avec contributions ✓ L'évaluation porte la version de policy et de l'evaluator ✓ Chaque évaluation a un ID unique (50 évaluations, 50 IDs distincts) ✓ Sanity perf : 1000 évaluations < 500 ms Invariants DSL ✓ Somme des weights != 1.0 → erreur à la construction ✓ Thresholds non monotones → erreur ✓ Dimension sans score() → erreur ✓ Cross-tenant evaluation blocked
BUILD SUCCESSFUL16 tests passed4. Démo CLI — 5 profils BCT
Section intitulée « 4. Démo CLI — 5 profils BCT »▶ 1. Résident TN standard, salarié, compte courant, face-à-face Decision: LOW | Global score: 11,0 | Path: score-based Recommended action: AUTO_APPROVE Mitigations: Revue 5 ans, Auto-approve autorisé
▶ 2. Non-résident FR, compte devise gros montant, remote selfie Decision: STANDARD | Global score: 30,0 | Path: score-based Recommended action: CDD Mitigations: CDD standard, Justificatif domicile, Revue 3 ans, SLA STR J+10
▶ 3. PEP famille, profession notary, country greylist Decision: HIGH | Global score: 59,0 | Path: score-based Recommended action: EDD Mitigations: Demander justificatif origine des fonds, Entretien visio VideoKYC, Approbation 4-eyes, Revue annuelle, SLA STR J+5
▶ 4. OFAC match → must PROHIBITED quel que soit le score Decision: PROHIBITED | Global score: 21,0 | Path: override:mustProhibit Recommended action: REFUSE Overrides: mustProhibit: OFAC true positive → refuse Mitigations: Refus automatique, Alerte compliance officer, STR à évaluer sous 48h, Gel d'avoirs si match sanctions, SLA STR J+2
▶ 5. Mineur pour produit compte-jeune (allowsMinor=true) Decision: LOW | Global score: 7,0 | Path: score-based Recommended action: AUTO_APPROVELa sortie montre bien :
- Le path traçable (
score-basedvsoverride:mustProhibit) - Les contributions dimensionnelles (client, geo, product, channel, aml)
- Les overrides déclenchés avec leur raison
- Les mitigations à appliquer selon le niveau
- La reproductibilité via
listRefs(snapshots FATF/OFAC/CPI utilisés)
5. Architecture du code
Section intitulée « 5. Architecture du code »poc-risk-engine/├── build.gradle.kts — Kotlin 2.0 + kotlinx.serialization + JUnit 5├── settings.gradle.kts├── README.md└── src/ ├── main/kotlin/io/vitakyc/risk/ │ ├── Model.kt — data classes (contexts, RiskEvaluation, Lists FATF/OFAC/CPI) │ ├── Dsl.kt — PolicyBuilder, DimensionBuilder, OverrideGroupBuilder, invariants │ ├── Evaluator.kt — exécution policy → RiskEvaluation (path, overrides, mitigations) │ ├── Policies.kt — template policy BCT TN-BANQUEX v2.1 │ └── Main.kt — CLI démo 5 profils └── test/kotlin/io/vitakyc/risk/ └── EvaluatorTest.kt — suite JUnit 5 + AssertJ (16 tests)5.1 Séparation des responsabilités
Section intitulée « 5.1 Séparation des responsabilités »- Model.kt : données du domaine sérialisables (kotlinx.serialization), pas de logique métier. Inclut les snapshots des listes (
Lists.FATF_BLACKLIST,Lists.HIGH_RISK_PROFESSIONS, etc.). - Dsl.kt : type-safe builders idiomatic Kotlin (receiver-less lambdas, immutability au build). Invariants cristallisés via
require(...). - Evaluator.kt : pure function
(RiskPolicy, EvaluationContext) -> RiskEvaluation, pas de side effects, testable trivialement. - Policies.kt : catalogue de templates (BCT TN, bientôt EU 6AMLD, Fintech, Insurance). Le MVP livre les 4 templates de l’ADR-025.
- Main.kt : démo CLI, pas utilisée en prod.
5.2 Choix techniques
Section intitulée « 5.2 Choix techniques »| Choix | Justification |
|---|---|
| Kotlin 2.0 + JVM 17 | Conforme ADR-001/ADR-004 + LTS JDK |
| kotlinx.serialization | JSON natif Kotlin, pas de réflexion, rapide |
| Pas de dépendance externe OPA | POC autonome. OPA sera wiré dans risk-svc pour règles inter-modules complexes (cf. ADR-004). |
| Pas de Spring | POC pur, isolé. Intégration Spring Boot dans risk-svc. |
| JUnit 5 + AssertJ | Cohérent avec les 5 autres POCs |
6. Ce que le POC ne fait PAS (scope risk-svc)
Section intitulée « 6. Ce que le POC ne fait PAS (scope risk-svc) »Volontairement hors-scope POC :
- Persistance PostgreSQL JSONB (policies, evaluations WORM)
- API REST / gRPC exposée
- Shadow mode (double évaluation parallèle)
- Backtesting Temporal workflow
- Dual control signatures Vault Ed25519
- UI visuelle (workflow 10 mockups)
- Cache Caffeine pour listes FATF/OFAC avec refresh Kafka
- Observabilité Micrometer / OpenTelemetry
- ML complémentaire (scoring adverse media, anomaly detection)
Tous ces points sont documentés dans la page engineering Risk Engine.
7. Prochaines étapes
Section intitulée « 7. Prochaines étapes »- Sprint S02-S04 — monter
risk-svcautour de ce POC (Spring Boot + persistence + API REST) - S05 — ajouter backtesting Temporal workflow
- S06 — ajouter shadow mode + dual control Vault
- S08 — wire UI admin (workflow 10)
- S10 — pilote avec premier tenant bancaire TN
- M+12 — revue policy + extension 6ème dimension
transactionalpour AML TxMon
8. Références
Section intitulée « 8. Références »- Source code :
/poc-risk-engine/dans le repo VitaKYC - ADR-025 — Modèle de risque client
- Risk Engine — architecture complète
- Playbook BCT — calibrer la matrice
- Maquettes UI workflow 10 — Admin Risk Matrix
- Autres POCs : FATCA, RNE, goAML, Tx Normalizer, Temenos