veza/docs/DB_TRANSACTION_PLAN.md
okinrev a3f2f2c59b P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.

Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.

Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).

Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.

Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 11:14:38 +01:00

47 KiB

🎯 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
  2. Inventaire des Opérations Critiques
  3. Patterns Transactionnels Recommandés
  4. Design Détaillé par Domaine (P0)
  5. Plan d'Implémentation par Phases
  6. Stratégie de Tests
  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

// 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

// 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

// 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

// 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

  • CreateOrderDéjà transactionnel (pas de modification nécessaire)
  • CreateProductDé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 : PENDINGCOMPLETED dans la même transaction

4.2 Playlists / Collaborations

Opérations Concernées

  • DuplicatePlaylistP0 — À rendre transactionnel
  • AddCollaboratorP1 — À 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

  • ToggleLikeP1/P0 — À rendre transactionnel
  • AddCommentP1/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_segmentP0 — À rendre transactionnel
  • StreamProcessor (Job + Segments) — P0 — À rendre transactionnel
  • EncodingPool.insert_segments_from_playlistP1 — À 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.AssignRoleToUserP0 — À rendre transactionnel
  • RoleService.AssignRoleToUserP1 — À 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é :

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é :

#[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 :

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

#[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 :

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

#[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

  • Toutes les opérations P0 sont couvertes par le plan

    • PlaylistDuplicateService.DuplicatePlaylist
    • RBACService.AssignRoleToUser
    • SegmentTracker.persist_segment
    • StreamProcessor (Job + Segments)
    • SocialService.ToggleLike / AddComment (si critiques)
  • Toutes les opérations P1 sont couvertes par le plan

    • PlaylistService.AddCollaborator
    • RoleService.AssignRoleToUser
    • EncodingPool.insert_segments_from_playlist

7.2 Design par Opération

  • Pour chaque opération P0 : pattern transactionnel défini

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

    • AddCollaborator : Transaction avec vérifications
    • AssignRoleToUser (RoleService) : Transaction avec vérifications
    • insert_segments_from_playlist : Transaction batch

7.3 Phases d'Implémentation

  • Phases d'implémentation claires et ordonnées

    • Phase 1 : P0 Backend Go (4-6h)
    • Phase 2 : P0 Rust Stream (6-8h)
    • Phase 3 : P1 Backend Go (2-3h)
    • Phase 4 : P1 Rust Stream (2-3h)
    • Phase 5 : Tests et Validation (4-6h)
  • Pour chaque phase : objectifs, fichiers, risques, critères de "done"

7.4 Stratégie de Tests

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

7.5 Points Critiques

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

8. PROCHAINES ÉTAPES

  1. Phase 1 : AuditCOMPLÉTÉ (AUDIT_DB_TRANSACTIONS.md)
  2. Phase 2 : DesignCOMPLÉ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