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:
jpgiannetti
2026-02-07 17:15:02 +01:00
parent 78422bb2c0
commit 5e5fcf4714
227 changed files with 1413 additions and 1967 deletions

View File

@@ -0,0 +1,485 @@
# language: fr
Fonctionnalité: API - Métriques et analytics audio-guides
En tant que système backend
Je veux collecter et exposer les métriques d'écoute des audio-guides
Afin de fournir des insights aux créateurs
Contexte:
Étant donné que l'API RoadWave est démarrée
Et que le créateur "creator@example.com" est authentifié
# Statistiques globales audio-guide
Scénario: GET /api/v1/creators/me/audio-guides/{id}/stats - Statistiques générales
Étant donné un audio-guide "ag_123" avec les métriques suivantes:
| ecoutes_totales | ecoutes_completes | taux_completion | temps_ecoute_total |
| 1542 | 892 | 58% | 423h |
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/stats"
Alors le code HTTP de réponse est 200
Et le corps de réponse contient:
"""json
{
"listens_total": 1542,
"listens_complete": 892,
"completion_rate": 58,
"total_listen_time_seconds": 1522800,
"avg_listen_time_seconds": 988,
"unique_listeners": 1124,
"repeat_listeners": 418
}
"""
Scénario: Statistiques par période (7j, 30j, 90j, all-time)
Étant donné un audio-guide avec historique
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/stats?period=7d"
Alors le code HTTP de réponse est 200
Et les statistiques sur les 7 derniers jours sont retournées
Plan du Scénario: Périodes disponibles
Quand je fais un GET avec period=<period>
Alors les stats de la période <description> sont retournées
Exemples:
| period | description |
| 7d | 7 derniers jours |
| 30d | 30 derniers jours |
| 90d | 90 derniers jours |
| all | Depuis la création |
# Métriques par séquence
Scénario: GET /api/v1/creators/me/audio-guides/{id}/sequences/stats - Stats par séquence
Étant donné un audio-guide de 12 séquences
Et les métriques suivantes:
| sequence | starts | completions | abandon_rate |
| 1 | 1000 | 950 | 5% |
| 2 | 950 | 920 | 3% |
| 3 | 920 | 850 | 8% |
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/sequences/stats"
Alors le code HTTP de réponse est 200
Et le corps de réponse contient:
"""json
{
"sequences": [
{
"sequence_id": "seq_1",
"sequence_number": 1,
"title": "Introduction",
"starts": 1000,
"completions": 950,
"completion_rate": 95,
"abandon_rate": 5,
"avg_listen_time": 132,
"duration": 135
},
{
"sequence_id": "seq_2",
"sequence_number": 2,
"title": "Pyramide du Louvre",
"starts": 950,
"completions": 920,
"completion_rate": 97,
"abandon_rate": 3,
"avg_listen_time": 106,
"duration": 108
}
]
}
"""
Scénario: Identification séquence la plus écoutée
Étant donné les statistiques par séquence
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/sequences/top"
Alors le code HTTP de réponse est 200
Et le corps de réponse contient:
"""json
{
"most_listened": {
"sequence_id": "seq_3",
"title": "La Joconde",
"starts": 920,
"reason": "popular_highlight"
},
"least_listened": {
"sequence_id": "seq_11",
"title": "Aile Richelieu",
"starts": 580,
"reason": "late_sequence"
}
}
"""
# Points d'abandon
Scénario: Détection point d'abandon critique
Étant donné un audio-guide avec taux de complétion 58%
Et que 35% des utilisateurs abandonnent à la séquence 7
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/abandon-analysis"
Alors le code HTTP de réponse est 200
Et le corps de réponse contient:
"""json
{
"critical_abandon_point": {
"sequence_id": "seq_7",
"sequence_number": 7,
"title": "Aile Richelieu",
"abandon_rate": 35,
"severity": "high",
"suggestion": "Réduire la durée (8 min actuellement) ou rendre plus captivant"
}
}
"""
Scénario: Heatmap des abandons
Étant donné un audio-guide
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/abandon-heatmap"
Alors le code HTTP de réponse est 200
Et le corps de réponse contient une heatmap:
"""json
{
"heatmap": [
{"sequence": 1, "abandon_count": 50, "intensity": "low"},
{"sequence": 2, "abandon_count": 30, "intensity": "low"},
{"sequence": 7, "abandon_count": 320, "intensity": "high"},
{"sequence": 12, "abandon_count": 70, "intensity": "medium"}
]
}
"""
# Métriques géographiques
Scénario: GET /api/v1/creators/me/audio-guides/{id}/geographic-stats - Stats géo
Étant donné un audio-guide géolocalisé
Et les écoutes suivantes par région:
| region | listens | completions |
| Île-de-France | 850 | 520 |
| PACA | 320 | 180 |
| Auvergne | 145 | 90 |
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/geographic-stats"
Alors le code HTTP de réponse est 200
Et le corps de réponse contient:
"""json
{
"by_region": [
{
"region": "Île-de-France",
"listens": 850,
"completions": 520,
"completion_rate": 61
},
{
"region": "PACA",
"listens": 320,
"completions": 180,
"completion_rate": 56
}
]
}
"""
Scénario: Heatmap géographique des écoutes
Étant donné un audio-guide avec points GPS
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/geographic-heatmap"
Alors le code HTTP de réponse est 200
Et le corps de réponse contient:
"""json
{
"points": [
{
"sequence_id": "seq_1",
"gps_point": {"lat": 43.1234, "lon": 2.5678},
"listen_count": 1000,
"density": "high"
},
{
"sequence_id": "seq_2",
"gps_point": {"lat": 43.1245, "lon": 2.5690},
"listen_count": 950,
"density": "high"
}
]
}
"""
# Métriques déclenchement GPS
Scénario: Attribution GPS auto vs manuel
Étant donné un audio-guide voiture avec 8 points GPS
Et les déclenchements suivants:
| type | count |
| GPS auto | 542 |
| Manuel | 123 |
| Point manqué | 89 |
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/trigger-stats"
Alors le code HTTP de réponse est 200
Et le corps de réponse contient:
"""json
{
"total_triggers": 754,
"by_type": {
"gps_auto": 542,
"manual": 123,
"missed_point": 89
},
"gps_auto_rate": 72,
"manual_rate": 16,
"missed_rate": 12
}
"""
Scénario: Points GPS les plus manqués
Étant donné les statistiques de points manqués
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/missed-points"
Alors le code HTTP de réponse est 200
Et le corps de réponse contient:
"""json
{
"most_missed": [
{
"sequence_id": "seq_5",
"title": "Enclos des éléphants",
"missed_count": 45,
"missed_rate": 12,
"suggestion": "Rayon trop petit (30m) ou point mal placé"
}
]
}
"""
# Temps moyen par séquence
Scénario: Comparaison durée audio vs temps d'écoute moyen
Étant donné les métriques temporelles suivantes:
| sequence | duration | avg_listen_time | ecart |
| 1 | 135 | 130 | -5s |
| 2 | 108 | 90 | -18s |
| 3 | 222 | 220 | -2s |
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/time-analysis"
Alors le code HTTP de réponse est 200
Et le corps de réponse contient:
"""json
{
"sequences": [
{
"sequence_id": "seq_1",
"duration_seconds": 135,
"avg_listen_time": 130,
"delta": -5,
"completion_avg": 96
},
{
"sequence_id": "seq_2",
"duration_seconds": 108,
"avg_listen_time": 90,
"delta": -18,
"completion_avg": 83,
"warning": "Séquence souvent skippée ou abandonnée avant la fin"
}
]
}
"""
# Notifications milestones
Scénario: POST /api/v1/audio-guides/{id}/milestones/check - Vérification milestone
Étant donné qu'un audio-guide atteint 1000 écoutes
Quand le système vérifie les milestones
Alors un événement "milestone_reached" est émis
Et une notification est envoyée au créateur:
"""json
{
"type": "milestone",
"milestone_type": "listens_1000",
"audio_guide_id": "ag_123",
"message": "Félicitations ! Votre audio-guide 'Visite du Louvre' a atteint 1000 écoutes !",
"stats": {
"listens": 1000,
"completion_rate": 58
}
}
"""
Plan du Scénario: Milestones prédéfinis
Étant donné qu'un audio-guide atteint <seuil> écoutes
Quand le milestone est vérifié
Alors une notification "<type>" est envoyée
Exemples:
| seuil | type |
| 100 | listens_100 |
| 500 | listens_500 |
| 1000 | listens_1000 |
| 5000 | listens_5000 |
| 10000 | listens_10000 |
# Graphiques et visualisations
Scénario: GET /api/v1/creators/me/audio-guides/{id}/completion-funnel - Entonnoir complétion
Étant donné un audio-guide de 12 séquences
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/completion-funnel"
Alors le code HTTP de réponse est 200
Et le corps de réponse contient un graphique en entonnoir:
"""json
{
"funnel": [
{"sequence": 1, "listeners": 1000, "percentage": 100},
{"sequence": 2, "listeners": 950, "percentage": 95},
{"sequence": 3, "listeners": 890, "percentage": 89},
{"sequence": 12, "listeners": 580, "percentage": 58}
]
}
"""
Scénario: GET /api/v1/creators/me/audio-guides/{id}/listens-over-time - Écoutes dans le temps
Étant donné un audio-guide avec historique
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/listens-over-time?period=30d"
Alors le code HTTP de réponse est 200
Et le corps de réponse contient une série temporelle:
"""json
{
"period": "30d",
"granularity": "day",
"data_points": [
{"date": "2026-01-01", "listens": 45, "completions": 28},
{"date": "2026-01-02", "listens": 52, "completions": 31},
{"date": "2026-01-03", "listens": 38, "completions": 24}
]
}
"""
# Comparaisons et benchmarks
Scénario: GET /api/v1/creators/me/audio-guides/compare - Comparaison audio-guides
Étant donné que le créateur a 3 audio-guides:
| audio_guide_id | title | listens | completion_rate |
| ag_1 | Tour de Paris | 1200 | 65% |
| ag_2 | Visite Louvre | 1542 | 58% |
| ag_3 | Safari du Paugre | 890 | 72% |
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/compare"
Alors le code HTTP de réponse est 200
Et le corps de réponse contient:
"""json
{
"audio_guides": [
{
"audio_guide_id": "ag_2",
"title": "Visite Louvre",
"listens": 1542,
"completion_rate": 58,
"rank_by_listens": 1,
"rank_by_completion": 2
},
{
"audio_guide_id": "ag_1",
"title": "Tour de Paris",
"listens": 1200,
"completion_rate": 65,
"rank_by_listens": 2,
"rank_by_completion": 1
}
]
}
"""
Scénario: Benchmark par rapport à la moyenne plateforme
Étant donné qu'un audio-guide a un taux de complétion de 58%
Et que la moyenne plateforme pour la catégorie "musée" est 62%
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/benchmark"
Alors le code HTTP de réponse est 200
Et le corps de réponse contient:
"""json
{
"your_completion_rate": 58,
"category_avg": 62,
"platform_avg": 60,
"performance": "below_category_avg",
"percentile": 45
}
"""
# Événements trackés
Scénario: POST /api/v1/events/track - Tracking événements utilisateur
Étant donné qu'un utilisateur interagit avec un audio-guide
Quand un événement se produit
Alors il est tracké avec les données suivantes:
| événement | données |
| audio_guide_started | audio_guide_id, mode, user_id |
| sequence_completed | sequence_id, completion_rate, duration |
| audio_guide_completed | audio_guide_id, total_time, sequences_count|
| point_gps_triggered | point_id, distance, auto_or_manual |
| point_gps_missed | point_id, distance, action_taken |
Scénario: Exemple événement audio_guide_started
Quand un audio-guide démarre
Alors l'événement suivant est envoyé:
"""json
{
"event_type": "audio_guide_started",
"timestamp": "2026-01-22T14:00:00Z",
"user_id": "user_456",
"audio_guide_id": "ag_123",
"mode": "voiture",
"device": "ios",
"location": {"lat": 43.1234, "lon": 2.5678}
}
"""
Scénario: Exemple événement point_gps_triggered
Quand un point GPS déclenche une séquence
Alors l'événement suivant est envoyé:
"""json
{
"event_type": "point_gps_triggered",
"timestamp": "2026-01-22T14:05:30Z",
"user_id": "user_456",
"audio_guide_id": "ag_123",
"sequence_id": "seq_2",
"trigger_type": "gps_auto",
"distance_to_point": 25,
"speed_kmh": 28
}
"""
# Export données
Scénario: GET /api/v1/creators/me/audio-guides/{id}/export - Export CSV
Étant donné un audio-guide avec historique complet
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/export?format=csv"
Alors le code HTTP de réponse est 200
Et le Content-Type est "text/csv"
Et le fichier CSV contient:
| user_id | sequence_id | started_at | completed_at | completion_rate |
| user_123 | seq_1 | 2026-01-22 14:10:00 | 2026-01-22 14:12:15 | 100 |
| user_123 | seq_2 | 2026-01-22 14:12:20 | 2026-01-22 14:14:08 | 100 |
Scénario: Export JSON pour analyse externe
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/export?format=json"
Alors le code HTTP de réponse est 200
Et le Content-Type est "application/json"
Et le fichier JSON contient toutes les métriques détaillées
# Cache et performance
Scénario: Cache Redis pour stats fréquemment consultées
Étant donné que les stats globales d'un audio-guide sont en cache Redis
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/stats"
Alors les stats sont récupérées depuis Redis (pas PostgreSQL)
Et le temps de réponse est < 50ms
Et le cache a un TTL de 5 minutes
Scénario: Invalidation cache lors de nouvelles écoutes
Étant donné que les stats sont en cache Redis
Quand une nouvelle écoute complète est enregistrée
Alors le cache Redis est invalidé pour cet audio-guide
Et le prochain GET recalcule les stats depuis PostgreSQL
Scénario: Pré-calcul stats quotidien (job batch)
Étant donné que le job batch s'exécute chaque nuit à 3h
Quand le job démarre
Alors pour chaque audio-guide actif:
- Les stats sont calculées depuis les événements bruts
- Les résultats sont stockés dans audio_guide_stats_daily
- Les agrégations (7j, 30j, 90j) sont pré-calculées
Et les requêtes du lendemain sont instantanées (lecture table pré-calculée)