Aller au contenu

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

  1. Objet et périmètre
  2. Principes d'architecture
  3. Entrées du moteur
  4. Sorties du moteur
  5. Spécification par famille de règle
  6. Flux d'exécution — Orchestration
  7. Extension du moteur — Nouvelles familles
  8. 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 Evaluation Django 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. RegleHybride dans 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-010 de l'inventaire actuariat_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_exact et anciennete_exacte sont calculés à l'import (Phase 4) via actuariat_lib.utils.calculer_age_exact() et calculer_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)

\[\text{PP} = \text{Grille}(k_r) \times S_r\]

\(k_r\) est l'ancienneté projetée à l'âge de retraite \(r\), et \(S_r\) le salaire projeté :

\[S_r = S_0 \times (1 + g)^{r - x}\]

La DBO individuelle est :

\[\text{DBO} = \text{PP} \times \frac{k}{k_r} \times v^{r-x} \times {}_{r-x}p_x^{(\tau)}\]

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 :

\[\text{Grille}(k) = \sum_j \left[\min(k, b_j^{\sup}) - b_j^{\inf}\right] \times \tau_j\]

\((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-002
  • dbo_individuelle_puc() — F-ENG-001
  • dbo_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 :

\[R = \min\left(k_r \times \tau_{\text{ann}},\ \tau_{\max}\right) \times S_r\]

La DBO est la valeur actuarielle de cette rente différée :

\[\text{DBO} = R \times v^{r-x} \times {}_{r-x}p_x^{(\tau)} \times \ddot{a}_r^{(\text{retraite})}\]

\(\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) :

\[\text{DBO}_{\text{capital}} = C_r \times v^{r-x} \times {}_{r-x}p_x^{(\tau)}\]

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-003
  • rente_differee_avec_maintien() — F-FIN-006
  • rente_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 :

\[C_r = C_0 \times (1 + \rho)^{r-x} + \frac{c^{(m)}}{m} \times \ddot{s}_{x : r|}^{(m,\tau)}\]

\(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) :

\[\text{DBO}_{\text{DC}} = \frac{c^{(m)}}{m} \times \ddot{a}_{x:r|}^{(m,\tau)}\]

Pour le type DC_RENTE (sortie en rente), on valorise en plus le capital converti :

\[\text{DBO}_{\text{DC\_RENTE}} = \text{DBO}_{\text{DC}} + C_{\text{acquis}} \times v^{r-x} \times {}_{r-x}p_x^{(\tau)} \times {}_{C \to R}\]

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-004
  • capital_differe() — F-FIN-007
  • rente_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\}\) :

\[\text{DBO} = \sum_{j : s_j > k} M_j \times v^{s_j - k} \times {}_{s_j - k}p_x^{(\tau)}\]

avec \(M_j = \text{nb\_mois}_j \times S_{s_j}\) (salaire projeté au seuil \(s_j\)).

Formule — Capital décès (prévoyance)

\[\text{DBO}_{\text{décès}} = C_{\text{décès}}(S) \times \bar{A}_{x:\overline{r-x}|}\]

\(\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-005
  • prestation_capital_deces() — F-PREST-006
  • capital_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 :

\[\text{DBO}_i = \sum_{j=1}^{J_i} C_{ij} \times v(t_0, H_j) \times p^{(\tau)}(x_i, t_0, H_j)\]

avec taux variable \(\{i_t\}\) :

\[v(t_0, H_j) = \prod_{k=0}^{H_j - t_0 - 1} \frac{1}{1 + i(t_0 + k)}\]

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-001
  • calculer_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 :

\[\text{DBO}_i = \mathbb{E}[P_i] \times v^{n} \times {}_{n}p_x^{(\tau)}\]

\(\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 :

\[P_i = P_{\text{global}} \times \frac{S_i \times k_i^{\alpha}}{\sum_j S_j \times k_j^{\alpha}}\]

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) SoftTimeLimitExceededstatut = 'ERREUR', message explicite
Annulation utilisateur Révocation de la tâche Celery + signal task_revokedstatut = '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