NOTE TECHNIQUE — Moteur de calcul DBO¶
Architecture et spécification — Plateforme ActuaryLab¶
Projet : ActuaryLab — Plateforme Actuarielle BFEV
Version : 1.0 — Mars 2026
Destinataire : Cursor (implémentation technique)
Statut : Document de référence — Confidentiel
Références : NOTE_TECHNIQUE_REGLES_PRESTATIONS.md, inventaire_actuariat_lib.md, RAPPORT_PHASE2/3/4_ACTUARYLAB.md
Table des matières¶
- Objet et périmètre
- Principes d'architecture
- Entrées du moteur
- Sorties du moteur
- Spécification par famille de règle
- Flux d'exécution — Orchestration
- Extension du moteur — Nouvelles familles
- Annexes
1. Objet et périmètre¶
1.1 Objet¶
Ce document décrit l'architecture et la spécification du moteur de calcul DBO d'ActuaryLab : la couche logicielle qui, à partir d'une population de salariés, d'un jeu d'hypothèses actuarielles et d'une règle de prestation, produit la dette d'avantages post-emploi (Defined Benefit Obligation — DBO) par salarié et les agrégats associés.
1.2 Périmètre¶
Dans le périmètre :
- Calcul en lot (population de N salariés, \(N\) pouvant aller de 1 à plusieurs milliers)
- Une règle de prestation par évaluation (une
EvaluationDjango référence exactement une règle) - Toutes les familles de règles identifiées dans la base documentaire ActuaryLab :
GRILLE_ANCIENNETE,TAUX_REMPLACEMENT,CAPITAL_ACCUMULE,MONTANT_FIXE,CAPITAL_MILLESIMES,ALEATOIRE - Sorties : DBO individuelle par salarié, agrégats (total, répartitions), indicateurs intermédiaires (prestation projetée, probabilité de maintien, facteur d'actualisation)
- Persistance des résultats dans les modèles Django
ResultatDBO,ResultatDBOParSalarie,ResultatAggrege
Hors périmètre (documenté mais non spécifié ici) :
- Calcul individuel hors lot (simulation ponctuelle pour un seul salarié, sans persistance)
- Règles hybrides multi-composantes dans leur détail (cf.
RegleHybridedans la note règles de prestations) - Décomposition IAS 19 complète en waterfall (CSR, IC, réévaluations) — couverte par
F-ENG-006àF-ENG-010de l'inventaireactuariat_lib, mais hors de la séquence de calcul principale décrite ici
2. Principes d'architecture¶
2.1 Séparation des responsabilités¶
Le moteur s'articule en trois couches strictement séparées. Aucune couche ne doit empiéter sur les responsabilités de la suivante.
┌─────────────────────────────────────────────────────────┐
│ COUCHE DJANGO — Orchestration et persistance │
│ apps/evaluations/services/evaluation_service.py │
│ apps/evaluations/tasks.py (Celery) │
│ Responsabilités : cycle de vie de l'évaluation, │
│ vérifications, transactions BDD, gestion des erreurs │
├─────────────────────────────────────────────────────────┤
│ COUCHE BRIDGE — Traduction Django ↔ lib │
│ apps/evaluations/services/bridge.py │
│ Responsabilités : transformer les modèles Django │
│ (Evaluation, HypothesesActuarielles, Salarie queryset) │
│ en objets purs de la bibliothèque (ContexteCalcul, │
│ règle concrète RegleXxx, DataFrames NumPy) │
├─────────────────────────────────────────────────────────┤
│ COUCHE BIBLIOTHÈQUE — Calculs actuariels purs │
│ actuariat_lib/ │
│ Responsabilités : mathématiques actuarielles, sans │
│ aucune dépendance Django, testable indépendamment │
└─────────────────────────────────────────────────────────┘
Règle d'or : actuariat_lib ne doit jamais importer quoi que ce soit de Django. Le bridge est le seul point de contact entre les deux mondes.
2.2 Dispatcher par famille¶
Le moteur ne contient pas de logique if/elif sur le type de produit. Il dispatche vers un sous-moteur spécialisé selon la propriété famille de la règle concrète, dérivée du type_regime Django :
# apps/evaluations/services/evaluation_service.py
DISPATCHER = {
'GRILLE_ANCIENNETE': _dbo_via_puc,
'TAUX_REMPLACEMENT': _dbo_rente_differee,
'CAPITAL_ACCUMULE': _dbo_capital_accumule,
'MONTANT_FIXE': _dbo_montant_fixe,
'CAPITAL_MILLESIMES': _dbo_millesimes,
'ALEATOIRE': _dbo_esperance_actuarielle,
}
# Familles actuellement implémentées en lot
FAMILLES_CALCUL_IMPLEMENTE = {'GRILLE_ANCIENNETE'}
L'ajout d'une nouvelle famille ne nécessite que : (1) une nouvelle entrée dans DISPATCHER, (2) l'implémentation du sous-moteur correspondant. Aucune modification des sous-moteurs existants.
2.3 Contexte de calcul unifié¶
Toute l'information nécessaire à un calcul est encapsulée dans ContexteCalcul (défini dans actuariat_lib/prestations/base.py). Le bridge construit ce contexte avant l'appel au sous-moteur.
@dataclass
class ContexteCalcul:
# Données salarié (vecteurs NumPy ou scalaires selon le mode)
age: float # Âge exact à la date d'évaluation
anciennete: float # Ancienneté exacte (années)
salaire: float # Salaire de référence courant (FCFA/mois)
date_evaluation: date
# Hypothèses communes
age_retraite: float = 60.0
taux_revalorisation_salaire: float = 0.035
taux_actualisation: float = 0.06
taux_charges_sociales: float = 0.0
# Données optionnelles (selon famille)
capital_acquis: float = 0.0 # DC / Épargne
millesimes: list = field(default_factory=list) # CAPITAL_MILLESIMES
donnees_financieres: dict = field(default_factory=dict) # ALEATOIRE
id_salarie: Optional[str] = None # Pour traçabilité
2.4 Résultat unifié¶
Chaque sous-moteur retourne un objet ResultatPrestation standardisé (quelle que soit la famille), que le moteur principal persiste ensuite en base.
@dataclass
class ResultatPrestation:
prestation_projetee: float # Montant brut projeté à l'horizon (FCFA)
base_calcul: float # Salaire ou capital de référence utilisé
horizon_annees: float # Durée jusqu'au versement
mode_versement: str # 'capital_unique' | 'rente_mensuelle' | 'mixte'
probabilite: float = 1.0 # Pour prestations aléatoires
detail: Dict[str, Any] = field(default_factory=dict)
3. Entrées du moteur¶
3.1 Côté Django — Entrées du service¶
Le moteur reçoit un evaluation_id et charge lui-même ses dépendances.
| Objet Django | Modèle | Champs clés utilisés |
|---|---|---|
Evaluation |
apps/evaluations/models.py |
type_produit, date_evaluation, mode_depart, methode (PUC/EAN), hypotheses (FK), grille_ifc (FK), statut, celery_task_id |
HypothesesActuarielles |
apps/evaluations/models.py |
taux_actualisation, taux_revalorisation_salaire, taux_charges_sociales, age_retraite, table_mortalite (FK), table_rotation (FK) |
GrilleIFC |
apps/referentiels/models.py |
paliers (JSON) — tranches d'ancienneté et taux |
TableMortalite |
apps/referentiels/models.py |
donnees (JSON) — valeurs \(q_x\) |
TableRotation |
apps/referentiels/models.py |
donnees (JSON) — taux de rotation par âge |
Salarie (queryset) |
apps/population/models.py |
Voir section 3.3 |
3.2 Règle effective — Résolution de priorité¶
La règle de prestation résulte d'une hiérarchie de priorité : si l'évaluation surcharge certains paramètres, ils priment sur les hypothèses par défaut de l'organisation.
Priorité (1 = plus haute) :
1. Paramètres portés directement par Evaluation (ex: mode_depart, grille_ifc_surcharge)
2. Paramètres du jeu HypothesesActuarielles lié à l'Evaluation
3. Hypothèses par défaut de l'organisation (est_hypothese_defaut=True)
Le bridge regle_prestation_django_to_lib() applique cette résolution et instancie la classe concrète appropriée (RegleIFC, RegleRetraiteDB, etc.).
3.3 Champs population requis par type de produit¶
Les champs marqués [O] sont obligatoires (l'absence bloque le calcul). Les champs [R] sont recommandés (présence améliore la précision). Les champs [F] sont facultatifs.
Champ Salarie |
IFC | Retraite DB | Retraite DC | Médaille | Épargne | Décès |
|---|---|---|---|---|---|---|
matricule |
O | O | O | O | O | O |
age_exact |
O | O | O | O | O | O |
anciennete_exacte |
O | O | O | O | O | F |
salaire_brut_mensuel |
O | O | O | O | O | O |
sexe |
R | R | R | F | F | F |
categorie |
R | R | R | F | F | F |
date_naissance |
R | R | R | F | F | F |
date_entree |
R | R | R | O | O | F |
capital_acquis |
— | — | R | — | R | — |
millesimes (JSON) |
— | — | — | — | O | — |
Note :
age_exactetanciennete_exactesont calculés à l'import (Phase 4) viaactuariat_lib.utils.calculer_age_exact()etcalculer_anciennete_exacte(). Le moteur les utilise directement sans recalcul.
3.4 Hypothèses obligatoires par famille¶
| Hypothèse | GRILLE_ANCIENNETE | TAUX_REMPLACEMENT | CAPITAL_ACCUMULE | MONTANT_FIXE | CAPITAL_MILLESIMES |
|---|---|---|---|---|---|
taux_actualisation |
O | O | O | O | O |
taux_revalorisation_salaire |
O | O | O | F | F |
table_mortalite |
O | O | O | O | O |
table_rotation |
O | O | O | O | O |
age_retraite |
O | O | O | O | O |
taux_rendement_actifs |
— | — | O | — | O |
grille_ifc |
O | — | — | — | — |
taux_remplacement_annuel |
— | O | — | — | — |
taux_cotisation_employeur |
— | — | O | — | F |
4. Sorties du moteur¶
4.1 Résultats par salarié¶
Pour chaque salarié inclus dans le calcul, le moteur produit un enregistrement ResultatDBOParSalarie (à créer en Phase 6) avec les champs suivants :
| Champ | Type | Description |
|---|---|---|
evaluation |
FK | Évaluation parente |
organisation |
FK | Tenant (pour isolation multi-tenant) |
salarie |
FK | Référence au Salarie |
matricule |
CharField | Copie dénormalisée pour les exports |
dbo |
DecimalField | DBO individuelle en FCFA |
prestation_projetee |
DecimalField | Prestation brute projetée (PP) |
unite_credit |
DecimalField | Unité de crédit (PUC uniquement) |
droit_acquis |
DecimalField | Droits acquis à la date d'évaluation |
prob_maintien |
FloatField | Probabilité \(_{r-x}p_x^{(\tau)}\) |
facteur_actualisation |
FloatField | \(v^{r-x}\) |
salaire_projete |
DecimalField | Salaire à l'âge de retraite (projeté) |
anciennete_projetee |
FloatField | Ancienneté projetée à la retraite |
detail_json |
JSONField | Détails complets pour audit |
exclu |
BooleanField | True si salarié exclu (âge > retraite, etc.) |
motif_exclusion |
CharField | Motif si exclu |
4.2 Résultats agrégés¶
Un enregistrement ResultatAggrege est créé (ou mis à jour) à l'issue du calcul :
| Champ | Description |
|---|---|
dbo_totale |
\(\sum_i \text{DBO}_i\) — Total du passif |
service_cost |
Coût des services rendus de l'exercice (\(\text{CSR}\)) |
interest_cost |
Coût d'intérêt (\(\text{IC} = \text{DBO}_{t-1} \times i\)) |
nb_salaries_inclus |
Nombre de salariés pris en compte |
nb_salaries_exclus |
Nombre de salariés exclus |
dbo_moyenne |
\(\text{DBO}_{\text{totale}} / N\) |
dbo_mediane |
Médiane des DBO individuelles |
repartition_age_json |
DBO agrégée par tranches d'âge (JSON) |
repartition_categorie_json |
DBO agrégée par catégorie professionnelle (JSON) |
metadata_json |
Version actuariat_lib, paramètres utilisés, timestamp |
4.3 Format de la repartition_age_json¶
{
"tranches": [
{"label": "< 30 ans", "nb": 12, "dbo": 45000000, "pct": 2.1},
{"label": "30-40 ans", "nb": 48, "dbo": 380000000, "pct": 17.7},
{"label": "40-50 ans", "nb": 67, "dbo": 850000000, "pct": 39.6},
{"label": "50-60 ans", "nb": 43, "dbo": 870000000, "pct": 40.6},
{"label": "> 60 ans", "nb": 3, "dbo": 0, "pct": 0.0, "exclus": true}
],
"total": {"nb": 173, "dbo": 2145000000}
}
5. Spécification par famille de règle¶
5.1 Famille GRILLE_ANCIENNETE — IFC et IFC rupture¶
Type(s) Django : IFC, IFC_RUPTURE
Statut : ✅ Implémenté en lot (Phase 6)
Formule — Méthode PUC (IAS 19)¶
où \(k_r\) est l'ancienneté projetée à l'âge de retraite \(r\), et \(S_r\) le salaire projeté :
La DBO individuelle est :
avec \(k\) = ancienneté actuelle, \(v = (1+i)^{-1}\), et \({}_{r-x}p_x^{(\tau)}\) la probabilité de maintien en service (mortalité + rotation combinées).
La Grille est calculée par tranches :
où \((b_j^{\inf}, b_j^{\sup}, \tau_j)\) sont les bornes et le taux de chaque tranche.
Paramètres de la règle (GrilleIFC.paliers JSON)¶
[
{"anciennete_debut": 0, "anciennete_fin": 5, "taux": 0.0},
{"anciennete_debut": 5, "anciennete_fin": 10, "taux": 1.0},
{"anciennete_debut": 10, "anciennete_fin": 15, "taux": 1.5},
{"anciennete_debut": 15, "anciennete_fin": 20, "taux": 2.0},
{"anciennete_debut": 20, "anciennete_fin": 999, "taux": 2.5}
]
Paramètres transverses : base_calcul (dernier_salaire / moy_12_mois / moy_3_ans), mode_depart (volontaire / employeur), plafond_mois.
Fonctions bibliothèque¶
prestation_ifc_projetee()— F-PREST-002dbo_individuelle_puc()— F-ENG-001dbo_population_puc()— F-ENG-002 (vectorisé NumPy)
5.2 Famille TAUX_REMPLACEMENT — Retraite DB¶
Type(s) Django : DB, DB_CAPITAL
Statut : 🟡 À intégrer (Phase 8)
Formule¶
La rente annuelle projetée est :
La DBO est la valeur actuarielle de cette rente différée :
où \(\ddot{a}_r^{(\text{retraite})}\) est la valeur actuelle d'une rente viagère à l'âge \(r\). Pour le type DB_CAPITAL (sortie en capital) :
Paramètres de la règle (ReglePrestation.parametres JSON)¶
{
"taux_remplacement_annuel": 0.018,
"plafond_taux_remplacement": 0.70,
"base_calcul": "dernier_salaire",
"taux_revalorisation_rente": 0.02,
"avec_reversion": true,
"taux_reversion": 0.60,
"anciennete_minimale": 5
}
Fonctions bibliothèque¶
prestation_retraite_db()— F-PREST-003rente_differee_avec_maintien()— F-FIN-006rente_vie_entiere()— F-FIN-003 (pour \(\ddot{a}_r\))
5.3 Famille CAPITAL_ACCUMULE — Retraite DC¶
Type(s) Django : DC, DC_RENTE
Statut : 🟡 À intégrer (Phase 8)
Formule¶
Le capital projeté à la retraite est :
où \(c^{(m)}\) est la cotisation annuelle fractionnée, \(\rho\) le taux de rendement des actifs, et \(\ddot{s}_{x:r|}^{(m,\tau)}\) la valeur future d'une rente temporaire avec maintien.
La DBO côté employeur porte sur les cotisations futures non encore versées (passif DC) :
Pour le type DC_RENTE (sortie en rente), on valorise en plus le capital converti :
Paramètres de la règle (ReglePrestation.parametres JSON)¶
{
"taux_cotisation_employeur": 0.05,
"taux_cotisation_salarie": 0.03,
"taux_rendement_actifs": 0.05,
"mode_sortie": "capital",
"proportion_capital": 1.0
}
Fonctions bibliothèque¶
prestation_retraite_dc()— F-PREST-004capital_differe()— F-FIN-007rente_temporaire()— F-FIN-004
5.4 Famille MONTANT_FIXE — Médailles et Capital décès¶
Type(s) Django : MEDAILLES, PREVOYANCE_DECES
Statut : 🟡 À intégrer (Phase 8 / Phase 12)
Formule — Médailles du travail¶
Plusieurs versements ponctuels aux seuils d'ancienneté \(\{s_1, s_2, \ldots, s_n\}\) :
avec \(M_j = \text{nb\_mois}_j \times S_{s_j}\) (salaire projeté au seuil \(s_j\)).
Formule — Capital décès (prévoyance)¶
où \(\bar{A}_{x:\overline{n}|}\) est la valeur d'une assurance temporaire décès sur \(n = r - x\) années.
Paramètres des règles¶
Médailles (ReglePrestation.parametres JSON) :
{
"grille_medailles": [
{"seuil_anciennete": 20, "nb_mois": 0.5},
{"seuil_anciennete": 30, "nb_mois": 1.0},
{"seuil_anciennete": 35, "nb_mois": 1.5},
{"seuil_anciennete": 40, "nb_mois": 2.0}
]
}
Capital décès :
{
"multiplicateur": 3.0,
"plafond_capital_fcfa": null,
"avec_parts_enfants": false,
"taux_part_enfant": 0.10
}
Fonctions bibliothèque¶
prestation_medaille_travail()— F-PREST-005prestation_capital_deces()— F-PREST-006capital_differe()— F-FIN-007 (médailles : N appels, un par seuil futur)assurance_temporaire_deces()— F-FIN-009 (capital décès)
5.5 Famille CAPITAL_MILLESIMES — Épargne salariale¶
Type(s) Django : EPARGNE, PEE, PERCOL
Statut : 🔴 À intégrer (Phase 8 — extensions F-ES-001 à F-ES-005 requises)
Spécificité fondamentale¶
Chaque salarié peut avoir \(J\) versements horodatés (millésimes), chacun avec son propre horizon de blocage \(H_j\). La DBO est la somme des valeurs actuelles de chaque millésime :
avec taux variable \(\{i_t\}\) :
Paramètres de la règle (JSON)¶
{
"sources_actives": ["S1_COTISATION", "S2_ABONDEMENT"],
"taux_cotisation_s1": {"mode": "FIXE", "valeur_fixe": 0.05},
"taux_abondement_s2": {"mode": "FIXE", "valeur_fixe": 1.0},
"frequence_versement": 12,
"type_blocage": "RETRAITE",
"duree_blocage_annees": 5,
"taux_rendement": {"mode": "TABLE_CALENDAIRE", "table": [[2025, 0.04], [2026, 0.045]]},
"rendement_garanti": false,
"mode_sortie": "capital",
"motifs_deblocage_actifs": ["MARIAGE", "IMMOBILIER", "INVALIDITE", "RUPTURE_CONTRAT"]
}
Fonctions bibliothèque (à créer — Phase 8)¶
evaluer_taux_variable()— F-ES-001calculer_base_distribuable()— F-ES-002 (pour S3/S4)dbo_regime_epargne()— F-ES-003 (individuel)dbo_population_epargne()— F-ES-004 (vectorisé N × J_max)projeter_cotisations_futures()— F-ES-005
5.6 Famille ALEATOIRE — Intéressement et Participation¶
Type(s) Django : INTERESSEMENT, PARTICIPATION
Statut : 🔴 À intégrer (Phase 12 — modules futurs)
Formule¶
La prestation individuelle est une espérance actuarielle :
où \(\mathbb{E}[P_i]\) est la prime individuelle espérée, calculée par F-ES-002 selon la clé de répartition paramétrée :
5.7 Tableau récapitulatif¶
type_produit Django |
type_regime |
Famille | Sous-moteur | Statut |
|---|---|---|---|---|
IFC |
IFC |
GRILLE_ANCIENNETE |
_dbo_via_puc |
✅ Implémenté |
IFC_RUPTURE |
IFC_RUPTURE |
GRILLE_ANCIENNETE |
_dbo_via_puc |
✅ Implémenté |
DB |
RETRAITE_DB |
TAUX_REMPLACEMENT |
_dbo_rente_differee |
🟡 Phase 8 |
DB_CAPITAL |
RETRAITE_DB |
TAUX_REMPLACEMENT |
_dbo_rente_differee |
🟡 Phase 8 |
DC |
RETRAITE_DC |
CAPITAL_ACCUMULE |
_dbo_capital_accumule |
🟡 Phase 8 |
DC_RENTE |
RETRAITE_DC |
CAPITAL_ACCUMULE |
_dbo_capital_accumule |
🟡 Phase 8 |
MEDAILLES |
MEDAILLE |
MONTANT_FIXE |
_dbo_montant_fixe |
🟡 Phase 8 |
PREVOYANCE_DECES |
CAPITAL_DECES |
MONTANT_FIXE |
_dbo_montant_fixe |
🟡 Phase 8 |
EPARGNE |
EPARGNE_SALARIALE |
CAPITAL_MILLESIMES |
_dbo_millesimes |
🔴 Phase 8+ |
PEE |
EPARGNE_SALARIALE |
CAPITAL_MILLESIMES |
_dbo_millesimes |
🔴 Phase 8+ |
INTERESSEMENT |
ALEATOIRE_INTERESSEMENT |
ALEATOIRE |
_dbo_esperance_actuarielle |
🔴 Phase 12 |
PARTICIPATION |
ALEATOIRE_PARTICIPATION |
ALEATOIRE |
_dbo_esperance_actuarielle |
🔴 Phase 12 |
6. Flux d'exécution — Orchestration¶
6.1 Vue d'ensemble¶
Utilisateur Django View Celery EvaluationService actuariat_lib
│ │ │ │ │
│── Lancer calcul ──►│ │ │ │
│ │── lancer_calcul()──► │ │
│ │ statut → EN_COURS │ │
│ │── submit task ──►│ │ │
│◄── Réponse 202 ───│ │ │ │
│ │ │── executer_calcul() ──►│ │
│ │ │ │── bridge ──────────►│
│ │ │ │◄── ContexteCalcul ──│
│ │ │ │── dispatcher ───────►│
│ │ │ │◄── ResultatPopulationDBO
│ │ │ │── persister ────────►BDD
│ │ │ │ statut → CALCULEE
│◄── HTMX polling ──│◄── mise à jour ─│ │ │
6.2 Étape 1 — Vérification des préconditions (verifier_pret_calcul)¶
Avant toute soumission à Celery, verifier_pret_calcul(evaluation_id) effectue les contrôles suivants. Tout échec lève une exception métier EvaluationNonPrete avec un code d'erreur lisible.
def verifier_pret_calcul(evaluation: Evaluation) -> None:
"""Lève EvaluationNonPrete si l'une des conditions n'est pas remplie."""
# 1. Statut compatible
if evaluation.statut not in ('BROUILLON', 'ERREUR'):
raise EvaluationNonPrete('STATUT_INCOMPATIBLE')
# 2. Population importée et non vide
import_pop = evaluation.import_population
if not import_pop or import_pop.statut != 'TERMINE':
raise EvaluationNonPrete('POPULATION_MANQUANTE')
if import_pop.nb_lignes_valides == 0:
raise EvaluationNonPrete('POPULATION_VIDE')
# 3. Hypothèses complètes
hyp = evaluation.hypotheses
if not hyp:
raise EvaluationNonPrete('HYPOTHESES_MANQUANTES')
if not hyp.table_mortalite or not hyp.table_rotation:
raise EvaluationNonPrete('TABLES_MANQUANTES')
# 4. Famille prise en charge
famille = _famille_depuis_type_regime(evaluation.type_produit)
if famille not in FAMILLES_CALCUL_IMPLEMENTE:
raise EvaluationNonPrete(
f'FAMILLE_NON_IMPLEMENTEE:{famille}',
detail=f"La famille '{famille}' n'est pas encore disponible. Familles disponibles : {FAMILLES_CALCUL_IMPLEMENTE}"
)
# 5. Champs population requis présents
champs_requis = CHAMPS_REQUIS_PAR_FAMILLE[famille]
_verifier_champs_population(import_pop, champs_requis)
6.3 Étape 2 — Lancement Celery (lancer_calcul)¶
def lancer_calcul(evaluation_id: int, lance_par: Utilisateur) -> None:
evaluation = Evaluation.objects.get(pk=evaluation_id)
verifier_pret_calcul(evaluation)
# Journalisation de l'action
JournalAudit.objects.create(
utilisateur=lance_par,
organisation=evaluation.organisation,
type_action='LANCEMENT_CALCUL',
modele='Evaluation',
objet_id=evaluation_id,
)
evaluation.statut = 'EN_COURS'
evaluation.save(update_fields=['statut'])
task = calculer_evaluation.delay(evaluation_id)
evaluation.celery_task_id = task.id
evaluation.save(update_fields=['celery_task_id'])
6.4 Étape 3 — Tâche Celery¶
# apps/evaluations/tasks.py
@shared_task(bind=True, max_retries=2, soft_time_limit=600, time_limit=660)
def calculer_evaluation(self, evaluation_id: int):
"""
Tâche principale de calcul DBO.
Timeout soft : 600 s → déclenche SoftTimeLimitExceeded → statut ERREUR propre.
Timeout hard : 660 s → SIGKILL (Celery) → statut ERREUR via signal post_revoke.
Idempotence partielle : si la tâche est relancée après une erreur,
les ResultatDBOParSalarie existants sont supprimés avant recalcul.
"""
try:
EvaluationService().executer_calcul(evaluation_id)
except SoftTimeLimitExceeded:
Evaluation.objects.filter(pk=evaluation_id).update(
statut='ERREUR',
commentaire='Timeout : calcul dépassant 10 minutes annulé.'
)
raise
except Exception as exc:
Evaluation.objects.filter(pk=evaluation_id).update(
statut='ERREUR',
commentaire=str(exc)[:500]
)
raise self.retry(exc=exc, countdown=30)
6.5 Étape 4 — executer_calcul — Orchestration principale¶
def executer_calcul(self, evaluation_id: int) -> None:
evaluation = Evaluation.objects.select_related(
'hypotheses', 'hypotheses__table_mortalite', 'hypotheses__table_rotation',
'grille_ifc', 'organisation'
).get(pk=evaluation_id)
# ── 1. Chargement de la population ───────────────────────────────────────
population_qs = Salarie.objects.for_tenant(evaluation.organisation).filter(
import_source=evaluation.import_population,
actif=True
)
population_df = _queryset_to_dataframe(population_qs)
# ── 2. Chargement des hypothèses ─────────────────────────────────────────
hyp = evaluation.hypotheses
table_mortalite_lib = _charger_table_mortalite(hyp.table_mortalite)
table_rotation_lib = _charger_table_rotation(hyp.table_rotation)
table_composite = construire_table_composite(table_mortalite_lib, table_rotation_lib)
# ── 3. Construction de la règle via le bridge ─────────────────────────────
regle = regle_prestation_django_to_lib(evaluation)
# → retourne instance concrète : RegleIFC, RegleRetraiteDB, etc.
# ── 4. Dispatch selon la famille ─────────────────────────────────────────
sous_moteur = DISPATCHER[regle.famille]
resultat: ResultatPopulationDBO = sous_moteur(
population=population_df,
regle=regle,
hypotheses=hyp,
table_composite=table_composite,
table_retraite=table_mortalite_lib,
date_evaluation=evaluation.date_evaluation,
)
# ── 5. Persistance en transaction atomique ────────────────────────────────
with transaction.atomic():
# Idempotence : supprimer les résultats précédents si relance
ResultatDBOParSalarie.objects.filter(evaluation=evaluation).delete()
# Enregistrement en masse (bulk_create par lots de 500)
_bulk_create_resultats(evaluation, resultat.dbo_par_salarie)
# Agrégats
ResultatAggrege.objects.update_or_create(
evaluation=evaluation,
defaults=_agreger(resultat, evaluation),
)
# Mise à jour du statut
evaluation.statut = 'CALCULEE'
evaluation.save(update_fields=['statut'])
6.6 Bridge regle_prestation_django_to_lib¶
Le bridge est la pièce la plus délicate. Il traduit les modèles Django en objets purs de la bibliothèque :
def regle_prestation_django_to_lib(evaluation: Evaluation) -> ReglePrestation:
"""
Construit la règle concrète à partir de l'évaluation Django.
Applique la hiérarchie de priorité :
evaluation.params_surcharge > hypotheses > defaut_organisation
"""
type_produit = evaluation.type_produit
famille = _famille_depuis_type_regime(type_produit)
# Résolution des paramètres selon priorité
params = _resoudre_parametres(evaluation)
constructeurs = {
'IFC': _construire_regle_ifc,
'IFC_RUPTURE': _construire_regle_ifc,
'RETRAITE_DB': _construire_regle_db,
'RETRAITE_DC': _construire_regle_dc,
'MEDAILLE': _construire_regle_medaille,
'CAPITAL_DECES': _construire_regle_deces,
'EPARGNE_SALARIALE': _construire_regle_epargne,
}
constructeur = constructeurs[type_produit]
regle = constructeur(evaluation, params)
# Validation des paramètres avant envoi à la lib
erreurs = regle.valider_parametres()
if erreurs:
raise ParametresRegleInvalides(erreurs)
return regle
6.7 Suivi de progression et notification HTMX¶
Celery ne dispose pas d'un mécanisme de progression natif. La solution retenue est le polling HTMX :
<!-- Fragment partials/statut_calcul.html -->
<div hx-get="/evaluations/{{ eval.id }}/statut/"
hx-trigger="every 3s [status=='EN_COURS']"
hx-swap="outerHTML">
<span class="badge">{{ eval.statut }}</span>
<span>{{ eval.nb_salaries_traites }} / {{ eval.nb_salaries_total }}</span>
</div>
La tâche Celery met à jour evaluation.nb_salaries_traites tous les 100 salariés via Evaluation.objects.filter(pk=id).update(nb_salaries_traites=n) (sans charger l'objet complet).
6.8 Gestion des erreurs et annulation¶
| Cas d'erreur | Comportement |
|---|---|
| Précondition non remplie | EvaluationNonPrete levée avant soumission Celery — statut reste BROUILLON |
| Erreur métier pendant calcul | statut = 'ERREUR', commentaire = message d'erreur, max_retries=2 |
| Timeout (> 600 s) | SoftTimeLimitExceeded → statut = 'ERREUR', message explicite |
| Annulation utilisateur | Révocation de la tâche Celery + signal task_revoked → statut = 'ANNULEE' |
| Contrainte multi-tenant | Validation dans verifier_pret_calcul : la population doit appartenir à evaluation.organisation |
7. Extension du moteur — Nouvelles familles¶
7.1 Checklist pour ajouter une nouvelle famille¶
L'exemple ci-dessous décrit l'intégration de la famille CAPITAL_ACCUMULE (Retraite DC).
Étape 1 — Bibliothèque : vérifier que les fonctions nécessaires sont disponibles dans actuariat_lib. Pour CAPITAL_ACCUMULE : prestation_retraite_dc() (F-PREST-004) et capital_differe() (F-FIN-007) existent déjà. Sinon, les implémenter et les tester indépendamment.
Étape 2 — Classe règle : créer RegleRetraiteDC dans actuariat_lib/prestations/retraite.py avec la propriété famille = 'CAPITAL_ACCUMULE' et les méthodes abstraites (calculer_prestation, valider_parametres).
Étape 3 — Bridge : dans bridge.py, ajouter 'RETRAITE_DC': _construire_regle_dc dans le dictionnaire constructeurs et implémenter _construire_regle_dc(evaluation, params) -> RegleRetraiteDC.
Étape 4 — Dispatcher : dans evaluation_service.py, ajouter 'CAPITAL_ACCUMULE': _dbo_capital_accumule dans DISPATCHER et implémenter _dbo_capital_accumule(population, regle, hypotheses, ...).
Étape 5 — Liste des familles implémentées : ajouter 'CAPITAL_ACCUMULE' à FAMILLES_CALCUL_IMPLEMENTE.
Étape 6 — Tests et documentation : - Tests unitaires de la règle concrète (3 cas min par règle) - Tests d'intégration du bridge pour ce type de produit - Tests de bout en bout du calcul en lot - Mise à jour du tableau récapitulatif (section 5.7) et de ce document
7.2 Matrice des champs requis — Extension¶
Lors de l'ajout d'une famille, mettre à jour CHAMPS_REQUIS_PAR_FAMILLE dans evaluation_service.py :
CHAMPS_REQUIS_PAR_FAMILLE = {
'GRILLE_ANCIENNETE': ['age_exact', 'anciennete_exacte', 'salaire_brut_mensuel'],
'TAUX_REMPLACEMENT': ['age_exact', 'anciennete_exacte', 'salaire_brut_mensuel', 'sexe'],
'CAPITAL_ACCUMULE': ['age_exact', 'anciennete_exacte', 'salaire_brut_mensuel'],
'MONTANT_FIXE': ['age_exact', 'anciennete_exacte', 'salaire_brut_mensuel'],
'CAPITAL_MILLESIMES': ['age_exact', 'anciennete_exacte', 'salaire_brut_mensuel', 'date_entree'],
'ALEATOIRE': ['age_exact', 'anciennete_exacte', 'salaire_brut_mensuel'],
}
7.3 Paramètres transverses — base_calcul et mode_depart¶
Ces deux paramètres traversent toutes les familles concernées. Leur résolution suit la hiérarchie de priorité décrite en section 3.2 :
| Paramètre | Source de vérité | Valeur par défaut | Familles concernées |
|---|---|---|---|
base_calcul |
Evaluation.base_calcul > HypothesesActuarielles.base_calcul |
dernier_salaire |
Toutes |
mode_depart |
Evaluation.mode_depart > HypothesesActuarielles.mode_depart |
volontaire |
GRILLE_ANCIENNETE |
taux_charges_sociales |
HypothesesActuarielles.taux_charges_sociales |
0.0 |
Toutes |
frequence_paiement m |
ReglePrestation.parametres['frequence'] |
1 (annuel) |
TAUX_REMPLACEMENT, CAPITAL_MILLESIMES |
Le bridge passe ces paramètres dans ContexteCalcul ou directement dans le constructeur de la règle concrète, selon leur nature.
8. Annexes¶
8.1 Glossaire¶
| Terme | Définition |
|---|---|
| DBO | Defined Benefit Obligation — valeur actuelle probable de l'ensemble des engagements futurs envers les salariés |
| PUC | Projected Unit Credit — méthode IAS 19 de calcul des droits acquis, proportionnelle à l'ancienneté |
| EAN | Entry Age Normal — méthode alternative (charge nivelée depuis l'entrée en entreprise) |
| CSR | Coût des Services Rendus — augmentation de la DBO due à une année supplémentaire de service |
| IC | Coût d'Intérêt (Interest Cost) — désactualisation d'un exercice : \(\text{IC} = \text{DBO}_{t-1} \times i\) |
| PP | Prestation Projetée — montant brut qui serait versé si le salarié partait à la retraite aujourd'hui en ayant sa carrière complète |
| Famille | Catégorie actuarielle d'une règle de prestation, déterminant la méthode de calcul DBO |
| type_regime | Identifiant technique de la règle côté Django (ex : IFC, RETRAITE_DB) |
| type_produit | Identifiant commercial côté évaluation (ex : IFC, DB, EPARGNE) |
| Bridge | Module de traduction entre les objets Django et les objets de actuariat_lib |
| ContexteCalcul | Dataclass regroupant toutes les données nécessaires à un calcul actuariel individuel |
| TableComposite | Table combinant mortalité et rotation pour calculer la probabilité de maintien en service \({}_{n}p_x^{(\tau)}\) |
| Tenant | Organisation cliente sur la plateforme multi-tenant — toutes les données sont filtrées par tenant |
| Millésime | Versement individuel horodaté dans un régime d'épargne salariale, avec son propre horizon de blocage |
8.2 Références¶
| Document | Objet |
|---|---|
NOTE_TECHNIQUE_REGLES_PRESTATIONS.md |
Architecture des règles de prestations, classes RegleXxx, ContexteCalcul, ResultatPrestation |
inventaire_actuariat_lib.md |
Inventaire complet des 50 fonctions de la bibliothèque actuarielle (signatures, formules, dépendances) |
Module_Epargne_Salariale_ActuaryLab_v2_0.md |
Spécification détaillée du module épargne salariale (F-ES-001 à F-ES-005, multi-millésimes) |
RAPPORT_PHASE2_ACTUARYLAB.md |
État des modèles Django existants (Evaluation, HypothesesActuarielles, ResultatDBO) |
RAPPORT_PHASE4_ACTUARYLAB.md |
Import et validation population — champs disponibles sur le modèle Salarie |
PLAN_DEVELOPPEMENT_ACTUARYLAB.md |
Plan 12 phases — Phase 6 (moteur IFC), Phase 8 (module retraite), Phase 12 (modules futurs) |
8.3 Tableau récapitulatif complet¶
type_produit |
type_regime |
Famille actuarielle | Sous-moteur | Fonctions actuariat_lib |
Phase | Statut |
|---|---|---|---|---|---|---|
IFC |
IFC |
GRILLE_ANCIENNETE |
_dbo_via_puc |
dbo_population_puc (F-ENG-002) |
6 | ✅ |
IFC_RUPTURE |
IFC_RUPTURE |
GRILLE_ANCIENNETE |
_dbo_via_puc |
dbo_population_puc (F-ENG-002) |
6 | ✅ |
DB |
RETRAITE_DB |
TAUX_REMPLACEMENT |
_dbo_rente_differee |
prestation_retraite_db + rente_differee_avec_maintien |
8 | 🟡 |
DB_CAPITAL |
RETRAITE_DB |
TAUX_REMPLACEMENT |
_dbo_rente_differee |
prestation_retraite_db + capital_differe |
8 | 🟡 |
DC |
RETRAITE_DC |
CAPITAL_ACCUMULE |
_dbo_capital_accumule |
prestation_retraite_dc + rente_temporaire |
8 | 🟡 |
DC_RENTE |
RETRAITE_DC |
CAPITAL_ACCUMULE |
_dbo_capital_accumule |
prestation_retraite_dc + rente_differee_avec_maintien |
8 | 🟡 |
MEDAILLES |
MEDAILLE |
MONTANT_FIXE |
_dbo_montant_fixe |
prestation_medaille_travail + capital_differe (N fois) |
8 | 🟡 |
PREVOYANCE_DECES |
CAPITAL_DECES |
MONTANT_FIXE |
_dbo_montant_fixe |
prestation_capital_deces + assurance_temporaire_deces |
12 | 🔴 |
EPARGNE / PEE |
EPARGNE_SALARIALE |
CAPITAL_MILLESIMES |
_dbo_millesimes |
dbo_population_epargne (F-ES-004) |
8+ | 🔴 |
INTERESSEMENT |
ALEATOIRE_INTERESSEMENT |
ALEATOIRE |
_dbo_esperance_actuarielle |
calculer_base_distribuable (F-ES-002) |
12 | 🔴 |
PARTICIPATION |
ALEATOIRE_PARTICIPATION |
ALEATOIRE |
_dbo_esperance_actuarielle |
calculer_base_distribuable (F-ES-002) |
12 | 🔴 |
Légende : ✅ Implémenté · 🟡 À intégrer (Phase 8) · 🔴 Futur (Phase 8+ ou 12)
Note Technique — Moteur de calcul DBO ActuaryLab v1.0
Cabinet BFEV — Mars 2026 — Usage interne confidentiel