# 🎯 PLAN D'IMPLÉMENTATION TRANSACTIONNELLE — PROJET VEZA

**Date** : 2025-01-27  
**Objectif** : Plan d'action complet pour rendre toutes les opérations critiques transactionnelles  
**Phase** : Design — Prêt pour implémentation  
**Références** : `AUDIT_DB_TRANSACTIONS.md`, `AUDIT_STABILITY.md`, `TRIAGE.md`

---

## 📋 TABLE DES MATIÈRES

1. [Résumé Exécutif](#1-résumé-exécutif)
2. [Inventaire des Opérations Critiques](#2-inventaire-des-opérations-critiques)
3. [Patterns Transactionnels Recommandés](#3-patterns-transactionnels-recommandés)
4. [Design Détaillé par Domaine (P0)](#4-design-détaillé-par-domaine-p0)
5. [Plan d'Implémentation par Phases](#5-plan-dimplémentation-par-phases)
6. [Stratégie de Tests](#6-stratégie-de-tests)
7. [Checklist de Validation](#7-checklist-de-validation)

---

## 1. RÉSUMÉ EXÉCUTIF

### Pourquoi ce plan est critique

Le projet Veza gère des opérations multi-étapes critiques qui, en cas d'échec partiel, peuvent laisser la base de données dans un état incohérent :

- **Marketplace** : Commandes partiellement créées (items sans order, licenses sans order)
- **Playlists** : Duplications incomplètes, collaborateurs sans playlist valide
- **Social** : Compteurs de likes/comments désynchronisés
- **Stream** : Segments HLS orphelins, jobs de transcodage incomplets
- **RBAC** : Assignations de rôles partiellement appliquées, compromettant la sécurité

**Impact métier** : Données corrompues, confusion utilisateur, streaming cassé, risques de sécurité.

### Domaines concernés

| Domaine | Service | Langage | Opérations P0 | Opérations P1 |
|---------|---------|---------|---------------|---------------|
| **Marketplace** | `MarketplaceService` | Go | 0 | 0 |
| **Playlists** | `PlaylistService`, `PlaylistDuplicateService` | Go | 1 | 1 |
| **Social** | `SocialService` | Go | 0 | 2 |
| **RBAC** | `RBACService`, `RoleService` | Go | 1 | 1 |
| **Stream** | `SegmentTracker`, `StreamProcessor`, `EncodingPool` | Rust | 2 | 1 |
| **Chat** | `Channels`, `DirectMessages` | Rust | 0 | 0 |

**Total** : **5 opérations P0**, **5 opérations P1**

### Objectif final

**100% des opérations P0 transactionnelles** avant déploiement en production.

**État actuel** : 8/18 opérations transactionnelles (44%)  
**État cible** : 18/18 opérations transactionnelles (100%)

---

## 2. INVENTAIRE DES OPÉRATIONS CRITIQUES

### 2.1 Opérations P0 (Critique — Must-Fix)

#### 1. `PlaylistDuplicateService.DuplicatePlaylist`

- **Service** : Backend Go (`internal/services/playlist_duplicate_service.go:41-131`)
- **Fichiers concernés** :
  - `internal/services/playlist_duplicate_service.go`
  - `internal/services/playlist_service.go` (CreatePlaylist)
  - `internal/repositories/playlist_track_repository.go` (AddTrack)
- **Risque actuel** :
  - Playlist créée mais tracks non ajoutés → **Playlist vide**
  - Crash au milieu de l'ajout des tracks → **Playlist partiellement dupliquée**
  - Erreur sur un track → **Playlist incomplète** (ligne 117 continue avec les autres)
- **Statut** : ❌ **Non transactionnelle**
- **Impact métier** : **ÉLEVÉ** — Confusion utilisateur, données corrompues

#### 2. `RBACService.AssignRoleToUser`

- **Service** : Backend Go (`internal/services/rbac_service.go:168-210`)
- **Fichiers concernés** :
  - `internal/services/rbac_service.go`
- **Risque actuel** :
  - 4 queries séparées (vérifications + INSERT) → **Race condition possible**
  - User/role supprimé entre vérification et INSERT → **Assignation incohérente**
  - Pas de gestion propre des doublons → **Erreurs DB non gérées**
- **Statut** : ❌ **Non transactionnelle**
- **Impact métier** : **ÉLEVÉ** — Sécurité compromise, permissions incorrectes

#### 3. `SegmentTracker.persist_segment`

- **Service** : Stream Server Rust (`src/core/processing/segment_tracker.rs:82-106`)
- **Fichiers concernés** :
  - `src/core/processing/segment_tracker.rs`
- **Risque actuel** :
  - INSERT segment + UPDATE job séparés → **Segments orphelins** si crash après INSERT
  - UPDATE job + INSERT segment séparés → **Incohérence durée** si crash après UPDATE
- **Statut** : ❌ **Non transactionnelle**
- **Impact métier** : **ÉLEVÉ** — Streaming cassé, playlists HLS incomplètes

#### 4. `StreamProcessor` (Job Creation + Segment Persistence)

- **Service** : Stream Server Rust (`src/core/processing/processor.rs`)
- **Fichiers concernés** :
  - `src/core/processing/processor.rs`
  - `src/core/processing/segment_tracker.rs`
- **Risque actuel** :
  - Job créé → Segments persistés individuellement → Job finalisé
  - Crash après création job → **Job orphelin sans segments**
  - Crash pendant persistance → **Segments partiellement créés**
  - Crash après segments → **Job bloqué en "processing"**
- **Statut** : ❌ **Non transactionnelle**
- **Impact métier** : **ÉLEVÉ** — Jobs de transcodage incomplets, streaming cassé

#### 5. `SocialService.ToggleLike` / `AddComment` (si compteurs critiques)

- **Service** : Backend Go (`internal/core/social/service.go:131-188`)
- **Fichiers concernés** :
  - `internal/core/social/service.go`
- **Risque actuel** :
  - CREATE/DELETE like + UPDATE compteur séparés → **Compteur désynchronisé**
  - CREATE comment + UPDATE compteur séparés → **Compteur désynchronisé**
- **Statut** : ❌ **Non transactionnelle**
- **Impact métier** : **MOYEN → ÉLEVÉ** (si compteurs critiques pour business)

### 2.2 Opérations P1 (Important — Production-grade)

#### 6. `PlaylistService.AddCollaborator`

- **Service** : Backend Go (`internal/services/playlist_service.go:611-665`)
- **Risque** : Collaborateur créé pour playlist supprimée entre vérification et création
- **Statut** : ❌ **Non transactionnelle**

#### 7. `RoleService.AssignRoleToUser`

- **Service** : Backend Go (`internal/services/role_service.go:86-99`)
- **Risque** : Pas de vérifications préalables, erreurs FK non gérées proprement
- **Statut** : ❌ **Non transactionnelle**

#### 8. `EncodingPool.insert_segments_from_playlist`

- **Service** : Stream Server Rust (`src/core/encoding_pool.rs:300-349`)
- **Risque** : Segments partiellement insérés si crash au milieu de la boucle
- **Statut** : ❌ **Non transactionnelle**

### 2.3 Tableau de Synthèse

| Domaine | Opération | Langage | Transactionnelle ? | Priorité | Commentaire |
|---------|-----------|---------|-------------------|----------|-------------|
| **Marketplace** | `CreateOrder` | Go | ✅ Oui | - | Déjà transactionnel |
| **Marketplace** | `CreateProduct` | Go | ✅ Oui | - | Déjà transactionnel |
| **Playlists** | `AddTrack` | Go | ✅ Oui | - | Déjà transactionnel |
| **Playlists** | `RemoveTrack` | Go | ✅ Oui | - | Déjà transactionnel |
| **Playlists** | `ReorderTracks` | Go | ✅ Oui | - | Déjà transactionnel |
| **Playlists** | `DuplicatePlaylist` | Go | ❌ Non | **P0** | Playlist vide/incomplète |
| **Playlists** | `AddCollaborator` | Go | ❌ Non | P1 | Collaborateur sans playlist |
| **Social** | `ToggleLike` | Go | ❌ Non | P1/P0 | Compteur désynchronisé |
| **Social** | `AddComment` | Go | ❌ Non | P1/P0 | Compteur désynchronisé |
| **RBAC** | `AssignRoleToUser` (RBACService) | Go | ❌ Non | **P0** | Sécurité compromise |
| **RBAC** | `AssignRoleToUser` (RoleService) | Go | ❌ Non | P1 | Erreurs non gérées |
| **HLS** | `CreateJob` | Go | ✅ Oui | - | Déjà transactionnel |
| **Auth** | `RotateToken` | Go | ✅ Oui | - | Déjà transactionnel |
| **Stream** | `persist_segment` | Rust | ❌ Non | **P0** | Segments orphelins |
| **Stream** | `insert_segments_from_playlist` | Rust | ❌ Non | P1 | Playlist HLS incomplète |
| **Stream** | Job + Segments | Rust | ❌ Non | **P0** | Jobs incomplets |
| **Chat** | `send_room_message` | Rust | ✅ Oui | - | Déjà transactionnel |
| **Chat** | `send_dm_message` | Rust | ✅ Oui | - | Déjà transactionnel |

---

## 3. PATTERNS TRANSACTIONNELS RECOMMANDÉS

### 3.1 Backend Go (GORM)

#### Pattern Standard

```go
// Pattern recommandé pour toutes les opérations multi-étapes
func (s *Service) OperationMultiSteps(ctx context.Context, params ...) error {
    return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
        // 1. VALIDATIONS (lectures uniquement, pas d'écritures)
        if err := s.validateInput(ctx, tx, params); err != nil {
            return fmt.Errorf("validation failed: %w", err)
        }
        
        // 2. ÉCRITURES MULTIPLES (toutes dans la transaction)
        if err := tx.Create(&entity1).Error; err != nil {
            return fmt.Errorf("failed to create entity1: %w", err)
        }
        
        if err := tx.Create(&entity2).Error; err != nil {
            return fmt.Errorf("failed to create entity2: %w", err)
        }
        
        if err := tx.Model(&entity3).Update("field", value).Error; err != nil {
            return fmt.Errorf("failed to update entity3: %w", err)
        }
        
        // 3. LOGS STRUCTURÉS (optionnel, mais recommandé)
        s.logger.Info("OperationMultiSteps completed",
            zap.String("entity1_id", entity1.ID.String()),
            zap.String("entity2_id", entity2.ID.String()),
        )
        
        // 4. RETOUR nil = commit automatique
        //    RETOUR erreur = rollback automatique
        return nil
    })
}
```

#### Règles Strictes

1. **Pas d'écritures post-transaction** : Toutes les écritures DB doivent être dans la transaction
2. **Chaque chemin d'erreur → rollback** : Retourner une erreur dans la closure = rollback automatique
3. **Wrapper les erreurs avec contexte** : `fmt.Errorf("OperationName: step description: %w", err)`
4. **Context propagation** : Toujours utiliser `WithContext(ctx)` pour annulation et timeouts
5. **Pas de side effects externes** : Pas d'appels API, pas d'écriture fichiers dans la transaction

#### Exemple Concret : DuplicatePlaylist

```go
// AVANT (non transactionnel)
func (s *PlaylistDuplicateService) DuplicatePlaylist(ctx context.Context, playlistID uuid.UUID, newName string) (*models.Playlist, error) {
    original, err := s.playlistService.GetPlaylist(ctx, playlistID)
    if err != nil {
        return nil, err
    }
    
    newPlaylist, err := s.playlistService.CreatePlaylist(ctx, ...)  // Transaction interne
    if err != nil {
        return nil, err
    }
    
    for _, track := range original.Tracks {
        if err := s.playlistService.AddTrackToPlaylist(ctx, newPlaylist.ID, track.ID); err != nil {
            // ⚠️ Continue avec les autres tracks → Playlist incomplète
            continue
        }
    }
    return newPlaylist, nil
}

// APRÈS (transactionnel)
func (s *PlaylistDuplicateService) DuplicatePlaylist(ctx context.Context, playlistID uuid.UUID, newName string) (*models.Playlist, error) {
    var newPlaylist *models.Playlist
    
    err := s.playlistService.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
        // 1. VALIDATION : Récupérer playlist originale
        var original models.Playlist
        if err := tx.Preload("Tracks").First(&original, "id = ?", playlistID).Error; err != nil {
            return fmt.Errorf("failed to load original playlist: %w", err)
        }
        
        // 2. CRÉATION : Nouvelle playlist
        newPlaylist = &models.Playlist{
            Name:        newName,
            UserID:      original.UserID,
            Description: original.Description,
            // ... autres champs
        }
        if err := tx.Create(newPlaylist).Error; err != nil {
            return fmt.Errorf("failed to create duplicate playlist: %w", err)
        }
        
        // 3. DUPLICATION : Tous les tracks dans la même transaction
        for i, track := range original.Tracks {
            playlistTrack := models.PlaylistTrack{
                PlaylistID: newPlaylist.ID,
                TrackID:    track.ID,
                Position:   i + 1,
            }
            if err := tx.Create(&playlistTrack).Error; err != nil {
                return fmt.Errorf("failed to add track %s to duplicate: %w", track.ID, err)
            }
        }
        
        // 4. MISE À JOUR : Compteur de tracks
        if err := tx.Model(newPlaylist).Update("track_count", len(original.Tracks)).Error; err != nil {
            return fmt.Errorf("failed to update track_count: %w", err)
        }
        
        // 5. LOG
        s.logger.Info("Playlist duplicated",
            zap.String("original_id", playlistID.String()),
            zap.String("new_id", newPlaylist.ID.String()),
            zap.Int("tracks_count", len(original.Tracks)),
        )
        
        return nil  // Commit automatique
    })
    
    if err != nil {
        return nil, err  // Rollback automatique si erreur
    }
    
    return newPlaylist, nil
}
```

### 3.2 Rust (SQLx)

#### Pattern Standard

```rust
// Pattern recommandé pour toutes les opérations multi-étapes
async fn operation_multi_steps(
    &self,
    pool: &PgPool,
    params: &Params,
) -> Result<Output, AppError> {
    // 1. DÉBUT TRANSACTION
    let mut tx = pool.begin().await
        .map_err(|e| AppError::DatabaseError {
            message: "failed to begin transaction".to_string(),
            source: e.into(),
        })?;
    
    // 2. VALIDATIONS (lectures uniquement)
    let entity = sqlx::query_as!(
        Entity,
        "SELECT * FROM entities WHERE id = $1",
        params.id
    )
    .fetch_optional(&mut *tx)
    .await
    .map_err(|e| AppError::DatabaseError {
        message: format!("failed to validate entity {}", params.id),
        source: e.into(),
    })?;
    
    if entity.is_none() {
        return Err(AppError::NotFound {
            resource: "entity",
            id: params.id.to_string(),
        });
    }
    
    // 3. ÉCRITURES MULTIPLES (toutes dans la transaction)
    sqlx::query!(
        "INSERT INTO table1 (field1, field2) VALUES ($1, $2)",
        params.value1,
        params.value2
    )
    .execute(&mut *tx)
    .await
    .map_err(|e| AppError::DatabaseError {
        message: "failed to insert into table1".to_string(),
        source: e.into(),
    })?;
    
    sqlx::query!(
        "UPDATE table2 SET field = $1 WHERE id = $2",
        params.value3,
        params.id
    )
    .execute(&mut *tx)
    .await
    .map_err(|e| AppError::DatabaseError {
        message: "failed to update table2".to_string(),
        source: e.into(),
    })?;
    
    // 4. COMMIT (si tout OK)
    tx.commit().await
        .map_err(|e| AppError::DatabaseError {
            message: "failed to commit transaction".to_string(),
            source: e.into(),
        })?;
    
    // 5. LOG
    tracing::info!(
        entity_id = %params.id,
        "operation_multi_steps completed"
    );
    
    Ok(Output { ... })
    
    // NOTE : Si une erreur est retournée avant commit(), 
    //        la transaction est automatiquement rollback à la fin du scope
}
```

#### Règles Strictes

1. **Pas d'écritures post-transaction** : Toutes les écritures DB doivent être dans la transaction
2. **Chaque erreur → rollback** : Si une erreur est retournée avant `tx.commit()`, la transaction est rollback automatiquement
3. **Wrapper les erreurs avec contexte** : `AppError::DatabaseError { message, source }`
4. **Utiliser `&mut *tx`** : Passer `&mut *tx` aux queries, pas `&tx`
5. **Pas de side effects externes** : Pas d'appels API, pas d'écriture fichiers dans la transaction

#### Exemple Concret : persist_segment

```rust
// AVANT (non transactionnel)
async fn persist_segment(&self, segment: &SegmentInfo) -> Result<(), AppError> {
    // INSERT segment
    sqlx::query!(
        "INSERT INTO stream_segments (job_id, segment_path, duration, ...) VALUES ($1, $2, $3, ...)",
        segment.job_id,
        segment.path,
        segment.duration,
        // ...
    )
    .execute(&self.db_pool)  // ⚠️ Pas de transaction
    .await?;
    
    // UPDATE job
    self.update_current_duration().await?;  // ⚠️ Pas dans la même transaction
    
    Ok(())
}

// APRÈS (transactionnel)
async fn persist_segment(&self, segment: &SegmentInfo) -> Result<(), AppError> {
    let mut tx = self.db_pool.begin().await
        .map_err(|e| AppError::DatabaseError {
            message: "failed to begin transaction for segment persistence".to_string(),
            source: e.into(),
        })?;
    
    // 1. INSERT segment
    sqlx::query!(
        "INSERT INTO stream_segments (job_id, segment_path, duration, sequence_number, ...) 
         VALUES ($1, $2, $3, $4, ...)",
        segment.job_id,
        segment.path.to_string(),
        segment.duration.as_secs_f64(),
        segment.sequence_number,
        // ...
    )
    .execute(&mut *tx)
    .await
    .map_err(|e| AppError::DatabaseError {
        message: format!("failed to insert segment {} for job {}", segment.path.display(), segment.job_id),
        source: e.into(),
    })?;
    
    // 2. UPDATE job (durée actuelle)
    let current_duration = self.calculate_current_duration(segment.job_id).await?;
    
    sqlx::query!(
        "UPDATE stream_jobs SET current_duration = $1, updated_at = NOW() WHERE id = $2",
        current_duration.as_secs_f64(),
        segment.job_id
    )
    .execute(&mut *tx)
    .await
    .map_err(|e| AppError::DatabaseError {
        message: format!("failed to update job {} duration", segment.job_id),
        source: e.into(),
    })?;
    
    // 3. COMMIT
    tx.commit().await
        .map_err(|e| AppError::DatabaseError {
            message: "failed to commit segment persistence transaction".to_string(),
            source: e.into(),
        })?;
    
    tracing::debug!(
        job_id = %segment.job_id,
        segment_path = %segment.path.display(),
        "Segment persisted successfully"
    );
    
    Ok(())
}
```

---

## 4. DESIGN DÉTAILLÉ PAR DOMAINE (P0)

### 4.1 Marketplace

#### Opérations Concernées

- ✅ `CreateOrder` — **Déjà transactionnel** (pas de modification nécessaire)
- ✅ `CreateProduct` — **Déjà transactionnel** (pas de modification nécessaire)

#### Schéma Transactionnel Cible

**Aucune modification nécessaire** — Les opérations marketplace sont déjà transactionnelles.

#### Règles d'Invariants

1. **Jamais d'item sans order** : Tous les `order_items` sont créés dans la même transaction que l'`order`
2. **Jamais de licence sans order** : Toutes les `licenses` sont créées dans la même transaction que l'`order`
3. **Order toujours dans un état cohérent** : `PENDING` → `COMPLETED` dans la même transaction

---

### 4.2 Playlists / Collaborations

#### Opérations Concernées

- ❌ `DuplicatePlaylist` — **P0** — À rendre transactionnel
- ❌ `AddCollaborator` — **P1** — À rendre transactionnel

#### Schéma Transactionnel Cible : DuplicatePlaylist

**Flux transactionnel** :

```
1. DÉBUT TRANSACTION
   ↓
2. VALIDATION : Charger playlist originale + tracks (SELECT avec Preload)
   ├─ Si playlist n'existe pas → ROLLBACK + erreur NotFound
   └─ Si erreur DB → ROLLBACK + erreur DatabaseError
   ↓
3. CRÉATION : Nouvelle playlist (INSERT INTO playlists)
   ├─ Si erreur DB → ROLLBACK + erreur DatabaseError
   └─ Si contrainte violée → ROLLBACK + erreur ValidationError
   ↓
4. DUPLICATION : Pour chaque track de l'originale
   ├─ INSERT INTO playlist_tracks (playlist_id, track_id, position)
   ├─ Si erreur sur un track → ROLLBACK complet (tous les tracks annulés)
   └─ Si tous les tracks OK → Continue
   ↓
5. MISE À JOUR : Compteur de tracks (UPDATE playlists.track_count)
   ├─ Si erreur DB → ROLLBACK + erreur DatabaseError
   └─ Si OK → Continue
   ↓
6. COMMIT
   ↓
7. RETOUR : Nouvelle playlist avec tous les tracks
```

**Erreurs possibles** :

| Étape | Erreur Possible | Action |
|-------|----------------|--------|
| 2 | Playlist n'existe pas | Rollback + `NotFound` |
| 2 | Erreur DB (timeout, connection) | Rollback + `DatabaseError` |
| 3 | Contrainte violée (nom dupliqué) | Rollback + `ValidationError` |
| 4 | Track n'existe plus | Rollback + `NotFound` (tous les tracks annulés) |
| 4 | Contrainte FK violée | Rollback + `ValidationError` |
| 5 | Erreur DB | Rollback + `DatabaseError` |

**Invariants garantis** :

- ✅ **Jamais de playlist vide créée** : Si ajout des tracks échoue, la playlist est rollback
- ✅ **Jamais de playlist partiellement dupliquée** : Tous les tracks ou aucun
- ✅ **Compteur toujours cohérent** : `track_count` = nombre réel de tracks dans `playlist_tracks`

#### Schéma Transactionnel Cible : AddCollaborator

**Flux transactionnel** :

```
1. DÉBUT TRANSACTION
   ↓
2. VALIDATION : Vérifier existence playlist (SELECT playlists WHERE id = $1)
   ├─ Si playlist n'existe pas → ROLLBACK + erreur NotFound
   └─ Si erreur DB → ROLLBACK + erreur DatabaseError
   ↓
3. VALIDATION : Vérifier existence user (SELECT users WHERE id = $1)
   ├─ Si user n'existe pas → ROLLBACK + erreur NotFound
   └─ Si erreur DB → ROLLBACK + erreur DatabaseError
   ↓
4. VALIDATION : Vérifier doublon (SELECT playlist_collaborators WHERE ...)
   ├─ Si doublon existe → ROLLBACK + erreur ValidationError
   └─ Si OK → Continue
   ↓
5. CRÉATION : Collaborateur (INSERT INTO playlist_collaborators)
   ├─ Si erreur DB → ROLLBACK + erreur DatabaseError
   └─ Si OK → Continue
   ↓
6. COMMIT
   ↓
7. RETOUR : Collaborateur créé
```

**Invariants garantis** :

- ✅ **Jamais de collaborateur sans playlist valide** : Vérification dans la transaction
- ✅ **Jamais de collaborateur sans user valide** : Vérification dans la transaction
- ✅ **Jamais de doublon** : Vérification dans la transaction

---

### 4.3 Social (Likes/Comments)

#### Opérations Concernées

- ❌ `ToggleLike` — **P1/P0** — À rendre transactionnel
- ❌ `AddComment` — **P1/P0** — À rendre transactionnel

#### Schéma Transactionnel Cible : ToggleLike

**Flux transactionnel** :

```
1. DÉBUT TRANSACTION
   ↓
2. VÉRIFICATION : Like existe déjà ? (SELECT likes WHERE user_id = $1 AND post_id = $2)
   ├─ Si like existe → Mode UNLIKE
   │   ├─ DELETE FROM likes WHERE ...
   │   ├─ UPDATE posts SET like_count = like_count - 1 WHERE id = $2
   │   └─ Si erreur → ROLLBACK
   └─ Si like n'existe pas → Mode LIKE
       ├─ INSERT INTO likes (user_id, post_id, ...)
       ├─ UPDATE posts SET like_count = like_count + 1 WHERE id = $2
       └─ Si erreur → ROLLBACK
   ↓
3. COMMIT
   ↓
4. RETOUR : État final (liked/unliked)
```

**Erreurs possibles** :

| Étape | Erreur Possible | Action |
|-------|----------------|--------|
| 2 | Post n'existe pas | Rollback + `NotFound` |
| 2 | Erreur DB (timeout) | Rollback + `DatabaseError` |
| 2 | Race condition (2 likes simultanés) | Rollback + `ConflictError` (contrainte UNIQUE) |

**Invariants garantis** :

- ✅ **Compteur toujours synchronisé** : `like_count` = nombre réel de likes dans `likes`
- ✅ **Pas de like sans post** : Vérification FK dans la transaction
- ✅ **Pas de unlike si pas de like** : Vérification dans la transaction

#### Schéma Transactionnel Cible : AddComment

**Flux transactionnel** :

```
1. DÉBUT TRANSACTION
   ↓
2. VALIDATION : Post existe ? (SELECT posts WHERE id = $1)
   ├─ Si post n'existe pas → ROLLBACK + erreur NotFound
   └─ Si OK → Continue
   ↓
3. CRÉATION : Commentaire (INSERT INTO comments)
   ├─ Si erreur DB → ROLLBACK + erreur DatabaseError
   └─ Si OK → Continue
   ↓
4. MISE À JOUR : Compteur (UPDATE posts SET comment_count = comment_count + 1)
   ├─ Si erreur DB → ROLLBACK + erreur DatabaseError
   └─ Si OK → Continue
   ↓
5. COMMIT
   ↓
6. RETOUR : Commentaire créé
```

**Invariants garantis** :

- ✅ **Compteur toujours synchronisé** : `comment_count` = nombre réel de comments dans `comments`
- ✅ **Jamais de commentaire sans post** : Vérification FK dans la transaction

---

### 4.4 Stream Jobs / Segments

#### Opérations Concernées

- ❌ `SegmentTracker.persist_segment` — **P0** — À rendre transactionnel
- ❌ `StreamProcessor` (Job + Segments) — **P0** — À rendre transactionnel
- ❌ `EncodingPool.insert_segments_from_playlist` — **P1** — À rendre transactionnel

#### Schéma Transactionnel Cible : persist_segment

**Flux transactionnel** :

```
1. DÉBUT TRANSACTION
   ↓
2. VALIDATION : Job existe et est en "processing" ? (SELECT stream_jobs WHERE id = $1)
   ├─ Si job n'existe pas → ROLLBACK + erreur NotFound
   ├─ Si job n'est pas en "processing" → ROLLBACK + erreur InvalidState
   └─ Si OK → Continue
   ↓
3. INSERTION : Segment (INSERT INTO stream_segments)
   ├─ Si erreur DB → ROLLBACK + erreur DatabaseError
   ├─ Si contrainte violée (doublon) → ROLLBACK + erreur ValidationError
   └─ Si OK → Continue
   ↓
4. CALCUL : Durée actuelle (SUM(duration) FROM stream_segments WHERE job_id = $1)
   ├─ Si erreur DB → ROLLBACK + erreur DatabaseError
   └─ Si OK → Continue
   ↓
5. MISE À JOUR : Job (UPDATE stream_jobs SET current_duration = $1, updated_at = NOW())
   ├─ Si erreur DB → ROLLBACK + erreur DatabaseError
   └─ Si OK → Continue
   ↓
6. COMMIT
   ↓
7. RETOUR : Segment persisté
```

**Erreurs possibles** :

| Étape | Erreur Possible | Action |
|-------|----------------|--------|
| 2 | Job n'existe pas | Rollback + `NotFound` |
| 2 | Job en état invalide (completed, failed) | Rollback + `InvalidState` |
| 3 | Segment déjà existant (sequence_number dupliqué) | Rollback + `ValidationError` |
| 4 | Erreur DB (timeout) | Rollback + `DatabaseError` |
| 5 | Erreur DB | Rollback + `DatabaseError` |

**Invariants garantis** :

- ✅ **Jamais de segment sans job valide** : Vérification dans la transaction
- ✅ **Job toujours à jour** : `current_duration` = somme réelle des segments
- ✅ **Pas de segments orphelins** : Si job supprimé, segments supprimés (CASCADE)

#### Schéma Transactionnel Cible : StreamProcessor (Job + Segments)

**Problème actuel** : Job créé, puis segments persistés individuellement, puis job finalisé.

**Solution recommandée** : **Pattern "Two-Phase"**

**Phase 1 : Création Job (PENDING)**

```
1. DÉBUT TRANSACTION
   ↓
2. CRÉATION : Job en état "pending" (INSERT INTO stream_jobs, status = 'pending')
   ├─ Si erreur DB → ROLLBACK + erreur DatabaseError
   └─ Si OK → Continue
   ↓
3. COMMIT
   ↓
4. RETOUR : Job créé (status = 'pending')
```

**Phase 2 : Traitement FFmpeg (hors transaction)**

```
1. Spawn FFmpeg process
2. Détecter segments (via FFmpegMonitor)
3. Persister segments (via persist_segment, chaque segment dans sa propre transaction)
```

**Phase 3 : Finalisation Job (COMPLETED)**

```
1. DÉBUT TRANSACTION
   ↓
2. VALIDATION : Vérifier que tous les segments sont persistés
   ├─ SELECT COUNT(*) FROM stream_segments WHERE job_id = $1
   ├─ Si aucun segment → ROLLBACK + erreur InvalidState
   └─ Si OK → Continue
   ↓
3. CALCUL : Durée totale (SUM(duration) FROM stream_segments WHERE job_id = $1)
   ├─ Si erreur DB → ROLLBACK + erreur DatabaseError
   └─ Si OK → Continue
   ↓
4. MISE À JOUR : Job (UPDATE stream_jobs SET status = 'completed', total_duration = $1, updated_at = NOW())
   ├─ Si erreur DB → ROLLBACK + erreur DatabaseError
   └─ Si OK → Continue
   ↓
5. COMMIT
   ↓
6. RETOUR : Job finalisé
```

**Alternative (plus simple) : Pattern "Batch Persistence"**

Si on veut éviter le pattern two-phase, on peut utiliser `persist_all()` à la fin :

```
1. DÉBUT TRANSACTION
   ↓
2. CRÉATION : Job en état "processing" (INSERT INTO stream_jobs)
   ↓
3. COMMIT (job créé)
   ↓
4. TRAITEMENT FFmpeg (hors transaction)
   ├─ Détecter segments (via FFmpegMonitor)
   ├─ Stocker segments en mémoire (SegmentTracker)
   └─ Ne PAS persister immédiatement
   ↓
5. DÉBUT TRANSACTION (batch)
   ↓
6. INSERTION : Tous les segments en batch (INSERT INTO stream_segments ... VALUES (...), (...), (...))
   ├─ Si erreur DB → ROLLBACK + erreur DatabaseError
   └─ Si OK → Continue
   ↓
7. CALCUL : Durée totale (SUM(duration))
   ↓
8. MISE À JOUR : Job (UPDATE stream_jobs SET status = 'completed', total_duration = $1)
   ├─ Si erreur DB → ROLLBACK + erreur DatabaseError
   └─ Si OK → Continue
   ↓
9. COMMIT
   ↓
10. RETOUR : Job finalisé
```

**Recommandation** : **Pattern "Batch Persistence"** (plus simple, moins de transactions)

**Invariants garantis** :

- ✅ **Jamais de job sans segments** : Validation avant finalisation
- ✅ **Job toujours à jour** : `total_duration` = somme réelle des segments
- ✅ **Pas de segments orphelins** : Tous les segments créés dans la même transaction que la finalisation

#### Schéma Transactionnel Cible : insert_segments_from_playlist

**Flux transactionnel** :

```
1. DÉBUT TRANSACTION
   ↓
2. VALIDATION : Job existe ? (SELECT stream_jobs WHERE id = $1)
   ├─ Si job n'existe pas → ROLLBACK + erreur NotFound
   └─ Si OK → Continue
   ↓
3. INSERTION : Tous les segments en batch
   ├─ Pour chaque segment dans la playlist :
   │   ├─ INSERT INTO stream_segments (job_id, segment_path, sequence_number, ...)
   │   ├─ Si erreur sur un segment → ROLLBACK complet (tous les segments annulés)
   │   └─ Si OK → Continue
   └─ Si tous les segments OK → Continue
   ↓
4. CALCUL : Durée totale (SUM(duration) FROM stream_segments WHERE job_id = $1)
   ↓
5. MISE À JOUR : Job (UPDATE stream_jobs SET total_duration = $1, updated_at = NOW())
   ↓
6. COMMIT
   ↓
7. RETOUR : Segments insérés
```

**Invariants garantis** :

- ✅ **Playlist HLS complète ou vide** : Tous les segments ou aucun
- ✅ **Job toujours à jour** : `total_duration` = somme réelle des segments

---

### 4.5 RBAC / Permissions

#### Opérations Concernées

- ❌ `RBACService.AssignRoleToUser` — **P0** — À rendre transactionnel
- ❌ `RoleService.AssignRoleToUser` — **P1** — À rendre transactionnel

#### Schéma Transactionnel Cible : RBACService.AssignRoleToUser

**Flux transactionnel** :

```
1. DÉBUT TRANSACTION
   ↓
2. VALIDATION : User existe ? (SELECT users WHERE id = $1 FOR UPDATE)
   ├─ Si user n'existe pas → ROLLBACK + erreur NotFound
   ├─ FOR UPDATE : Verrouille la ligne pour éviter race condition
   └─ Si OK → Continue
   ↓
3. VALIDATION : Role existe ? (SELECT roles WHERE id = $1 FOR UPDATE)
   ├─ Si role n'existe pas → ROLLBACK + erreur NotFound
   ├─ FOR UPDATE : Verrouille la ligne pour éviter race condition
   └─ Si OK → Continue
   ↓
4. VALIDATION : Doublon ? (SELECT user_roles WHERE user_id = $1 AND role_id = $2)
   ├─ Si doublon existe → ROLLBACK + erreur ValidationError
   └─ Si OK → Continue
   ↓
5. INSERTION : Assignation (INSERT INTO user_roles (user_id, role_id, ...))
   ├─ Si erreur DB → ROLLBACK + erreur DatabaseError
   ├─ Si contrainte UNIQUE violée → ROLLBACK + erreur ValidationError (race condition détectée)
   └─ Si OK → Continue
   ↓
6. COMMIT
   ↓
7. RETOUR : Assignation créée
```

**Erreurs possibles** :

| Étape | Erreur Possible | Action |
|-------|----------------|--------|
| 2 | User n'existe pas | Rollback + `NotFound` |
| 2 | User supprimé entre vérification et INSERT | Rollback (FK constraint) |
| 3 | Role n'existe pas | Rollback + `NotFound` |
| 3 | Role supprimé entre vérification et INSERT | Rollback (FK constraint) |
| 4 | Doublon détecté | Rollback + `ValidationError` |
| 5 | Race condition (2 assignations simultanées) | Rollback + `ValidationError` (contrainte UNIQUE) |

**Invariants garantis** :

- ✅ **Jamais d'assignation sans user valide** : Vérification + FK dans la transaction
- ✅ **Jamais d'assignation sans role valide** : Vérification + FK dans la transaction
- ✅ **Jamais de doublon** : Vérification + contrainte UNIQUE dans la transaction
- ✅ **Pas de race condition** : `FOR UPDATE` + contrainte UNIQUE

#### Schéma Transactionnel Cible : RoleService.AssignRoleToUser

**Flux transactionnel** :

```
1. DÉBUT TRANSACTION
   ↓
2. VALIDATION : User existe ? (SELECT users WHERE id = $1)
   ├─ Si user n'existe pas → ROLLBACK + erreur NotFound
   └─ Si OK → Continue
   ↓
3. VALIDATION : Role existe ? (SELECT roles WHERE id = $1)
   ├─ Si role n'existe pas → ROLLBACK + erreur NotFound
   └─ Si OK → Continue
   ↓
4. VALIDATION : Doublon ? (SELECT user_roles WHERE user_id = $1 AND role_id = $2)
   ├─ Si doublon existe → ROLLBACK + erreur ValidationError
   └─ Si OK → Continue
   ↓
5. INSERTION : Assignation (INSERT INTO user_roles)
   ├─ Si erreur DB → ROLLBACK + erreur DatabaseError
   └─ Si OK → Continue
   ↓
6. COMMIT
   ↓
7. RETOUR : Assignation créée
```

**Invariants garantis** :

- ✅ **Même garanties que RBACService** : Vérifications + FK + contrainte UNIQUE

---

## 5. PLAN D'IMPLÉMENTATION PAR PHASES

### Phase 1 — P0 Backend Go

**Objectif** : Rendre transactionnelles toutes les opérations P0 du backend Go.

**Durée estimée** : 4-6 heures

**Opérations à traiter** :

1. ✅ `PlaylistDuplicateService.DuplicatePlaylist`
2. ✅ `RBACService.AssignRoleToUser`
3. ⚠️ `SocialService.ToggleLike` (si compteurs critiques)
4. ⚠️ `SocialService.AddComment` (si compteurs critiques)

**Ordre recommandé** :

1. **RBAC** (sécurité critique) → `RBACService.AssignRoleToUser`
2. **Playlists** (impact utilisateur élevé) → `PlaylistDuplicateService.DuplicatePlaylist`
3. **Social** (si compteurs critiques) → `SocialService.ToggleLike`, `AddComment`

**Fichiers principaux** :

- `internal/services/rbac_service.go` (lignes 168-210)
- `internal/services/playlist_duplicate_service.go` (lignes 41-131)
- `internal/core/social/service.go` (lignes 131-188)

**Risques et points d'attention** :

- ⚠️ **RBAC** : Utiliser `FOR UPDATE` pour éviter race conditions
- ⚠️ **Playlists** : Vérifier que `CreatePlaylist` peut être appelé avec un `*gorm.DB` transactionnel (sinon, refactoriser)
- ⚠️ **Social** : Vérifier si les compteurs sont critiques pour le business (si non, garder en P1)

**Critères de "done"** :

- [ ] Toutes les opérations P0 Backend Go sont transactionnelles
- [ ] Tests unitaires passent (simulation d'erreur au milieu de la transaction)
- [ ] Tests d'intégration passent (vérification rollback en cas d'erreur)
- [ ] Aucune régression sur les fonctionnalités existantes

---

### Phase 2 — P0 Rust Stream

**Objectif** : Rendre transactionnelles toutes les opérations P0 du Stream Server.

**Durée estimée** : 6-8 heures

**Opérations à traiter** :

1. ✅ `SegmentTracker.persist_segment`
2. ✅ `StreamProcessor` (Job + Segments) — Pattern "Batch Persistence"

**Ordre recommandé** :

1. **SegmentTracker** (base) → `persist_segment`
2. **StreamProcessor** (orchestration) → Pattern "Batch Persistence"

**Fichiers principaux** :

- `src/core/processing/segment_tracker.rs` (lignes 82-106)
- `src/core/processing/processor.rs` (lignes 238-243, `finalize()`)
- `src/core/processing/callbacks.rs` (si nécessaire pour batch persistence)

**Risques et points d'attention** :

- ⚠️ **SegmentTracker** : Vérifier que `update_current_duration()` peut être intégré dans la transaction
- ⚠️ **StreamProcessor** : Décider entre pattern "Two-Phase" ou "Batch Persistence" (recommandation : Batch)
- ⚠️ **Performance** : Batch persistence peut être plus lent si beaucoup de segments (optimiser avec `INSERT ... VALUES (...), (...), (...)`)

**Critères de "done"** :

- [ ] Toutes les opérations P0 Stream Server sont transactionnelles
- [ ] Tests unitaires passent (simulation d'erreur au milieu de la transaction)
- [ ] Tests d'intégration passent (vérification rollback en cas d'erreur)
- [ ] Aucune régression sur le streaming (tester avec un fichier audio réel)

---

### Phase 3 — P1 Backend Go

**Objectif** : Rendre transactionnelles toutes les opérations P1 du backend Go.

**Durée estimée** : 2-3 heures

**Opérations à traiter** :

1. ✅ `PlaylistService.AddCollaborator`
2. ✅ `RoleService.AssignRoleToUser`

**Fichiers principaux** :

- `internal/services/playlist_service.go` (lignes 611-665)
- `internal/services/role_service.go` (lignes 86-99)

**Risques et points d'attention** :

- ⚠️ **AddCollaborator** : Risque faible, mais bonne pratique de rendre transactionnel
- ⚠️ **RoleService** : S'assurer que les vérifications sont bien faites avant INSERT

**Critères de "done"** :

- [ ] Toutes les opérations P1 Backend Go sont transactionnelles
- [ ] Tests unitaires passent
- [ ] Aucune régression

---

### Phase 4 — P1 Rust Stream

**Objectif** : Rendre transactionnelle l'opération P1 du Stream Server.

**Durée estimée** : 2-3 heures

**Opérations à traiter** :

1. ✅ `EncodingPool.insert_segments_from_playlist`

**Fichiers principaux** :

- `src/core/encoding_pool.rs` (lignes 300-349)

**Risques et points d'attention** :

- ⚠️ **Performance** : Utiliser batch INSERT pour éviter trop de queries

**Critères de "done"** :

- [ ] Opération P1 Stream Server est transactionnelle
- [ ] Tests unitaires passent
- [ ] Aucune régression

---

### Phase 5 — Tests et Validation

**Objectif** : Valider que toutes les transactions fonctionnent correctement.

**Durée estimée** : 4-6 heures

**Actions** :

1. Écrire tests unitaires pour chaque opération transactionnelle
2. Écrire tests d'intégration pour vérifier rollback
3. Tests de charge (optionnel, si nécessaire)
4. Documentation mise à jour

**Critères de "done"** :

- [ ] Tous les tests passent
- [ ] Documentation mise à jour (`TRIAGE.md`, `AUDIT_STABILITY.md`)
- [ ] Checklist de validation complétée

---

## 6. STRATÉGIE DE TESTS

### 6.1 Tests Unitaires

#### Backend Go

**Pattern de test recommandé** :

```go
func TestPlaylistDuplicateService_DuplicatePlaylist_TransactionRollback(t *testing.T) {
    // Setup : DB de test, mock data
    db := setupTestDB(t)
    service := NewPlaylistDuplicateService(db, ...)
    
    // Test : Simuler erreur au milieu de la transaction
    originalPlaylist := createTestPlaylist(t, db, 5) // 5 tracks
    
    // Mock : Faire échouer l'ajout du 3ème track
    // (en injectant une erreur dans AddTrack)
    
    // Action
    _, err := service.DuplicatePlaylist(ctx, originalPlaylist.ID, "Duplicate")
    
    // Assert : Erreur retournée
    assert.Error(t, err)
    
    // Assert : Aucune playlist créée (rollback complet)
    var count int64
    db.Model(&models.Playlist{}).Where("name = ?", "Duplicate").Count(&count)
    assert.Equal(t, int64(0), count, "Playlist should not be created on error")
    
    // Assert : Aucun track créé (rollback complet)
    db.Model(&models.PlaylistTrack{}).Where("playlist_id = ?", ...).Count(&count)
    assert.Equal(t, int64(0), count, "Tracks should not be created on error")
}
```

**Tests à écrire pour chaque opération** :

1. ✅ **Succès** : Transaction complète, toutes les écritures OK
2. ✅ **Rollback sur erreur** : Erreur au milieu → rollback complet
3. ✅ **Validation** : Erreur de validation → rollback (pas d'écritures partielles)
4. ✅ **Race condition** : 2 requêtes simultanées → une seule réussit (si applicable)

#### Rust (SQLx)

**Pattern de test recommandé** :

```rust
#[tokio::test]
async fn test_persist_segment_transaction_rollback() {
    // Setup : DB de test, mock data
    let pool = setup_test_db().await;
    let tracker = SegmentTracker::new(pool.clone());
    
    // Test : Simuler erreur au milieu de la transaction
    let job = create_test_job(&pool, "processing").await;
    let segment = SegmentInfo {
        job_id: job.id,
        path: PathBuf::from("/test/segment.ts"),
        // ...
    };
    
    // Mock : Faire échouer l'UPDATE job (en injectant une erreur)
    // (ex: supprimer le job avant l'UPDATE)
    
    // Action
    let result = tracker.persist_segment(&segment).await;
    
    // Assert : Erreur retournée
    assert!(result.is_err());
    
    // Assert : Aucun segment créé (rollback complet)
    let count: i64 = sqlx::query_scalar!(
        "SELECT COUNT(*) FROM stream_segments WHERE job_id = $1",
        job.id
    )
    .fetch_one(&pool)
    .await
    .unwrap();
    
    assert_eq!(count, 0, "Segment should not be created on error");
}
```

**Tests à écrire pour chaque opération** :

1. ✅ **Succès** : Transaction complète, toutes les écritures OK
2. ✅ **Rollback sur erreur** : Erreur au milieu → rollback complet
3. ✅ **Validation** : Erreur de validation → rollback
4. ✅ **Race condition** : 2 requêtes simultanées → une seule réussit (si applicable)

### 6.2 Tests d'Intégration

#### Simulation d'Erreur "au Milieu" d'une Transaction

**Backend Go** :

```go
func TestPlaylistDuplicateService_DuplicatePlaylist_Integration(t *testing.T) {
    db := setupIntegrationDB(t)
    
    // Créer une playlist avec 10 tracks
    original := createTestPlaylist(t, db, 10)
    
    // Injecter une erreur DB au 5ème track (en utilisant un hook GORM)
    db.Callback().Create().Before("gorm:create").Register("inject_error", func(db *gorm.DB) {
        // Compter les tracks créés
        var count int64
        db.Model(&models.PlaylistTrack{}).Count(&count)
        if count == 4 { // 5ème track
            db.AddError(errors.New("simulated DB error"))
        }
    })
    
    service := NewPlaylistDuplicateService(db, ...)
    _, err := service.DuplicatePlaylist(ctx, original.ID, "Duplicate")
    
    assert.Error(t, err)
    
    // Vérifier rollback : Aucune playlist créée
    var playlistCount int64
    db.Model(&models.Playlist{}).Where("name = ?", "Duplicate").Count(&playlistCount)
    assert.Equal(t, int64(0), playlistCount)
    
    // Vérifier rollback : Aucun track créé
    var trackCount int64
    db.Model(&models.PlaylistTrack{}).Where("playlist_id = ?", ...).Count(&trackCount)
    assert.Equal(t, int64(0), trackCount)
}
```

**Rust (SQLx)** :

```rust
#[tokio::test]
async fn test_persist_segment_integration_rollback() {
    let pool = setup_integration_db().await;
    let tracker = SegmentTracker::new(pool.clone());
    
    let job = create_test_job(&pool, "processing").await;
    
    // Injecter une erreur : Supprimer le job avant l'UPDATE
    sqlx::query!("DELETE FROM stream_jobs WHERE id = $1", job.id)
        .execute(&pool)
        .await
        .unwrap();
    
    let segment = SegmentInfo { ... };
    let result = tracker.persist_segment(&segment).await;
    
    assert!(result.is_err());
    
    // Vérifier rollback : Aucun segment créé
    let count: i64 = sqlx::query_scalar!(
        "SELECT COUNT(*) FROM stream_segments WHERE job_id = $1",
        job.id
    )
    .fetch_one(&pool)
    .await
    .unwrap();
    
    assert_eq!(count, 0);
}
```

### 6.3 Vérification dans la DB après Rollback

**Backend Go** :

```go
func TestRBACService_AssignRoleToUser_RollbackVerification(t *testing.T) {
    db := setupTestDB(t)
    service := NewRBACService(db, ...)
    
    user := createTestUser(t, db)
    role := createTestRole(t, db)
    
    // Simuler erreur : Supprimer le role avant l'INSERT
    db.Delete(&role)
    
    err := service.AssignRoleToUser(ctx, user.ID, role.ID)
    assert.Error(t, err)
    
    // Vérifier rollback : Aucune assignation créée
    var count int64
    db.Model(&models.UserRole{}).
        Where("user_id = ? AND role_id = ?", user.ID, role.ID).
        Count(&count)
    assert.Equal(t, int64(0), count, "UserRole should not be created on error")
}
```

**Rust (SQLx)** :

```rust
#[tokio::test]
async fn test_assign_role_rollback_verification() {
    let pool = setup_test_db().await;
    let service = RBACService::new(pool.clone());
    
    let user = create_test_user(&pool).await;
    let role = create_test_role(&pool).await;
    
    // Simuler erreur : Supprimer le role avant l'INSERT
    sqlx::query!("DELETE FROM roles WHERE id = $1", role.id)
        .execute(&pool)
        .await
        .unwrap();
    
    let result = service.assign_role_to_user(user.id, role.id).await;
    assert!(result.is_err());
    
    // Vérifier rollback : Aucune assignation créée
    let count: i64 = sqlx::query_scalar!(
        "SELECT COUNT(*) FROM user_roles WHERE user_id = $1 AND role_id = $2",
        user.id,
        role.id
    )
    .fetch_one(&pool)
    .await
    .unwrap();
    
    assert_eq!(count, 0);
}
```

### 6.4 Intégration aux Suites de Tests Existantes

**Backend Go** :

- Ajouter les tests dans les fichiers `*_test.go` existants
- Utiliser `internal/database/test_helpers.go` pour setup DB de test
- Utiliser `testify/assert` pour les assertions

**Rust** :

- Ajouter les tests dans les fichiers `*_test.rs` ou `tests/` existants
- Utiliser `sqlx::test` ou containers Docker pour DB de test
- Utiliser `assert!` et `assert_eq!` pour les assertions

---

## 7. CHECKLIST DE VALIDATION

### 7.1 Couverture du Plan

- [x] Toutes les opérations P0 sont couvertes par le plan
  - [x] `PlaylistDuplicateService.DuplicatePlaylist`
  - [x] `RBACService.AssignRoleToUser`
  - [x] `SegmentTracker.persist_segment`
  - [x] `StreamProcessor` (Job + Segments)
  - [x] `SocialService.ToggleLike` / `AddComment` (si critiques)

- [x] Toutes les opérations P1 sont couvertes par le plan
  - [x] `PlaylistService.AddCollaborator`
  - [x] `RoleService.AssignRoleToUser`
  - [x] `EncodingPool.insert_segments_from_playlist`

### 7.2 Design par Opération

- [x] Pour chaque opération P0 : pattern transactionnel défini
  - [x] `DuplicatePlaylist` : Transaction complète (playlist + tracks + compteur)
  - [x] `AssignRoleToUser` (RBACService) : Transaction avec `FOR UPDATE` + vérifications
  - [x] `persist_segment` : Transaction (INSERT segment + UPDATE job)
  - [x] `StreamProcessor` : Pattern "Batch Persistence" (job + segments batch)

- [x] Pour chaque opération P1 : pattern transactionnel défini
  - [x] `AddCollaborator` : Transaction avec vérifications
  - [x] `AssignRoleToUser` (RoleService) : Transaction avec vérifications
  - [x] `insert_segments_from_playlist` : Transaction batch

### 7.3 Phases d'Implémentation

- [x] Phases d'implémentation claires et ordonnées
  - [x] Phase 1 : P0 Backend Go (4-6h)
  - [x] Phase 2 : P0 Rust Stream (6-8h)
  - [x] Phase 3 : P1 Backend Go (2-3h)
  - [x] Phase 4 : P1 Rust Stream (2-3h)
  - [x] Phase 5 : Tests et Validation (4-6h)

- [x] Pour chaque phase : objectifs, fichiers, risques, critères de "done"

### 7.4 Stratégie de Tests

- [x] Stratégie de test documentée
  - [x] Tests unitaires (succès, rollback, validation, race condition)
  - [x] Tests d'intégration (simulation d'erreur, vérification rollback)
  - [x] Intégration aux suites de tests existantes

### 7.5 Points Critiques

- [x] Aucun point critique laissé en flou
  - [x] Patterns transactionnels définis (Go + Rust)
  - [x] Règles d'invariants documentées
  - [x] Erreurs possibles identifiées
  - [x] Risques et points d'attention documentés

---

## 8. PROCHAINES ÉTAPES

1. ✅ **Phase 1 : Audit** — **COMPLÉTÉ** (`AUDIT_DB_TRANSACTIONS.md`)
2. ✅ **Phase 2 : Design** — **COMPLÉTÉ** (ce document)
3. ⏳ **Phase 3 : Implémentation** — Prêt à commencer
   - Commencer par Phase 1 (P0 Backend Go)
   - Suivre l'ordre recommandé (RBAC → Playlists → Social)
4. ⏳ **Phase 4 : Tests** — Après chaque phase d'implémentation
5. ⏳ **Phase 5 : Documentation** — Mettre à jour `TRIAGE.md` et `AUDIT_STABILITY.md`

---

**Date de création** : 2025-01-27  
**Dernière mise à jour** : 2025-01-27  
**Statut** : ✅ Design complet — Prêt pour implémentation

**Références** :
- `docs/AUDIT_DB_TRANSACTIONS.md` — Audit détaillé des opérations
- `docs/AUDIT_STABILITY.md` — Audit de stabilité global
- `TRIAGE.md` — État fonctionnel des features

