POC : poc-case-mgmt/ (~700 lignes Kotlin pur, 0 dépendance externe runtime). Référence pour case-mgmt-svc MVP.
Status : 18/18 tests passants. Démo CLI 10 étapes.
Ce POC démontre les invariants critiques du module Case Management en code exécutable :
- State machine déterministe — 10 états, transitions strictement énumérées, illegal-transition rejetée
- Router skill-based scoré — algorithme déterministe + fairness round-robin sur égalité
- 4-eyes engine — détection auto des cas sensibles + validation contrainte signataire
- SLA engine — calcul rungs croisés + breach detection avec gestion paused-time
- Audit trail append-only — chaînage hash SHA-256 + verify-chain end-to-end
├── build.gradle.kts // Kotlin 2.0, kotlinx.serialization, JUnit 5
├── src/main/kotlin/io/vitakyc/cases/
│ ├── Model.kt // Case, AgentProfile, FourEyesPolicy, errors typés
│ ├── StateMachine.kt // transitions énumérées, assertTransition
│ ├── Router.kt // scoring + filters + tie-break round-robin
│ ├── FourEyes.kt // requiresSecondSignature + assertCanConfirm
│ ├── SlaEngine.kt // rungsCrossed, isBreached, deadline calc
│ ├── AuditTrail.kt // append-only + chainage SHA-256 + verify
│ ├── CaseService.kt // orchestration end-to-end
│ └── Main.kt // démo CLI 10 étapes
└── src/test/kotlin/io/vitakyc/cases/
└── CaseTest.kt // 18 tests
| Test | Vérifie |
|---|
state machine — happy path NEW to CLOSED | parcours nominal complet avec 4-eyes |
state machine — illegal transition rejected | claim sans assignment → IllegalTransition |
state machine — pause and resume | IN_REVIEW ↔ PAUSED |
state machine — closed is terminal, no further transitions | tout depuis CLOSED jette |
| Test | Vérifie |
|---|
router — selects best skill match within tenant | scoring privilégie skill match exact |
router — refuses cross-tenant agent | filter tenantId obligatoire |
router — fairness round-robin on equal scores | tie-break par lastAssignedAt le plus ancien |
router — no eligible agent throws | NoEligibleAgent si aucun candidat |
router — respects max load | filter currentLoad < maxLoad |
| Test | Vérifie |
|---|
4-eyes — required for PEP | confirmer ≠ proposer enforcé |
4-eyes — confirmer must be L2 or MLRO | seniority constraint |
4-eyes — required for high transaction amount | seuil tenant (100 000 par défaut) |
4-eyes — required on rejection | toute REJECTED requiert 2 sig |
4-eyes — not required for low-risk approved single signature | path single-sig OK |
| Test | Vérifie |
|---|
audit — chain hashes verifiable | verifyChain retourne null après workflow nominal |
audit — first event has zero previous hash, others chain forward | chainage explicite |
| Test | Vérifie |
|---|
SLA — STANDARD case 24h deadline, breached at 25h | détection breach + rungs NOTIFY_AGENT, NOTIFY_MANAGER |
SLA — paused time excluded from breach calculation | 25h - 5h paused = 20h, pas breach |
reassignment — limited to 2 times | 3e tentative jette ReassignmentLimitReached |
escalation — bumps min seniority to L2 on first escalation | router suivant exigera L2+ |
=== VitaKYC POC Case Management — démo ===
[1] Ingest case STANDARD retail PEP
[2] Router assigne au top-1
→ assigné à agent_amine state=ASSIGNED
[3] Agent claim → IN_REVIEW
[4] Agent propose APPROVED
→ state=DECISION_PENDING, proposedBy=agent_amine
[5] 4-eyes requis (PEP) → second agent confirme
→ finalDecision=APPROVED, confirmedBy=agent_leila
[7] Audit trail du case 1 — chaîne de hashes vérifiable
CREATED actor=ingest hash=sha256:227b7ed508a53…
ASSIGNED actor=router hash=sha256:7df212543f1da…
CLAIMED actor=agent_amine hash=sha256:4c0cdafaddf8c…
PROPOSED_DECISION actor=agent_amine hash=sha256:d453366783093…
CONFIRMED_DECISION actor=agent_leila hash=sha256:535f112a8ecde…
DECIDED actor=engine hash=sha256:7cb66fb662967…
CLOSED actor=engine hash=sha256:0f8685fd9c030…
[8] Tentative 4-eyes par le même agent → REJET
✅ rejeté : 4-eyes violation: same agent cannot propose and confirm
[9] Cross-tenant isolation : case TN-BANQUEX ne peut être pris par agent FR-BANQUEY
→ case TN assigné à agent_amine (jamais agent_other_tenant)
✅ isolation tenant respectée
[10] SLA breach — case STANDARD non décidé après 24 h
rungs crossed à 25h : [NOTIFY_AGENT, NOTIFY_MANAGER]
- Signatures Ed25519 réelles — le POC chaîne les hashes ; la prod ajoute la signature par acteur via Vault HSM.
- Workflow Temporal
CaseSlaWorkflow — le POC évalue SLA en synchronie ; la prod utilise Temporal pour exactly-once des escalades.
- PostgreSQL append-only avec RLS — le POC stocke en mémoire ; la prod persiste avec contraintes Postgres et policies RLS multi-tenant.
- Bulk operations MLRO — pas implémentées (transactions multi-cases avec audit individuel).
- Search full-text — pas implémentée (en prod via OpenSearch ICU).
- Notifications email + Kafka — pas implémentées (en prod émis via outbox pattern).
- OIDC + MFA agent — pas implémentés (auth simulée par String).
./gradlew test # 18/18 tests verts
./gradlew run # démo CLI 10 étapes
Dépendances : Kotlin 2.0.20, JVM 17, kotlinx-serialization-json 1.7.3, JUnit 5.11, AssertJ 3.26. Aucune dépendance runtime externe.