Files
jpgiannetti 5e5fcf4714 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.
2026-02-07 17:15:02 +01:00

358 lines
12 KiB
Gherkin

# 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