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.
486 lines
17 KiB
Gherkin
486 lines
17 KiB
Gherkin
# 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)
|