# language: fr Fonctionnalité: API - Progression et synchronisation audio-guides En tant que système backend Je veux sauvegarder et synchroniser la progression des audio-guides Afin de permettre une reprise fluide et multi-device Contexte: Étant donné que l'API RoadWave est démarrée Et que l'utilisateur "user@example.com" est authentifié # 16.6.1 - Sauvegarde progression Scénario: POST /api/v1/audio-guides/{id}/progress - Sauvegarde progression Étant donné un audio-guide "ag_123" en cours d'écoute Et que l'utilisateur est à la séquence 3 position 1:42 Quand je fais un POST sur "/api/v1/audio-guides/ag_123/progress": """json { "current_sequence_id": "seq_3", "current_position_seconds": 102, "completed_sequences": ["seq_1", "seq_2"], "gps_position": { "latitude": 43.1234, "longitude": 2.5678 } } """ Alors le code HTTP de réponse est 200 Et la progression est sauvegardée en PostgreSQL Et le timestamp last_played_at est mis à jour Et le corps de réponse contient: """json { "saved": true, "synced_to_cloud": true, "updated_at": "2026-01-22T14:35:42Z" } """ Scénario: Sauvegarde automatique toutes les 30 secondes (client) Étant donné que le client mobile envoie la progression toutes les 30s Quand je fais un POST sur "/api/v1/audio-guides/{id}/progress" Alors la progression précédente est écrasée Et le timestamp est mis à jour Et la réponse est retournée en < 100ms Scénario: Sauvegarde des séquences complétées (>80%) Étant donné qu'une séquence de durée 180 secondes Et que l'utilisateur a écouté 150 secondes (83%) Quand la progression est sauvegardée Alors la séquence est ajoutée à completed_sequences Et le completion_rate est enregistré à 83% Scénario: Séquence non marquée complétée si <80% Étant donné qu'une séquence de durée 222 secondes Et que l'utilisateur a écouté 90 secondes (40%) Quand la progression est sauvegardée Alors la séquence n'est PAS ajoutée à completed_sequences Et le current_position est sauvegardé (pour reprise) Scénario: Structure de données progression en PostgreSQL Étant donné une sauvegarde de progression Alors la table audio_guide_progress contient: | champ | type | description | | id | UUID | ID progression | | user_id | UUID | ID utilisateur | | audio_guide_id | UUID | ID audio-guide | | current_sequence_id | UUID | Séquence en cours | | current_position | INTEGER | Position en secondes | | completed_sequences | UUID[] | Tableau séquences complétées | | last_played_at | TIMESTAMP | Dernière écoute | | gps_position | GEOGRAPHY | Position GPS optionnelle | | created_at | TIMESTAMP | Création | | updated_at | TIMESTAMP | Dernière MAJ | # 16.6.2 - Reprise progression Scénario: GET /api/v1/audio-guides/{id}/progress - Récupération progression Étant donné une progression sauvegardée pour "ag_123" Quand je fais un GET sur "/api/v1/audio-guides/ag_123/progress" Alors le code HTTP de réponse est 200 Et le corps de réponse contient: """json { "has_progress": true, "current_sequence_id": "seq_3", "current_position_seconds": 102, "completed_sequences": ["seq_1", "seq_2"], "completion_percentage": 25, "last_played_at": "2026-01-20T14:35:42Z", "can_resume": true } """ Scénario: Calcul completion_percentage Étant donné un audio-guide de 12 séquences Et que l'utilisateur a complété 3 séquences Quand le pourcentage de complétion est calculé Alors completion_percentage est 25% (3/12) Scénario: Progression inexistante (première écoute) Étant donné qu'aucune progression n'existe pour "ag_456" Quand je fais un GET sur "/api/v1/audio-guides/ag_456/progress" Alors le code HTTP de réponse est 200 Et le corps de réponse contient: """json { "has_progress": false, "can_resume": false } """ Scénario: Progression expirée (>30 jours) Étant donné une progression avec last_played_at à 35 jours Quand je fais un GET sur "/api/v1/audio-guides/ag_123/progress" Alors can_resume est false Et le message "Progression expirée après 30 jours" est retourné Et les données sont conservées mais marquées "expired" Scénario: DELETE /api/v1/audio-guides/{id}/progress - Réinitialisation Étant donné une progression existante Quand je fais un DELETE sur "/api/v1/audio-guides/ag_123/progress" Alors le code HTTP de réponse est 204 Et la progression est supprimée Et l'utilisateur peut recommencer depuis le début # 16.6.3 - Multi-device et synchronisation Scénario: Synchronisation cloud automatique Étant donné qu'une progression est sauvegardée sur iPhone Quand l'utilisateur ouvre l'app sur iPad Et fait un GET sur "/api/v1/audio-guides/ag_123/progress" Alors la progression iPhone est récupérée Et l'utilisateur peut reprendre exactement où il était Scénario: Conflit de synchronisation (dernier timestamp gagne) Étant donné une progression sur iPhone avec timestamp "2026-01-22T14:00:00Z" Et une progression sur iPad avec timestamp "2026-01-22T14:05:00Z" Quand les deux appareils synchronisent Alors la progression avec timestamp le plus récent (iPad) est conservée Et la progression iPhone est écrasée Scénario: GET /api/v1/audio-guides/progress/sync - Synchronisation batch Étant donné que l'utilisateur a 5 progressions locales non synchronisées Quand je fais un POST sur "/api/v1/audio-guides/progress/sync": """json { "progressions": [ { "audio_guide_id": "ag_1", "current_sequence_id": "seq_3", "current_position_seconds": 102, "updated_at": "2026-01-22T14:00:00Z" }, { "audio_guide_id": "ag_2", "current_sequence_id": "seq_5", "current_position_seconds": 45, "updated_at": "2026-01-22T14:10:00Z" } ] } """ Alors le code HTTP de réponse est 200 Et toutes les progressions sont synchronisées Et le corps de réponse contient: """json { "synced_count": 2, "conflicts": 0 } """ Scénario: Résolution conflit avec notification Étant donné une progression locale sur iPhone avec timestamp ancien Et une progression cloud plus récente (depuis iPad) Quand le sync est effectué Alors la progression cloud est conservée Et un conflit est signalé dans la réponse: """json { "synced_count": 1, "conflicts": 1, "conflict_details": [ { "audio_guide_id": "ag_123", "cloud_timestamp": "2026-01-22T15:00:00Z", "local_timestamp": "2026-01-22T14:30:00Z", "resolution": "cloud_wins" } ] } """ # Historique et statistiques Scénario: GET /api/v1/audio-guides/progress/history - Historique écoutes Étant donné que l'utilisateur a écouté 3 séquences d'un audio-guide Quand je fais un GET sur "/api/v1/audio-guides/ag_123/progress/history" Alors le code HTTP de réponse est 200 Et le corps de réponse contient l'historique: """json { "listening_sessions": [ { "sequence_id": "seq_1", "started_at": "2026-01-22T14:10:00Z", "completed_at": "2026-01-22T14:12:15Z", "completion_rate": 100, "duration_listened": 135 }, { "sequence_id": "seq_2", "started_at": "2026-01-22T14:12:20Z", "completed_at": "2026-01-22T14:14:08Z", "completion_rate": 100, "duration_listened": 108 }, { "sequence_id": "seq_3", "started_at": "2026-01-22T14:14:15Z", "completed_at": "2026-01-22T14:17:45Z", "completion_rate": 92, "duration_listened": 204 } ], "total_time_spent": 447 } """ Scénario: Détection complétion 100% de l'audio-guide Étant donné un audio-guide de 12 séquences Et que l'utilisateur complète la 12ème et dernière séquence à >80% Quand la progression est sauvegardée Alors is_completed passe à true Et completed_at est mis à jour avec le timestamp actuel Et un événement "audio_guide_completed" est émis Scénario: GET /api/v1/users/me/audio-guides/completed - Liste des complétés Étant donné que l'utilisateur a complété 2 audio-guides Quand je fais un GET sur "/api/v1/users/me/audio-guides/completed" Alors le code HTTP de réponse est 200 Et le corps de réponse contient: """json { "completed_count": 2, "audio_guides": [ { "audio_guide_id": "ag_1", "title": "Tour de Paris", "completed_at": "2026-01-15T10:00:00Z", "total_sequences": 10, "total_duration": 3600 }, { "audio_guide_id": "ag_2", "title": "Découverte de Lyon", "completed_at": "2026-01-20T14:00:00Z", "total_sequences": 8, "total_duration": 2700 } ] } """ Scénario: GET /api/v1/users/me/audio-guides/in-progress - Liste en cours Étant donné que l'utilisateur a 3 audio-guides en cours Quand je fais un GET sur "/api/v1/users/me/audio-guides/in-progress" Alors le code HTTP de réponse est 200 Et le corps de réponse contient: """json { "in_progress_count": 3, "audio_guides": [ { "audio_guide_id": "ag_123", "title": "Visite du Louvre", "current_sequence": 6, "total_sequences": 12, "completion_percentage": 50, "last_played_at": "2026-01-22T14:35:42Z" } ] } """ # Badges et achievements Scénario: Attribution badge "Premier audio-guide" Étant donné qu'un utilisateur complète son 1er audio-guide Quand le système détecte la complétion Alors un badge "first_audio_guide" est attribué Et une notification est envoyée: """json { "type": "badge_unlocked", "badge_id": "first_audio_guide", "title": "🎧 Premier audio-guide", "message": "Félicitations ! Vous avez complété votre premier audio-guide" } """ Plan du Scénario: Attribution badges selon nombre complétés Étant donné qu'un utilisateur complète son ème audio-guide Quand le système détecte la complétion Alors le badge "" est attribué Exemples: | nombre | badge_id | | 1 | first_audio_guide | | 5 | explorer | | 10 | completist | | 25 | expert | | 50 | master | # Nettoyage et archivage Scénario: Archivage progressions inactives (>6 mois) Étant donné une progression avec last_played_at à 200 jours Quand le job de nettoyage automatique s'exécute Alors la progression est déplacée vers la table audio_guide_progress_archive Et elle reste récupérable via l'API pendant 30 jours supplémentaires Et après 7 mois total, elle est supprimée définitivement Scénario: GET /api/v1/audio-guides/{id}/progress/archived - Récupération archivée Étant donné une progression archivée Quand je fais un GET sur "/api/v1/audio-guides/ag_123/progress/archived" Alors le code HTTP de réponse est 200 Et la progression archivée est retournée Et un message indique: "Progression archivée. Vous pouvez la restaurer." Scénario: POST /api/v1/audio-guides/{id}/progress/restore - Restauration Étant donné une progression archivée Quand je fais un POST sur "/api/v1/audio-guides/ag_123/progress/restore" Alors le code HTTP de réponse est 200 Et la progression est déplacée de archive vers la table active Et l'utilisateur peut reprendre son écoute # Cas d'erreur Scénario: Sauvegarde progression audio-guide inexistant Quand je fais un POST sur "/api/v1/audio-guides/ag_nonexistant/progress" Alors le code HTTP de réponse est 404 Et le message d'erreur est "Audio-guide non trouvé" Scénario: Sauvegarde progression séquence invalide Étant donné un audio-guide "ag_123" avec 8 séquences Et une requête avec current_sequence_id "seq_99" (inexistant) Quand je fais un POST sur "/api/v1/audio-guides/ag_123/progress" Alors le code HTTP de réponse est 400 Et le message d'erreur est "Séquence inexistante pour cet audio-guide" Scénario: Récupération progression sans authentification Étant donné une requête sans token JWT Quand je fais un GET sur "/api/v1/audio-guides/ag_123/progress" Alors le code HTTP de réponse est 401 Et le message d'erreur est "Authentification requise" Scénario: Corruption de données progression (récupération) Étant donné une progression avec données corrompues (JSON invalide) Quand je fais un GET sur "/api/v1/audio-guides/ag_123/progress" Alors le système tente une récupération depuis le backup quotidien Et si récupération réussie, les données sont restaurées Et un log d'erreur est créé pour investigation Scénario: Échec synchronisation cloud (mode dégradé) Étant donné que la base PostgreSQL est temporairement indisponible Quand je fais un POST sur "/api/v1/audio-guides/ag_123/progress" Alors le code HTTP de réponse est 503 Et le message d'erreur est "Service temporairement indisponible. Réessayez dans quelques instants." Et le client doit conserver la progression localement et réessayer # Performance et optimisations Scénario: Index sur user_id + audio_guide_id pour requêtes rapides Étant donné un index composite (user_id, audio_guide_id) Quand je fais un GET sur "/api/v1/audio-guides/ag_123/progress" Alors la requête PostgreSQL utilise l'index Et le temps de réponse est < 20ms Scénario: Cache Redis pour progressions actives Étant donné qu'une progression est fréquemment mise à jour (toutes les 30s) Quand la progression est sauvegardée Alors elle est également cachée dans Redis avec TTL 1h Et les GET suivants lisent depuis Redis (pas PostgreSQL) Et la latence est < 10ms Scénario: Invalidation cache Redis lors de réinitialisation Étant donné qu'une progression est en cache Redis Quand je fais un DELETE sur "/api/v1/audio-guides/ag_123/progress" Alors l'entrée cache Redis est supprimée Et la base PostgreSQL est mise à jour Et la cohérence est garantie