refactor(docs): réorganiser la documentation selon principes DDD
Réorganise la documentation du projet selon les principes du Domain-Driven Design (DDD) pour améliorer la cohésion, la maintenabilité et l'alignement avec l'architecture modulaire du backend. **Structure cible:** ``` docs/domains/ ├── README.md (Context Map) ├── _shared/ (Core Domain) ├── recommendation/ (Supporting Subdomain) ├── content/ (Supporting Subdomain) ├── moderation/ (Supporting Subdomain) ├── advertising/ (Generic Subdomain) ├── premium/ (Generic Subdomain) └── monetization/ (Generic Subdomain) ``` **Changements effectués:** Phase 1: Création de l'arborescence des 7 bounded contexts Phase 2: Déplacement des règles métier (01-19) vers domains/*/rules/ Phase 3: Déplacement des diagrammes d'entités vers domains/*/entities/ Phase 4: Déplacement des diagrammes flux/états/séquences vers domains/*/ Phase 5: Création des README.md pour chaque domaine Phase 6: Déplacement des features Gherkin vers domains/*/features/ Phase 7: Création du Context Map (domains/README.md) Phase 8: Mise à jour de mkdocs.yml pour la nouvelle navigation Phase 9: Correction automatique des liens internes (script fix-markdown-links.sh) Phase 10: Nettoyage de l'ancienne structure (regles-metier/, diagrammes/, features/) **Configuration des tests:** - Makefile: godog run docs/domains/*/features/ - scripts/generate-bdd-docs.py: features_dir → docs/domains **Avantages:** ✅ Cohésion forte: toute la doc d'un domaine au même endroit ✅ Couplage faible: domaines indépendants, dépendances explicites ✅ Navigabilité améliorée: README par domaine = entrée claire ✅ Alignement code/docs: miroir de backend/internal/ ✅ Onboarding facilité: exploration domaine par domaine ✅ Tests BDD intégrés: features au plus près des règles métier Voir docs/REFACTOR-DDD.md pour le plan complet.
This commit is contained in:
@@ -0,0 +1,394 @@
|
||||
# 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 <nombre>ème audio-guide
|
||||
Quand le système détecte la complétion
|
||||
Alors le badge "<badge_id>" 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
|
||||
Reference in New Issue
Block a user