================================================================================
     GUIDE COMPLET DES ARCHITECTURES LOGICIELLES - VOLUME 1
     Vue d'ensemble et Concepts Fondamentaux
     Pour étudiants en Génie Logiciel
================================================================================

AVANT-PROPOS
================================================================================
Ce guide a été conçu pour accompagner un étudiant débutant jusqu'au niveau
professionnel en architecture logicielle. Chaque concept est expliqué depuis
ses fondations, illustré par des exemples concrets, et renforcé par des
exercices pratiques.

Aucun prérequis avancé n'est nécessaire, si ce n'est une connaissance basique
d'un langage de programmation (Python, Java ou JavaScript).

Projet fil rouge : tout au long de ce guide, nous construirons progressivement
une application SaaS complète nommée "EduConnect" — une plateforme
d'apprentissage en ligne.

================================================================================
CHAPITRE 1 : QU'EST-CE QUE L'ARCHITECTURE LOGICIELLE ?
================================================================================

------------------------------------------------------------------------
1.1 DÉFINITION FONDAMENTALE
------------------------------------------------------------------------

L'architecture logicielle est l'ensemble des décisions structurelles qui
définissent comment un système logiciel est organisé, construit et fait
fonctionner ses composants ensemble.

Pensez à l'architecture d'un bâtiment : avant de poser la première brique,
un architecte dessine des plans. Ces plans décrivent :
  - Comment les pièces sont organisées
  - Comment les gens circulent d'une pièce à l'autre
  - Quels murs sont porteurs (ne peuvent pas être déplacés)
  - Comment les systèmes électriques, plomberie, chauffage sont distribués

L'architecture logicielle fait exactement la même chose, mais pour un logiciel.

Définition formelle (IEEE 1471) :
  "L'architecture d'un système est l'organisation fondamentale du système,
   incarnée dans ses composants, leurs relations mutuelles et avec
   l'environnement, et les principes guidant sa conception et son évolution."

En termes simples :
  Architecture = Structure + Organisation + Règles de communication

------------------------------------------------------------------------
1.2 POURQUOI L'ARCHITECTURE LOGICIELLE EXISTE-T-ELLE ?
------------------------------------------------------------------------

L'architecture logicielle existe parce que sans elle, les logiciels deviennent
rapidement ingérables. Voici l'évolution naturelle d'un projet sans architecture :

PHASE 1 - LE DÉMARRAGE (Jours 1 à 30)
  - Un développeur écrit un script Python de 200 lignes
  - Tout fonctionne, tout est dans un seul fichier
  - Le code est simple, lisible, modifiable facilement
  - "C'est parfait, pourquoi compliquer ?"

PHASE 2 - LA CROISSANCE (Mois 2 à 6)
  - Le projet grandit : 10 000 lignes dans 5 fichiers
  - D'autres développeurs rejoignent l'équipe
  - Les fonctionnalités s'entremêlent
  - "Je ne comprends plus comment ça marche"

PHASE 3 - LE CHAOS (Mois 6 à 12)
  - 50 000 lignes, 20 développeurs
  - Modifier une fonctionnalité casse trois autres
  - Les bugs se multiplient
  - Chaque déploiement est une terreur
  - "On doit tout réécrire"

PHASE 4 - LA CATASTROPHE
  - Le projet est abandonné ou complètement réécrit
  - Des mois/années de travail perdus
  - L'entreprise perd de l'argent et des clients

L'architecture logicielle existe pour ÉVITER ces phases. Elle impose une
discipline de construction qui permet :
  [OK] La croissance maîtrisée du code
  [OK] La collaboration entre équipes
  [OK] La maintenance à long terme
  [OK] L'évolutivité du système
  [OK] La testabilité du code
  [OK] La réutilisabilité des composants

------------------------------------------------------------------------
1.3 LES QUATRE PILIERS DE L'ARCHITECTURE LOGICIELLE
------------------------------------------------------------------------

Tout système logiciel bien architecturé repose sur quatre piliers :

PILIER 1 : LA STRUCTURE
  Définit comment le code est organisé en modules, couches, services.
  Question : "Comment est découpé le système ?"

PILIER 2 : LA COMMUNICATION
  Définit comment les composants échangent des données.
  Question : "Comment les parties parlent-elles entre elles ?"

PILIER 3 : LES RESPONSABILITÉS
  Définit qui fait quoi dans le système.
  Question : "Quel composant est responsable de quoi ?"

PILIER 4 : LES CONTRAINTES
  Définit les règles et limites à respecter.
  Question : "Qu'est-ce qui est interdit/obligatoire ?"

------------------------------------------------------------------------
1.4 LES ATTRIBUTS DE QUALITÉ (Quality Attributes)
------------------------------------------------------------------------

Une architecture est jugée selon des attributs de qualité. Ce sont les
critères qui permettent d'évaluer si une architecture est "bonne" ou "mauvaise"
pour un contexte donné.

MAINTENABILITÉ (Maintainability)
  Définition : Facilité à modifier, corriger, améliorer le système
  Mesure : "Combien de temps faut-il pour corriger un bug ?"
  Mauvais exemple : Modifier une fonctionnalité nécessite de toucher 50 fichiers
  Bon exemple : Modifier une fonctionnalité ne touche qu'un seul module

SCALABILITÉ (Scalability)
  Définition : Capacité du système à grandir pour gérer plus de charge
  Deux types :
    - Scalabilité verticale : Ajouter des ressources à un serveur (plus de RAM, CPU)
    - Scalabilité horizontale : Ajouter plus de serveurs
  Mesure : "Que se passe-t-il si le trafic est multiplié par 10 ?"

DISPONIBILITÉ (Availability)
  Définition : Proportion de temps pendant laquelle le système est opérationnel
  Mesure en pourcentage :
    - 99%    = ~87 heures d'indisponibilité/an
    - 99.9%  = ~8.7 heures d'indisponibilité/an
    - 99.99% = ~52 minutes d'indisponibilité/an
    - 99.999% = ~5 minutes d'indisponibilité/an (5 nines - Gold Standard)

PERFORMANCE
  Définition : Vitesse et efficacité du système
  Mesures :
    - Latence : Temps de réponse pour une requête
    - Débit (Throughput) : Nombre de requêtes traitées par seconde
  Exemple : "Une page web doit s'afficher en moins de 2 secondes"

SÉCURITÉ (Security)
  Définition : Protection contre les accès non autorisés et les attaques
  Aspects :
    - Confidentialité : Seules les personnes autorisées peuvent lire les données
    - Intégrité : Les données ne peuvent pas être modifiées sans autorisation
    - Disponibilité : Le système reste accessible aux utilisateurs légitimes

TESTABILITÉ (Testability)
  Définition : Facilité à écrire et exécuter des tests
  Mesure : "Quel pourcentage du code peut être testé automatiquement ?"
  Impact direct : Plus un système est testable, plus il est fiable

DÉPLOYABILITÉ (Deployability)
  Définition : Facilité à déployer de nouvelles versions
  Mesure : "Combien de fois peut-on déployer par jour ?"
  Objectif moderne : Plusieurs déploiements par jour sans risque

OBSERVABILITÉ (Observability)
  Définition : Capacité à comprendre ce qui se passe dans le système
  Trois dimensions :
    - Logs : Enregistrement des événements
    - Métriques : Mesures numériques (CPU, mémoire, requêtes/sec)
    - Traces : Suivi du chemin d'une requête dans le système

------------------------------------------------------------------------
1.5 LES DÉCISIONS ARCHITECTURALES
------------------------------------------------------------------------

Une décision architecturale est un choix de conception qui :
  1. A un impact significatif sur le système
  2. Est difficile à revenir en arrière
  3. Implique des compromis importants

Exemples de décisions architecturales :
  - Choisir entre une base de données SQL ou NoSQL
  - Décider d'une architecture monolithique ou microservices
  - Utiliser REST ou GraphQL pour l'API
  - Choisir le langage de programmation
  - Adopter un framework particulier

Les décisions architecturales sont documentées dans des ADR
(Architecture Decision Records - Enregistrements de Décisions Architecturales).

Format d'un ADR :
─────────────────────────────────────────────────────
ADR-001 : Choix de la base de données

Statut : Accepté
Date : 2024-01-15

Contexte :
  Notre application EduConnect doit stocker des profils utilisateurs,
  des cours, et des interactions. Le volume prévu est de 100 000
  utilisateurs dans la première année.

Décision :
  Nous utiliserons PostgreSQL comme base de données principale.

Conséquences :
  [OK] Transactions ACID garanties
  [OK] Requêtes complexes avec SQL
  [OK] Scalabilité verticale facile
  [X] Scalabilité horizontale plus complexe que NoSQL
  [X] Schéma rigide - migrations nécessaires pour changements

Alternatives considérées :
  - MongoDB : Rejeté car nos données sont structurées et relationnelles
  - DynamoDB : Rejeté car coût et vendor lock-in AWS
─────────────────────────────────────────────────────

================================================================================
CHAPITRE 2 : LE PAYSAGE DES ARCHITECTURES LOGICIELLES
================================================================================

------------------------------------------------------------------------
2.1 TAXONOMIE DES ARCHITECTURES
------------------------------------------------------------------------

Il existe plusieurs façons de classer les architectures logicielles.
Voici la classification la plus utile pour un débutant :

PAR NIVEAU D'ABSTRACTION :

  Niveau 1 : Architecture du système (macro-architecture)
    - Décrit comment les systèmes entiers interagissent
    - Exemple : Microservices, SOA, Monolithe
    - Vision : À 10 000 pieds

  Niveau 2 : Architecture applicative (architecture de l'application)
    - Décrit comment une application est organisée en interne
    - Exemple : MVC, Layered, Clean Architecture
    - Vision : À 1 000 pieds

  Niveau 3 : Architecture des composants (micro-architecture)
    - Décrit comment les classes/modules sont organisés
    - Exemple : Design patterns (Factory, Observer, Strategy)
    - Vision : À 100 pieds

PAR STYLE ARCHITECTURAL :

  STYLE STRUCTUREL
    - Architecture en couches (Layered)
    - Architecture en pipeline (Pipes & Filters)
    - Architecture monolithique

  STYLE DISTRIBUÉ
    - Architecture microservices
    - Architecture SOA (Service-Oriented Architecture)
    - Architecture serverless
    - Architecture event-driven

  STYLE CENTRÉ SUR LE DOMAINE
    - Clean Architecture
    - Architecture hexagonale (Ports & Adapters)
    - DDD (Domain-Driven Design)

  STYLE PAR CONTEXTE
    - MVC (Model-View-Controller)
    - MVVM (Model-View-ViewModel)
    - MVP (Model-View-Presenter)

------------------------------------------------------------------------
2.2 CARTE MENTALE DES ARCHITECTURES
------------------------------------------------------------------------

                    ARCHITECTURES LOGICIELLES
                            │
            ┌───────────────┼───────────────┐
            │               │               │
      MONOLITHIQUE       DISTRIBUÉES    CENTRÉES DOMAINE
            │               │               │
    ┌───────┼───────┐   ┌───┴───┐       ┌───┴───┐
    │       │       │   │       │       │       │
  Simple  MVC  Layered  MSA  Event   Clean  Hexa-
                        │   Driven    │    gonale
                    Serverless      DDD

Légende :
  MSA = Microservices Architecture
  DDD = Domain-Driven Design
  Hexa = Architecture Hexagonale

------------------------------------------------------------------------
2.3 ÉVOLUTION HISTORIQUE DES ARCHITECTURES
------------------------------------------------------------------------

ANNÉES 1960-1970 : L'ÈRE MONOLITHIQUE BRUTE
  - Tout le code dans un seul programme
  - Assembleur, COBOL, Fortran
  - Pas de séparation des responsabilités
  - Mainframes centralisés

ANNÉES 1980 : PROGRAMMATION PROCÉDURALE
  - Introduction des fonctions et procédures
  - Pascal, C, BASIC
  - Début de la modularisation
  - "Diviser pour régner"

ANNÉES 1990 : PROGRAMMATION ORIENTÉE OBJET
  - Java, C++, Smalltalk
  - Encapsulation, héritage, polymorphisme
  - Design Patterns (Gang of Four - 1994)
  - Architecture en 3 couches (Client-Serveur)

ANNÉES 2000 : SOA ET WEB
  - Services Web (SOAP, WSDL)
  - Architecture Orientée Services (SOA)
  - MVC populaire avec Rails, Spring MVC
  - Ajax et applications web dynamiques

ANNÉES 2010 : L'ÈRE MICROSERVICES ET CLOUD
  - Netflix, Amazon, Google popularisent les microservices
  - Docker (2013) et conteneurisation
  - Kubernetes (2014)
  - DevOps et CI/CD
  - Clean Architecture (Uncle Bob - 2012)
  - Serverless (AWS Lambda - 2014)

ANNÉES 2020 : L'ÈRE CLOUD-NATIVE
  - Microservices comme standard industriel
  - GitOps et Infrastructure as Code
  - Service Mesh (Istio, Linkerd)
  - Edge Computing
  - Architecture event-driven généralisée

------------------------------------------------------------------------
2.4 LES GRANDES ÉCOLES DE PENSÉE
------------------------------------------------------------------------

ÉCOLE 1 : "KISS - Keep It Simple, Stupid"
  Philosophie : La simplicité est la meilleure architecture
  Représentants : DHH (Ruby on Rails), Joel Spolsky
  Principe : Ne jamais sur-ingénier
  Applications : Startups, MVPs, petites équipes

ÉCOLE 2 : "SOLID et Orienté Objet"
  Philosophie : Principes SOLID, design patterns, OOP
  Représentants : Robert C. Martin (Uncle Bob), Martin Fowler
  Principe : Code propre, testable, maintenable
  Applications : Applications d'entreprise moyennes

ÉCOLE 3 : "Domain-Driven Design"
  Philosophie : Le code doit refléter le domaine métier
  Représentants : Eric Evans, Vaughn Vernon
  Principe : Le langage ubiquitaire unit développeurs et experts métier
  Applications : Systèmes complexes avec règles métier riches

ÉCOLE 4 : "Microservices et Architecture Distribuée"
  Philosophie : Petits services indépendants et déployables séparément
  Représentants : Sam Newman, Adrian Cockcroft
  Principe : Scalabilité et indépendance des équipes
  Applications : Grandes entreprises, fort trafic

ÉCOLE 5 : "Pragmatisme et Context-Driven"
  Philosophie : La meilleure architecture dépend du contexte
  Représentants : Mark Richards, Neal Ford
  Principe : Il n'y a pas de bonne architecture universelle
  Applications : Toutes situations - choisir selon les besoins

================================================================================
CHAPITRE 3 : PRINCIPES FONDAMENTAUX
================================================================================

------------------------------------------------------------------------
3.1 LES PRINCIPES SOLID
------------------------------------------------------------------------

Les principes SOLID sont les cinq principes fondamentaux de conception
orientée objet, formulés par Robert C. Martin. Ils s'appliquent à toutes
les architectures modernes.

─────────────────────────────────────────────────────────────────────
S - SINGLE RESPONSIBILITY PRINCIPLE (Principe de Responsabilité Unique)
─────────────────────────────────────────────────────────────────────

Définition : "Une classe ne doit avoir qu'une seule raison de changer"

En d'autres termes : Chaque classe/module/fonction doit faire UNE seule chose.

MAUVAIS EXEMPLE :
┌─────────────────────────────────────────────────────┐
│ class UserManager:                                  │
│   def create_user(self, data): ...                  │  <- Gestion utilisateur
│   def send_welcome_email(self, user): ...           │  <- Envoi email
│   def save_to_database(self, user): ...             │  <- Persistance DB
│   def generate_report(self, users): ...             │  <- Génération rapport
│   def validate_password(self, pwd): ...             │  <- Validation
└─────────────────────────────────────────────────────┘
Problème : Cette classe a CINQ responsabilités différentes.
           Modifier l'envoi d'email implique de toucher la classe UserManager.

BON EXEMPLE :
┌──────────────────────┐   ┌──────────────────────┐
│ class UserService:   │   │ class EmailService:  │
│   def create(): ...  │   │   def send(): ...    │
│   def update(): ...  │   └──────────────────────┘
│   def delete(): ...  │
└──────────────────────┘   ┌──────────────────────┐
                           │ class UserRepository:│
                           │   def save(): ...    │
                           │   def find(): ...    │
                           └──────────────────────┘

Chaque classe a UNE seule responsabilité.

Règle pratique : "Si vous devez utiliser le mot 'et' pour décrire ce
que fait une classe, elle viole le SRP."

─────────────────────────────────────────────────────────────────────
O - OPEN/CLOSED PRINCIPLE (Principe Ouvert/Fermé)
─────────────────────────────────────────────────────────────────────

Définition : "Une entité logicielle doit être ouverte à l'extension
             mais fermée à la modification"

En d'autres termes : Vous devez pouvoir ajouter de nouvelles fonctionnalités
sans modifier le code existant.

MAUVAIS EXEMPLE :
  def calculate_discount(user_type, price):
      if user_type == "student":
          return price * 0.8
      elif user_type == "senior":
          return price * 0.7
      elif user_type == "employee":    # <- Modification nécessaire
          return price * 0.5          #   pour chaque nouveau type

Problème : Chaque nouveau type d'utilisateur nécessite de modifier
           la fonction existante. Risque de casser ce qui marche.

BON EXEMPLE :
  # Interface de base
  class DiscountStrategy:
      def apply(self, price): pass

  class StudentDiscount(DiscountStrategy):
      def apply(self, price):
          return price * 0.8

  class SeniorDiscount(DiscountStrategy):
      def apply(self, price):
          return price * 0.7

  # Pour ajouter un nouveau type : créer une nouvelle classe
  class EmployeeDiscount(DiscountStrategy):
      def apply(self, price):
          return price * 0.5

  # Le code principal ne change jamais
  def calculate_discount(strategy, price):
      return strategy.apply(price)

Avantage : Ajouter "VIPDiscount" ne modifie AUCUN code existant.

─────────────────────────────────────────────────────────────────────
L - LISKOV SUBSTITUTION PRINCIPLE (Principe de Substitution de Liskov)
─────────────────────────────────────────────────────────────────────

Définition : "Les objets d'une sous-classe doivent pouvoir remplacer
             les objets de la classe parent sans altérer le comportement"

En d'autres termes : Si B hérite de A, on doit pouvoir utiliser B
partout où on utilise A.

MAUVAIS EXEMPLE :
  class Rectangle:
      def set_width(self, w): self.width = w
      def set_height(self, h): self.height = h
      def area(self): return self.width * self.height

  class Square(Rectangle):    # Un carré EST un rectangle (mathématiquement)
      def set_width(self, w):
          self.width = w
          self.height = w   # <- Problème ! Modifie aussi la hauteur

  def test(rectangle):
      rectangle.set_width(4)
      rectangle.set_height(5)
      assert rectangle.area() == 20  # <- Échoue pour Square !

BON EXEMPLE :
  # Utiliser une interface commune
  class Shape:
      def area(self): pass

  class Rectangle(Shape):
      def __init__(self, w, h): ...
      def area(self): return self.width * self.height

  class Square(Shape):
      def __init__(self, side): ...
      def area(self): return self.side ** 2

─────────────────────────────────────────────────────────────────────
I - INTERFACE SEGREGATION PRINCIPLE (Principe de Ségrégation des Interfaces)
─────────────────────────────────────────────────────────────────────

Définition : "Les clients ne devraient pas être forcés de dépendre
             d'interfaces qu'ils n'utilisent pas"

En d'autres termes : Mieux vaut plusieurs petites interfaces qu'une
grande interface fourre-tout.

MAUVAIS EXEMPLE :
  interface Animal:
      def eat()
      def sleep()
      def fly()     # <- Les chiens ne volent pas !
      def swim()    # <- Les oiseaux ne nagent pas (tous) !

  class Dog(Animal):
      def fly(self):
          raise NotImplementedError("Les chiens ne volent pas")

BON EXEMPLE :
  interface Eater:     def eat()
  interface Sleeper:   def sleep()
  interface Flyer:     def fly()
  interface Swimmer:   def swim()

  class Dog(Eater, Sleeper, Swimmer):
      def eat(self): ...
      def sleep(self): ...
      def swim(self): ...

  class Bird(Eater, Sleeper, Flyer):
      def eat(self): ...
      def sleep(self): ...
      def fly(self): ...

─────────────────────────────────────────────────────────────────────
D - DEPENDENCY INVERSION PRINCIPLE (Principe d'Inversion des Dépendances)
─────────────────────────────────────────────────────────────────────

Définition :
  "Les modules de haut niveau ne doivent pas dépendre de modules
   de bas niveau. Les deux doivent dépendre d'abstractions.
   Les abstractions ne doivent pas dépendre des détails.
   Les détails doivent dépendre des abstractions."

En d'autres termes : Dépendez des interfaces, pas des implémentations.

MAUVAIS EXEMPLE :
  class UserService:
      def __init__(self):
          self.db = MySQLDatabase()  # <- Dépend directement de MySQL
                                     #   Changer de DB = modifier UserService

BON EXEMPLE :
  class DatabaseInterface:    # Abstraction
      def save(self, data): pass
      def find(self, id): pass

  class MySQLDatabase(DatabaseInterface):   # Implémentation
      def save(self, data): ...
      def find(self, id): ...

  class UserService:
      def __init__(self, db: DatabaseInterface):  # Dépend de l'abstraction
          self.db = db

  # Utilisation
  mysql_db = MySQLDatabase()
  user_service = UserService(mysql_db)  # Injection de dépendance

  # Changer pour PostgreSQL : aucune modification de UserService !
  postgres_db = PostgreSQLDatabase()
  user_service = UserService(postgres_db)

------------------------------------------------------------------------
3.2 AUTRES PRINCIPES FONDAMENTAUX
------------------------------------------------------------------------

DRY - DON'T REPEAT YOURSELF
  Définition : "Chaque piece de connaissance doit avoir une représentation
               unique, non ambiguë, et faisant autorité dans un système"
  En pratique : Ne jamais dupliquer du code. Factoriser dans des fonctions.
  Exemple violation :
    def calculate_vat_france(price): return price * 1.20
    def calculate_vat_germany(price): return price * 1.19
    # Si le taux change, modifier 2 endroits = risque d'oubli

  Exemple correct :
    VAT_RATES = {"france": 0.20, "germany": 0.19}
    def calculate_vat(price, country):
        return price * (1 + VAT_RATES[country])

KISS - KEEP IT SIMPLE, STUPID
  Définition : La plupart des systèmes fonctionnent mieux s'ils sont
               simples plutôt que complexes.
  En pratique : Toujours choisir la solution la plus simple qui résout
               le problème. Ne pas anticiper des besoins hypothétiques.
  Exemple violation :
    Créer une architecture microservices pour un blog de 100 visiteurs/jour
  Exemple correct :
    Utiliser un monolithe simple avec MVC pour ce même blog

YAGNI - YOU AIN'T GONNA NEED IT
  Définition : N'implémentez pas une fonctionnalité tant que vous n'en
               avez pas réellement besoin.
  En pratique : Ne pas ajouter du code "au cas où".
  Exemple violation :
    Créer un système de cache distribué "pour le futur" alors que
    le projet en est au MVP avec 10 utilisateurs.

SEPARATION OF CONCERNS (SoC)
  Définition : Les différentes préoccupations d'un programme doivent
               être gérées par des parties distinctes du code.
  En pratique : Chaque module gère un aspect spécifique.
  Exemple :
    - Le HTML gère la structure
    - Le CSS gère la présentation
    - Le JavaScript gère le comportement

LAW OF DEMETER (Principe de Moindre Connaissance)
  Définition : Un module ne devrait connaître que ses amis directs
  Règle : Ne parlez qu'à vos amis immédiats, pas aux amis de vos amis
  Exemple violation :
    user.getProfile().getAddress().getCity().toUpperCase()
    # Trop de couplage en chaîne
  Exemple correct :
    user.getCityUppercase()
    # UserService connaît le détail, pas l'appelant

------------------------------------------------------------------------
3.3 LES PRINCIPES DE COUPLAGE ET COHÉSION
------------------------------------------------------------------------

Ces deux concepts sont fondamentaux pour évaluer la qualité d'une architecture.

COUPLAGE (Coupling)
  Définition : Mesure de la dépendance entre modules
  Objectif : FAIBLE couplage (Low Coupling)

  Types de couplage (du pire au meilleur) :
  
  1. Couplage de contenu (le pire)
     Module A accède directement aux données internes de B
     A.internal_data = "hack"  # <- TERRIBLE

  2. Couplage commun
     Modules partagent une variable globale
     global_state = {}  # Partagé entre tous

  3. Couplage de données
     Modules passent des données simples en paramètres
     result = calculate(price, quantity)  # <- BON

  4. Couplage de messages (le meilleur)
     Modules communiquent via des interfaces définies
     service.execute(command)  # <- IDÉAL

COHÉSION (Cohesion)
  Définition : Mesure de l'unité interne d'un module
  Objectif : FORTE cohésion (High Cohesion)

  Types de cohésion (du pire au meilleur) :
  
  1. Cohésion accidentelle (la pire)
     Éléments regroupés sans raison logique
     class Utilities: parse_xml() + calculate_vat() + send_email()
  
  2. Cohésion fonctionnelle (la meilleure)
     Tous les éléments contribuent à une seule tâche bien définie
     class EmailSender: compose() + validate() + send() + track()

RÈGLE D'OR :
  ┌─────────────────────────────────┐
  │  FAIBLE couplage                │
  │  +                              │
  │  FORTE cohésion                 │
  │  =                              │
  │  Bonne Architecture             │
  └─────────────────────────────────┘

================================================================================
CHAPITRE 4 : LES COMPOSANTS D'UN SYSTÈME
================================================================================

------------------------------------------------------------------------
4.1 LES COMPOSANTS FONDAMENTAUX
------------------------------------------------------------------------

Tout système logiciel moderne est composé de blocs fondamentaux que
vous retrouverez dans toutes les architectures.

┌──────────────────────────────────────────────────────────┐
│              ANATOMIE D'UN SYSTÈME LOGICIEL              │
├──────────────────────────────────────────────────────────┤
│                                                          │
│  UTILISATEURS                                            │
│       │                                                  │
│  INTERFACE UTILISATEUR (UI)                              │
│       │                                                  │
│  COUCHE PRÉSENTATION (API / Contrôleurs)                 │
│       │                                                  │
│  COUCHE LOGIQUE MÉTIER (Services / Use Cases)            │
│       │                                                  │
│  COUCHE DONNÉES (Repositories / DAOs)                    │
│       │                                                  │
│  STOCKAGE (Base de données / Cache / Fichiers)           │
│                                                          │
└──────────────────────────────────────────────────────────┘

COMPOSANT 1 : L'INTERFACE UTILISATEUR (UI)
  Rôle : Point d'interaction avec l'utilisateur
  Types :
    - Application Web (navigateur)
    - Application Mobile (iOS/Android)
    - Application Desktop
    - CLI (Command Line Interface)
    - API (interface pour d'autres programmes)
  Technologies : React, Vue, Angular, Flutter, SwiftUI

COMPOSANT 2 : L'API (Application Programming Interface)
  Rôle : Pont entre le frontend et le backend
  Types :
    - REST API : Standard HTTP, ressources, verbes (GET, POST, PUT, DELETE)
    - GraphQL : Requêtes flexibles, une seule URL
    - gRPC : Protocol Buffers, haute performance
    - WebSocket : Connexion persistante, temps réel
  Standard industriel : REST pour la plupart des applications

COMPOSANT 3 : LA LOGIQUE MÉTIER (Business Logic)
  Rôle : Le cœur de l'application, les règles qui définissent le domaine
  Exemple pour EduConnect :
    - "Un étudiant ne peut s'inscrire à un cours que s'il a complété les prérequis"
    - "Un certificat est délivré après 80% de réussite aux examens"
  C'est la partie qui ne change PAS en changeant de technologie

COMPOSANT 4 : LA COUCHE DE DONNÉES
  Rôle : Accès et manipulation des données stockées
  Patterns courants :
    - Repository Pattern : Interface d'accès aux données
    - DAO (Data Access Object) : Objet dédié à l'accès BD
    - ORM (Object-Relational Mapping) : Mappage objet-relationnel

COMPOSANT 5 : LE STOCKAGE
  Types :
    - Base de données relationnelle (SQL) : PostgreSQL, MySQL
      Usage : Données structurées, transactions
    - Base de données non relationnelle (NoSQL) : MongoDB, Redis
      Usage : Documents, clé-valeur, graphes, colonnes
    - Cache : Redis, Memcached
      Usage : Accélération des lectures fréquentes
    - Système de fichiers : S3, Google Cloud Storage
      Usage : Images, vidéos, documents

COMPOSANT 6 : LA MESSAGERIE (Message Queue)
  Rôle : Communication asynchrone entre composants
  Exemples : RabbitMQ, Apache Kafka, AWS SQS
  Usage : Découplage des services, gestion de pics de charge

COMPOSANT 7 : L'INFRASTRUCTURE
  Éléments :
    - Serveurs (physiques ou virtuels)
    - Conteneurs (Docker)
    - Orchestration (Kubernetes)
    - Load Balancer (équilibreur de charge)
    - CDN (Content Delivery Network)
    - DNS (Domain Name System)

------------------------------------------------------------------------
4.2 PATTERNS DE COMMUNICATION
------------------------------------------------------------------------

Les composants d'un système communiquent de deux façons fondamentales :

COMMUNICATION SYNCHRONE
  Définition : L'appelant attend la réponse avant de continuer
  Analogie : Téléphone - vous attendez que l'autre réponde
  Avantages : Simple, résultat immédiat, cohérence facile
  Inconvénients : Couplage temporel, cascade de pannes possible
  
  Protocoles communs :
    HTTP/HTTPS (REST)
    gRPC
    GraphQL

  Schéma :
  ┌─────────┐    Requête    ┌─────────┐
  │ Client  │ ───────────-> │ Service │
  │         │              │         │
  │  (attend)│ <-─────────── │         │
  │         │    Réponse   │         │
  └─────────┘              └─────────┘

COMMUNICATION ASYNCHRONE
  Définition : L'appelant n'attend pas la réponse, continue son travail
  Analogie : Email - vous envoyez et continuez votre journée
  Avantages : Découplage, résilience, performance
  Inconvénients : Complexité, cohérence éventuelle
  
  Protocoles communs :
    Message Queue (RabbitMQ, SQS)
    Event Streaming (Kafka)
    Webhooks

  Schéma :
  ┌─────────┐   Publie     ┌─────────┐    Consomme   ┌─────────┐
  │Producer │ ──────────-> │  Queue  │ ─────────────-> │Consumer │
  │         │             │         │                 │         │
  │(continue│             │(stocke) │                 │(traite) │
  └─────────┘             └─────────┘                 └─────────┘

------------------------------------------------------------------------
4.3 LES PATTERNS DE DISTRIBUTION DES DONNÉES
------------------------------------------------------------------------

Quand les données doivent être partagées entre composants ou services,
plusieurs stratégies existent :

SINGLE SOURCE OF TRUTH (Source Unique de Vérité)
  Principe : Une seule base de données, tous les services y accèdent
  Avantage : Cohérence des données garantie
  Inconvénient : Goulot d'étranglement possible

  ┌──────────┐   ┌──────────┐   ┌──────────┐
  │Service A │   │Service B │   │Service C │
  └────┬─────┘   └────┬─────┘   └────┬─────┘
       │              │              │
       └──────────────┴──────────────┘
                      │
                 ┌────┴────┐
                 │  Base   │
                 │  Unifiée│
                 └─────────┘

DATABASE PER SERVICE
  Principe : Chaque service a sa propre base de données
  Avantage : Indépendance totale, polyglot persistence
  Inconvénient : Cohérence distribuée complexe (Saga Pattern nécessaire)

  ┌──────────┐   ┌──────────┐   ┌──────────┐
  │Service A │   │Service B │   │Service C │
  │  + DB A  │   │  + DB B  │   │  + DB C  │
  └──────────┘   └──────────┘   └──────────┘

CQRS - Command Query Responsibility Segregation
  Principe : Séparer les opérations de lecture et d'écriture
  Avantage : Optimisation indépendante lecture/écriture
  Inconvénient : Complexité, cohérence éventuelle

  ┌──────────┐          ┌──────────────────────────────┐
  │ Client   │ ─Write─-> │ Command Stack (écriture)      │
  │          │          │ -> Base de données écriture    │
  │          │ ─Read──-> │ Query Stack (lecture)         │
  │          │          │ -> Base de données lecture     │
  └──────────┘          └──────────────────────────────┘

================================================================================
CHAPITRE 5 : INTRODUCTION AU PROJET FIL ROUGE - EDUCONNECT
================================================================================

------------------------------------------------------------------------
5.1 PRÉSENTATION DU PROJET
------------------------------------------------------------------------

Tout au long de ce guide, nous développerons progressivement EduConnect,
une plateforme SaaS d'apprentissage en ligne (similaire à Coursera ou Udemy).

FONCTIONNALITÉS DE LA PLATEFORME :

Module 1 : Authentification et Gestion des Utilisateurs
  - Inscription et connexion (email/password + OAuth)
  - Profils utilisateurs (étudiants et formateurs)
  - Gestion des sessions (JWT)
  - Réinitialisation de mot de passe
  - 2FA (Authentification à deux facteurs)

Module 2 : Catalogue de Cours
  - Création et gestion de cours par les formateurs
  - Catégories et tags
  - Recherche et filtres
  - Aperçus et prérequis
  - Notation et commentaires

Module 3 : Gestion des Inscriptions
  - Inscription aux cours (gratuits et payants)
  - Paiement en ligne (Stripe)
  - Gestion des licences
  - Abonnements

Module 4 : Apprentissage
  - Lecture de vidéos
  - Contenu texte et quiz
  - Suivi de progression
  - Téléchargement de ressources
  - Certificats de complétion

Module 5 : Communication
  - Messagerie entre étudiants et formateurs
  - Forums de discussion par cours
  - Notifications (email, push)

Module 6 : Analytics et Reporting
  - Tableau de bord étudiant
  - Analytics formateur (vues, complétion, revenus)
  - Rapports administrateur

Module 7 : Infrastructure et Opérations
  - Monitoring et logs
  - API publique pour intégrations tierces
  - Webhooks

------------------------------------------------------------------------
5.2 LES PERSONAS (UTILISATEURS TYPES)
------------------------------------------------------------------------

PERSONA 1 : L'ÉTUDIANT
  Nom : Aminata, 22 ans, étudiante en informatique à Dakar
  Objectifs :
    - Apprendre le développement web
    - Obtenir des certifications reconnues
    - Progresser à son rythme depuis son téléphone
  Frustrations :
    - Contenu pas à jour
    - Certificats non reconnus
    - Connexion internet instable (important pour conception offline-first)

PERSONA 2 : LE FORMATEUR
  Nom : Jean-Marc, 35 ans, développeur senior à Paris
  Objectifs :
    - Partager son expertise
    - Générer des revenus passifs
    - Construire sa réputation
  Frustrations :
    - Création de cours chronophage
    - Plateforme complexe à utiliser
    - Délais de paiement longs

PERSONA 3 : L'ADMINISTRATEUR
  Nom : Sarah, 30 ans, responsable technique
  Objectifs :
    - Gérer la qualité du contenu
    - Surveiller les performances de la plateforme
    - Gérer les incidents rapidement
  Frustrations :
    - Manque de visibilité sur les problèmes
    - Trop d'alertes inutiles

------------------------------------------------------------------------
5.3 REQUIREMENTS TECHNIQUES
------------------------------------------------------------------------

CONTRAINTES DE PERFORMANCE :
  - Temps de réponse API < 200ms pour 95% des requêtes
  - Support de 10 000 utilisateurs simultanés en phase 1
  - Scalabilité jusqu'à 1 million d'utilisateurs
  - Disponibilité 99.9% (< 9h d'indisponibilité/an)

CONTRAINTES DE SÉCURITÉ :
  - Chiffrement des données sensibles (AES-256)
  - Conformité RGPD
  - Protection contre OWASP Top 10
  - Audit trail de toutes les actions

CONTRAINTES MÉTIER :
  - Support multilingue (FR, EN, AR minimum)
  - Paiements multi-devises
  - Support mobile-first

STACK TECHNIQUE CHOISIE :
  Frontend : React.js + TypeScript
  Backend : Node.js (Express) + Python (FastAPI pour certains services)
  Base de données : PostgreSQL (principale) + Redis (cache)
  Stockage media : AWS S3
  Search : Elasticsearch
  Message Queue : RabbitMQ
  Infrastructure : Docker + Kubernetes

------------------------------------------------------------------------
5.4 VERSION 0 : LA BASE ARCHITECTURALE
------------------------------------------------------------------------

Dans ce premier volume, nous posons les fondations. À la fin de ce guide,
EduConnect sera passé par toutes les architectures étudiées :

ÉVOLUTION ARCHITECTURALE D'EDUCONNECT :

Volume 1 (ce fichier) -> Fondations et concepts
Volume 2 -> Monolithe simple (prototype)
Volume 3 -> MVC structuré
Volume 4 -> Architecture en couches (N-tiers)
Volume 5 -> Microservices
Volume 6 -> Clean Architecture
Volume 7 -> Architecture Hexagonale
Volume 8 -> Event-Driven
Volume 9 -> Serverless pour certaines fonctionnalités
Volume 10 -> Architecture finale complète

Première structure de dossiers pour notre projet :

  educonnect/
  ├── README.md              # Documentation projet
  ├── docs/                  # Documentation architecture
  │   ├── adr/               # Architecture Decision Records
  │   ├── diagrams/          # Diagrammes
  │   └── api/               # Documentation API
  ├── src/                   # Code source
  │   ├── auth/              # Module authentification
  │   ├── courses/           # Module cours
  │   ├── users/             # Module utilisateurs
  │   ├── payments/          # Module paiements
  │   └── notifications/     # Module notifications
  ├── tests/                 # Tests automatisés
  │   ├── unit/              # Tests unitaires
  │   ├── integration/       # Tests d'intégration
  │   └── e2e/               # Tests end-to-end
  ├── infrastructure/        # Configuration infra
  │   ├── docker/
  │   ├── kubernetes/
  │   └── terraform/
  └── scripts/               # Scripts utilitaires

================================================================================
CHAPITRE 6 : OUTILS ET NOTATIONS
================================================================================

------------------------------------------------------------------------
6.1 LA NOTATION UML SIMPLIFIÉE
------------------------------------------------------------------------

UML (Unified Modeling Language) est le langage standard pour représenter
les architectures logicielles visuellement. Nous utiliserons une version
simplifiée tout au long de ce guide.

DIAGRAMME DE CLASSES (Simplifié)

┌─────────────────────────────────┐
│         <<Classe>>              │
│           User                  │
├─────────────────────────────────┤
│ - id: UUID                      │  <- Attribut privé (-)
│ - email: String                 │
│ + name: String                  │  <- Attribut public (+)
│ # created_at: DateTime          │  <- Attribut protégé (#)
├─────────────────────────────────┤
│ + register(): void              │  <- Méthode publique
│ + login(pwd: String): Token     │
│ - validateEmail(): Boolean      │  <- Méthode privée
└─────────────────────────────────┘

RELATIONS ENTRE CLASSES :

  Association simple : A ───── B
    A "connaît" B

  Dépendance : A -----> B
    A "utilise" B temporairement

  Héritage : A ───[WHITE_RIGHT-POINTING_TRIANGLE] B
    A "est un" B

  Implémentation : A ----[WHITE_RIGHT-POINTING_TRIANGLE] B
    A "implémente" B (interface)

  Composition : A [BLACK_DIAMOND]──── B
    A "possède" B, B n'existe pas sans A

  Agrégation : A [WHITE_DIAMOND]──── B
    A "a" B, B peut exister sans A

EXEMPLE DE DIAGRAMME DE CLASSES POUR EDUCONNECT :

  ┌──────────┐         ┌───────────┐
  │   User   │         │  Course   │
  ├──────────┤         ├───────────┤
  │ id       │         │ id        │
  │ email    │         │ title     │
  │ name     │         │ price     │
  └────┬─────┘         └─────┬─────┘
       │                     │
       │ many-to-many        │
       └──────────────────────┘
              Enrollment
              - user_id
              - course_id
              - enrolled_at
              - progress

DIAGRAMME DE SÉQUENCE (Interaction entre composants)

  Représente le flux d'actions dans le temps, de haut en bas.

  User       Frontend    API          DB
   │            │         │            │
   │ login      │         │            │
   │──────────->│         │            │
   │           │  POST /auth/login    │
   │           │────────->│            │
   │           │         │  query user│
   │           │         │───────────->│
   │           │         │  user data │
   │           │         │<-───────────│
   │           │  {token}│            │
   │           │<-────────│            │
   │ {token}   │         │            │
   │<-──────────│         │            │

------------------------------------------------------------------------
6.2 LES DIAGRAMMES C4
------------------------------------------------------------------------

Le modèle C4 (Context, Containers, Components, Code) est une façon moderne
et pragmatique de visualiser l'architecture à 4 niveaux de zoom.

NIVEAU 1 - CONTEXTE SYSTÈME
  Vue de très haut niveau
  Question : "Comment le système s'intègre dans son environnement ?"

  ┌─────────────────────────────────────────────┐
  │              SYSTÈME                        │
  │                                             │
  │  ┌──────────┐         ┌──────────────────┐  │
  │  │ Étudiant │ ──────-> │  EduConnect      │  │
  │  └──────────┘         │  (SaaS Platform) │  │
  │                       └──────┬───────────┘  │
  │  ┌──────────┐                │              │
  │  │Formateur │ ──────->        │              │
  │  └──────────┘                [BLACK_DOWN-POINTING_TRIANGLE]              │
  │                       ┌──────────────────┐  │
  │                       │ Payment Gateway  │  │
  │                       │ (Stripe)         │  │
  │                       └──────────────────┘  │
  └─────────────────────────────────────────────┘

NIVEAU 2 - CONTENEURS
  Vue des grandes parties du système
  Question : "Quelles sont les applications et bases de données ?"

  ┌─────────────────────────────────────────────────────────┐
  │                    EDUCONNECT                           │
  │                                                         │
  │  ┌──────────────┐   ┌──────────────┐   ┌────────────┐ │
  │  │   React SPA  │   │  Node.js API │   │ PostgreSQL │ │
  │  │  (Frontend)  │──->│  (Backend)   │──->│    (DB)    │ │
  │  └──────────────┘   └──────┬───────┘   └────────────┘ │
  │                            │                            │
  │                     ┌──────┴──────┐                    │
  │                     │    Redis    │                    │
  │                     │   (Cache)   │                    │
  │                     └─────────────┘                    │
  └─────────────────────────────────────────────────────────┘

NIVEAU 3 - COMPOSANTS
  Vue interne d'un conteneur
  Question : "Quels sont les composants internes ?"

  ┌──────────────────────────────────────────────────────────────┐
  │                      NODE.JS API                             │
  │                                                              │
  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐ │
  │  │Auth Component│  │Course Comp. │  │Enrollment Component│ │
  │  │  (Routes +  │  │  (Routes +  │  │   (Routes +        │ │
  │  │  Controller)│  │  Controller)│  │   Controller)       │ │
  │  └──────┬──────┘  └──────┬──────┘  └─────────┬───────── ┘ │
  │         │                │                    │             │
  │  ┌──────[BLACK_DOWN-POINTING_TRIANGLE]──────┐  ┌──────[BLACK_DOWN-POINTING_TRIANGLE]──────┐  ┌─────────[BLACK_DOWN-POINTING_TRIANGLE]──────────┐ │
  │  │Auth Service │  │Course Serv. │  │Enrollment Service  │ │
  │  └──────┬──────┘  └──────┬──────┘  └─────────┬──────────┘ │
  │         │                │                    │             │
  │         └────────────────┴────────────────────┘             │
  │                          │                                   │
  │                   ┌──────[BLACK_DOWN-POINTING_TRIANGLE]──────┐                          │
  │                   │  Database   │                          │
  │                   │  Layer      │                          │
  │                   └─────────────┘                          │
  └──────────────────────────────────────────────────────────────┘

NIVEAU 4 - CODE
  Vue du code source (classes, fonctions)
  Ce niveau est couvert dans les chapitres d'implémentation.

================================================================================
CHAPITRE 7 : MÉTHODOLOGIES DE CONCEPTION ARCHITECTURALE
================================================================================

------------------------------------------------------------------------
7.1 L'APPROCHE TOP-DOWN VS BOTTOM-UP
------------------------------------------------------------------------

TOP-DOWN (Du général au particulier)
  Processus :
    1. Définir la vision globale du système
    2. Décomposer en grands blocs fonctionnels
    3. Affiner chaque bloc en sous-composants
    4. Implémenter les détails
  Avantages :
    - Vue cohérente du système dès le début
    - Moins de redondances
  Inconvénients :
    - Peut mener à l'over-engineering
    - Difficile si les besoins ne sont pas clairs

BOTTOM-UP (Du particulier au général)
  Processus :
    1. Identifier les fonctionnalités de base
    2. Implémenter chaque fonctionnalité
    3. Identifier les patterns et factoriser
    4. Construire une architecture émergente
  Avantages :
    - Pragmatique et rapide au démarrage
    - Architecture basée sur la réalité du code
  Inconvénients :
    - Risque de design incohérent
    - Refactoring fréquent

APPROCHE HYBRIDE (Recommandée)
  "Think big, start small, move fast"
    1. Définir une architecture cible de haut niveau (Top-Down)
    2. Commencer simple et fonctionnel (Bottom-Up)
    3. Refactorer vers l'architecture cible progressivement
    4. Itérer et ajuster

------------------------------------------------------------------------
7.2 LE PROCESSUS DE CONCEPTION ARCHITECTURALE
------------------------------------------------------------------------

ÉTAPE 1 : RECUEIL DES BESOINS
  - Fonctionnels (Features) : Qu'est-ce que le système doit faire ?
  - Non-fonctionnels (Qualités) : Performance, sécurité, scalabilité
  - Contraintes : Budget, délai, technologies imposées

ÉTAPE 2 : ANALYSE ET PRIORISATION
  Classer les besoins par :
  - Priorité : Essentiel / Important / Nice-to-have
  - Risque : Haute incertitude vs Connu

ÉTAPE 3 : IDENTIFICATION DES DRIVERS ARCHITECTURAUX
  Ce sont les besoins qui ont le plus d'impact sur l'architecture.
  Exemples pour EduConnect :
    - Performance : 10 000 utilisateurs simultanés -> Impact sur scalabilité
    - Disponibilité 99.9% -> Impact sur redondance et monitoring
    - RGPD -> Impact sur gestion et stockage des données

ÉTAPE 4 : EXPLORATION DES STYLES ARCHITECTURAUX
  Pour chaque driver, identifier les styles adaptés :
    - Fort trafic -> Microservices ou Scalabilité horizontale
    - Domaine complexe -> Clean Architecture ou DDD
    - Équipe petite -> Monolithe modulaire

ÉTAPE 5 : DÉCISIONS ET COMPROMIS (Trade-offs)
  Chaque décision implique des compromis. Documenter :
    - Ce qu'on gagne
    - Ce qu'on perd
    - Pourquoi ce choix est le meilleur pour le contexte

ÉTAPE 6 : DOCUMENTATION (ADRs + Diagrammes)
  - Architecture Decision Records
  - Diagrammes C4
  - Documentation API

ÉTAPE 7 : VALIDATION ET RÉVISION
  - Proof of Concept (PoC) pour les décisions risquées
  - Revues d'architecture avec l'équipe
  - Ajustements basés sur les retours

------------------------------------------------------------------------
7.3 LES ANTI-PATTERNS ARCHITECTURAUX
------------------------------------------------------------------------

Un anti-pattern est une "mauvaise solution" reconnue à un problème récurrent.
Voici les plus courants :

ANTI-PATTERN 1 : BIG BALL OF MUD
  Description : Aucune structure architecturale. Tout est connecté à tout.
  Symptômes : Impossible de modifier quoi que ce soit sans tout casser
  Cause : Manque de planification, croissance non maîtrisée
  Solution : Refactoring progressif, introduction de modules

ANTI-PATTERN 2 : GOLDEN HAMMER
  Description : Utiliser la même technologie pour tous les problèmes
  Symptôme : "On utilise MySQL pour tout, même pour les logs en temps réel"
  Cause : Confort, expertise limitée
  Solution : Évaluer la bonne technologie pour chaque besoin

ANTI-PATTERN 3 : VENDOR LOCK-IN
  Description : Dépendance totale à un fournisseur spécifique
  Symptôme : Impossible de changer de cloud provider sans tout réécrire
  Cause : Utilisation d'APIs propriétaires sans abstraction
  Solution : Interfaces d'abstraction, Architecture Hexagonale

ANTI-PATTERN 4 : PREMATURE OPTIMIZATION
  Description : Optimiser avant de savoir où est le vrai problème
  Symptôme : Mois passés à optimiser du code rarement exécuté
  Cause : Anxiété de performance, ingénierie sans mesure
  Solution : "Measure, don't guess" - Profiler avant d'optimiser

ANTI-PATTERN 5 : DISTRIBUTED MONOLITH
  Description : Microservices qui ne peuvent pas fonctionner indépendamment
  Symptôme : Déployer le Service A nécessite de déployer B, C, D en même temps
  Cause : Mauvaise décomposition, couplage excessif entre services
  Solution : Revoir les limites des services (Bounded Contexts)

ANTI-PATTERN 6 : RESUMÉ-DRIVEN DEVELOPMENT
  Description : Adopter des technologies pour ajouter des mots sur son CV
  Symptôme : Architecture Kubernetes pour un blog personnel
  Cause : Ego et ambition déconnectés du besoin réel
  Solution : YAGNI + KISS, toujours choisir selon le contexte

================================================================================
CHAPITRE 8 : EXERCICES PRATIQUES
================================================================================

------------------------------------------------------------------------
EXERCICES FACILES
------------------------------------------------------------------------

EXERCICE 1 : Identifier les violations SOLID
  Contexte : Analysez le code suivant et identifiez toutes les violations
  des principes SOLID.

  class OnlineCourseApp:
      def __init__(self):
          self.courses = []
          self.users = []

      def create_course(self, title, price, instructor_id):
          course = {"id": len(self.courses)+1, "title": title,
                    "price": price, "instructor": instructor_id}
          self.courses.append(course)
          # Envoyer un email à l'instructor
          import smtplib
          # ... code email direct ici
          # Enregistrer dans un fichier CSV
          with open("courses.csv", "a") as f:
              f.write(f"{title},{price}\n")
          return course

      def generate_pdf_report(self, course_id):
          # Génère un PDF des stats du cours
          import reportlab
          # ... code PDF ici
          pass

      def charge_student(self, student_id, course_id):
          # Appel direct à Stripe sans abstraction
          import stripe
          stripe.api_key = "sk_live_secret_key_ici"
          stripe.Charge.create(amount=100, currency="eur")

  Questions :
  a) Combien de responsabilités a la classe OnlineCourseApp ?
  b) Quels principes SOLID sont violés ?
  c) Proposez une décomposition en classes respectant le SRP.

EXERCICE 2 : Dessiner un diagramme de contexte C4
  Contexte : Vous devez concevoir un système de gestion de bibliothèque.
  
  Le système doit :
  - Permettre aux membres d'emprunter des livres
  - Notifier par email les retards
  - S'intégrer avec un système de paiement pour les amendes
  - Fournir des statistiques aux bibliothécaires

  Tâche : Dessinez le diagramme C4 niveau 1 (Contexte Système)
          Identifiez : Acteurs, Systèmes externes, Flux principaux

EXERCICE 3 : Rédiger un ADR
  Contexte : Vous êtes architecte sur EduConnect.
  Un débat a lieu dans l'équipe : utiliser PostgreSQL ou MongoDB ?

  Tâche : Rédigez un ADR complet selon le format présenté dans ce chapitre.
          Includez : Contexte, Décision, Conséquences, Alternatives.

------------------------------------------------------------------------
EXERCICES INTERMÉDIAIRES
------------------------------------------------------------------------

EXERCICE 4 : Appliquer le principe DIP
  Contexte : Voici un service de notification qui envoie des emails.

  class NotificationService:
      def send_notification(self, user_id, message):
          # Récupère user directement
          user = MySQLDatabase.execute(
              f"SELECT * FROM users WHERE id = {user_id}")
          # Envoie l'email directement
          smtp = smtplib.SMTP('smtp.gmail.com')
          smtp.sendmail("from@example.com", user['email'], message)

  Tâche :
  a) Appliquez le DIP pour découpler le service de la base de données
  b) Appliquez le DIP pour découpler le service du fournisseur d'email
  c) Montrez comment le service peut maintenant utiliser SendGrid ou SMTP
     sans modification du code du service

EXERCICE 5 : Analyse des attributs de qualité
  Contexte : Vous analysez les exigences d'EduConnect.

  Besoins identifiés :
  1. "Les vidéos de cours doivent charger rapidement, même en Afrique"
  2. "Si le serveur de paiement tombe, les cours doivent rester accessibles"
  3. "Un formateur doit pouvoir publier un cours en 5 minutes maximum"
  4. "Toutes les transactions financières doivent être auditables"
  5. "Le système doit tenir 100 000 utilisateurs sans refactoring"

  Tâche :
  a) Identifiez l'attribut de qualité principal pour chaque besoin
  b) Proposez une mesure quantifiable pour chaque attribut
  c) Identifiez les conflits potentiels entre ces attributs

EXERCICE 6 : Décomposition en composants
  Contexte : Décomposez le module d'authentification d'EduConnect
             en sous-composants selon le principe SRP.

  Fonctionnalités à décomposer :
  - Inscription avec email/mot de passe
  - Connexion et génération de JWT
  - Connexion OAuth (Google, GitHub)
  - Réinitialisation de mot de passe
  - Vérification d'email
  - 2FA par SMS ou application
  - Gestion des sessions actives
  - Révocation des tokens

  Tâche :
  a) Listez tous les composants nécessaires
  b) Définissez la responsabilité unique de chaque composant
  c) Dessinez les interactions entre composants

------------------------------------------------------------------------
EXERCICES AVANCÉS
------------------------------------------------------------------------

EXERCICE 7 : Conception complète d'une feature
  Contexte : Vous devez concevoir la feature "Certificats de Complétion"
             pour EduConnect.

  Exigences :
  - Un étudiant reçoit un certificat quand il complète un cours à 100%
  - Le certificat doit être unique et vérifiable par un employeur
  - Les certificats doivent être générés en PDF
  - Un lien de vérification public doit exister
  - Les certificats expirés (si l'étudiant est remboursé) doivent être
    révocables

  Tâche :
  a) Identifiez tous les attributs de qualité concernés
  b) Listez toutes les décisions architecturales à prendre
  c) Rédigez 3 ADRs pour les décisions les plus importantes
  d) Dessinez le diagramme C4 niveau 2 pour cette feature
  e) Écrivez les interfaces (pas l'implémentation) des composants

EXERCICE 8 : Détection d'anti-patterns
  Contexte : Vous rejoignez une équipe qui travaille sur une application
             depuis 3 ans. Vous lisez cette description du système :

  "Notre application Express.js fait tout. La route POST /order appelle
   directement les fonctions de la base de données MySQL via mysql2,
   envoie un email via nodemailer, appelle l'API Stripe, met à jour le stock
   en temps réel, génère une facture PDF, et envoie une notification push.
   Tout ça dans le même handler de route.
   
   Nos microservices UserService et OrderService partagent la même base de
   données MySQL. Ils doivent toujours être déployés ensemble.
   
   Pour nos besoins de reporting temps réel, on utilise MySQL. Les requêtes
   prennent 30 secondes parfois.
   
   On a Kubernetes avec 200 pods parce que 'c'est ce que font les grandes
   entreprises' mais on a seulement 1000 utilisateurs."

  Tâche :
  a) Identifiez TOUS les anti-patterns présents
  b) Évaluez l'impact de chaque anti-pattern sur les attributs de qualité
  c) Proposez un plan de remédiation priorisé (Quick wins vs Long terme)
  d) Rédigez un email persuasif à votre CTO expliquant les risques

EXERCICE 9 : Architecture Decision Workshop
  Contexte : EduConnect vient de lever des fonds et doit passer de
             1 000 à 1 000 000 utilisateurs en 18 mois.

  Situation actuelle :
  - Monolithe Node.js sur un serveur dédié
  - PostgreSQL sur un seul serveur
  - Déploiement manuel toutes les 2 semaines
  - 3 développeurs

  Contraintes du futur :
  - Budget : 50k€/mois infrastructure max
  - Délai : 18 mois pour atteindre 1M d'utilisateurs
  - Équipe grandira à 20 développeurs
  - Nouvelles features : Live streaming, Marketplace formateurs

  Tâche :
  a) Listez les problèmes architecturaux actuels
  b) Définissez les drivers architecturaux pour la cible
  c) Proposez 3 scénarios d'évolution architecturale différents
  d) Évaluez chaque scénario selon 5 critères de votre choix
  e) Recommandez un scénario et justifiez votre choix
  f) Créez une roadmap architecturale sur 18 mois

================================================================================
CHAPITRE 9 : CORRIGÉS DÉTAILLÉS
================================================================================

------------------------------------------------------------------------
CORRIGÉ EXERCICE 1 : Violations SOLID
------------------------------------------------------------------------

RÉPONSE a) Responsabilités de OnlineCourseApp :
  La classe a au moins 5 responsabilités :
  1. Gestion des cours (CRUD)
  2. Gestion des utilisateurs (CRUD)
  3. Envoi d'emails (SMTP)
  4. Génération de rapports PDF
  5. Gestion des paiements (Stripe)

RÉPONSE b) Violations SOLID :

  Violation SRP (Single Responsibility) :
    Principale violation. Voir les 5 responsabilités ci-dessus.

  Violation OCP (Open/Closed) :
    Pour ajouter un nouveau type de notification (SMS, Push), il faudra
    modifier la méthode create_course existante.

  Violation DIP (Dependency Inversion) :
    Le code dépend directement de :
    - smtplib (implémentation email)
    - reportlab (implémentation PDF)
    - stripe.Charge (implémentation paiement)
    - Écriture directe en CSV (stockage)
    Toutes ces dépendances devraient être inversées via des interfaces.

RÉPONSE c) Décomposition proposée :

  ┌─────────────────────────────────────────────────────────────┐
  │                    Interfaces                               │
  ├─────────────────────────────────────────────────────────────┤
  │ class NotificationInterface:                                │
  │     def send(self, recipient, message): pass                │
  │                                                             │
  │ class PaymentInterface:                                     │
  │     def charge(self, amount, currency, user_id): pass       │
  │                                                             │
  │ class CourseRepositoryInterface:                            │
  │     def save(self, course): pass                            │
  │     def find(self, course_id): pass                         │
  │                                                             │
  │ class ReportInterface:                                      │
  │     def generate(self, data): pass                          │
  └─────────────────────────────────────────────────────────────┘

  ┌─────────────────────────────────────────────────────────────┐
  │                  Implémentations                            │
  ├─────────────────────────────────────────────────────────────┤
  │ class SmtpNotification(NotificationInterface):              │
  │     def send(self, recipient, message):                     │
  │         import smtplib                                      │
  │         # Logique SMTP ici                                  │
  │                                                             │
  │ class StripePayment(PaymentInterface):                      │
  │     def charge(self, amount, currency, user_id):            │
  │         import stripe                                       │
  │         # Logique Stripe ici                                │
  │                                                             │
  │ class PostgresCourseRepository(CourseRepositoryInterface):  │
  │     def save(self, course): ...                             │
  │     def find(self, course_id): ...                          │
  └─────────────────────────────────────────────────────────────┘

  ┌─────────────────────────────────────────────────────────────┐
  │                    Services Métier                          │
  ├─────────────────────────────────────────────────────────────┤
  │ class CourseService:                                        │
  │     def __init__(self,                                      │
  │                  repo: CourseRepositoryInterface,           │
  │                  notification: NotificationInterface):      │
  │         self.repo = repo                                    │
  │         self.notification = notification                    │
  │                                                             │
  │     def create_course(self, title, price, instructor_id):   │
  │         course = Course(title, price, instructor_id)        │
  │         self.repo.save(course)                              │
  │         self.notification.send(                             │
  │             instructor_id,                                  │
  │             "Votre cours a été créé !"                      │
  │         )                                                   │
  │         return course                                       │
  └─────────────────────────────────────────────────────────────┘

  Résultat : 
  [OK] SRP : Chaque classe a UNE responsabilité
  [OK] OCP : Ajouter SMS = créer SmsNotification sans modifier CourseService
  [OK] DIP : CourseService dépend d'interfaces, pas d'implémentations

------------------------------------------------------------------------
CORRIGÉ EXERCICE 3 : ADR
------------------------------------------------------------------------

─────────────────────────────────────────────────────
ADR-002 : Choix de la base de données - PostgreSQL vs MongoDB

Statut : Accepté
Date : 2024-03-01
Auteurs : [Votre nom]
Décideurs : Lead développeur, Architecte, CTO

Contexte :
  EduConnect doit stocker :
  - Profils utilisateurs avec authentification
  - Catalogue de cours (titre, description, prérequis, modules)
  - Inscriptions et progression des étudiants
  - Transactions financières
  - Forums de discussion

  Volume estimé : 100k utilisateurs en an 1, 1M en an 3.
  Équipe : 3 développeurs, expérience PostgreSQL et MongoDB.

  Les données sont fortement relationnelles :
    User -> many Enrollments -> many Courses
    Course -> many Modules -> many Lessons
    User -> many Transactions

  Les transactions financières nécessitent des garanties ACID.

Décision :
  Nous utiliserons PostgreSQL comme base de données principale.
  Redis sera utilisé comme cache et pour les sessions.

Conséquences positives :
  [OK] Transactions ACID : Critique pour les paiements et inscriptions
  [OK] Jointures SQL efficaces pour requêtes relationnelles complexes
  [OK] Schéma strict garantit l'intégrité des données
  [OK] Support JSONB pour données semi-structurées (metadata cours)
  [OK] Excellente intégration avec les ORMs Python (SQLAlchemy) et Node.js
  [OK] Expertise équipe disponible

Conséquences négatives :
  [X] Scalabilité horizontale plus complexe que MongoDB
  [X] Migrations de schéma nécessaires lors d'évolutions
  [X] Flexibilité des données moins grande pour contenu de cours

Alternatives considérées :
  1. MongoDB
     Rejeté car :
     - Transactions ACID limitées dans les versions pré-4.0
     - Nos données sont intrinsèquement relationnelles
     - Jointures inefficaces sans SQL
     - Risque de inconsistance données financières

  2. MySQL
     Non retenu car :
     - PostgreSQL supérieur sur données JSON, full-text search
     - Meilleur support des fonctionnalités avancées (CTE, Window functions)

  3. CockroachDB (NewSQL)
     Rejeté car :
     - Complexité opérationnelle inutile à ce stade
     - Expertise équipe insuffisante
     - Coût plus élevé

Notes :
  Réviser cette décision si on dépasse 10M utilisateurs.
  À ce stade, évaluer le sharding PostgreSQL ou CockroachDB.
─────────────────────────────────────────────────────

------------------------------------------------------------------------
CORRIGÉ EXERCICE 5 : Attributs de qualité
------------------------------------------------------------------------

RÉPONSE a & b) Identification et mesures :

  Besoin 1 : "Les vidéos doivent charger rapidement en Afrique"
    Attribut principal : PERFORMANCE
    Attribut secondaire : DISPONIBILITÉ
    Mesure quantifiable :
      - Temps de démarrage vidéo < 3 secondes pour une connexion 3G
      - TTFB (Time To First Byte) < 500ms
    Implication architecturale : CDN avec PoP (Points of Presence) en Afrique
                                 (Cloudflare, AWS CloudFront)

  Besoin 2 : "Si le serveur de paiement tombe, les cours restent accessibles"
    Attribut principal : DISPONIBILITÉ
    Attribut secondaire : RÉSILIENCE
    Mesure quantifiable :
      - Le module cours doit avoir 99.9% de disponibilité indépendamment
        du module paiement
    Implication architecturale : Isolation des services (Circuit Breaker Pattern)

  Besoin 3 : "Un formateur publie un cours en 5 minutes"
    Attribut principal : USABILITÉ
    Attribut secondaire : PERFORMANCE
    Mesure quantifiable :
      - Upload et traitement vidéo en < 5 minutes pour 100MB
      - Latence API création cours < 500ms
    Implication architecturale : Processing asynchrone des médias

  Besoin 4 : "Transactions financières auditables"
    Attribut principal : SÉCURITÉ / CONFORMITÉ
    Attribut secondaire : FIABILITÉ
    Mesure quantifiable :
      - 100% des transactions enregistrées avec timestamp, acteur, action
      - Logs immuables (append-only)
      - Conservation 7 ans (obligation légale)
    Implication architecturale : Event Sourcing pour transactions financières

  Besoin 5 : "Tenir 100 000 utilisateurs sans refactoring"
    Attribut principal : SCALABILITÉ
    Mesure quantifiable :
      - Architecture doit gérer 10x la charge actuelle
      - Load testing validé à 100k utilisateurs simultanés
    Implication architecturale : Architecture scalable horizontalement

RÉPONSE c) Conflits potentiels :

  CONFLIT 1 : Performance vs Sécurité
    Plus on chiffre, plus c'est lent. Chiffrer toutes les données vidéo
    impacte le temps de chargement.
    Résolution : Chiffrer uniquement les données sensibles (personnelles,
                 financières), pas le contenu publié.

  CONFLIT 2 : Disponibilité vs Cohérence (CAP Theorem)
    Si le serveur de paiement tombe et les cours restent accessibles,
    les données d'inscription peuvent être temporairement incohérentes.
    Résolution : Cohérence éventuelle pour les statistiques,
                 cohérence forte pour les transactions financières.

  CONFLIT 3 : Scalabilité vs Coût
    Une architecture qui scale à 100k utilisateurs est plus chère à opérer.
    Résolution : Architecture progressive qui scale selon la charge réelle
                 (auto-scaling).

================================================================================
RÉCAPITULATIF DU CHAPITRE
================================================================================

Points clés à retenir :

1. L'ARCHITECTURE, c'est l'organisation fondamentale d'un système.
   Elle répond aux questions : Structure, Communication, Responsabilités, Contraintes.

2. LES ATTRIBUTS DE QUALITÉ sont les critères d'évaluation d'une architecture :
   Maintenabilité, Scalabilité, Disponibilité, Performance, Sécurité, Testabilité.

3. LES PRINCIPES SOLID sont la base de toute bonne conception :
   S - Une seule responsabilité par classe
   O - Ouvert à l'extension, fermé à la modification
   L - Substitution de Liskov
   I - Ségrégation des interfaces
   D - Inversion des dépendances

4. FAIBLE COUPLAGE + FORTE COHÉSION = Bonne Architecture

5. LES ANTI-PATTERNS sont des solutions reconnues comme mauvaises.
   Les plus dangereux : Big Ball of Mud, Distributed Monolith, Vendor Lock-in.

6. EDUCONNECT est notre projet fil rouge qui évoluera à travers toutes les architectures.

7. LES DÉCISIONS ARCHITECTURALES doivent être documentées dans des ADRs.

Prochaine étape :
  Volume 2 : Architecture Monolithique
  -> Construire la première version d'EduConnect comme un monolithe
  -> Comprendre pourquoi les monolithes sont souvent le bon départ

================================================================================
GLOSSAIRE DU VOLUME 1
================================================================================

ADR (Architecture Decision Record) : Document formalisant une décision
  architecturale avec son contexte, sa décision et ses conséquences.

Attribut de qualité : Critère non-fonctionnel d'évaluation d'un système
  (performance, sécurité, maintenabilité...).

Couplage : Mesure de la dépendance entre modules. Objectif : faible couplage.

Cohésion : Mesure de l'unité interne d'un module. Objectif : forte cohésion.

Design Pattern : Solution réutilisable à un problème de conception récurrent.

DRY : Don't Repeat Yourself - Ne jamais dupliquer la connaissance.

KISS : Keep It Simple, Stupid - Toujours préférer la solution simple.

Scalabilité horizontale : Ajouter plus de serveurs pour gérer plus de charge.

Scalabilité verticale : Améliorer un seul serveur (plus de RAM, CPU).

SLA (Service Level Agreement) : Contrat définissant le niveau de service garanti.

SOLID : Cinq principes fondamentaux de conception orientée objet.

YAGNI : You Ain't Gonna Need It - N'implémentez que ce dont vous avez besoin.

================================================================================
FIN DU VOLUME 1 - FONDATIONS ET CONCEPTS
Prochaine étape -> architecture_monolithique.txt
================================================================================

================================================================================
     GUIDE COMPLET DES ARCHITECTURES LOGICIELLES - VOLUME 2
     Architecture Monolithique
     Pour étudiants en Génie Logiciel
================================================================================

================================================================================
CHAPITRE 1 : INTRODUCTION — QU'EST-CE QU'UN MONOLITHE ?
================================================================================

------------------------------------------------------------------------
1.1 DÉFINITION
------------------------------------------------------------------------

Une architecture monolithique est une application où TOUS les composants
fonctionnels sont construits, déployés et exécutés comme une seule unité.

C'est l'architecture la plus ancienne et la plus naturelle. Quand vous écrivez
votre premier programme, vous créez instinctivement un monolithe.

Analogie :
  Un couteau suisse — un seul outil qui fait tout :
  coupe, tire-bouchon, tournevis, pince...
  Un seul objet, multi-fonctionnel, tout intégré.

Vs.

  Une boîte à outils de menuisier :
  Scie, marteau, tournevis, perceuse — chacun spécialisé.
  (C'est l'architecture microservices)

DÉFINITION TECHNIQUE :
  Dans un monolithe, les modules (authentification, cours, paiements,
  notifications) partagent :
    - Le même processus (process)
    - La même base de code
    - La même base de données
    - Le même déploiement

------------------------------------------------------------------------
1.2 TYPES DE MONOLITHES
------------------------------------------------------------------------

Il existe plusieurs types de monolithes, du moins bon au meilleur :

TYPE 1 : LE MONOLITHE SPAGHETTI (à éviter absolument)
  Tout dans un seul fichier ou sans aucune organisation.
  Aucune séparation des responsabilités.
  Résultat d'une croissance non maîtrisée.

  Exemple visuel :
  ┌─────────────────────────────────────────────────────────┐
  │                 app.py / index.js                       │
  │                                                         │
  │  def login() { ... }    def send_email() { ... }       │
  │  sql = "SELECT * ..."   stripe.charge() { ... }        │
  │  def create_course(){ } html = "<div>..." { ... }      │
  │  pdf_gen() { ... }      validate() { ... }             │
  │  ... (3000 lignes)                                      │
  └─────────────────────────────────────────────────────────┘

TYPE 2 : LE MONOLITHE MODULAIRE (la bonne approche)
  Même déploiement unique, mais code organisé en modules distincts.
  Chaque module a sa propre responsabilité.
  Communication entre modules par des interfaces claires.

  Exemple visuel :
  ┌─────────────────────────────────────────────────────────┐
  │               MONOLITHE EDUCONNECT                      │
  │                                                         │
  │  ┌───────────┐  ┌───────────┐  ┌───────────┐          │
  │  │  Auth     │  │  Courses  │  │ Payments  │          │
  │  │  Module   │  │  Module   │  │  Module   │          │
  │  └─────┬─────┘  └─────┬─────┘  └─────┬─────┘          │
  │        │              │              │                  │
  │        └──────────────┴──────────────┘                  │
  │                       │                                 │
  │               ┌───────[BLACK_DOWN-POINTING_TRIANGLE]───────┐                        │
  │               │  PostgreSQL   │                        │
  │               └───────────────┘                        │
  └─────────────────────────────────────────────────────────┘

TYPE 3 : MODULAR MONOLITH (Monolithe Modulaire Strict)
  Forme évoluée du monolithe modulaire.
  Les modules ont des interfaces strictes — ils ne partagent pas de code
  interne directement.
  Prêt pour être découpé en microservices si nécessaire.

------------------------------------------------------------------------
1.3 POURQUOI L'ARCHITECTURE MONOLITHIQUE EXISTE
------------------------------------------------------------------------

Le monolithe est l'architecture naturelle de toute application.
Il existe pour des raisons très pratiques :

SIMPLICITÉ DE DÉVELOPPEMENT
  - Un seul projet à cloner, configurer, lancer
  - Pas de communication réseau entre composants
  - Debugging simple (tout est dans le même processus)
  - Pas de versioning d'APIs internes

SIMPLICITÉ DE DÉPLOIEMENT
  - Un seul artifact à déployer (JAR, WAR, conteneur Docker)
  - Pas de coordination entre services
  - Rollback simple

COHÉRENCE DES TRANSACTIONS
  - Toute la logique dans le même processus = transactions faciles
  - Une seule base de données = pas de problème de cohérence distribuée

PERFORMANCE INTERNE
  - Les modules s'appellent via des appels de fonction en mémoire
  - Pas de latence réseau entre composants (contrairement aux microservices)

========================================================================
CHAPITRE 2 : THÉORIE — FONCTIONNEMENT INTERNE
================================================================================

------------------------------------------------------------------------
2.1 ANATOMIE D'UN MONOLITHE WEB
------------------------------------------------------------------------

Un monolithe web typique se compose de ces couches :

┌──────────────────────────────────────────────────────────────────────┐
│                        CLIENT (Navigateur)                           │
└────────────────────────────────┬─────────────────────────────────────┘
                                 │ HTTP Requests
                                 [BLACK_DOWN-POINTING_TRIANGLE]
┌──────────────────────────────────────────────────────────────────────┐
│                      COUCHE ROUTAGE / API                            │
│                                                                      │
│   GET /courses        POST /auth/login        PUT /users/:id         │
│   GET /courses/:id    POST /enrollments       DELETE /courses/:id    │
│                                                                      │
│   Rôle : Reçoit les requêtes HTTP, dirige vers le bon contrôleur    │
└────────────────────────────────┬─────────────────────────────────────┘
                                 │
                                 [BLACK_DOWN-POINTING_TRIANGLE]
┌──────────────────────────────────────────────────────────────────────┐
│                     COUCHE CONTRÔLEURS                               │
│                                                                      │
│   AuthController     CourseController     UserController             │
│                                                                      │
│   Rôle : Valide les entrées, appelle les services, retourne JSON    │
└────────────────────────────────┬─────────────────────────────────────┘
                                 │
                                 [BLACK_DOWN-POINTING_TRIANGLE]
┌──────────────────────────────────────────────────────────────────────┐
│                     COUCHE SERVICES (Logique Métier)                 │
│                                                                      │
│   AuthService        CourseService        EnrollmentService          │
│   UserService        PaymentService       NotificationService        │
│                                                                      │
│   Rôle : Contient toute la logique métier de l'application          │
└────────────────────────────────┬─────────────────────────────────────┘
                                 │
                                 [BLACK_DOWN-POINTING_TRIANGLE]
┌──────────────────────────────────────────────────────────────────────┐
│                   COUCHE REPOSITORIES (Accès données)                │
│                                                                      │
│   UserRepository     CourseRepository     EnrollmentRepository       │
│                                                                      │
│   Rôle : Toutes les opérations base de données                      │
└────────────────────────────────┬─────────────────────────────────────┘
                                 │
                                 [BLACK_DOWN-POINTING_TRIANGLE]
┌──────────────────────────────────────────────────────────────────────┐
│                         BASE DE DONNÉES                              │
│                          PostgreSQL                                  │
│                                                                      │
│   tables: users, courses, enrollments, payments, notifications      │
└──────────────────────────────────────────────────────────────────────┘

------------------------------------------------------------------------
2.2 CYCLE DE VIE D'UNE REQUÊTE DANS UN MONOLITHE
------------------------------------------------------------------------

Exemple : Un étudiant s'inscrit à un cours

ÉTAPE 1 : La requête arrive
  POST /api/enrollments
  Body: { "course_id": "123", "payment_token": "tok_visa" }
  Headers: Authorization: Bearer <jwt_token>

ÉTAPE 2 : Middleware d'authentification
  1. Extrait le JWT du header
  2. Vérifie la signature du token
  3. Extrait l'ID utilisateur
  4. Ajoute user_id à la requête

ÉTAPE 3 : Routeur
  Dirige vers EnrollmentController.create()

ÉTAPE 4 : Contrôleur
  1. Valide les données d'entrée (course_id existe ? token valide ?)
  2. Appelle EnrollmentService.enroll(user_id, course_id, payment_token)
  3. Attend le résultat
  4. Retourne 201 Created avec les données d'inscription

ÉTAPE 5 : Service (logique métier)
  1. Vérifie que l'utilisateur n'est pas déjà inscrit
  2. Vérifie les prérequis du cours
  3. Calcule le prix (avec réductions si applicable)
  4. Appelle PaymentService.charge(token, amount)
  5. Si paiement OK -> crée l'enrollment en base
  6. Appelle NotificationService.sendEnrollmentConfirmation(user, course)
  7. Retourne l'enrollment créé

ÉTAPE 6 : Repository
  1. INSERT INTO enrollments (user_id, course_id, ...) VALUES (...)
  2. Retourne l'enrollment créé

ÉTAPE 7 : Notification
  1. Génère l'email HTML
  2. Envoie via SMTP/SendGrid

ÉTAPE 8 : Réponse
  HTTP 201 Created
  { "enrollment_id": "456", "course": {...}, "enrolled_at": "..." }

Ce flux entier se passe dans le MÊME PROCESSUS.
Avantage : Simple, cohérent, debuggable.

------------------------------------------------------------------------
2.3 AVANTAGES ET INCONVÉNIENTS
------------------------------------------------------------------------

[OK] AVANTAGES DU MONOLITHE

Développement simple :
  - Un seul dépôt (monorepo)
  - Appels de fonction directs entre modules
  - Pas de sérialisation/désérialisation entre services
  - IDE comprend tout le code en même temps
  - Debugging facile avec un seul processus à attacher

Déploiement simple :
  - Un seul artefact : npm run build -> dist/app.js
  - Un seul docker build -> une image
  - Un seul déploiement à orchestrer
  - Rollback = redéployer la version précédente

Performance interne :
  - Appels en mémoire vs appels réseau (100x plus rapide)
  - Pas de latence ajoutée entre modules
  - Transactions ACID triviales

Test simple :
  - Tests d'intégration faciles (tout dans le même process)
  - Pas de mocking de services distants
  - Coverage facile à mesurer

Cohérence :
  - Refactoring possible sur toute la codebase
  - Pas de versioning d'API interne
  - Partage facile de code commun

[X] INCONVÉNIENTS DU MONOLITHE

Scalabilité limitée :
  - On ne peut pas scaler UNE SEULE partie du système
  - Si les vidéos consomment beaucoup de CPU, tout le monolithe est affecté
  - Doit scaler l'application entière (coûteux)

Fiabilité :
  - Un bug dans le module notifications peut faire crasher TOUT le système
  - Un memory leak dans le module vidéo affecte tout
  - "Blast radius" important : une panne touche tout

Déploiements risqués :
  - Déployer une petite feature = redéployer TOUT le système
  - Risk d'introduire des régressions dans des parties non modifiées
  - Downtime pendant le déploiement (sauf avec stratégies blue/green)

Contrainte technologique :
  - Tout le monolithe doit utiliser le même langage/framework
  - Impossible d'utiliser Python pour les calculs ML et Node pour l'API

Barrière à l'entrée pour les nouveaux développeurs :
  - Comprendre un monolithe de 500k lignes = semaines/mois
  - "Peur de toucher" certaines parties
  - Risque de régression élevé

Couplage progressif :
  - Les modules tendent naturellement à devenir couplés
  - "Je vais juste accéder directement à cette table depuis ce module"
  - Résultat à terme : Big Ball of Mud

------------------------------------------------------------------------
2.4 QUAND LE MONOLITHE EST-IL ADAPTÉ ?
------------------------------------------------------------------------

Scénarios où le monolithe EST la bonne réponse :

  [OK] STARTUP / MVP (Minimum Viable Product)
    - Vous ne connaissez pas encore votre domaine
    - Rapidité > Performance
    - Équipe < 10 développeurs
    - Budget et délai serrés

  [OK] PETITE ÉQUIPE
    - < 5-10 développeurs sur le projet
    - Pas besoin d'équipes indépendantes
    - Communication directe possible entre tous

  [OK] DOMAINE PAS ENCORE COMPRIS
    - Impossible de découper en microservices si vous ne comprenez pas
      les frontières naturelles du domaine
    - "Commencer par un monolithe, puis extraire les microservices"
      — Sam Newman, auteur de "Building Microservices"

  [OK] SYSTÈME À FAIBLE TRAFIC
    - Application interne d'entreprise
    - < 1 000 utilisateurs simultanés
    - Pas de pics de charge importants

  [OK] DOMAINE MÉTIER COHÉRENT
    - Toutes les features sont fortement liées
    - Beaucoup de transactions cross-domaines

RÈGLE PRATIQUE :
  "Commencez avec un monolithe. Extrayez des services quand vous avez
   une raison spécifique de le faire."
  — Martin Fowler, "MonolithFirst"

================================================================================
CHAPITRE 3 : SCHÉMAS ET DIAGRAMMES
================================================================================

------------------------------------------------------------------------
3.1 VUE MACRO D'UN MONOLITHE
------------------------------------------------------------------------

               ┌─────────────────────────────────────────┐
               │         LOAD BALANCER                   │
               └──────────────┬──────────────────────────┘
                              │
         ┌────────────────────┼────────────────────┐
         │                   │                    │
         [BLACK_DOWN-POINTING_TRIANGLE]                   [BLACK_DOWN-POINTING_TRIANGLE]                    [BLACK_DOWN-POINTING_TRIANGLE]
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│  Instance 1     │ │  Instance 2     │ │  Instance 3     │
│  (Node.js App)  │ │  (Node.js App)  │ │  (Node.js App)  │
│                 │ │                 │ │                 │
│  Auth Module    │ │  Auth Module    │ │  Auth Module    │
│  Course Module  │ │  Course Module  │ │  Course Module  │
│  Payment Module │ │  Payment Module │ │  Payment Module │
│  Notify Module  │ │  Notify Module  │ │  Notify Module  │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
         │                   │                    │
         └───────────────────┴────────────────────┘
                             │
                    ┌────────[BLACK_DOWN-POINTING_TRIANGLE]────────┐
                    │   PostgreSQL    │
                    │   (Shared DB)   │
                    └─────────────────┘

Notes sur ce schéma :
  - 3 instances du même monolithe = scalabilité horizontale possible
  - Toutes partagent la même base de données
  - Le Load Balancer distribue les requêtes
  - Limité : on ne peut pas scaler uniquement le module "videos"

------------------------------------------------------------------------
3.2 STRUCTURE INTERNE D'UN MODULE
------------------------------------------------------------------------

Chaque module bien conçu dans un monolithe ressemble à ça :

  ┌─────────────────────────────────────────────┐
  │              MODULE COURSES                 │
  │                                             │
  │  PUBLIC INTERFACE (API interne)             │
  │  ┌─────────────────────────────────────┐   │
  │  │  CourseService                      │   │
  │  │  + createCourse(dto) -> Course       │   │
  │  │  + publishCourse(id) -> Course       │   │
  │  │  + enrollStudent(userId, courseId)  │   │
  │  └──────────────┬──────────────────────┘   │
  │                 │                           │
  │  PRIVATE INTERNALS                          │
  │  ┌──────────────┴──────────────────────┐   │
  │  │  CourseRepository (DB Access)       │   │
  │  │  CourseValidator (Validation)       │   │
  │  │  CoursePricingEngine (Calcul prix)  │   │
  │  └─────────────────────────────────────┘   │
  └─────────────────────────────────────────────┘

Règle : D'autres modules ne peuvent appeler QUE l'interface publique.
        Jamais accéder directement aux internals d'un autre module.

------------------------------------------------------------------------
3.3 FLUX D'AUTHENTIFICATION DANS LE MONOLITHE
------------------------------------------------------------------------

ENREGISTREMENT :

  Client          API Gateway      AuthController     AuthService      UserRepo
    │                  │                │                │               │
    │ POST /register   │                │                │               │
    │─────────────────->│                │                │               │
    │                  │ route()        │                │               │
    │                  │───────────────->│                │               │
    │                  │               │ validate(dto)  │               │
    │                  │               │───────────────->│               │
    │                  │               │               │ findByEmail()  │
    │                  │               │               │───────────────->│
    │                  │               │               │  null          │
    │                  │               │               │<-───────────────│
    │                  │               │               │ hashPassword() │
    │                  │               │               │ createUser()   │
    │                  │               │               │───────────────->│
    │                  │               │               │  user          │
    │                  │               │               │<-───────────────│
    │                  │               │  {user,token} │                │
    │                  │               │<-───────────────│               │
    │                  │ 201 Created   │                │               │
    │                  │<-──────────────│                │               │
    │ {user, token}    │                │                │               │
    │<-─────────────────│                │                │               │

================================================================================
CHAPITRE 4 : POURQUOI UTILISER LE MONOLITHE ?
================================================================================

------------------------------------------------------------------------
4.1 PRODUCTIVITÉ EN DÉMARRAGE
------------------------------------------------------------------------

VITESSE DE DÉVELOPPEMENT :
  Une startup a besoin de valider ses hypothèses rapidement.
  Avec un monolithe :
    - Semaine 1-2 : Scaffold de l'application
    - Semaine 3-4 : Fonctionnalités core
    - Mois 2 : MVP déployé et testable

  Avec des microservices dès le départ :
    - Semaines 1-4 : Infrastructure (Kubernetes, Service Discovery, etc.)
    - Mois 2-3 : Premières fonctionnalités
    - Mois 6 : MVP déployé
    -> La startup peut avoir perdu ses premiers clients ou son financement

EXEMPLE RÉEL :
  Instagram (2010-2012) :
    - Monolithe Python/Django
    - 13 employés, des millions d'utilisateurs
    - Architecture simple = focus sur le produit
    - Rachetés par Facebook pour 1 milliard $
    - Décision de migrer vers microservices APRÈS le succès

------------------------------------------------------------------------
4.2 MAINTENABILITÉ DU MONOLITHE MODULAIRE
------------------------------------------------------------------------

Un monolithe BIEN ÉCRIT est très maintenable :

REFACTORING FACILITÉ :
  - L'IDE peut analyser l'ensemble du code
  - Rename d'une méthode = changé partout automatiquement
  - Extraction de code dans un nouveau module = simple

DÉTECTION D'ERREURS :
  - Le compilateur/linter peut détecter les erreurs entre modules
  - Tests d'intégration exécutables localement
  - Pas besoin de "contrats d'API" complexes entre services

TRAÇABILITÉ :
  - Stack traces complètes et lisibles
  - Debugging pas-à-pas dans tout le code
  - Pas de "distributed tracing" nécessaire

================================================================================
CHAPITRE 5 : QUAND UTILISER LE MONOLITHE ?
================================================================================

------------------------------------------------------------------------
5.1 GUIDE DÉCISIONNEL
------------------------------------------------------------------------

Répondez à ces questions pour savoir si le monolithe est adapté :

  QUESTION                                    OUI    NON
  ──────────────────────────────────────────────────────
  Votre équipe fait < 10 développeurs ?        [OK]     -> Microservices
  C'est un MVP ou prototype ?                  [OK]     ─
  Le domaine est-il bien compris ?             ─     [OK] (commencez monolithe)
  Avez-vous besoin de déployer < 1x/semaine ?  [OK]     ─
  Votre trafic est-il < 100k users/jour ?      [OK]     ─
  Avez-vous des ressources DevOps limitées ?   [OK]     ─
  Les modules sont-ils fortement couplés ?     [OK]     ─
  ──────────────────────────────────────────────────────

Si vous avez majorité OUI -> Commencez avec un monolithe.

------------------------------------------------------------------------
5.2 SCÉNARIOS PROFESSIONNELS
------------------------------------------------------------------------

SCÉNARIO A : STARTUP FINTECH AFRICAINE
  Situation : 2 développeurs, 6 mois pour sortir un MVP de transfert d'argent
  Choix : Monolithe Node.js + PostgreSQL
  Raison : Vitesse critique, équipe petite, domaine encore à valider
  Stack : Express.js + Prisma ORM + PostgreSQL + Docker
  Résultat : MVP en 8 semaines, 1er client en mois 3

SCÉNARIO B : APPLICATION INTERNE D'ENTREPRISE
  Situation : Application RH pour 500 employés, 1 développeur responsable
  Choix : Monolithe Django + PostgreSQL
  Raison : Faible trafic, maintenance par 1 personne, domaine connu
  Stack : Django + DRF + PostgreSQL + Redis
  Résultat : Stable depuis 5 ans, maintenance facile

SCÉNARIO C : APPLICATION ÉDUCATIVE RÉGIONALE
  Situation : Plateforme scolaire pour 50 000 étudiants d'une région
  Choix : Monolithe modulaire Rails + PostgreSQL
  Raison : Équipe de 5, budget limité, scalabilité modérée suffisante
  Stack : Ruby on Rails + PostgreSQL + Redis (sessions+cache)
  Résultat : Scale jusqu'à 50k avec optimisations DB et cache

================================================================================
CHAPITRE 6 : IMPLÉMENTATION PRATIQUE — EDUCONNECT MONOLITHE
================================================================================

------------------------------------------------------------------------
6.1 STRUCTURE DU PROJET
------------------------------------------------------------------------

  educonnect-monolith/
  ├── src/
  │   ├── config/           # Configuration (env, database, etc.)
  │   │   ├── database.js
  │   │   ├── env.js
  │   │   └── logger.js
  │   │
  │   ├── modules/          # Modules fonctionnels
  │   │   ├── auth/
  │   │   │   ├── auth.controller.js
  │   │   │   ├── auth.service.js
  │   │   │   ├── auth.repository.js
  │   │   │   ├── auth.routes.js
  │   │   │   └── auth.dto.js    # Data Transfer Objects
  │   │   │
  │   │   ├── courses/
  │   │   │   ├── course.controller.js
  │   │   │   ├── course.service.js
  │   │   │   ├── course.repository.js
  │   │   │   ├── course.routes.js
  │   │   │   └── course.dto.js
  │   │   │
  │   │   ├── enrollments/
  │   │   │   ├── enrollment.controller.js
  │   │   │   ├── enrollment.service.js
  │   │   │   ├── enrollment.repository.js
  │   │   │   └── enrollment.routes.js
  │   │   │
  │   │   ├── users/
  │   │   │   ├── user.controller.js
  │   │   │   ├── user.service.js
  │   │   │   ├── user.repository.js
  │   │   │   └── user.routes.js
  │   │   │
  │   │   └── payments/
  │   │       ├── payment.controller.js
  │   │       ├── payment.service.js
  │   │       └── payment.routes.js
  │   │
  │   ├── shared/           # Code partagé entre modules
  │   │   ├── middleware/
  │   │   │   ├── auth.middleware.js   # Vérification JWT
  │   │   │   ├── error.middleware.js  # Gestion des erreurs
  │   │   │   └── logger.middleware.js
  │   │   │
  │   │   ├── utils/
  │   │   │   ├── validator.js
  │   │   │   ├── paginator.js
  │   │   │   └── hash.js
  │   │   │
  │   │   └── errors/
  │   │       ├── AppError.js
  │   │       ├── ValidationError.js
  │   │       └── NotFoundError.js
  │   │
  │   └── app.js            # Point d'entrée de l'application
  │
  ├── migrations/           # Migrations de base de données
  ├── tests/
  │   ├── unit/
  │   ├── integration/
  │   └── e2e/
  ├── .env.example
  ├── docker-compose.yml
  └── package.json

------------------------------------------------------------------------
6.2 CODE COMPLET ET COMMENTÉ — app.js (Point d'entrée)
------------------------------------------------------------------------

  // ============================================================
  // app.js — Point d'entrée du monolithe EduConnect
  // ============================================================

  // Import du framework Express.js
  const express = require('express');

  // Import des modules de sécurité
  const helmet = require('helmet');   // Sécurise les headers HTTP
  const cors = require('cors');       // Gère les requêtes Cross-Origin
  const rateLimit = require('express-rate-limit'); // Limite les requêtes

  // Import des routes de chaque module
  const authRoutes = require('./modules/auth/auth.routes');
  const courseRoutes = require('./modules/courses/course.routes');
  const enrollmentRoutes = require('./modules/enrollments/enrollment.routes');
  const userRoutes = require('./modules/users/user.routes');

  // Import des middlewares partagés
  const errorMiddleware = require('./shared/middleware/error.middleware');
  const loggerMiddleware = require('./shared/middleware/logger.middleware');

  // Import de la configuration
  const { connectDatabase } = require('./config/database');
  const logger = require('./config/logger');

  // ============================================================
  // CRÉATION DE L'APPLICATION
  // ============================================================
  const app = express();

  // ============================================================
  // CONFIGURATION DES MIDDLEWARES GLOBAUX
  // L'ordre des middlewares EST IMPORTANT en Express.js
  // Ils s'exécutent dans l'ordre de déclaration
  // ============================================================

  // 1. Sécurité : helmet ajoute des headers HTTP sécurisés
  //    Ex: X-Content-Type-Options, X-Frame-Options, etc.
  app.use(helmet());

  // 2. CORS : permet au frontend (autre domaine) d'accéder à l'API
  app.use(cors({
    origin: process.env.FRONTEND_URL || 'http://localhost:3000',
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
    allowedHeaders: ['Content-Type', 'Authorization']
  }));

  // 3. Rate Limiting : max 100 requêtes par IP par 15 minutes
  //    Protège contre les attaques par force brute
  const limiter = rateLimit({
    windowMs: 15 * 60 * 1000,  // 15 minutes en millisecondes
    max: 100,                   // Maximum 100 requêtes par fenêtre
    message: 'Trop de requêtes depuis cette IP, réessayez dans 15 minutes'
  });
  app.use('/api', limiter);  // Appliqué uniquement aux routes /api

  // 4. Parsing JSON : nécessaire pour lire req.body en JSON
  //    limite : 10mb pour éviter les attaques par grands payloads
  app.use(express.json({ limit: '10mb' }));

  // 5. Parsing URL-encoded : pour les formulaires HTML classiques
  app.use(express.urlencoded({ extended: true, limit: '10mb' }));

  // 6. Logger : enregistre chaque requête (URL, méthode, durée)
  app.use(loggerMiddleware);

  // ============================================================
  // ROUTE DE SANTÉ (Health Check)
  // Utilisée par Kubernetes/Load Balancer pour vérifier si
  // l'application est vivante
  // ============================================================
  app.get('/health', (req, res) => {
    res.status(200).json({
      status: 'healthy',
      timestamp: new Date().toISOString(),
      version: process.env.APP_VERSION || '1.0.0',
      environment: process.env.NODE_ENV || 'development'
    });
  });

  // ============================================================
  // MONTAGE DES ROUTES DE CHAQUE MODULE
  // Chaque module a son propre préfixe d'URL
  // ============================================================
  app.use('/api/v1/auth', authRoutes);           // Routes authentification
  app.use('/api/v1/courses', courseRoutes);       // Routes cours
  app.use('/api/v1/enrollments', enrollmentRoutes); // Routes inscriptions
  app.use('/api/v1/users', userRoutes);           // Routes utilisateurs

  // ============================================================
  // GESTION DES ROUTES NON TROUVÉES (404)
  // Doit être APRÈS toutes les routes définies
  // ============================================================
  app.use('*', (req, res) => {
    res.status(404).json({
      error: 'Not Found',
      message: `Route ${req.originalUrl} non trouvée`,
      timestamp: new Date().toISOString()
    });
  });

  // ============================================================
  // GESTION GLOBALE DES ERREURS
  // Doit être LE DERNIER middleware (4 paramètres pour Express)
  // ============================================================
  app.use(errorMiddleware);

  // ============================================================
  // DÉMARRAGE DU SERVEUR
  // ============================================================
  const PORT = process.env.PORT || 3000;

  async function startServer() {
    try {
      // 1. Connexion à la base de données
      await connectDatabase();
      logger.info('[OK] Base de données connectée');

      // 2. Démarrage du serveur HTTP
      app.listen(PORT, () => {
        logger.info(`[OK] Serveur EduConnect démarré sur le port ${PORT}`);
        logger.info(`[MONDE] Environnement : ${process.env.NODE_ENV}`);
        logger.info(`[NOTE] API disponible : http://localhost:${PORT}/api/v1`);
      });

    } catch (error) {
      logger.error('[X] Erreur au démarrage du serveur:', error);
      process.exit(1);  // Arrêt forcé si échec critique
    }
  }

  startServer();

  module.exports = app;  // Export pour les tests

------------------------------------------------------------------------
6.3 MODULE AUTH — SERVICE COMPLET ET COMMENTÉ
------------------------------------------------------------------------

  // ============================================================
  // modules/auth/auth.service.js
  // Service d'authentification - Contient toute la logique métier
  // ============================================================

  const bcrypt = require('bcrypt');         // Pour hasher les mots de passe
  const jwt = require('jsonwebtoken');      // Pour créer/vérifier les JWT
  const { v4: uuidv4 } = require('uuid');  // Pour générer des UUIDs

  // Import du repository (accès base de données)
  const UserRepository = require('../users/user.repository');

  // Import des erreurs personnalisées
  const AppError = require('../../shared/errors/AppError');

  // Constantes de configuration
  const SALT_ROUNDS = 12;       // Coût du hashing bcrypt (12 est recommandé)
  const JWT_EXPIRES_IN = '7d';  // Durée de validité du token

  class AuthService {

    // ──────────────────────────────────────────────────────
    // INSCRIPTION D'UN NOUVEL UTILISATEUR
    // ──────────────────────────────────────────────────────
    async register({ email, password, firstName, lastName, role = 'student' }) {

      // ÉTAPE 1 : Vérifier si l'email existe déjà
      // Cette vérification est critique pour éviter les doublons
      const existingUser = await UserRepository.findByEmail(email);
      if (existingUser) {
        // On retourne une erreur 409 Conflict, pas 400
        // car l'email existe déjà (conflit de ressource)
        throw new AppError('Un compte avec cet email existe déjà', 409);
      }

      // ÉTAPE 2 : Valider la complexité du mot de passe
      // En production, cette validation serait dans un DTO/validator
      if (password.length < 8) {
        throw new AppError('Le mot de passe doit contenir au moins 8 caractères', 400);
      }

      // ÉTAPE 3 : Hasher le mot de passe
      // JAMAIS stocker un mot de passe en clair en base de données
      // bcrypt.hash est asynchrone et utilise SALT_ROUNDS comme facteur de coût
      // Plus SALT_ROUNDS est élevé, plus c'est sécurisé mais plus lent
      // 12 rounds = ~300ms, bon équilibre sécurité/performance
      const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);

      // ÉTAPE 4 : Préparer les données utilisateur
      const userData = {
        id: uuidv4(),           // Identifiant unique aléatoire
        email: email.toLowerCase().trim(),  // Normaliser l'email
        password: hashedPassword,
        firstName: firstName.trim(),
        lastName: lastName.trim(),
        role,                   // 'student' ou 'instructor'
        isEmailVerified: false, // L'email n'est pas encore vérifié
        emailVerificationToken: uuidv4(), // Token pour vérifier l'email
        createdAt: new Date(),
        updatedAt: new Date()
      };

      // ÉTAPE 5 : Sauvegarder en base de données
      const user = await UserRepository.create(userData);

      // ÉTAPE 6 : Envoyer l'email de vérification
      // Note : Dans un monolithe, on appelle directement le service email
      // On NE PAS await pour ne pas bloquer la réponse
      // On gère les erreurs email séparément (non bloquant)
      this._sendVerificationEmail(user).catch(err => {
        console.error('Erreur envoi email vérification:', err);
        // L'utilisateur est créé même si l'email échoue
        // On peut re-essayer plus tard
      });

      // ÉTAPE 7 : Générer le JWT de connexion
      const token = this._generateJWT(user);

      // ÉTAPE 8 : Retourner l'utilisateur (sans le mot de passe !)
      // Règle de sécurité : ne JAMAIS retourner le hash du mot de passe
      return {
        user: this._sanitizeUser(user),  // Supprime les champs sensibles
        token
      };
    }

    // ──────────────────────────────────────────────────────
    // CONNEXION D'UN UTILISATEUR EXISTANT
    // ──────────────────────────────────────────────────────
    async login({ email, password }) {

      // ÉTAPE 1 : Trouver l'utilisateur par email
      const user = await UserRepository.findByEmail(email.toLowerCase());

      // SÉCURITÉ CRITIQUE : Ne pas divulguer si l'email existe
      // Message générique pour les deux cas (email inexistant + mauvais password)
      // Évite l'énumération des emails (User Enumeration Attack)
      if (!user) {
        throw new AppError('Email ou mot de passe incorrect', 401);
      }

      // ÉTAPE 2 : Vérifier le mot de passe
      // bcrypt.compare compare de manière sécurisée (timing-safe)
      const isPasswordValid = await bcrypt.compare(password, user.password);
      if (!isPasswordValid) {
        // Même message que ci-dessus - pas de divulgation d'information
        throw new AppError('Email ou mot de passe incorrect', 401);
      }

      // ÉTAPE 3 : Vérifier si le compte est actif
      if (user.isDisabled) {
        throw new AppError('Ce compte a été désactivé. Contactez le support.', 403);
      }

      // ÉTAPE 4 : Mettre à jour la date de dernière connexion
      await UserRepository.updateLastLogin(user.id, new Date());

      // ÉTAPE 5 : Générer et retourner le JWT
      const token = this._generateJWT(user);

      return {
        user: this._sanitizeUser(user),
        token
      };
    }

    // ──────────────────────────────────────────────────────
    // VÉRIFICATION D'UN TOKEN JWT
    // Utilisé par le middleware d'authentification
    // ──────────────────────────────────────────────────────
    async verifyToken(token) {
      try {
        // Vérifie la signature et l'expiration du JWT
        const decoded = jwt.verify(token, process.env.JWT_SECRET);

        // Récupérer l'utilisateur actuel (au cas où il aurait été désactivé)
        const user = await UserRepository.findById(decoded.userId);

        if (!user || user.isDisabled) {
          throw new AppError('Token invalide', 401);
        }

        return user;

      } catch (error) {
        if (error.name === 'JsonWebTokenError') {
          throw new AppError('Token invalide', 401);
        }
        if (error.name === 'TokenExpiredError') {
          throw new AppError('Token expiré, veuillez vous reconnecter', 401);
        }
        throw error;
      }
    }

    // ──────────────────────────────────────────────────────
    // MÉTHODES PRIVÉES (convention : préfixe _ en JavaScript)
    // ──────────────────────────────────────────────────────

    // Génère un token JWT signé
    _generateJWT(user) {
      return jwt.sign(
        {
          // Payload du JWT (informations incluses dans le token)
          // Ne jamais inclure d'informations sensibles (mot de passe, etc.)
          userId: user.id,
          email: user.email,
          role: user.role
        },
        process.env.JWT_SECRET,  // Clé secrète pour signer le token
        {
          expiresIn: JWT_EXPIRES_IN,  // Expiration
          algorithm: 'HS256'           // Algorithme de signature
        }
      );
    }

    // Supprime les champs sensibles de l'objet utilisateur
    _sanitizeUser(user) {
      const { password, emailVerificationToken, ...safeUser } = user;
      return safeUser;
    }

    // Envoie l'email de vérification
    async _sendVerificationEmail(user) {
      // Dans le monolithe, appel direct au service email
      const EmailService = require('../notifications/email.service');
      await EmailService.sendVerificationEmail(
        user.email,
        user.emailVerificationToken
      );
    }
  }

  module.exports = new AuthService();  // Export singleton

------------------------------------------------------------------------
6.4 MODULE AUTH — CONTRÔLEUR COMMENTÉ
------------------------------------------------------------------------

  // ============================================================
  // modules/auth/auth.controller.js
  // Contrôleur - Interface entre HTTP et la logique métier
  // ============================================================

  const AuthService = require('./auth.service');
  const { registerSchema, loginSchema } = require('./auth.dto');

  class AuthController {

    // ──────────────────────────────────────────────────────
    // POST /api/v1/auth/register
    // ──────────────────────────────────────────────────────
    async register(req, res, next) {
      try {
        // ÉTAPE 1 : Valider les données d'entrée avec le schéma DTO
        // Le DTO définit le format attendu et les règles de validation
        const { error, value } = registerSchema.validate(req.body);
        if (error) {
          // Retourner 400 Bad Request si validation échoue
          return res.status(400).json({
            error: 'Validation échouée',
            details: error.details.map(d => d.message)
          });
        }

        // ÉTAPE 2 : Appeler le service métier
        const result = await AuthService.register(value);

        // ÉTAPE 3 : Retourner la réponse HTTP 201 Created
        return res.status(201).json({
          message: 'Compte créé avec succès',
          data: result
        });

      } catch (error) {
        // Passe l'erreur au middleware de gestion d'erreurs global
        next(error);
      }
    }

    // ──────────────────────────────────────────────────────
    // POST /api/v1/auth/login
    // ──────────────────────────────────────────────────────
    async login(req, res, next) {
      try {
        // Validation des données d'entrée
        const { error, value } = loginSchema.validate(req.body);
        if (error) {
          return res.status(400).json({
            error: 'Données invalides',
            details: error.details.map(d => d.message)
          });
        }

        // Appel du service d'authentification
        const result = await AuthService.login(value);

        // Réponse 200 OK avec token et infos utilisateur
        return res.status(200).json({
          message: 'Connexion réussie',
          data: result
        });

      } catch (error) {
        next(error);
      }
    }

    // ──────────────────────────────────────────────────────
    // GET /api/v1/auth/me
    // Récupère le profil de l'utilisateur connecté
    // ──────────────────────────────────────────────────────
    async me(req, res, next) {
      try {
        // req.user est ajouté par le middleware d'authentification
        // qui vérifie le JWT avant d'atteindre ce contrôleur
        const user = req.user;

        return res.status(200).json({
          data: user
        });

      } catch (error) {
        next(error);
      }
    }
  }

  module.exports = new AuthController();

------------------------------------------------------------------------
6.5 MIDDLEWARE D'AUTHENTIFICATION
------------------------------------------------------------------------

  // ============================================================
  // shared/middleware/auth.middleware.js
  // Middleware JWT - Vérifie l'authentification de chaque requête
  // ============================================================

  const AuthService = require('../../modules/auth/auth.service');

  // Ce middleware est une fonction Express standard avec 3 params
  // Il est exécuté AVANT le contrôleur pour les routes protégées
  const authMiddleware = async (req, res, next) => {

    try {
      // ÉTAPE 1 : Extraire le token du header Authorization
      // Format attendu : "Bearer <token>"
      const authHeader = req.headers.authorization;

      if (!authHeader || !authHeader.startsWith('Bearer ')) {
        return res.status(401).json({
          error: 'Authentification requise',
          message: 'Fournissez un token Bearer dans le header Authorization'
        });
      }

      // Extraire le token (supprimer "Bearer ")
      const token = authHeader.substring(7);

      // ÉTAPE 2 : Vérifier le token
      const user = await AuthService.verifyToken(token);

      // ÉTAPE 3 : Attacher l'utilisateur à la requête
      // Les contrôleurs suivants auront accès à req.user
      req.user = user;

      // ÉTAPE 4 : Passer au middleware/contrôleur suivant
      next();

    } catch (error) {
      // Si le token est invalide, retourner 401
      return res.status(401).json({
        error: 'Non autorisé',
        message: error.message
      });
    }
  };

  // Middleware pour vérifier les rôles
  // Utilisation : authMiddleware, roleMiddleware(['instructor', 'admin'])
  const roleMiddleware = (allowedRoles) => {
    return (req, res, next) => {
      // req.user est garanti présent (authMiddleware exécuté avant)
      if (!allowedRoles.includes(req.user.role)) {
        return res.status(403).json({
          error: 'Accès refusé',
          message: `Cette action requiert les rôles : ${allowedRoles.join(', ')}`
        });
      }
      next();
    };
  };

  module.exports = { authMiddleware, roleMiddleware };

------------------------------------------------------------------------
6.6 ROUTES — AUTHENTIFICATION
------------------------------------------------------------------------

  // ============================================================
  // modules/auth/auth.routes.js
  // Définition des routes du module Auth
  // ============================================================

  const express = require('express');
  const router = express.Router();

  const AuthController = require('./auth.controller');
  const { authMiddleware } = require('../../shared/middleware/auth.middleware');

  // Routes publiques (pas d'authentification requise)
  // POST /api/v1/auth/register
  router.post('/register', AuthController.register.bind(AuthController));

  // POST /api/v1/auth/login
  router.post('/login', AuthController.login.bind(AuthController));

  // Routes protégées (authMiddleware appliqué)
  // GET /api/v1/auth/me -> Profil de l'utilisateur connecté
  router.get('/me', authMiddleware, AuthController.me.bind(AuthController));

  module.exports = router;

------------------------------------------------------------------------
6.7 SCHÉMA BASE DE DONNÉES — MIGRATIONS
------------------------------------------------------------------------

  -- ============================================================
  -- migrations/001_create_users_table.sql
  -- Première migration : Création de la table users
  -- ============================================================

  -- Créer l'extension UUID si pas déjà présente
  CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

  -- Table des utilisateurs
  CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    email VARCHAR(255) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL,  -- Toujours hashé (bcrypt)
    first_name VARCHAR(100) NOT NULL,
    last_name VARCHAR(100) NOT NULL,
    role VARCHAR(20) NOT NULL DEFAULT 'student'
      CHECK (role IN ('student', 'instructor', 'admin')),
    is_email_verified BOOLEAN DEFAULT FALSE,
    email_verification_token VARCHAR(255),
    is_disabled BOOLEAN DEFAULT FALSE,
    last_login_at TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  );

  -- Index pour les requêtes fréquentes
  CREATE INDEX idx_users_email ON users(email);
  CREATE INDEX idx_users_role ON users(role);

  -- ============================================================
  -- migrations/002_create_courses_table.sql
  -- ============================================================

  CREATE TABLE courses (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    title VARCHAR(255) NOT NULL,
    description TEXT,
    instructor_id UUID NOT NULL REFERENCES users(id),
    price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
    currency VARCHAR(3) DEFAULT 'EUR',
    thumbnail_url VARCHAR(500),
    category VARCHAR(100),
    level VARCHAR(20) CHECK (level IN ('beginner', 'intermediate', 'advanced')),
    status VARCHAR(20) DEFAULT 'draft'
      CHECK (status IN ('draft', 'published', 'archived')),
    prerequisites TEXT[], -- Array des prérequis
    language VARCHAR(10) DEFAULT 'fr',
    duration_minutes INTEGER, -- Durée totale estimée
    published_at TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  );

  CREATE INDEX idx_courses_instructor ON courses(instructor_id);
  CREATE INDEX idx_courses_status ON courses(status);
  CREATE INDEX idx_courses_category ON courses(category);

  -- ============================================================
  -- migrations/003_create_enrollments_table.sql
  -- ============================================================

  CREATE TABLE enrollments (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID NOT NULL REFERENCES users(id),
    course_id UUID NOT NULL REFERENCES courses(id),
    enrolled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    completed_at TIMESTAMP,
    progress_percentage INTEGER DEFAULT 0
      CHECK (progress_percentage BETWEEN 0 AND 100),
    status VARCHAR(20) DEFAULT 'active'
      CHECK (status IN ('active', 'completed', 'refunded', 'expired')),
    payment_id UUID, -- Référence au paiement
    UNIQUE(user_id, course_id)  -- Empêche les doublons d'inscription
  );

  CREATE INDEX idx_enrollments_user ON enrollments(user_id);
  CREATE INDEX idx_enrollments_course ON enrollments(course_id);

------------------------------------------------------------------------
6.8 CONFIGURATION DOCKER — DÉPLOIEMENT MONOLITHE
------------------------------------------------------------------------

  # ============================================================
  # Dockerfile — Image Docker du monolithe EduConnect
  # ============================================================

  # Image de base Node.js LTS (Long Term Support)
  # alpine = version légère basée sur Alpine Linux (~50MB vs ~900MB)
  FROM node:20-alpine

  # Définir le répertoire de travail dans le conteneur
  WORKDIR /app

  # ÉTAPE 1 : Copier les fichiers de dépendances en premier
  # Ceci optimise le cache Docker : si package.json ne change pas,
  # cette couche est réutilisée sans réinstaller les dépendances
  COPY package*.json ./

  # ÉTAPE 2 : Installer uniquement les dépendances de production
  # --omit=dev exclut les devDependencies (jest, nodemon, etc.)
  RUN npm ci --omit=dev

  # ÉTAPE 3 : Copier le code source de l'application
  COPY src/ ./src/

  # ÉTAPE 4 : Créer un utilisateur non-root pour la sécurité
  # Principe du moindre privilège : l'app ne tourne pas en root
  RUN addgroup -S appgroup && adduser -S appuser -G appgroup
  USER appuser

  # Le conteneur écoutera sur le port 3000
  EXPOSE 3000

  # Commande de démarrage
  CMD ["node", "src/app.js"]

  ---

  # ============================================================
  # docker-compose.yml — Environnement de développement complet
  # ============================================================

  version: '3.8'

  services:

    # L'application EduConnect (monolithe)
    app:
      build: .
      ports:
        - "3000:3000"      # Port local:Port conteneur
      environment:
        NODE_ENV: development
        PORT: 3000
        DATABASE_URL: postgresql://educonnect:password@postgres:5432/educonnect_db
        REDIS_URL: redis://redis:6379
        JWT_SECRET: votre_secret_jwt_super_securise_en_prod_utiliser_256bits
        FRONTEND_URL: http://localhost:3001
      depends_on:
        postgres:
          condition: service_healthy  # Attendre que Postgres soit prêt
        redis:
          condition: service_healthy
      volumes:
        - ./src:/app/src   # Hot reload en développement

    # Base de données PostgreSQL
    postgres:
      image: postgres:16-alpine
      environment:
        POSTGRES_DB: educonnect_db
        POSTGRES_USER: educonnect
        POSTGRES_PASSWORD: password
      ports:
        - "5432:5432"
      volumes:
        - postgres_data:/var/lib/postgresql/data  # Persistance des données
        - ./migrations:/docker-entrypoint-initdb.d  # Auto-execute migrations
      healthcheck:
        test: ["CMD-SHELL", "pg_isready -U educonnect -d educonnect_db"]
        interval: 5s
        timeout: 5s
        retries: 5

    # Cache Redis
    redis:
      image: redis:7-alpine
      ports:
        - "6379:6379"
      healthcheck:
        test: ["CMD", "redis-cli", "ping"]
        interval: 5s
        timeout: 3s
        retries: 5

  # Volumes nommés pour la persistance des données
  volumes:
    postgres_data:

================================================================================
CHAPITRE 7 : BONNES PRATIQUES POUR LE MONOLITHE
================================================================================

------------------------------------------------------------------------
7.1 ORGANISATION DU CODE
------------------------------------------------------------------------

RÈGLE 1 : MODULES AVEC INTERFACES EXPLICITES
  Chaque module expose une interface publique claire.
  D'autres modules ne peuvent PAS accéder aux internals.

  [OK] BON :
    // Dans enrollment.service.js
    const CourseService = require('../courses/course.service');  // Interface publique
    const course = await CourseService.findById(courseId);

  [X] MAUVAIS :
    // Dans enrollment.service.js
    const CourseRepository = require('../courses/course.repository');  // Internal !
    const course = await CourseRepository.findById(courseId);  // Couplage fort

RÈGLE 2 : NE PAS PARTAGER LES MODÈLES DE BASE DE DONNÉES ENTRE MODULES
  Si le module Courses change son schéma, Enrollments ne doit pas être impacté.

RÈGLE 3 : EVENTS INTERNES POUR DÉCOUPLAGE
  Utiliser des événements Node.js pour communiquer entre modules de manière
  asynchrone et découplée.

  // Dans enrollment.service.js (après inscription réussie)
  const EventEmitter = require('events');
  const eventBus = require('../../shared/eventBus');

  // Émettre un événement
  eventBus.emit('enrollment:created', {
    userId: user.id,
    courseId: course.id,
    enrolledAt: new Date()
  });

  // Dans notification.service.js (à l'initialisation)
  eventBus.on('enrollment:created', async (data) => {
    await sendEnrollmentConfirmationEmail(data);
  });

  Avantage : Si le service de notification tombe, l'inscription n'est pas affectée.
  Préparation : Cette approche avec events prépare la migration vers microservices.

------------------------------------------------------------------------
7.2 GESTION DES ERREURS
------------------------------------------------------------------------

  // ============================================================
  // shared/errors/AppError.js
  // Classe d'erreur personnalisée pour toutes les erreurs applicatives
  // ============================================================

  class AppError extends Error {
    constructor(message, statusCode = 500, isOperational = true) {
      // Appel du constructeur parent (Error)
      super(message);

      // Code HTTP de l'erreur
      this.statusCode = statusCode;

      // isOperational = erreur gérée (400, 401, 404, etc.)
      // vs erreur de programmation (bug - 500)
      this.isOperational = isOperational;

      // Capture la stack trace (chemin de l'erreur dans le code)
      Error.captureStackTrace(this, this.constructor);
    }
  }

  // Types d'erreurs spécifiques
  class ValidationError extends AppError {
    constructor(message, details = []) {
      super(message, 400);
      this.details = details;
    }
  }

  class NotFoundError extends AppError {
    constructor(resource) {
      super(`${resource} non trouvé`, 404);
    }
  }

  class UnauthorizedError extends AppError {
    constructor(message = 'Non autorisé') {
      super(message, 401);
    }
  }

  module.exports = { AppError, ValidationError, NotFoundError, UnauthorizedError };

  // ============================================================
  // shared/middleware/error.middleware.js
  // Middleware de gestion globale des erreurs
  // ============================================================

  const logger = require('../../config/logger');

  const errorMiddleware = (error, req, res, next) => {
    // Valeurs par défaut
    let statusCode = error.statusCode || 500;
    let message = error.message || 'Erreur interne du serveur';

    // Log de l'erreur
    if (statusCode >= 500) {
      logger.error('Erreur serveur:', {
        error: error.message,
        stack: error.stack,
        url: req.url,
        method: req.method,
        body: req.body
      });
    } else {
      logger.warn('Erreur client:', {
        error: error.message,
        url: req.url,
        statusCode
      });
    }

    // En production, ne pas exposer les détails des erreurs 500
    if (process.env.NODE_ENV === 'production' && statusCode === 500) {
      message = 'Une erreur interne est survenue';
    }

    // Réponse HTTP standardisée
    res.status(statusCode).json({
      error: true,
      message,
      statusCode,
      timestamp: new Date().toISOString(),
      // En développement seulement : stack trace
      ...(process.env.NODE_ENV === 'development' && { stack: error.stack })
    });
  };

  module.exports = errorMiddleware;

------------------------------------------------------------------------
7.3 LOGGING ET OBSERVABILITÉ
------------------------------------------------------------------------

  // ============================================================
  // config/logger.js
  // Configuration du logger centralisé avec Winston
  // ============================================================

  const winston = require('winston');

  // Format de log structuré JSON (meilleur pour les outils d'analyse)
  const jsonFormat = winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  );

  // Format lisible pour le développement
  const devFormat = winston.format.combine(
    winston.format.colorize(),
    winston.format.timestamp({ format: 'HH:mm:ss' }),
    winston.format.printf(({ level, message, timestamp, ...meta }) =>
      `${timestamp} [${level}] ${message} ${Object.keys(meta).length ? JSON.stringify(meta) : ''}`
    )
  );

  const logger = winston.createLogger({
    level: process.env.LOG_LEVEL || 'info',
    format: process.env.NODE_ENV === 'production' ? jsonFormat : devFormat,
    transports: [
      // Toujours logger dans la console
      new winston.transports.Console(),

      // En production : logger dans des fichiers
      ...(process.env.NODE_ENV === 'production' ? [
        // Fichier pour les erreurs uniquement
        new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
        // Fichier pour tous les logs
        new winston.transports.File({ filename: 'logs/combined.log' })
      ] : [])
    ]
  });

  module.exports = logger;

================================================================================
CHAPITRE 8 : ERREURS FRÉQUENTES AVEC LE MONOLITHE
================================================================================

------------------------------------------------------------------------
ERREUR 1 : LE MONOLITHE SPAGHETTI
------------------------------------------------------------------------

Symptôme :
  app.js de 5000 lignes où tout est mélangé.

Comment ça arrive :
  "J'ajoute juste une petite feature ici, c'est plus rapide"
  × 50 développeurs × 2 ans = chaos

Solution :
  Imposer une structure dès le premier jour.
  Code review strict sur la séparation des modules.
  Linting avec des règles d'import entre modules.

------------------------------------------------------------------------
ERREUR 2 : ACCÈS DIRECT À LA BASE DE DONNÉES DEPUIS N'IMPORTE OÙ
------------------------------------------------------------------------

Symptôme :
  // Dans un contrôleur d'authentification
  const db = require('./config/database');
  const course = await db.query("SELECT * FROM courses");
  // Pourquoi le contrôleur auth accède aux cours ??

Solution :
  SEULS les repositories peuvent accéder à la base de données.
  Les contrôleurs appellent les services, jamais la DB directement.

------------------------------------------------------------------------
ERREUR 3 : TRANSACTIONS IGNORÉES
------------------------------------------------------------------------

Symptôme :
  // Inscription à un cours
  await createEnrollment(userId, courseId);  // Enregistré
  await chargePayment(userId, amount);       // Si ça échoue ici,
  // l'inscription est créée mais pas payée !

Solution :
  // Utiliser des transactions SQL
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    await createEnrollment(client, userId, courseId);
    await chargePayment(client, userId, amount);
    await client.query('COMMIT');
  } catch (error) {
    await client.query('ROLLBACK');
    throw error;
  } finally {
    client.release();
  }

------------------------------------------------------------------------
ERREUR 4 : NE PAS SCALER HORIZONTALEMENT
------------------------------------------------------------------------

Symptôme :
  Votre monolithe est sur UN SEUL serveur.
  Si ce serveur tombe -> tout le monde est impacté.

Solution :
  Déployer plusieurs instances derrière un Load Balancer.
  S'assurer que l'application est "stateless" (pas d'état en mémoire).
  Utiliser Redis pour les sessions partagées.

------------------------------------------------------------------------
ERREUR 5 : IGNORER LES MIGRATIONS
------------------------------------------------------------------------

Symptôme :
  "J'ai ajouté une colonne directement en production via SQL, pas de migration"
  2 semaines plus tard : "Pourquoi le dev n'a pas cette colonne ?"

Solution :
  TOUJOURS utiliser un outil de migration (Flyway, Liquibase, Knex.js, Prisma).
  Les migrations sont dans le dépôt git, versionnées.
  Jamais de modification manuelle directement en production.

================================================================================
CHAPITRE 9 : EXERCICES PRATIQUES
================================================================================

------------------------------------------------------------------------
EXERCICES FACILES
------------------------------------------------------------------------

EXERCICE 1 : Bootstrap du projet
  Créez la structure de dossiers complète du monolithe EduConnect décrite
  dans ce chapitre. Initialisez un projet Node.js avec npm init.
  Installez les dépendances : express, helmet, cors, bcrypt, jsonwebtoken,
  express-rate-limit, winston.

EXERCICE 2 : Route Health Check
  Implémentez la route GET /health qui retourne :
  - Le statut de l'application
  - La version
  - Le timestamp
  - L'état de la connexion base de données
  Testez avec curl ou Postman.

EXERCICE 3 : Schéma SQL complet
  Écrivez les migrations SQL pour créer toutes les tables d'EduConnect :
  users, courses, modules, lessons, enrollments, payments.
  Ajoutez les index appropriés et les contraintes de clés étrangères.

------------------------------------------------------------------------
EXERCICES INTERMÉDIAIRES
------------------------------------------------------------------------

EXERCICE 4 : Module Users complet
  Implémentez le module Users avec :
  - GET /api/v1/users/:id (profil public)
  - PUT /api/v1/users/me (mise à jour profil)
  - GET /api/v1/users/me/courses (mes cours inscrits)
  Utilisez le middleware authMiddleware pour les routes protégées.

EXERCICE 5 : Module Courses avec pagination
  Implémentez :
  - GET /api/v1/courses (liste avec pagination et filtres)
  - GET /api/v1/courses/:id (détail d'un cours)
  - POST /api/v1/courses (créer un cours - instructors only)
  - PUT /api/v1/courses/:id (modifier - instructor propriétaire only)
  La liste doit supporter : ?page=1&limit=20&category=web&level=beginner

EXERCICE 6 : Tests d'intégration
  Écrivez des tests d'intégration Jest pour :
  - Le flux complet d'inscription (register + vérification en DB)
  - Le flux de login (succès + erreur email inexistant + erreur mauvais pwd)
  - Une route protégée (accès avec token valide + accès sans token)
  Utilisez une base de données de test séparée.

------------------------------------------------------------------------
EXERCICES AVANCÉS
------------------------------------------------------------------------

EXERCICE 7 : Système d'événements interne
  Implémentez un EventBus partagé.
  Refactorez le service d'inscription pour émettre des événements.
  Créez un module Notifications qui écoute ces événements et envoie
  des emails (simulés via console.log en développement).
  Vérifiez que si NotificationService lance une erreur, l'inscription
  n'est pas annulée.

EXERCICE 8 : Rate Limiting avancé par utilisateur
  Implémentez un rate limiting par utilisateur authentifié en plus
  du rate limiting par IP.
  Limites :
  - POST /api/v1/auth/login : 5 tentatives/15min par IP
  - POST /api/v1/courses : 10 créations/heure par instructor
  - GET /api/v1/courses : 1000 requêtes/heure par utilisateur
  Stockez les compteurs dans Redis.

EXERCICE 9 : Migration vers Monolithe Modulaire Strict
  Prenez la structure actuelle et imposez des règles strictes :
  1. Créez un registre de modules (module registry)
  2. Chaque module enregistre ses services disponibles
  3. Les modules communiquent uniquement via le registre
  4. Ajoutez une validation au démarrage qui vérifie qu'aucun module
     n'importe directement depuis le dossier interne d'un autre module
  5. Écrivez un test qui vérifie ces règles d'architecture

================================================================================
CHAPITRE 10 : CORRIGÉS
================================================================================

------------------------------------------------------------------------
CORRIGÉ EXERCICE 5 : Module Courses avec pagination
------------------------------------------------------------------------

  // modules/courses/course.repository.js

  const { pool } = require('../../config/database');

  class CourseRepository {

    async findAll({ page = 1, limit = 20, category, level, search, status = 'published' }) {
      // Calcul de l'offset pour la pagination
      const offset = (page - 1) * limit;

      // Construction dynamique de la requête SQL
      // On utilise un tableau pour les paramètres pour éviter les injections SQL
      let conditions = ['status = $1'];
      let params = [status];
      let paramIndex = 2;  // Le prochain index de paramètre

      // Filtre par catégorie si fourni
      if (category) {
        conditions.push(`category = $${paramIndex}`);
        params.push(category);
        paramIndex++;
      }

      // Filtre par niveau si fourni
      if (level) {
        conditions.push(`level = $${paramIndex}`);
        params.push(level);
        paramIndex++;
      }

      // Recherche full-text si fournie
      if (search) {
        conditions.push(
          `(title ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`
        );
        params.push(`%${search}%`);
        paramIndex++;
      }

      // Construction de la clause WHERE
      const whereClause = conditions.join(' AND ');

      // Requête pour le total (sans LIMIT/OFFSET)
      const countQuery = `
        SELECT COUNT(*) as total
        FROM courses
        WHERE ${whereClause}
      `;

      // Requête pour les données avec pagination
      const dataQuery = `
        SELECT
          c.*,
          u.first_name as instructor_first_name,
          u.last_name as instructor_last_name
        FROM courses c
        JOIN users u ON c.instructor_id = u.id
        WHERE ${whereClause}
        ORDER BY c.created_at DESC
        LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
      `;

      // Exécution des deux requêtes en parallèle (Promise.all = plus rapide)
      const [countResult, dataResult] = await Promise.all([
        pool.query(countQuery, params),
        pool.query(dataQuery, [...params, limit, offset])
      ]);

      const total = parseInt(countResult.rows[0].total);

      return {
        data: dataResult.rows,
        pagination: {
          total,
          page: parseInt(page),
          limit: parseInt(limit),
          totalPages: Math.ceil(total / limit),
          hasNext: page * limit < total,
          hasPrev: page > 1
        }
      };
    }
  }

  module.exports = new CourseRepository();

------------------------------------------------------------------------
CORRIGÉ EXERCICE 7 : EventBus
------------------------------------------------------------------------

  // ============================================================
  // shared/eventBus.js
  // Bus d'événements partagé entre les modules
  // ============================================================

  const EventEmitter = require('events');

  class EventBus extends EventEmitter {
    constructor() {
      super();
      // Augmenter la limite des listeners (défaut Node.js : 10)
      this.setMaxListeners(50);

      // Activer le mode debug en développement
      if (process.env.NODE_ENV === 'development') {
        this.on('newListener', (event) => {
          console.debug(`[EventBus] Nouveau listener pour: ${event}`);
        });
      }
    }

    // Méthode safe : emit avec gestion d'erreur
    safeEmit(event, data) {
      try {
        this.emit(event, data);
      } catch (error) {
        console.error(`[EventBus] Erreur pour l'événement ${event}:`, error);
        // L'erreur ne se propage pas — les listeners défaillants
        // ne cassent pas l'émetteur
      }
    }
  }

  // Export du singleton (même instance dans toute l'app)
  module.exports = new EventBus();

  // ============================================================
  // modules/enrollments/enrollment.service.js (partie EventBus)
  // ============================================================

  const eventBus = require('../../shared/eventBus');

  class EnrollmentService {
    async enroll(userId, courseId, paymentToken) {
      // ... logique d'inscription ...

      const enrollment = await EnrollmentRepository.create({...});

      // Émettre l'événement (de manière non bloquante)
      // Si le listener de notification plante, l'inscription reste valide
      eventBus.safeEmit('enrollment:created', {
        userId,
        courseId,
        enrollmentId: enrollment.id,
        enrolledAt: enrollment.enrolledAt
      });

      return enrollment;
    }
  }

  // ============================================================
  // modules/notifications/notification.listener.js
  // S'inscrit aux événements au démarrage de l'application
  // ============================================================

  const eventBus = require('../../shared/eventBus');
  const NotificationService = require('./notification.service');
  const logger = require('../../config/logger');

  function registerListeners() {
    // Écouter l'événement d'inscription
    eventBus.on('enrollment:created', async (data) => {
      try {
        await NotificationService.sendEnrollmentConfirmation(data);
        logger.info(`Email de confirmation envoyé pour enrollment ${data.enrollmentId}`);
      } catch (error) {
        // Log l'erreur mais ne relance pas
        // Les erreurs de notification sont non-critiques
        logger.error(`Erreur notification enrollment ${data.enrollmentId}:`, error);
      }
    });

    logger.info('[OK] Listeners de notifications enregistrés');
  }

  module.exports = { registerListeners };

  // Dans app.js, au démarrage :
  const { registerListeners } = require('./modules/notifications/notification.listener');
  registerListeners();

================================================================================
RÉCAPITULATIF — MONOLITHE
================================================================================

Points clés :

1. Un monolithe est une application déployée comme une seule unité.

2. Il existe plusieurs types : Spaghetti (mauvais), Modulaire (bon),
   Modulaire Strict (très bon, prêt pour microservices).

3. Le monolithe est souvent LE BON CHOIX pour :
   - Les startups et MVPs
   - Les petites équipes
   - Les domaines pas encore bien compris

4. Un BON monolithe respecte :
   - Modules avec interfaces publiques claires
   - Pas d'accès aux internals d'autres modules
   - Couches distinctes (Contrôleur -> Service -> Repository)
   - EventBus pour découplage entre modules

5. Les limites du monolithe apparaissent quand :
   - L'équipe dépasse 10-15 personnes
   - Le besoin de scalabilité sélective est avéré
   - Plusieurs équipes travaillent en parallèle

6. EDUCONNECT VERSION MONOLITHE : Foundation solide avec modules séparés,
   prête à être découpée en microservices si besoin.

Prochaine étape :
  Volume 3 : Architecture MVC
  -> Approfondir la structuration interne du monolithe avec MVC
  -> Introduire les templates, les vues, et l'architecture 3 couches

================================================================================
FIN DU VOLUME 2 — ARCHITECTURE MONOLITHIQUE
Prochaine étape -> architecture_mvc.txt
================================================================================

================================================================================
     GUIDE COMPLET DES ARCHITECTURES LOGICIELLES - VOLUME 3
     Architecture MVC (Model-View-Controller)
     Pour étudiants en Génie Logiciel
================================================================================

================================================================================
CHAPITRE 1 : INTRODUCTION — QU'EST-CE QUE MVC ?
================================================================================

------------------------------------------------------------------------
1.1 DÉFINITION
------------------------------------------------------------------------

MVC (Model-View-Controller) est un pattern d'architecture logicielle qui
divise une application en TROIS composants distincts :

  M — MODEL     : Représente les données et la logique métier
  V — VIEW      : Représente l'interface utilisateur (ce que voit l'utilisateur)
  C — CONTROLLER: Sert d'intermédiaire entre le Model et la View

Inventé en 1979 par Trygve Reenskaug chez Xerox PARC pour le langage Smalltalk,
MVC est aujourd'hui l'un des patterns les plus utilisés dans le monde.

Analogie au restaurant :
  MODEL      = La cuisine (les données, les recettes, les ingrédients)
  VIEW       = La salle à manger (l'interface que voit le client)
  CONTROLLER = Le serveur (orchestre les échanges cuisine <-> client)

Le client (utilisateur) ne va jamais en cuisine.
La cuisine ne parle pas directement au client.
Le serveur traduit et orchestre.

------------------------------------------------------------------------
1.2 LES TROIS COMPOSANTS EN DÉTAIL
------------------------------------------------------------------------

LE MODEL
  Responsabilités :
    - Représenter les données de l'application (objets métier)
    - Contenir la logique métier (règles, validations)
    - Accéder et manipuler les données (base de données)
    - Notifier les Views des changements (dans MVC classique)

  Ce que le Model N'EST PAS :
    - Il ne connaît pas les Views
    - Il ne génère pas de HTML
    - Il ne gère pas les requêtes HTTP

  Exemple EduConnect :
    - User (données + méthodes : hashPassword, verify, etc.)
    - Course (données + méthodes : publish, calculatePrice, etc.)
    - Enrollment (données + méthodes : complete, calculateProgress, etc.)

LA VIEW
  Responsabilités :
    - Afficher les données fournies par le Controller
    - Représenter l'interface utilisateur
    - Capturer les interactions utilisateur (formulaires, clics)
    - Envoyer les actions au Controller

  Ce que la View N'EST PAS :
    - Elle ne contient pas de logique métier
    - Elle ne parle pas directement au Model (dans MVC strict)
    - Elle ne fait pas de requêtes base de données

  Exemple EduConnect :
    - page HTML de la liste des cours
    - Template du formulaire d'inscription
    - Composant React de la page de cours
    - Réponse JSON de l'API (MVC côté serveur)

LE CONTROLLER
  Responsabilités :
    - Recevoir les requêtes/actions de l'utilisateur
    - Valider et sanitiser les données d'entrée
    - Appeler les opérations appropriées sur le Model
    - Choisir la View appropriée et lui passer les données
    - Orchestrer le flux de l'application

  Ce que le Controller N'EST PAS :
    - Il ne contient pas de logique métier complexe (délégue au Model)
    - Il ne génère pas de HTML directement
    - Il n'accède pas directement à la base de données

  Règle : Un Controller MINCE (Thin Controller), un Model ÉPAIS (Fat Model)

------------------------------------------------------------------------
1.3 POURQUOI MVC EXISTE
------------------------------------------------------------------------

Avant MVC, les applications mélangeaient tout :
  - Le même fichier PHP générait le HTML, faisait les requêtes SQL,
    calculait la logique métier, et gérait les sessions

  Exemple "spaghetti code" d'avant MVC :
  <?php
    $user_id = $_GET['id'];
    $result = mysql_query("SELECT * FROM users WHERE id = $user_id");  // SQL injection!
    $user = mysql_fetch_array($result);
    if ($user['role'] == 'admin') {
      echo "<div class='admin'>Bienvenue admin</div>";
      // 200 lignes de logique admin ici
    } else {
      echo "<div class='user'>Bienvenue " . $user['name'] . "</div>";
    }
    // 500 lignes mélangeant HTML, PHP, SQL...
  ?>

  Problèmes :
    - Impossible de tester (mélange UI + logique + données)
    - Impossible de modifier l'UI sans toucher la logique
    - Impossible de réutiliser la logique dans une API
    - Maintenance cauchemardesque

MVC résout ces problèmes en séparant clairement :
    - Les données (Model) -> modifiable indépendamment
    - L'affichage (View) -> peut changer (HTML -> JSON -> XML) sans logique
    - L'orchestration (Controller) -> gère le flux

================================================================================
CHAPITRE 2 : THÉORIE — FONCTIONNEMENT INTERNE
================================================================================

------------------------------------------------------------------------
2.1 FLUX DE DONNÉES DANS MVC
------------------------------------------------------------------------

FLUX CLASSIQUE (Application Web côté serveur) :

  1. L'utilisateur interagit (clique, soumet un formulaire)
  2. La requête arrive au Controller
  3. Le Controller valide les données
  4. Le Controller appelle les méthodes du Model
  5. Le Model accède aux données (base de données)
  6. Le Model retourne les données au Controller
  7. Le Controller passe les données à la View
  8. La View génère l'HTML/JSON/XML à afficher
  9. La réponse est envoyée à l'utilisateur

Schéma :

  UTILISATEUR
      │
      │ 1. Action (clic/formulaire)
      [BLACK_DOWN-POINTING_TRIANGLE]
  ┌──────────┐
  │  VIEW    │ <-─────────────────────────────┐
  └────┬─────┘  7. Données pour affichage    │
       │                                      │
       │ 2. Action utilisateur (Input)        │
       [BLACK_DOWN-POINTING_TRIANGLE]                                      │
  ┌──────────────┐                           │
  │  CONTROLLER  │ ──────────────────────────┘
  └───┬──────┬───┘  6. Données du Model
      │      │
      │3.    │5.
      │Appel │Résultat
      [BLACK_DOWN-POINTING_TRIANGLE]      │
  ┌──────────┴┐
  │   MODEL   │
  │           │ 4. Accès base de données
  └───────────┘

------------------------------------------------------------------------
2.2 VARIANTES DE MVC
------------------------------------------------------------------------

MVC A ÉTÉ ADAPTÉ DE NOMBREUSES FAÇONS :

MVC CLASSIQUE (Desktop, années 80)
  - Model notifie directement les Views (Observer Pattern)
  - Plusieurs Views peuvent observer le même Model
  - Utilisé dans Swing (Java), Qt (C++)

MVC WEB CÔTÉ SERVEUR (années 2000)
  - Pas de notification directe Model -> View (HTTP est sans état)
  - Controller récupère les données et les passe à la View
  - Utilisé dans Rails, Django, Spring MVC, Laravel

MVC CÔTÉ CLIENT (Single Page Applications)
  - Framework JavaScript gère MVC côté client
  - Backbone.js était le premier MVC JS
  - React, Vue, Angular sont des évolutions de ce concept

PATTERNS DÉRIVÉS :
  MVP (Model-View-Presenter)
    - Presenter remplace le Controller
    - View est plus passive (pas de logique)
    - Utilisé dans Android (classique), WinForms

  MVVM (Model-View-ViewModel)
    - ViewModel expose les données bindées
    - Binding bidirectionnel View <-> ViewModel
    - Utilisé dans Angular, Vue.js, WPF, SwiftUI

  MVC + Service Layer (Pattern courant en entreprise)
    - Controller appelle des Services
    - Services contiennent la logique métier
    - Models = entités de données uniquement
    - C'est ce que nous utilisons pour EduConnect

------------------------------------------------------------------------
2.3 MVC POUR API REST
------------------------------------------------------------------------

Dans une API REST, la View est remplacée par la réponse JSON.
C'est le cas le plus courant aujourd'hui en développement backend.

FLUX API REST MVC :

  Client HTTP (navigateur, mobile, autre service)
       │
       │ POST /api/courses { title, price, ... }
       │ Header: Authorization: Bearer <token>
       [BLACK_DOWN-POINTING_TRIANGLE]
  ┌─────────────────────────────────────────────────┐
  │                   ROUTER                        │
  │  Associe /api/courses POST -> CourseController  │
  └──────────────────────┬──────────────────────────┘
                         │
                         [BLACK_DOWN-POINTING_TRIANGLE]
  ┌─────────────────────────────────────────────────┐
  │              COURSE CONTROLLER                  │
  │                                                 │
  │  1. Extrait et valide req.body                  │
  │  2. Vérifie permissions (instructor?)           │
  │  3. Appelle CourseService.create(dto)           │
  └──────────────────────┬──────────────────────────┘
                         │
                         [BLACK_DOWN-POINTING_TRIANGLE]
  ┌─────────────────────────────────────────────────┐
  │              COURSE MODEL/SERVICE               │
  │                                                 │
  │  1. Valide les règles métier                    │
  │  2. Calcule les champs dérivés                  │
  │  3. Appelle CourseRepository.save(course)       │
  └──────────────────────┬──────────────────────────┘
                         │
                         [BLACK_DOWN-POINTING_TRIANGLE]
  ┌─────────────────────────────────────────────────┐
  │                 DATABASE                        │
  │      INSERT INTO courses (...) VALUES (...)     │
  └──────────────────────┬──────────────────────────┘
                         │
                         │ Retourne le course créé
                         [BLACK_DOWN-POINTING_TRIANGLE]
  ┌─────────────────────────────────────────────────┐
  │               JSON RESPONSE (View)              │
  │                                                 │
  │  HTTP 201 Created                               │
  │  {                                              │
  │    "id": "uuid",                                │
  │    "title": "Intro à Python",                   │
  │    "price": 29.99,                              │
  │    "status": "draft",                           │
  │    "createdAt": "2024-01-15T..."                │
  │  }                                              │
  └─────────────────────────────────────────────────┘

------------------------------------------------------------------------
2.4 AVANTAGES ET INCONVÉNIENTS DE MVC
------------------------------------------------------------------------

AVANTAGES :

  Séparation des responsabilités claire
    Chaque couche a un rôle défini.
    Un développeur front peut modifier les Views sans connaître le Model.
    Un développeur back peut modifier le Model sans toucher les Views.

  Testabilité améliorée
    On peut tester le Model sans UI.
    On peut tester les Controllers avec des Models mockés.
    On peut tester les Views avec des données statiques.

  Réutilisabilité
    Un même Model peut servir plusieurs Views.
    Exemple : Le même CourseModel sert l'API REST et un email template.

  Développement parallèle
    L'équipe front travaille sur les Views pendant que l'équipe back
    travaille sur les Models et Controllers.

  Maintenance facilitée
    Changer le mode de stockage (SQL -> NoSQL) n'impacte que le Model.
    Changer le design (HTML) n'impacte que la View.

INCONVÉNIENTS :

  Verbosité
    Peut demander beaucoup de classes/fichiers pour des features simples.
    Parfois, une simple fonction ferait l'affaire.

  Apprentissage initial
    Comprendre où mettre quelle logique demande de l'expérience.
    Surtout : où est la frontière entre Controller et Model ?

  Over-engineering possible
    Pour un script simple ou un protot rapide, MVC peut être surdimensionné.

  La "Fat Model vs Fat Controller" bataille
    Où mettre la logique complexe ? Dans le Model ou un Service ?
    Réponse : Dans un Service Layer (extension de MVC).

================================================================================
CHAPITRE 3 : SCHÉMAS EXPLICATIFS
================================================================================

------------------------------------------------------------------------
3.1 MVC POUR APPLICATION WEB CLASSIQUE (RENDU SERVEUR)
------------------------------------------------------------------------

  NAVIGATEUR          SERVEUR (Application MVC)            DATABASE
       │                                                        │
       │ GET /courses                                           │
       │─────────────────────────────────────────────────────->│
       │               Routes                                   │
       │               │                                        │
       │               [BLACK_DOWN-POINTING_TRIANGLE] match: CourseController.index         │
       │         ┌─────────────────────────────────────────┐   │
       │         │         CourseController                 │   │
       │         │         .index(req, res)                 │   │
       │         └─────────────────┬───────────────────────┘   │
       │                           │                            │
       │                           │ CourseModel.findAll(...)   │
       │                           [BLACK_DOWN-POINTING_TRIANGLE]                            │
       │         ┌─────────────────────────────────────────┐   │
       │         │           CourseModel                   │   │
       │         │      .findAll({published: true})        │   │
       │         └─────────────────┬───────────────────────┘   │
       │                           │ SELECT * FROM courses...  │
       │                           │────────────────────────->  │
       │                           │  [course1, course2, ...]  │
       │                           │<-────────────────────────  │
       │                           │                            │
       │                           │ Données disponibles        │
       │                           [BLACK_DOWN-POINTING_TRIANGLE]                            │
       │         ┌─────────────────────────────────────────┐   │
       │         │           CourseView                    │   │
       │         │   render('courses/index', {courses})    │   │
       │         │   -> Génère HTML avec les données        │   │
       │         └─────────────────┬───────────────────────┘   │
       │                           │                            │
       │  HTTP 200 + HTML de la   │                            │
       │  liste des cours          │                            │
       │<-──────────────────────────│                            │

------------------------------------------------------------------------
3.2 MVC AVEC SERVICE LAYER (Pattern recommandé pour EduConnect)
------------------------------------------------------------------------

  REQUEST
     │
     │ POST /api/v1/enrollments
     │ Body: {courseId, paymentToken}
     │ JWT Token
     [BLACK_DOWN-POINTING_TRIANGLE]
  ┌────────────────────────────────────────────────────────────────┐
  │                         MIDDLEWARE LAYER                       │
  │                                                                │
  │  authMiddleware -> loggerMiddleware -> validationMiddleware      │
  └─────────────────────────────────┬──────────────────────────────┘
                                    │
                                    [BLACK_DOWN-POINTING_TRIANGLE]
  ┌────────────────────────────────────────────────────────────────┐
  │                      CONTROLLER LAYER                          │
  │                  EnrollmentController.create()                 │
  │                                                                │
  │  Responsabilités:                                              │
  │  - Extraire user_id du req.user (mis par authMiddleware)       │
  │  - Valider course_id et paymentToken (format)                  │
  │  - Appeler le service                                          │
  │  - Formater et retourner la réponse HTTP                       │
  └─────────────────────────────────┬──────────────────────────────┘
                                    │ Appel méthode
                                    [BLACK_DOWN-POINTING_TRIANGLE]
  ┌────────────────────────────────────────────────────────────────┐
  │                      SERVICE LAYER                             │
  │                   EnrollmentService.enroll()                   │
  │                                                                │
  │  Responsabilités (LOGIQUE MÉTIER) :                            │
  │  - Vérifier que l'utilisateur n'est pas déjà inscrit           │
  │  - Vérifier les prérequis du cours                             │
  │  - Calculer le prix (réductions, promotions)                   │
  │  - Appeler PaymentService.charge()                             │
  │  - Créer l'enrollment en base                                  │
  │  - Émettre les événements post-inscription                     │
  └──────┬──────────────────────────┬──────────────────────────────┘
         │                          │
         [BLACK_DOWN-POINTING_TRIANGLE]                          [BLACK_DOWN-POINTING_TRIANGLE]
  ┌────────────────┐      ┌─────────────────────────────────────┐
  │ CourseService  │      │         EnrollmentRepository        │
  │ .findById()    │      │         .create(enrollment)         │
  │ .checkPrereqs()│      │                                     │
  └────────────────┘      └─────────────────────┬───────────────┘
                                                 │
                                                 [BLACK_DOWN-POINTING_TRIANGLE]
                                        ┌──────────────┐
                                        │  PostgreSQL  │
                                        │  INSERT ...  │
                                        └──────────────┘

------------------------------------------------------------------------
3.3 STRUCTURE DE DOSSIERS MVC POUR EDUCONNECT
------------------------------------------------------------------------

  educonnect/
  ├── models/            <- MODELS (Données + Logique métier)
  │   ├── User.js        <- Classe User (attributs + méthodes métier)
  │   ├── Course.js      <- Classe Course
  │   ├── Enrollment.js  <- Classe Enrollment
  │   └── Payment.js     <- Classe Payment
  │
  ├── views/             <- VIEWS (Présentation des données)
  │   ├── api/           <- Pour l'API JSON (Serializers)
  │   │   ├── UserSerializer.js
  │   │   ├── CourseSerializer.js
  │   │   └── EnrollmentSerializer.js
  │   └── email/         <- Pour les emails (Templates)
  │       ├── welcome.html
  │       └── enrollment_confirmation.html
  │
  ├── controllers/       <- CONTROLLERS (Orchestration)
  │   ├── AuthController.js
  │   ├── CourseController.js
  │   ├── EnrollmentController.js
  │   └── UserController.js
  │
  ├── services/          <- SERVICES (Logique métier complexe)
  │   ├── AuthService.js
  │   ├── CourseService.js
  │   ├── EnrollmentService.js
  │   └── PaymentService.js
  │
  ├── repositories/      <- REPOSITORIES (Accès base de données)
  │   ├── UserRepository.js
  │   ├── CourseRepository.js
  │   └── EnrollmentRepository.js
  │
  ├── routes/            <- ROUTES (Association URL -> Controller)
  │   ├── auth.routes.js
  │   ├── course.routes.js
  │   └── enrollment.routes.js
  │
  └── app.js             <- Point d'entrée

================================================================================
CHAPITRE 4 : POURQUOI UTILISER MVC ?
================================================================================

------------------------------------------------------------------------
4.1 PRODUCTIVITÉ D'ÉQUIPE
------------------------------------------------------------------------

MVC permet à plusieurs développeurs de travailler en parallèle :

  ÉQUIPE FRONT :
    Travaille sur les Views (composants React, templates HTML)
    N'a besoin de connaître que l'interface des Controllers (API contracts)

  ÉQUIPE BACK :
    Travaille sur Models, Services, Controllers
    Définit les API contracts qui sont respectés par le front

  EXEMPLE CONCRET POUR EDUCONNECT :
    Sprint 1 :
      - Back : Implémente les endpoints API courses (Model + Service + Controller)
      - Front : Crée les components React avec des données mockées
    Sprint 2 :
      - Connexion front/back, tout s'assemble proprement

------------------------------------------------------------------------
4.2 MAINTENABILITÉ
------------------------------------------------------------------------

  Scénario : Vous devez changer le design de la liste des cours.
    MVC : Modifier uniquement CourseView/CourseSerializer
    Spaghetti : Chercher dans 50 fichiers entremêlés

  Scénario : Vous devez changer le prix de calcul des cours.
    MVC : Modifier uniquement CourseService.calculatePrice()
    Spaghetti : Le même calcul est dupliqué dans 15 fichiers

------------------------------------------------------------------------
4.3 TESTABILITÉ
------------------------------------------------------------------------

MVC rend le code naturellement testable :

  TESTS UNITAIRES DU MODEL :
    - Tester que hashPassword() hash correctement
    - Tester que calculatePrice() retourne le bon prix avec réductions
    - Pas besoin de serveur HTTP ou de base de données

  TESTS DU CONTROLLER :
    - Tester que le Controller retourne 400 si données invalides
    - Mocker le Service pour isoler le Controller
    - Tester les codes HTTP retournés

  TESTS DU SERVICE :
    - Tester toute la logique métier
    - Mocker le Repository pour tester sans base de données

  TESTS D'INTÉGRATION :
    - Tester le flux complet avec une vraie base de données de test

================================================================================
CHAPITRE 5 : QUAND UTILISER MVC ?
================================================================================

------------------------------------------------------------------------
5.1 CONTEXTES APPROPRIÉS
------------------------------------------------------------------------

MVC EST IDÉAL POUR :

  Applications web classiques (rendu serveur)
    - Blogs, CMS, e-commerce
    - Frameworks : Ruby on Rails, Django, Laravel, Spring MVC

  APIs REST
    - La "View" est simplement la réponse JSON
    - Frameworks : Express.js, FastAPI, Node.js

  Applications avec équipes distinctes front/back
    - Le contrat entre Controller et View est l'API

  Applications de taille moyenne
    - Plus de quelques fonctionnalités, pas encore assez grand pour microservices
    - 1 à 15 développeurs environ

  Projets éducatifs et formations
    - MVC est le pattern pédagogique par excellence
    - Base de tous les frameworks modernes

QUAND MVC NE SUFFIT PAS :

  Logique métier très complexe
    -> Utiliser Clean Architecture ou DDD en plus de MVC

  Haute performance en temps réel
    -> Considérer des patterns spécifiques (event sourcing, CQRS)

  Très grosse équipe avec domaines métier séparés
    -> Microservices, chaque service peut avoir son propre MVC interne

================================================================================
CHAPITRE 6 : IMPLÉMENTATION PRATIQUE COMPLÈTE
================================================================================

------------------------------------------------------------------------
6.1 LE MODEL — COURS (Course Model)
------------------------------------------------------------------------

  // ============================================================
  // models/Course.js
  // Model Course — Entité avec attributs et logique métier
  // ============================================================

  class Course {
    // ──────────────────────────────────────────────────────
    // CONSTRUCTEUR
    // ──────────────────────────────────────────────────────
    constructor({
      id = null,
      title,
      description = null,
      instructorId,
      price = 0,
      currency = 'EUR',
      level = 'beginner',
      category = null,
      status = 'draft',
      thumbnailUrl = null,
      prerequisites = [],
      language = 'fr',
      durationMinutes = null,
      publishedAt = null,
      createdAt = new Date(),
      updatedAt = new Date()
    }) {
      this.id = id;
      this.title = title;
      this.description = description;
      this.instructorId = instructorId;
      this.price = price;
      this.currency = currency;
      this.level = level;
      this.category = category;
      this.status = status;
      this.thumbnailUrl = thumbnailUrl;
      this.prerequisites = prerequisites;
      this.language = language;
      this.durationMinutes = durationMinutes;
      this.publishedAt = publishedAt;
      this.createdAt = createdAt;
      this.updatedAt = updatedAt;
    }

    // ──────────────────────────────────────────────────────
    // LOGIQUE MÉTIER — Méthodes du Model
    // ──────────────────────────────────────────────────────

    // Peut-on publier ce cours ?
    canBePublished() {
      return (
        this.title &&                           // Titre obligatoire
        this.title.trim().length >= 5 &&        // Min 5 caractères
        this.description &&                     // Description obligatoire
        this.description.trim().length >= 50 && // Min 50 caractères
        this.durationMinutes !== null &&         // Durée renseignée
        this.durationMinutes > 0                // Durée positive
      );
    }

    // Publier le cours
    publish() {
      if (!this.canBePublished()) {
        throw new Error('Ce cours ne peut pas être publié. Vérifiez titre, description et durée.');
      }
      if (this.status === 'published') {
        throw new Error('Ce cours est déjà publié');
      }

      this.status = 'published';
      this.publishedAt = new Date();
      this.updatedAt = new Date();

      return this;
    }

    // Archiver le cours
    archive() {
      if (this.status === 'draft') {
        throw new Error('Un cours brouillon ne peut pas être archivé directement');
      }
      this.status = 'archived';
      this.updatedAt = new Date();
      return this;
    }

    // Calculer le prix avec une réduction
    // discount = 0.20 signifie 20% de réduction
    calculateDiscountedPrice(discount) {
      if (discount < 0 || discount > 1) {
        throw new Error('La réduction doit être entre 0 et 1 (0% à 100%)');
      }
      return Math.round(this.price * (1 - discount) * 100) / 100;
      // Math.round(...* 100) / 100 évite les problèmes de virgule flottante
      // Ex: 29.99 * 0.8 = 23.992000000000004 -> on veut 23.99
    }

    // Le cours est-il gratuit ?
    isFree() {
      return this.price === 0;
    }

    // Vérifier si un cours est accessible (publié et pas archivé)
    isAvailable() {
      return this.status === 'published';
    }

    // Convertir en objet plain (pour sérialisation/sauvegarde)
    toJSON() {
      return {
        id: this.id,
        title: this.title,
        description: this.description,
        instructorId: this.instructorId,
        price: this.price,
        currency: this.currency,
        level: this.level,
        category: this.category,
        status: this.status,
        thumbnailUrl: this.thumbnailUrl,
        prerequisites: this.prerequisites,
        language: this.language,
        durationMinutes: this.durationMinutes,
        publishedAt: this.publishedAt,
        createdAt: this.createdAt,
        updatedAt: this.updatedAt
      };
    }

    // Créer un Course depuis les données brutes de la base de données
    // Pattern : Factory method
    static fromDatabase(row) {
      return new Course({
        id: row.id,
        title: row.title,
        description: row.description,
        instructorId: row.instructor_id,  // snake_case -> camelCase
        price: parseFloat(row.price),
        currency: row.currency,
        level: row.level,
        category: row.category,
        status: row.status,
        thumbnailUrl: row.thumbnail_url,
        prerequisites: row.prerequisites || [],
        language: row.language,
        durationMinutes: row.duration_minutes,
        publishedAt: row.published_at,
        createdAt: row.created_at,
        updatedAt: row.updated_at
      });
    }
  }

  module.exports = Course;

------------------------------------------------------------------------
6.2 LE SERVICE — COURS
------------------------------------------------------------------------

  // ============================================================
  // services/CourseService.js
  // Service — Logique métier des cours
  // ============================================================

  const Course = require('../models/Course');
  const CourseRepository = require('../repositories/CourseRepository');
  const { NotFoundError, ForbiddenError, ValidationError } = require('../shared/errors');

  class CourseService {

    // ──────────────────────────────────────────────────────
    // CRÉER UN COURS
    // ──────────────────────────────────────────────────────
    async createCourse(courseData, requestingUser) {
      // Règle métier : Seuls les instructeurs peuvent créer des cours
      if (requestingUser.role !== 'instructor' && requestingUser.role !== 'admin') {
        throw new ForbiddenError('Seuls les instructeurs peuvent créer des cours');
      }

      // Créer l'objet Course (validation via le modèle)
      const course = new Course({
        ...courseData,
        instructorId: requestingUser.id,  // L'instructeur est l'utilisateur courant
        status: 'draft'                    // Toujours en brouillon au départ
      });

      // Sauvegarder en base
      const savedCourse = await CourseRepository.save(course);

      return savedCourse;
    }

    // ──────────────────────────────────────────────────────
    // RÉCUPÉRER UN COURS
    // ──────────────────────────────────────────────────────
    async getCourseById(courseId, requestingUser = null) {
      const course = await CourseRepository.findById(courseId);

      if (!course) {
        throw new NotFoundError('Cours');
      }

      // Règle métier : Les brouillons sont visibles uniquement par
      // l'instructeur propriétaire ou un admin
      if (course.status === 'draft') {
        if (!requestingUser) {
          throw new NotFoundError('Cours');  // Ne pas révéler l'existence du brouillon
        }
        if (
          requestingUser.id !== course.instructorId &&
          requestingUser.role !== 'admin'
        ) {
          throw new NotFoundError('Cours');  // Même réponse pour ne pas révéler
        }
      }

      return course;
    }

    // ──────────────────────────────────────────────────────
    // LISTER LES COURS (avec pagination et filtres)
    // ──────────────────────────────────────────────────────
    async listCourses({ page = 1, limit = 20, category, level, search, instructorId } = {}) {
      // Validation des paramètres de pagination
      if (page < 1) throw new ValidationError('La page doit être >= 1');
      if (limit < 1 || limit > 100) throw new ValidationError('La limite doit être entre 1 et 100');

      return await CourseRepository.findAll({
        page,
        limit,
        category,
        level,
        search,
        instructorId,
        status: 'published'  // Toujours uniquement les cours publiés en liste publique
      });
    }

    // ──────────────────────────────────────────────────────
    // PUBLIER UN COURS
    // ──────────────────────────────────────────────────────
    async publishCourse(courseId, requestingUser) {
      // Récupérer le cours
      const course = await CourseRepository.findById(courseId);
      if (!course) throw new NotFoundError('Cours');

      // Vérifier les permissions
      if (
        course.instructorId !== requestingUser.id &&
        requestingUser.role !== 'admin'
      ) {
        throw new ForbiddenError('Vous n\'êtes pas le propriétaire de ce cours');
      }

      // Utiliser la méthode métier du Model (qui valide les règles)
      // Si le cours ne peut pas être publié, publish() lance une erreur
      course.publish();

      // Sauvegarder les changements
      const updatedCourse = await CourseRepository.update(course);

      return updatedCourse;
    }

    // ──────────────────────────────────────────────────────
    // MODIFIER UN COURS
    // ──────────────────────────────────────────────────────
    async updateCourse(courseId, updateData, requestingUser) {
      const course = await CourseRepository.findById(courseId);
      if (!course) throw new NotFoundError('Cours');

      // Vérifier les permissions
      if (
        course.instructorId !== requestingUser.id &&
        requestingUser.role !== 'admin'
      ) {
        throw new ForbiddenError('Vous n\'êtes pas le propriétaire de ce cours');
      }

      // Règle métier : Un cours publié avec des inscrits ne peut pas
      // avoir son prix modifié
      if (course.status === 'published' && updateData.price !== undefined) {
        const enrollmentCount = await this.getEnrollmentCount(courseId);
        if (enrollmentCount > 0 && updateData.price !== course.price) {
          throw new ValidationError(
            'Impossible de modifier le prix d\'un cours avec des inscrits. ' +
            'Créez un nouveau cours ou appliquez une réduction.'
          );
        }
      }

      // Appliquer les modifications (uniquement les champs autorisés)
      const allowedUpdates = ['title', 'description', 'level', 'category',
                               'thumbnailUrl', 'prerequisites', 'language',
                               'durationMinutes', 'price'];

      allowedUpdates.forEach(field => {
        if (updateData[field] !== undefined) {
          course[field] = updateData[field];
        }
      });

      course.updatedAt = new Date();

      return await CourseRepository.update(course);
    }

    // Méthode privée : compter les inscriptions
    async getEnrollmentCount(courseId) {
      const EnrollmentRepository = require('../repositories/EnrollmentRepository');
      return await EnrollmentRepository.countByCourse(courseId);
    }
  }

  module.exports = new CourseService();

------------------------------------------------------------------------
6.3 LE CONTROLLER — COURS
------------------------------------------------------------------------

  // ============================================================
  // controllers/CourseController.js
  // Controller — Interface HTTP pour les cours
  // ============================================================

  const CourseService = require('../services/CourseService');
  const CourseSerializer = require('../views/api/CourseSerializer');
  const { validateCreateCourse, validateUpdateCourse } = require('../dtos/course.dto');

  class CourseController {

    // ──────────────────────────────────────────────────────
    // GET /api/v1/courses
    // Liste des cours publics avec pagination
    // ──────────────────────────────────────────────────────
    async index(req, res, next) {
      try {
        // Extraire les paramètres de query string
        const { page, limit, category, level, search } = req.query;

        // Appeler le service
        const result = await CourseService.listCourses({
          page: parseInt(page) || 1,
          limit: parseInt(limit) || 20,
          category,
          level,
          search
        });

        // Sérialiser les données pour la réponse
        // Le serializer contrôle quels champs sont exposés dans l'API
        const serializedCourses = result.data.map(
          course => CourseSerializer.serialize(course)
        );

        // Retourner la réponse paginée
        return res.status(200).json({
          data: serializedCourses,
          pagination: result.pagination
        });

      } catch (error) {
        next(error);
      }
    }

    // ──────────────────────────────────────────────────────
    // GET /api/v1/courses/:id
    // Détail d'un cours
    // ──────────────────────────────────────────────────────
    async show(req, res, next) {
      try {
        const { id } = req.params;

        // req.user peut être null si route accessible publiquement
        const course = await CourseService.getCourseById(id, req.user || null);

        // Sérialiser avec plus de détails (vue complète)
        const serialized = CourseSerializer.serializeWithDetails(course);

        return res.status(200).json({ data: serialized });

      } catch (error) {
        next(error);
      }
    }

    // ──────────────────────────────────────────────────────
    // POST /api/v1/courses
    // Créer un nouveau cours (instructor uniquement)
    // ──────────────────────────────────────────────────────
    async create(req, res, next) {
      try {
        // Valider les données d'entrée
        const { error, value } = validateCreateCourse(req.body);
        if (error) {
          return res.status(400).json({
            error: 'Données invalides',
            details: error.details.map(d => ({
              field: d.path.join('.'),
              message: d.message
            }))
          });
        }

        // req.user est garanti présent (authMiddleware a été appliqué)
        const course = await CourseService.createCourse(value, req.user);

        return res.status(201).json({
          message: 'Cours créé avec succès',
          data: CourseSerializer.serialize(course)
        });

      } catch (error) {
        next(error);
      }
    }

    // ──────────────────────────────────────────────────────
    // PUT /api/v1/courses/:id
    // Modifier un cours
    // ──────────────────────────────────────────────────────
    async update(req, res, next) {
      try {
        const { id } = req.params;

        const { error, value } = validateUpdateCourse(req.body);
        if (error) {
          return res.status(400).json({
            error: 'Données invalides',
            details: error.details.map(d => d.message)
          });
        }

        const course = await CourseService.updateCourse(id, value, req.user);

        return res.status(200).json({
          message: 'Cours mis à jour',
          data: CourseSerializer.serialize(course)
        });

      } catch (error) {
        next(error);
      }
    }

    // ──────────────────────────────────────────────────────
    // POST /api/v1/courses/:id/publish
    // Publier un cours
    // ──────────────────────────────────────────────────────
    async publish(req, res, next) {
      try {
        const { id } = req.params;

        const course = await CourseService.publishCourse(id, req.user);

        return res.status(200).json({
          message: 'Cours publié avec succès',
          data: CourseSerializer.serialize(course)
        });

      } catch (error) {
        next(error);
      }
    }
  }

  module.exports = new CourseController();

------------------------------------------------------------------------
6.4 LA VIEW — SERIALIZER (API JSON)
------------------------------------------------------------------------

  // ============================================================
  // views/api/CourseSerializer.js
  // View API — Contrôle la présentation des données dans l'API
  // ============================================================

  class CourseSerializer {

    // Sérialisation de base (liste)
    // Moins d'informations pour économiser la bande passante
    serialize(course) {
      return {
        id: course.id,
        title: course.title,
        instructorId: course.instructorId,
        price: course.price,
        currency: course.currency,
        level: course.level,
        category: course.category,
        status: course.status,
        thumbnailUrl: course.thumbnailUrl,
        language: course.language,
        durationMinutes: course.durationMinutes,
        createdAt: course.createdAt
        // On n'inclut PAS : description (longue), prerequisites (array)
        // Pour économiser la bande passante sur les listes
      };
    }

    // Sérialisation complète (détail d'un cours)
    serializeWithDetails(course) {
      return {
        ...this.serialize(course),  // Tous les champs de base
        description: course.description,
        prerequisites: course.prerequisites,
        publishedAt: course.publishedAt,
        updatedAt: course.updatedAt
      };
    }

    // Sérialisation pour l'instructeur (champs supplémentaires)
    serializeForInstructor(course, stats) {
      return {
        ...this.serializeWithDetails(course),
        enrollmentCount: stats?.enrollmentCount || 0,
        revenue: stats?.revenue || 0,
        averageRating: stats?.averageRating || null,
        completionRate: stats?.completionRate || 0
      };
    }
  }

  module.exports = new CourseSerializer();

------------------------------------------------------------------------
6.5 LES ROUTES — ASSOCIATION URL -> CONTROLLER
------------------------------------------------------------------------

  // ============================================================
  // routes/course.routes.js
  // Définition des routes du module Courses
  // ============================================================

  const express = require('express');
  const router = express.Router();

  const CourseController = require('../controllers/CourseController');
  const { authMiddleware, roleMiddleware } = require('../shared/middleware/auth.middleware');
  const optionalAuth = require('../shared/middleware/optionalAuth.middleware');

  // ──────────────────────────────────────────────────────────────
  // ROUTES PUBLIQUES (pas d'authentification requise)
  // ──────────────────────────────────────────────────────────────

  // GET /api/v1/courses
  // Liste tous les cours publiés
  // optionalAuth : Authentification optionnelle (permet de savoir si
  //                l'utilisateur est connecté pour personnaliser la réponse)
  router.get('/', optionalAuth, CourseController.index.bind(CourseController));

  // GET /api/v1/courses/:id
  // Détail d'un cours
  router.get('/:id', optionalAuth, CourseController.show.bind(CourseController));

  // ──────────────────────────────────────────────────────────────
  // ROUTES PROTÉGÉES (authentification requise)
  // ──────────────────────────────────────────────────────────────

  // POST /api/v1/courses
  // Créer un cours (instructors et admins uniquement)
  router.post(
    '/',
    authMiddleware,
    roleMiddleware(['instructor', 'admin']),
    CourseController.create.bind(CourseController)
  );

  // PUT /api/v1/courses/:id
  // Modifier un cours (instructor propriétaire ou admin)
  router.put(
    '/:id',
    authMiddleware,
    CourseController.update.bind(CourseController)
  );

  // POST /api/v1/courses/:id/publish
  // Publier un cours
  router.post(
    '/:id/publish',
    authMiddleware,
    roleMiddleware(['instructor', 'admin']),
    CourseController.publish.bind(CourseController)
  );

  // DELETE /api/v1/courses/:id
  // Archiver un cours (soft delete)
  router.delete(
    '/:id',
    authMiddleware,
    CourseController.archive.bind(CourseController)
  );

  module.exports = router;

------------------------------------------------------------------------
6.6 DTO — VALIDATION DES DONNÉES
------------------------------------------------------------------------

  // ============================================================
  // dtos/course.dto.js
  // Data Transfer Objects — Schémas de validation Joi
  // ============================================================

  const Joi = require('joi');

  // Schéma de validation pour la création d'un cours
  const createCourseSchema = Joi.object({
    title: Joi.string()
      .min(5)
      .max(255)
      .required()
      .messages({
        'string.min': 'Le titre doit contenir au moins 5 caractères',
        'string.max': 'Le titre ne peut pas dépasser 255 caractères',
        'any.required': 'Le titre est obligatoire'
      }),

    description: Joi.string()
      .max(5000)
      .optional()
      .allow(null, ''),

    price: Joi.number()
      .min(0)       // Prix minimum 0 (gratuit)
      .max(9999.99) // Prix maximum
      .precision(2) // Max 2 décimales
      .required()
      .messages({
        'number.min': 'Le prix ne peut pas être négatif',
        'any.required': 'Le prix est obligatoire'
      }),

    currency: Joi.string()
      .length(3)     // ISO 4217 : EUR, USD, XOF...
      .uppercase()
      .default('EUR'),

    level: Joi.string()
      .valid('beginner', 'intermediate', 'advanced')
      .required(),

    category: Joi.string()
      .max(100)
      .optional(),

    language: Joi.string()
      .length(2)     // ISO 639-1 : fr, en, ar...
      .lowercase()
      .default('fr'),

    prerequisites: Joi.array()
      .items(Joi.string().uuid())  // Array d'UUIDs de cours prérequis
      .default([])
  });

  // Schéma de validation pour la mise à jour (tous les champs optionnels)
  const updateCourseSchema = Joi.object({
    title: Joi.string().min(5).max(255),
    description: Joi.string().max(5000).allow(null, ''),
    price: Joi.number().min(0).max(9999.99).precision(2),
    level: Joi.string().valid('beginner', 'intermediate', 'advanced'),
    category: Joi.string().max(100),
    language: Joi.string().length(2).lowercase(),
    prerequisites: Joi.array().items(Joi.string().uuid()),
    durationMinutes: Joi.number().integer().min(1)
  }).min(1);  // Au moins un champ requis pour la mise à jour

  module.exports = {
    validateCreateCourse: (data) => createCourseSchema.validate(data, { abortEarly: false }),
    validateUpdateCourse: (data) => updateCourseSchema.validate(data, { abortEarly: false })
  };

================================================================================
CHAPITRE 7 : BONNES PRATIQUES MVC
================================================================================

------------------------------------------------------------------------
7.1 THIN CONTROLLER, FAT MODEL/SERVICE
------------------------------------------------------------------------

RÈGLE D'OR :
  Les Controllers doivent être minces (10-30 lignes typiquement).
  La logique métier va dans les Services/Models.

  [X] CONTROLLER FAT (MAUVAIS) :
  async create(req, res) {
    const { title, price } = req.body;
    // 50 lignes de logique métier ici
    if (price < 0) throw new Error(...);
    const existing = await db.query("SELECT * FROM courses WHERE title = $1", [title]);
    if (existing.rows.length > 0) throw new Error(...);
    const instructor = await db.query("SELECT * FROM users WHERE id = $1", [req.user.id]);
    if (instructor.rows[0].role !== 'instructor') throw new Error(...);
    // ... encore 30 lignes
  }

  [OK] CONTROLLER MINCE (BON) :
  async create(req, res, next) {
    try {
      const { error, value } = validateCreateCourse(req.body);
      if (error) return res.status(400).json({...});
      const course = await CourseService.createCourse(value, req.user);
      return res.status(201).json({ data: CourseSerializer.serialize(course) });
    } catch (error) { next(error); }
  }

------------------------------------------------------------------------
7.2 UTILISER LES SÉRIALISEURS
------------------------------------------------------------------------

Ne JAMAIS retourner directement les objets de base de données dans l'API.
Toujours passer par un serializer pour :
  - Contrôler quels champs sont exposés (sécurité)
  - Formater les données (dates, nombres)
  - Renommer les champs (snake_case -> camelCase)
  - Ajouter des champs calculés

  [X] MAUVAIS :
  const user = await UserRepository.findById(id);
  res.json(user);  // Expose le hash du mot de passe !

  [OK] BON :
  const user = await UserRepository.findById(id);
  res.json({ data: UserSerializer.serialize(user) });  // Champs contrôlés

------------------------------------------------------------------------
7.3 VALIDATION À DEUX NIVEAUX
------------------------------------------------------------------------

  NIVEAU 1 : Controller — Validation de format (DTO/Joi)
    - Le format est-il correct ? (type, longueur, regex)
    - Les champs obligatoires sont-ils présents ?
    - Retourne HTTP 400 Bad Request

  NIVEAU 2 : Service/Model — Validation métier
    - Les règles métier sont-elles respectées ?
    - L'email existe-t-il déjà ?
    - L'utilisateur a-t-il les droits ?
    - Retourne HTTP 409 Conflict, 403 Forbidden, etc.

  Ne pas faire la validation métier dans le Controller !
  Ne pas faire la validation de format dans le Service !

================================================================================
CHAPITRE 8 : ERREURS FRÉQUENTES DANS MVC
================================================================================

ERREUR 1 : LOGIQUE MÉTIER DANS LA VIEW (Serializer)
  Symptôme : "Dans le serializer, je calcule le prix avec réduction"
  Correction : La logique de calcul va dans le Model/Service.
               Le Serializer appelle course.getDiscountedPrice()

ERREUR 2 : ACCÈS À LA BASE DE DONNÉES DANS LE CONTROLLER
  Symptôme : require('../config/db') dans un Controller
  Correction : Les Controllers appellent des Services,
               les Services appellent des Repositories,
               les Repositories accèdent à la DB.

ERREUR 3 : LOGIQUE DE PRÉSENTATION DANS LE MODEL
  Symptôme : course.toHTMLCard() dans le Model
  Correction : La génération HTML/JSON va dans la View.
               Le Model fait course.toJSON() (données, pas présentation).

ERREUR 4 : CONTROLLERS TESTÉS AVEC UNE VRAIE BASE DE DONNÉES
  Symptôme : Tests lents et fragiles qui nécessitent PostgreSQL
  Correction : Mocker le Service dans les tests du Controller.
               La base de données est testée dans les tests d'intégration.

ERREUR 5 : UN SEUL SERIALIZER POUR TOUS LES CONTEXTES
  Symptôme : Exposer les données privées dans l'API publique
             parce qu'on utilise le même serializer partout
  Correction : Plusieurs méthodes de sérialisation selon le contexte
               (public, authenticated, instructor, admin)

================================================================================
CHAPITRE 9 : EXERCICES PRATIQUES
================================================================================

EXERCICES FACILES

EXERCICE 1 : Implémenter le Model User complet
  Créez la classe User avec tous les attributs et méthodes métier :
  - isEmailVerified()
  - canCreateCourse() (doit être instructor)
  - getFullName()
  - toPublicProfile() (sans données sensibles)
  - static fromDatabase(row)

EXERCICE 2 : Créer le UserSerializer
  Créez un UserSerializer avec 3 méthodes :
  - serialize() : Profil public minimal
  - serializeForOwner() : Profil complet pour le propriétaire
  - serializeForAdmin() : Profil complet avec métadonnées admin

EXERCICE 3 : Implémenter les routes Users
  Définissez toutes les routes pour le module Users :
  - GET /users/:id (public)
  - GET /users/me (authentifié)
  - PUT /users/me (authentifié)
  - GET /users/:id/courses (public)

EXERCICES INTERMÉDIAIRES

EXERCICE 4 : Model Enrollment avec logique métier
  Créez la classe Enrollment avec :
  - updateProgress(percentage) : Met à jour la progression
  - complete() : Marque le cours comme complété
  - isCompleted() : Vérifie si complété
  - canGetCertificate() : Vérifie si le certificat peut être délivré (>= 80%)
  - calculateTimeSpent() : Calcule le temps passé

EXERCICE 5 : Service Enrollment complet
  Implémentez EnrollmentService avec :
  - enroll(userId, courseId, paymentToken)
    -> Vérifier non-inscription, prérequis, gérer paiement
  - updateProgress(enrollmentId, lessonId, completed)
    -> Recalculer la progression
  - getCertificate(enrollmentId)
    -> Vérifier les droits, générer le certificat

EXERCICE 6 : Tests unitaires du Service
  Écrivez des tests Jest pour CourseService :
  - createCourse avec user non-instructor -> erreur ForbiddenError
  - createCourse avec user instructor -> cours créé
  - publishCourse avec cours vide -> erreur
  - publishCourse avec cours complet -> succès
  Mocker CourseRepository pour ne pas utiliser de base de données.

EXERCICES AVANCÉS

EXERCICE 7 : Implement Search avec Elasticsearch (simulé)
  Implémentez une recherche avancée de cours.
  Pour cet exercice, simulez Elasticsearch avec un simple filtre en mémoire.
  L'important est l'architecture : SearchController -> SearchService ->
  SearchRepository (avec une interface qui pourrait être Elasticsearch ou SQL).

EXERCICE 8 : Pagination avec curseur
  Refactorez la pagination de CourseRepository pour utiliser une pagination
  par curseur (cursor-based pagination) au lieu de la pagination par offset.
  Avantage : Plus performante sur les grandes collections, cohérente avec
  les updates en temps réel.
  Implémentez : cursor = last_id, sens : after_cursor / before_cursor

EXERCICE 9 : API Versioning
  Implémentez le versioning de l'API :
  - v1 : API actuelle
  - v2 : Nouvelle version avec format différent (e.g., price en cents,
         not float)
  Comment maintenir les deux versions en parallèle dans la même application ?
  Suggestion : routes/v1/ et routes/v2/, serializers versionnés.

================================================================================
CHAPITRE 10 : CORRIGÉS
================================================================================

CORRIGÉ EXERCICE 4 : Model Enrollment

  class Enrollment {
    constructor({ id, userId, courseId, status = 'active',
                  progressPercentage = 0, enrolledAt = new Date(),
                  completedAt = null }) {
      this.id = id;
      this.userId = userId;
      this.courseId = courseId;
      this.status = status;
      this.progressPercentage = progressPercentage;
      this.enrolledAt = enrolledAt;
      this.completedAt = completedAt;
    }

    // Met à jour la progression (0-100)
    updateProgress(percentage) {
      if (percentage < 0 || percentage > 100) {
        throw new Error('La progression doit être entre 0 et 100');
      }
      this.progressPercentage = percentage;

      // Si 100%, marquer comme complété automatiquement
      if (percentage === 100 && this.status === 'active') {
        this.complete();
      }
      return this;
    }

    // Marquer le cours comme complété
    complete() {
      if (this.status !== 'active') {
        throw new Error('Seule une inscription active peut être complétée');
      }
      this.status = 'completed';
      this.completedAt = new Date();
      this.progressPercentage = 100;
      return this;
    }

    isCompleted() {
      return this.status === 'completed';
    }

    // Le certificat est délivré si progression >= 80% ET status completed
    // OU si progressPercentage >= 80 (pour les cours sans marque complété)
    canGetCertificate() {
      return this.progressPercentage >= 80;
    }

    calculateTimeSpent() {
      const now = this.completedAt || new Date();
      const diffMs = now - this.enrolledAt;
      const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
      return {
        days: diffDays,
        milliseconds: diffMs
      };
    }
  }

CORRIGÉ EXERCICE 6 : Tests CourseService

  const CourseService = require('./CourseService');
  const CourseRepository = require('../repositories/CourseRepository');
  const Course = require('../models/Course');

  // Mocker le repository — on ne veut pas toucher la base de données
  jest.mock('../repositories/CourseRepository');

  describe('CourseService', () => {

    const instructorUser = { id: 'user-1', role: 'instructor' };
    const studentUser = { id: 'user-2', role: 'student' };
    const validCourseData = {
      title: 'Introduction à Python',
      price: 29.99,
      level: 'beginner'
    };

    beforeEach(() => {
      jest.clearAllMocks();  // Reset les mocks avant chaque test
    });

    describe('createCourse', () => {

      test('doit créer un cours pour un instructor', async () => {
        // ARRANGE : Configurer le mock du repository
        const mockCourse = new Course({ ...validCourseData, instructorId: 'user-1' });
        CourseRepository.save.mockResolvedValue(mockCourse);

        // ACT : Appeler le service
        const result = await CourseService.createCourse(validCourseData, instructorUser);

        // ASSERT : Vérifier le résultat
        expect(CourseRepository.save).toHaveBeenCalledTimes(1);
        expect(result.instructorId).toBe('user-1');
        expect(result.status).toBe('draft');  // Toujours brouillon
      });

      test('doit lever ForbiddenError pour un student', async () => {
        // ASSERT : Vérifier que l'erreur est levée
        await expect(
          CourseService.createCourse(validCourseData, studentUser)
        ).rejects.toThrow('Seuls les instructeurs peuvent créer des cours');

        // Le repository ne doit JAMAIS être appelé
        expect(CourseRepository.save).not.toHaveBeenCalled();
      });
    });

    describe('publishCourse', () => {

      test('doit lever une erreur si le cours est incomplet', async () => {
        const incompleteCourse = new Course({
          id: 'course-1',
          title: 'Ok',  // Moins de 5 chars
          price: 0,
          instructorId: 'user-1',
          status: 'draft',
          durationMinutes: null  // Pas de durée
        });
        CourseRepository.findById.mockResolvedValue(incompleteCourse);

        await expect(
          CourseService.publishCourse('course-1', instructorUser)
        ).rejects.toThrow();
      });

      test('doit publier un cours complet', async () => {
        const completeCourse = new Course({
          id: 'course-1',
          title: 'Introduction à Python',
          description: 'Apprenez Python depuis les bases. ' + 'x'.repeat(30),
          price: 29.99,
          instructorId: 'user-1',
          status: 'draft',
          durationMinutes: 120
        });
        CourseRepository.findById.mockResolvedValue(completeCourse);
        CourseRepository.update.mockImplementation(c => c);  // Retourne le cours

        const result = await CourseService.publishCourse('course-1', instructorUser);

        expect(result.status).toBe('published');
        expect(result.publishedAt).not.toBeNull();
      });
    });
  });

================================================================================
RÉCAPITULATIF — ARCHITECTURE MVC
================================================================================

Points clés :

1. MVC = Model (données + logique) + View (présentation) + Controller (orchestration)

2. THIN CONTROLLER : Les Controllers orchestrent, ils ne contiennent PAS
   de logique métier.

3. FAT MODEL/SERVICE : La logique métier va dans les Models ou Services.

4. LA VIEW pour une API REST = Le Serializer JSON
   Contrôle quels champs sont exposés et comment.

5. VALIDATION À DEUX NIVEAUX :
   - Controller : Format des données (DTO/Joi)
   - Service/Model : Règles métier

6. TESTABILITÉ : MVC facilite les tests unitaires en isolant les couches.

7. EDUCONNECT MVC : Structure claire avec Models, Services, Controllers,
   Serializers et Routes séparés.

Prochaine étape :
  Volume 4 : Architecture en Couches (Layered / N-Tiers)
  -> Formaliser et approfondir les couches de l'application
  -> Introduire le concept de dépendances unidirectionnelles

================================================================================
FIN DU VOLUME 3 — ARCHITECTURE MVC
Prochaine étape -> architecture_layered.txt
================================================================================

================================================================================
     GUIDE COMPLET DES ARCHITECTURES LOGICIELLES - VOLUME 4
     Architecture en Couches (Layered / N-Tiers)
     Pour étudiants en Génie Logiciel
================================================================================

================================================================================
CHAPITRE 1 : INTRODUCTION
================================================================================

------------------------------------------------------------------------
1.1 DÉFINITION
------------------------------------------------------------------------

L'architecture en couches (Layered Architecture ou N-Tier Architecture)
organise le code en strates horizontales empilées, où chaque couche a une
responsabilité exclusive et ne communique qu'avec la couche directement
adjacente.

Analogie : Un millefeuille
  - Couche feuilletée = Présentation (ce qu'on voit)
  - Couche crème     = Logique applicative
  - Couche crème     = Logique métier
  - Couche feuilletée = Données (fondation)
  Chaque couche tient seule ET contribue à l'ensemble.

RÈGLE FONDAMENTALE :
  Une couche ne peut communiquer qu'avec la couche IMMÉDIATEMENT en dessous.
  Jamais sauter une couche. Jamais communiquer vers le haut.

  ┌───────────────────────┐  COUCHE 1
  │   Présentation        │  <- Peut appeler Couche 2
  └──────────┬────────────┘
             │ (uniquement vers le bas)
  ┌──────────[BLACK_DOWN-POINTING_TRIANGLE]────────────┐  COUCHE 2
  │   Application         │  <- Peut appeler Couche 3
  └──────────┬────────────┘
             │
  ┌──────────[BLACK_DOWN-POINTING_TRIANGLE]────────────┐  COUCHE 3
  │   Domaine / Métier    │  <- Peut appeler Couche 4
  └──────────┬────────────┘
             │
  ┌──────────[BLACK_DOWN-POINTING_TRIANGLE]────────────┐  COUCHE 4
  │   Infrastructure      │  <- Ne remonte pas vers le haut
  └───────────────────────┘

------------------------------------------------------------------------
1.2 ÉVOLUTION HISTORIQUE
------------------------------------------------------------------------

ARCHITECTURE 1-TIER (Mainframe)
  Tout sur un seul ordinateur. Interface + logique + données centralisées.
  Années 60-70.

ARCHITECTURE 2-TIERS (Client-Serveur)
  Client (UI) + Serveur (logique + données).
  Applications desktop des années 80-90.
  Ex : Application Windows + SQL Server

ARCHITECTURE 3-TIERS (Standard actuel)
  Tier 1 : Présentation (navigateur, app mobile)
  Tier 2 : Logique métier (serveur applicatif)
  Tier 3 : Données (base de données)
  Dominant depuis les années 2000.

ARCHITECTURE N-TIERS (Enterprise)
  3 tiers + couches supplémentaires pour les grands systèmes.
  Séparation fine de l'Application, du Domaine, de l'Infrastructure.

------------------------------------------------------------------------
1.3 POURQUOI L'ARCHITECTURE EN COUCHES ?
------------------------------------------------------------------------

SANS COUCHES :
  Chaque partie du code peut accéder à n'importe quelle autre.
  Résultat : Spaghetti code impossible à maintenir.

AVEC COUCHES :
  Chaque couche est isolée. Changer une couche n'impacte pas les autres
  (tant que l'interface reste la même).

  Exemple concret :
  - Vous changez de PostgreSQL à MongoDB -> Seule la couche Infrastructure change
  - Vous changez l'API REST en GraphQL -> Seule la couche Présentation change
  - Vous changez une règle métier -> Seule la couche Domaine change

================================================================================
CHAPITRE 2 : THÉORIE DÉTAILLÉE
================================================================================

------------------------------------------------------------------------
2.1 LES QUATRE COUCHES STANDARD
------------------------------------------------------------------------

COUCHE 1 : PRÉSENTATION (Presentation Layer)
  Noms alternatifs : UI Layer, API Layer, Interface Layer
  
  Responsabilités :
    - Recevoir et valider les entrées utilisateur
    - Appeler la couche Application
    - Formater et retourner les résultats
    - Gérer l'authentification et l'autorisation (middleware)
  
  Composants typiques :
    - Controllers HTTP
    - Middlewares (auth, validation, logging)
    - Sérialiseurs / Transformers
    - Routes

  Ce qu'elle NE FAIT PAS :
    - Logique métier
    - Accès base de données
    - Calculs complexes

COUCHE 2 : APPLICATION (Application Layer)
  Noms alternatifs : Service Layer, Use Case Layer
  
  Responsabilités :
    - Orchestrer les Use Cases (scénarios d'utilisation)
    - Coordonner les objets du domaine
    - Gérer les transactions
    - Appeler les services d'infrastructure (email, SMS...)
  
  Composants typiques :
    - Services applicatifs
    - Use Cases / Interactors
    - DTOs (Data Transfer Objects)
    - Command / Query handlers

  Ce qu'elle NE FAIT PAS :
    - Logique métier (dans les entités du Domaine)
    - Accès direct à la base de données
    - Génération de réponses HTTP

COUCHE 3 : DOMAINE (Domain Layer)
  Noms alternatifs : Business Layer, Core Layer
  
  Responsabilités :
    - Représenter les concepts métier (entités)
    - Contenir les règles métier fondamentales
    - Définir les interfaces des repositories
    - Définir les événements du domaine
  
  Composants typiques :
    - Entités (Entity)
    - Objets-valeur (Value Object)
    - Agrégats (Aggregate)
    - Interfaces des repositories
    - Domain Events
    - Domain Services

  RÈGLE CRITIQUE :
    La couche Domaine ne dépend d'AUCUNE autre couche.
    Elle ne connaît ni les frameworks, ni la base de données, ni HTTP.
    C'est le "cœur pur" de l'application.

COUCHE 4 : INFRASTRUCTURE (Infrastructure Layer)
  Noms alternatifs : Data Layer, Persistence Layer
  
  Responsabilités :
    - Implémenter les interfaces définies par le Domaine
    - Accès et persistance des données (SQL, NoSQL, fichiers)
    - Communication avec services externes (email, paiement, SMS)
    - Configuration et connexions
  
  Composants typiques :
    - Implémentations des repositories
    - ORM / Query builders
    - Clients HTTP pour services externes
    - Adaptateurs email, SMS, paiement
    - Configuration base de données

------------------------------------------------------------------------
2.2 RÈGLES DE DÉPENDANCES
------------------------------------------------------------------------

RÈGLE STRICTE :
  Les dépendances ne vont QUE vers le bas.

  ┌─────────────────────────────────────────────────────────┐
  │  Présentation  ->  Application  ->  Domaine               │
  │                                     ^                   │
  │  Infrastructure  ->  Domaine         │                   │
  │                       ^             │                   │
  │                       │             │                   │
  │  La couche Domaine est au centre, pas au fond !         │
  └─────────────────────────────────────────────────────────┘

INVERSEMENT DES DÉPENDANCES :
  Comment la couche Domaine peut-elle utiliser la base de données
  sans dépendre de la couche Infrastructure ?
  
  Réponse : Via des INTERFACES (Dependency Inversion Principle)
  
  Le Domaine définit l'interface :
    interface CourseRepository {
      save(course: Course): Promise<Course>
      findById(id: string): Promise<Course | null>
      findAll(filters): Promise<Course[]>
    }
  
  L'Infrastructure implémente l'interface :
    class PostgresCourseRepository implements CourseRepository {
      save(course) { // SQL INSERT/UPDATE }
      findById(id) { // SQL SELECT }
      findAll(filters) { // SQL SELECT avec WHERE }
    }
  
  L'Application injecte l'implémentation dans le Domaine :
    const courseService = new CourseService(new PostgresCourseRepository())

  Résultat : Le Domaine ne connaît que l'interface, pas Postgres.

------------------------------------------------------------------------
2.3 COUCHE OUVERTE VS FERMÉE
------------------------------------------------------------------------

COUCHE FERMÉE (par défaut) :
  Chaque requête doit passer par TOUTES les couches dans l'ordre.
  Présentation -> Application -> Domaine -> Infrastructure
  
  Avantage : Isolation maximale, changements maîtrisés
  Inconvénient : Verbeux pour les opérations simples

COUCHE OUVERTE :
  Certaines couches peuvent être "court-circuitées" pour les cas simples.
  Ex : Données de référence (pays, devises) -> Présentation -> Infrastructure directement
  
  Usage : À utiliser avec parcimonie et justification

------------------------------------------------------------------------
2.4 AVANTAGES ET INCONVÉNIENTS
------------------------------------------------------------------------

AVANTAGES :
  [OK] Isolation des changements
  [OK] Testabilité par couche
  [OK] Réutilisabilité des couches inférieures
  [OK] Compréhension progressive (étudier couche par couche)
  [OK] Séparation nette des responsabilités
  [OK] Standard bien connu -> onboarding facile

INCONVÉNIENTS :
  [X] Performance : Chaque appel traverse toutes les couches (overhead)
  [X] Verbosité : Beaucoup de classes et interfaces pour des features simples
  [X] Tendance à "sink" toute la logique dans la couche Application
  [X] "Architecture lasagne" si mal appliquée : trop de couches inutiles

================================================================================
CHAPITRE 3 : SCHÉMAS
================================================================================

------------------------------------------------------------------------
3.1 ARCHITECTURE 3-TIERS CLASSIQUE
------------------------------------------------------------------------

  ╔══════════════════════════════════════════════════════════════╗
  ║                     TIER 1 : CLIENT                         ║
  ║                                                              ║
  ║   Navigateur Web    App Mobile     Application Desktop       ║
  ╚══════════════════════════════╤═══════════════════════════════╝
                                 │ HTTP / WebSocket / gRPC
  ╔══════════════════════════════[BLACK_DOWN-POINTING_TRIANGLE]═══════════════════════════════╗
  ║                    TIER 2 : SERVEUR                          ║
  ║                                                              ║
  ║  ┌─────────────────────────────────────────────────────┐    ║
  ║  │             COUCHE PRÉSENTATION                     │    ║
  ║  │  Controllers + Routes + Auth + Validation          │    ║
  ║  └──────────────────────────┬──────────────────────────┘    ║
  ║                             │                               ║
  ║  ┌──────────────────────────[BLACK_DOWN-POINTING_TRIANGLE]──────────────────────────┐    ║
  ║  │             COUCHE APPLICATION                      │    ║
  ║  │  Services + Use Cases + Orchestration              │    ║
  ║  └──────────────────────────┬──────────────────────────┘    ║
  ║                             │                               ║
  ║  ┌──────────────────────────[BLACK_DOWN-POINTING_TRIANGLE]──────────────────────────┐    ║
  ║  │                COUCHE DOMAINE                       │    ║
  ║  │  Entités + Règles métier + Interfaces Repo         │    ║
  ║  └──────────────────────────┬──────────────────────────┘    ║
  ║                             │                               ║
  ║  ┌──────────────────────────[BLACK_DOWN-POINTING_TRIANGLE]──────────────────────────┐    ║
  ║  │            COUCHE INFRASTRUCTURE                    │    ║
  ║  │  Repositories + ORM + Email + Paiement + Cache     │    ║
  ║  └──────────────────────────┬──────────────────────────┘    ║
  ╚══════════════════════════════╤═══════════════════════════════╝
                                 │
  ╔══════════════════════════════[BLACK_DOWN-POINTING_TRIANGLE]═══════════════════════════════╗
  ║                     TIER 3 : DONNÉES                        ║
  ║                                                              ║
  ║   PostgreSQL        Redis Cache       AWS S3 (Files)         ║
  ╚══════════════════════════════════════════════════════════════╝

------------------------------------------------------------------------
3.2 FLUX DE REQUÊTE COMPLET
------------------------------------------------------------------------

  POST /api/v1/courses/:id/enroll

  TIER CLIENT
  │ Étudiant clique "S'inscrire" -> requête HTTP
  │
  [BLACK_DOWN-POINTING_TRIANGLE]
  ═══════════════════════ COUCHE PRÉSENTATION ═══════════════════
  │
  │ 1. authMiddleware -> vérifie JWT -> extrait user
  │ 2. validationMiddleware -> valide req.body format
  │ 3. EnrollmentController.create()
  │    - extrait courseId, paymentToken
  │    - crée le DTO EnrollEnrollDTO
  │    - appelle EnrollmentApplicationService.enroll(dto)
  │
  [BLACK_DOWN-POINTING_TRIANGLE]
  ═══════════════════════ COUCHE APPLICATION ════════════════════
  │
  │ 4. EnrollmentApplicationService.enroll(dto):
  │    a. Charge l'étudiant via UserRepository.findById()
  │    b. Charge le cours via CourseRepository.findById()
  │    c. Vérifie existence de l'enrollment (déjà inscrit ?)
  │    d. Crée l'Enrollment (entité domaine)
  │    e. Appelle PaymentService.charge()
  │    f. Sauvegarde Enrollment via EnrollmentRepository.save()
  │    g. Émet l'événement EnrollmentCreated
  │
  [BLACK_DOWN-POINTING_TRIANGLE]
  ═══════════════════════ COUCHE DOMAINE ═══════════════════════
  │
  │ 5. Enrollment (Entité):
  │    - validate() : règles métier (prérequis, disponibilité...)
  │    - Construit l'Enrollment avec statut 'active'
  │    - Ajoute l'événement domaine EnrollmentCreated
  │
  │ 6. Interfaces (définitions uniquement):
  │    - EnrollmentRepository (interface)
  │    - CourseRepository (interface)
  │
  [BLACK_DOWN-POINTING_TRIANGLE]
  ═══════════════════════ COUCHE INFRASTRUCTURE ════════════════
  │
  │ 7. PostgresEnrollmentRepository.save():
  │    - INSERT INTO enrollments ... RETURNING *
  │    - Convertit Row -> Entité Enrollment
  │
  │ 8. StripePaymentAdapter.charge():
  │    - Appel API Stripe
  │    - Retourne PaymentResult
  │
  │ 9. SendGridEmailAdapter.send():
  │    - Envoie email de confirmation
  │
  [BLACK_DOWN-POINTING_TRIANGLE]
  TIER DONNÉES : INSERT + Commit transaction

------------------------------------------------------------------------
3.3 STRUCTURE DE PROJET EDUCONNECT EN COUCHES
------------------------------------------------------------------------

  src/
  ├── presentation/                <- COUCHE PRÉSENTATION
  │   ├── http/
  │   │   ├── controllers/
  │   │   │   ├── CourseController.ts
  │   │   │   ├── EnrollmentController.ts
  │   │   │   └── UserController.ts
  │   │   ├── middlewares/
  │   │   │   ├── auth.middleware.ts
  │   │   │   ├── error.middleware.ts
  │   │   │   └── validation.middleware.ts
  │   │   ├── routes/
  │   │   │   ├── course.routes.ts
  │   │   │   └── enrollment.routes.ts
  │   │   └── serializers/
  │   │       ├── CourseSerializer.ts
  │   │       └── EnrollmentSerializer.ts
  │   └── app.ts
  │
  ├── application/                 <- COUCHE APPLICATION
  │   ├── services/
  │   │   ├── CourseApplicationService.ts
  │   │   ├── EnrollmentApplicationService.ts
  │   │   └── UserApplicationService.ts
  │   ├── dtos/
  │   │   ├── CreateCourseDTO.ts
  │   │   ├── EnrollDTO.ts
  │   │   └── UpdateUserDTO.ts
  │   └── use-cases/
  │       ├── EnrollStudentUseCase.ts
  │       └── PublishCourseUseCase.ts
  │
  ├── domain/                      <- COUCHE DOMAINE (cœur)
  │   ├── entities/
  │   │   ├── Course.ts
  │   │   ├── Enrollment.ts
  │   │   └── User.ts
  │   ├── value-objects/
  │   │   ├── Money.ts
  │   │   ├── Email.ts
  │   │   └── CourseLevel.ts
  │   ├── repositories/            <- Interfaces UNIQUEMENT
  │   │   ├── ICourseRepository.ts
  │   │   ├── IEnrollmentRepository.ts
  │   │   └── IUserRepository.ts
  │   ├── events/
  │   │   ├── EnrollmentCreated.ts
  │   │   └── CoursePublished.ts
  │   └── services/
  │       └── PricingDomainService.ts
  │
  └── infrastructure/              <- COUCHE INFRASTRUCTURE
      ├── persistence/
      │   ├── postgres/
      │   │   ├── PostgresCourseRepository.ts
      │   │   ├── PostgresEnrollmentRepository.ts
      │   │   └── PostgresUserRepository.ts
      │   └── redis/
      │       └── RedisCacheRepository.ts
      ├── external/
      │   ├── stripe/
      │   │   └── StripePaymentAdapter.ts
      │   ├── sendgrid/
      │   │   └── SendGridEmailAdapter.ts
      │   └── s3/
      │       └── S3FileStorageAdapter.ts
      └── config/
          ├── database.ts
          └── container.ts          <- Injection de dépendances

================================================================================
CHAPITRE 4 : IMPLÉMENTATION COMPLÈTE
================================================================================

------------------------------------------------------------------------
4.1 COUCHE DOMAINE — ENTITÉS ET VALUE OBJECTS
------------------------------------------------------------------------

  // ============================================================
  // domain/value-objects/Money.ts
  // Value Object — Représente une valeur monétaire
  // Un Value Object est immuable et défini par sa valeur, pas son identité
  // ============================================================

  export class Money {
    // Properties readonly = immuabilité garantie
    private readonly _amount: number;   // Montant en centimes (évite les virgules flottantes)
    private readonly _currency: string; // ISO 4217

    constructor(amount: number, currency: string) {
      // Validation dans le constructeur
      if (amount < 0) {
        throw new Error('Un montant monétaire ne peut pas être négatif');
      }
      if (!['EUR', 'USD', 'XOF', 'GBP'].includes(currency)) {
        throw new Error(`Devise non supportée : ${currency}`);
      }
      // Stocker en centimes pour éviter les problèmes de virgule flottante
      this._amount = Math.round(amount * 100);
      this._currency = currency;
    }

    // Accès en euros (pas en centimes)
    get amount(): number { return this._amount / 100; }
    get currency(): string { return this._currency; }
    get amountInCents(): number { return this._amount; }

    // Opérations qui retournent un NOUVEAU Money (immuabilité)
    add(other: Money): Money {
      if (this._currency !== other._currency) {
        throw new Error('Impossible d\'additionner des devises différentes');
      }
      return new Money((this._amount + other._amount) / 100, this._currency);
    }

    multiply(factor: number): Money {
      return new Money((this._amount * factor) / 100, this._currency);
    }

    applyDiscount(percentage: number): Money {
      if (percentage < 0 || percentage > 100) {
        throw new Error('Le pourcentage de réduction doit être entre 0 et 100');
      }
      const factor = 1 - (percentage / 100);
      return this.multiply(factor);
    }

    // Comparaisons
    equals(other: Money): boolean {
      return this._amount === other._amount && this._currency === other._currency;
    }

    isZero(): boolean { return this._amount === 0; }

    // Représentation
    toString(): string {
      return `${this.amount.toFixed(2)} ${this._currency}`;
    }

    // Factory method
    static of(amount: number, currency: string = 'EUR'): Money {
      return new Money(amount, currency);
    }

    static zero(currency: string = 'EUR'): Money {
      return new Money(0, currency);
    }
  }

  // ============================================================
  // domain/entities/Course.ts
  // Entité Course — Identifiée par son id
  // ============================================================

  import { Money } from '../value-objects/Money';
  import { CoursePublished } from '../events/CoursePublished';

  // Enum pour les états valides d'un cours
  export enum CourseStatus {
    DRAFT = 'draft',
    PUBLISHED = 'published',
    ARCHIVED = 'archived'
  }

  export enum CourseLevel {
    BEGINNER = 'beginner',
    INTERMEDIATE = 'intermediate',
    ADVANCED = 'advanced'
  }

  // Interface pour reconstruire une entité depuis la persistance
  export interface CourseProps {
    id: string;
    title: string;
    description: string | null;
    instructorId: string;
    price: Money;
    level: CourseLevel;
    status: CourseStatus;
    prerequisites: string[];
    durationMinutes: number | null;
    publishedAt: Date | null;
    createdAt: Date;
    updatedAt: Date;
  }

  export class Course {
    private _props: CourseProps;
    private _domainEvents: any[] = [];   // Événements du domaine à dispatcher

    // Constructeur privé — forcer l'utilisation des factory methods
    private constructor(props: CourseProps) {
      this._props = props;
    }

    // ── Factory Methods ──────────────────────────────────────

    // Créer un nouveau cours (use case: instructor crée un cours)
    static create(
      id: string,
      title: string,
      instructorId: string,
      price: Money,
      level: CourseLevel
    ): Course {
      // Validation à la création
      if (!title || title.trim().length < 5) {
        throw new Error('Le titre doit contenir au moins 5 caractères');
      }

      return new Course({
        id,
        title: title.trim(),
        description: null,
        instructorId,
        price,
        level,
        status: CourseStatus.DRAFT,
        prerequisites: [],
        durationMinutes: null,
        publishedAt: null,
        createdAt: new Date(),
        updatedAt: new Date()
      });
    }

    // Reconstruire depuis la persistance
    static reconstitute(props: CourseProps): Course {
      return new Course(props);
    }

    // ── Getters (accès lecture) ──────────────────────────────
    get id() { return this._props.id; }
    get title() { return this._props.title; }
    get instructorId() { return this._props.instructorId; }
    get price() { return this._props.price; }
    get status() { return this._props.status; }
    get level() { return this._props.level; }
    get description() { return this._props.description; }
    get prerequisites() { return [...this._props.prerequisites]; }
    get durationMinutes() { return this._props.durationMinutes; }
    get publishedAt() { return this._props.publishedAt; }
    get createdAt() { return this._props.createdAt; }
    get updatedAt() { return this._props.updatedAt; }
    get domainEvents() { return [...this._domainEvents]; }

    // ── Comportements (logique métier) ───────────────────────

    updateDescription(description: string): void {
      if (!description || description.trim().length < 50) {
        throw new Error('La description doit contenir au moins 50 caractères');
      }
      this._props.description = description.trim();
      this._props.updatedAt = new Date();
    }

    setDuration(minutes: number): void {
      if (minutes <= 0) throw new Error('La durée doit être positive');
      this._props.durationMinutes = minutes;
      this._props.updatedAt = new Date();
    }

    publish(): void {
      // Invariant : Un cours ne peut être publié que si complet
      this._ensureCanBePublished();

      this._props.status = CourseStatus.PUBLISHED;
      this._props.publishedAt = new Date();
      this._props.updatedAt = new Date();

      // Émettre l'événement du domaine
      this._domainEvents.push(
        new CoursePublished(this.id, this.instructorId, new Date())
      );
    }

    archive(): void {
      if (this._props.status === CourseStatus.DRAFT) {
        throw new Error('Un brouillon ne peut pas être archivé directement');
      }
      this._props.status = CourseStatus.ARCHIVED;
      this._props.updatedAt = new Date();
    }

    updatePrice(newPrice: Money): void {
      if (this._props.status === CourseStatus.PUBLISHED) {
        // Règle métier : avertissement si le cours a des inscrits
        // (vérification complète dans le service applicatif)
      }
      this._props.price = newPrice;
      this._props.updatedAt = new Date();
    }

    clearDomainEvents(): void {
      this._domainEvents = [];
    }

    isPublished(): boolean {
      return this._props.status === CourseStatus.PUBLISHED;
    }

    isFree(): boolean {
      return this._props.price.isZero();
    }

    // ── Invariants (règles inviolables) ─────────────────────

    private _ensureCanBePublished(): void {
      const errors: string[] = [];

      if (!this._props.title || this._props.title.length < 5) {
        errors.push('Titre trop court (min 5 caractères)');
      }
      if (!this._props.description || this._props.description.length < 50) {
        errors.push('Description trop courte (min 50 caractères)');
      }
      if (!this._props.durationMinutes || this._props.durationMinutes <= 0) {
        errors.push('Durée non renseignée');
      }
      if (this._props.status === CourseStatus.PUBLISHED) {
        errors.push('Le cours est déjà publié');
      }
      if (this._props.status === CourseStatus.ARCHIVED) {
        errors.push('Un cours archivé ne peut pas être republié');
      }

      if (errors.length > 0) {
        throw new Error(`Impossible de publier le cours : ${errors.join(', ')}`);
      }
    }
  }

------------------------------------------------------------------------
4.2 COUCHE DOMAINE — INTERFACE REPOSITORY
------------------------------------------------------------------------

  // ============================================================
  // domain/repositories/ICourseRepository.ts
  // Interface — Contrat que l'Infrastructure doit respecter
  // Le Domaine définit CE QU'IL VEUT, l'Infrastructure livre COMMENT
  // ============================================================

  import { Course } from '../entities/Course';

  export interface CourseFilters {
    status?: string;
    category?: string;
    level?: string;
    search?: string;
    instructorId?: string;
    page?: number;
    limit?: number;
  }

  export interface PaginatedResult<T> {
    data: T[];
    total: number;
    page: number;
    limit: number;
    totalPages: number;
  }

  export interface ICourseRepository {
    // Sauvegarde (create ou update selon si id existe)
    save(course: Course): Promise<Course>;

    // Lecture
    findById(id: string): Promise<Course | null>;
    findAll(filters: CourseFilters): Promise<PaginatedResult<Course>>;
    findByInstructor(instructorId: string): Promise<Course[]>;

    // Vérifications
    existsByTitle(title: string, excludeId?: string): Promise<boolean>;
    countByInstructor(instructorId: string): Promise<number>;

    // Suppression (soft delete)
    delete(id: string): Promise<void>;
  }

------------------------------------------------------------------------
4.3 COUCHE APPLICATION — USE CASE
------------------------------------------------------------------------

  // ============================================================
  // application/use-cases/EnrollStudentUseCase.ts
  // Use Case — Un scénario d'utilisation complet
  // ============================================================

  import { ICourseRepository } from '../../domain/repositories/ICourseRepository';
  import { IEnrollmentRepository } from '../../domain/repositories/IEnrollmentRepository';
  import { IUserRepository } from '../../domain/repositories/IUserRepository';
  import { IPaymentService } from '../../domain/services/IPaymentService';
  import { IEventBus } from '../../domain/events/IEventBus';
  import { Enrollment } from '../../domain/entities/Enrollment';
  import { Money } from '../../domain/value-objects/Money';

  // Input DTO : ce que le Use Case reçoit
  export interface EnrollStudentInput {
    studentId: string;
    courseId: string;
    paymentToken: string | null;  // null si cours gratuit
  }

  // Output DTO : ce que le Use Case retourne
  export interface EnrollStudentOutput {
    enrollmentId: string;
    courseId: string;
    studentId: string;
    enrolledAt: Date;
    paymentId: string | null;
  }

  export class EnrollStudentUseCase {
    // Injection de dépendances via le constructeur
    // Le Use Case ne connaît que les interfaces, jamais les implémentations
    constructor(
      private readonly courseRepository: ICourseRepository,
      private readonly enrollmentRepository: IEnrollmentRepository,
      private readonly userRepository: IUserRepository,
      private readonly paymentService: IPaymentService,
      private readonly eventBus: IEventBus
    ) {}

    async execute(input: EnrollStudentInput): Promise<EnrollStudentOutput> {

      // ── Étape 1 : Charger les agrégats nécessaires ──────────

      const student = await this.userRepository.findById(input.studentId);
      if (!student) {
        throw new Error(`Étudiant ${input.studentId} non trouvé`);
      }

      const course = await this.courseRepository.findById(input.courseId);
      if (!course) {
        throw new Error(`Cours ${input.courseId} non trouvé`);
      }

      // ── Étape 2 : Règles métier applicatives ─────────────────

      // Vérifier que le cours est disponible
      if (!course.isPublished()) {
        throw new Error('Ce cours n\'est pas disponible pour inscription');
      }

      // Vérifier que l'étudiant n'est pas déjà inscrit
      const existingEnrollment = await this.enrollmentRepository.findByStudentAndCourse(
        input.studentId,
        input.courseId
      );
      if (existingEnrollment) {
        throw new Error('Vous êtes déjà inscrit à ce cours');
      }

      // Vérifier les prérequis
      if (course.prerequisites.length > 0) {
        const completedCourses = await this.enrollmentRepository.findCompletedByStudent(
          input.studentId
        );
        const completedCourseIds = completedCourses.map(e => e.courseId);

        const missingPrereqs = course.prerequisites.filter(
          prereqId => !completedCourseIds.includes(prereqId)
        );

        if (missingPrereqs.length > 0) {
          throw new Error(
            `Prérequis non complétés : ${missingPrereqs.join(', ')}`
          );
        }
      }

      // ── Étape 3 : Gestion du paiement ────────────────────────

      let paymentId: string | null = null;

      if (!course.isFree()) {
        if (!input.paymentToken) {
          throw new Error('Un token de paiement est requis pour ce cours payant');
        }

        const paymentResult = await this.paymentService.charge({
          token: input.paymentToken,
          amount: course.price,
          description: `Inscription au cours : ${course.title}`,
          customerId: input.studentId
        });

        paymentId = paymentResult.paymentId;
      }

      // ── Étape 4 : Créer l'enrollment (entité domaine) ────────

      const { v4: uuidv4 } = require('uuid');

      const enrollment = Enrollment.create(
        uuidv4(),
        input.studentId,
        input.courseId,
        paymentId
      );

      // ── Étape 5 : Persister ───────────────────────────────────

      const savedEnrollment = await this.enrollmentRepository.save(enrollment);

      // ── Étape 6 : Publier les événements du domaine ──────────

      // Les événements sont dispatchés APRÈS la persistence
      // (pas avant : cohérence garantie)
      for (const event of enrollment.domainEvents) {
        await this.eventBus.publish(event);
      }
      enrollment.clearDomainEvents();

      // ── Étape 7 : Retourner le résultat ──────────────────────

      return {
        enrollmentId: savedEnrollment.id,
        courseId: savedEnrollment.courseId,
        studentId: savedEnrollment.studentId,
        enrolledAt: savedEnrollment.enrolledAt,
        paymentId
      };
    }
  }

------------------------------------------------------------------------
4.4 COUCHE INFRASTRUCTURE — REPOSITORY POSTGRESQL
------------------------------------------------------------------------

  // ============================================================
  // infrastructure/persistence/postgres/PostgresCourseRepository.ts
  // Implémentation concrète du ICourseRepository pour PostgreSQL
  // ============================================================

  import { Pool } from 'pg';
  import { ICourseRepository, CourseFilters, PaginatedResult } from '../../../domain/repositories/ICourseRepository';
  import { Course, CourseStatus, CourseLevel } from '../../../domain/entities/Course';
  import { Money } from '../../../domain/value-objects/Money';

  export class PostgresCourseRepository implements ICourseRepository {

    constructor(private readonly pool: Pool) {}

    async save(course: Course): Promise<Course> {
      const query = `
        INSERT INTO courses (id, title, description, instructor_id,
                             price_amount, price_currency, level, status,
                             prerequisites, duration_minutes, published_at,
                             created_at, updated_at)
        VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
        ON CONFLICT (id) DO UPDATE SET
          title = EXCLUDED.title,
          description = EXCLUDED.description,
          price_amount = EXCLUDED.price_amount,
          price_currency = EXCLUDED.price_currency,
          level = EXCLUDED.level,
          status = EXCLUDED.status,
          prerequisites = EXCLUDED.prerequisites,
          duration_minutes = EXCLUDED.duration_minutes,
          published_at = EXCLUDED.published_at,
          updated_at = EXCLUDED.updated_at
        RETURNING *
      `;

      const result = await this.pool.query(query, [
        course.id,
        course.title,
        course.description,
        course.instructorId,
        course.price.amountInCents,   // Stocker en centimes
        course.price.currency,
        course.level,
        course.status,
        JSON.stringify(course.prerequisites),
        course.durationMinutes,
        course.publishedAt,
        course.createdAt,
        course.updatedAt
      ]);

      return this._toDomain(result.rows[0]);
    }

    async findById(id: string): Promise<Course | null> {
      const result = await this.pool.query(
        'SELECT * FROM courses WHERE id = $1',
        [id]
      );

      if (result.rows.length === 0) return null;
      return this._toDomain(result.rows[0]);
    }

    async findAll(filters: CourseFilters): Promise<PaginatedResult<Course>> {
      const { page = 1, limit = 20, status = 'published', category, level, search } = filters;
      const offset = (page - 1) * limit;

      let whereConditions = ['status = $1'];
      let params: any[] = [status];
      let paramIdx = 2;

      if (category) {
        whereConditions.push(`category = $${paramIdx++}`);
        params.push(category);
      }
      if (level) {
        whereConditions.push(`level = $${paramIdx++}`);
        params.push(level);
      }
      if (search) {
        whereConditions.push(`(title ILIKE $${paramIdx} OR description ILIKE $${paramIdx})`);
        params.push(`%${search}%`);
        paramIdx++;
      }

      const where = whereConditions.join(' AND ');

      const [countResult, dataResult] = await Promise.all([
        this.pool.query(`SELECT COUNT(*) FROM courses WHERE ${where}`, params),
        this.pool.query(
          `SELECT * FROM courses WHERE ${where} ORDER BY created_at DESC LIMIT $${paramIdx} OFFSET $${paramIdx + 1}`,
          [...params, limit, offset]
        )
      ]);

      const total = parseInt(countResult.rows[0].count);

      return {
        data: dataResult.rows.map(row => this._toDomain(row)),
        total,
        page,
        limit,
        totalPages: Math.ceil(total / limit)
      };
    }

    async existsByTitle(title: string, excludeId?: string): Promise<boolean> {
      const query = excludeId
        ? 'SELECT 1 FROM courses WHERE title = $1 AND id != $2'
        : 'SELECT 1 FROM courses WHERE title = $1';
      const params = excludeId ? [title, excludeId] : [title];
      const result = await this.pool.query(query, params);
      return result.rows.length > 0;
    }

    async countByInstructor(instructorId: string): Promise<number> {
      const result = await this.pool.query(
        'SELECT COUNT(*) FROM courses WHERE instructor_id = $1',
        [instructorId]
      );
      return parseInt(result.rows[0].count);
    }

    async findByInstructor(instructorId: string): Promise<Course[]> {
      const result = await this.pool.query(
        'SELECT * FROM courses WHERE instructor_id = $1 ORDER BY created_at DESC',
        [instructorId]
      );
      return result.rows.map(row => this._toDomain(row));
    }

    async delete(id: string): Promise<void> {
      // Soft delete : changer le statut plutôt que supprimer
      await this.pool.query(
        'UPDATE courses SET status = $1, updated_at = $2 WHERE id = $3',
        ['archived', new Date(), id]
      );
    }

    // ── Méthode de mapping : Row DB -> Entité Domaine ──────────
    private _toDomain(row: any): Course {
      return Course.reconstitute({
        id: row.id,
        title: row.title,
        description: row.description,
        instructorId: row.instructor_id,
        // Reconstruire le Value Object Money depuis les centimes
        price: new Money(row.price_amount / 100, row.price_currency),
        level: row.level as CourseLevel,
        status: row.status as CourseStatus,
        prerequisites: row.prerequisites || [],
        durationMinutes: row.duration_minutes,
        publishedAt: row.published_at,
        createdAt: row.created_at,
        updatedAt: row.updated_at
      });
    }
  }

------------------------------------------------------------------------
4.5 COUCHE INFRASTRUCTURE — STRIPE ADAPTER
------------------------------------------------------------------------

  // ============================================================
  // infrastructure/external/stripe/StripePaymentAdapter.ts
  // Adaptateur — Encapsule Stripe derrière une interface domaine
  // ============================================================

  import Stripe from 'stripe';
  import { IPaymentService, PaymentInput, PaymentResult } from '../../../domain/services/IPaymentService';

  export class StripePaymentAdapter implements IPaymentService {
    private stripe: Stripe;

    constructor(apiKey: string) {
      this.stripe = new Stripe(apiKey, { apiVersion: '2023-10-16' });
    }

    async charge(input: PaymentInput): Promise<PaymentResult> {
      try {
        const paymentIntent = await this.stripe.paymentIntents.create({
          amount: input.amount.amountInCents, // Stripe travaille en centimes
          currency: input.amount.currency.toLowerCase(),
          payment_method: input.token,
          confirm: true,
          description: input.description,
          metadata: {
            customerId: input.customerId
          }
        });

        if (paymentIntent.status !== 'succeeded') {
          throw new Error(`Paiement échoué avec statut: ${paymentIntent.status}`);
        }

        return {
          paymentId: paymentIntent.id,
          status: 'succeeded',
          amount: input.amount,
          createdAt: new Date(paymentIntent.created * 1000)
        };

      } catch (error) {
        if (error instanceof Stripe.errors.StripeCardError) {
          // Erreur de carte (refusée, expirée, etc.)
          throw new Error(`Paiement refusé : ${error.message}`);
        }
        throw error;
      }
    }
  }

------------------------------------------------------------------------
4.6 INJECTION DE DÉPENDANCES — ASSEMBLAGE DES COUCHES
------------------------------------------------------------------------

  // ============================================================
  // infrastructure/config/container.ts
  // Conteneur IoC — Assemble toutes les dépendances
  // ============================================================

  import { Pool } from 'pg';
  import { PostgresCourseRepository } from '../persistence/postgres/PostgresCourseRepository';
  import { PostgresEnrollmentRepository } from '../persistence/postgres/PostgresEnrollmentRepository';
  import { PostgresUserRepository } from '../persistence/postgres/PostgresUserRepository';
  import { StripePaymentAdapter } from '../external/stripe/StripePaymentAdapter';
  import { SendGridEmailAdapter } from '../external/sendgrid/SendGridEmailAdapter';
  import { EventBus } from '../events/EventBus';
  import { EnrollStudentUseCase } from '../../application/use-cases/EnrollStudentUseCase';
  import { CourseApplicationService } from '../../application/services/CourseApplicationService';
  import { EnrollmentController } from '../../presentation/http/controllers/EnrollmentController';
  import { CourseController } from '../../presentation/http/controllers/CourseController';

  // Créer la connexion à la base de données
  const pool = new Pool({
    connectionString: process.env.DATABASE_URL
  });

  // ── Infrastructure ────────────────────────────────────────────
  const courseRepository = new PostgresCourseRepository(pool);
  const enrollmentRepository = new PostgresEnrollmentRepository(pool);
  const userRepository = new PostgresUserRepository(pool);
  const paymentService = new StripePaymentAdapter(process.env.STRIPE_SECRET_KEY!);
  const emailService = new SendGridEmailAdapter(process.env.SENDGRID_API_KEY!);
  const eventBus = new EventBus(emailService);

  // ── Use Cases (Application) ───────────────────────────────────
  const enrollStudentUseCase = new EnrollStudentUseCase(
    courseRepository,
    enrollmentRepository,
    userRepository,
    paymentService,
    eventBus
  );

  // ── Services Applicatifs ──────────────────────────────────────
  const courseApplicationService = new CourseApplicationService(
    courseRepository,
    userRepository,
    eventBus
  );

  // ── Controllers (Présentation) ────────────────────────────────
  export const enrollmentController = new EnrollmentController(enrollStudentUseCase);
  export const courseController = new CourseController(courseApplicationService);

================================================================================
CHAPITRE 5 : BONNES PRATIQUES
================================================================================

------------------------------------------------------------------------
5.1 RÈGLE DES DÉPENDANCES
------------------------------------------------------------------------

TOUJOURS vérifier que vos imports respectent les règles :

  Présentation -> Application [OK]
  Présentation -> Domaine     [OK] (uniquement les entités pour les types)
  Présentation -> Infrastructure [X] INTERDIT
  Application -> Domaine      [OK]
  Application -> Infrastructure [X] INTERDIT (via injection de dépendances)
  Domaine -> Infrastructure   [X] INTERDIT ABSOLU
  Infrastructure -> Domaine   [OK] (implémente les interfaces)
  Infrastructure -> Application [X] INTERDIT

OUTIL DE VÉRIFICATION :
  // tsconfig.json + eslint-plugin-import
  // Ou ArchUnit (Java) pour vérifier automatiquement

------------------------------------------------------------------------
5.2 ANÉMIQUE VS RICHE
------------------------------------------------------------------------

MODÈLE ANÉMIQUE (à éviter) :
  Les entités n'ont que des getters/setters, aucune logique.
  Toute la logique dans les services.

  class Course {
    id: string;
    title: string;
    status: string;
    // Que des setters, aucune méthode métier
  }

  // Et dans le service :
  course.status = 'published';  // Pas de validation !

MODÈLE RICHE (recommandé) :
  Les entités contiennent la logique qui leur appartient naturellement.

  class Course {
    publish() {    // Méthode métier avec invariants
      this._validate();
      this._props.status = 'published';
    }
  }

------------------------------------------------------------------------
5.3 TRANSACTIONS
------------------------------------------------------------------------

Les transactions traversent les couches. Comment les gérer ?

APPROCHE : Unit of Work Pattern

  // infrastructure/persistence/UnitOfWork.ts
  class UnitOfWork {
    private client: PoolClient;

    async begin() {
      this.client = await pool.connect();
      await this.client.query('BEGIN');
    }

    async commit() {
      await this.client.query('COMMIT');
      this.client.release();
    }

    async rollback() {
      await this.client.query('ROLLBACK');
      this.client.release();
    }

    // Repositories liés à cette transaction
    get courseRepo() { return new PostgresCourseRepository(this.client); }
    get enrollmentRepo() { return new PostgresEnrollmentRepository(this.client); }
  }

  // Dans le Use Case :
  const uow = new UnitOfWork();
  await uow.begin();
  try {
    await uow.enrollmentRepo.save(enrollment);
    await uow.courseRepo.incrementEnrollmentCount(courseId);
    await uow.commit();
  } catch (e) {
    await uow.rollback();
    throw e;
  }

================================================================================
CHAPITRE 6 : ERREURS FRÉQUENTES
================================================================================

ERREUR 1 : LOGIQUE MÉTIER DANS LA COUCHE INFRASTRUCTURE
  Symptôme :
    PostgresCourseRepository.save() qui vérifie si le cours peut être publié
  Correction :
    La validation métier reste dans le Domaine.
    Le Repository sauvegarde ce qu'il reçoit (déjà validé par l'entité).

ERREUR 2 : COUCHE APPLICATION QUI CONNAIT STRIPE
  Symptôme :
    import Stripe from 'stripe' dans EnrollStudentUseCase
  Correction :
    Le Use Case dépend de IPaymentService (interface domaine).
    StripePaymentAdapter est injecté depuis le container.

ERREUR 3 : COUCHE PRÉSENTATION QUI ACCÈDE AU REPOSITORY
  Symptôme :
    CourseController qui importe PostgresCourseRepository
  Correction :
    CourseController -> CourseApplicationService -> ICourseRepository
    Jamais sauter des couches.

ERREUR 4 : DTO UTILISÉS DANS LE DOMAINE
  Symptôme :
    L'entité Course.fromDTO(createCourseDto) dans le domaine
  Correction :
    La couche Application convertit DTO -> Entité Domaine.
    Le Domaine ne connaît pas les DTOs de la couche Application.

ERREUR 5 : TROP DE COUCHES (Over-layering)
  Symptôme :
    ControllerLayer -> FacadeLayer -> ServiceLayer -> DomainServiceLayer ->
    RepositoryLayer -> DAOLayer -> DBAdapterLayer -> DB
  Correction :
    4 couches standard suffisent dans 95% des cas.
    N'ajoutez une couche que si elle a une responsabilité clairement distincte.

================================================================================
CHAPITRE 7 : EXERCICES
================================================================================

EXERCICES FACILES

EXERCICE 1 : Identifier les violations
  Regardez ce code. Identifiez toutes les violations de l'architecture en couches :
  
  // Dans EnrollmentController.ts (couche Présentation)
  import { Pool } from 'pg';
  import Stripe from 'stripe';
  
  const pool = new Pool({...});
  const stripe = new Stripe('sk_live...');
  
  async create(req, res) {
    const course = await pool.query('SELECT * FROM courses WHERE id = $1', [req.body.courseId]);
    if (!course.rows[0]) throw new Error('Not found');
    await stripe.paymentIntents.create({...});
    await pool.query('INSERT INTO enrollments...', [...]);
    res.json({ success: true });
  }

EXERCICE 2 : Value Object Email
  Créez le Value Object Email avec :
  - Validation du format (regex)
  - Normalisation (lowercase, trim)
  - Méthodes : getDomain(), isWorkEmail(), equals()
  - Immuabilité garantie

EXERCICE 3 : Interface INotificationService
  Définissez l'interface INotificationService dans la couche Domaine.
  Elle doit supporter l'envoi d'email, SMS, et notification push.
  Assurez-vous que l'interface ne mentionne pas de technologie spécifique
  (pas de "email", "sendgrid", "twilio" dans les noms).

EXERCICES INTERMÉDIAIRES

EXERCICE 4 : Use Case CompleteLessonUseCase
  Implémentez ce Use Case :
  Input : { studentId, courseId, lessonId }
  Logique :
    - Vérifier l'enrollment actif
    - Marquer la leçon comme complétée
    - Recalculer la progression du cours
    - Si 100% -> marquer le cours comme complété
    - Si completé -> déclencher l'événement CourseCompleted
  Output : { enrollmentId, newProgress, isCompleted }

EXERCICE 5 : Adaptateur pour les deux providers email
  Créez SendGridEmailAdapter ET SESEmailAdapter (AWS SES).
  Les deux doivent implémenter IEmailService.
  Dans le container.ts, sélectionner l'adaptateur selon EMAIL_PROVIDER env var.

EXERCICE 6 : Cache dans la couche Infrastructure
  Implémentez CachedCourseRepository qui :
  - Wraps PostgresCourseRepository avec Redis
  - findById() : cherche d'abord dans Redis, puis Postgres si absent
  - save() : invalide le cache Redis après sauvegarde
  - TTL : 5 minutes pour les cours publiés
  Ce pattern s'appelle "Decorator Pattern".

EXERCICES AVANCÉS

EXERCICE 7 : Architecture complète avec tests par couche
  Pour le module "Reviews" (avis sur les cours) :
  a) Domain : Review entity, IReviewRepository
  b) Application : AddReviewUseCase, GetCourseReviewsUseCase
  c) Infrastructure : PostgresReviewRepository
  d) Presentation : ReviewController + Routes
  e) Tests unitaires pour chaque couche (avec mocks)

EXERCICE 8 : Gestion des transactions cross-aggregats
  La commande "RefundEnrollment" doit :
  - Rembourser le paiement (Stripe)
  - Passer le statut enrollment à 'refunded'
  - Révoquer l'accès au cours
  - Enregistrer la transaction de remboursement
  Tout doit être transactionnel.
  Implémentez avec le Unit of Work Pattern.

EXERCICE 9 : Pagination par curseur dans la couche Infrastructure
  Implémentez la pagination par curseur dans PostgresCourseRepository.findAll().
  Le curseur = encodage base64 du dernier { createdAt, id }.
  Exposer via l'API : GET /courses?cursor=<encoded>&limit=20
  Avantage vs offset : performant même avec 1M d'enregistrements.

================================================================================
RÉCAPITULATIF — ARCHITECTURE EN COUCHES
================================================================================

Points clés :

1. L'architecture en couches divise en : Présentation, Application, Domaine, Infrastructure.

2. LES DÉPENDANCES ne vont que vers le bas (ou vers le centre pour le Domaine).

3. LA COUCHE DOMAINE est isolée de toutes les technologies.
   Elle définit des INTERFACES que l'Infrastructure implémente.

4. L'INJECTION DE DÉPENDANCES connecte toutes les couches au démarrage.

5. LES VALUE OBJECTS (Money, Email...) encapsulent les règles des données fondamentales.

6. UN MODÈLE RICHE contient sa propre logique (vs modèle anémique).

7. EDUCONNECT LAYERED : Course/Enrollment avec Value Objects,
   Use Cases précis, Repositories injectés, Adapters pour Stripe et SendGrid.

Prochaine étape :
  Volume 5 : Architecture Microservices
  -> Découper EduConnect en services indépendants
  -> Gérer la communication inter-services, la consistance distribuée

================================================================================
FIN DU VOLUME 4 — ARCHITECTURE EN COUCHES
Prochaine étape -> architecture_microservices.txt
================================================================================

================================================================================
     GUIDE COMPLET DES ARCHITECTURES LOGICIELLES - VOLUME 5
     Architecture Microservices
     Pour étudiants en Génie Logiciel
================================================================================

================================================================================
CHAPITRE 1 : INTRODUCTION AUX MICROSERVICES
================================================================================

------------------------------------------------------------------------
1.1 DÉFINITION
------------------------------------------------------------------------

L'architecture microservices organise une application comme un ensemble de
petits services indépendants, chacun :
  - Responsable d'une seule capacité métier
  - Déployable indépendamment des autres
  - Propriétaire de ses propres données
  - Communicant avec les autres via API réseau

Analogie : Une ville moderne
  Chaque quartier (service) a sa propre fonction :
  - Quartier commercial (service produits)
  - Quartier bancaire (service paiements)
  - Quartier résidentiel (service utilisateurs)
  Ils communiquent par des routes (réseau), mais fonctionnent indépendamment.
  Si un quartier brûle, les autres continuent.

Définition technique de Martin Fowler :
  "Une approche de développement d'application unique comme une suite
   de petits services, chacun tournant dans son propre processus et
   communiquant avec des mécanismes légers, souvent une API HTTP."

------------------------------------------------------------------------
1.2 POURQUOI LES MICROSERVICES ?
------------------------------------------------------------------------

LE PROBLÈME DU MONOLITHE À GRANDE ÉCHELLE :

  AVANT (Netflix en 2008) :
    - Un seul monolithe Java
    - 1 heure pour déployer une modification
    - Si un module bugge -> tout le site tombe
    - Impossible de scaler uniquement le streaming vidéo
    - 800 développeurs travaillent sur le même codebase = chaos

  APRÈS (Netflix en 2012) :
    - ~1000 microservices
    - Déploiements en quelques minutes
    - Un service down -> les autres continuent
    - Le service vidéo scale indépendamment
    - Équipes autonomes, chacune maître de son service

LES TROIS FORCES QUI POUSSENT VERS LES MICROSERVICES :

  Force 1 : SCALABILITÉ SÉLECTIVE
    Problème : Votre module de streaming vidéo consomme 80% des ressources.
    Monolithe : Devoir scaler TOUTE l'application.
    Microservices : Scaler UNIQUEMENT le service vidéo.

  Force 2 : DÉPLOIEMENTS INDÉPENDANTS
    Problème : 20 équipes qui veulent déployer en parallèle.
    Monolithe : File d'attente pour les déploiements, risque de conflits.
    Microservices : Chaque équipe déploie son service quand elle veut.

  Force 3 : ISOLATION DES PANNES
    Problème : Un bug dans le module paiements.
    Monolithe : Tout le site est impacté.
    Microservices : Seul le service paiements est affecté (avec Circuit Breaker).

------------------------------------------------------------------------
1.3 MONOLITHE FIRST, MICROSERVICES SECOND
------------------------------------------------------------------------

RÈGLE IMPORTANTE :
  Ne commencez PAS par les microservices. Commencez par un monolithe modulaire.
  
  Raison 1 : Vous ne connaissez pas encore vos "bounded contexts"
    Pour découper en microservices, vous devez comprendre les frontières
    naturelles de votre domaine. Un monolithe les révèle organiquement.
  
  Raison 2 : Overhead opérationnel immense
    Microservices nécessitent : Docker, Kubernetes, Service Discovery,
    API Gateway, Distributed Tracing, Circuit Breakers, etc.
    Une startup de 3 personnes ne peut pas gérer ça.
  
  Règle de Sam Newman :
    "Don't start with microservices. If you already have a monolith,
     extract services when you need to scale specific parts."

================================================================================
CHAPITRE 2 : THÉORIE DÉTAILLÉE
================================================================================

------------------------------------------------------------------------
2.1 CARACTÉRISTIQUES DES MICROSERVICES
------------------------------------------------------------------------

CARACTÉRISTIQUE 1 : SINGLE RESPONSIBILITY BUSINESS CAPABILITY
  Un microservice = une capacité métier unique
  
  Bons exemples de services pour EduConnect :
    - Auth Service : Authentification, autorisation, sessions
    - User Service : Profils utilisateurs, préférences
    - Course Service : Catalogue, contenu des cours
    - Enrollment Service : Inscriptions, progression
    - Payment Service : Transactions, remboursements
    - Notification Service : Emails, SMS, push notifications
    - Video Service : Upload, transcoding, streaming
    - Search Service : Recherche full-text
    - Certificate Service : Génération des certificats

  Mauvais exemples :
    - "Core Service" qui fait tout (monolithe déguisé)
    - "Utility Service" sans limite clairement définie

CARACTÉRISTIQUE 2 : DECENTRALIZED DATA MANAGEMENT
  Chaque service possède sa propre base de données.
  Aucun service n'accède directement à la DB d'un autre service.
  Communication uniquement via API.

  ┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
  │  User Service   │    │ Course Service  │    │Enrollment Svc   │
  │                 │    │                 │    │                 │
  │  PostgreSQL     │    │  PostgreSQL     │    │  PostgreSQL     │
  │  (users, auth)  │    │  (courses,      │    │  (enrollments,  │
  │                 │    │   modules,      │    │   progress)     │
  │                 │    │   lessons)      │    │                 │
  └─────────────────┘    └─────────────────┘    └─────────────────┘

  Avantage : Indépendance totale (polyglot persistence possible)
  Défi : Cohérence des données distribuées

CARACTÉRISTIQUE 3 : COMMUNICATION LÉGÈRE
  Les services communiquent via des protocoles légers :
    - REST/HTTP pour les appels synchrones
    - Message Broker (Kafka, RabbitMQ) pour l'asynchrone
    - gRPC pour la haute performance

CARACTÉRISTIQUE 4 : DESIGN FOR FAILURE
  Chaque service assume que les autres services peuvent être indisponibles.
  Patterns : Circuit Breaker, Retry, Timeout, Fallback

CARACTÉRISTIQUE 5 : DÉPLOIEMENT INDÉPENDANT
  Chaque service a son propre cycle de déploiement.
  Les changements d'un service ne nécessitent pas de redéployer les autres.
  Condition : Compatibilité ascendante des API (versioning).

------------------------------------------------------------------------
2.2 COMMUNICATION INTER-SERVICES
------------------------------------------------------------------------

TYPE 1 : SYNCHRONE (Request/Response)

  REST / HTTP :
    Course Service appelle User Service pour vérifier un utilisateur.
    
    GET http://user-service/api/users/123
    -> Response: { id: "123", name: "Aminata", role: "student" }
    
    Avantages : Simple, universel, résultat immédiat
    Inconvénients : Couplage temporel, cascade de pannes, latence

  gRPC :
    Protocole binaire, plus rapide que HTTP/JSON.
    Utilisé pour les services internes à haute performance.
    Avantages : Performance, typage fort (Protocol Buffers)
    Inconvénients : Complexité, moins lisible

TYPE 2 : ASYNCHRONE (Message-based)

  Message Queue (RabbitMQ, AWS SQS) :
    Producteur envoie un message dans une queue.
    Consommateur lit et traite les messages à son rythme.
    
    Enrollment Service -> "enrollment.created" -> Queue
                                                    v
                                          Notification Service
                                          (envoie l'email)
    
    Avantages : Découplage, résilience, absorption des pics
    Inconvénients : Cohérence éventuelle, complexité debugging

  Event Streaming (Apache Kafka) :
    Les événements sont un log immuable et ordonné.
    Plusieurs consommateurs peuvent lire le même événement.
    
    Avantages : Historique des événements, replay possible, many consumers
    Inconvénients : Complexité opérationnelle, overkill pour faible volume

QUAND UTILISER QUOI :
  Synchrone -> Requête qui nécessite une réponse immédiate
              Ex: "Vérifier si cet utilisateur existe avant de l'inscrire"
  
  Asynchrone -> Actions dont le résultat n'est pas immédiatement nécessaire
               Ex: "Envoyer l'email de confirmation après inscription"

------------------------------------------------------------------------
2.3 PATTERNS DE DÉCOMPOSITION
------------------------------------------------------------------------

COMMENT DÉCOUPER UN MONOLITHE EN MICROSERVICES ?

PATTERN 1 : DECOMPOSE BY BUSINESS CAPABILITY
  Identifier les capacités métier et créer un service par capacité.
  
  EduConnect Capabilities :
    - Gestion des identités -> Auth Service + User Service
    - Catalogue de cours -> Course Service
    - Apprentissage -> Enrollment Service + Progress Service
    - Monétisation -> Payment Service
    - Communication -> Notification Service
    - Contenu média -> Video Service
    - Découverte -> Search Service

PATTERN 2 : DECOMPOSE BY SUBDOMAIN (DDD Bounded Contexts)
  Identifier les sous-domaines métier (Domain-Driven Design).
  Chaque bounded context devient un service (ou groupe de services).
  
  EduConnect Bounded Contexts :
    - Identity : Qui est l'utilisateur ?
    - Catalog : Quels cours existent ?
    - Learning : L'étudiant progresse-t-il ?
    - Commerce : Comment est géré l'argent ?
    - Delivery : Comment le contenu est-il livré ?

ANTI-PATTERN : DÉCOMPOSER PAR COUCHE TECHNIQUE
  [X] MAUVAIS :
    - Frontend Service
    - Backend Service
    - Database Service
  
  Ce n'est pas des microservices, c'est juste un monolithe avec des
  appels réseau (distributed monolith).

------------------------------------------------------------------------
2.4 PATTERNS DE MICROSERVICES ESSENTIELS
------------------------------------------------------------------------

PATTERN 1 : API GATEWAY
  Problème : Les clients ne doivent pas connaître tous les services.
  Solution : Un point d'entrée unique qui route vers les services appropriés.
  
  Client -> API Gateway -> Routing -> Service A
                                 -> Service B
                                 -> Service C
  
  Responsabilités du Gateway :
    - Authentification et autorisation (une seule fois)
    - Rate limiting
    - SSL termination
    - Routing
    - Load balancing
    - Logging et monitoring
    - Response transformation
  
  Implémentations : Kong, AWS API Gateway, Nginx, Traefik, Express Gateway

PATTERN 2 : CIRCUIT BREAKER
  Problème : Si Payment Service est down, Enrollment Service doit-il se bloquer ?
  Solution : Après N échecs, "ouvrir le circuit" = arrêter d'appeler le service défaillant.
  
  États du Circuit Breaker :
    CLOSED (normal) -> Requêtes passent
    OPEN (défaillance) -> Requêtes bloquées (fallback immédiat)
    HALF-OPEN (test) -> Quelques requêtes pour tester si le service est revenu
  
  Seuil typique : 5 échecs en 30 secondes -> Circuit ouvert 60 secondes
  
  Implémentations : Hystrix (Java), Polly (.NET), Resilience4j, opossum (Node.js)

PATTERN 3 : SAGA
  Problème : Comment gérer une transaction qui implique plusieurs services ?
  Exemple : Inscription à un cours = Vérifier User + Vérifier Course +
            Charger Paiement + Créer Enrollment
  
  Solution A : SAGA CHORÉGRAPHIE
    Chaque service écoute des événements et publie ses propres événements.
    Pas de coordinateur central.
    
    Enrollment Cmd -> EnrollmentService publie "enrollment.initiated"
                            v
                    PaymentService écoute, effectue le paiement
                    PaymentService publie "payment.completed"
                            v
                    EnrollmentService écoute, confirme l'inscription
                    EnrollmentService publie "enrollment.confirmed"
                            v
                    NotificationService écoute, envoie l'email
    
    Avantages : Simple, découplé
    Inconvénients : Logique distribuée difficile à suivre
  
  Solution B : SAGA ORCHESTRATION
    Un coordinateur (Saga Orchestrator) dirige les étapes.
    
    EnrollmentOrchestrator:
      1. Appelle PaymentService.charge()
      2. Si succès -> Appelle EnrollmentService.create()
      3. Si succès -> Appelle NotificationService.send()
      4. Si échec en étape 2 -> Appelle PaymentService.refund() (compensation)
    
    Avantages : Logique centralisée, plus facile à déboguer
    Inconvénients : Couplage au coordinateur

PATTERN 4 : SERVICE DISCOVERY
  Problème : Comment Service A connaît-il l'adresse de Service B ?
  Les IPs changent en environnement cloud/containers.
  
  Solution : Registre de services
    - Chaque service s'enregistre avec son adresse
    - Les clients interrogent le registre pour trouver l'adresse
    
  ┌─────────────┐    "Je suis à 10.0.1.5:3001"    ┌──────────────────┐
  │User Service │ ─────────────────────────────->  │Service Registry  │
  └─────────────┘                                 │ (Consul/Eureka)  │
                                                  └───────┬──────────┘
  ┌─────────────────┐  "Où est User Service?"            │
  │Enrollment Svc   │ ──────────────────────────────────->│
  │                 │  "10.0.1.5:3001"                   │
  │                 │ <-──────────────────────────────────│
  └─────────────────┘

PATTERN 5 : SIDECAR / SERVICE MESH
  Problème : Chaque service doit implémenter : retry, circuit breaker,
             TLS, tracing... Code dupliqué dans tous les services.
  Solution : Déplacer ce code dans un "sidecar" proxy colocalisé.
  
  [Service Pod]
    ┌────────────────────────────────┐
    │  [User Service Container]      │
    │  [Envoy Sidecar Container]    │  <- Gère TLS, retry, tracing
    └────────────────────────────────┘
  
  Implémentation : Istio, Linkerd (Service Mesh)

------------------------------------------------------------------------
2.5 AVANTAGES ET INCONVÉNIENTS
------------------------------------------------------------------------

[OK] AVANTAGES :
  Scalabilité sélective : Scaler uniquement les services qui en ont besoin
  Déploiements indépendants : Chaque équipe déploie quand elle veut
  Isolation des pannes : Un service down n'impacte pas les autres
  Polyglot : Chaque service peut utiliser la technologie la plus adaptée
  Équipes autonomes : Conway's Law - structure de code reflète l'organisation

[X] INCONVÉNIENTS :
  Complexité opérationnelle : Kubernetes, monitoring distribué, CI/CD pour N services
  Latence réseau : Appels HTTP vs appels en mémoire (10-100x plus lents)
  Cohérence des données : Plus de transactions ACID cross-services
  Distributed tracing : Debugger une erreur impliquant 5 services = difficile
  Surcharge de communication : Gestion des erreurs réseau partout
  Overhead développement : Chaque service a son propre repo, pipeline, monitoring

================================================================================
CHAPITRE 3 : SCHÉMAS — EDUCONNECT MICROSERVICES
================================================================================

------------------------------------------------------------------------
3.1 VUE MACRO DE L'ARCHITECTURE
------------------------------------------------------------------------

                              INTERNET
                                 │
                    ┌────────────[BLACK_DOWN-POINTING_TRIANGLE]────────────┐
                    │       API GATEWAY       │
                    │  (Kong / Nginx / AWS)   │
                    │                         │
                    │ Auth + Rate Limit +     │
                    │ Routing + SSL           │
                    └────────────┬────────────┘
                                 │
              ┌──────────────────┼──────────────────┐
              │                  │                   │
              [BLACK_DOWN-POINTING_TRIANGLE]                  [BLACK_DOWN-POINTING_TRIANGLE]                   [BLACK_DOWN-POINTING_TRIANGLE]
  ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
  │   Auth Service  │ │  User Service   │ │ Course Service  │
  │   :3001         │ │   :3002         │ │   :3003         │
  │                 │ │                 │ │                 │
  │  PostgreSQL     │ │  PostgreSQL     │ │  PostgreSQL     │
  │  (tokens, 2FA)  │ │  (users,        │ │  (courses,      │
  │                 │ │   profiles)     │ │   modules)      │
  └─────────────────┘ └─────────────────┘ └─────────────────┘

              ┌──────────────────┼──────────────────┐
              │                  │                   │
              [BLACK_DOWN-POINTING_TRIANGLE]                  [BLACK_DOWN-POINTING_TRIANGLE]                   [BLACK_DOWN-POINTING_TRIANGLE]
  ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
  │Enrollment Svc   │ │ Payment Service │ │Notif Service    │
  │   :3004         │ │   :3005         │ │   :3006         │
  │                 │ │                 │ │                 │
  │  PostgreSQL     │ │  PostgreSQL     │ │  Redis          │
  │  (enrollments,  │ │  (transactions, │ │  (templates,    │
  │   progress)     │ │   billing)      │ │   queue)        │
  └─────────────────┘ └─────────────────┘ └─────────────────┘

              ┌──────────────────┼──────────────────┐
              │                  │                   │
              [BLACK_DOWN-POINTING_TRIANGLE]                  [BLACK_DOWN-POINTING_TRIANGLE]                   [BLACK_DOWN-POINTING_TRIANGLE]
  ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
  │  Video Service  │ │  Search Service │ │Certificate Svc  │
  │   :3007         │ │   :3008         │ │   :3009         │
  │                 │ │                 │ │                 │
  │  S3 + Redis     │ │ Elasticsearch   │ │  PostgreSQL +   │
  │  (videos,       │ │  (full-text     │ │  S3 (certs PDF) │
  │   transcoding)  │ │   search)       │ │                 │
  └─────────────────┘ └─────────────────┘ └─────────────────┘

  [MESSAGE BROKER : RabbitMQ / Kafka]
  <- Connecte tous les services pour la communication asynchrone

------------------------------------------------------------------------
3.2 FLUX D'INSCRIPTION - COMMUNICATION INTER-SERVICES
------------------------------------------------------------------------

  CLIENT
    │
    │ POST /api/enrollments { courseId, paymentToken }
    │ Bearer Token
    [BLACK_DOWN-POINTING_TRIANGLE]
  API GATEWAY
    │ Vérifie JWT avec Auth Service
    │ Route vers Enrollment Service
    [BLACK_DOWN-POINTING_TRIANGLE]
  ENROLLMENT SERVICE
    │
    ├──[Sync REST]──-> USER SERVICE
    │                 GET /internal/users/:id
    │                 <- { id, role, email }
    │
    ├──[Sync REST]──-> COURSE SERVICE
    │                 GET /internal/courses/:id
    │                 <- { id, title, price, prerequisites }
    │
    ├──[Sync REST]──-> ENROLLMENT SERVICE (self)
    │                 Vérification non-doublon
    │
    ├──[Sync REST]──-> PAYMENT SERVICE
    │                 POST /internal/charges { token, amount }
    │                 <- { paymentId, status }
    │
    │ [TRANSACTION: Créer enrollment en DB]
    │
    ├──[Async Event]──-> MESSAGE BROKER
    │                   Publie "enrollment.created" event
    │                   {enrollmentId, userId, courseId, paymentId}
    │
    [BLACK_DOWN-POINTING_TRIANGLE]
  RÉPONSE 201 Created { enrollmentId, courseId, enrolledAt }

  En parallèle (listeners de l'event "enrollment.created") :
    NOTIFICATION SERVICE -> Envoie email de confirmation
    SEARCH SERVICE       -> Met à jour le compteur d'inscrits
    VIDEO SERVICE        -> Pré-charge le contenu pour l'étudiant

------------------------------------------------------------------------
3.3 STRUCTURE DES SERVICES EDUCONNECT
------------------------------------------------------------------------

  educonnect-microservices/
  ├── services/
  │   ├── auth-service/
  │   │   ├── src/
  │   │   ├── Dockerfile
  │   │   ├── package.json
  │   │   └── .env.example
  │   │
  │   ├── user-service/
  │   │   ├── src/
  │   │   ├── Dockerfile
  │   │   └── package.json
  │   │
  │   ├── course-service/
  │   ├── enrollment-service/
  │   ├── payment-service/
  │   ├── notification-service/
  │   └── video-service/
  │
  ├── api-gateway/
  │   ├── kong.yml            <- Configuration Kong
  │   └── nginx.conf          <- Ou Nginx
  │
  ├── infrastructure/
  │   ├── docker-compose.yml  <- Environnement de développement
  │   ├── kubernetes/         <- Déploiement production
  │   │   ├── namespace.yaml
  │   │   ├── auth-service.yaml
  │   │   ├── course-service.yaml
  │   │   └── ...
  │   ├── rabbitmq/
  │   └── monitoring/
  │       ├── prometheus.yml
  │       └── grafana/
  │
  └── shared/                 <- Code partagé (types, events)
      ├── events/
      │   ├── EnrollmentCreatedEvent.ts
      │   └── CoursePublishedEvent.ts
      └── proto/              <- Protocol Buffers pour gRPC (optionnel)

================================================================================
CHAPITRE 4 : IMPLÉMENTATION COMPLÈTE
================================================================================

------------------------------------------------------------------------
4.1 AUTH SERVICE
------------------------------------------------------------------------

  // auth-service/src/app.js
  // Service d'authentification autonome

  const express = require('express');
  const jwt = require('jsonwebtoken');
  const bcrypt = require('bcrypt');
  const { Pool } = require('pg');

  const app = express();
  app.use(express.json());

  const pool = new Pool({ connectionString: process.env.DATABASE_URL });

  // ──────────────────────────────────────────────────────────────
  // ENDPOINT INTERNE : Vérifie un token JWT
  // Appelé par l'API Gateway pour chaque requête
  // ──────────────────────────────────────────────────────────────
  app.post('/internal/verify-token', async (req, res) => {
    try {
      const { token } = req.body;

      if (!token) {
        return res.status(400).json({ valid: false, error: 'Token manquant' });
      }

      // Vérifier la signature JWT
      const decoded = jwt.verify(token, process.env.JWT_SECRET);

      // Vérifier que la session n'est pas révoquée (blacklist Redis)
      const redis = require('./config/redis');
      const isRevoked = await redis.get(`revoked_token:${decoded.jti}`);
      if (isRevoked) {
        return res.status(401).json({ valid: false, error: 'Token révoqué' });
      }

      return res.json({
        valid: true,
        userId: decoded.userId,
        email: decoded.email,
        role: decoded.role
      });

    } catch (error) {
      if (error.name === 'TokenExpiredError') {
        return res.status(401).json({ valid: false, error: 'Token expiré' });
      }
      return res.status(401).json({ valid: false, error: 'Token invalide' });
    }
  });

  // ──────────────────────────────────────────────────────────────
  // ENDPOINT PUBLIC : Connexion
  // ──────────────────────────────────────────────────────────────
  app.post('/auth/login', async (req, res) => {
    try {
      const { email, password } = req.body;

      // Trouver l'utilisateur
      const result = await pool.query(
        'SELECT id, email, password_hash, role FROM users WHERE email = $1',
        [email.toLowerCase()]
      );

      if (result.rows.length === 0) {
        return res.status(401).json({ error: 'Email ou mot de passe incorrect' });
      }

      const user = result.rows[0];

      // Vérifier le mot de passe
      const valid = await bcrypt.compare(password, user.password_hash);
      if (!valid) {
        return res.status(401).json({ error: 'Email ou mot de passe incorrect' });
      }

      // Générer le JWT avec un JTI (JWT ID) unique pour permettre la révocation
      const jti = require('uuid').v4();
      const token = jwt.sign(
        { userId: user.id, email: user.email, role: user.role, jti },
        process.env.JWT_SECRET,
        { expiresIn: '7d' }
      );

      return res.json({
        token,
        user: { id: user.id, email: user.email, role: user.role }
      });

    } catch (error) {
      res.status(500).json({ error: 'Erreur interne' });
    }
  });

  app.listen(process.env.PORT || 3001, () => {
    console.log(`Auth Service démarré sur :${process.env.PORT || 3001}`);
  });

------------------------------------------------------------------------
4.2 API GATEWAY AVEC EXPRESS
------------------------------------------------------------------------

  // api-gateway/src/gateway.js
  // API Gateway — Point d'entrée unique

  const express = require('express');
  const httpProxy = require('http-proxy-middleware');
  const axios = require('axios');
  const rateLimit = require('express-rate-limit');

  const app = express();
  app.use(express.json());

  // Configuration des services
  const SERVICES = {
    auth:        process.env.AUTH_SERVICE_URL        || 'http://auth-service:3001',
    users:       process.env.USER_SERVICE_URL        || 'http://user-service:3002',
    courses:     process.env.COURSE_SERVICE_URL      || 'http://course-service:3003',
    enrollments: process.env.ENROLLMENT_SERVICE_URL  || 'http://enrollment-service:3004',
    payments:    process.env.PAYMENT_SERVICE_URL     || 'http://payment-service:3005',
    search:      process.env.SEARCH_SERVICE_URL      || 'http://search-service:3008'
  };

  // ── Rate Limiting Global ──────────────────────────────────────
  app.use(rateLimit({
    windowMs: 15 * 60 * 1000,
    max: 200,
    message: 'Trop de requêtes'
  }));

  // ── Middleware d'authentification ─────────────────────────────
  // Routes publiques qui n'ont PAS besoin d'auth
  const PUBLIC_ROUTES = [
    { method: 'POST', path: '/api/auth/login' },
    { method: 'POST', path: '/api/auth/register' },
    { method: 'GET',  path: /^\/api\/courses/ },     // Regex pour toutes les routes courses
    { method: 'GET',  path: /^\/api\/search/ }
  ];

  const isPublicRoute = (req) => {
    return PUBLIC_ROUTES.some(route => {
      const methodMatch = route.method === req.method;
      const pathMatch = route.path instanceof RegExp
        ? route.path.test(req.path)
        : req.path === route.path;
      return methodMatch && pathMatch;
    });
  };

  const authMiddleware = async (req, res, next) => {
    // Passer les routes publiques
    if (isPublicRoute(req)) return next();

    const authHeader = req.headers.authorization;
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return res.status(401).json({ error: 'Authentification requise' });
    }

    try {
      // Vérifier le token auprès du Auth Service
      const token = authHeader.substring(7);
      const response = await axios.post(
        `${SERVICES.auth}/internal/verify-token`,
        { token },
        { timeout: 3000 }  // Timeout de 3 secondes
      );

      if (!response.data.valid) {
        return res.status(401).json({ error: response.data.error });
      }

      // Injecter les infos utilisateur dans les headers pour les services
      req.headers['X-User-Id'] = response.data.userId;
      req.headers['X-User-Email'] = response.data.email;
      req.headers['X-User-Role'] = response.data.role;
      // Supprimer le token du header pour ne pas le propager
      delete req.headers.authorization;

      next();

    } catch (error) {
      if (error.code === 'ECONNREFUSED') {
        return res.status(503).json({ error: 'Service d\'authentification indisponible' });
      }
      return res.status(401).json({ error: 'Erreur d\'authentification' });
    }
  };

  app.use(authMiddleware);

  // ── Routing vers les services ─────────────────────────────────

  // Helper pour créer un proxy avec gestion d'erreur
  const createProxy = (target) => httpProxy.createProxyMiddleware({
    target,
    changeOrigin: true,
    pathRewrite: (path) => path.replace(/^\/api/, ''),  // Enlève /api du path
    on: {
      error: (err, req, res) => {
        console.error(`Proxy error to ${target}:`, err.message);
        res.status(503).json({
          error: 'Service temporairement indisponible',
          service: target
        });
      }
    }
  });

  app.use('/api/auth',        createProxy(SERVICES.auth));
  app.use('/api/users',       createProxy(SERVICES.users));
  app.use('/api/courses',     createProxy(SERVICES.courses));
  app.use('/api/enrollments', createProxy(SERVICES.enrollments));
  app.use('/api/payments',    createProxy(SERVICES.payments));
  app.use('/api/search',      createProxy(SERVICES.search));

  // ── Health Check du Gateway ───────────────────────────────────
  app.get('/health', async (req, res) => {
    const checks = {};

    // Vérifier chaque service en parallèle
    await Promise.all(
      Object.entries(SERVICES).map(async ([name, url]) => {
        try {
          await axios.get(`${url}/health`, { timeout: 2000 });
          checks[name] = 'healthy';
        } catch {
          checks[name] = 'unhealthy';
        }
      })
    );

    const allHealthy = Object.values(checks).every(s => s === 'healthy');

    res.status(allHealthy ? 200 : 206).json({
      gateway: 'healthy',
      services: checks,
      timestamp: new Date().toISOString()
    });
  });

  app.listen(process.env.PORT || 8080, () => {
    console.log(`API Gateway démarré sur :${process.env.PORT || 8080}`);
  });

------------------------------------------------------------------------
4.3 CIRCUIT BREAKER AVEC OPOSSUM
------------------------------------------------------------------------

  // shared/CircuitBreaker.js
  // Implémentation du Circuit Breaker Pattern

  const CircuitBreaker = require('opossum');

  // Créer un circuit breaker pour un appel de service externe
  function createServiceCircuitBreaker(serviceCall, options = {}) {
    const defaultOptions = {
      timeout: 3000,           // Timeout de l'appel (3s)
      errorThresholdPercentage: 50, // Ouvrir si 50% d'erreurs
      resetTimeout: 10000,     // Réessayer après 10s
      volumeThreshold: 5       // Min 5 requêtes avant d'évaluer
    };

    const breaker = new CircuitBreaker(serviceCall, {
      ...defaultOptions,
      ...options
    });

    // Logging des transitions d'état
    breaker.on('open', () =>
      console.warn(`[RAPIDE] Circuit OUVERT pour ${options.name || 'service'}`)
    );
    breaker.on('halfOpen', () =>
      console.info(`[RAPIDE] Circuit SEMI-OUVERT, test en cours...`)
    );
    breaker.on('close', () =>
      console.info(`[OK] Circuit FERMÉ, service rétabli`)
    );
    breaker.on('fallback', (result) =>
      console.warn(`[RAPIDE] Fallback utilisé:`, result)
    );

    return breaker;
  }

  module.exports = { createServiceCircuitBreaker };

  // ─────────────────────────────────────────────────────────────────
  // Utilisation dans Enrollment Service
  // ─────────────────────────────────────────────────────────────────

  const axios = require('axios');
  const { createServiceCircuitBreaker } = require('../../shared/CircuitBreaker');

  // Appel au User Service
  const getUserCall = async (userId) => {
    const response = await axios.get(
      `${process.env.USER_SERVICE_URL}/internal/users/${userId}`,
      { timeout: 3000 }
    );
    return response.data;
  };

  // Circuit breaker avec fallback
  const getUserBreaker = createServiceCircuitBreaker(getUserCall, {
    name: 'UserService'
  });

  // Fallback : retourner des données minimales si User Service est down
  getUserBreaker.fallback((userId) => ({
    id: userId,
    email: 'unknown@educonnect.com',
    role: 'student',
    _fromFallback: true  // Marquer pour alerter
  }));

  // Utilisation
  class EnrollmentService {
    async enroll(userId, courseId, paymentToken) {
      // Ce circuit breaker retourne le fallback automatiquement si User Service est down
      const user = await getUserBreaker.fire(userId);

      // Si fallback utilisé, on peut décider de refuser l'inscription
      if (user._fromFallback) {
        throw new Error('Service utilisateur temporairement indisponible. Réessayez dans quelques instants.');
      }

      // ... suite de la logique
    }
  }

------------------------------------------------------------------------
4.4 MESSAGE BROKER — PUBLICATION ET CONSOMMATION
------------------------------------------------------------------------

  // ══════════════════════════════════════════════════════════════
  // shared/messaging/MessageBroker.js
  // Abstraction du broker de messages (RabbitMQ)
  // ══════════════════════════════════════════════════════════════

  const amqp = require('amqplib');

  class MessageBroker {
    constructor() {
      this.connection = null;
      this.channel = null;
      this.EXCHANGE = 'educonnect.events';  // Exchange principal
    }

    async connect() {
      this.connection = await amqp.connect(process.env.RABBITMQ_URL);
      this.channel = await this.connection.createChannel();

      // Déclarer l'exchange de type "topic"
      // "topic" permet le routing par pattern : "enrollment.#" ou "course.published"
      await this.channel.assertExchange(this.EXCHANGE, 'topic', { durable: true });

      console.log('[OK] Connecté à RabbitMQ');
    }

    // Publier un événement
    async publish(eventName, data) {
      if (!this.channel) throw new Error('Non connecté au broker');

      const message = JSON.stringify({
        eventName,
        data,
        timestamp: new Date().toISOString(),
        correlationId: require('uuid').v4()
      });

      // Publier avec le routing key = eventName
      this.channel.publish(
        this.EXCHANGE,
        eventName,
        Buffer.from(message),
        { persistent: true }  // Message survit au redémarrage du broker
      );

      console.log(`[SORTIE] Événement publié: ${eventName}`);
    }

    // S'abonner à un pattern d'événements
    async subscribe(pattern, queueName, handler) {
      const channel = await this.connection.createChannel();

      // Déclarer la queue avec durabilité
      await channel.assertQueue(queueName, { durable: true });

      // Lier la queue à l'exchange avec le pattern
      await channel.bindQueue(queueName, this.EXCHANGE, pattern);

      // Limiter à 1 message à la fois (prefetch)
      // Garantit le traitement séquentiel et la distribution équitable
      channel.prefetch(1);

      // Consommer les messages
      channel.consume(queueName, async (msg) => {
        if (!msg) return;

        try {
          const event = JSON.parse(msg.content.toString());
          console.log(`[ENTREE] Événement reçu: ${event.eventName}`);

          await handler(event.data, event);

          // Acknowledge : message traité avec succès
          channel.ack(msg);

        } catch (error) {
          console.error('Erreur traitement événement:', error);

          // Nack : remettre dans la queue si erreur (une fois seulement)
          channel.nack(msg, false, !msg.fields.redelivered);
        }
      });

      console.log(`[OK] Abonné au pattern: ${pattern} -> Queue: ${queueName}`);
    }
  }

  module.exports = new MessageBroker();  // Singleton

  // ════════════════════════════════════════════════════════════════
  // Dans Enrollment Service — Publication d'événement
  // ════════════════════════════════════════════════════════════════

  const broker = require('./messaging/MessageBroker');

  class EnrollmentService {
    async enroll(userId, courseId, paymentToken) {
      // ... logique d'inscription ...

      const enrollment = await this.enrollmentRepo.create({...});

      // Publier l'événement (asynchrone, non bloquant)
      await broker.publish('enrollment.created', {
        enrollmentId: enrollment.id,
        userId,
        courseId,
        enrolledAt: enrollment.enrolledAt,
        paymentId: enrollment.paymentId
      });

      return enrollment;
    }
  }

  // ════════════════════════════════════════════════════════════════
  // Dans Notification Service — Consommation d'événement
  // ════════════════════════════════════════════════════════════════

  const broker = require('./messaging/MessageBroker');
  const EmailService = require('./EmailService');

  async function startNotificationListeners() {
    // S'abonner à tous les événements enrollment.*
    await broker.subscribe(
      'enrollment.*',           // Pattern : enrollment.created, enrollment.completed...
      'notification-enrollments',// Nom de la queue
      async (data, event) => {
        switch (event.eventName) {
          case 'enrollment.created':
            await EmailService.sendEnrollmentConfirmation(data);
            break;
          case 'enrollment.completed':
            await EmailService.sendCompletionCertificate(data);
            break;
          default:
            console.log(`Événement ignoré: ${event.eventName}`);
        }
      }
    );

    // S'abonner aux événements course.*
    await broker.subscribe(
      'course.published',
      'notification-courses',
      async (data) => {
        await EmailService.notifyFollowers(data);
      }
    );
  }

  startNotificationListeners();

------------------------------------------------------------------------
4.5 DOCKER COMPOSE — ENVIRONNEMENT LOCAL COMPLET
------------------------------------------------------------------------

  # docker-compose.yml — EduConnect Microservices en local

  version: '3.8'

  services:

    # ── API Gateway ───────────────────────────────────────────────
    api-gateway:
      build: ./api-gateway
      ports:
        - "8080:8080"
      environment:
        AUTH_SERVICE_URL: http://auth-service:3001
        USER_SERVICE_URL: http://user-service:3002
        COURSE_SERVICE_URL: http://course-service:3003
        ENROLLMENT_SERVICE_URL: http://enrollment-service:3004
      depends_on:
        - auth-service
        - user-service
        - course-service
        - enrollment-service

    # ── Auth Service ──────────────────────────────────────────────
    auth-service:
      build: ./services/auth-service
      ports:
        - "3001:3001"
      environment:
        DATABASE_URL: postgresql://auth_user:auth_pass@auth-db:5432/auth_db
        JWT_SECRET: ${JWT_SECRET}
        PORT: 3001
      depends_on:
        auth-db:
          condition: service_healthy

    auth-db:
      image: postgres:16-alpine
      environment:
        POSTGRES_DB: auth_db
        POSTGRES_USER: auth_user
        POSTGRES_PASSWORD: auth_pass
      volumes:
        - auth_data:/var/lib/postgresql/data
      healthcheck:
        test: ["CMD-SHELL", "pg_isready -U auth_user"]
        interval: 5s
        retries: 5

    # ── Course Service ────────────────────────────────────────────
    course-service:
      build: ./services/course-service
      ports:
        - "3003:3003"
      environment:
        DATABASE_URL: postgresql://course_user:course_pass@course-db:5432/course_db
        RABBITMQ_URL: amqp://guest:guest@rabbitmq:5672
        PORT: 3003
      depends_on:
        - course-db
        - rabbitmq

    course-db:
      image: postgres:16-alpine
      environment:
        POSTGRES_DB: course_db
        POSTGRES_USER: course_user
        POSTGRES_PASSWORD: course_pass
      volumes:
        - course_data:/var/lib/postgresql/data

    # ── Message Broker ────────────────────────────────────────────
    rabbitmq:
      image: rabbitmq:3-management-alpine
      ports:
        - "5672:5672"    # AMQP
        - "15672:15672"  # Interface d'administration web
      environment:
        RABBITMQ_DEFAULT_USER: guest
        RABBITMQ_DEFAULT_PASS: guest
      volumes:
        - rabbitmq_data:/var/lib/rabbitmq

    # ── Cache Redis (partagé entre services) ─────────────────────
    redis:
      image: redis:7-alpine
      ports:
        - "6379:6379"

    # ── Monitoring ────────────────────────────────────────────────
    prometheus:
      image: prom/prometheus
      ports:
        - "9090:9090"
      volumes:
        - ./infrastructure/monitoring/prometheus.yml:/etc/prometheus/prometheus.yml

    grafana:
      image: grafana/grafana
      ports:
        - "3000:3000"
      depends_on:
        - prometheus

  volumes:
    auth_data:
    course_data:
    rabbitmq_data:

------------------------------------------------------------------------
4.6 DISTRIBUTED TRACING
------------------------------------------------------------------------

  // shared/tracing/tracer.js
  // OpenTelemetry — Trace les requêtes à travers tous les services

  const { NodeSDK } = require('@opentelemetry/sdk-node');
  const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');
  const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
  const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express');
  const { PgInstrumentation } = require('@opentelemetry/instrumentation-pg');

  const sdk = new NodeSDK({
    traceExporter: new JaegerExporter({
      endpoint: process.env.JAEGER_URL || 'http://jaeger:14268/api/traces'
    }),
    instrumentations: [
      new HttpInstrumentation(),      // Trace tous les appels HTTP
      new ExpressInstrumentation(),   // Trace les routes Express
      new PgInstrumentation()         // Trace les requêtes PostgreSQL
    ]
  });

  // Initialiser avant tout autre code
  sdk.start();

  // Maintenant, chaque requête HTTP entrante et sortante est tracée automatiquement
  // avec un "trace ID" propagé dans les headers : traceparent
  // Cela permet de voir le chemin complet d'une requête dans Jaeger

================================================================================
CHAPITRE 5 : BONNES PRATIQUES MICROSERVICES
================================================================================

------------------------------------------------------------------------
5.1 CONTRATS D'API ET VERSIONING
------------------------------------------------------------------------

Chaque service expose une API contractuelle.
Les changements doivent être rétrocompatibles.

STRATÉGIES DE VERSIONING :
  1. Version dans l'URL : /api/v1/courses, /api/v2/courses
  2. Version dans le header : Accept: application/vnd.educonnect.v2+json

RÈGLES DE COMPATIBILITÉ ASCENDANTE :
  [OK] Ajouter un nouveau champ (optionnel)
  [OK] Ajouter un nouveau endpoint
  [OK] Rendre un champ optionnel (était requis)
  [X] Supprimer un champ existant
  [X] Changer le type d'un champ
  [X] Changer le comportement d'un endpoint existant

CONTRAT D'API (OpenAPI/Swagger) :
  Documenter chaque endpoint dans un fichier openapi.yaml.
  Les consumers utilisent ce contrat pour tester la compatibilité.

------------------------------------------------------------------------
5.2 HEALTH CHECKS
------------------------------------------------------------------------

Chaque service doit implémenter des health checks :

  GET /health/live   -> Le service est-il vivant ? (liveness)
  GET /health/ready  -> Le service est-il prêt à recevoir du trafic ? (readiness)

  app.get('/health/live', (req, res) => {
    // Simple : le processus répond
    res.json({ status: 'alive' });
  });

  app.get('/health/ready', async (req, res) => {
    try {
      // Vérifier les dépendances critiques
      await pool.query('SELECT 1');  // Base de données
      await redis.ping();             // Cache

      res.json({ status: 'ready', checks: { db: 'ok', redis: 'ok' } });
    } catch (error) {
      res.status(503).json({ status: 'not ready', error: error.message });
    }
  });

------------------------------------------------------------------------
5.3 LOGGING STRUCTURÉ ET CORRÉLATION
------------------------------------------------------------------------

  // Tous les services doivent logger de manière structurée
  // et inclure le trace ID pour la corrélation

  const logger = {
    info: (message, context = {}) => {
      console.log(JSON.stringify({
        level: 'info',
        message,
        service: process.env.SERVICE_NAME,
        traceId: context.traceId || 'none',
        ...context,
        timestamp: new Date().toISOString()
      }));
    }
  };

  // Middleware pour extraire et logger le trace ID
  app.use((req, res, next) => {
    // Le trace ID est propagé dans les headers par OpenTelemetry
    req.traceId = req.headers['x-trace-id'] || require('uuid').v4();
    req.log = {
      info: (msg, ctx) => logger.info(msg, { traceId: req.traceId, ...ctx }),
      error: (msg, ctx) => logger.error(msg, { traceId: req.traceId, ...ctx })
    };
    next();
  });

================================================================================
CHAPITRE 6 : ERREURS FRÉQUENTES
================================================================================

ERREUR 1 : DISTRIBUTED MONOLITH
  Symptôme : "Nos microservices doivent être déployés ensemble"
  Cause : Services trop couplés, partage de base de données, dépendances synchrones en cascade
  Solution : 
    - Database per service
    - Communication asynchrone pour la cohérence éventuelle
    - Définir des bounded contexts clairs

ERREUR 2 : NANO-SERVICES
  Symptôme : 150 services pour 10 développeurs
  Cause : Trop granulaire, un service = une fonction
  Solution : Regrouper selon les capacités métier réelles
             Règle : ~1 service pour 2-3 développeurs (Team Topologies)

ERREUR 3 : SYNCHRONE PARTOUT
  Symptôme : Chaque appel d'action lance 10 appels HTTP synchrones en cascade
  Impact : Latence cumulée = 50ms x 10 = 500ms minimum
  Solution : Asynchrone pour tout ce qui ne nécessite pas de réponse immédiate

ERREUR 4 : PAS DE CIRCUIT BREAKERS
  Symptôme : Payment Service down -> Enrollment Service en timeout -> cascade de pannes
  Solution : Circuit Breaker sur tous les appels inter-services

ERREUR 5 : DONNÉES DUPLIQUÉES SANS STRATÉGIE
  Symptôme : "On a copié la table users dans 5 services"
  Impact : Incohérences de données, difficile à maintenir
  Solution : Single Source of Truth + API calls, ou
             Event-driven data synchronization avec Kafka

ERREUR 6 : MICROSERVICES SANS AUTOMATISATION
  Symptôme : Déployer 15 services = 15 déploiements manuels
  Solution : CI/CD pipeline complet pour chaque service
             Docker + Kubernetes + Helm

================================================================================
CHAPITRE 7 : EXERCICES
================================================================================

EXERCICES FACILES

EXERCICE 1 : Analyser la décomposition
  EduConnect a ces fonctionnalités. Proposez une décomposition en services.
  Justifiez chaque choix.
  - Authentification (register, login, OAuth, 2FA, sessions)
  - Profils utilisateurs
  - Catalogue cours
  - Inscription aux cours
  - Lecture vidéos + progression
  - Paiements
  - Emails et notifications push
  - Recherche full-text
  - Génération de certificats

EXERCICE 2 : Diagramme de communication
  Pour le scénario "Un formateur publie un cours" :
  a) Quels services sont impliqués ?
  b) Quels appels sont synchrones vs asynchrones ?
  c) Dessinez le diagramme de séquence

EXERCICE 3 : Health Check complet
  Implémentez un health check pour le Course Service qui vérifie :
  - La connexion PostgreSQL
  - La connexion Redis
  - La connexion RabbitMQ
  - L'espace disque disponible
  Retournez le statut global et le détail de chaque check.

EXERCICES INTERMÉDIAIRES

EXERCICE 4 : Saga pour remboursement
  Implémentez une Saga Chorégraphie pour le scénario "RemboursementCours" :
  1. Student demande remboursement
  2. Payment Service rembourse via Stripe
  3. Enrollment Service invalide l'accès
  4. Certificate Service révoque le certificat
  5. Notification Service envoie confirmation

  Gérez les cas d'erreur (compensation si étape 3 échoue).

EXERCICE 5 : API Gateway avec rate limiting par service
  Ajoutez au gateway des rate limits différents par service et par rôle :
  - Auth Service : 10 req/min (anti brute force)
  - Course Service (lecture) : 1000 req/min
  - Course Service (écriture) : 30 req/min
  - Payment Service : 20 req/min
  - Instructors : limites plus élevées que students

EXERCICE 6 : Service Discovery manuel
  Implémentez un registre de services simple avec :
  - Les services s'enregistrent via POST /register
  - Les services envoient un heartbeat toutes les 10s
  - Si un service ne donne plus de heartbeat -> retiré du registre
  - L'API Gateway interroge le registre pour trouver les URLs

EXERCICES AVANCÉS

EXERCICE 7 : Outbox Pattern pour fiabilité des événements
  Problème : Comment garantir qu'un événement est publié MÊME SI
  RabbitMQ est temporairement indisponible ?
  
  Implémentez l'Outbox Pattern :
  1. Lors de la transaction DB, écrire l'événement dans une table "outbox"
     dans la MÊME transaction
  2. Un worker lit périodiquement la table outbox
  3. Publie les événements non publiés dans RabbitMQ
  4. Marque les événements comme publiés
  
  Garantie : At-least-once delivery

EXERCICE 8 : CQRS dans le Course Service
  Séparez les opérations d'écriture et de lecture du Course Service :
  - Write Side : PostgreSQL (source de vérité)
  - Read Side : Redis / Elasticsearch (lecture optimisée)
  - Synchronisation via événements domaine
  
  Avantage : Requêtes de lecture ultra-rapides, sans impacter les écritures

EXERCICE 9 : Migration progressive du monolithe
  Vous avez le monolithe EduConnect.
  Extrayez UNIQUEMENT le Payment Service en microservice.
  
  Stratégie Strangler Fig Pattern :
  1. Créer le Payment Service indépendant
  2. Configurer l'API Gateway pour router /api/payments -> nouveau service
  3. Migrer les données de paiement
  4. Supprimer le module payment du monolithe
  5. Tester et valider
  
  Documentez chaque étape avec le risque associé.

================================================================================
RÉCAPITULATIF — MICROSERVICES
================================================================================

Points clés :

1. Microservices = services indépendants, chacun responsable d'une capacité métier.

2. LES TROIS PILLIERS : Déploiement indépendant + DB per service + Communication API.

3. PATTERNS ESSENTIELS : API Gateway, Circuit Breaker, Saga, Service Discovery.

4. COMMUNICATION : Synchrone (REST) pour réponse immédiate,
                    Asynchrone (Message Broker) pour découplage.

5. NE PAS COMMENCER PAR MICROSERVICES : Commencer monolithe -> extraire si nécessaire.

6. DISTRIBUTED TRACING obligatoire pour déboguer dans un environnement distribué.

7. EDUCONNECT MICROSERVICES : 9 services identifiés, API Gateway, RabbitMQ,
   Circuit Breakers, Distributed Tracing.

Prochaine étape :
  Volume 6 : Clean Architecture
  -> Organiser chaque service selon la Clean Architecture
  -> Logique métier complètement indépendante des frameworks et databases

================================================================================
FIN DU VOLUME 5 — ARCHITECTURE MICROSERVICES
Prochaine étape -> architecture_clean.txt
================================================================================

================================================================================
     GUIDE COMPLET DES ARCHITECTURES LOGICIELLES - VOLUME 6
     Clean Architecture
     Pour étudiants en Génie Logiciel
================================================================================

================================================================================
CHAPITRE 1 : INTRODUCTION
================================================================================

------------------------------------------------------------------------
1.1 DÉFINITION
------------------------------------------------------------------------

La Clean Architecture, définie par Robert C. Martin (Uncle Bob) en 2012,
est une organisation du code en cercles concentriques où les dépendances
ne peuvent aller QUE vers l'intérieur.

L'objectif principal : SÉPARER la logique métier des détails techniques.
  - La logique métier ne doit pas savoir si on utilise PostgreSQL ou MongoDB
  - La logique métier ne doit pas savoir si c'est une API REST ou GraphQL
  - La logique métier ne doit pas savoir si le framework est Express ou Fastify

Citation d'Uncle Bob :
  "Architecture is about intent. The intent of an application should be
   obvious from its code. Not 'this is a web application', but
   'this is a course management system'."

------------------------------------------------------------------------
1.2 LE DIAGRAMME DES CERCLES CONCENTRIQUES
------------------------------------------------------------------------

                    ┌─────────────────────────────────────────┐
                    │          FRAMEWORKS & DRIVERS           │
                    │  (Express, React, PostgreSQL, Stripe)   │
                    │                                         │
                    │   ┌─────────────────────────────────┐   │
                    │   │      INTERFACE ADAPTERS         │   │
                    │   │  (Controllers, Gateways,        │   │
                    │   │   Presenters, Repositories)     │   │
                    │   │                                 │   │
                    │   │   ┌───────────────────────┐     │   │
                    │   │   │   APPLICATION         │     │   │
                    │   │   │   BUSINESS RULES      │     │   │
                    │   │   │ (Use Cases)           │     │   │
                    │   │   │                       │     │   │
                    │   │   │  ┌─────────────────┐  │     │   │
                    │   │   │  │   ENTERPRISE    │  │     │   │
                    │   │   │  │ BUSINESS RULES  │  │     │   │
                    │   │   │  │   (Entities)    │  │     │   │
                    │   │   │  └─────────────────┘  │     │   │
                    │   │   └───────────────────────┘     │   │
                    │   └─────────────────────────────────┘   │
                    └─────────────────────────────────────────┘

LA RÈGLE DE DÉPENDANCE (The Dependency Rule) :
  Les dépendances du code source ne peuvent pointer que vers l'intérieur.
  Rien dans un cercle intérieur ne peut connaître quoi que ce soit du cercle extérieur.

------------------------------------------------------------------------
1.3 LES 4 COUCHES
------------------------------------------------------------------------

COUCHE 1 — ENTERPRISE BUSINESS RULES (Entités)
  Le cœur absolu. Logique métier fondamentale de l'entreprise.
  Indépendante de TOUT (frameworks, DB, UI, services externes).
  Change très rarement — uniquement si les règles métier fondamentales changent.
  
  Contient :
    - Entités métier (objets avec données + comportements)
    - Value Objects
    - Règles métier fondamentales (invariants)

COUCHE 2 — APPLICATION BUSINESS RULES (Use Cases)
  Orchestre les entités pour accomplir un Use Case spécifique.
  Change si les Use Cases changent (nouvelles fonctionnalités).
  
  Contient :
    - Use Cases (Interactors)
    - Interfaces des repositories (définies ici, implémentées dans couche 4)
    - Input/Output DTOs des Use Cases

COUCHE 3 — INTERFACE ADAPTERS
  Convertit les données entre le format des Use Cases et le format externe.
  Change si l'interface change (REST -> GraphQL, PostgreSQL -> MongoDB).
  
  Contient :
    - Controllers (HTTP -> Use Case)
    - Presenters (Use Case output -> HTTP response / View)
    - Repositories implémentations
    - Gateways (services externes)

COUCHE 4 — FRAMEWORKS & DRIVERS
  Détails d'implémentation : framework web, base de données, etc.
  Change si on change de technologie.
  
  Contient :
    - Frameworks (Express, Django)
    - Base de données (PostgreSQL driver)
    - Interfaces utilisateur (React, templates)
    - Services externes (Stripe SDK, SendGrid)

========================================================================
CHAPITRE 2 : THÉORIE — CONCEPTS AVANCÉS
================================================================================

------------------------------------------------------------------------
2.1 ENTITÉS VS USE CASES — LA DISTINCTION CRITIQUE
------------------------------------------------------------------------

ENTITÉ = Règles métier UNIVERSELLES
  Ces règles existent indépendamment de l'application.
  Elles seraient les mêmes dans n'importe quelle implémentation.
  
  Exemple EduConnect :
    "Un cours doit avoir une description d'au moins 50 caractères avant publication"
    Cette règle est vraie qu'on ait une API REST, une app mobile, ou une CLI.
    -> Règle dans l'Entité Course.

USE CASE = Règles métier SPÉCIFIQUES À L'APPLICATION
  Ces règles définissent comment l'application utilise les entités.
  Elles pourraient changer si les besoins de l'application changent.
  
  Exemple EduConnect :
    "Quand un étudiant s'inscrit à un cours payant, vérifier les prérequis,
     charger la carte, envoyer un email de confirmation"
    Cette séquence est spécifique à l'application EduConnect.
    Si on crée EduConnect Enterprise avec un flux différent, ce Use Case change.
    -> Logique dans le Use Case EnrollStudent.

------------------------------------------------------------------------
2.2 LE PRINCIPE D'INVERSION (Dependency Inversion dans Clean Arch)
------------------------------------------------------------------------

Problème : Le Use Case EnrollStudent a besoin d'accéder à la base de données.
           Mais le Use Case est dans le cercle intérieur, et la DB dans l'extérieur.
           Comment une couche intérieure peut-elle utiliser une couche extérieure ?

Solution : INTERFACES DÉFINIES PAR LE CERCLE INTÉRIEUR

  CERCLE 2 (Use Cases) définit l'interface :
    interface EnrollmentRepository {
      save(enrollment: Enrollment): Promise<Enrollment>
      findByStudentAndCourse(studentId, courseId): Promise<Enrollment | null>
    }
  
  CERCLE 3 (Interface Adapters) implémente l'interface :
    class PostgresEnrollmentRepository implements EnrollmentRepository {
      save(enrollment) { /* SQL */ }
      findByStudentAndCourse(s, c) { /* SQL */ }
    }
  
  CERCLE 4 (Frameworks) injecte l'implémentation :
    const useCase = new EnrollStudentUseCase(
      new PostgresEnrollmentRepository(pool)
    )

  Résultat : Le Use Case (cercle 2) ne connaît QUE l'interface.
             Il ne sait pas que PostgreSQL existe.

------------------------------------------------------------------------
2.3 PORTS ET ADAPTERS (Relation avec Clean Architecture)
------------------------------------------------------------------------

La Clean Architecture et l'Architecture Hexagonale (Ports & Adapters) sont
deux formulations du même principe fondamental.

  Clean Architecture -> Architecture Hexagonale
  Entités + Use Cases -> Core / Domain
  Interface Adapters -> Adapters
  Frameworks & Drivers -> Ports (Primary) + External Systems (Secondary)

Les deux disent : "Protège ton cœur métier des détails techniques."

------------------------------------------------------------------------
2.4 AVANTAGES ET INCONVÉNIENTS
------------------------------------------------------------------------

AVANTAGES :
  [OK] Logique métier complètement indépendante et testable sans infrastructure
  [OK] Changement de framework sans modifier les Use Cases ni Entités
  [OK] Changement de base de données sans modifier les Use Cases ni Entités
  [OK] Tests rapides : les Use Cases s'exécutent en mémoire, sans I/O
  [OK] Évolutivité : ajouter un nouveau Use Case sans impacter les autres
  [OK] Lisibilité : la structure du code révèle l'intention métier

INCONVÉNIENTS :
  [X] Verbosité : beaucoup de classes, interfaces, DTOs
  [X] Courbe d'apprentissage élevée
  [X] Over-engineering pour des applications simples
  [X] Mapping constant entre les couches (DTOs -> Entités -> DTOs)

QUAND L'UTILISER :
  -> Applications avec logique métier complexe et évolutive
  -> Applications avec longue durée de vie (5+ ans)
  -> Équipes qui changent régulièrement de technologie
  -> Systèmes avec multiple interfaces (API REST + GraphQL + CLI + Worker)

================================================================================
CHAPITRE 3 : SCHÉMAS
================================================================================

------------------------------------------------------------------------
3.1 FLUX COMPLET D'UN USE CASE
------------------------------------------------------------------------

  POST /api/v1/courses/:id/publish
         │
         [BLACK_DOWN-POINTING_TRIANGLE]
  ════════════════════════ CERCLE 4 : FRAMEWORK ══════════════
  Express Router
         │
         [BLACK_DOWN-POINTING_TRIANGLE]
  ════════════════════════ CERCLE 3 : INTERFACE ADAPTERS ═════
  CourseHttpController
    - Extrait courseId, userId depuis req
    - Construit PublishCourseInput DTO
    - Appelle publishCourseUseCase.execute(input)
         │
         [BLACK_DOWN-POINTING_TRIANGLE] (Input DTO)
  ════════════════════════ CERCLE 2 : USE CASES ══════════════
  PublishCourseUseCase
    - Appelle courseRepository.findById(input.courseId)
    - Vérifie que l'user est le propriétaire
    - Appelle course.publish() [logique entité]
    - Appelle courseRepository.save(course)
    - Émet l'événement CoursePublished
    - Retourne PublishCourseOutput DTO
         │
         [BLACK_DOWN-POINTING_TRIANGLE] (appel interface)
  ════════════════════════ CERCLE 1 : ENTITÉS ════════════════
  Course.publish()
    - Valide les invariants
    - Change status -> published
    - Ajoute domain event CoursePublished
         │
         [BLACK_DOWN-POINTING_TRIANGLE] (via interface IRepository)
  ════════════════════════ CERCLE 3 : INTERFACE ADAPTERS ═════
  PostgresCourseRepository.save(course)
    - Mappe Entité Course -> Objet DB
    - SQL UPDATE courses SET status='published'...
         │
         [BLACK_DOWN-POINTING_TRIANGLE]
  ════════════════════════ CERCLE 4 : FRAMEWORK ══════════════
  PostgreSQL Driver -> Base de données
         │
         [BLACK_DOWN-POINTING_TRIANGLE]
  Retour vers Controller -> Presenter -> HTTP Response

------------------------------------------------------------------------
3.2 STRUCTURE DE DOSSIERS CLEAN ARCHITECTURE
------------------------------------------------------------------------

  src/
  ├── domain/                        <- CERCLE 1 : Entités
  │   ├── entities/
  │   │   ├── Course.ts
  │   │   ├── Enrollment.ts
  │   │   └── User.ts
  │   ├── value-objects/
  │   │   ├── CourseId.ts
  │   │   ├── Money.ts
  │   │   └── Email.ts
  │   └── events/
  │       ├── CoursePublished.ts
  │       └── StudentEnrolled.ts
  │
  ├── use-cases/                     <- CERCLE 2 : Use Cases
  │   ├── course/
  │   │   ├── PublishCourse.usecase.ts
  │   │   ├── CreateCourse.usecase.ts
  │   │   └── __tests__/
  │   │       └── PublishCourse.test.ts
  │   ├── enrollment/
  │   │   ├── EnrollStudent.usecase.ts
  │   │   └── __tests__/
  │   └── ports/                     <- Interfaces définies par les Use Cases
  │       ├── repositories/
  │       │   ├── ICourseRepository.ts
  │       │   └── IEnrollmentRepository.ts
  │       └── services/
  │           ├── IPaymentService.ts
  │           └── IEmailService.ts
  │
  ├── adapters/                      <- CERCLE 3 : Interface Adapters
  │   ├── http/
  │   │   ├── controllers/
  │   │   │   ├── CourseController.ts
  │   │   │   └── EnrollmentController.ts
  │   │   └── presenters/
  │   │       ├── CoursePresenter.ts
  │   │       └── ErrorPresenter.ts
  │   ├── repositories/
  │   │   ├── PostgresCourseRepository.ts
  │   │   └── PostgresEnrollmentRepository.ts
  │   └── services/
  │       ├── StripePaymentGateway.ts
  │       └── SendGridEmailGateway.ts
  │
  └── infrastructure/                <- CERCLE 4 : Frameworks & Drivers
      ├── http/
      │   ├── server.ts              <- Configuration Express
      │   └── routes.ts              <- Montage des routes
      ├── database/
      │   ├── connection.ts          <- Pool PostgreSQL
      │   └── migrations/
      ├── config/
      │   └── container.ts           <- IoC Container (assemblage)
      └── workers/
          └── EventHandlers.ts       <- Handlers d'événements

================================================================================
CHAPITRE 4 : IMPLÉMENTATION COMPLÈTE
================================================================================

------------------------------------------------------------------------
4.1 CERCLE 1 — ENTITÉS
------------------------------------------------------------------------

  // ════════════════════════════════════════════════════════════
  // domain/entities/Course.ts
  // ════════════════════════════════════════════════════════════

  import { CourseId } from '../value-objects/CourseId';
  import { Money } from '../value-objects/Money';
  import { CoursePublished } from '../events/CoursePublished';

  // Statuts valides — Enum dans le domaine
  export type CourseStatus = 'draft' | 'published' | 'archived';
  export type CourseLevel = 'beginner' | 'intermediate' | 'advanced';

  interface CourseState {
    id: CourseId;
    title: string;
    description: string | null;
    instructorId: string;
    price: Money;
    level: CourseLevel;
    status: CourseStatus;
    durationMinutes: number | null;
    prerequisites: CourseId[];
    publishedAt: Date | null;
    createdAt: Date;
    updatedAt: Date;
  }

  export class Course {
    private _state: CourseState;
    private _events: object[] = [];

    private constructor(state: CourseState) {
      this._state = Object.freeze({ ...state });  // Immuabilité via freeze
    }

    // ── Accès en lecture ─────────────────────────────────────
    get id() { return this._state.id; }
    get title() { return this._state.title; }
    get instructorId() { return this._state.instructorId; }
    get price() { return this._state.price; }
    get status() { return this._state.status; }
    get description() { return this._state.description; }
    get level() { return this._state.level; }
    get durationMinutes() { return this._state.durationMinutes; }
    get prerequisites() { return [...this._state.prerequisites]; }
    get publishedAt() { return this._state.publishedAt; }
    get createdAt() { return this._state.createdAt; }
    get updatedAt() { return this._state.updatedAt; }
    get domainEvents() { return [...this._events]; }

    // ── Factory method : Créer un nouveau cours ───────────────
    static create(props: {
      id: string;
      title: string;
      instructorId: string;
      price: Money;
      level: CourseLevel;
    }): Course {
      // Validation à la création
      Course._validateTitle(props.title);

      return new Course({
        id: new CourseId(props.id),
        title: props.title.trim(),
        description: null,
        instructorId: props.instructorId,
        price: props.price,
        level: props.level,
        status: 'draft',
        durationMinutes: null,
        prerequisites: [],
        publishedAt: null,
        createdAt: new Date(),
        updatedAt: new Date()
      });
    }

    // ── Factory method : Reconstituer depuis la persistance ───
    static reconstitute(state: CourseState): Course {
      return new Course(state);
    }

    // ── Comportements métier ──────────────────────────────────

    // Mettre à jour la description — retourne une NOUVELLE entité (immuabilité)
    withDescription(description: string): Course {
      if (description.trim().length < 50) {
        throw new DomainError('La description doit contenir au moins 50 caractères');
      }
      return new Course({
        ...this._state,
        description: description.trim(),
        updatedAt: new Date()
      });
    }

    withDuration(minutes: number): Course {
      if (minutes <= 0) {
        throw new DomainError('La durée doit être un entier positif');
      }
      return new Course({
        ...this._state,
        durationMinutes: minutes,
        updatedAt: new Date()
      });
    }

    // Publier le cours
    publish(): Course {
      this._ensureCanBePublished();

      const published = new Course({
        ...this._state,
        status: 'published',
        publishedAt: new Date(),
        updatedAt: new Date()
      });

      // Ajouter l'événement du domaine
      published._events = [
        ...this._events,
        new CoursePublished(
          this._state.id.value,
          this._state.instructorId,
          new Date()
        )
      ];

      return published;
    }

    isPublished(): boolean { return this._state.status === 'published'; }
    isFree(): boolean { return this._state.price.isZero(); }
    isOwnedBy(userId: string): boolean { return this._state.instructorId === userId; }

    clearEvents(): Course {
      const copy = new Course({ ...this._state });
      copy._events = [];
      return copy;
    }

    // ── Invariants (validation interne) ──────────────────────

    private static _validateTitle(title: string): void {
      if (!title || title.trim().length < 5) {
        throw new DomainError('Le titre doit contenir au moins 5 caractères');
      }
      if (title.trim().length > 255) {
        throw new DomainError('Le titre ne peut pas dépasser 255 caractères');
      }
    }

    private _ensureCanBePublished(): void {
      const violations: string[] = [];

      if (!this._state.description || this._state.description.length < 50) {
        violations.push('Description trop courte (min 50 caractères)');
      }
      if (!this._state.durationMinutes || this._state.durationMinutes <= 0) {
        violations.push('Durée non renseignée');
      }
      if (this._state.status === 'published') {
        violations.push('Le cours est déjà publié');
      }
      if (this._state.status === 'archived') {
        violations.push('Impossible de republier un cours archivé');
      }

      if (violations.length > 0) {
        throw new DomainError(
          `Impossible de publier ce cours : ${violations.join('; ')}`
        );
      }
    }
  }

  // Erreur du domaine — ne dépend d'aucun framework
  export class DomainError extends Error {
    constructor(message: string) {
      super(message);
      this.name = 'DomainError';
    }
  }

------------------------------------------------------------------------
4.2 CERCLE 2 — USE CASES ET PORTS
------------------------------------------------------------------------

  // ════════════════════════════════════════════════════════════
  // use-cases/ports/repositories/ICourseRepository.ts
  // Interface définie PAR les Use Cases (pas par l'infrastructure)
  // ════════════════════════════════════════════════════════════

  import { Course } from '../../../domain/entities/Course';

  export interface ICourseRepository {
    findById(id: string): Promise<Course | null>;
    findAll(filters: CourseQueryFilters): Promise<PaginatedCourses>;
    save(course: Course): Promise<Course>;
    delete(id: string): Promise<void>;
  }

  export interface CourseQueryFilters {
    status?: string;
    level?: string;
    instructorId?: string;
    page?: number;
    limit?: number;
  }

  export interface PaginatedCourses {
    items: Course[];
    total: number;
    page: number;
    totalPages: number;
  }

  // ════════════════════════════════════════════════════════════
  // use-cases/ports/services/IPaymentService.ts
  // ════════════════════════════════════════════════════════════

  import { Money } from '../../../domain/value-objects/Money';

  export interface PaymentRequest {
    token: string;
    amount: Money;
    customerId: string;
    description: string;
  }

  export interface PaymentResult {
    paymentId: string;
    status: 'succeeded' | 'failed';
    amount: Money;
  }

  export interface IPaymentService {
    charge(request: PaymentRequest): Promise<PaymentResult>;
    refund(paymentId: string, amount: Money): Promise<void>;
  }

  // ════════════════════════════════════════════════════════════
  // use-cases/course/PublishCourse.usecase.ts
  // Use Case : Publier un cours
  // ════════════════════════════════════════════════════════════

  import { ICourseRepository } from '../ports/repositories/ICourseRepository';
  import { IEventBus } from '../ports/services/IEventBus';

  // Input DTO — Ce que le Use Case reçoit
  export interface PublishCourseInput {
    courseId: string;
    requestingUserId: string;
    requestingUserRole: string;
  }

  // Output DTO — Ce que le Use Case retourne
  export interface PublishCourseOutput {
    courseId: string;
    title: string;
    status: string;
    publishedAt: Date;
  }

  // Erreurs spécifiques au Use Case (pas des erreurs HTTP !)
  export class CourseNotFoundError extends Error {
    constructor(id: string) {
      super(`Cours ${id} non trouvé`);
      this.name = 'CourseNotFoundError';
    }
  }

  export class UnauthorizedError extends Error {
    constructor() {
      super('Vous n\'avez pas les droits pour publier ce cours');
      this.name = 'UnauthorizedError';
    }
  }

  export class PublishCourseUseCase {
    // Le Use Case dépend d'interfaces, jamais d'implémentations
    constructor(
      private readonly courseRepository: ICourseRepository,
      private readonly eventBus: IEventBus
    ) {}

    async execute(input: PublishCourseInput): Promise<PublishCourseOutput> {

      // ── Étape 1 : Charger l'agrégat ────────────────────────
      const course = await this.courseRepository.findById(input.courseId);
      if (!course) {
        throw new CourseNotFoundError(input.courseId);
      }

      // ── Étape 2 : Autorisation ─────────────────────────────
      const canPublish =
        course.isOwnedBy(input.requestingUserId) ||
        input.requestingUserRole === 'admin';

      if (!canPublish) {
        throw new UnauthorizedError();
      }

      // ── Étape 3 : Exécuter la logique domaine ──────────────
      // course.publish() valide les invariants et retourne un nouveau Course
      const publishedCourse = course.publish();

      // ── Étape 4 : Persister ────────────────────────────────
      const savedCourse = await this.courseRepository.save(publishedCourse);

      // ── Étape 5 : Publier les événements ───────────────────
      for (const event of publishedCourse.domainEvents) {
        await this.eventBus.publish(event);
      }

      // ── Étape 6 : Retourner l'Output DTO ──────────────────
      return {
        courseId: savedCourse.id.value,
        title: savedCourse.title,
        status: savedCourse.status,
        publishedAt: savedCourse.publishedAt!
      };
    }
  }

  // ════════════════════════════════════════════════════════════
  // use-cases/course/__tests__/PublishCourse.test.ts
  // Tests unitaires SANS base de données, SANS framework
  // ════════════════════════════════════════════════════════════

  import { PublishCourseUseCase, CourseNotFoundError, UnauthorizedError } from '../PublishCourse.usecase';
  import { Course, DomainError } from '../../../domain/entities/Course';
  import { Money } from '../../../domain/value-objects/Money';

  // Mocks des repositories
  const mockCourseRepository = {
    findById: jest.fn(),
    save: jest.fn(),
    findAll: jest.fn(),
    delete: jest.fn()
  };

  const mockEventBus = {
    publish: jest.fn()
  };

  describe('PublishCourseUseCase', () => {
    let useCase: PublishCourseUseCase;

    beforeEach(() => {
      jest.clearAllMocks();
      useCase = new PublishCourseUseCase(mockCourseRepository, mockEventBus);
    });

    // Helper pour créer un cours complet et valide
    const makeValidCourse = (overrides = {}) => Course.reconstitute({
      id: { value: 'course-1' } as any,
      title: 'Introduction à Python',
      description: 'Apprenez Python depuis zéro. Ce cours couvre tous les fondamentaux.',
      instructorId: 'user-1',
      price: Money.of(29.99),
      level: 'beginner',
      status: 'draft',
      durationMinutes: 120,
      prerequisites: [],
      publishedAt: null,
      createdAt: new Date(),
      updatedAt: new Date(),
      ...overrides
    });

    it('devrait publier un cours valide appartenant à l\'instructeur', async () => {
      const course = makeValidCourse();
      mockCourseRepository.findById.mockResolvedValue(course);
      mockCourseRepository.save.mockImplementation(c => c);

      const result = await useCase.execute({
        courseId: 'course-1',
        requestingUserId: 'user-1',    // Propriétaire du cours
        requestingUserRole: 'instructor'
      });

      expect(result.status).toBe('published');
      expect(result.publishedAt).toBeDefined();
      expect(mockCourseRepository.save).toHaveBeenCalledTimes(1);
      expect(mockEventBus.publish).toHaveBeenCalledTimes(1);
    });

    it('devrait lever CourseNotFoundError si le cours n\'existe pas', async () => {
      mockCourseRepository.findById.mockResolvedValue(null);

      await expect(
        useCase.execute({ courseId: 'nonexistent', requestingUserId: 'user-1', requestingUserRole: 'instructor' })
      ).rejects.toThrow(CourseNotFoundError);

      expect(mockCourseRepository.save).not.toHaveBeenCalled();
    });

    it('devrait lever UnauthorizedError si l\'utilisateur n\'est pas le propriétaire', async () => {
      const course = makeValidCourse({ instructorId: 'other-user' });
      mockCourseRepository.findById.mockResolvedValue(course);

      await expect(
        useCase.execute({ courseId: 'course-1', requestingUserId: 'user-1', requestingUserRole: 'instructor' })
      ).rejects.toThrow(UnauthorizedError);
    });

    it('devrait permettre à un admin de publier n\'importe quel cours', async () => {
      const course = makeValidCourse({ instructorId: 'other-user' });
      mockCourseRepository.findById.mockResolvedValue(course);
      mockCourseRepository.save.mockImplementation(c => c);

      const result = await useCase.execute({
        courseId: 'course-1',
        requestingUserId: 'admin-user',
        requestingUserRole: 'admin'   // Admin peut publier n'importe quel cours
      });

      expect(result.status).toBe('published');
    });

    it('devrait lever DomainError si le cours est incomplet', async () => {
      const incompleteCourse = makeValidCourse({
        description: null,         // Pas de description
        durationMinutes: null      // Pas de durée
      });
      mockCourseRepository.findById.mockResolvedValue(incompleteCourse);

      await expect(
        useCase.execute({ courseId: 'course-1', requestingUserId: 'user-1', requestingUserRole: 'instructor' })
      ).rejects.toThrow(DomainError);
    });
  });

------------------------------------------------------------------------
4.3 CERCLE 3 — INTERFACE ADAPTERS
------------------------------------------------------------------------

  // ════════════════════════════════════════════════════════════
  // adapters/http/controllers/CourseController.ts
  // Adapte HTTP -> Use Case -> HTTP
  // ════════════════════════════════════════════════════════════

  import { Request, Response, NextFunction } from 'express';
  import { PublishCourseUseCase, CourseNotFoundError, UnauthorizedError } from '../../../use-cases/course/PublishCourse.usecase';
  import { DomainError } from '../../../domain/entities/Course';

  export class CourseController {
    constructor(
      private readonly publishCourseUseCase: PublishCourseUseCase
    ) {}

    async publishCourse(req: Request, res: Response, next: NextFunction): Promise<void> {
      try {
        // ADAPTER : Traduire HTTP -> Input DTO du Use Case
        const input = {
          courseId: req.params.id,
          requestingUserId: req.headers['x-user-id'] as string,
          requestingUserRole: req.headers['x-user-role'] as string
        };

        // Appeler le Use Case
        const output = await this.publishCourseUseCase.execute(input);

        // ADAPTER : Traduire Output DTO du Use Case -> HTTP Response
        res.status(200).json({
          message: 'Cours publié avec succès',
          data: {
            courseId: output.courseId,
            title: output.title,
            status: output.status,
            publishedAt: output.publishedAt.toISOString()
          }
        });

      } catch (error) {
        // Traduire les erreurs du Use Case -> Codes HTTP
        if (error instanceof CourseNotFoundError) {
          res.status(404).json({ error: error.message });
        } else if (error instanceof UnauthorizedError) {
          res.status(403).json({ error: error.message });
        } else if (error instanceof DomainError) {
          // Erreur métier -> 422 Unprocessable Entity
          res.status(422).json({ error: error.message });
        } else {
          next(error);  // Erreur technique -> middleware global
        }
      }
    }
  }

  // ════════════════════════════════════════════════════════════
  // adapters/repositories/PostgresCourseRepository.ts
  // ════════════════════════════════════════════════════════════

  import { Pool } from 'pg';
  import { ICourseRepository, CourseQueryFilters, PaginatedCourses } from '../../use-cases/ports/repositories/ICourseRepository';
  import { Course, CourseLevel, CourseStatus } from '../../domain/entities/Course';
  import { CourseId } from '../../domain/value-objects/CourseId';
  import { Money } from '../../domain/value-objects/Money';

  export class PostgresCourseRepository implements ICourseRepository {
    constructor(private readonly pool: Pool) {}

    async findById(id: string): Promise<Course | null> {
      const { rows } = await this.pool.query(
        'SELECT * FROM courses WHERE id = $1',
        [id]
      );
      if (!rows[0]) return null;
      return this._toDomain(rows[0]);
    }

    async save(course: Course): Promise<Course> {
      // UPSERT : INSERT si nouveau, UPDATE si existant
      const { rows } = await this.pool.query(`
        INSERT INTO courses (id, title, description, instructor_id,
                             price_cents, price_currency, level, status,
                             duration_minutes, published_at, created_at, updated_at)
        VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
        ON CONFLICT (id) DO UPDATE SET
          title = EXCLUDED.title, description = EXCLUDED.description,
          price_cents = EXCLUDED.price_cents, price_currency = EXCLUDED.price_currency,
          level = EXCLUDED.level, status = EXCLUDED.status,
          duration_minutes = EXCLUDED.duration_minutes,
          published_at = EXCLUDED.published_at, updated_at = EXCLUDED.updated_at
        RETURNING *
      `, [
        course.id.value,
        course.title, course.description, course.instructorId,
        course.price.amountInCents, course.price.currency,
        course.level, course.status, course.durationMinutes,
        course.publishedAt, course.createdAt, course.updatedAt
      ]);
      return this._toDomain(rows[0]);
    }

    async findAll(filters: CourseQueryFilters): Promise<PaginatedCourses> {
      // ... (similaire aux implémentations précédentes)
      return { items: [], total: 0, page: 1, totalPages: 0 };
    }

    async delete(id: string): Promise<void> {
      await this.pool.query(
        'UPDATE courses SET status = $1 WHERE id = $2',
        ['archived', id]
      );
    }

    // Mapping Row DB -> Entité Domaine
    private _toDomain(row: any): Course {
      return Course.reconstitute({
        id: new CourseId(row.id),
        title: row.title,
        description: row.description,
        instructorId: row.instructor_id,
        price: new Money(row.price_cents / 100, row.price_currency),
        level: row.level as CourseLevel,
        status: row.status as CourseStatus,
        durationMinutes: row.duration_minutes,
        prerequisites: [],
        publishedAt: row.published_at,
        createdAt: row.created_at,
        updatedAt: row.updated_at
      });
    }
  }

------------------------------------------------------------------------
4.4 CERCLE 4 — INFRASTRUCTURE ET ASSEMBLAGE
------------------------------------------------------------------------

  // ════════════════════════════════════════════════════════════
  // infrastructure/config/container.ts
  // Assemblage de toutes les couches — IoC Container
  // ════════════════════════════════════════════════════════════

  import { Pool } from 'pg';

  // Cercle 3 : Adapters
  import { PostgresCourseRepository } from '../../adapters/repositories/PostgresCourseRepository';
  import { PostgresEnrollmentRepository } from '../../adapters/repositories/PostgresEnrollmentRepository';
  import { StripePaymentGateway } from '../../adapters/services/StripePaymentGateway';
  import { EventBus } from '../../adapters/events/EventBus';
  import { CourseController } from '../../adapters/http/controllers/CourseController';

  // Cercle 2 : Use Cases
  import { PublishCourseUseCase } from '../../use-cases/course/PublishCourse.usecase';
  import { EnrollStudentUseCase } from '../../use-cases/enrollment/EnrollStudent.usecase';
  import { CreateCourseUseCase } from '../../use-cases/course/CreateCourse.usecase';

  export function buildContainer() {
    // ── Infrastructure
    const pool = new Pool({ connectionString: process.env.DATABASE_URL });

    // ── Adapters (Cercle 3)
    const courseRepository = new PostgresCourseRepository(pool);
    const enrollmentRepository = new PostgresEnrollmentRepository(pool);
    const paymentService = new StripePaymentGateway(process.env.STRIPE_KEY!);
    const eventBus = new EventBus();

    // ── Use Cases (Cercle 2) — Injection des Adapters
    const publishCourseUseCase = new PublishCourseUseCase(courseRepository, eventBus);
    const createCourseUseCase = new CreateCourseUseCase(courseRepository, eventBus);
    const enrollStudentUseCase = new EnrollStudentUseCase(
      courseRepository,
      enrollmentRepository,
      paymentService,
      eventBus
    );

    // ── Controllers (Cercle 3) — Injection des Use Cases
    const courseController = new CourseController(
      createCourseUseCase,
      publishCourseUseCase
    );

    return { courseController, enrollStudentUseCase };
  }

  // ════════════════════════════════════════════════════════════
  // infrastructure/http/server.ts
  // Configuration du serveur Express
  // ════════════════════════════════════════════════════════════

  import express from 'express';
  import { buildContainer } from '../config/container';

  export function createServer() {
    const app = express();
    app.use(express.json());

    // Construire le conteneur
    const container = buildContainer();

    // Monter les routes
    app.post('/api/v1/courses', (req, res, next) =>
      container.courseController.createCourse(req, res, next)
    );
    app.post('/api/v1/courses/:id/publish', (req, res, next) =>
      container.courseController.publishCourse(req, res, next)
    );

    return app;
  }

================================================================================
CHAPITRE 5 : BONNES PRATIQUES CLEAN ARCHITECTURE
================================================================================

------------------------------------------------------------------------
5.1 LES TESTS COMME VALIDATION DE L'ARCHITECTURE
------------------------------------------------------------------------

La Clean Architecture garantit des tests rapides et isolés :

  TESTS UNITAIRES (Cercles 1 et 2) :
    - Testent uniquement les Entités et Use Cases
    - Pas de base de données, pas de réseau, pas de framework
    - Vitesse : millisecondes
    - Couverture : ~70% du code

  TESTS D'INTÉGRATION (Cercles 3 et 4) :
    - Testent les Repositories avec une vraie base de données (PostgreSQL de test)
    - Testent les Gateways avec des APIs simulées (Stripe test mode)
    - Vitesse : secondes
    - Couverture : ~20% du code

  TESTS E2E (Tout) :
    - Testent les flux complets via HTTP
    - Vitesse : dizaines de secondes
    - Couverture : ~10% du code (les scénarios critiques)

LA PYRAMIDE DE TESTS :
        /\
       /E2E\   <- Peu nombreux, lents, coûteux
      /─────\
     /Intégra\  <- Quelques-uns, moyennement rapides
    /─────────\
   / Unit Tests \  <- Nombreux, rapides, la base
  /─────────────\

------------------------------------------------------------------------
5.2 LE MAPPING ENTRE LES COUCHES
------------------------------------------------------------------------

Chaque traversée de frontière de couche nécessite un mapping :

  HTTP JSON -> Input DTO (dans le Controller)
  Input DTO -> Entité (dans le Use Case)
  Entité -> Output DTO (dans le Use Case)
  Output DTO -> HTTP Response (dans le Controller)
  Entité -> DB Row (dans le Repository)
  DB Row -> Entité (dans le Repository)

Ce mapping est verbeux mais il :
  - Découple les couches
  - Protège les entités des formats externes
  - Permet d'évoluer chaque représentation indépendamment

PATTERN MAPPER :
  // Pour chaque entité, un mapper dans la couche Adapter

  class CourseMapper {
    static toDomain(row: CourseRow): Course { ... }
    static toPersistence(course: Course): CourseRow { ... }
    static toDTO(course: Course): CourseDTO { ... }
  }

------------------------------------------------------------------------
5.3 GESTION DES ERREURS PAR COUCHE
------------------------------------------------------------------------

DOMAINE : DomainError (règle métier violée)
  Course.publish() -> throw new DomainError('Description trop courte')

USE CASE : Erreurs applicatives (NotFound, Unauthorized, Conflict)
  PublishCourseUseCase -> throw new CourseNotFoundError(id)

ADAPTER (Controller) : Traduit en codes HTTP
  DomainError -> 422 Unprocessable Entity
  NotFoundError -> 404 Not Found
  UnauthorizedError -> 403 Forbidden
  Erreur inattendue -> 500 Internal Server Error

Règle : Les erreurs du Domaine ne contiennent pas de codes HTTP.
        Les codes HTTP sont une décision de l'Adapter (HTTP).
        Si on expose la même logique en GraphQL, les erreurs seront différentes.

================================================================================
CHAPITRE 6 : ERREURS FRÉQUENTES
================================================================================

ERREUR 1 : FRAMEWORK DANS LE DOMAINE
  Symptôme : import { Entity } from 'typeorm' dans domain/entities/Course.ts
  Impact : Le Domaine dépend de TypeORM -> impossible de changer d'ORM
  Correction : Les entités du domaine sont des classes TypeScript pures
               TypeORM ne doit apparaître QUE dans les Adapters/Repositories

ERREUR 2 : USE CASE QUI IMPORTE EXPRESS
  Symptôme : import { Request, Response } from 'express' dans un Use Case
  Impact : Le Use Case est couplé à HTTP
  Correction : Le Use Case reçoit un Input DTO simple (pas req, res)
               La conversion HTTP -> DTO est faite dans le Controller

ERREUR 3 : DOMAINE QUI CONNAIT LA BASE DE DONNÉES
  Symptôme : Les entités ont des méthodes find(), save() (Active Record)
  Correction : Les entités sont de pures classes métier
               L'accès DB est dans les Repositories (Adapters)

ERREUR 4 : USE CASE ANÉMIQUE
  Symptôme :
    class PublishCourse:
      course.status = 'published'  // Logique dans le Use Case, pas dans l'entité
  Correction :
    class PublishCourse:
      course.publish()  // Déléguer à l'entité qui valide les invariants

ERREUR 5 : TROP DE Use Cases GÉNÉRIQUES
  Symptôme : UpdateCourseUseCase qui fait tout
  Correction : Un Use Case = une intention métier précise
               PublishCourseUseCase, UpdateCourseTitleUseCase, ArchiveCourseUseCase

================================================================================
CHAPITRE 7 : EXERCICES
================================================================================

EXERCICES FACILES

EXERCICE 1 : Entité User Clean Architecture
  Implémentez l'entité User en respectant la Clean Architecture :
  - Aucun import de framework
  - Constructeur privé avec factory methods (create, reconstitute)
  - Comportements : changePassword, deactivate, promoteToInstructor
  - Invariants : email valide, mot de passe hashé
  - Value Object: Email (avec validation)

EXERCICE 2 : Value Object Money amélioré
  Complétez le Value Object Money :
  - Conversion entre devises (taux fixe pour l'exercice : 1 EUR = 1.1 USD)
  - Comparaison : isGreaterThan, isLessThan
  - Formatage localisé : format('fr-FR') -> "29,99 €"
  - Validation : montant max 99 999,99

EXERCICE 3 : Use Case GetCourseDetails
  Implémentez le Use Case GetCourseDetails :
  Input : { courseId, requestingUserId? }
  Logique :
    - Récupère le cours
    - Si cours draft, vérifie que le demandeur est le propriétaire ou admin
    - Si cours publié, accessible à tous
  Output : CourseDetailsOutput avec tous les champs du cours

EXERCICES INTERMÉDIAIRES

EXERCICE 4 : Test suite complète du Use Case EnrollStudent
  Écrivez tous les tests unitaires pour EnrollStudentUseCase :
  - Inscription réussie (cours gratuit)
  - Inscription réussie (cours payant)
  - Étudiant déjà inscrit -> erreur
  - Cours non trouvé -> erreur
  - Cours pas publié -> erreur
  - Prérequis manquants -> erreur
  - Paiement échoué -> erreur + rollback de l'enrollment

EXERCICE 5 : Implémentation complète EnrollStudentUseCase
  Codez le Use Case complet avec :
  - Validation des prérequis via IEnrollmentRepository
  - Gestion du paiement via IPaymentService
  - Création de l'Enrollment (entité)
  - Dispatch des événements domaine
  - Tests unitaires complets (voir exercice 4)

EXERCICE 6 : Repository avec cache
  Créez CachedCourseRepository qui implémente ICourseRepository et wraps
  PostgresCourseRepository avec Redis.
  Pattern Decorator : le code des Use Cases ne change pas.
  TTL : 5 minutes pour findById, 1 minute pour findAll.

EXERCICES AVANCÉS

EXERCICE 7 : Aggregate Root pour Enrollment
  Un Enrollment agrège : Enrollment + List<LessonProgress>.
  Implémentez l'agrégat avec :
  - markLessonCompleted(lessonId) -> Recalcule la progression
  - complete() -> Marque terminé + génère domain event
  - Invariant : ne peut pas compléter si progression < 80%
  Tester que les invariants sont respectés.

EXERCICE 8 : Architecture Decision — ORM ou SQL natif ?
  Analysez les deux options pour les Repositories :
  Option A : TypeORM (ORM avec decorators)
  Option B : node-postgres (SQL natif)
  
  a) Quels sont les avantages de chaque option pour la Clean Architecture ?
  b) Rédigez l'ADR pour ce choix dans le contexte d'EduConnect
  c) Implémentez le même repository avec les deux approches

EXERCICE 9 : Vertical Slice Architecture
  La Clean Architecture peut être organisée en "couches horizontales" (notre approche)
  ou en "slices verticaux" (par feature).
  
  Reorganisez le projet en Vertical Slices :
  src/
    features/
      publish-course/
        PublishCourse.usecase.ts
        PublishCourse.controller.ts
        PublishCourse.repository.ts
        PublishCourse.test.ts
  
  Quels sont les avantages et inconvénients vs la structure horizontale ?

================================================================================
RÉCAPITULATIF — CLEAN ARCHITECTURE
================================================================================

Points clés :

1. Clean Architecture = 4 cercles, dépendances uniquement vers l'intérieur.

2. RÈGLE DE DÉPENDANCE : Rien dans un cercle intérieur ne connaît le cercle extérieur.

3. ENTITÉS = Logique métier universelle. No framework. No DB. No HTTP.

4. USE CASES = Logique applicative. Dépend uniquement d'interfaces (ports).

5. ADAPTERS = Traduction entre Use Cases et monde extérieur (HTTP, DB).

6. TESTS SANS INFRASTRUCTURE : Les Use Cases se testent en millisecondes.

7. Le MAPPING entre couches est verbeux mais garantit l'indépendance.

8. EDUCONNECT CLEAN : Course/Enrollment comme agrégats purs, Use Cases précis,
   Repositories avec interfaces, Controller qui traduit HTTP <-> Use Case.

Prochaine étape :
  Volume 7 : Architecture Hexagonale (Ports & Adapters)
  -> Complément naturel de la Clean Architecture
  -> Vision différente du même principe d'isolation

================================================================================
FIN DU VOLUME 6 — CLEAN ARCHITECTURE
Prochaine étape -> architecture_hexagonale.txt
================================================================================

================================================================================
     GUIDE COMPLET DES ARCHITECTURES LOGICIELLES - VOLUME 7
     Architecture Hexagonale (Ports & Adapters)
     Pour étudiants en Génie Logiciel
================================================================================

================================================================================
CHAPITRE 1 : INTRODUCTION
================================================================================

------------------------------------------------------------------------
1.1 DÉFINITION ET ORIGINE
------------------------------------------------------------------------

L'Architecture Hexagonale, aussi appelée "Ports & Adapters", a été inventée
par Alistair Cockburn en 2005. Elle vise le même objectif que la Clean
Architecture : isoler la logique métier des technologies externes.

La différence principale avec la Clean Architecture :
  Clean Architecture -> Cercles concentriques, hiérarchie claire
  Architecture Hexagonale -> Hexagone central avec ports, adaptateurs autour

Pourquoi un hexagone ?
  L'hexagone n'a pas de signification mathématique particulière.
  Il a été choisi pour pouvoir dessiner facilement plusieurs ports
  (entrées/sorties) sur ses côtés sans qu'un côté soit "supérieur" à un autre.
  Dans la pratique, on peut avoir autant de ports qu'on veut.

------------------------------------------------------------------------
1.2 VISUALISATION
------------------------------------------------------------------------

         ┌─────────────────────────────────────────────────┐
         │              DRIVING SIDE                        │
         │         (Pilote l'application)                  │
         │                                                  │
         │  API REST    CLI    Tests    Interface Web       │
         │     │         │       │           │              │
         │     [BLACK_DOWN-POINTING_TRIANGLE]         [BLACK_DOWN-POINTING_TRIANGLE]       [BLACK_DOWN-POINTING_TRIANGLE]           [BLACK_DOWN-POINTING_TRIANGLE]              │
         │  ┌──────────────────────────────────────┐        │
         │  │   HTTP   CLI   Test   Web            │        │
         │  │   Adapter Adapter Adapter Adapter    │        │
         └──┤                                      ├────────┘
            │  ┌────────────────────────────────┐  │
            │  │                                │  │
            │  │                                │  │
            │  │     HEXAGONE (APPLICATION)     │  │
            │  │     PORT <- -> Logique Métier    │  │
            │  │                                │  │
            │  │    Use Cases + Entities        │  │
            │  └──────────────┬─────────────────┘  │
         ┌──┤                  │                    ├────────┐
         │  │   DB    Email  SMS  Stripe           │        │
         │  │   Adapter Adapter Adapter Adapter    │        │
         │  └──────────────────────────────────────┘        │
         │              DRIVEN SIDE                         │
         │          (Piloté par l'application)              │
         │                                                  │
         │   PostgreSQL  SendGrid  Twilio  Stripe           │
         └──────────────────────────────────────────────────┘

------------------------------------------------------------------------
1.3 PORTS ET ADAPTERS — TERMINOLOGIE
------------------------------------------------------------------------

PORT :
  Une interface définie par l'Hexagone (l'application).
  C'est un contrat que l'Adapter doit respecter.
  Il existe deux types de ports.

ADAPTER :
  Une implémentation concrète d'un Port.
  Il traduit entre le monde extérieur et l'Hexagone.

PORT PRIMAIRE (Primary Port / Driving Port) :
  Interface que l'Hexagone EXPOSE pour être utilisé par le monde extérieur.
  L'Hexagone est "conduit" via ce port.
  
  Exemple :
    Port : ICourseService { createCourse(), publishCourse(), getCourse() }
    Adapters primaires :
      - HttpCourseAdapter (API REST -> ICourseService)
      - CliCourseAdapter (CLI -> ICourseService)
      - GraphQLCourseAdapter (GraphQL -> ICourseService)
      - TestCourseAdapter (Tests -> ICourseService directement)

PORT SECONDAIRE (Secondary Port / Driven Port) :
  Interface que l'Hexagone UTILISE pour accéder au monde extérieur.
  L'Hexagone "conduit" les adapters secondaires.
  
  Exemple :
    Port : ICourseRepository { save(), findById(), findAll() }
    Adapters secondaires :
      - PostgresCourseRepository (ICourseRepository -> PostgreSQL)
      - MongoCourseRepository (ICourseRepository -> MongoDB)
      - InMemoryCourseRepository (ICourseRepository -> Memory, pour les tests)

------------------------------------------------------------------------
1.4 POURQUOI L'ARCHITECTURE HEXAGONALE ?
------------------------------------------------------------------------

OBJECTIF 1 : TESTABILITÉ MAXIMALE
  Remplacer tous les adapters secondaires par des implémentations en mémoire.
  -> Tester toute la logique métier sans une seule connexion réseau/base de données.
  -> Tests ultra-rapides (milliseconde vs secondes).

OBJECTIF 2 : INDÉPENDANCE TECHNOLOGIQUE
  Changer PostgreSQL pour MongoDB -> Créer un nouveau PostgresCourseRepository.
  Rien d'autre ne change.
  
  Changer Express pour Fastify -> Créer un nouveau FastifyAdapter.
  Rien d'autre ne change.

OBJECTIF 3 : MULTIPLE INTERFACES SANS DUPLICATION
  La même logique métier peut être exposée via :
  - API REST (HttpAdapter)
  - CLI (CliAdapter)
  - Message Queue (MessageQueueAdapter)
  - Tests automatisés (DirectAdapter)
  
  Sans dupliquer la logique.

OBJECTIF 4 : CLARTÉ DU DESIGN
  Le code dit clairement : "Ce sont mes ports d'entrée (ce que je fais).
                            Ce sont mes ports de sortie (ce dont j'ai besoin)."

================================================================================
CHAPITRE 2 : THÉORIE
================================================================================

------------------------------------------------------------------------
2.1 DIFFÉRENCES AVEC CLEAN ARCHITECTURE
------------------------------------------------------------------------

SIMILITUDES :
  - Les deux isolent la logique métier
  - Les deux utilisent l'inversion de dépendances
  - Les deux permettent de remplacer les technologies
  - Les deux facilitent les tests

DIFFÉRENCES :

  TOPOLOGIE :
    Clean Architecture -> Cercles concentriques (intérieur vs extérieur)
    Architecture Hexagonale -> Centre + Périphérie (pas de hiérarchie stricte entre cercles)
  
  TERMINOLOGIE :
    Clean Architecture : Entities, Use Cases, Interface Adapters, Frameworks
    Architecture Hexagonale : Domain, Application, Ports (Primary/Secondary), Adapters
  
  FOCUS :
    Clean Architecture -> Emphasis sur les niveaux d'abstraction et la hiérarchie
    Architecture Hexagonale -> Emphasis sur les points d'entrée/sortie (ports)
  
  GRANULARITÉ :
    Clean Architecture -> Distingue Entities de Use Cases (2 couches internes)
    Architecture Hexagonale -> Application = Core (combine les deux)

En pratique : beaucoup d'équipes combinent les deux.
"Clean Architecture est une spécification plus détaillée d'Hexagonal Architecture"

------------------------------------------------------------------------
2.2 COMPOSITION DE L'HEXAGONE
------------------------------------------------------------------------

L'HEXAGONE CONTIENT :

  DOMAIN LAYER (Entités, Value Objects, Domain Events)
    - Les concepts métier purs
    - Pas de dépendances vers l'extérieur
    - Mêmes règles que la Clean Architecture

  APPLICATION LAYER (Use Cases, Application Services)
    - Orchestre les entités
    - Définit les ports secondaires (interfaces)
    - Ne dépend que du Domain Layer et des Ports

L'HEXAGONE EXPOSE (Ports Primaires) :
  Des interfaces que les adaptateurs primaires vont appeler.
  
  Ex :
    interface ICourseApplicationService {
      createCourse(command: CreateCourseCommand): Promise<CourseDTO>
      publishCourse(command: PublishCourseCommand): Promise<CourseDTO>
      listCourses(query: ListCoursesQuery): Promise<PaginatedCoursesDTO>
    }

L'HEXAGONE REQUIERT (Ports Secondaires) :
  Des interfaces que les adaptateurs secondaires vont implémenter.
  
  Ex :
    interface ICourseRepository { ... }
    interface IEmailPort { ... }
    interface IPaymentPort { ... }

------------------------------------------------------------------------
2.3 AVANTAGES ET INCONVÉNIENTS
------------------------------------------------------------------------

AVANTAGES :
  [OK] Testabilité maximale via les adapters en mémoire
  [OK] Indépendance totale des technologies
  [OK] Multiple interfaces (REST + GraphQL + CLI) sans duplication
  [OK] Design clair et explicite (ports bien nommés)
  [OK] Facilite l'évolution technologique

INCONVÉNIENTS :
  [X] Plus verbeux que l'architecture en couches simple
  [X] Beaucoup d'interfaces à gérer
  [X] Over-engineering pour applications simples
  [X] Courbe d'apprentissage

================================================================================
CHAPITRE 3 : SCHÉMAS
================================================================================

------------------------------------------------------------------------
3.1 VUE COMPLÈTE EDUCONNECT HEXAGONAL
------------------------------------------------------------------------

  ADAPTERS PRIMAIRES (Pilotent l'Hexagone)
  ════════════════════════════════════════

  ┌─────────────┐   ┌─────────────┐   ┌─────────────┐   ┌──────────────┐
  │ HTTP REST   │   │  GraphQL    │   │    CLI      │   │  Test Suite  │
  │  Adapter   │   │  Adapter   │   │  Adapter   │   │   Adapter   │
  │             │   │             │   │             │   │              │
  │ Express     │   │ Apollo      │   │ Commander   │   │ Jest Mocks   │
  │ Controller  │   │ Resolver    │   │ Command     │   │ Direct Call  │
  └──────┬──────┘   └──────┬──────┘   └──────┬──────┘   └──────┬───────┘
         │                 │                 │                  │
         └─────────────────┴─────────────────┴──────────────────┘
                                     │
                          ┌──────────[BLACK_DOWN-POINTING_TRIANGLE]──────────┐
                          │    PRIMARY PORT      │
                          │   ICourseService     │
                          │                      │
                          │ createCourse()       │
                          │ publishCourse()      │
                          │ listCourses()        │
                          └──────────┬───────────┘
                                     │
  ════════════════════════════════════════════════════════════════
                         L ' H E X A G O N E
  ════════════════════════════════════════════════════════════════
                                     │
                          ┌──────────[BLACK_DOWN-POINTING_TRIANGLE]───────────────┐
                          │                          │
                          │  DOMAIN LAYER            │
                          │  Course, Enrollment,     │
                          │  User (Entities)         │
                          │  Money, Email (VOs)      │
                          │                          │
                          │  APPLICATION LAYER       │
                          │  CourseApplicationSvc    │
                          │  EnrollStudentUseCase    │
                          │                          │
                          └──────────┬───────────────┘
                                     │
                          ┌──────────[BLACK_DOWN-POINTING_TRIANGLE]───────────────┐
                          │    SECONDARY PORTS       │
                          │                          │
                          │  ICourseRepository       │
                          │  IEmailPort              │
                          │  IPaymentPort            │
                          │  IEventPort              │
                          └──────────┬───────────────┘
  ════════════════════════════════════════════════════════════════

  ADAPTERS SECONDAIRES (Pilotés par l'Hexagone)
  ══════════════════════════════════════════════

         │           │           │           │
  ┌──────[BLACK_DOWN-POINTING_TRIANGLE]──┐  ┌─────[BLACK_DOWN-POINTING_TRIANGLE]────┐ ┌───[BLACK_DOWN-POINTING_TRIANGLE]───────┐ ┌─[BLACK_DOWN-POINTING_TRIANGLE]──────────┐
  │Postgres │  │SendGrid  │ │  Stripe   │ │  Kafka     │
  │ Course  │  │  Email   │ │ Payment   │ │  Event     │
  │  Repo   │  │  Adapter │ │ Adapter   │ │  Adapter   │
  └─────────┘  └──────────┘ └───────────┘ └────────────┘

------------------------------------------------------------------------
3.2 STRUCTURE DE DOSSIERS HEXAGONALE
------------------------------------------------------------------------

  educonnect/
  └── src/
      ├── hexagon/                         <- L'HEXAGONE
      │   ├── domain/                      <- Domain Layer
      │   │   ├── course/
      │   │   │   ├── Course.ts            <- Entité
      │   │   │   ├── CourseId.ts          <- Value Object
      │   │   │   └── CoursePublished.ts   <- Domain Event
      │   │   ├── enrollment/
      │   │   │   └── Enrollment.ts
      │   │   └── shared/
      │   │       └── Money.ts
      │   │
      │   └── application/                 <- Application Layer
      │       ├── services/
      │       │   ├── CourseApplicationService.ts    <- PRIMARY PORT impl
      │       │   └── EnrollmentApplicationService.ts
      │       ├── use-cases/
      │       │   ├── EnrollStudentUseCase.ts
      │       │   └── PublishCourseUseCase.ts
      │       └── ports/
      │           ├── primary/             <- Interfaces primaires (ce que l'hexagone expose)
      │           │   ├── ICourseService.ts
      │           │   └── IEnrollmentService.ts
      │           └── secondary/           <- Interfaces secondaires (ce que l'hexagone requiert)
      │               ├── ICourseRepository.ts
      │               ├── IEmailPort.ts
      │               ├── IPaymentPort.ts
      │               └── IEventPort.ts
      │
      └── adapters/                        <- LES ADAPTERS
          ├── primary/                     <- Adapters primaires
          │   ├── http/
          │   │   ├── CourseHttpAdapter.ts
          │   │   └── EnrollmentHttpAdapter.ts
          │   ├── graphql/
          │   │   └── CourseGraphQLAdapter.ts
          │   └── cli/
          │       └── CourseCliAdapter.ts
          │
          └── secondary/                   <- Adapters secondaires
              ├── persistence/
              │   ├── PostgresCourseRepository.ts
              │   └── InMemoryCourseRepository.ts    <- Pour les tests !
              ├── email/
              │   ├── SendGridEmailAdapter.ts
              │   └── InMemoryEmailAdapter.ts         <- Pour les tests !
              ├── payment/
              │   ├── StripePaymentAdapter.ts
              │   └── FakePaymentAdapter.ts           <- Pour les tests !
              └── events/
                  └── KafkaEventAdapter.ts

================================================================================
CHAPITRE 4 : IMPLÉMENTATION COMPLÈTE
================================================================================

------------------------------------------------------------------------
4.1 LES PORTS (INTERFACES)
------------------------------------------------------------------------

  // ═══════════════════════════════════════════════════════════════
  // hexagon/application/ports/primary/ICourseService.ts
  // PORT PRIMAIRE — Ce que l'Hexagone EXPOSE vers l'extérieur
  // ═══════════════════════════════════════════════════════════════

  // Command Objects — Intentions de modification
  export interface CreateCourseCommand {
    title: string;
    instructorId: string;
    priceAmount: number;
    priceCurrency: string;
    level: 'beginner' | 'intermediate' | 'advanced';
  }

  export interface PublishCourseCommand {
    courseId: string;
    requestingUserId: string;
    requestingUserRole: string;
  }

  // Query Objects — Intentions de lecture
  export interface ListCoursesQuery {
    page?: number;
    limit?: number;
    level?: string;
    search?: string;
  }

  // DTO de sortie
  export interface CourseDTO {
    id: string;
    title: string;
    instructorId: string;
    priceAmount: number;
    priceCurrency: string;
    level: string;
    status: string;
    description?: string | null;
    durationMinutes?: number | null;
    publishedAt?: string | null;
    createdAt: string;
  }

  // PORT PRIMAIRE : Interface que les adapters primaires vont utiliser
  export interface ICourseService {
    createCourse(command: CreateCourseCommand): Promise<CourseDTO>;
    publishCourse(command: PublishCourseCommand): Promise<CourseDTO>;
    updateCourseDescription(courseId: string, description: string, userId: string): Promise<CourseDTO>;
    getCourse(courseId: string, requestingUserId?: string): Promise<CourseDTO>;
    listCourses(query: ListCoursesQuery): Promise<{ items: CourseDTO[]; total: number }>;
  }

  // ═══════════════════════════════════════════════════════════════
  // hexagon/application/ports/secondary/IEmailPort.ts
  // PORT SECONDAIRE — Ce que l'Hexagone REQUIERT de l'extérieur
  // ═══════════════════════════════════════════════════════════════

  export interface EmailMessage {
    to: string;
    subject: string;
    htmlBody: string;
    textBody?: string;
  }

  // PORT SECONDAIRE : Interface que les adapters secondaires vont implémenter
  export interface IEmailPort {
    send(message: EmailMessage): Promise<void>;
    sendBulk(messages: EmailMessage[]): Promise<void>;
  }

  // ═══════════════════════════════════════════════════════════════
  // hexagon/application/ports/secondary/IPaymentPort.ts
  // ═══════════════════════════════════════════════════════════════

  export interface ChargeRequest {
    token: string;
    amountInCents: number;
    currency: string;
    description: string;
    customerId: string;
  }

  export interface ChargeResult {
    paymentId: string;
    status: 'succeeded' | 'failed' | 'pending';
  }

  export interface IPaymentPort {
    charge(request: ChargeRequest): Promise<ChargeResult>;
    refund(paymentId: string, amountInCents: number): Promise<void>;
  }

------------------------------------------------------------------------
4.2 L'APPLICATION SERVICE (Implémentation du Port Primaire)
------------------------------------------------------------------------

  // ═══════════════════════════════════════════════════════════════
  // hexagon/application/services/CourseApplicationService.ts
  // Implémentation du PORT PRIMAIRE ICourseService
  // ═══════════════════════════════════════════════════════════════

  import { ICourseService, CreateCourseCommand, PublishCourseCommand,
           CourseDTO, ListCoursesQuery } from '../ports/primary/ICourseService';
  import { ICourseRepository } from '../ports/secondary/ICourseRepository';
  import { IEventPort } from '../ports/secondary/IEventPort';
  import { Course, DomainError } from '../../domain/course/Course';
  import { Money } from '../../domain/shared/Money';
  import { v4 as uuidv4 } from 'uuid';

  // Erreurs applicatives (pas d'erreurs HTTP ici !)
  export class CourseNotFoundException extends Error {
    constructor(id: string) { super(`Cours non trouvé : ${id}`); }
  }
  export class InsufficientPermissionsException extends Error {
    constructor() { super('Permissions insuffisantes pour cette action'); }
  }

  export class CourseApplicationService implements ICourseService {

    // Injection des ports secondaires
    constructor(
      private readonly courseRepository: ICourseRepository,
      private readonly eventPort: IEventPort
    ) {}

    async createCourse(command: CreateCourseCommand): Promise<CourseDTO> {
      const price = new Money(command.priceAmount, command.priceCurrency);

      const course = Course.create({
        id: uuidv4(),
        title: command.title,
        instructorId: command.instructorId,
        price,
        level: command.level
      });

      const saved = await this.courseRepository.save(course);
      return this._toDTO(saved);
    }

    async publishCourse(command: PublishCourseCommand): Promise<CourseDTO> {
      const course = await this.courseRepository.findById(command.courseId);
      if (!course) throw new CourseNotFoundException(command.courseId);

      const canPublish =
        course.isOwnedBy(command.requestingUserId) ||
        command.requestingUserRole === 'admin';

      if (!canPublish) throw new InsufficientPermissionsException();

      // Appel de la méthode domaine (peut lever DomainError)
      const published = course.publish();
      const saved = await this.courseRepository.save(published);

      // Dispatching des événements domaine via le port secondaire
      for (const event of published.domainEvents) {
        await this.eventPort.dispatch(event);
      }

      return this._toDTO(saved);
    }

    async getCourse(courseId: string, requestingUserId?: string): Promise<CourseDTO> {
      const course = await this.courseRepository.findById(courseId);
      if (!course) throw new CourseNotFoundException(courseId);

      // Règle d'accès
      if (!course.isPublished()) {
        if (!requestingUserId || (!course.isOwnedBy(requestingUserId))) {
          throw new CourseNotFoundException(courseId);  // Ne pas révéler l'existence
        }
      }

      return this._toDTO(course);
    }

    async updateCourseDescription(courseId: string, description: string, userId: string): Promise<CourseDTO> {
      const course = await this.courseRepository.findById(courseId);
      if (!course) throw new CourseNotFoundException(courseId);
      if (!course.isOwnedBy(userId)) throw new InsufficientPermissionsException();

      const updated = course.withDescription(description);
      const saved = await this.courseRepository.save(updated);
      return this._toDTO(saved);
    }

    async listCourses(query: ListCoursesQuery) {
      const result = await this.courseRepository.findAll({
        page: query.page || 1,
        limit: query.limit || 20,
        level: query.level,
        search: query.search,
        status: 'published'
      });

      return {
        items: result.items.map(c => this._toDTO(c)),
        total: result.total
      };
    }

    // Mapper Entité -> DTO de sortie
    private _toDTO(course: Course): CourseDTO {
      return {
        id: course.id.value,
        title: course.title,
        instructorId: course.instructorId,
        priceAmount: course.price.amount,
        priceCurrency: course.price.currency,
        level: course.level,
        status: course.status,
        description: course.description,
        durationMinutes: course.durationMinutes,
        publishedAt: course.publishedAt?.toISOString() || null,
        createdAt: course.createdAt.toISOString()
      };
    }
  }

------------------------------------------------------------------------
4.3 ADAPTERS PRIMAIRES
------------------------------------------------------------------------

  // ═══════════════════════════════════════════════════════════════
  // adapters/primary/http/CourseHttpAdapter.ts
  // ADAPTER PRIMAIRE HTTP — Traduit HTTP vers ICourseService
  // ═══════════════════════════════════════════════════════════════

  import { Router, Request, Response, NextFunction } from 'express';
  import { ICourseService } from '../../../hexagon/application/ports/primary/ICourseService';
  import {
    CourseNotFoundException,
    InsufficientPermissionsException
  } from '../../../hexagon/application/services/CourseApplicationService';
  import { DomainError } from '../../../hexagon/domain/course/Course';

  export class CourseHttpAdapter {
    public readonly router: Router;

    constructor(private readonly courseService: ICourseService) {
      this.router = Router();
      this._setupRoutes();
    }

    private _setupRoutes(): void {
      // L'adapter traduit les routes HTTP vers les méthodes du port primaire
      this.router.get('/', this._listCourses.bind(this));
      this.router.get('/:id', this._getCourse.bind(this));
      this.router.post('/', this._createCourse.bind(this));
      this.router.post('/:id/publish', this._publishCourse.bind(this));
      this.router.put('/:id/description', this._updateDescription.bind(this));
    }

    private async _listCourses(req: Request, res: Response): Promise<void> {
      try {
        const result = await this.courseService.listCourses({
          page: parseInt(req.query.page as string) || 1,
          limit: parseInt(req.query.limit as string) || 20,
          level: req.query.level as string,
          search: req.query.search as string
        });
        res.json({ data: result.items, total: result.total });
      } catch (err) {
        this._handleError(err, res);
      }
    }

    private async _getCourse(req: Request, res: Response): Promise<void> {
      try {
        const userId = req.headers['x-user-id'] as string;
        const course = await this.courseService.getCourse(req.params.id, userId);
        res.json({ data: course });
      } catch (err) {
        this._handleError(err, res);
      }
    }

    private async _createCourse(req: Request, res: Response): Promise<void> {
      try {
        const { title, priceAmount, priceCurrency, level } = req.body;
        const instructorId = req.headers['x-user-id'] as string;

        const course = await this.courseService.createCourse({
          title, instructorId, priceAmount, priceCurrency, level
        });
        res.status(201).json({ data: course });
      } catch (err) {
        this._handleError(err, res);
      }
    }

    private async _publishCourse(req: Request, res: Response): Promise<void> {
      try {
        const course = await this.courseService.publishCourse({
          courseId: req.params.id,
          requestingUserId: req.headers['x-user-id'] as string,
          requestingUserRole: req.headers['x-user-role'] as string
        });
        res.json({ data: course });
      } catch (err) {
        this._handleError(err, res);
      }
    }

    private async _updateDescription(req: Request, res: Response): Promise<void> {
      try {
        const course = await this.courseService.updateCourseDescription(
          req.params.id,
          req.body.description,
          req.headers['x-user-id'] as string
        );
        res.json({ data: course });
      } catch (err) {
        this._handleError(err, res);
      }
    }

    // Traduire les exceptions de l'hexagone en codes HTTP
    private _handleError(err: any, res: Response): void {
      if (err instanceof CourseNotFoundException) {
        res.status(404).json({ error: err.message });
      } else if (err instanceof InsufficientPermissionsException) {
        res.status(403).json({ error: err.message });
      } else if (err instanceof DomainError) {
        res.status(422).json({ error: err.message });
      } else {
        console.error('Unexpected error:', err);
        res.status(500).json({ error: 'Erreur interne' });
      }
    }
  }

  // ═══════════════════════════════════════════════════════════════
  // adapters/primary/cli/CourseCliAdapter.ts
  // ADAPTER PRIMAIRE CLI — Même logique, interface différente
  // ═══════════════════════════════════════════════════════════════

  import { Command } from 'commander';
  import { ICourseService } from '../../../hexagon/application/ports/primary/ICourseService';

  export class CourseCliAdapter {
    private program: Command;

    constructor(private readonly courseService: ICourseService) {
      this.program = new Command();
      this._setupCommands();
    }

    private _setupCommands(): void {
      this.program
        .command('create <title>')
        .description('Créer un nouveau cours')
        .option('-p, --price <price>', 'Prix du cours', '0')
        .option('-l, --level <level>', 'Niveau', 'beginner')
        .option('-i, --instructor <id>', 'ID de l\'instructeur (requis)')
        .action(async (title, options) => {
          try {
            if (!options.instructor) {
              console.error('Erreur: --instructor est requis');
              process.exit(1);
            }

            const course = await this.courseService.createCourse({
              title,
              instructorId: options.instructor,
              priceAmount: parseFloat(options.price),
              priceCurrency: 'EUR',
              level: options.level
            });

            console.log(`[OK] Cours créé : ${course.id} - "${course.title}"`);
          } catch (err: any) {
            console.error(`[X] Erreur : ${err.message}`);
            process.exit(1);
          }
        });

      this.program
        .command('publish <courseId>')
        .description('Publier un cours')
        .requiredOption('-u, --user <userId>', 'ID de l\'utilisateur')
        .action(async (courseId, options) => {
          try {
            await this.courseService.publishCourse({
              courseId,
              requestingUserId: options.user,
              requestingUserRole: 'instructor'
            });
            console.log(`[OK] Cours ${courseId} publié avec succès`);
          } catch (err: any) {
            console.error(`[X] Erreur : ${err.message}`);
          }
        });
    }

    run(argv: string[]): void {
      this.program.parse(argv);
    }
  }

  // Usage CLI :
  //   node cli.js create "Introduction à Node.js" --instructor user-1 --price 29.99
  //   node cli.js publish course-1 --user user-1

------------------------------------------------------------------------
4.4 ADAPTERS SECONDAIRES — PRODUCTION ET TEST
------------------------------------------------------------------------

  // ═══════════════════════════════════════════════════════════════
  // adapters/secondary/email/SendGridEmailAdapter.ts
  // ADAPTER SECONDAIRE pour SendGrid (Production)
  // ═══════════════════════════════════════════════════════════════

  import sgMail from '@sendgrid/mail';
  import { IEmailPort, EmailMessage } from '../../../hexagon/application/ports/secondary/IEmailPort';

  export class SendGridEmailAdapter implements IEmailPort {
    constructor(apiKey: string) {
      sgMail.setApiKey(apiKey);
    }

    async send(message: EmailMessage): Promise<void> {
      await sgMail.send({
        to: message.to,
        from: 'noreply@educonnect.com',
        subject: message.subject,
        html: message.htmlBody,
        text: message.textBody || ''
      });
    }

    async sendBulk(messages: EmailMessage[]): Promise<void> {
      const sgMessages = messages.map(m => ({
        to: m.to,
        from: 'noreply@educonnect.com',
        subject: m.subject,
        html: m.htmlBody
      }));
      await sgMail.send(sgMessages);
    }
  }

  // ═══════════════════════════════════════════════════════════════
  // adapters/secondary/email/InMemoryEmailAdapter.ts
  // ADAPTER SECONDAIRE en mémoire (Tests)
  // ═══════════════════════════════════════════════════════════════

  import { IEmailPort, EmailMessage } from '../../../hexagon/application/ports/secondary/IEmailPort';

  export class InMemoryEmailAdapter implements IEmailPort {
    // Stocke tous les emails envoyés pour les assertions dans les tests
    public sentEmails: EmailMessage[] = [];

    async send(message: EmailMessage): Promise<void> {
      // En mémoire uniquement, pas d'envoi réel
      this.sentEmails.push(message);
      console.log(`[TEST EMAIL] To: ${message.to} | Subject: ${message.subject}`);
    }

    async sendBulk(messages: EmailMessage[]): Promise<void> {
      this.sentEmails.push(...messages);
    }

    // Méthodes utilitaires pour les tests
    getEmailsSentTo(email: string): EmailMessage[] {
      return this.sentEmails.filter(e => e.to === email);
    }

    reset(): void {
      this.sentEmails = [];
    }

    hasEmailWith(subject: string): boolean {
      return this.sentEmails.some(e => e.subject.includes(subject));
    }
  }

  // ═══════════════════════════════════════════════════════════════
  // adapters/secondary/payment/FakePaymentAdapter.ts
  // ADAPTER SECONDAIRE de paiement factice (Tests)
  // ═══════════════════════════════════════════════════════════════

  import { IPaymentPort, ChargeRequest, ChargeResult } from '../../../hexagon/application/ports/secondary/IPaymentPort';

  export class FakePaymentAdapter implements IPaymentPort {
    private _shouldFail: boolean = false;
    public charges: ChargeRequest[] = [];

    // Contrôle le comportement en test
    simulateFailure(fail: boolean): void {
      this._shouldFail = fail;
    }

    async charge(request: ChargeRequest): Promise<ChargeResult> {
      this.charges.push(request);

      if (this._shouldFail) {
        throw new Error('Paiement refusé (simulation)');
      }

      return {
        paymentId: `fake-payment-${Date.now()}`,
        status: 'succeeded'
      };
    }

    async refund(paymentId: string, amountInCents: number): Promise<void> {
      if (this._shouldFail) {
        throw new Error('Remboursement échoué (simulation)');
      }
      console.log(`[TEST PAYMENT] Remboursement de ${amountInCents} cents pour ${paymentId}`);
    }

    reset(): void {
      this.charges = [];
      this._shouldFail = false;
    }
  }

------------------------------------------------------------------------
4.5 TESTS D'INTÉGRATION AVEC LES FAUX ADAPTERS
------------------------------------------------------------------------

  // ═══════════════════════════════════════════════════════════════
  // tests/integration/CoursePublishing.test.ts
  // Tests d'intégration utilisant les faux adapters
  // AUCUNE connexion réseau ou base de données réelle !
  // ═══════════════════════════════════════════════════════════════

  import { CourseApplicationService } from '../../hexagon/application/services/CourseApplicationService';
  import { InMemoryCourseRepository } from '../../adapters/secondary/persistence/InMemoryCourseRepository';
  import { InMemoryEventAdapter } from '../../adapters/secondary/events/InMemoryEventAdapter';

  describe('Course Publishing Flow - Integration Tests', () => {
    let courseService: CourseApplicationService;
    let courseRepository: InMemoryCourseRepository;
    let eventAdapter: InMemoryEventAdapter;

    // Setup AVANT chaque test avec des adapters en mémoire
    beforeEach(() => {
      courseRepository = new InMemoryCourseRepository();
      eventAdapter = new InMemoryEventAdapter();

      courseService = new CourseApplicationService(
        courseRepository,
        eventAdapter
      );
    });

    it('devrait créer et publier un cours complet', async () => {
      // ── Arrange : Créer un cours
      const created = await courseService.createCourse({
        title: 'Introduction à TypeScript',
        instructorId: 'instructor-1',
        priceAmount: 39.99,
        priceCurrency: 'EUR',
        level: 'beginner'
      });

      expect(created.status).toBe('draft');

      // ── Arrange : Compléter le cours avec description et durée
      await courseService.updateCourseDescription(
        created.id,
        'Apprenez TypeScript depuis zéro. Ce cours couvre tous les fondamentaux ' +
        'du typage statique, les génériques, et les types avancés.',
        'instructor-1'
      );

      // Note : Dans un vrai Use Case, il y aurait aussi setDuration
      // On simule en modifiant directement le repository pour les tests
      const course = await courseRepository.findById(created.id);
      const withDuration = course!.withDuration(180);
      await courseRepository.save(withDuration);

      // ── Act : Publier le cours
      const published = await courseService.publishCourse({
        courseId: created.id,
        requestingUserId: 'instructor-1',
        requestingUserRole: 'instructor'
      });

      // ── Assert : Vérifier l'état
      expect(published.status).toBe('published');
      expect(published.publishedAt).toBeDefined();

      // Vérifier que l'événement a été dispatché
      const events = eventAdapter.dispatchedEvents;
      expect(events).toHaveLength(1);
      expect(events[0].eventName).toBe('CoursePublished');
    });

    it('devrait refuser la publication sans description', async () => {
      const created = await courseService.createCourse({
        title: 'Cours sans description',
        instructorId: 'instructor-1',
        priceAmount: 0,
        priceCurrency: 'EUR',
        level: 'beginner'
      });

      // Tenter de publier sans description
      await expect(
        courseService.publishCourse({
          courseId: created.id,
          requestingUserId: 'instructor-1',
          requestingUserRole: 'instructor'
        })
      ).rejects.toThrow('Description trop courte');

      // Aucun événement ne doit être dispatché
      expect(eventAdapter.dispatchedEvents).toHaveLength(0);
    });
  });

  // ═══════════════════════════════════════════════════════════════
  // adapters/secondary/persistence/InMemoryCourseRepository.ts
  // Repository en mémoire pour les tests
  // ═══════════════════════════════════════════════════════════════

  import { ICourseRepository, CourseQueryFilters, PaginatedCourses }
    from '../../../hexagon/application/ports/secondary/ICourseRepository';
  import { Course } from '../../../hexagon/domain/course/Course';

  export class InMemoryCourseRepository implements ICourseRepository {
    private store: Map<string, Course> = new Map();

    async findById(id: string): Promise<Course | null> {
      return this.store.get(id) || null;
    }

    async save(course: Course): Promise<Course> {
      this.store.set(course.id.value, course);
      return course;
    }

    async findAll(filters: CourseQueryFilters): Promise<PaginatedCourses> {
      let items = Array.from(this.store.values());

      if (filters.status) {
        items = items.filter(c => c.status === filters.status);
      }
      if (filters.level) {
        items = items.filter(c => c.level === filters.level);
      }
      if (filters.search) {
        const search = filters.search.toLowerCase();
        items = items.filter(c =>
          c.title.toLowerCase().includes(search) ||
          (c.description || '').toLowerCase().includes(search)
        );
      }

      const total = items.length;
      const page = filters.page || 1;
      const limit = filters.limit || 20;
      const start = (page - 1) * limit;
      const paginated = items.slice(start, start + limit);

      return { items: paginated, total, page, totalPages: Math.ceil(total / limit) };
    }

    async delete(id: string): Promise<void> {
      this.store.delete(id);
    }

    // Utilitaires de test
    count(): number { return this.store.size; }
    clear(): void { this.store.clear(); }
    getAll(): Course[] { return Array.from(this.store.values()); }
  }

------------------------------------------------------------------------
4.6 ASSEMBLAGE FINAL
------------------------------------------------------------------------

  // ═══════════════════════════════════════════════════════════════
  // infrastructure/di/container.ts
  // Assemblage production vs test
  // ═══════════════════════════════════════════════════════════════

  export function buildProductionContainer() {
    const { Pool } = require('pg');
    const pool = new Pool({ connectionString: process.env.DATABASE_URL });

    // Secondary Adapters (Production)
    const courseRepository = new PostgresCourseRepository(pool);
    const emailAdapter = new SendGridEmailAdapter(process.env.SENDGRID_KEY!);
    const paymentAdapter = new StripePaymentAdapter(process.env.STRIPE_KEY!);
    const eventAdapter = new KafkaEventAdapter(process.env.KAFKA_URL!);

    // Application Services (Hexagone)
    const courseService = new CourseApplicationService(courseRepository, eventAdapter);
    const enrollmentService = new EnrollmentApplicationService(
      courseRepository, enrollmentRepository, paymentAdapter, eventAdapter
    );

    // Primary Adapters
    const courseHttpAdapter = new CourseHttpAdapter(courseService);
    const enrollmentHttpAdapter = new EnrollmentHttpAdapter(enrollmentService);

    return { courseHttpAdapter, enrollmentHttpAdapter };
  }

  export function buildTestContainer() {
    // Tout en mémoire pour les tests
    const courseRepository = new InMemoryCourseRepository();
    const emailAdapter = new InMemoryEmailAdapter();
    const paymentAdapter = new FakePaymentAdapter();
    const eventAdapter = new InMemoryEventAdapter();

    const courseService = new CourseApplicationService(courseRepository, eventAdapter);
    const enrollmentService = new EnrollmentApplicationService(
      courseRepository, new InMemoryEnrollmentRepository(), paymentAdapter, eventAdapter
    );

    return { courseService, enrollmentService, emailAdapter, paymentAdapter, eventAdapter };
  }

================================================================================
CHAPITRE 5 : BONNES PRATIQUES
================================================================================

------------------------------------------------------------------------
5.1 NOMMAGE DES PORTS
------------------------------------------------------------------------

Les ports secondaires doivent être nommés selon leur INTENTION MÉTIER,
pas selon leur technologie.

  [X] MAUVAIS NOMMAGE :
    ISendGridAdapter, IPostgresRepository, IKafkaProducer

  [OK] BON NOMMAGE :
    IEmailPort, ICourseRepository, IEventPort
    
    Pourquoi ? Si vous changez de SendGrid à SES,
                le port s'appelle toujours IEmailPort.
                Seul l'adapter change.

------------------------------------------------------------------------
5.2 ANTI-CORRUPTION LAYER (ACL)
------------------------------------------------------------------------

Quand un adapter secondaire doit communiquer avec un système externe
avec un modèle différent, utiliser une couche anti-corruption.

  Exemple : Stripe utilise les montants en centimes et en lowercase currency.
            Votre domaine utilise des montants décimaux et en uppercase.
  
  // StripePaymentAdapter traduit votre domaine -> format Stripe
  async charge(request: ChargeRequest): Promise<ChargeResult> {
    // Anti-Corruption : adapte votre modèle au modèle Stripe
    const stripeResult = await this.stripe.paymentIntents.create({
      amount: request.amountInCents,                    // Déjà en centimes
      currency: request.currency.toLowerCase(),          // Stripe veut lowercase
      payment_method: request.token,
      confirm: true
    });
    
    // Anti-Corruption : traduit le résultat Stripe -> votre modèle
    return {
      paymentId: stripeResult.id,
      status: stripeResult.status === 'succeeded' ? 'succeeded' : 'failed'
    };
  }

------------------------------------------------------------------------
5.3 EVENT-DRIVEN DANS L'ARCHITECTURE HEXAGONALE
------------------------------------------------------------------------

Les événements du domaine transitent par le port IEventPort.

  // Port secondaire
  interface IEventPort {
    dispatch(event: DomainEvent): Promise<void>
  }

  // Adapter Kafka (production)
  class KafkaEventAdapter implements IEventPort {
    async dispatch(event: DomainEvent): Promise<void> {
      await this.kafka.produce({ topic: event.eventName, value: JSON.stringify(event) });
    }
  }

  // Adapter In-Memory (test + développement)
  class InMemoryEventAdapter implements IEventPort {
    public dispatchedEvents: DomainEvent[] = [];
    async dispatch(event: DomainEvent): Promise<void> {
      this.dispatchedEvents.push(event);
    }
  }

================================================================================
CHAPITRE 6 : ERREURS FRÉQUENTES
================================================================================

ERREUR 1 : TECHNOLOGIE DANS LE NOM DU PORT
  [X] interface ISendGridPort
  [OK] interface IEmailPort

ERREUR 2 : ADAPTER QUI CONTIENT DE LA LOGIQUE MÉTIER
  [X] CourseHttpAdapter qui valide les règles métier du cours
  [OK] CourseHttpAdapter qui traduit HTTP -> AppService uniquement
     La logique métier reste dans l'hexagone

ERREUR 3 : HEXAGONE QUI IMPORTE DES ADAPTERS
  [X] CourseApplicationService qui importe PostgresCourseRepository directement
  [OK] CourseApplicationService qui dépend de ICourseRepository (interface)
     L'implémentation est injectée depuis l'extérieur

ERREUR 4 : PAS DE FAKE ADAPTERS POUR LES TESTS
  Symptôme : Tests qui nécessitent une vraie base de données, vraiment Stripe, etc.
  Solution : InMemoryCourseRepository, FakePaymentAdapter -> Tests en millisecondes

ERREUR 5 : ADAPTER PRIMAIRE QUI ACCÈDE AUX PORTS SECONDAIRES
  [X] CourseHttpAdapter qui accède à PostgresCourseRepository
  [OK] CourseHttpAdapter -> ICourseService (port primaire) -> PostgresCourseRepository (secondaire)
     Les adapters primaires ne connaissent que les ports primaires

================================================================================
CHAPITRE 7 : EXERCICES
================================================================================

EXERCICES FACILES

EXERCICE 1 : Créer INotificationPort
  Définissez un port secondaire INotificationPort qui supporte :
  - Email (avec template et paramètres)
  - SMS (numéro + message)
  - Push Notification (token device + titre + body)
  Créez l'InMemoryNotificationAdapter avec tracking des notifications.

EXERCICE 2 : Adapter GraphQL
  Créez CourseGraphQLAdapter (primary adapter) qui expose la même logique
  via GraphQL (résolveurs) en utilisant ICourseService.
  Schema :
    type Query { course(id: ID!): Course, courses(level: String, page: Int): CourseList }
    type Mutation { createCourse(input: CreateCourseInput!): Course, publishCourse(id: ID!): Course }

EXERCICE 3 : Adapter CLI pour enrollment
  Créez EnrollmentCliAdapter avec les commandes :
    enroll <studentId> <courseId>
    list-enrolled <studentId>
    progress <enrollmentId>

EXERCICES INTERMÉDIAIRES

EXERCICE 4 : Hexagone complet pour les certificats
  Créez le module Certificate avec :
  Domain : Certificate entity (id, enrollmentId, userId, courseId, issuedAt, isRevoked)
  Application Port Primary : ICertificateService { issueCertificate(), revokeCertificate(), verifyCertificate(token) }
  Application Port Secondary : ICertificateRepository, IPDFPort, IQRCodePort
  Adapters Secondary : PdfKit adapter, FakePDFAdapter pour tests

EXERCICE 5 : Tests d'acceptation (BDD style)
  Écrivez des tests BDD pour l'enrollment :
  
  Given un étudiant avec 2 cours complétés (prérequis)
  And un cours payant avec ces prérequis
  When l'étudiant s'inscrit avec un token de paiement valide
  Then l'enrollment est créé avec statut 'active'
  And un email de confirmation est envoyé
  And l'événement EnrollmentCreated est dispatché

  Utilisez les faux adapters pour les tests.

EXERCICE 6 : Adapter Webhook pour Stripe
  Stripe peut envoyer des webhooks (événements post-paiement).
  Créez un StripeWebhookAdapter (primary adapter) qui :
  - Reçoit les webhooks Stripe POST /webhooks/stripe
  - Vérifie la signature Stripe
  - Traduit les événements Stripe -> événements de votre domaine
  - Appelle IPaymentEventService (port primaire) pour traiter

EXERCICES AVANCÉS

EXERCICE 7 : Multi-tenant avec l'architecture hexagonale
  EduConnect veut supporter plusieurs "tenants" (entreprises clientes).
  Chaque tenant a son propre espace de données.
  
  Modifiez l'architecture hexagonale pour supporter :
  - TenantContext injecté dans chaque requête
  - MultiTenantCourseRepository qui filtre par tenant
  - Les ports primaires reçoivent le tenantId dans les commandes
  
  Assurez-vous que l'hexagone reste propre (le domain ne connaît pas les tenants).

EXERCICE 8 : Sync vs Async ports
  Actuellement, tous les ports sont synchrones (async/await).
  Certaines opérations bénéficieraient d'être vraiment asynchrones :
  - Envoi d'email -> Fire and forget
  - Génération PDF -> Long background job
  
  Modifiez IEmailPort et IPDFPort pour supporter :
  - Envoi immédiat synchrone
  - Envoi en background (retourne un jobId)
  - Récupération du statut du job (polling)

EXERCICE 9 : Architecture hexagonale en Python avec FastAPI
  Réimplémentez le module Course en Python :
  - Domain : dataclass Course avec méthodes métier
  - Ports : Protocol classes (Python 3.8+)
  - Adapters : SQLAlchemy + AsyncPG pour DB, httpx pour APIs externes
  - Primary adapter : FastAPI router
  
  Comparez la verbosité et l'expressivité Python vs TypeScript.

================================================================================
RÉCAPITULATIF — ARCHITECTURE HEXAGONALE
================================================================================

Points clés :

1. L'hexagone est isolé au centre. Tout ce qui est extérieur est un adapter.

2. PORTS PRIMAIRES : Ce que l'hexagone expose (ICourseService, IEnrollmentService).
   PORTS SECONDAIRES : Ce que l'hexagone requiert (ICourseRepository, IEmailPort).

3. ADAPTERS PRIMAIRES : HTTP, GraphQL, CLI, Tests -> Appellent les ports primaires.
   ADAPTERS SECONDAIRES : PostgreSQL, SendGrid, Stripe -> Implémentent les ports secondaires.

4. TESTABILITÉ MAXIMALE : Remplacer tous les adapters secondaires par des adapters
   en mémoire (InMemory*, Fake*) pour des tests rapides sans infrastructure.

5. INDÉPENDANCE TECHNOLOGIQUE : Changer de technologie = créer un nouvel adapter.
   L'hexagone ne change pas.

6. MÊME LOGIQUE, MULTIPLE INTERFACES : REST + GraphQL + CLI = 3 adapters primaires,
   même application service (hexagone).

7. NOMMER LES PORTS PAR INTENTION MÉTIER, pas par technologie.

Prochaine étape :
  Volume 8 : Architecture Événementielle (Event-Driven)
  -> Découplage maximal via les événements
  -> Event Sourcing, CQRS, Saga

================================================================================
FIN DU VOLUME 7 — ARCHITECTURE HEXAGONALE
Prochaine étape -> architecture_event_driven.txt
================================================================================

