Aller au contenu

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.


Fenêtre de terminal
cd poc-risk-engine
./gradlew test # suite complète 16 tests
./gradlew run # évalue 5 profils BCT types, dump JSON explain
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 }
}
}
InvariantMécanisme
Somme des poids = 1.0 (± 0.001)require dans PolicyBuilder.build()
Thresholds strictement monotoneslowStd < stdHigh < highProh vérifié
Chaque dimension a au moins une règlerequire dans DimensionBuilder.build()
Évaluation cross-tenant refuséerequire(ctx.tenantId == policy.tenantId) dans l’evaluator
{
"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"
}
}

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 SUCCESSFUL
16 tests passed
▶ 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_APPROVE

La sortie montre bien :

  • Le path traçable (score-based vs override: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)

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)
  • 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.
ChoixJustification
Kotlin 2.0 + JVM 17Conforme ADR-001/ADR-004 + LTS JDK
kotlinx.serializationJSON natif Kotlin, pas de réflexion, rapide
Pas de dépendance externe OPAPOC autonome. OPA sera wiré dans risk-svc pour règles inter-modules complexes (cf. ADR-004).
Pas de SpringPOC pur, isolé. Intégration Spring Boot dans risk-svc.
JUnit 5 + AssertJCohérent avec les 5 autres POCs

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.


  1. Sprint S02-S04 — monter risk-svc autour de ce POC (Spring Boot + persistence + API REST)
  2. S05 — ajouter backtesting Temporal workflow
  3. S06 — ajouter shadow mode + dual control Vault
  4. S08 — wire UI admin (workflow 10)
  5. S10 — pilote avec premier tenant bancaire TN
  6. M+12 — revue policy + extension 6ème dimension transactional pour AML TxMon