# 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 à Et un audio-guide avec 12 séquences Quand les insertions pub sont calculées Alors le nombre de pubs insérées est 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