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:
357
docs/domains/content/features/audio-guides/publicites.feature
Normal file
357
docs/domains/content/features/audio-guides/publicites.feature
Normal file
@@ -0,0 +1,357 @@
|
||||
# language: fr
|
||||
|
||||
Fonctionnalité: API - Publicités dans audio-guides
|
||||
En tant que système backend
|
||||
Je veux gérer l'insertion et la diffusion de publicités dans les audio-guides
|
||||
Afin de monétiser les contenus gratuits
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est démarrée
|
||||
Et que l'utilisateur "user@example.com" est authentifié (gratuit)
|
||||
|
||||
# 16.5.1 - Insertion publicité
|
||||
|
||||
Scénario: Calcul insertion publicité (1 pub toutes les 5 séquences)
|
||||
Étant donné un audio-guide gratuit avec 12 séquences
|
||||
Et que la fréquence pub est configurée à "1/5"
|
||||
Quand je fais un GET sur "/api/v1/audio-guides/ag_123/ad-schedule"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"ad_frequency": "1/5",
|
||||
"ad_insertions": [
|
||||
{"after_sequence": 5, "position": "after"},
|
||||
{"after_sequence": 10, "position": "after"}
|
||||
],
|
||||
"total_ads": 2
|
||||
}
|
||||
"""
|
||||
|
||||
Plan du Scénario: Fréquence publicité configurable admin
|
||||
Étant donné que la fréquence pub est configurée à <frequence>
|
||||
Et un audio-guide avec 12 séquences
|
||||
Quand les insertions pub sont calculées
|
||||
Alors le nombre de pubs insérées est <nombre_pubs>
|
||||
|
||||
Exemples:
|
||||
| frequence | nombre_pubs |
|
||||
| 1/3 | 4 |
|
||||
| 1/5 | 2 |
|
||||
| 1/10 | 1 |
|
||||
|
||||
Scénario: Utilisateur Premium - Aucune publicité
|
||||
Étant donné un utilisateur Premium
|
||||
Et un audio-guide gratuit
|
||||
Quand je fais un GET sur "/api/v1/audio-guides/ag_123/ad-schedule"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"ad_frequency": "0",
|
||||
"ad_insertions": [],
|
||||
"total_ads": 0,
|
||||
"reason": "premium_user"
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: POST /api/v1/audio-guides/playback/next-ad - Récupération pub suivante
|
||||
Étant donné qu'un utilisateur termine la séquence 5
|
||||
Et qu'une pub doit être insérée
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/ag_123/playback/next-ad":
|
||||
"""json
|
||||
{
|
||||
"sequence_completed": 5,
|
||||
"user_position": {
|
||||
"latitude": 43.1234,
|
||||
"longitude": 2.5678
|
||||
},
|
||||
"mode": "voiture"
|
||||
}
|
||||
"""
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"should_play_ad": true,
|
||||
"ad": {
|
||||
"ad_id": "ad_789",
|
||||
"audio_url": "https://cdn.roadwave.fr/ads/ad_789.m4a",
|
||||
"duration_seconds": 30,
|
||||
"skippable_after": 5,
|
||||
"advertiser": "Brand X"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Sélection pub géolocalisée
|
||||
Étant donné que l'utilisateur est en Île-de-France (43.1234, 2.5678)
|
||||
Et que des publicités géolocalisées existent pour cette région
|
||||
Quand la pub suivante est sélectionnée
|
||||
Alors l'API filtre les pubs par:
|
||||
| critère | valeur |
|
||||
| Géolocalisation | Île-de-France |
|
||||
| Catégorie | Tourisme, Culture |
|
||||
| Langue | Français |
|
||||
| Budget actif | true |
|
||||
Et une pub correspondante est retournée
|
||||
|
||||
Scénario: Fallback pub nationale si pas de pub locale
|
||||
Étant donné que l'utilisateur est dans une région sans pubs locales
|
||||
Quand la pub suivante est sélectionnée
|
||||
Alors l'API sélectionne une pub nationale (France entière)
|
||||
Et la pub est retournée normalement
|
||||
|
||||
Scénario: Pas de pub si séquence non multiple de 5
|
||||
Étant donné qu'un utilisateur termine la séquence 4
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/ag_123/playback/next-ad"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"should_play_ad": false,
|
||||
"reason": "not_ad_sequence"
|
||||
}
|
||||
"""
|
||||
|
||||
# Comportement selon mode
|
||||
|
||||
Scénario: Pub en mode piéton (auto-play puis pause)
|
||||
Étant donné un audio-guide en mode piéton
|
||||
Et qu'une pub doit être insérée après séquence 5
|
||||
Quand la pub est récupérée
|
||||
Alors le mode_behavior retourné est:
|
||||
"""json
|
||||
{
|
||||
"auto_play": true,
|
||||
"pause_after": true,
|
||||
"reason": "pedestrian_mode"
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Pub en mode voiture/vélo/transport (auto-play puis séquence suivante)
|
||||
Étant donné un audio-guide en mode voiture
|
||||
Et qu'une pub doit être insérée
|
||||
Quand la pub est récupérée
|
||||
Alors le mode_behavior retourné est:
|
||||
"""json
|
||||
{
|
||||
"auto_play": true,
|
||||
"pause_after": false,
|
||||
"continue_to_next": true,
|
||||
"reason": "vehicle_mode"
|
||||
}
|
||||
"""
|
||||
|
||||
# 16.5.2 - Métriques et tracking
|
||||
|
||||
Scénario: POST /api/v1/ads/{ad_id}/impressions - Enregistrement impression
|
||||
Étant donné qu'une pub "ad_789" démarre
|
||||
Quand je fais un POST sur "/api/v1/ads/ad_789/impressions":
|
||||
"""json
|
||||
{
|
||||
"audio_guide_id": "ag_123",
|
||||
"sequence_after": 5,
|
||||
"user_id": "user_456",
|
||||
"timestamp": "2026-01-22T14:35:00Z"
|
||||
}
|
||||
"""
|
||||
Alors le code HTTP de réponse est 201
|
||||
Et l'impression est enregistrée dans ad_impressions
|
||||
Et le compteur impressions_count est incrémenté
|
||||
|
||||
Scénario: POST /api/v1/ads/{ad_id}/completions - Enregistrement écoute complète
|
||||
Étant donné qu'une pub de 30 secondes est écoutée à 25 secondes (83%)
|
||||
Quand je fais un POST sur "/api/v1/ads/ad_789/completions":
|
||||
"""json
|
||||
{
|
||||
"audio_guide_id": "ag_123",
|
||||
"listened_seconds": 25,
|
||||
"total_duration": 30,
|
||||
"completion_rate": 83
|
||||
}
|
||||
"""
|
||||
Alors le code HTTP de réponse est 201
|
||||
Et l'écoute complète est enregistrée (>80%)
|
||||
Et le créateur de l'audio-guide reçoit 0.003€ de revenus
|
||||
|
||||
Scénario: POST /api/v1/ads/{ad_id}/skips - Enregistrement skip
|
||||
Étant donné qu'une pub est skippée après 6 secondes
|
||||
Quand je fais un POST sur "/api/v1/ads/ad_789/skips":
|
||||
"""json
|
||||
{
|
||||
"audio_guide_id": "ag_123",
|
||||
"skipped_at_second": 6
|
||||
}
|
||||
"""
|
||||
Alors le code HTTP de réponse est 201
|
||||
Et le skip est enregistré dans ad_skips
|
||||
Et le taux de skip est mis à jour
|
||||
|
||||
Scénario: Validation écoute complète (>80%)
|
||||
Étant donné qu'une pub de 30 secondes est écoutée 20 secondes (66%)
|
||||
Quand je fais un POST sur "/api/v1/ads/ad_789/completions"
|
||||
Alors le code HTTP de réponse est 400
|
||||
Et le message d'erreur est "completion_rate: minimum 80% requis pour écoute complète"
|
||||
Et aucun revenu n'est attribué
|
||||
|
||||
# Métriques créateur
|
||||
|
||||
Scénario: GET /api/v1/creators/me/audio-guides/{id}/ad-metrics - Métriques pub
|
||||
Étant donné un audio-guide "ag_123" avec publicités
|
||||
Et les métriques suivantes sur 30 jours:
|
||||
| impressions | ecoutes_completes | skips | revenus |
|
||||
| 1000 | 650 | 350 | 1.95€ |
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/ad-metrics"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"period": "30_days",
|
||||
"impressions": 1000,
|
||||
"completions": 650,
|
||||
"skips": 350,
|
||||
"completion_rate": 65,
|
||||
"revenue": 1.95,
|
||||
"cpm": 1.95
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Distinction revenus contenus classiques vs audio-guides
|
||||
Étant donné un créateur avec contenus classiques et audio-guides
|
||||
Quand je fais un GET sur "/api/v1/creators/me/revenue-breakdown"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"total_revenue": 85.40,
|
||||
"breakdown": {
|
||||
"classic_content": {
|
||||
"revenue": 45.20,
|
||||
"impressions": 15000
|
||||
},
|
||||
"audio_guides": {
|
||||
"revenue": 40.20,
|
||||
"impressions": 13000
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
# Répartition revenus
|
||||
|
||||
Scénario: Calcul revenus créateur (3€ / 1000 écoutes complètes)
|
||||
Étant donné un audio-guide avec 1000 écoutes complètes pub ce mois
|
||||
Quand les revenus sont calculés
|
||||
Alors le créateur reçoit 3€
|
||||
Et le revenu par écoute complète est 0.003€
|
||||
|
||||
Scénario: POST /api/v1/ads/revenue/process - Calcul revenus batch mensuel
|
||||
Étant donné le 1er du mois
|
||||
Et que 500 créateurs ont des revenus pub à calculer
|
||||
Quand le job batch s'exécute
|
||||
Alors pour chaque créateur:
|
||||
| creator_id | ecoutes_completes | revenus |
|
||||
| creator_1 | 5000 | 15.00€ |
|
||||
| creator_2 | 2000 | 6.00€ |
|
||||
| creator_3 | 1200 | 3.60€ |
|
||||
Et les revenus sont ajoutés au solde creator_balance
|
||||
|
||||
# Normalisation audio
|
||||
|
||||
Scénario: Validation volume pub (-14 LUFS)
|
||||
Étant donné qu'une pub est uploadée avec volume -10 LUFS
|
||||
Quand la pub est validée
|
||||
Alors un processus de normalisation est déclenché
|
||||
Et le volume est ajusté à -14 LUFS (standard RoadWave)
|
||||
Et la pub normalisée est stockée sur le CDN
|
||||
|
||||
Scénario: Validation durée pub (max 60 secondes)
|
||||
Étant donné qu'une pub de 75 secondes est uploadée
|
||||
Quand la validation est effectuée
|
||||
Alors le code HTTP de réponse est 400
|
||||
Et le message d'erreur est "duration: maximum 60 secondes pour une publicité"
|
||||
|
||||
# Cas d'erreur
|
||||
|
||||
Scénario: Aucune pub disponible (stock épuisé)
|
||||
Étant donné qu'aucune campagne pub n'est active dans la région
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/ag_123/playback/next-ad"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"should_play_ad": false,
|
||||
"reason": "no_ads_available"
|
||||
}
|
||||
"""
|
||||
Et aucune pub n'est insérée (séquence suivante démarre directement)
|
||||
|
||||
Scénario: Budget campagne épuisé
|
||||
Étant donné qu'une campagne pub a un budget de 1000€
|
||||
Et que le budget est épuisé
|
||||
Quand la pub est sélectionnée
|
||||
Alors cette campagne est exclue de la sélection
|
||||
Et une autre campagne active est choisie
|
||||
|
||||
Scénario: Pub corrompue ou indisponible
|
||||
Étant donné qu'une pub sélectionnée a un fichier audio corrompu
|
||||
Quand le client tente de la charger
|
||||
Alors une pub de fallback (backup) est retournée
|
||||
Et un log d'erreur est créé pour investigation
|
||||
|
||||
# Filtrage et ciblage
|
||||
|
||||
Scénario: Ciblage par catégorie audio-guide
|
||||
Étant donné un audio-guide tagué "tourisme", "culture", "musée"
|
||||
Et une campagne pub ciblée "tourisme + culture"
|
||||
Quand la pub est sélectionnée
|
||||
Alors cette campagne a une priorité élevée (matching tags)
|
||||
Et elle est préférée aux pubs génériques
|
||||
|
||||
Scénario: Filtrage par classification âge
|
||||
Étant donné un audio-guide classifié "tout_public"
|
||||
Et une campagne pub classifiée "18+"
|
||||
Quand la pub est sélectionnée
|
||||
Alors cette campagne est exclue
|
||||
Et seules les pubs "tout_public" sont éligibles
|
||||
|
||||
Scénario: Limite fréquence pub par utilisateur (cap frequency)
|
||||
Étant donné qu'un utilisateur a déjà entendu la pub "ad_789" 3 fois ce jour
|
||||
Et que le cap frequency est configuré à 3/jour
|
||||
Quand la pub est sélectionnée
|
||||
Alors "ad_789" est exclue
|
||||
Et une autre pub est choisie
|
||||
|
||||
Scénario: GET /api/v1/audio-guides/{id}/ad-policy - Politique pub
|
||||
Étant donné un audio-guide
|
||||
Quand je fais un GET sur "/api/v1/audio-guides/ag_123/ad-policy"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"has_ads": true,
|
||||
"frequency": "1/5",
|
||||
"skippable_after_seconds": 5,
|
||||
"average_ad_duration": 30,
|
||||
"revenue_share": {
|
||||
"creator": "100%",
|
||||
"platform": "0%"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
# Performance
|
||||
|
||||
Scénario: Cache Redis pour pubs actives
|
||||
Étant donné que les campagnes actives sont en cache Redis
|
||||
Quand je fais un POST sur "/api/v1/audio-guides/ag_123/playback/next-ad"
|
||||
Alors les pubs sont récupérées depuis Redis (pas PostgreSQL)
|
||||
Et le temps de réponse est < 30ms
|
||||
|
||||
Scénario: Pre-fetching pub suivante (client)
|
||||
Étant donné que l'utilisateur est à la séquence 3
|
||||
Et qu'une pub sera insérée après la séquence 5
|
||||
Quand le client détecte l'approche de la séquence 5
|
||||
Alors il peut pré-charger la pub via GET "/api/v1/audio-guides/ag_123/ad-prefetch?after_sequence=5"
|
||||
Et la transition sera instantanée
|
||||
Reference in New Issue
Block a user