# [DOCS] Projet Fil Rouge Spring Boot — LibraryHub API

> **Un guide complet, progressif et pratique pour maîtriser Spring Boot en partant de zéro.**

---

## [OBJECTIF] Le Projet : LibraryHub API

Tu vas construire **LibraryHub**, une API REST complète de gestion de bibliothèque. Ce projet réel te permettra d'apprendre chaque concept de Spring Boot dans son **contexte concret**, et non dans l'abstrait.

### Ce que tu vas construire

Une API backend permettant de :
- Gérer des **livres**, des **auteurs** et des **catégories**
- Gérer les **membres** de la bibliothèque
- Gérer les **emprunts** (qui a emprunté quel livre, quand, retour prévu)
- Sécuriser l'accès avec **JWT** (authentification + rôles)
- Supporter la **recherche**, la **pagination**, et le **cache**
- Être **testée**, **monitorée** et **déployée** avec Docker

---

## [DOSSIER] Structure des fichiers du guide

| Fichier | Contenu | Parties couvertes |
|--------|---------|-------------------|
| `00_INDEX.md` | Ce fichier — vue d'ensemble | — |
| `01_FONDATIONS.md` | Spring, IoC, DI, Spring Boot | Parties 1–2 |
| `02_WEB_REST_API.md` | Spring MVC, REST, Validation | Partie 3 |
| `03_JPA_PERSISTENCE.md` | JPA, Hibernate, Spring Data, Transactions | Partie 4 |
| `04_SECURITE.md` | Spring Security, JWT, Rôles | Partie 5 |
| `05_ARCHITECTURE.md` | Couches, Exceptions, Logging | Partie 6 |
| `06_TESTS.md` | JUnit 5, Mockito, Tests d'intégration | Partie 7 |
| `07_PERFORMANCE_AVANCE.md` | Cache, Async, Microservices, Docker | Parties 8–10 |
| `08_PROJET_FINAL.md` | Assemblage complet + Checklist finale | Partie 11 |

---

## [CONSTRUCTION] Architecture finale du projet

```
libraryhub/
├── src/
│   ├── main/
│   │   ├── java/com/libraryhub/
│   │   │   ├── LibraryHubApplication.java
│   │   │   ├── config/
│   │   │   │   ├── SecurityConfig.java
│   │   │   │   ├── JwtConfig.java
│   │   │   │   └── CacheConfig.java
│   │   │   ├── controller/
│   │   │   │   ├── BookController.java
│   │   │   │   ├── AuthorController.java
│   │   │   │   ├── MemberController.java
│   │   │   │   ├── LoanController.java
│   │   │   │   └── AuthController.java
│   │   │   ├── service/
│   │   │   │   ├── BookService.java
│   │   │   │   ├── AuthorService.java
│   │   │   │   ├── MemberService.java
│   │   │   │   ├── LoanService.java
│   │   │   │   └── AuthService.java
│   │   │   ├── repository/
│   │   │   │   ├── BookRepository.java
│   │   │   │   ├── AuthorRepository.java
│   │   │   │   ├── MemberRepository.java
│   │   │   │   └── LoanRepository.java
│   │   │   ├── entity/
│   │   │   │   ├── Book.java
│   │   │   │   ├── Author.java
│   │   │   │   ├── Member.java
│   │   │   │   ├── Loan.java
│   │   │   │   └── User.java
│   │   │   ├── dto/
│   │   │   │   ├── BookDTO.java
│   │   │   │   ├── BookCreateRequest.java
│   │   │   │   ├── MemberDTO.java
│   │   │   │   ├── LoanDTO.java
│   │   │   │   └── AuthRequest.java
│   │   │   ├── mapper/
│   │   │   │   ├── BookMapper.java
│   │   │   │   └── MemberMapper.java
│   │   │   ├── exception/
│   │   │   │   ├── GlobalExceptionHandler.java
│   │   │   │   ├── BookNotFoundException.java
│   │   │   │   └── LoanAlreadyActiveException.java
│   │   │   └── security/
│   │   │       ├── JwtUtil.java
│   │   │       ├── JwtFilter.java
│   │   │       └── UserDetailsServiceImpl.java
│   │   └── resources/
│   │       ├── application.yml
│   │       ├── application-dev.yml
│   │       └── application-prod.yml
│   └── test/
│       └── java/com/libraryhub/
│           ├── service/BookServiceTest.java
│           └── controller/BookControllerTest.java
├── Dockerfile
├── docker-compose.yml
└── pom.xml
```

---

## [OUTILS] Stack technique

| Technologie | Version | Rôle |
|-------------|---------|------|
| Java | 17 | Langage |
| Spring Boot | 3.2.x | Framework principal |
| Spring Data JPA | inclus | Persistance |
| Spring Security | inclus | Sécurité |
| PostgreSQL | 15 | Base de données prod |
| H2 | inclus | Base de données tests |
| JWT (jjwt) | 0.11.5 | Authentification stateless |
| MapStruct | 1.5.5 | Mapping Entity <-> DTO |
| Lombok | inclus | Réduction boilerplate |
| JUnit 5 + Mockito | inclus | Tests |
| Docker | latest | Déploiement |

---

## [RAPIDE] Comment utiliser ce guide

1. **Lis chaque fichier dans l'ordre** — chaque chapitre s'appuie sur le précédent
2. **Code en même temps** — ne lis pas passivement, crée le projet en parallèle
3. **Comprends avant de copier** — chaque bloc de code est accompagné d'explications
4. **Fais les exercices** — chaque chapitre se termine par des exercices pratiques
5. **Le projet final** est le vrai test — assemble tout sans aide puis compare

---

## [RAPIDE] Démarrage rapide — Créer le projet

Rends-toi sur [start.spring.io](https://start.spring.io) et configure :

- **Project** : Maven
- **Language** : Java
- **Spring Boot** : 3.2.x
- **Group** : `com.libraryhub`
- **Artifact** : `libraryhub`
- **Java** : 17

**Dépendances à ajouter :**
- Spring Web
- Spring Data JPA
- Spring Security
- PostgreSQL Driver
- H2 Database
- Lombok
- Validation
- Spring Boot Actuator

> -> Continue avec `01_FONDATIONS.md`


# [LIVRE] Module 01 — Fondations Spring & Configuration

> **Parties couvertes :** Partie 1 (Chapitres 1–3) + Partie 2 (Chapitres 4–5)
> **Projet fil rouge :** Mise en place du projet LibraryHub de A à Z

---

## Chapitre 1 — L'Écosystème Spring

### [REFLEXION] Pourquoi Spring existe-t-il ?

Avant Spring (années 2000), développer une application Java d'entreprise signifiait utiliser **Java EE** (Enterprise Edition). C'était lourd, complexe, et demandait d'écrire énormément de code répétitif (boilerplate) juste pour faire fonctionner des choses simples.

**Les problèmes concrets que Spring résout :**

| Problème avant Spring | Solution Spring |
|-----------------------|----------------|
| Créer et gérer manuellement les objets et leurs dépendances | IoC Container gère les objets pour toi |
| Connexions base de données verboses | Spring Data simplifie tout |
| Sécurité complexe à implémenter | Spring Security clé en main |
| Tests difficiles à écrire | Architecture testable par design |
| Configuration XML interminable | Auto-configuration, annotations |

### 🆚 Spring vs Spring Boot — La différence fondamentale

Beaucoup de débutants confondent les deux. Voici la vérité simple :

```
Spring Framework  =  Le moteur
Spring Boot       =  La voiture toute montée avec le moteur dedans
```

**Spring Framework** est un ensemble de modules (Core, MVC, Security, Data…). Tu peux l'utiliser, mais tu dois tout configurer toi-même : le serveur, les dépendances, les configurations XML ou Java…

**Spring Boot** est une **surcouche** de Spring Framework qui :
- Configure automatiquement tout ce dont tu as besoin (**auto-configuration**)
- Embarque un serveur web (Tomcat) directement dans ton JAR
- Fournit des **starters** (paquets de dépendances prêts à l'emploi)
- Te permet de démarrer en quelques minutes au lieu de quelques heures

### [CLASSICAL_BUILDING] Architecture globale de Spring

```
┌─────────────────────────────────────────────────────────────┐
│                        Spring Boot                          │
│  (Auto-configuration + Starters + Embedded Server)          │
├─────────────────────────────────────────────────────────────┤
│                      Spring Framework                        │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐   │
│  │  Spring  │  │  Spring  │  │  Spring  │  │  Spring  │   │
│  │   Core   │  │   Web/   │  │ Security │  │   Data   │   │
│  │  (IoC)   │  │   MVC    │  │          │  │   JPA    │   │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘   │
└─────────────────────────────────────────────────────────────┘
```

---

## Chapitre 2 — Inversion de Contrôle (IoC) & Dependency Injection

### [LOGIQUE] IoC expliqué avec LibraryHub

Imaginons que tu crées un `BookService` qui a besoin d'un `BookRepository` pour accéder à la base de données.

**Sans IoC (le mauvais chemin) :**

```java
// [X] Tu crées toi-même les objets — couplage fort
public class BookService {
    
    private BookRepository bookRepository;
    
    public BookService() {
        // BookService CRÉE lui-même BookRepository
        // Problème : si BookRepository change (ex: nouveau constructeur), 
        // tu dois modifier BookService aussi
        this.bookRepository = new BookRepository(); // <- problème ici
    }
    
    public Book findById(Long id) {
        return bookRepository.findById(id);
    }
}
```

**Avec IoC (la bonne approche Spring) :**

```java
// [OK] Spring crée et injecte BookRepository automatiquement
@Service  // <- Spring sait que c'est un bean à gérer
public class BookService {
    
    private final BookRepository bookRepository;
    
    // Spring INJECTE le BookRepository ici — tu ne le crées pas toi-même
    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }
    
    public Book findById(Long id) {
        return bookRepository.findById(id).orElseThrow();
    }
}
```

> **Le principe IoC en une phrase :** Ce n'est plus TON code qui contrôle la création des objets, c'est le **conteneur Spring** qui le fait à ta place. L'inversion, c'est ça.

### [SYRINGE] Dependency Injection — Les 3 types

#### Type 1 : Injection par constructeur [OK] (Recommandée)

```java
@Service
public class BookService {
    
    private final BookRepository bookRepository;
    private final AuthorService authorService;
    
    // Injection par constructeur — Spring remplit les paramètres
    // Avantage : les dépendances sont obligatoires et immuables (final)
    // Avantage : facilite les tests unitaires (tu passes des mocks)
    public BookService(BookRepository bookRepository, AuthorService authorService) {
        this.bookRepository = bookRepository;
        this.authorService = authorService;
    }
}
```

#### Type 2 : Injection par setter (moins utilisée)

```java
@Service
public class BookService {
    
    private BookRepository bookRepository;
    
    // Utilisé pour des dépendances optionnelles
    @Autowired
    public void setBookRepository(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }
}
```

#### Type 3 : Injection par champ avec `@Autowired` [ATTENTION] (Déconseillée)

```java
@Service
public class BookService {
    
    @Autowired  // <- Spring injecte directement dans le champ
    private BookRepository bookRepository;
    // Problème : impossible de faire final, difficile à tester
}
```

> **Règle d'or :** Utilise toujours l'**injection par constructeur**. Lombok avec `@RequiredArgsConstructor` la génère automatiquement.

### [BEANS] Les Beans — Qu'est-ce que c'est ?

Un **bean** est simplement un objet Java géré par le conteneur Spring. Spring :
1. Crée le bean au démarrage
2. Injecte ses dépendances
3. Le garde en vie pendant l'application
4. Le détruit à l'arrêt

```java
// Ces annotations disent à Spring "crée un bean de cette classe"
@Component   // Bean générique
@Service     // Bean de couche service (sémantique)
@Repository  // Bean de couche données (sémantique + gestion exceptions)
@Controller  // Bean de couche web
```

### [USINE] ApplicationContext vs BeanFactory

```
BeanFactory      = Le conteneur basique (lazy — crée les beans à la demande)
ApplicationContext = BeanFactory enrichi (eager — crée les beans au démarrage)
                   + support des events
                   + internationalisation
                   + intégration Spring MVC
```

Dans **LibraryHub** (et dans 99% des apps Spring Boot), tu utiliseras toujours `ApplicationContext`. Spring Boot le crée automatiquement.

---

## Chapitre 3 — Spring Boot en profondeur

### [CONFIG] Auto-configuration — La magie expliquée

Quand tu ajoutes `spring-boot-starter-data-jpa` à ton `pom.xml`, Spring Boot détecte automatiquement la présence de JPA et configure :
- Un `EntityManagerFactory`
- Un `DataSource`
- Un `TransactionManager`
- Les repositories Spring Data

**Comment ça marche concrètement ?**

```java
// Quelque part dans Spring Boot (tu n'as pas à écrire ça)
@ConditionalOnClass(JpaRepository.class)  // "Si JPA est dans le classpath..."
@ConditionalOnMissingBean(DataSource.class) // "...et qu'aucun DataSource n'est défini..."
@Configuration
public class JpaAutoConfiguration {
    // ...alors, je configure tout automatiquement !
}
```

Tu peux voir toutes les auto-configurations actives en ajoutant ceci dans `application.yml` :
```yaml
logging:
  level:
    org.springframework.boot.autoconfigure: DEBUG
```

### [PACKAGE] Les Starters — Des dépendances groupées

Un starter est un groupe de dépendances Maven/Gradle cohérentes. Au lieu d'ajouter 8 dépendances séparément, tu en ajoutes une seule.

```xml
<!-- Dans pom.xml de LibraryHub -->

<!-- Un seul starter remplace: spring-webmvc, jackson-databind, 
     tomcat-embed-core, validation-api, etc. -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Remplace: hibernate-core, spring-data-jpa, 
     hibernate-validator, etc. -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- Remplace: spring-security-core, spring-security-web, 
     spring-security-config, etc. -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
```

### [ACCUEIL] `@SpringBootApplication` — Le point d'entrée

```java
package com.libraryhub;

// @SpringBootApplication est une combinaison de 3 annotations :
// @Configuration       -> cette classe peut définir des beans
// @EnableAutoConfiguration -> active l'auto-configuration Spring Boot
// @ComponentScan       -> scanne le package et sous-packages pour les beans
@SpringBootApplication
public class LibraryHubApplication {
    
    public static void main(String[] args) {
        // Lance le contexte Spring, démarre Tomcat, tout s'initialise
        SpringApplication.run(LibraryHubApplication.class, args);
        System.out.println("[RAPIDE] LibraryHub API is running!");
    }
}
```

> **Important :** Place cette classe à la **racine de ton package** (`com.libraryhub`). Le `@ComponentScan` scanne tous les sous-packages automatiquement. Si tu la mets dans un sous-package, certains beans ne seront pas détectés.

---

## Chapitre 4 — Fichiers de configuration

### [FICHIER] `application.properties` vs `application.yml`

Les deux formats font exactement la même chose. `YAML` est plus lisible pour les configurations hiérarchiques. LibraryHub utilisera `YAML`.

**Comparaison côte à côte :**

```properties
# application.properties (plat, répétitif)
spring.datasource.url=jdbc:postgresql://localhost:5432/libraryhub
spring.datasource.username=postgres
spring.datasource.password=secret
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
```

```yaml
# application.yml (hiérarchique, lisible)
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/libraryhub
    username: postgres
    password: secret
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
```

### [OUTIL] Configuration complète de LibraryHub

```yaml
# src/main/resources/application.yml

spring:
  application:
    name: libraryhub
  
  # Base de données (sera surchargée par les profils)
  datasource:
    url: jdbc:h2:mem:libraryhub  # H2 en mémoire par défaut
    driver-class-name: org.h2.Driver
    username: sa
    password: ""
  
  # Console H2 (utile pour le développement)
  h2:
    console:
      enabled: true
      path: /h2-console
  
  # JPA / Hibernate
  jpa:
    hibernate:
      ddl-auto: create-drop  # Recrée le schéma à chaque démarrage
    show-sql: true           # Affiche les requêtes SQL dans les logs
    properties:
      hibernate:
        format_sql: true     # SQL lisible dans les logs

# Port du serveur
server:
  port: 8080

# Configuration custom pour LibraryHub
libraryhub:
  jwt:
    secret: "monSecretTresLongPourJWT123456789"
    expiration: 86400000     # 24h en millisecondes
  loan:
    max-duration-days: 21    # Durée max d'un emprunt
  
# Logging
logging:
  level:
    com.libraryhub: DEBUG
    org.hibernate.SQL: DEBUG
```

### [MONDE] Profils — dev, test, prod

Les profils permettent d'avoir des configurations différentes selon l'environnement. Spring active le profil via :
- La variable d'environnement `SPRING_PROFILES_ACTIVE=prod`
- La propriété `spring.profiles.active=dev`
- Le paramètre de démarrage `--spring.profiles.active=prod`

```yaml
# src/main/resources/application-dev.yml
# Activé avec : spring.profiles.active=dev

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/libraryhub_dev
    username: postgres
    password: devpassword
  jpa:
    hibernate:
      ddl-auto: update   # Met à jour le schéma sans le recréer
    show-sql: true

logging:
  level:
    com.libraryhub: DEBUG
    org.hibernate.SQL: DEBUG
```

```yaml
# src/main/resources/application-prod.yml
# Activé avec : SPRING_PROFILES_ACTIVE=prod

spring:
  datasource:
    url: ${DATABASE_URL}        # Lire depuis variable d'environnement
    username: ${DATABASE_USER}
    password: ${DATABASE_PASSWORD}
  jpa:
    hibernate:
      ddl-auto: validate        # Valide le schéma sans le modifier
    show-sql: false             # Pas de SQL dans les logs en prod

logging:
  level:
    com.libraryhub: INFO        # Moins verbeux en production
    root: WARN
```

```yaml
# src/main/resources/application-test.yml
# Activé automatiquement dans les tests avec @ActiveProfiles("test")

spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: create-drop
```

### [LABEL] `@ConfigurationProperties` — Lier la config à des classes Java

Au lieu de lire les propriétés une par une avec `@Value("${libraryhub.jwt.secret}")`, tu peux les regrouper dans une classe typée.

```java
package com.libraryhub.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

// Lie toutes les propriétés préfixées "libraryhub.jwt" à cette classe
@ConfigurationProperties(prefix = "libraryhub.jwt")
@Component
public class JwtProperties {
    
    private String secret;
    private long expiration;
    
    // Getters et setters requis pour le binding
    public String getSecret() { return secret; }
    public void setSecret(String secret) { this.secret = secret; }
    
    public long getExpiration() { return expiration; }
    public void setExpiration(long expiration) { this.expiration = expiration; }
}
```

```java
// Utilisation dans un service
@Service
public class JwtService {
    
    private final JwtProperties jwtProperties;
    
    public JwtService(JwtProperties jwtProperties) {
        this.jwtProperties = jwtProperties;
    }
    
    public String generateToken(String username) {
        // jwtProperties.getSecret() -> "monSecretTresLongPourJWT123456789"
        // jwtProperties.getExpiration() -> 86400000
        return Jwts.builder()
            .setSubject(username)
            .setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getExpiration()))
            .signWith(Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes()))
            .compact();
    }
}
```

---

## Chapitre 5 — Gestion des beans avancée

### [LABEL] Les annotations stéréotypes

Spring utilise des annotations pour identifier les beans et leur rôle :

```java
// @Component — Bean générique, aucune sémantique particulière
@Component
public class IsbnGenerator {
    public String generate() {
        return "ISBN-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
    }
}

// @Service — Bean de logique métier
// Sémantiquement, indique "je suis dans la couche service"
// Techniquement identique à @Component, mais plus expressif
@Service
public class BookService {
    // Logique métier ici
}

// @Repository — Bean de couche données
// En plus de @Component, ajoute la traduction des exceptions de base de données
// en exceptions Spring (DataAccessException)
@Repository
public interface BookRepository extends JpaRepository<Book, Long> {
    // Spring Data génère l'implémentation automatiquement
}

// @Controller / @RestController — Bean de couche web
@RestController
public class BookController {
    // Gestion des requêtes HTTP
}
```

### [OUTIL] `@Configuration` et `@Bean` — Beans déclarés manuellement

Parfois tu dois créer des beans pour des classes que tu ne peux pas annoter (classes de bibliothèques tierces, configuration complexe).

```java
package com.libraryhub.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration  // Indique que cette classe définit des beans
public class AppConfig {
    
    // @Bean : Spring appelle cette méthode et enregistre le résultat comme bean
    // Le nom du bean est "objectMapper" (nom de la méthode)
    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        // Configure Jackson pour gérer les dates Java 8 (LocalDate, etc.)
        mapper.registerModule(new JavaTimeModule());
        return mapper;
    }
    
    // Bean conditionnel : créé seulement si une propriété est vraie
    @Bean
    @ConditionalOnProperty(name = "libraryhub.demo.enabled", havingValue = "true")
    public DataSeeder dataSeeder(BookRepository bookRepository) {
        return new DataSeeder(bookRepository);
    }
}
```

### [SYNC] Scopes des beans

Le **scope** définit combien d'instances du bean Spring crée.

```java
// SINGLETON (défaut) — Une seule instance partagée partout
// Convient pour : services, repositories (sans état mutable entre requêtes)
@Service
@Scope("singleton")  // Inutile de le spécifier, c'est le défaut
public class BookService { ... }

// PROTOTYPE — Nouvelle instance à chaque injection
// Convient pour : objets avec état temporaire
@Component
@Scope("prototype")
public class LoanBuilder {
    private Loan loan = new Loan();  // Chaque injection a sa propre instance
    
    public LoanBuilder forBook(Book book) {
        loan.setBook(book);
        return this;
    }
    
    public Loan build() { return loan; }
}

// REQUEST — Une instance par requête HTTP (web uniquement)
// Convient pour : contexte de requête (utilisateur courant, etc.)
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
    private String currentUsername;
    // Nouvelle instance pour chaque requête HTTP entrante
}
```

### [TEMPS] Cycle de vie d'un bean dans LibraryHub

```java
@Service
@Slf4j  // Lombok : génère un logger "log"
public class BookService implements InitializingBean, DisposableBean {
    
    private final BookRepository bookRepository;
    private Map<String, Long> categoryCountCache;
    
    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
        log.info("1⃣ Constructeur appelé — bean instancié");
    }
    
    // Appelé après injection des dépendances
    @PostConstruct
    public void init() {
        log.info("2⃣ @PostConstruct — Initialisation du cache des catégories");
        categoryCountCache = new HashMap<>();
        // Précharge quelques statistiques
    }
    
    // Alternative : implémenter InitializingBean
    @Override
    public void afterPropertiesSet() {
        log.info("3⃣ afterPropertiesSet — Bean prêt à l'emploi");
    }
    
    // Appelé avant la destruction du bean
    @PreDestroy
    public void cleanup() {
        log.info("4⃣ @PreDestroy — Nettoyage du cache");
        categoryCountCache.clear();
    }
    
    // Méthiers de service
    public List<Book> findAll() {
        return bookRepository.findAll();
    }
}
```

---

## [OUTILS] Mise en pratique — Initialiser LibraryHub

### Étape 1 : Créer le projet

Depuis [start.spring.io](https://start.spring.io) avec les dépendances listées dans `00_INDEX.md`, ou avec Maven :

```bash
# Structure de dossiers à créer manuellement
mkdir -p src/main/java/com/libraryhub/{config,controller,service,repository,entity,dto,mapper,exception,security}
mkdir -p src/main/resources
mkdir -p src/test/java/com/libraryhub/{service,controller}
```

### Étape 2 : Le `pom.xml` complet

```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
    </parent>
    
    <groupId>com.libraryhub</groupId>
    <artifactId>libraryhub</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>LibraryHub API</name>
    
    <properties>
        <java.version>17</java.version>
        <jjwt.version>0.11.5</jjwt.version>
        <mapstruct.version>1.5.5.Final</mapstruct.version>
    </properties>
    
    <dependencies>
        <!-- Spring Boot Starters -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        
        <!-- Base de données -->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- JWT -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>${jjwt.version}</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>${jjwt.version}</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>${jjwt.version}</version>
            <scope>runtime</scope>
        </dependency>
        
        <!-- Utilitaires -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>${mapstruct.version}</version>
        </dependency>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-processor</artifactId>
            <version>${mapstruct.version}</version>
            <scope>provided</scope>
        </dependency>
        
        <!-- Tests -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
```

### Étape 3 : Vérifier que ça démarre

```bash
mvn spring-boot:run
```

Tu devrais voir dans les logs :
```
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
...
Started LibraryHubApplication in 2.345 seconds
```

---

## [OK] Exercices du Module 01

1. **Exercice IoC :** Crée une classe `RecommendationService` qui dépend de `BookService`. Injecte-la par constructeur. Ajoute un log dans le constructeur pour voir quand Spring instancie les beans.

2. **Exercice Profils :** Ajoute une propriété `libraryhub.max-books-per-member` valant `5` en dev et `3` en prod. Lis-la avec `@ConfigurationProperties` dans une classe `LibraryHubProperties`.

3. **Exercice Bean :** Crée un bean `Clock` dans une classe `@Configuration` qui retourne un `java.time.Clock`. Injecte-le dans `BookService` (utilisé pour calculer les dates d'emprunt).

4. **Défi :** Démarre l'application avec le profil `dev` et observe les logs SQL. Quelle différence avec le profil par défaut ?

---

> -> Continue avec `02_WEB_REST_API.md`


# [LIVRE] Module 02 — Spring Web & REST API

> **Parties couvertes :** Partie 3 (Chapitres 6–8)
> **Projet fil rouge :** Créer les endpoints REST de LibraryHub (Books, Authors, Members)

---

## Chapitre 6 — Spring MVC

### [CONSTRUCTION] Architecture MVC dans LibraryHub

**MVC** = **M**odel **V**iew **C**ontroller. Dans une API REST, la "Vue" est remplacée par du JSON.

```
Requête HTTP entrante
        │
        [BLACK_DOWN-POINTING_TRIANGLE]
┌──────────────────────────────────────────────────┐
│              DispatcherServlet                    │
│         (Aiguilleur central de Spring MVC)        │
└──────────────┬───────────────────────────────────┘
               │ Route vers le bon Controller
               [BLACK_DOWN-POINTING_TRIANGLE]
┌──────────────────────────┐
│  BookController          │ <- Reçoit la requête, valide, délègue
│  @RestController         │
│  GET /api/books/{id}     │
└──────────┬───────────────┘
           │ Appelle
           [BLACK_DOWN-POINTING_TRIANGLE]
┌──────────────────────────┐
│  BookService             │ <- Logique métier
│  @Service                │
└──────────┬───────────────┘
           │ Appelle
           [BLACK_DOWN-POINTING_TRIANGLE]
┌──────────────────────────┐
│  BookRepository          │ <- Accès aux données
│  @Repository             │
└──────────┬───────────────┘
           │ SQL
           [BLACK_DOWN-POINTING_TRIANGLE]
┌──────────────────────────┐
│  Base de données         │
└──────────────────────────┘
```

### 🆚 `@Controller` vs `@RestController`

```java
// @Controller — Retourne des vues HTML (Thymeleaf, JSP…)
// Pas utilisé dans une API REST pure
@Controller
public class OldController {
    
    @GetMapping("/books")
    public String listBooks(Model model) {
        model.addAttribute("books", bookService.findAll());
        return "books/list";  // -> résout vers books/list.html
    }
}

// @RestController = @Controller + @ResponseBody sur chaque méthode
// Retourne directement du JSON — utilisé dans LibraryHub
@RestController
public class BookController {
    
    @GetMapping("/api/books")
    public List<BookDTO> listBooks() {
        // Le retour est automatiquement sérialisé en JSON par Jackson
        return bookService.findAll();
    }
}
```

### [MOTORWAY] `@RequestMapping` — Définir les routes

```java
package com.libraryhub.controller;

import org.springframework.web.bind.annotation.*;

// Préfixe commun à tous les endpoints de ce controller
@RestController
@RequestMapping("/api/books")
public class BookController {
    
    private final BookService bookService;
    
    public BookController(BookService bookService) {
        this.bookService = bookService;
    }
    
    // GET /api/books
    @GetMapping
    public ResponseEntity<List<BookDTO>> getAllBooks() {
        return ResponseEntity.ok(bookService.findAll());
    }
    
    // GET /api/books/42
    @GetMapping("/{id}")
    public ResponseEntity<BookDTO> getBookById(@PathVariable Long id) {
        return ResponseEntity.ok(bookService.findById(id));
    }
    
    // GET /api/books/search?title=Harry&author=Rowling
    @GetMapping("/search")
    public ResponseEntity<List<BookDTO>> searchBooks(
            @RequestParam(required = false) String title,
            @RequestParam(required = false) String author) {
        return ResponseEntity.ok(bookService.search(title, author));
    }
    
    // POST /api/books
    @PostMapping
    public ResponseEntity<BookDTO> createBook(@RequestBody @Valid BookCreateRequest request) {
        BookDTO created = bookService.create(request);
        // 201 Created avec l'URL du nouveau livre dans le header Location
        URI location = URI.create("/api/books/" + created.getId());
        return ResponseEntity.created(location).body(created);
    }
    
    // PUT /api/books/42
    @PutMapping("/{id}")
    public ResponseEntity<BookDTO> updateBook(
            @PathVariable Long id,
            @RequestBody @Valid BookUpdateRequest request) {
        return ResponseEntity.ok(bookService.update(id, request));
    }
    
    // DELETE /api/books/42
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteBook(@PathVariable Long id) {
        bookService.delete(id);
        return ResponseEntity.noContent().build();  // 204 No Content
    }
}
```

---

## Chapitre 7 — Construction d'une API REST

### [MESURE] Design RESTful — Les bonnes pratiques

| Principe | [X] Mauvais | [OK] Bon |
|----------|-----------|-------|
| Nommage des routes | `/getBooks`, `/createBook` | `/books` |
| Verbes HTTP | POST `/deleteBook/1` | DELETE `/books/1` |
| Ressources imbriquées | `/getBooksByAuthor?id=1` | `/authors/1/books` |
| Codes HTTP | Toujours 200 | 200, 201, 204, 400, 404, 500 |
| Pluriel | `/book` | `/books` |

**Codes HTTP utilisés dans LibraryHub :**

```
200 OK          -> GET/PUT réussi, retourne une ressource
201 Created     -> POST réussi, retourne la nouvelle ressource
204 No Content  -> DELETE réussi, rien à retourner
400 Bad Request -> Données invalides (validation échouée)
401 Unauthorized-> Non authentifié
403 Forbidden   -> Authentifié mais pas autorisé
404 Not Found   -> Ressource inexistante
409 Conflict    -> Conflit (ex: livre déjà emprunté)
500 Server Error-> Erreur interne (jamais volontaire)
```

### [PACKAGE] DTO — Data Transfer Object

Un DTO est un objet qui définit **exactement** ce que tu envoies ou reçois via l'API. Il ne correspond pas forcément à ton entité JPA.

**Pourquoi des DTOs ?**
- Éviter d'exposer des champs sensibles (mot de passe, données internes)
- Contrôler précisément le format d'entrée/sortie
- Découpler la couche API de la couche données
- Adapter les données pour des cas d'usage spécifiques

```java
package com.libraryhub.dto;

import lombok.Data;
import lombok.Builder;
import java.time.LocalDate;

// DTO de RÉPONSE — Ce que l'API retourne au client
@Data
@Builder
public class BookDTO {
    private Long id;
    private String title;
    private String isbn;
    private String authorName;   // Nom de l'auteur (dénormalisé pour la lisibilité)
    private String categoryName;
    private LocalDate publishedDate;
    private boolean available;   // Calculé : pas de prêt actif
    
    // Pas de champ "loans" ou "author.email" — on ne surexpose pas
}

// DTO de CRÉATION — Ce que le client envoie pour créer un livre
@Data
public class BookCreateRequest {
    
    @NotBlank(message = "Le titre est obligatoire")
    @Size(max = 255, message = "Le titre ne peut pas dépasser 255 caractères")
    private String title;
    
    @NotBlank(message = "L'ISBN est obligatoire")
    @Pattern(regexp = "^(?:ISBN(?:-13)?:? )?(?=[0-9]{13}$|(?=(?:[0-9]+[- ]){4})[- 0-9]{17}$)...",
             message = "Format ISBN invalide")
    private String isbn;
    
    @NotNull(message = "L'ID de l'auteur est obligatoire")
    private Long authorId;
    
    @NotNull(message = "L'ID de la catégorie est obligatoire")
    private Long categoryId;
    
    private LocalDate publishedDate;
}

// DTO de MISE À JOUR
@Data
public class BookUpdateRequest {
    
    @NotBlank
    @Size(max = 255)
    private String title;
    
    private LocalDate publishedDate;
    // On ne permet pas de changer l'ISBN ni l'auteur après création
}
```

### [WORLD_MAP] Mapping Entity <-> DTO avec MapStruct

**MapStruct** génère automatiquement le code de mapping à la compilation. C'est plus sûr et plus rapide que de le faire manuellement.

```java
package com.libraryhub.entity;

import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDate;
import java.util.List;

// L'entité JPA — representé en base de données
@Entity
@Table(name = "books")
@Data
public class Book {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private String title;
    
    @Column(unique = true, nullable = false)
    private String isbn;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "author_id")
    private Author author;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "category_id")
    private Category category;
    
    private LocalDate publishedDate;
    
    @OneToMany(mappedBy = "book")
    private List<Loan> loans;
}
```

```java
package com.libraryhub.mapper;

import com.libraryhub.dto.BookDTO;
import com.libraryhub.dto.BookCreateRequest;
import com.libraryhub.entity.Book;
import org.mapstruct.*;

@Mapper(componentModel = "spring")  // Génère un bean Spring
public interface BookMapper {
    
    // Entity -> DTO
    @Mapping(source = "author.fullName", target = "authorName")
    @Mapping(source = "category.name", target = "categoryName")
    @Mapping(target = "available", expression = "java(isBookAvailable(book))")
    BookDTO toDTO(Book book);
    
    // DTO -> Entity (sans id, sans relations)
    @Mapping(target = "id", ignore = true)
    @Mapping(target = "author", ignore = true)   // géré dans le service
    @Mapping(target = "category", ignore = true) // géré dans le service
    @Mapping(target = "loans", ignore = true)
    Book toEntity(BookCreateRequest request);
    
    // Méthode par défaut pour calculer la disponibilité
    default boolean isBookAvailable(Book book) {
        if (book.getLoans() == null) return true;
        return book.getLoans().stream()
            .noneMatch(loan -> loan.getReturnDate() == null);
    }
    
    List<BookDTO> toDTOList(List<Book> books);
}
```

```java
package com.libraryhub.service;

@Service
@Slf4j
public class BookService {
    
    private final BookRepository bookRepository;
    private final AuthorRepository authorRepository;
    private final CategoryRepository categoryRepository;
    private final BookMapper bookMapper;
    
    public BookService(BookRepository bookRepository,
                       AuthorRepository authorRepository,
                       CategoryRepository categoryRepository,
                       BookMapper bookMapper) {
        this.bookRepository = bookRepository;
        this.authorRepository = authorRepository;
        this.categoryRepository = categoryRepository;
        this.bookMapper = bookMapper;
    }
    
    public List<BookDTO> findAll() {
        return bookMapper.toDTOList(bookRepository.findAll());
    }
    
    public BookDTO findById(Long id) {
        Book book = bookRepository.findById(id)
            .orElseThrow(() -> new BookNotFoundException("Livre non trouvé avec l'ID: " + id));
        return bookMapper.toDTO(book);
    }
    
    public BookDTO create(BookCreateRequest request) {
        // Vérifier que l'auteur et la catégorie existent
        Author author = authorRepository.findById(request.getAuthorId())
            .orElseThrow(() -> new AuthorNotFoundException("Auteur non trouvé"));
        Category category = categoryRepository.findById(request.getCategoryId())
            .orElseThrow(() -> new CategoryNotFoundException("Catégorie non trouvée"));
        
        Book book = bookMapper.toEntity(request);
        book.setAuthor(author);
        book.setCategory(category);
        
        Book saved = bookRepository.save(book);
        log.info("Livre créé : {} (ISBN: {})", saved.getTitle(), saved.getIsbn());
        return bookMapper.toDTO(saved);
    }
    
    public BookDTO update(Long id, BookUpdateRequest request) {
        Book book = bookRepository.findById(id)
            .orElseThrow(() -> new BookNotFoundException("Livre non trouvé avec l'ID: " + id));
        book.setTitle(request.getTitle());
        book.setPublishedDate(request.getPublishedDate());
        return bookMapper.toDTO(bookRepository.save(book));
    }
    
    public void delete(Long id) {
        if (!bookRepository.existsById(id)) {
            throw new BookNotFoundException("Livre non trouvé avec l'ID: " + id);
        }
        bookRepository.deleteById(id);
    }
    
    public List<BookDTO> search(String title, String author) {
        return bookMapper.toDTOList(
            bookRepository.findByTitleContainingIgnoreCaseOrAuthorFullNameContainingIgnoreCase(
                title != null ? title : "",
                author != null ? author : ""
            )
        );
    }
}
```

---

## Chapitre 8 — Validation des données

### [SECURITE] Bean Validation (JSR-380)

La validation se fait avec des annotations sur les DTOs. Spring déclenche la validation automatiquement quand tu ajoutes `@Valid` sur le paramètre dans le controller.

```java
package com.libraryhub.dto;

import jakarta.validation.constraints.*;
import lombok.Data;
import java.time.LocalDate;

@Data
public class MemberCreateRequest {
    
    @NotBlank(message = "Le prénom est obligatoire")
    @Size(min = 2, max = 50, message = "Le prénom doit contenir entre 2 et 50 caractères")
    private String firstName;
    
    @NotBlank(message = "Le nom est obligatoire")
    @Size(min = 2, max = 50)
    private String lastName;
    
    @NotBlank(message = "L'email est obligatoire")
    @Email(message = "Format d'email invalide")
    private String email;
    
    @NotBlank
    @Size(min = 8, message = "Le mot de passe doit contenir au moins 8 caractères")
    @Pattern(regexp = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).*$",
             message = "Le mot de passe doit contenir au moins un chiffre, une minuscule et une majuscule")
    private String password;
    
    @NotNull(message = "La date de naissance est obligatoire")
    @Past(message = "La date de naissance doit être dans le passé")
    private LocalDate birthDate;
    
    @Min(value = 0, message = "Le nombre de livres max ne peut pas être négatif")
    @Max(value = 10, message = "Maximum 10 livres par membre")
    private int maxBooks = 3;
}
```

### [OUTIL] Validation personnalisée

Parfois les annotations standard ne suffisent pas. Crée ta propre annotation de validation :

```java
// 1. Créer l'annotation
package com.libraryhub.validation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueIsbnValidator.class)
@Documented
public @interface UniqueIsbn {
    String message() default "Cet ISBN existe déjà";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// 2. Créer le validateur
@Component
public class UniqueIsbnValidator implements ConstraintValidator<UniqueIsbn, String> {
    
    private final BookRepository bookRepository;
    
    public UniqueIsbnValidator(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }
    
    @Override
    public boolean isValid(String isbn, ConstraintValidatorContext context) {
        if (isbn == null || isbn.isBlank()) return true; // @NotBlank gère ça
        return !bookRepository.existsByIsbn(isbn);
    }
}

// 3. Utiliser l'annotation dans le DTO
@Data
public class BookCreateRequest {
    
    @NotBlank
    @UniqueIsbn  // <- Vérifie en base de données que l'ISBN n'existe pas
    private String isbn;
    
    // ...
}
```

### [X] Gestion des erreurs de validation

Sans configuration, Spring retourne une réponse d'erreur peu lisible. Crée un handler global (détaillé dans `05_ARCHITECTURE.md`).

```java
// Aperçu — détaillé dans le Module Architecture
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationErrors(
            MethodArgumentNotValidException ex) {
        
        // Collecter tous les messages d'erreur des champs
        Map<String, String> fieldErrors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error -> 
            fieldErrors.put(error.getField(), error.getDefaultMessage())
        );
        
        ErrorResponse response = ErrorResponse.builder()
            .timestamp(LocalDateTime.now())
            .status(400)
            .message("Erreur de validation")
            .errors(fieldErrors)
            .build();
        
        return ResponseEntity.badRequest().body(response);
    }
}
```

**Résultat :** au lieu d'une erreur illisible, le client reçoit :
```json
{
  "timestamp": "2024-01-15T10:30:00",
  "status": 400,
  "message": "Erreur de validation",
  "errors": {
    "email": "Format d'email invalide",
    "password": "Le mot de passe doit contenir au moins un chiffre, une minuscule et une majuscule",
    "isbn": "Cet ISBN existe déjà"
  }
}
```

### [SYNC] Controllers complets — Author & Member

```java
// AuthorController.java
@RestController
@RequestMapping("/api/authors")
@RequiredArgsConstructor  // Lombok génère le constructeur d'injection
@Slf4j
public class AuthorController {
    
    private final AuthorService authorService;
    
    @GetMapping
    public ResponseEntity<Page<AuthorDTO>> getAllAuthors(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size,
            @RequestParam(defaultValue = "lastName") String sortBy) {
        
        Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
        return ResponseEntity.ok(authorService.findAll(pageable));
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<AuthorDTO> getAuthor(@PathVariable Long id) {
        return ResponseEntity.ok(authorService.findById(id));
    }
    
    // GET /api/authors/1/books — Ressource imbriquée
    @GetMapping("/{id}/books")
    public ResponseEntity<List<BookDTO>> getAuthorBooks(@PathVariable Long id) {
        return ResponseEntity.ok(authorService.findBooksByAuthor(id));
    }
    
    @PostMapping
    @PreAuthorize("hasRole('ADMIN')")  // Sécurité (détaillée dans Module 04)
    public ResponseEntity<AuthorDTO> createAuthor(@RequestBody @Valid AuthorCreateRequest request) {
        AuthorDTO created = authorService.create(request);
        return ResponseEntity.created(URI.create("/api/authors/" + created.getId())).body(created);
    }
    
    @PutMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<AuthorDTO> updateAuthor(
            @PathVariable Long id, 
            @RequestBody @Valid AuthorUpdateRequest request) {
        return ResponseEntity.ok(authorService.update(id, request));
    }
    
    @DeleteMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<Void> deleteAuthor(@PathVariable Long id) {
        authorService.delete(id);
        return ResponseEntity.noContent().build();
    }
}
```

```java
// LoanController.java — Emprunts
@RestController
@RequestMapping("/api/loans")
@RequiredArgsConstructor
@Slf4j
public class LoanController {
    
    private final LoanService loanService;
    
    // POST /api/loans — Créer un emprunt
    @PostMapping
    public ResponseEntity<LoanDTO> createLoan(@RequestBody @Valid LoanCreateRequest request) {
        LoanDTO loan = loanService.createLoan(request);
        return ResponseEntity.created(URI.create("/api/loans/" + loan.getId())).body(loan);
    }
    
    // PATCH /api/loans/{id}/return — Retourner un livre
    @PatchMapping("/{id}/return")
    public ResponseEntity<LoanDTO> returnBook(@PathVariable Long id) {
        return ResponseEntity.ok(loanService.returnBook(id));
    }
    
    // GET /api/loans?memberId=5&status=active
    @GetMapping
    public ResponseEntity<List<LoanDTO>> getLoans(
            @RequestParam(required = false) Long memberId,
            @RequestParam(required = false) String status) {
        return ResponseEntity.ok(loanService.findLoans(memberId, status));
    }
    
    // GET /api/loans/overdue — Emprunts en retard
    @GetMapping("/overdue")
    @PreAuthorize("hasRole('LIBRARIAN') or hasRole('ADMIN')")
    public ResponseEntity<List<LoanDTO>> getOverdueLoans() {
        return ResponseEntity.ok(loanService.findOverdueLoans());
    }
}
```

---

## [OK] Exercices du Module 02

1. **CRUD Complet :** Implémente le `MemberController` et `MemberService` avec les opérations CRUD complètes. Un membre a : nom, prénom, email, téléphone, adresse.

2. **Validation custom :** Crée une annotation `@AgeAbove` qui valide que la date de naissance correspond à un âge minimum (paramètre configurable). Utilise-la sur `MemberCreateRequest`.

3. **Ressource imbriquée :** Ajoute `GET /api/members/{id}/loans` pour voir les emprunts d'un membre, et `GET /api/members/{id}/loans/active` pour les emprunts actifs seulement.

4. **Codes HTTP :** Vérifie avec Postman (ou Insomnia) que :
   - Créer un livre retourne `201` avec le header `Location`
   - Chercher un ID inexistant retourne `404`
   - Envoyer des données invalides retourne `400` avec les détails

5. **Défi :** Implémente `GET /api/books/available` qui retourne seulement les livres disponibles (non empruntés), avec pagination et tri par titre.

---

> -> Continue avec `03_JPA_PERSISTENCE.md`


# [LIVRE] Module 03 — Accès aux données (JPA & Hibernate)

> **Parties couvertes :** Partie 4 (Chapitres 9–11)
> **Projet fil rouge :** Modéliser et persister les entités de LibraryHub en base de données

---

## Chapitre 9 — JPA & Hibernate

### [REFLEXION] ORM — Object Relational Mapping

**Le problème :** Tu programmes en Java avec des **objets** (Book, Author, Loan...). Ta base de données stocke des **tables** avec des lignes et colonnes. Ces deux mondes sont fondamentalement différents.

**La solution ORM :** Un outil qui fait le pont automatiquement.

```
Monde Java          <-->    Base de données
─────────────────         ─────────────────
Classe Book          ->    Table "books"
Instance de Book     ->    Ligne dans "books"
Champ "title"        ->    Colonne "title"
Référence Author     ->    Clé étrangère "author_id"
```

**JPA** (Jakarta Persistence API) est la **spécification** (un contrat, des interfaces).
**Hibernate** est l'**implémentation** (le code qui fait vraiment le travail).
**Spring Data JPA** est la **surcouche** Spring qui simplifie encore plus l'utilisation de JPA.

### [CLASSICAL_BUILDING] Les entités de LibraryHub

```java
package com.libraryhub.entity;

// ═══════════════════════════════════════
// ENTITÉ AUTHOR
// ═══════════════════════════════════════
@Entity
@Table(name = "authors")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Author {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    // IDENTITY -> auto-increment en base (PostgreSQL SERIAL, MySQL AUTO_INCREMENT)
    private Long id;
    
    @Column(name = "first_name", nullable = false, length = 50)
    private String firstName;
    
    @Column(name = "last_name", nullable = false, length = 50)
    private String lastName;
    
    @Column(unique = true)
    private String email;
    
    // Colonne dérivée — non persistée en base, calculée côté Java
    @Transient
    public String getFullName() {
        return firstName + " " + lastName;
    }
    
    // Relation : Un auteur a plusieurs livres
    @OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true)
    @JsonIgnore  // Évite la récursion infinie JSON
    private List<Book> books = new ArrayList<>();
}
```

```java
// ═══════════════════════════════════════
// ENTITÉ BOOK
// ═══════════════════════════════════════
@Entity
@Table(name = "books", indexes = {
    @Index(name = "idx_book_isbn", columnList = "isbn"),
    @Index(name = "idx_book_title", columnList = "title")
})
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Book {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private String title;
    
    @Column(unique = true, nullable = false, length = 20)
    private String isbn;
    
    // ManyToOne : Plusieurs livres -> un auteur
    // FetchType.LAZY : l'auteur n'est chargé que quand on y accède explicitement
    // (évite de charger tout le graphe d'objets à chaque requête)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "author_id", nullable = false)
    private Author author;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "category_id")
    private Category category;
    
    @Column(name = "published_date")
    private LocalDate publishedDate;
    
    @Column(name = "total_copies", nullable = false)
    private int totalCopies = 1;
    
    // OneToMany : Un livre -> plusieurs emprunts
    @OneToMany(mappedBy = "book", cascade = CascadeType.ALL)
    private List<Loan> loans = new ArrayList<>();
    
    // Audit — rempli automatiquement par Spring Data Auditing
    @CreatedDate
    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdAt;
    
    @LastModifiedDate
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
}
```

```java
// ═══════════════════════════════════════
// ENTITÉ MEMBER
// ═══════════════════════════════════════
@Entity
@Table(name = "members")
@Data
@NoArgsConstructor
@Builder
public class Member {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "first_name", nullable = false)
    private String firstName;
    
    @Column(name = "last_name", nullable = false)
    private String lastName;
    
    @Column(unique = true, nullable = false)
    private String email;
    
    @Column(name = "phone_number")
    private String phoneNumber;
    
    // Enum stocké comme String en base ("ACTIVE", "SUSPENDED", "EXPIRED")
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private MemberStatus status = MemberStatus.ACTIVE;
    
    @Column(name = "birth_date")
    private LocalDate birthDate;
    
    @Column(name = "member_since")
    private LocalDate memberSince = LocalDate.now();
    
    // Relation vers les emprunts
    @OneToMany(mappedBy = "member", cascade = CascadeType.ALL)
    private List<Loan> loans = new ArrayList<>();
}

// Enum du statut
public enum MemberStatus {
    ACTIVE, SUSPENDED, EXPIRED
}
```

```java
// ═══════════════════════════════════════
// ENTITÉ LOAN (Emprunt)
// ═══════════════════════════════════════
@Entity
@Table(name = "loans")
@Data
@NoArgsConstructor
@Builder
public class Loan {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    // Plusieurs emprunts -> un membre
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id", nullable = false)
    private Member member;
    
    // Plusieurs emprunts -> un livre
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "book_id", nullable = false)
    private Book book;
    
    @Column(name = "loan_date", nullable = false)
    private LocalDate loanDate;
    
    @Column(name = "due_date", nullable = false)
    private LocalDate dueDate;
    
    // null = non retourné, date = date de retour effectif
    @Column(name = "return_date")
    private LocalDate returnDate;
    
    @Enumerated(EnumType.STRING)
    private LoanStatus status = LoanStatus.ACTIVE;
    
    // Méthode utilitaire — non persistée
    @Transient
    public boolean isOverdue() {
        return returnDate == null && LocalDate.now().isAfter(dueDate);
    }
}

public enum LoanStatus {
    ACTIVE, RETURNED, OVERDUE
}
```

### [LIEN] Mapping des relations — Lazy vs Eager

```
FetchType.LAZY   = "Charge-moi cette relation seulement si j'y accède"
FetchType.EAGER  = "Charge-moi cette relation systématiquement, même si je n'en ai pas besoin"
```

**Règle :** Toujours utiliser `LAZY` par défaut. `EAGER` cause souvent des problèmes de performance (le fameux **N+1 problem**).

**Le problème N+1 expliqué :**

```java
// [X] Scénario problématique avec EAGER
// Si tu charges 100 livres, Hibernate fait :
// 1 requête pour les livres + 100 requêtes pour charger l'auteur de chaque livre = 101 requêtes !

List<Book> books = bookRepository.findAll();  // 1 requête SQL
for (Book book : books) {
    System.out.println(book.getAuthor().getFullName()); // +1 requête par livre !
}

// [OK] Solution : utiliser une JPQL JOIN FETCH
@Query("SELECT b FROM Book b JOIN FETCH b.author WHERE b.category.id = :categoryId")
List<Book> findByCategoryWithAuthor(@Param("categoryId") Long categoryId);
// -> 1 seule requête SQL avec JOIN
```

---

## Chapitre 10 — Spring Data JPA

### [DOCS] JpaRepository — L'interface magique

Spring Data JPA génère automatiquement l'implémentation de tes repositories.

```java
package com.libraryhub.repository;

// JpaRepository<Book, Long> signifie :
// - Entité gérée : Book
// - Type de l'ID : Long
// Spring génère automatiquement : save(), findById(), findAll(), 
// deleteById(), count(), existsById(), etc.
public interface BookRepository extends JpaRepository<Book, Long> {
    
    // ════════════════════════════════════════════
    // MÉTHODES DÉRIVÉES — Spring génère le SQL depuis le nom
    // ════════════════════════════════════════════
    
    // SELECT * FROM books WHERE isbn = ?
    Optional<Book> findByIsbn(String isbn);
    
    // SELECT * FROM books WHERE UPPER(title) LIKE UPPER('%?%')
    List<Book> findByTitleContainingIgnoreCase(String title);
    
    // SELECT * FROM books WHERE author_id = ? ORDER BY title ASC
    List<Book> findByAuthorIdOrderByTitleAsc(Long authorId);
    
    // SELECT COUNT(*) FROM books WHERE category_id = ?
    long countByCategoryId(Long categoryId);
    
    // SELECT (COUNT(*) > 0) FROM books WHERE isbn = ?
    boolean existsByIsbn(String isbn);
    
    // SELECT * FROM books WHERE published_date BETWEEN ? AND ?
    List<Book> findByPublishedDateBetween(LocalDate start, LocalDate end);
    
    // Combinaison de critères
    List<Book> findByAuthorLastNameAndCategoryName(String authorLastName, String categoryName);
    
    // ════════════════════════════════════════════
    // REQUÊTES JPQL — Java Persistence Query Language
    // ════════════════════════════════════════════
    
    // JPQL travaille avec les classes Java, pas les tables SQL
    @Query("SELECT b FROM Book b " +
           "JOIN FETCH b.author a " +    // JOIN FETCH évite le N+1
           "LEFT JOIN FETCH b.category c " +
           "WHERE (:title IS NULL OR LOWER(b.title) LIKE LOWER(CONCAT('%', :title, '%'))) " +
           "AND (:authorName IS NULL OR LOWER(a.lastName) LIKE LOWER(CONCAT('%', :authorName, '%')))")
    List<Book> searchBooks(@Param("title") String title, @Param("authorName") String authorName);
    
    // Requête pour les livres disponibles (sans emprunt actif)
    @Query("SELECT b FROM Book b " +
           "WHERE b.id NOT IN (" +
           "   SELECT l.book.id FROM Loan l WHERE l.status = 'ACTIVE'" +
           ")")
    List<Book> findAvailableBooks();
    
    // ════════════════════════════════════════════
    // REQUÊTES NATIVES SQL
    // ════════════════════════════════════════════
    
    // Quand JPQL n'est pas assez puissant (fonctions spécifiques à la DB, etc.)
    @Query(value = "SELECT b.*, a.first_name, a.last_name " +
                   "FROM books b " +
                   "JOIN authors a ON b.author_id = a.id " +
                   "WHERE b.id NOT IN (SELECT book_id FROM loans WHERE return_date IS NULL) " +
                   "ORDER BY RANDOM() LIMIT :limit",
           nativeQuery = true)
    List<Object[]> findRandomAvailableBooks(@Param("limit") int limit);
    
    // ════════════════════════════════════════════
    // PAGINATION
    // ════════════════════════════════════════════
    
    // La signature change : retourne Page<Book> et prend un Pageable
    Page<Book> findByCategoryId(Long categoryId, Pageable pageable);
    
    @Query("SELECT b FROM Book b JOIN FETCH b.author WHERE b.category.id = :categoryId")
    Page<Book> findByCategoryWithAuthor(@Param("categoryId") Long categoryId, Pageable pageable);
}
```

```java
// LoanRepository — Exemples de requêtes spécifiques aux emprunts
public interface LoanRepository extends JpaRepository<Loan, Long> {
    
    // Emprunts actifs d'un membre
    List<Loan> findByMemberIdAndStatus(Long memberId, LoanStatus status);
    
    // Vérifier si un livre est actuellement emprunté
    boolean existsByBookIdAndStatus(Long bookId, LoanStatus status);
    
    // Trouver les emprunts en retard
    @Query("SELECT l FROM Loan l " +
           "JOIN FETCH l.member " +
           "JOIN FETCH l.book " +
           "WHERE l.status = 'ACTIVE' AND l.dueDate < :today")
    List<Loan> findOverdueLoans(@Param("today") LocalDate today);
    
    // Statistiques — nombre d'emprunts par membre ce mois
    @Query("SELECT COUNT(l) FROM Loan l " +
           "WHERE l.member.id = :memberId " +
           "AND FUNCTION('MONTH', l.loanDate) = FUNCTION('MONTH', CURRENT_DATE) " +
           "AND FUNCTION('YEAR', l.loanDate) = FUNCTION('YEAR', CURRENT_DATE)")
    long countLoansThisMonthByMember(@Param("memberId") Long memberId);
}
```

### [GRAPHIQUE] Utiliser la pagination dans le service et le controller

```java
// Dans BookService
public Page<BookDTO> findAll(int page, int size, String sortBy, String direction) {
    Sort sort = direction.equalsIgnoreCase("desc") 
        ? Sort.by(sortBy).descending() 
        : Sort.by(sortBy).ascending();
    
    Pageable pageable = PageRequest.of(page, size, sort);
    Page<Book> bookPage = bookRepository.findAll(pageable);
    
    // Transformer Page<Book> en Page<BookDTO>
    return bookPage.map(bookMapper::toDTO);
}

// Dans BookController — la réponse inclut la pagination
@GetMapping
public ResponseEntity<Page<BookDTO>> getAllBooks(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "20") int size,
        @RequestParam(defaultValue = "title") String sortBy,
        @RequestParam(defaultValue = "asc") String direction) {
    
    return ResponseEntity.ok(bookService.findAll(page, size, sortBy, direction));
}
```

Réponse JSON avec pagination :
```json
{
  "content": [
    { "id": 1, "title": "Clean Code", "authorName": "Robert Martin" },
    { "id": 2, "title": "Design Patterns", "authorName": "Erich Gamma" }
  ],
  "pageable": {
    "sort": { "sorted": true, "unsorted": false },
    "pageNumber": 0,
    "pageSize": 20
  },
  "totalElements": 150,
  "totalPages": 8,
  "first": true,
  "last": false,
  "number": 0,
  "size": 20
}
```

---

## Chapitre 11 — Transactions

### [IDEE] Pourquoi les transactions ?

Imagine qu'un membre emprunte un livre. L'opération nécessite :
1. Vérifier que le livre est disponible
2. Créer un emprunt (INSERT dans `loans`)
3. Mettre à jour le statut du livre (UPDATE dans `books`)

Si l'étape 3 échoue, on a créé un emprunt sans mettre à jour le livre -> **incohérence**. Une transaction garantit que soit tout réussit, soit tout est annulé.

### [SYNC] `@Transactional` — Le contrat

```java
@Service
@Slf4j
public class LoanService {
    
    private final LoanRepository loanRepository;
    private final BookRepository bookRepository;
    private final MemberRepository memberRepository;
    private final LibraryHubProperties properties;
    
    // ════════════════════════════════════════════
    // Transaction lecture seule — plus performante
    // ════════════════════════════════════════════
    @Transactional(readOnly = true)
    public List<LoanDTO> findLoansByMember(Long memberId) {
        // readOnly = true -> Hibernate optimise (pas de dirty checking)
        return loanRepository.findByMemberIdAndStatus(memberId, LoanStatus.ACTIVE)
            .stream()
            .map(loanMapper::toDTO)
            .collect(Collectors.toList());
    }
    
    // ════════════════════════════════════════════
    // Transaction d'écriture — atomique
    // ════════════════════════════════════════════
    @Transactional
    public LoanDTO createLoan(LoanCreateRequest request) {
        // Toutes ces opérations font partie de la même transaction
        
        Member member = memberRepository.findById(request.getMemberId())
            .orElseThrow(() -> new MemberNotFoundException("Membre non trouvé"));
        
        // Vérifier que le membre est actif
        if (member.getStatus() != MemberStatus.ACTIVE) {
            throw new MemberSuspendedException("Le membre est suspendu ou expiré");
        }
        
        // Vérifier le nombre d'emprunts actifs
        long activeLoans = loanRepository.findByMemberIdAndStatus(
            member.getId(), LoanStatus.ACTIVE).size();
        if (activeLoans >= properties.getLoan().getMaxDurationDays()) {
            throw new MaxLoansExceededException("Nombre maximum d'emprunts atteint");
        }
        
        Book book = bookRepository.findById(request.getBookId())
            .orElseThrow(() -> new BookNotFoundException("Livre non trouvé"));
        
        // Vérifier la disponibilité (avec lock pessimiste pour éviter la concurrence)
        if (loanRepository.existsByBookIdAndStatus(book.getId(), LoanStatus.ACTIVE)) {
            throw new BookNotAvailableException("Le livre '" + book.getTitle() + "' est déjà emprunté");
        }
        
        // Créer l'emprunt
        Loan loan = Loan.builder()
            .member(member)
            .book(book)
            .loanDate(LocalDate.now())
            .dueDate(LocalDate.now().plusDays(properties.getLoan().getMaxDurationDays()))
            .status(LoanStatus.ACTIVE)
            .build();
        
        Loan savedLoan = loanRepository.save(loan);
        
        log.info("Emprunt créé : membre={} livre='{}' retour prévu={}", 
            member.getEmail(), book.getTitle(), savedLoan.getDueDate());
        
        // Si une exception est levée ici, les deux saves() ci-dessus sont annulés
        return loanMapper.toDTO(savedLoan);
    }
    
    // ════════════════════════════════════════════
    // Retour d'un livre
    // ════════════════════════════════════════════
    @Transactional
    public LoanDTO returnBook(Long loanId) {
        Loan loan = loanRepository.findById(loanId)
            .orElseThrow(() -> new LoanNotFoundException("Emprunt non trouvé"));
        
        if (loan.getStatus() != LoanStatus.ACTIVE) {
            throw new LoanAlreadyReturnedException("Ce livre a déjà été retourné");
        }
        
        loan.setReturnDate(LocalDate.now());
        loan.setStatus(LoanStatus.RETURNED);
        
        Loan saved = loanRepository.save(loan);
        log.info("Livre retourné : '{}' par {}", 
            loan.getBook().getTitle(), loan.getMember().getEmail());
        
        return loanMapper.toDTO(saved);
    }
    
    // ════════════════════════════════════════════
    // Propagation de transaction
    // ════════════════════════════════════════════
    
    // REQUIRED (défaut) : rejoindre la transaction existante ou en créer une
    @Transactional(propagation = Propagation.REQUIRED)
    public void updateLoanStatus(Long loanId, LoanStatus status) {
        // Si appelé depuis une méthode @Transactional, même transaction
        // Si appelé seul, nouvelle transaction
        Loan loan = loanRepository.findById(loanId).orElseThrow();
        loan.setStatus(status);
        loanRepository.save(loan);
    }
    
    // REQUIRES_NEW : toujours une nouvelle transaction indépendante
    // Utilisé pour les logs d'audit qui doivent être persistés même si la 
    // transaction principale échoue
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logAuditEvent(String event, Long memberId) {
        // Cette transaction est indépendante de celle qui l'a appelée
        // Sera committée même si l'appelant fait un rollback
        auditRepository.save(new AuditLog(event, memberId, LocalDateTime.now()));
    }
    
    // ════════════════════════════════════════════
    // Rollback conditionnel
    // ════════════════════════════════════════════
    
    // Par défaut, @Transactional rollback sur RuntimeException et Error
    // mais PAS sur les exceptions checked
    
    @Transactional(rollbackFor = Exception.class)
    // ^ Rollback sur toutes les exceptions (checked et unchecked)
    
    @Transactional(noRollbackFor = BookNotAvailableException.class)
    // ^ Ne rollback PAS si BookNotAvailableException est levée
    public void someOperation() { }
}
```

### [VERROUILLE] Lock pessimiste — Éviter les conflits de concurrence

```java
// Si deux personnes essaient d'emprunter le même livre en même temps :
public interface BookRepository extends JpaRepository<Book, Long> {
    
    // Lock pessimiste : la ligne est verrouillée le temps de la transaction
    // L'autre transaction attend que la première soit terminée
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT b FROM Book b WHERE b.id = :id")
    Optional<Book> findByIdForUpdate(@Param("id") Long id);
}

// Utilisation dans LoanService
@Transactional
public LoanDTO createLoan(LoanCreateRequest request) {
    // Verrouille le livre pendant toute la transaction
    Book book = bookRepository.findByIdForUpdate(request.getBookId())
        .orElseThrow(() -> new BookNotFoundException("Livre non trouvé"));
    
    // Maintenant, même si deux requêtes arrivent en même temps,
    // la deuxième attendra que la première termine
    if (loanRepository.existsByBookIdAndStatus(book.getId(), LoanStatus.ACTIVE)) {
        throw new BookNotAvailableException("Le livre est déjà emprunté");
    }
    
    // ... créer l'emprunt
}
```

### [CONFIG] Activer l'audit automatique

```java
// Configuration de l'audit JPA dans LibraryHub
@Configuration
@EnableJpaAuditing  // Active l'audit automatique
public class JpaAuditingConfig {
    
    // Fournit l'utilisateur courant pour les champs @CreatedBy / @LastModifiedBy
    @Bean
    public AuditorAware<String> auditorProvider() {
        return () -> {
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            if (auth == null || !auth.isAuthenticated()) {
                return Optional.of("system");
            }
            return Optional.of(auth.getName());
        };
    }
}

// Dans les entités avec audit complet
@Entity
@EntityListeners(AuditingEntityListener.class)  // Active l'audit pour cette entité
public class Book {
    
    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;
    
    @LastModifiedDate
    private LocalDateTime updatedAt;
    
    @CreatedBy
    @Column(updatable = false)
    private String createdBy;
    
    @LastModifiedBy
    private String lastModifiedBy;
}
```

---

## [OK] Exercices du Module 03

1. **Entités :** Ajoute une entité `Category` (id, name, description) et crée une relation `@ManyToMany` entre `Book` et `Tag` (un livre peut avoir plusieurs tags, un tag peut appartenir à plusieurs livres).

2. **JPQL :** Écris une requête JPQL dans `LoanRepository` qui retourne les 5 livres les plus empruntés du mois courant (avec leur nombre d'emprunts).

3. **Transactions :** Implémente `renewLoan(Long loanId)` dans `LoanService` qui prolonge la date de retour de 7 jours, mais seulement si le livre n'est pas réservé par quelqu'un d'autre (imaginons une table `Reservation`).

4. **Pagination :** Ajoute au `LoanController` un endpoint `GET /api/loans/history?memberId=5&page=0&size=10` qui retourne l'historique paginé et trié par date décroissante.

5. **Défi N+1 :** Lance l'application avec `logging.level.org.hibernate.SQL=DEBUG`, appelle `GET /api/books` et observe les requêtes SQL. Corrige le problème N+1 avec une requête `JOIN FETCH`.

---

> -> Continue avec `04_SECURITE.md`


# [LIVRE] Module 04 — Sécurité avec Spring Security & JWT

> **Parties couvertes :** Partie 5 (Chapitres 12–14)
> **Projet fil rouge :** Sécuriser l'API LibraryHub avec JWT, rôles ADMIN/LIBRARIAN/MEMBER

---

## Chapitre 12 — Bases de Spring Security

### [SECURISE] Comment Spring Security fonctionne

Spring Security fonctionne comme une **chaîne de filtres** (Filter Chain). Chaque requête HTTP passe à travers cette chaîne avant d'atteindre ton controller.

```
Requête HTTP
     │
     [BLACK_DOWN-POINTING_TRIANGLE]
┌────────────────────────────────────────┐
│           Security Filter Chain        │
│                                        │
│  1. JwtAuthenticationFilter <-── Notre filtre custom
│     (extrait et valide le JWT)         │
│                                        │
│  2. UsernamePasswordAuthenticationFilter
│     (login form — non utilisé en REST) │
│                                        │
│  3. ExceptionTranslationFilter         │
│     (gère 401, 403)                    │
│                                        │
│  4. FilterSecurityInterceptor          │
│     (vérifie les autorizations)        │
└──────────────────┬─────────────────────┘
                   │ Si autorisé
                   [BLACK_DOWN-POINTING_TRIANGLE]
           DispatcherServlet
           (ton controller)
```

### 🆚 Authentication vs Authorization

```
Authentication (Authentification) = QUI es-tu ?
    -> "Je suis alice@libraryhub.com avec le mot de passe XXXX"
    -> Spring Security vérifie les credentials

Authorization (Autorisation) = Qu'as-tu le droit de faire ?
    -> "Alice a le rôle MEMBER, elle peut voir les livres mais pas les supprimer"
    -> Spring Security vérifie les rôles/permissions
```

---

## Chapitre 13 — Authentification dans LibraryHub

### [UTILISATEUR] L'entité User

```java
package com.libraryhub.entity;

@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@Builder
// Implémente UserDetails : contrat requis par Spring Security
public class User implements UserDetails {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(unique = true, nullable = false)
    private String email;
    
    @Column(nullable = false)
    private String password;  // Stocké en HASH (bcrypt), jamais en clair !
    
    @Enumerated(EnumType.STRING)
    private Role role;
    
    private boolean enabled = true;
    
    // OneToOne avec Member (optionnel — un user peut ne pas être membre)
    @OneToOne
    @JoinColumn(name = "member_id")
    private Member member;
    
    // ────────────────────────────────────────────────
    // Méthodes de UserDetails — requises par Spring Security
    // ────────────────────────────────────────────────
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // Spring Security utilise le préfixe "ROLE_" par convention
        return List.of(new SimpleGrantedAuthority("ROLE_" + role.name()));
    }
    
    @Override
    public String getUsername() {
        return email;  // On utilise l'email comme identifiant
    }
    
    @Override
    public boolean isAccountNonExpired() { return true; }
    
    @Override
    public boolean isAccountNonLocked() { return enabled; }
    
    @Override
    public boolean isCredentialsNonExpired() { return true; }
    
    @Override
    public boolean isEnabled() { return enabled; }
}

// Les rôles disponibles
public enum Role {
    ADMIN,      // Accès total
    LIBRARIAN,  // Gestion livres et emprunts
    MEMBER      // Consultation et emprunt
}
```

### [CLE] UserDetailsService — Charger l'utilisateur

```java
package com.libraryhub.security;

@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
    
    private final UserRepository userRepository;
    
    // Spring Security appelle cette méthode pendant l'authentification
    // Elle doit retourner un UserDetails ou lancer UsernameNotFoundException
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // Dans LibraryHub, le "username" est l'email
        return userRepository.findByEmail(username)
            .orElseThrow(() -> new UsernameNotFoundException(
                "Utilisateur non trouvé avec l'email : " + username
            ));
    }
}
```

### [VERROUILLE] BCrypt — Hasher les mots de passe

```java
// [X] JAMAIS stocker un mot de passe en clair
user.setPassword("monMotDePasse123");  // DANGEREUX !

// [OK] Toujours hasher avec BCrypt
@Autowired
private PasswordEncoder passwordEncoder;

user.setPassword(passwordEncoder.encode("monMotDePasse123"));
// Stocké en base : "$2a$10$xn3LI/AjqicFYZFruSwve.681477XaVNaUQbr1gioaWPn4t1KsnmG"

// Vérification (lors du login)
boolean isValid = passwordEncoder.matches("monMotDePasse123", hashedPassword);
// -> true si le mot de passe correspond au hash
```

---

## Chapitre 14 — JWT & Sécurité Stateless

### [TICKET] Comment fonctionne JWT dans LibraryHub

```
FLUX D'AUTHENTIFICATION :

1. Client POST /api/auth/login { email, password }
          │
          [BLACK_DOWN-POINTING_TRIANGLE]
2. Spring Security vérifie les credentials avec UserDetailsService
          │
          [BLACK_DOWN-POINTING_TRIANGLE]
3. Si valide -> générer un JWT et le retourner
          │
          [BLACK_DOWN-POINTING_TRIANGLE]
4. Client reçoit { accessToken, refreshToken }

─────────────────────────────────────────────────────

FLUX D'ACCÈS AUX RESSOURCES PROTÉGÉES :

1. Client GET /api/books
   Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
          │
          [BLACK_DOWN-POINTING_TRIANGLE]
2. JwtFilter intercepte la requête, extrait et valide le JWT
          │
          [BLACK_DOWN-POINTING_TRIANGLE]
3. Si JWT valide -> charge l'utilisateur, place dans SecurityContext
          │
          [BLACK_DOWN-POINTING_TRIANGLE]
4. Spring Security vérifie les permissions pour /api/books
          │
          [BLACK_DOWN-POINTING_TRIANGLE]
5. Si autorisé -> Controller gère la requête
```

### [OUTILS] JwtUtil — Gestion des tokens

```java
package com.libraryhub.security;

@Component
@RequiredArgsConstructor
@Slf4j
public class JwtUtil {
    
    private final JwtProperties jwtProperties;
    
    private SecretKey getSigningKey() {
        byte[] keyBytes = jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8);
        return Keys.hmacShaKeyFor(keyBytes);
    }
    
    // Générer un Access Token (courte durée — 24h)
    public String generateAccessToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        
        // Ajouter les rôles dans le token
        claims.put("roles", userDetails.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .collect(Collectors.toList()));
        
        return Jwts.builder()
            .setClaims(claims)
            .setSubject(userDetails.getUsername())
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getExpiration()))
            .signWith(getSigningKey())
            .compact();
    }
    
    // Générer un Refresh Token (longue durée — 30 jours)
    public String generateRefreshToken(UserDetails userDetails) {
        return Jwts.builder()
            .setSubject(userDetails.getUsername())
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + 30L * 24 * 60 * 60 * 1000))
            .signWith(getSigningKey())
            .compact();
    }
    
    // Extraire le nom d'utilisateur du token
    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }
    
    // Extraire un claim générique
    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }
    
    private Claims extractAllClaims(String token) {
        return Jwts.parserBuilder()
            .setSigningKey(getSigningKey())
            .build()
            .parseClaimsJws(token)
            .getBody();
    }
    
    // Vérifier si le token est valide
    public boolean isTokenValid(String token, UserDetails userDetails) {
        try {
            final String username = extractUsername(token);
            return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
        } catch (JwtException e) {
            log.warn("JWT invalide : {}", e.getMessage());
            return false;
        }
    }
    
    private boolean isTokenExpired(String token) {
        return extractClaim(token, Claims::getExpiration).before(new Date());
    }
}
```

### [SIGNAL] JwtFilter — Intercepter chaque requête

```java
package com.libraryhub.security;

@Component
@RequiredArgsConstructor
@Slf4j
// OncePerRequestFilter garantit que ce filtre est appelé UNE SEULE fois par requête
public class JwtFilter extends OncePerRequestFilter {
    
    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl userDetailsService;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        
        // 1. Extraire le header Authorization
        final String authHeader = request.getHeader("Authorization");
        
        // Si pas de JWT ou format incorrect -> passer au filtre suivant
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }
        
        // 2. Extraire le token (enlever "Bearer ")
        final String jwt = authHeader.substring(7);
        
        try {
            // 3. Extraire l'email depuis le JWT
            final String userEmail = jwtUtil.extractUsername(jwt);
            
            // 4. Si l'utilisateur n'est pas encore authentifié dans le contexte de la requête
            if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                
                // 5. Charger les détails de l'utilisateur depuis la base
                UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail);
                
                // 6. Valider le token
                if (jwtUtil.isTokenValid(jwt, userDetails)) {
                    
                    // 7. Créer l'objet d'authentification et le placer dans le contexte
                    UsernamePasswordAuthenticationToken authToken = 
                        new UsernamePasswordAuthenticationToken(
                            userDetails,
                            null,                          // credentials (null car JWT)
                            userDetails.getAuthorities()   // rôles
                        );
                    
                    authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authToken);
                    
                    log.debug("Utilisateur authentifié via JWT : {}", userEmail);
                }
            }
        } catch (JwtException e) {
            log.warn("Token JWT invalide : {}", e.getMessage());
            // On ne lève pas d'exception ici — la requête continue sans authentification
            // Le filtre de sécurité rejettera ensuite si l'endpoint est protégé
        }
        
        // 8. Passer au filtre suivant dans la chaîne
        filterChain.doFilter(request, response);
    }
}
```

### [CONFIG] SecurityConfig — La configuration principale

```java
package com.libraryhub.config;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity  // Active @PreAuthorize, @PostAuthorize dans les controllers
@RequiredArgsConstructor
public class SecurityConfig {
    
    private final JwtFilter jwtFilter;
    private final UserDetailsServiceImpl userDetailsService;
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // Désactiver CSRF — inutile pour une API REST stateless avec JWT
            .csrf(csrf -> csrf.disable())
            
            // Pas de session HTTP — on est stateless avec JWT
            .sessionManagement(session -> 
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            
            // Règles d'autorisation des endpoints
            .authorizeHttpRequests(auth -> auth
                // Endpoints publics — accessibles sans authentification
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers(HttpMethod.GET, "/api/books/**").permitAll()
                .requestMatchers(HttpMethod.GET, "/api/authors/**").permitAll()
                .requestMatchers("/h2-console/**").permitAll()
                .requestMatchers("/actuator/health").permitAll()
                
                // Endpoints LIBRARIAN ou ADMIN
                .requestMatchers("/api/loans/**").hasAnyRole("LIBRARIAN", "ADMIN")
                .requestMatchers(HttpMethod.POST, "/api/books/**").hasAnyRole("LIBRARIAN", "ADMIN")
                .requestMatchers(HttpMethod.PUT, "/api/books/**").hasAnyRole("LIBRARIAN", "ADMIN")
                .requestMatchers(HttpMethod.DELETE, "/api/books/**").hasRole("ADMIN")
                
                // Gestion des membres — ADMIN uniquement
                .requestMatchers("/api/members/**").hasRole("ADMIN")
                
                // Tout le reste nécessite une authentification
                .anyRequest().authenticated()
            )
            
            // Gestion des erreurs d'authentification/autorisation
            .exceptionHandling(exceptions -> exceptions
                .authenticationEntryPoint((request, response, authException) -> {
                    // 401 Unauthorized — non authentifié
                    response.setContentType("application/json");
                    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                    response.getWriter().write(
                        "{\"error\": \"Non authentifié\", \"message\": \"" + 
                        authException.getMessage() + "\"}"
                    );
                })
                .accessDeniedHandler((request, response, accessDeniedException) -> {
                    // 403 Forbidden — authentifié mais pas autorisé
                    response.setContentType("application/json");
                    response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                    response.getWriter().write(
                        "{\"error\": \"Accès refusé\", \"message\": \"" + 
                        accessDeniedException.getMessage() + "\"}"
                    );
                })
            )
            
            // Ajouter notre filtre JWT avant le filtre d'authentification par défaut
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
        
        // Nécessaire pour la console H2 (utilise des frames)
        http.headers(headers -> headers.frameOptions(frame -> frame.disable()));
        
        return http.build();
    }
    
    // Bean PasswordEncoder — BCrypt avec force 10 (recommandée)
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(10);
    }
    
    // Bean AuthenticationManager — utilisé dans AuthService
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) 
            throws Exception {
        return config.getAuthenticationManager();
    }
}
```

### [CLE] AuthController — Login et Refresh

```java
package com.libraryhub.controller;

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
@Slf4j
public class AuthController {
    
    private final AuthService authService;
    
    // POST /api/auth/register
    @PostMapping("/register")
    public ResponseEntity<AuthResponse> register(@RequestBody @Valid RegisterRequest request) {
        AuthResponse response = authService.register(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }
    
    // POST /api/auth/login
    @PostMapping("/login")
    public ResponseEntity<AuthResponse> login(@RequestBody @Valid LoginRequest request) {
        AuthResponse response = authService.login(request);
        return ResponseEntity.ok(response);
    }
    
    // POST /api/auth/refresh
    @PostMapping("/refresh")
    public ResponseEntity<AuthResponse> refreshToken(@RequestBody @Valid RefreshTokenRequest request) {
        return ResponseEntity.ok(authService.refreshToken(request.getRefreshToken()));
    }
    
    // GET /api/auth/me — Profil de l'utilisateur connecté
    @GetMapping("/me")
    public ResponseEntity<UserProfileDTO> getCurrentUser(
            @AuthenticationPrincipal UserDetails userDetails) {
        // @AuthenticationPrincipal injecte l'utilisateur depuis le SecurityContext
        return ResponseEntity.ok(authService.getCurrentUserProfile(userDetails.getUsername()));
    }
}
```

```java
package com.libraryhub.service;

@Service
@RequiredArgsConstructor
@Slf4j
public class AuthService {
    
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final JwtUtil jwtUtil;
    private final AuthenticationManager authenticationManager;
    
    public AuthResponse register(RegisterRequest request) {
        // Vérifier que l'email n'est pas déjà utilisé
        if (userRepository.existsByEmail(request.getEmail())) {
            throw new EmailAlreadyExistsException("Email déjà utilisé : " + request.getEmail());
        }
        
        User user = User.builder()
            .email(request.getEmail())
            .password(passwordEncoder.encode(request.getPassword())) // Hash du mot de passe
            .role(Role.MEMBER) // Rôle par défaut
            .enabled(true)
            .build();
        
        userRepository.save(user);
        log.info("Nouvel utilisateur enregistré : {}", user.getEmail());
        
        // Générer les tokens directement après l'inscription
        String accessToken = jwtUtil.generateAccessToken(user);
        String refreshToken = jwtUtil.generateRefreshToken(user);
        
        return AuthResponse.builder()
            .accessToken(accessToken)
            .refreshToken(refreshToken)
            .email(user.getEmail())
            .role(user.getRole().name())
            .build();
    }
    
    public AuthResponse login(LoginRequest request) {
        try {
            // Spring Security vérifie email + mot de passe via UserDetailsService
            authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    request.getEmail(),
                    request.getPassword()
                )
            );
        } catch (BadCredentialsException e) {
            throw new InvalidCredentialsException("Email ou mot de passe incorrect");
        }
        
        User user = userRepository.findByEmail(request.getEmail())
            .orElseThrow(() -> new UserNotFoundException("Utilisateur non trouvé"));
        
        String accessToken = jwtUtil.generateAccessToken(user);
        String refreshToken = jwtUtil.generateRefreshToken(user);
        
        log.info("Connexion réussie pour : {}", user.getEmail());
        
        return AuthResponse.builder()
            .accessToken(accessToken)
            .refreshToken(refreshToken)
            .email(user.getEmail())
            .role(user.getRole().name())
            .build();
    }
    
    public AuthResponse refreshToken(String refreshToken) {
        String email = jwtUtil.extractUsername(refreshToken);
        
        User user = userRepository.findByEmail(email)
            .orElseThrow(() -> new UserNotFoundException("Utilisateur non trouvé"));
        
        if (!jwtUtil.isTokenValid(refreshToken, user)) {
            throw new InvalidTokenException("Refresh token invalide ou expiré");
        }
        
        // Générer un nouveau access token uniquement
        String newAccessToken = jwtUtil.generateAccessToken(user);
        
        return AuthResponse.builder()
            .accessToken(newAccessToken)
            .refreshToken(refreshToken) // Réutiliser le même refresh token
            .email(user.getEmail())
            .role(user.getRole().name())
            .build();
    }
}
```

### [SECURITE] `@PreAuthorize` — Sécurité fine dans les services

```java
@Service
@RequiredArgsConstructor
public class LoanService {
    
    // Seul un ADMIN ou LIBRARIAN peut voir tous les emprunts
    @PreAuthorize("hasAnyRole('ADMIN', 'LIBRARIAN')")
    public List<LoanDTO> findAllLoans() {
        return loanRepository.findAll().stream()
            .map(loanMapper::toDTO)
            .collect(Collectors.toList());
    }
    
    // Un MEMBER peut voir uniquement SES emprunts
    // Un ADMIN ou LIBRARIAN peut voir ceux de n'importe qui
    @PreAuthorize("hasAnyRole('ADMIN', 'LIBRARIAN') or " +
                  "#memberId == authentication.principal.id")
    public List<LoanDTO> findLoansByMember(Long memberId) {
        return loanRepository.findByMemberId(memberId)
            .stream()
            .map(loanMapper::toDTO)
            .collect(Collectors.toList());
    }
    
    // Vérification après l'exécution (pour accès basé sur l'objet retourné)
    @PostAuthorize("returnObject.memberEmail == authentication.name or hasRole('ADMIN')")
    public LoanDTO findLoanById(Long id) {
        return loanMapper.toDTO(
            loanRepository.findById(id)
                .orElseThrow(() -> new LoanNotFoundException("Emprunt non trouvé"))
        );
    }
}
```

### [LISTE] DTOs d'authentification

```java
// LoginRequest
@Data
public class LoginRequest {
    @NotBlank @Email
    private String email;
    
    @NotBlank
    private String password;
}

// RegisterRequest
@Data
public class RegisterRequest {
    @NotBlank @Size(min = 2)
    private String firstName;
    
    @NotBlank @Size(min = 2)
    private String lastName;
    
    @NotBlank @Email
    private String email;
    
    @NotBlank @Size(min = 8)
    @Pattern(regexp = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).*$")
    private String password;
}

// AuthResponse — retourné après login/register
@Data
@Builder
public class AuthResponse {
    private String accessToken;
    private String refreshToken;
    private String email;
    private String role;
    private String tokenType = "Bearer";
}
```

---

## [OK] Exercices du Module 04

1. **Test de sécurité :** Avec Postman, vérifie que :
   - `GET /api/books` -> 200 (public)
   - `POST /api/books` sans token -> 401
   - `POST /api/books` avec token MEMBER -> 403
   - `POST /api/books` avec token LIBRARIAN -> 201

2. **Refresh Token :** Implémente une table `RefreshToken` en base de données pour invalider les refresh tokens à la déconnexion (endpoint `POST /api/auth/logout`).

3. **Rôle dynamique :** Ajoute `PATCH /api/users/{id}/role` accessible uniquement aux ADMINs pour changer le rôle d'un utilisateur.

4. **Sécurité objet :** Un MEMBER ne doit pouvoir voir que SES propres emprunts. Implémente cette vérification dans `LoanService.findLoanById()` avec `@PostAuthorize`.

5. **Défi :** Implémente la rotation des refresh tokens : à chaque utilisation d'un refresh token, génère un nouveau refresh token et invalide l'ancien.

---

> -> Continue avec `05_ARCHITECTURE.md`


# [LIVRE] Module 05 — Architecture, Exceptions, Logging & Tests

> **Parties couvertes :** Partie 6 (Chapitres 15–17) + Partie 7 (Chapitres 18–19)
> **Projet fil rouge :** Rendre LibraryHub robuste, maintenable et testée

---

## Chapitre 15 — Architecture en couches

### [CONSTRUCTION] Séparation des responsabilités dans LibraryHub

Chaque couche a **une seule responsabilité** et ne connaît que la couche en dessous.

```
┌─────────────────────────────────────────────────────────────┐
│  COUCHE PRÉSENTATION (Controller)                           │
│  • Reçoit les requêtes HTTP                                 │
│  • Valide les données d'entrée (@Valid)                     │
│  • Délègue au Service                                       │
│  • Retourne la réponse HTTP correcte                        │
│  • NE contient PAS de logique métier                        │
├─────────────────────────────────────────────────────────────┤
│  COUCHE MÉTIER (Service)                                    │
│  • Contient toute la logique métier                         │
│  • Orchestre les repositories                               │
│  • Gère les transactions (@Transactional)                   │
│  • Lance les exceptions métier                              │
│  • NE connaît PAS HTTP, NE connaît PAS JPA directement      │
├─────────────────────────────────────────────────────────────┤
│  COUCHE DONNÉES (Repository)                                │
│  • Accès à la base de données uniquement                    │
│  • Requêtes JPQL/SQL                                        │
│  • NE contient PAS de logique métier                        │
└─────────────────────────────────────────────────────────────┘
```

**Règle de Clean Code :** Une méthode = une action, un nom = une intention.

```java
// [X] Controller qui fait trop de choses
@PostMapping("/loans")
public ResponseEntity<LoanDTO> createLoan(@RequestBody LoanCreateRequest request) {
    Member member = memberRepository.findById(request.getMemberId()).orElseThrow();
    if (member.getStatus() != MemberStatus.ACTIVE) throw new Exception("...");
    long count = loanRepository.countByMemberIdAndStatus(member.getId(), LoanStatus.ACTIVE);
    if (count >= 3) throw new Exception("...");
    // ... 40 lignes de logique métier dans le controller
}

// [OK] Controller qui délègue proprement
@PostMapping("/loans")
public ResponseEntity<LoanDTO> createLoan(@RequestBody @Valid LoanCreateRequest request) {
    LoanDTO loan = loanService.createLoan(request);  // 1 ligne !
    return ResponseEntity.created(URI.create("/api/loans/" + loan.getId())).body(loan);
}
```

---

## Chapitre 16 — Gestion des exceptions

### [ALERTE] La hiérarchie des exceptions LibraryHub

```
RuntimeException
│
├── LibraryHubException (base)
│   ├── NotFoundException
│   │   ├── BookNotFoundException
│   │   ├── AuthorNotFoundException
│   │   ├── MemberNotFoundException
│   │   └── LoanNotFoundException
│   │
│   ├── ConflictException
│   │   ├── BookNotAvailableException
│   │   ├── EmailAlreadyExistsException
│   │   └── LoanAlreadyReturnedException
│   │
│   └── BusinessException
│       ├── MaxLoansExceededException
│       ├── MemberSuspendedException
│       └── InvalidCredentialsException
```

```java
// Exception de base — toutes les exceptions métier héritent de celle-ci
package com.libraryhub.exception;

public class LibraryHubException extends RuntimeException {
    
    private final String errorCode;
    
    public LibraryHubException(String message, String errorCode) {
        super(message);
        this.errorCode = errorCode;
    }
    
    public String getErrorCode() { return errorCode; }
}

// Exceptions spécifiques
public class BookNotFoundException extends LibraryHubException {
    public BookNotFoundException(Long id) {
        super("Livre non trouvé avec l'ID: " + id, "BOOK_NOT_FOUND");
    }
}

public class BookNotAvailableException extends LibraryHubException {
    public BookNotAvailableException(String title) {
        super("Le livre '" + title + "' est déjà emprunté", "BOOK_NOT_AVAILABLE");
    }
}

public class MaxLoansExceededException extends LibraryHubException {
    public MaxLoansExceededException(int max) {
        super("Nombre maximum d'emprunts atteint (" + max + ")", "MAX_LOANS_EXCEEDED");
    }
}
```

### [SECURITE] `@RestControllerAdvice` — Le gestionnaire global

```java
package com.libraryhub.exception;

@RestControllerAdvice  // Intercepte les exceptions de tous les controllers
@Slf4j
public class GlobalExceptionHandler {
    
    // ─────────────────────────────────────────────────────
    // Réponse d'erreur standard
    // ─────────────────────────────────────────────────────
    @Data
    @Builder
    public static class ErrorResponse {
        private LocalDateTime timestamp;
        private int status;
        private String error;
        private String message;
        private String errorCode;
        private String path;
        private Map<String, String> fieldErrors;
    }
    
    // ─────────────────────────────────────────────────────
    // 404 — Ressource non trouvée
    // ─────────────────────────────────────────────────────
    @ExceptionHandler(BookNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ErrorResponse handleBookNotFound(BookNotFoundException ex, HttpServletRequest request) {
        log.warn("Ressource non trouvée : {}", ex.getMessage());
        return ErrorResponse.builder()
            .timestamp(LocalDateTime.now())
            .status(404)
            .error("Not Found")
            .message(ex.getMessage())
            .errorCode(ex.getErrorCode())
            .path(request.getRequestURI())
            .build();
    }
    
    // Gérer toutes les NotFoundException avec un seul handler
    @ExceptionHandler({
        AuthorNotFoundException.class,
        MemberNotFoundException.class,
        LoanNotFoundException.class
    })
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ErrorResponse handleNotFound(LibraryHubException ex, HttpServletRequest request) {
        return buildError(HttpStatus.NOT_FOUND, ex, request);
    }
    
    // ─────────────────────────────────────────────────────
    // 409 — Conflit
    // ─────────────────────────────────────────────────────
    @ExceptionHandler({
        BookNotAvailableException.class,
        EmailAlreadyExistsException.class,
        LoanAlreadyReturnedException.class
    })
    @ResponseStatus(HttpStatus.CONFLICT)
    public ErrorResponse handleConflict(LibraryHubException ex, HttpServletRequest request) {
        return buildError(HttpStatus.CONFLICT, ex, request);
    }
    
    // ─────────────────────────────────────────────────────
    // 422 — Règle métier violée
    // ─────────────────────────────────────────────────────
    @ExceptionHandler({
        MaxLoansExceededException.class,
        MemberSuspendedException.class
    })
    @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
    public ErrorResponse handleBusinessError(LibraryHubException ex, HttpServletRequest request) {
        return buildError(HttpStatus.UNPROCESSABLE_ENTITY, ex, request);
    }
    
    // ─────────────────────────────────────────────────────
    // 400 — Erreurs de validation (@Valid)
    // ─────────────────────────────────────────────────────
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleValidationErrors(MethodArgumentNotValidException ex, 
                                                HttpServletRequest request) {
        Map<String, String> fieldErrors = new LinkedHashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error ->
            fieldErrors.put(error.getField(), error.getDefaultMessage())
        );
        
        log.warn("Erreur de validation : {}", fieldErrors);
        
        return ErrorResponse.builder()
            .timestamp(LocalDateTime.now())
            .status(400)
            .error("Validation Failed")
            .message("Les données envoyées sont invalides")
            .errorCode("VALIDATION_ERROR")
            .path(request.getRequestURI())
            .fieldErrors(fieldErrors)
            .build();
    }
    
    // ─────────────────────────────────────────────────────
    // 401 — Non authentifié
    // ─────────────────────────────────────────────────────
    @ExceptionHandler(InvalidCredentialsException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public ErrorResponse handleUnauthorized(InvalidCredentialsException ex, 
                                            HttpServletRequest request) {
        return buildError(HttpStatus.UNAUTHORIZED, ex, request);
    }
    
    // ─────────────────────────────────────────────────────
    // 500 — Erreur inattendue (toujours attraper en dernier)
    // ─────────────────────────────────────────────────────
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResponse handleUnexpectedError(Exception ex, HttpServletRequest request) {
        // Log complet avec stack trace pour debug
        log.error("Erreur inattendue sur {} {}", request.getMethod(), request.getRequestURI(), ex);
        
        return ErrorResponse.builder()
            .timestamp(LocalDateTime.now())
            .status(500)
            .error("Internal Server Error")
            // Ne jamais exposer les détails techniques en prod !
            .message("Une erreur inattendue s'est produite. Veuillez réessayer.")
            .errorCode("INTERNAL_ERROR")
            .path(request.getRequestURI())
            .build();
    }
    
    private ErrorResponse buildError(HttpStatus status, LibraryHubException ex, 
                                     HttpServletRequest request) {
        return ErrorResponse.builder()
            .timestamp(LocalDateTime.now())
            .status(status.value())
            .error(status.getReasonPhrase())
            .message(ex.getMessage())
            .errorCode(ex.getErrorCode())
            .path(request.getRequestURI())
            .build();
    }
}
```

---

## Chapitre 17 — Logging

### [NOTE] Configuration des logs avec Logback

```yaml
# application.yml — Configuration logging
logging:
  level:
    root: WARN                        # Niveau par défaut
    com.libraryhub: DEBUG             # Logs détaillés pour notre code
    org.springframework.web: INFO     # Logs Spring Web
    org.hibernate.SQL: DEBUG          # Affiche les requêtes SQL
    org.hibernate.type.descriptor.sql: TRACE  # Affiche les valeurs des paramètres
  
  pattern:
    # Pattern des logs en console : [niveau] timestamp [thread] logger - message
    console: "%clr(%d{HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n"
  
  file:
    name: logs/libraryhub.log  # Fichier de log en production
```

### [RECHERCHE] Logging dans LibraryHub — Bonnes pratiques

```java
@Service
@Slf4j  // Lombok génère : private static final Logger log = LoggerFactory.getLogger(BookService.class);
public class LoanService {
    
    public LoanDTO createLoan(LoanCreateRequest request) {
        // [OK] DEBUG — Informations de développement, désactivées en prod
        log.debug("Tentative de création d'emprunt : memberId={}, bookId={}", 
            request.getMemberId(), request.getBookId());
        
        // ... logique métier ...
        
        // [OK] INFO — Événements importants de l'application
        log.info("Emprunt créé avec succès : loanId={}, member={}, book='{}', retour={}", 
            savedLoan.getId(), member.getEmail(), book.getTitle(), savedLoan.getDueDate());
        
        return loanMapper.toDTO(savedLoan);
    }
    
    public List<LoanDTO> findOverdueLoans() {
        List<Loan> overdueLoans = loanRepository.findOverdueLoans(LocalDate.now());
        
        // [OK] WARN — Situations anormales mais récupérables
        if (!overdueLoans.isEmpty()) {
            log.warn("{} emprunt(s) en retard détecté(s)", overdueLoans.size());
        }
        
        return overdueLoans.stream().map(loanMapper::toDTO).collect(Collectors.toList());
    }
    
    @Scheduled(cron = "0 0 8 * * *")  // Chaque matin à 8h
    public void markOverdueLoans() {
        try {
            int updated = loanRepository.updateOverdueLoans(LocalDate.now());
            log.info("Mise à jour des emprunts en retard : {} emprunt(s) marqué(s)", updated);
        } catch (Exception e) {
            // [OK] ERROR — Erreurs qui nécessitent une attention immédiate
            log.error("Échec de la mise à jour des emprunts en retard", e);
        }
    }
    
    // [X] Ce qu'il ne faut PAS logger
    public void badLogging(String password, User user) {
        log.info("Connexion avec password={}", password);  // JAMAIS logger un mot de passe !
        log.info("User: {}", user);                        // Peut logger des données sensibles
        log.debug("Entering method");                      // Inutile
    }
}
```

---

## Chapitre 18 — Tests unitaires

### [TEST] Structure d'un test JUnit 5 avec Mockito

```java
package com.libraryhub.service;

// @ExtendWith(MockitoExtension.class) active Mockito pour cette classe de test
@ExtendWith(MockitoExtension.class)
class BookServiceTest {
    
    // @Mock crée un faux objet (mock) — ses méthodes retournent null/0 par défaut
    @Mock
    private BookRepository bookRepository;
    
    @Mock
    private AuthorRepository authorRepository;
    
    @Mock
    private BookMapper bookMapper;
    
    // @InjectMocks crée une vraie instance et injecte les mocks dedans
    @InjectMocks
    private BookService bookService;
    
    // Données de test réutilisables
    private Book testBook;
    private BookDTO testBookDTO;
    private Author testAuthor;
    
    @BeforeEach  // Appelé avant chaque test
    void setUp() {
        testAuthor = Author.builder()
            .id(1L)
            .firstName("Robert")
            .lastName("Martin")
            .build();
        
        testBook = Book.builder()
            .id(1L)
            .title("Clean Code")
            .isbn("978-0-13-235088-4")
            .author(testAuthor)
            .build();
        
        testBookDTO = BookDTO.builder()
            .id(1L)
            .title("Clean Code")
            .authorName("Robert Martin")
            .build();
    }
    
    // ════════════════════════════════════════════
    // Tests de findById
    // ════════════════════════════════════════════
    
    @Test
    @DisplayName("findById : doit retourner le DTO quand le livre existe")
    void findById_WhenBookExists_ReturnsDTO() {
        // GIVEN — Mise en place du comportement des mocks
        when(bookRepository.findById(1L)).thenReturn(Optional.of(testBook));
        when(bookMapper.toDTO(testBook)).thenReturn(testBookDTO);
        
        // WHEN — Appel de la méthode testée
        BookDTO result = bookService.findById(1L);
        
        // THEN — Vérification des résultats
        assertNotNull(result);
        assertEquals("Clean Code", result.getTitle());
        assertEquals("Robert Martin", result.getAuthorName());
        
        // Vérifier que findById du repository a été appelé une fois avec l'ID 1
        verify(bookRepository, times(1)).findById(1L);
        verify(bookMapper, times(1)).toDTO(testBook);
    }
    
    @Test
    @DisplayName("findById : doit lever BookNotFoundException quand le livre n'existe pas")
    void findById_WhenBookNotExists_ThrowsException() {
        // GIVEN
        when(bookRepository.findById(99L)).thenReturn(Optional.empty());
        
        // THEN — Vérifier que l'exception est levée
        assertThrows(BookNotFoundException.class, () -> bookService.findById(99L));
        
        // Vérifier que le mapper N'A PAS été appelé (livre non trouvé)
        verify(bookMapper, never()).toDTO(any());
    }
    
    // ════════════════════════════════════════════
    // Tests de create
    // ════════════════════════════════════════════
    
    @Test
    @DisplayName("create : doit créer et retourner le livre")
    void create_WithValidRequest_ReturnsCreatedBook() {
        // GIVEN
        BookCreateRequest request = new BookCreateRequest();
        request.setTitle("Clean Code");
        request.setIsbn("978-0-13-235088-4");
        request.setAuthorId(1L);
        request.setCategoryId(1L);
        
        Category category = new Category(1L, "Informatique", null);
        
        when(authorRepository.findById(1L)).thenReturn(Optional.of(testAuthor));
        when(categoryRepository.findById(1L)).thenReturn(Optional.of(category));
        when(bookMapper.toEntity(request)).thenReturn(testBook);
        when(bookRepository.save(any(Book.class))).thenReturn(testBook);
        when(bookMapper.toDTO(testBook)).thenReturn(testBookDTO);
        
        // WHEN
        BookDTO result = bookService.create(request);
        
        // THEN
        assertNotNull(result);
        assertEquals("Clean Code", result.getTitle());
        
        // Vérifier que save a été appelé
        verify(bookRepository, times(1)).save(any(Book.class));
    }
    
    @Test
    @DisplayName("create : doit lever exception si auteur non trouvé")
    void create_WithInvalidAuthor_ThrowsException() {
        // GIVEN
        BookCreateRequest request = new BookCreateRequest();
        request.setAuthorId(999L);
        
        when(authorRepository.findById(999L)).thenReturn(Optional.empty());
        
        // THEN
        assertThrows(AuthorNotFoundException.class, () -> bookService.create(request));
        
        // Le repository de livres ne doit PAS être appelé
        verify(bookRepository, never()).save(any());
    }
    
    // ════════════════════════════════════════════
    // Tests avec ArgumentCaptor
    // ════════════════════════════════════════════
    
    @Test
    @DisplayName("create : doit assigner l'auteur et la catégorie au livre avant de sauvegarder")
    void create_ShouldAssignAuthorAndCategoryToBook() {
        // GIVEN
        BookCreateRequest request = new BookCreateRequest();
        request.setAuthorId(1L);
        request.setCategoryId(1L);
        
        Category category = new Category(1L, "Informatique", null);
        
        when(authorRepository.findById(1L)).thenReturn(Optional.of(testAuthor));
        when(categoryRepository.findById(1L)).thenReturn(Optional.of(category));
        when(bookMapper.toEntity(request)).thenReturn(new Book());
        when(bookRepository.save(any())).thenReturn(testBook);
        when(bookMapper.toDTO(any())).thenReturn(testBookDTO);
        
        // WHEN
        bookService.create(request);
        
        // THEN — Capturer l'objet passé à save() pour vérifier ses propriétés
        ArgumentCaptor<Book> bookCaptor = ArgumentCaptor.forClass(Book.class);
        verify(bookRepository).save(bookCaptor.capture());
        
        Book capturedBook = bookCaptor.getValue();
        assertEquals(testAuthor, capturedBook.getAuthor());
        assertEquals(category, capturedBook.getCategory());
    }
}
```

```java
// LoanServiceTest.java — Tests de logique métier complexe
@ExtendWith(MockitoExtension.class)
class LoanServiceTest {
    
    @Mock private LoanRepository loanRepository;
    @Mock private BookRepository bookRepository;
    @Mock private MemberRepository memberRepository;
    @Mock private LoanMapper loanMapper;
    @Mock private LibraryHubProperties properties;
    
    @InjectMocks
    private LoanService loanService;
    
    @Test
    @DisplayName("createLoan : doit échouer si le livre est déjà emprunté")
    void createLoan_WhenBookAlreadyLoaned_ThrowsException() {
        // GIVEN
        LoanCreateRequest request = new LoanCreateRequest(1L, 1L);
        Member activeMember = Member.builder().id(1L).status(MemberStatus.ACTIVE).build();
        Book book = Book.builder().id(1L).title("Clean Code").build();
        
        when(memberRepository.findById(1L)).thenReturn(Optional.of(activeMember));
        when(bookRepository.findById(1L)).thenReturn(Optional.of(book));
        when(loanRepository.findByMemberIdAndStatus(1L, LoanStatus.ACTIVE))
            .thenReturn(Collections.emptyList());
        when(loanRepository.existsByBookIdAndStatus(1L, LoanStatus.ACTIVE))
            .thenReturn(true); // Le livre est déjà emprunté
        
        // THEN
        BookNotAvailableException exception = assertThrows(
            BookNotAvailableException.class, 
            () -> loanService.createLoan(request)
        );
        
        assertTrue(exception.getMessage().contains("Clean Code"));
        verify(loanRepository, never()).save(any()); // Aucun emprunt créé
    }
    
    @Test
    @DisplayName("createLoan : doit échouer si le membre est suspendu")
    void createLoan_WhenMemberSuspended_ThrowsException() {
        LoanCreateRequest request = new LoanCreateRequest(1L, 1L);
        Member suspendedMember = Member.builder().id(1L).status(MemberStatus.SUSPENDED).build();
        
        when(memberRepository.findById(1L)).thenReturn(Optional.of(suspendedMember));
        
        assertThrows(MemberSuspendedException.class, () -> loanService.createLoan(request));
    }
}
```

---

## Chapitre 19 — Tests d'intégration

### [LIEN] `@SpringBootTest` — Tester avec le contexte complet

```java
package com.libraryhub.controller;

// @SpringBootTest lance le contexte Spring complet (comme en production)
// webEnvironment = RANDOM_PORT démarre le serveur sur un port aléatoire
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")  // Utilise application-test.yml (H2 en mémoire)
@Transactional  // Rollback après chaque test -> base de données propre
class BookControllerIntegrationTest {
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Autowired
    private BookRepository bookRepository;
    
    @Autowired
    private AuthorRepository authorRepository;
    
    @BeforeEach
    void setUp() {
        // Insérer des données de test
        Author author = authorRepository.save(
            Author.builder().firstName("Robert").lastName("Martin").build()
        );
        bookRepository.save(
            Book.builder().title("Clean Code").isbn("978-1").author(author).build()
        );
        bookRepository.save(
            Book.builder().title("Clean Architecture").isbn("978-2").author(author).build()
        );
    }
    
    @Test
    @DisplayName("GET /api/books : doit retourner tous les livres")
    void getAllBooks_ReturnsAllBooks() {
        ResponseEntity<String> response = restTemplate.getForEntity("/api/books", String.class);
        
        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertNotNull(response.getBody());
        assertTrue(response.getBody().contains("Clean Code"));
    }
    
    @Test
    @DisplayName("GET /api/books/{id} : doit retourner 404 pour un ID inexistant")
    void getBookById_NotFound_Returns404() {
        ResponseEntity<String> response = restTemplate.getForEntity("/api/books/9999", String.class);
        
        assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
    }
}
```

### [SCENARIO] `@WebMvcTest` — Tester le controller isolément (plus rapide)

```java
// @WebMvcTest charge SEULEMENT la couche web (controllers, filters)
// Bien plus rapide que @SpringBootTest
@WebMvcTest(BookController.class)
@ActiveProfiles("test")
class BookControllerTest {
    
    // MockMvc simule les requêtes HTTP sans vrai serveur
    @Autowired
    private MockMvc mockMvc;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    // @MockBean crée un mock et le place dans le contexte Spring
    @MockBean
    private BookService bookService;
    
    @MockBean
    private JwtUtil jwtUtil;
    
    @MockBean
    private UserDetailsServiceImpl userDetailsService;
    
    @Test
    @DisplayName("GET /api/books : doit retourner une liste de livres avec statut 200")
    void getAllBooks_ReturnsOk() throws Exception {
        // GIVEN
        List<BookDTO> books = List.of(
            BookDTO.builder().id(1L).title("Clean Code").authorName("Robert Martin").build(),
            BookDTO.builder().id(2L).title("DDIA").authorName("Martin Kleppmann").build()
        );
        when(bookService.findAll(anyInt(), anyInt(), anyString(), anyString()))
            .thenReturn(new PageImpl<>(books));
        
        // WHEN & THEN — Requête simulée + vérification de la réponse
        mockMvc.perform(get("/api/books")
                .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.content", hasSize(2)))
            .andExpect(jsonPath("$.content[0].title").value("Clean Code"))
            .andExpect(jsonPath("$.content[1].title").value("DDIA"));
    }
    
    @Test
    @DisplayName("POST /api/books : doit retourner 401 sans authentification")
    void createBook_WithoutAuth_Returns401() throws Exception {
        mockMvc.perform(post("/api/books")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"title\":\"Test\",\"isbn\":\"123\",\"authorId\":1,\"categoryId\":1}"))
            .andExpect(status().isUnauthorized());
    }
    
    @Test
    @DisplayName("POST /api/books : doit retourner 400 avec données invalides")
    void createBook_WithInvalidData_Returns400() throws Exception {
        // JSON avec des champs manquants/invalides
        String invalidJson = "{\"title\":\"\",\"isbn\":\"\"}";
        
        mockMvc.perform(post("/api/books")
                .with(jwt().authorities(new SimpleGrantedAuthority("ROLE_ADMIN")))
                .contentType(MediaType.APPLICATION_JSON)
                .content(invalidJson))
            .andExpect(status().isBadRequest())
            .andExpect(jsonPath("$.errorCode").value("VALIDATION_ERROR"))
            .andExpect(jsonPath("$.fieldErrors.title").exists())
            .andExpect(jsonPath("$.fieldErrors.isbn").exists());
    }
    
    @Test
    @DisplayName("DELETE /api/books/{id} : doit retourner 204 pour ADMIN")
    void deleteBook_AsAdmin_Returns204() throws Exception {
        doNothing().when(bookService).delete(1L);
        
        mockMvc.perform(delete("/api/books/1")
                .with(jwt().authorities(new SimpleGrantedAuthority("ROLE_ADMIN"))))
            .andExpect(status().isNoContent());
        
        verify(bookService, times(1)).delete(1L);
    }
}
```

### [ARCHIVE] Tests de repository avec `@DataJpaTest`

```java
// @DataJpaTest charge uniquement la couche JPA + H2
// Plus rapide que @SpringBootTest pour tester les requêtes
@DataJpaTest
@ActiveProfiles("test")
class LoanRepositoryTest {
    
    @Autowired
    private LoanRepository loanRepository;
    
    @Autowired
    private BookRepository bookRepository;
    
    @Autowired
    private MemberRepository memberRepository;
    
    // TestEntityManager : utilitaire pour insérer des données de test
    @Autowired
    private TestEntityManager entityManager;
    
    @Test
    @DisplayName("findOverdueLoans : doit retourner les emprunts en retard")
    void findOverdueLoans_ReturnsOnlyOverdueLoans() {
        // GIVEN — Insérer des données directement via EntityManager
        Author author = entityManager.persistAndFlush(
            Author.builder().firstName("Test").lastName("Author").build()
        );
        Book book1 = entityManager.persistAndFlush(
            Book.builder().title("Book 1").isbn("ISBN-001").author(author).build()
        );
        Book book2 = entityManager.persistAndFlush(
            Book.builder().title("Book 2").isbn("ISBN-002").author(author).build()
        );
        Member member = entityManager.persistAndFlush(
            Member.builder().firstName("Alice").lastName("Martin")
                .email("alice@test.com").status(MemberStatus.ACTIVE).build()
        );
        
        // Emprunt en retard (due_date dans le passé)
        Loan overdueloan = entityManager.persistAndFlush(
            Loan.builder().member(member).book(book1)
                .loanDate(LocalDate.now().minusDays(30))
                .dueDate(LocalDate.now().minusDays(9))  // En retard !
                .status(LoanStatus.ACTIVE)
                .build()
        );
        
        // Emprunt normal (due_date dans le futur)
        entityManager.persistAndFlush(
            Loan.builder().member(member).book(book2)
                .loanDate(LocalDate.now().minusDays(5))
                .dueDate(LocalDate.now().plusDays(16))  // Pas en retard
                .status(LoanStatus.ACTIVE)
                .build()
        );
        
        // WHEN
        List<Loan> overdueLoans = loanRepository.findOverdueLoans(LocalDate.now());
        
        // THEN
        assertEquals(1, overdueLoans.size());
        assertEquals(overdueloan.getId(), overdueLoans.get(0).getId());
    }
}
```

---

## [OK] Exercices des Modules 05 & 06

1. **Exception custom :** Crée `LoanOverdueException` avec un code `LOAN_OVERDUE`. Lance-la dans `LoanService.returnBook()` si le livre est retourné en retard, et log un WARN avec les détails (membre, livre, retard en jours).

2. **Tests unitaires :** Écris 5 tests pour `MemberService.register()` couvrant : email unique, mot de passe hashé, statut initial ACTIVE, retour DTO correct, et exception si email dupliqué.

3. **Test d'intégration :** Écris un test `@WebMvcTest` pour `LoanController` vérifiant que `POST /api/loans` avec un token MEMBER retourne 200, et sans token retourne 401.

4. **Logging :** Ajoute un aspect (`@Aspect`) `LoggingAspect` qui log automatiquement l'entrée et la sortie de chaque méthode publique des services (avec nom de la méthode, paramètres, et temps d'exécution).

5. **Couverture :** Lance `mvn test jacoco:report` et vise **80% de couverture** sur les services.

---

> -> Continue avec `07_PERFORMANCE_AVANCE.md`


# [LIVRE] Module 07 — Performance, Scalabilité & Déploiement

> **Parties couvertes :** Partie 8 (Chapitres 20–21) + Partie 9 (Chapitres 22–24) + Partie 10 (Chapitres 25–27)
> **Projet fil rouge :** Optimiser, déployer et monitorer LibraryHub en production

---

## Chapitre 20 — Performance

### [FICHIER] Pagination — Éviter de charger toute la base

La pagination est **indispensable** dès qu'une collection peut contenir plus de quelques dizaines d'éléments.

```java
// [X] Ne jamais faire ça en production
List<Book> allBooks = bookRepository.findAll(); // Si 50 000 livres -> OOM possible !

// [OK] Toujours paginer
Page<Book> books = bookRepository.findAll(PageRequest.of(0, 20, Sort.by("title")));
```

Déjà implémenté dans LibraryHub. Rappel de l'utilisation :

```bash
# Appels API paginés
GET /api/books?page=0&size=20&sortBy=title&direction=asc
GET /api/books?page=2&size=10&sortBy=publishedDate&direction=desc
```

### [RAPIDE] Cache avec Spring Cache

Le cache évite d'aller chercher en base des données qui changent peu.

```java
// 1. Activer le cache dans la configuration
@Configuration
@EnableCaching
public class CacheConfig {
    
    // Cache simple en mémoire (pour le développement/petite prod)
    @Bean
    public CacheManager cacheManager() {
        ConcurrentMapCacheManager manager = new ConcurrentMapCacheManager(
            "books", "authors", "categories", "booksByCategory"
        );
        return manager;
    }
}
```

```yaml
# Pour utiliser Redis en production :
spring:
  cache:
    type: redis
  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: 6379
      password: ${REDIS_PASSWORD:}
```

```java
@Service
@RequiredArgsConstructor
@Slf4j
public class BookService {
    
    // @Cacheable : si le résultat est déjà en cache, retourne-le directement
    // sans appeler la méthode (donc sans aller en base)
    @Cacheable(value = "books", key = "#id")
    public BookDTO findById(Long id) {
        log.debug("Cache MISS pour book#{} — requête en base", id);
        return bookMapper.toDTO(
            bookRepository.findById(id)
                .orElseThrow(() -> new BookNotFoundException(id))
        );
    }
    
    // @CachePut : met à jour le cache APRÈS l'exécution de la méthode
    // (la méthode est toujours appelée)
    @CachePut(value = "books", key = "#result.id")
    @Transactional
    public BookDTO update(Long id, BookUpdateRequest request) {
        Book book = bookRepository.findById(id)
            .orElseThrow(() -> new BookNotFoundException(id));
        book.setTitle(request.getTitle());
        return bookMapper.toDTO(bookRepository.save(book));
    }
    
    // @CacheEvict : supprime du cache (après une mise à jour ou suppression)
    @CacheEvict(value = "books", key = "#id")
    @Transactional
    public void delete(Long id) {
        bookRepository.deleteById(id);
    }
    
    // Vider tout le cache d'une catégorie (si les livres d'une catégorie changent)
    @CacheEvict(value = "booksByCategory", allEntries = true)
    public void invalidateCategoryCache() { }
    
    // @Cacheable sur une liste — cache toute la liste sous une clé
    @Cacheable(value = "categories")
    public List<CategoryDTO> findAllCategories() {
        return categoryRepository.findAll()
            .stream()
            .map(categoryMapper::toDTO)
            .collect(Collectors.toList());
    }
}
```

### [RAPIDE] Optimisation JPA

```java
// 1. Projections — Sélectionner seulement les colonnes nécessaires
// Au lieu de charger l'entité entière (avec tous ses champs et relations),
// ne charge que ce dont tu as besoin
public interface BookSummary {
    Long getId();
    String getTitle();
    String getIsbn();
    // Pas de relations chargées -> beaucoup plus rapide
}

public interface BookRepository extends JpaRepository<Book, Long> {
    
    // Spring Data génère automatiquement une requête SELECT id, title, isbn
    List<BookSummary> findAllProjectedBy();
    
    // Avec Spring Projections fermées
    @Query("SELECT b.id AS id, b.title AS title, b.isbn AS isbn FROM Book b")
    List<BookSummary> findBookSummaries();
}

// 2. Batch operations — Insérer/modifier plusieurs entités à la fois
@Modifying
@Query("UPDATE Loan l SET l.status = 'OVERDUE' WHERE l.dueDate < :today AND l.status = 'ACTIVE'")
@Transactional
int updateOverdueLoans(@Param("today") LocalDate today);
// -> 1 seule requête UPDATE au lieu de N requêtes save() en boucle

// 3. application.yml — Batch size Hibernate
spring:
  jpa:
    properties:
      hibernate:
        jdbc:
          batch_size: 50          # Batch les INSERTs par groupes de 50
        order_inserts: true
        order_updates: true
```

---

## Chapitre 21 — Asynchronisme

### [CONFIG] `@Async` — Exécuter en arrière-plan

Certaines opérations peuvent se faire en parallèle sans bloquer la réponse HTTP.

```java
// Configuration du pool de threads pour @Async
@Configuration
@EnableAsync  // Active @Async
public class AsyncConfig {
    
    @Bean(name = "libraryHubTaskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);      // 5 threads permanents
        executor.setMaxPoolSize(20);      // Max 20 threads
        executor.setQueueCapacity(100);   // File d'attente de 100 tâches
        executor.setThreadNamePrefix("LibraryHub-Async-");
        executor.initialize();
        return executor;
    }
}
```

```java
@Service
@RequiredArgsConstructor
@Slf4j
public class NotificationService {
    
    private final JavaMailSender mailSender;
    
    // @Async : cette méthode s'exécute dans un thread séparé
    // L'appelant n'attend pas — il continue immédiatement
    @Async("libraryHubTaskExecutor")
    public CompletableFuture<Void> sendLoanConfirmationEmail(LoanDTO loan) {
        try {
            log.info("Envoi email de confirmation d'emprunt à {} [thread: {}]", 
                loan.getMemberEmail(), Thread.currentThread().getName());
            
            SimpleMailMessage message = new SimpleMailMessage();
            message.setTo(loan.getMemberEmail());
            message.setSubject("Confirmation d'emprunt - LibraryHub");
            message.setText("Vous avez emprunté '" + loan.getBookTitle() + 
                "'. Date de retour: " + loan.getDueDate());
            
            mailSender.send(message);
            log.info("Email envoyé avec succès à {}", loan.getMemberEmail());
            
        } catch (Exception e) {
            log.error("Échec envoi email à {}", loan.getMemberEmail(), e);
        }
        return CompletableFuture.completedFuture(null);
    }
    
    @Async("libraryHubTaskExecutor")
    public CompletableFuture<Void> sendOverdueNotification(LoanDTO loan, long daysLate) {
        log.warn("Notification de retard : {} a {} jour(s) de retard pour '{}'",
            loan.getMemberEmail(), daysLate, loan.getBookTitle());
        // ... envoi email de rappel
        return CompletableFuture.completedFuture(null);
    }
}

// Dans LoanService, après création d'un emprunt :
@Transactional
public LoanDTO createLoan(LoanCreateRequest request) {
    // ... logique de création ...
    LoanDTO loanDTO = loanMapper.toDTO(savedLoan);
    
    // Envoi email en arrière-plan — ne bloque pas la réponse HTTP !
    notificationService.sendLoanConfirmationEmail(loanDTO);
    
    return loanDTO; // Réponse immédiate au client
}
```

### [ALARM_CLOCK] `@Scheduled` — Tâches planifiées

```java
@Service
@RequiredArgsConstructor
@Slf4j
public class ScheduledTasks {
    
    private final LoanRepository loanRepository;
    private final NotificationService notificationService;
    
    // Chaque jour à 8h du matin
    @Scheduled(cron = "0 0 8 * * *")
    public void checkOverdueLoans() {
        log.info("Vérification des emprunts en retard...");
        
        List<Loan> overdueLoans = loanRepository.findOverdueLoans(LocalDate.now());
        
        if (overdueLoans.isEmpty()) {
            log.info("Aucun emprunt en retard");
            return;
        }
        
        log.warn("{} emprunt(s) en retard détecté(s)", overdueLoans.size());
        
        overdueLoans.forEach(loan -> {
            long daysLate = ChronoUnit.DAYS.between(loan.getDueDate(), LocalDate.now());
            notificationService.sendOverdueNotification(
                loanMapper.toDTO(loan), daysLate
            );
        });
        
        // Mettre à jour le statut en base
        loanRepository.updateOverdueLoans(LocalDate.now());
    }
    
    // Chaque premier du mois à minuit — rapport mensuel
    @Scheduled(cron = "0 0 0 1 * *")
    public void generateMonthlyReport() {
        log.info("Génération du rapport mensuel...");
        // ... générer et envoyer le rapport
    }
    
    // Toutes les 15 minutes — invalidation du cache
    @Scheduled(fixedRate = 15 * 60 * 1000)  // en millisecondes
    public void refreshStatisticsCache() {
        log.debug("Invalidation du cache des statistiques");
        bookService.invalidateCategoryCache();
    }
}
```

---

## Chapitre 22-24 — Introduction aux Microservices

### [CLASSICAL_BUILDING] Monolithe vs Microservices

LibraryHub est actuellement un **monolithe** — tout est dans un seul projet. C'est adapté pour commencer.

```
MONOLITHE (actuel LibraryHub)              MICROSERVICES (évolution possible)
─────────────────────────────             ─────────────────────────────────────
┌────────────────────────────┐            ┌──────────┐  ┌──────────────────┐
│        LibraryHub          │            │  Book    │  │  Member          │
│  ┌─────┐  ┌─────┐  ┌────┐ │            │  Service │  │  Service         │
│  │Book │  │Loan │  │Auth│ │            └──────────┘  └──────────────────┘
│  └─────┘  └─────┘  └────┘ │            ┌──────────┐  ┌──────────────────┐
│        1 base de données   │            │  Loan    │  │  Auth            │
└────────────────────────────┘            │  Service │  │  Service         │
                                          └──────────┘  └──────────────────┘
Avantages :                               Avantages :
[OK] Simple à développer                   [OK] Scalabilité indépendante
[OK] Simple à déployer                     [OK] Technologies différentes par service
[OK] Pas de latence réseau                 [OK] Déploiements indépendants
[OK] Transactions ACID faciles             Inconvénients :
Inconvénients :                          [X] Complexité opérationnelle
[X] Scalabilité globale uniquement        [X] Transactions distribuées complexes
[X] Une technologie pour tout             [X] Latence réseau entre services
```

> **Conseil pour débutants :** Commence TOUJOURS par un monolithe. Migre vers les microservices quand tu en as vraiment besoin (scalabilité, équipes multiples).

### [PLUGIN] OpenFeign — Appels REST entre services

Si LibraryHub évolue vers des microservices, Feign simplifie les appels HTTP.

```java
// Ajouter la dépendance
// <dependency>
//   <groupId>org.springframework.cloud</groupId>
//   <artifactId>spring-cloud-starter-openfeign</artifactId>
// </dependency>

// @FeignClient génère le code HTTP automatiquement
@FeignClient(name = "member-service", url = "${services.member.url}")
public interface MemberServiceClient {
    
    @GetMapping("/api/members/{id}")
    MemberDTO getMember(@PathVariable Long id);
    
    @GetMapping("/api/members/{id}/active-loans-count")
    Integer getActiveLoansCount(@PathVariable Long id);
}

// Utilisation dans LoanService comme si c'était un service local
@Service
@RequiredArgsConstructor
public class LoanService {
    
    private final MemberServiceClient memberServiceClient; // Feign injecté
    
    public LoanDTO createLoan(LoanCreateRequest request) {
        // Appel HTTP vers member-service (transparent)
        MemberDTO member = memberServiceClient.getMember(request.getMemberId());
        Integer activeLoans = memberServiceClient.getActiveLoansCount(request.getMemberId());
        // ...
    }
}
```

### [SECURITE] Circuit Breaker avec Resilience4j

Empêche les pannes en cascade entre services.

```yaml
# application.yml
resilience4j:
  circuitbreaker:
    instances:
      member-service:
        slidingWindowSize: 10
        failureRateThreshold: 50    # Ouvre si 50% des appels échouent
        waitDurationInOpenState: 30s # Attend 30s avant de réessayer
```

```java
@Service
public class LoanService {
    
    @CircuitBreaker(name = "member-service", fallbackMethod = "getMemberFallback")
    public LoanDTO createLoan(LoanCreateRequest request) {
        // Si member-service est down, circuitbreaker appelle getMemberFallback
        MemberDTO member = memberServiceClient.getMember(request.getMemberId());
        // ...
    }
    
    // Méthode de fallback — appelée quand le circuit est ouvert
    public LoanDTO getMemberFallback(LoanCreateRequest request, Exception ex) {
        log.error("Member-service indisponible : {}", ex.getMessage());
        throw new ServiceUnavailableException("Service temporairement indisponible");
    }
}
```

---

## Chapitre 25-26 — Docker & Déploiement

### [DOCKER] Dockerfile pour LibraryHub

```dockerfile
# Dockerfile — Multi-stage build pour optimiser la taille de l'image

# ─────────────────────────────────────────
# STAGE 1 : Build avec Maven
# ─────────────────────────────────────────
FROM eclipse-temurin:17-jdk-alpine AS builder

WORKDIR /app

# Copier pom.xml et télécharger les dépendances (cache Docker)
COPY pom.xml .
RUN mvn dependency:go-offline -B

# Copier le code source et builder
COPY src ./src
RUN mvn clean package -DskipTests -B

# ─────────────────────────────────────────
# STAGE 2 : Image de production (légère)
# ─────────────────────────────────────────
FROM eclipse-temurin:17-jre-alpine

# Créer un user non-root pour la sécurité
RUN addgroup -S libraryhub && adduser -S libraryhub -G libraryhub

WORKDIR /app

# Copier le JAR depuis le stage builder
COPY --from=builder /app/target/libraryhub-*.jar app.jar

# Changer le propriétaire
RUN chown libraryhub:libraryhub app.jar

USER libraryhub

# Port exposé
EXPOSE 8080

# Healthcheck — Docker vérifie que l'app est vivante
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
  CMD wget -qO- http://localhost:8080/actuator/health || exit 1

# Point d'entrée avec options JVM optimisées
ENTRYPOINT ["java", \
  "-XX:+UseContainerSupport", \
  "-XX:MaxRAMPercentage=75.0", \
  "-Djava.security.egd=file:/dev/./urandom", \
  "-jar", "app.jar"]
```

### [OCTOPUS] Docker Compose — Stack complète

```yaml
# docker-compose.yml
version: '3.8'

services:
  
  # ─────────────────────────────────────────
  # Base de données PostgreSQL
  # ─────────────────────────────────────────
  postgres:
    image: postgres:15-alpine
    container_name: libraryhub-postgres
    environment:
      POSTGRES_DB: libraryhub
      POSTGRES_USER: ${DB_USER:-postgres}
      POSTGRES_PASSWORD: ${DB_PASSWORD:-secret}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql  # Script d'init
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - libraryhub-network
  
  # ─────────────────────────────────────────
  # Cache Redis
  # ─────────────────────────────────────────
  redis:
    image: redis:7-alpine
    container_name: libraryhub-redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
    networks:
      - libraryhub-network
  
  # ─────────────────────────────────────────
  # Application LibraryHub
  # ─────────────────────────────────────────
  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: libraryhub-app
    environment:
      SPRING_PROFILES_ACTIVE: prod
      DATABASE_URL: jdbc:postgresql://postgres:5432/libraryhub
      DATABASE_USER: ${DB_USER:-postgres}
      DATABASE_PASSWORD: ${DB_PASSWORD:-secret}
      REDIS_HOST: redis
      JWT_SECRET: ${JWT_SECRET:-changeMeInProduction}
    ports:
      - "8080:8080"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped
    networks:
      - libraryhub-network
    volumes:
      - app_logs:/app/logs

volumes:
  postgres_data:
  redis_data:
  app_logs:

networks:
  libraryhub-network:
    driver: bridge
```

```bash
# Commandes utiles
docker-compose up -d                    # Démarrer en arrière-plan
docker-compose logs -f app              # Suivre les logs de l'app
docker-compose down                     # Arrêter
docker-compose down -v                  # Arrêter + supprimer les volumes
docker-compose build --no-cache         # Rebuilder l'image
docker exec -it libraryhub-app bash     # Se connecter au container
```

---

## Chapitre 27 — Monitoring avec Actuator

### [GRAPHIQUE] Configuration Actuator

```yaml
# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health, info, metrics, env, loggers, beans
      base-path: /actuator
  endpoint:
    health:
      show-details: when-authorized  # Détails seulement pour les users authentifiés
    info:
      enabled: true
  info:
    env:
      enabled: true
    build:
      enabled: true

# Informations affichées par /actuator/info
info:
  app:
    name: LibraryHub API
    version: "@project.version@"
    description: API de gestion de bibliothèque
  contact:
    email: admin@libraryhub.com
```

```java
// Health Check personnalisé
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
    
    private final BookRepository bookRepository;
    
    public DatabaseHealthIndicator(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }
    
    @Override
    public Health health() {
        try {
            long bookCount = bookRepository.count();
            return Health.up()
                .withDetail("books_count", bookCount)
                .withDetail("status", "Connexion DB OK")
                .build();
        } catch (Exception e) {
            return Health.down()
                .withDetail("error", e.getMessage())
                .build();
        }
    }
}
```

```bash
# Endpoints disponibles
GET /actuator/health          -> {"status":"UP","components":{...}}
GET /actuator/info            -> Infos de l'application
GET /actuator/metrics         -> Liste des métriques disponibles
GET /actuator/metrics/jvm.memory.used -> Mémoire JVM utilisée
GET /actuator/env             -> Variables d'environnement (selon config)
GET /actuator/beans           -> Tous les beans Spring chargés
GET /actuator/loggers         -> Niveaux de log actuels
POST /actuator/loggers/com.libraryhub -> Changer le niveau de log en live !
```

### [HAUSSE] Métriques custom avec Micrometer

```java
@Service
@RequiredArgsConstructor
public class LoanService {
    
    private final MeterRegistry meterRegistry;
    private Counter loansCreatedCounter;
    private Counter loansReturnedCounter;
    private Timer loanCreationTimer;
    
    @PostConstruct
    public void initMetrics() {
        loansCreatedCounter = Counter.builder("libraryhub.loans.created")
            .description("Nombre d'emprunts créés")
            .register(meterRegistry);
        
        loansReturnedCounter = Counter.builder("libraryhub.loans.returned")
            .description("Nombre de retours effectués")
            .register(meterRegistry);
        
        loanCreationTimer = Timer.builder("libraryhub.loans.creation.duration")
            .description("Temps de création d'un emprunt")
            .register(meterRegistry);
    }
    
    @Transactional
    public LoanDTO createLoan(LoanCreateRequest request) {
        return loanCreationTimer.record(() -> {
            // ... logique de création ...
            loansCreatedCounter.increment();
            return loanMapper.toDTO(savedLoan);
        });
    }
    
    @Transactional
    public LoanDTO returnBook(Long loanId) {
        // ...
        loansReturnedCounter.increment();
        return loanMapper.toDTO(saved);
    }
}

// Accéder aux métriques custom :
// GET /actuator/metrics/libraryhub.loans.created
// GET /actuator/metrics/libraryhub.loans.creation.duration
```

---

## [OK] Exercices du Module 07

1. **Cache :** Implémente le cache sur `findAllCategories()`. Écris un test qui vérifie que la méthode du repository n'est appelée qu'une fois même si la méthode service est appelée 5 fois.

2. **Scheduled :** Crée une tâche `@Scheduled` qui s'exécute chaque dimanche à minuit et génère un rapport JSON avec : nombre total d'emprunts actifs, nombre de retards, et les 5 livres les plus empruntés de la semaine.

3. **Docker :** Build l'image Docker et lance la stack avec `docker-compose up`. Vérifie que `GET /actuator/health` retourne `UP` et que l'API répond correctement.

4. **Monitoring :** Ajoute une métrique `Gauge` qui expose en temps réel le nombre d'emprunts actifs en base. Vérifie qu'elle apparaît dans `/actuator/metrics`.

5. **Défi :** Ajoute un endpoint `PATCH /actuator/loggers/com.libraryhub` qui permet de changer le niveau de log en production sans redémarrer l'application. Documente la procédure.

---

> -> Continue avec `08_PROJET_FINAL.md` — l'assemblage complet !


# [LIVRE] Module 08 — Projet Final : LibraryHub Complet

> **Partie 11 — Le grand assemblage**
> **Objectif :** Construire LibraryHub de A à Z sans aide, puis vérifier avec cette checklist

---

## [OBJECTIF] Le défi final

Tu as maintenant toutes les connaissances. Il est temps de **tout assembler** sans regarder les modules précédents. Voici les exigences complètes.

---

## [LISTE] Spécifications fonctionnelles complètes

### [SECURISE] Authentification & Gestion des utilisateurs
- `POST /api/auth/register` — Inscription (email, password, firstName, lastName)
- `POST /api/auth/login` — Connexion -> retourne access token + refresh token
- `POST /api/auth/refresh` — Renouveler l'access token
- `POST /api/auth/logout` — Invalider le refresh token
- `GET /api/auth/me` — Profil de l'utilisateur connecté
- `PATCH /api/admin/users/{id}/role` — Changer le rôle (ADMIN only)

### [DOCS] Gestion des livres
- `GET /api/books` — Liste paginée (public) avec filtres : title, isbn, author, category
- `GET /api/books/{id}` — Détail d'un livre (public)
- `GET /api/books/available` — Livres disponibles à l'emprunt (public)
- `GET /api/books/search?q=keyword` — Recherche full-text (public)
- `POST /api/books` — Créer un livre (LIBRARIAN, ADMIN)
- `PUT /api/books/{id}` — Modifier un livre (LIBRARIAN, ADMIN)
- `DELETE /api/books/{id}` — Supprimer un livre (ADMIN only)

### [UTILISATEUR] Gestion des auteurs & catégories
- CRUD complet pour `/api/authors` (ADMIN pour écriture, public pour lecture)
- CRUD complet pour `/api/categories` (ADMIN pour écriture, public pour lecture)
- `GET /api/authors/{id}/books` — Livres d'un auteur

### [UTILISATEURS] Gestion des membres
- `GET /api/members` — Liste (ADMIN, LIBRARIAN)
- `GET /api/members/{id}` — Détail (ADMIN, LIBRARIAN, ou le membre lui-même)
- `POST /api/members` — Créer un membre (ADMIN)
- `PUT /api/members/{id}` — Modifier (ADMIN)
- `PATCH /api/members/{id}/status` — Suspendre/Activer (ADMIN)

### [GUIDE] Gestion des emprunts
- `GET /api/loans` — Tous les emprunts (ADMIN, LIBRARIAN)
- `GET /api/loans/overdue` — Emprunts en retard (ADMIN, LIBRARIAN)
- `GET /api/loans/my` — Mes propres emprunts (tout utilisateur authentifié)
- `POST /api/loans` — Créer un emprunt (LIBRARIAN, ADMIN)
- `PATCH /api/loans/{id}/return` — Retourner un livre (LIBRARIAN, ADMIN)
- `GET /api/members/{id}/loans` — Emprunts d'un membre (ADMIN, LIBRARIAN)

### [GRAPHIQUE] Statistiques
- `GET /api/stats/overview` — Vue globale (ADMIN) : total livres, membres, emprunts actifs, retards
- `GET /api/stats/popular-books` — Top 10 livres les plus empruntés (ADMIN, LIBRARIAN)
- `GET /api/stats/active-members` — Membres les plus actifs (ADMIN)

---

## [OK] Checklist de vérification

### Partie 1-2 : Fondations & Configuration

- [ ] `@SpringBootApplication` bien placée à la racine du package
- [ ] `application.yml` avec config base de données, JWT, cache
- [ ] `application-dev.yml` avec PostgreSQL local
- [ ] `application-test.yml` avec H2 en mémoire
- [ ] `application-prod.yml` avec variables d'environnement
- [ ] `@ConfigurationProperties` pour `JwtProperties` et `LibraryHubProperties`
- [ ] Injection par constructeur partout (pas de `@Autowired` sur les champs)
- [ ] `@PostConstruct` utilisé dans au moins un service

### Partie 3 : Web & REST API

- [ ] `@RestController` + `@RequestMapping` sur chaque controller
- [ ] Tous les codes HTTP corrects (200, 201, 204, 400, 401, 403, 404, 409)
- [ ] `ResponseEntity` utilisé pour contrôler la réponse
- [ ] Header `Location` retourné après création (201)
- [ ] `@PathVariable`, `@RequestParam`, `@RequestBody` utilisés correctement
- [ ] Pagination sur toutes les listes (Page<DTO>)
- [ ] DTOs séparés : request (création/modification) et response (lecture)
- [ ] MapStruct pour le mapping Entity <-> DTO
- [ ] Validation `@Valid` + contraintes sur tous les DTOs de requête
- [ ] Validation personnalisée (`@UniqueIsbn` ou similaire)

### Partie 4 : JPA & Persistance

- [ ] Toutes les entités annotées `@Entity`, `@Table`
- [ ] Relations correctement mappées (`@OneToMany`, `@ManyToOne`)
- [ ] `FetchType.LAZY` par défaut sur les relations
- [ ] `@Index` sur les colonnes fréquemment filtrées
- [ ] Audit JPA (`@CreatedDate`, `@LastModifiedDate`)
- [ ] Requêtes avec `JOIN FETCH` pour éviter N+1
- [ ] Méthodes dérivées dans les repositories
- [ ] Au moins 2 requêtes JPQL custom (`@Query`)
- [ ] Pagination dans au moins un repository
- [ ] `@Transactional(readOnly = true)` sur les méthodes de lecture

### Partie 5 : Sécurité

- [ ] Spring Security configuré avec stateless (no session)
- [ ] CSRF désactivé
- [ ] JWT : génération, validation, extraction
- [ ] Filtre JWT (`OncePerRequestFilter`)
- [ ] `UserDetailsService` implémenté
- [ ] Mots de passe hashés avec BCrypt
- [ ] Règles d'autorisation dans `SecurityConfig`
- [ ] `@PreAuthorize` sur les méthodes de service
- [ ] Access Token (courte durée) + Refresh Token (longue durée)
- [ ] Gestion des erreurs 401 et 403 en JSON

### Partie 6 : Architecture

- [ ] Séparation stricte Controller / Service / Repository
- [ ] Hiérarchie d'exceptions custom
- [ ] `@RestControllerAdvice` global
- [ ] Toutes les exceptions métier gèrent le bon code HTTP
- [ ] Réponse d'erreur standardisée (timestamp, status, message, errorCode)
- [ ] Logs SLF4J avec les bons niveaux (DEBUG, INFO, WARN, ERROR)
- [ ] Aucun log de données sensibles (passwords, tokens)

### Partie 7 : Tests

- [ ] Tests unitaires avec JUnit 5 + Mockito pour tous les services
- [ ] Tests `@WebMvcTest` pour les controllers
- [ ] Tests `@DataJpaTest` pour les repositories avec requêtes complexes
- [ ] Couverture minimum 75% sur les services
- [ ] Tests des cas d'erreur (not found, conflit, validation)
- [ ] `@BeforeEach` pour la mise en place des données de test

### Partie 8 : Performance

- [ ] Cache sur les données peu changeantes (catégories, livres populaires)
- [ ] `@CacheEvict` déclenché lors des modifications
- [ ] Tâche `@Scheduled` pour les emprunts en retard
- [ ] Notification email `@Async` lors de la création d'emprunt

### Partie 10 : Déploiement

- [ ] `Dockerfile` multi-stage fonctionnel
- [ ] `docker-compose.yml` avec app + PostgreSQL + Redis
- [ ] `GET /actuator/health` retourne `UP`
- [ ] Health indicator personnalisé
- [ ] Métriques custom avec Micrometer

---

## [TROPHEE] Fonctionnalités bonus (pour aller plus loin)

Ces fonctionnalités ne sont pas dans les modules, mais te permettront d'approfondir :

### Bonus niveau 1 — Facile
- [ ] **Swagger/OpenAPI** : Ajouter `springdoc-openapi-starter-webmvc-ui` et documenter tous les endpoints avec `@Operation`, `@ApiResponse`
- [ ] **Export CSV** : `GET /api/books/export.csv` — exporter la liste des livres
- [ ] **Profil utilisateur** : Permettre à un membre de voir et modifier son propre profil

### Bonus niveau 2 — Intermédiaire
- [ ] **Système de réservation** : Réserver un livre actuellement emprunté. Notification quand il est disponible
- [ ] **Audit trail** : Stocker l'historique de toutes les modifications (qui a créé/modifié quoi et quand)
- [ ] **Recherche avancée** : Recherche full-text avec PostgreSQL `tsvector` ou Elasticsearch

### Bonus niveau 3 — Avancé
- [ ] **Rate limiting** : Limiter le nombre de requêtes par utilisateur (Bucket4j)
- [ ] **Webhook** : Notifier un système externe lors des emprunts/retours
- [ ] **Multi-tenant** : Gérer plusieurs bibliothèques dans la même application

---

## [WORLD_MAP] Roadmap post-LibraryHub

Une fois LibraryHub terminé, voici les étapes logiques pour continuer :

```
LibraryHub terminé
│
├── 1. Spring Batch — Traitement en masse (imports CSV, migrations)
├── 2. Spring WebFlux — API réactive (Project Reactor)
├── 3. GraphQL avec Spring Boot — Alternative à REST
├── 4. Kafka / RabbitMQ — Messaging asynchrone inter-services
├── 5. Kubernetes — Orchestration de containers
└── 6. Observabilité complète — Prometheus + Grafana + Jaeger (tracing)
```

---

## [DOCS] Ressources pour continuer

| Ressource | Pour quoi | Lien |
|-----------|-----------|------|
| Spring docs officiels | Référence complète | spring.io/docs |
| Baeldung | Tutoriels pratiques Spring | baeldung.com |
| Spring Security in Action | Sécurité avancée | Manning (livre) |
| Designing Data-Intensive Applications | Architecture backend | O'Reilly (livre) |
| YouTube : Amigoscode | Spring Boot vidéos | youtube.com |
| GitHub Explore | Projets Spring réels | github.com/explore |

---

## [BRAVO] Félicitations !

Si tu as suivi tous les modules et complété la checklist, tu maîtrises :

[OK] L'écosystème Spring Boot et son fonctionnement interne  
[OK] La conception d'une API REST professionnelle  
[OK] La persistance des données avec JPA et Spring Data  
[OK] La sécurité avec JWT et Spring Security  
[OK] Une architecture propre et maintenable  
[OK] Les tests à tous les niveaux  
[OK] Le déploiement avec Docker  
[OK] Le monitoring en production  

Tu es maintenant capable de **travailler sur une API Spring Boot en production** dans une équipe professionnelle. Le reste s'acquiert par la pratique.

> [IDEE] **Dernier conseil :** Partage LibraryHub sur GitHub avec un README détaillé. C'est ton premier projet de portfolio — il compte !
