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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,240 @@
## 1. Authentification & Inscription
### 1.1 Méthodes d'inscription
**Décision** : Email/Password uniquement (pas d'OAuth tiers)
-**Pas de Google, Apple, Facebook OAuth** (dépendance services US/Chine)
-**Email + mot de passe** (formulaire natif Zitadel)
- ✅ 2FA (Two-Factor Authentication) disponible
- ✅ Option "Appareil de confiance" (skip 2FA pour 30 jours)
**Clarification technique** :
- Zitadel utilise OAuth2/OIDC comme **protocole** (standard moderne pour mobile)
- Mais l'authentification reste 100% **email/password natif**
- **Aucun fournisseur externe** (Google, Apple, etc.) n'est intégré
**Justification** :
- Souveraineté : pas de dépendance externe
- RGPD : données 100% contrôlées
- Coût : 0€ (Zitadel intégré)
> 📋 **Référence technique** : Voir [ADR-008 - OAuth2 vs Fournisseurs Tiers](../adr/008-authentification.md#oauth2-pkce--protocole-vs-fournisseurs-tiers) pour clarification protocole vs providers.
---
### 1.2 Vérification email
**Décision** : Différenciée selon le rôle utilisateur
#### Pour les auditeurs (écoute uniquement)
| État | Capacités |
|------|-----------|
| **Email non vérifié** | Lecture illimitée + création max 5 contenus |
| **Email vérifié** | Toutes fonctionnalités débloquées |
**Paramètres** :
- Lien de vérification expire après **7 jours**
- Possibilité de renvoyer le lien (max 3 fois/jour)
- Rappel in-app après création du 3ème contenu
**Justification** :
- Friction minimale à l'inscription
- Anti-spam sans bloquer l'essai du produit
- Incitation naturelle à vérifier (déblocage)
#### Pour les créateurs (monétisation)
**Vérification obligatoire sous 7 jours** pour :
- Accès au programme de monétisation
- KYC et reversement des revenus (conformité Mangopay)
- Publication illimitée de contenus
**Justification** :
- **Conformité légale** : KYC obligatoire pour transferts financiers
- **Anti-fraude** : Vérification identité réelle pour paiements
- **Responsabilité** : RoadWave doit pouvoir prouver identité créateurs monétisés
---
### 1.3 Données requises à l'inscription
**Obligatoires** :
- ✅ Email (format validé)
- ✅ Mot de passe (voir règles ci-dessous)
- ✅ Pseudo (3-30 caractères, alphanumérique + underscore)
- ✅ Date de naissance (vérification âge minimum)
**Optionnelles** :
- ❌ Nom complet (privacy by design)
- ❌ Photo de profil (avatar par défaut généré)
- ❌ Bio (ajout ultérieur)
**Âge minimum** :
- **13 ans minimum** (conformité réglementation réseaux sociaux EU)
- Vérification à l'inscription via date de naissance
- Blocage inscription si <13 ans avec message explicite
**Justification** :
- RGPD minimal data
- Friction réduite (4 champs max)
- Protection mineurs (obligation légale)
---
### 1.4 Tranches d'âge des contenus
**Décision** : Classification obligatoire des contenus
**Catégories** :
- 🟢 **Tout public** (défaut)
- 🟡 **13+** : contenu mature léger (débats, actualité sensible)
- 🟠 **16+** : contenu mature (violence verbale, sujets sensibles)
- 🔴 **18+** : contenu adulte (langage explicite, sujets réservés)
**Règles de diffusion** :
- Utilisateur 13-15 ans → contenus 🟢 🟡 (Tout public + 13+)
- Utilisateur 16-17 ans → contenus 🟢 🟡 🟠 (Tout public + 13+ + 16+)
- Utilisateur 18+ → tous contenus 🟢 🟡 🟠 🔴
**Modération** :
- Vérification obligatoire de la classification lors de la validation
- Reclassification possible par modérateurs
- Strike si classification volontairement incorrecte
**Justification** :
- Protection mineurs (obligation légale)
- Responsabilité plateforme
- Coût : champ supplémentaire + règle algo
---
### 1.5 Validation mot de passe
**Règles** :
- ✅ Minimum **8 caractères**
- ✅ Au moins **1 majuscule**
- ✅ Au moins **1 chiffre**
- ❌ Pas de symbole obligatoire (simplicité)
**Validation** :
- Côté client (feedback temps réel)
- Côté backend (sécurité)
- Message d'erreur explicite par règle non respectée
**Justification** :
- Standard industrie
- Bloque 95% des mots de passe faibles
- UX acceptable (pas trop restrictif)
---
### 1.6 Two-Factor Authentication (2FA)
**Décision** : Optionnel mais recommandé
**Méthodes disponibles** :
- ✅ TOTP (Time-based One-Time Password) via app (Google Authenticator, Authy)
- ✅ Email (code 6 chiffres, expire 10 min)
- ❌ SMS (coût élevé ~0.05€/SMS)
**Appareil de confiance** :
- Option "Ne plus demander sur cet appareil" → bypass 2FA pendant **30 jours**
- Révocable depuis paramètres compte
- Liste des appareils de confiance visible
**Justification** :
- Sécurité renforcée sans coût SMS
- UX : appareil de confiance évite friction quotidienne
- Zitadel natif (0€)
---
### 1.7 Tentatives de connexion
**Règles** :
- Maximum **5 tentatives** par période de **15 minutes**
- Blocage temporaire après 5 échecs
- Compteur reset automatique après 15 min
- Notification email si blocage (tentative suspecte)
**Déblocage** :
- Automatique après 15 min
- Ou via lien "Mot de passe oublié"
**Justification** :
- Anti brute-force
- Standard industrie (équilibre sécurité/UX)
- Zitadel natif (0€)
---
### 1.8 Sessions et refresh tokens
**Durée de vie** :
- **Access token** : 15 minutes
- **Refresh token** : 30 jours
**Rotation** :
- Refresh token rotatif (nouveau token à chaque refresh)
- Ancien token invalidé immédiatement
- Détection token replay attack
**Extension automatique** :
- Si app utilisée, session prolongée automatiquement
- Inactivité 30 jours → déconnexion
**Justification** :
- Sécurité (token court-vie)
- UX (pas de reconnexion fréquente)
- Standard OAuth2/OIDC
---
### 1.9 Multi-device
**Décision** : Sessions simultanées illimitées
**Gestion** :
- Liste des devices connectés visible (OS, navigateur, dernière connexion, IP/ville)
- Révocation individuelle possible
- Révocation globale "Déconnecter tous les appareils"
**Alertes** :
- Notification push + email si connexion depuis nouveau device
- Détection localisation suspecte (IP pays différent)
**Justification** :
- UX maximale (écoute voiture + tablette maison + web)
- Sécurité via transparence (utilisateur voit tout)
- Coût : table sessions PostgreSQL
---
### 1.10 Récupération de compte
**Méthode** : Email uniquement
**Processus** :
1. Utilisateur clique "Mot de passe oublié"
2. Email avec lien de reset envoyé
3. Lien expire après **1 heure**
4. Page de reset : nouveau mot de passe (validation règles)
5. Confirmation + déconnexion tous devices (sauf celui en cours)
**Notifications** :
- Email immédiat si changement mot de passe
- Push si changement depuis appareil non reconnu
**Limite** :
- Maximum **3 demandes/heure** (anti-spam)
**Justification** :
- Standard sécurité
- Pas de coût SMS
- Protection contre attaque sociale
---
## Récapitulatif Section 1

View File

@@ -0,0 +1,135 @@
## 12. Gestion des erreurs
### 12.1 Aucun contenu disponible
**Stratégie** : Élargissement automatique progressif
**Flow** :
```
1. Recherche rayon 50 km → aucun résultat
2. Élargissement auto 100 km
3. Si toujours rien → département
4. Si toujours rien → région
5. Dernier recours → contenu national (toujours disponible)
```
**Messages adaptatifs** :
| Cas | Message |
|-----|---------|
| **Trouvé à 100 km** | "Aucun contenu dans votre zone immédiate. Voici du contenu à proximité (100 km)" |
| **Trouvé département** | "Aucun contenu local disponible. Voici du contenu dans votre département" |
| **Contenu national** | "Aucun contenu local disponible. Voici du contenu national qui pourrait vous intéresser" |
**Justification** :
- **UX fluide** : pas de message d'erreur bloquant "Aucun contenu"
- **User ne reste jamais sans contenu**
- **Contenu national = filet de sécurité** : actualités Le Monde, podcasts génériques
---
### 12.2 Contenu signalé/supprimé pendant l'écoute
**Décision** : Pas d'interruption brutale
**Flow** :
```
1. Contenu supprimé côté backend (modération)
2. Si contenu en écoute → laisser terminer lecture en cours
3. Après fin lecture → désactiver bouton "Précédent" pour ce contenu
4. Passage automatique suivant après 2s
5. Toast notification discrète : "Contenu précédent retiré (violation règles)"
```
**Si tentative "Précédent" manuellement** :
- Message : "Ce contenu n'est plus disponible"
- Retour au contenu actuel
**Justification** :
- **Sécurité routière** : pas d'interruption brutale pendant conduite
- **User informé mais pas alarmé** : message discret
- **Empêche réécoute** : contenu modéré inaccessible
---
### 12.3 Perte de réseau
**Buffer adaptatif** (cf. TECHNICAL.md) :
| Réseau | Buffer min | Buffer cible | Buffer max |
|--------|------------|--------------|------------|
| **WiFi** | 5s | 30s | 120s |
| **4G/5G** | 10s | 45s | 120s |
| **3G** | 30s | 90s | 300s |
**Comportement détaillé** :
**Phase 1 : Connexion instable** (latence élevée, paquets perdus)
- Aucun message immédiat
- Lecture continue sur buffer
- Si > 10s latence : toast discret "Connexion instable"
**Phase 2 : Perte totale réseau**
- Lecture continue jusqu'à épuisement buffer
- Toast : "Hors ligne, lecture sur buffer (30s restantes)"
- Compte à rebours visible
**Phase 3 : Buffer épuisé sans reconnexion**
- Pause automatique
- Overlay : "Connexion perdue. Reconnexion en cours..."
- Retry automatique toutes les 5s (max 6 tentatives = 30s)
**Phase 4 : Basculement mode offline** (après 30s échec)
- Popup : "Voulez-vous continuer avec vos contenus téléchargés ?"
- Boutons : "Réessayer" / "Mode offline"
- Si "Mode offline" → lecture contenus téléchargés
**Reconnexion réussie** :
- Reprise automatique lecture au point d'arrêt exact
- Toast : "Connexion rétablie"
**Justification** :
- **Expérience fluide zones blanches** (tunnels, campagne)
- **Buffer généreux** : absorbe fluctuations réseau mobile
- **Mode offline secours** : si coupure prolongée
---
### 12.4 Géolocalisation désactivée
**Mode dégradé automatique**
**Contenu disponible** :
| Type contenu | Disponible |
|--------------|-----------|
| **Contenu national** (podcasts, actualités) | ✅ |
| **Contenu téléchargé** (offline) | ✅ |
| **Contenus "Neutre"** géographiquement | ✅ |
| **Contenu géolocalisé** (Ancré/Contextuel) | ❌ |
| **Audio-guides** | ❌ |
| **Notifications push géo-déclenchées** | ❌ |
**Popup au lancement** :
- **Apparition** : Premier lancement après refus géolocalisation
- **Message** : "RoadWave fonctionne mieux avec la géolocalisation activée. Sans elle, seul le contenu national sera disponible."
- **Boutons** :
- "Activer" → Redirection paramètres OS
- "Continuer sans" → Mode dégradé
- **Checkbox** : "Ne plus me demander"
**Banner permanent si refus** :
- Bandeau haut écran : "Mode limité : géolocalisation désactivée. [Activer]"
- Pas intrusif mais rappel constant
- Disparaît si géolocalisation réactivée
**Justification** :
- **App reste fonctionnelle** sans GPS (pas de blocage)
- **Incitation forte** à activer (meilleure UX)
- **Respecte choix user** (RGPD : consentement libre)
---
## Récapitulatif Section 12

View File

@@ -0,0 +1,336 @@
## 13. Conformité RGPD
### 13.1 Gestion du consentement
**Décision** : Tarteaucitron.js + PostgreSQL backend
**Implémentation web** :
- ✅ Tarteaucitron.js (opensource, self-hosted)
- ✅ Banner RGPD français, customisable
- ✅ Granularité : fonctionnel / analytique / marketing
**Implémentation backend** :
- Table `user_consents` avec versioning
- Champs : user_id, consent_type, version, accepted, timestamp
- Historique complet conservé (preuve légale)
**Consentements requis** :
- **Géolocalisation précise** : obligatoire (banner + permission OS)
- **Analytics** : optionnel (Matomo)
- **Notifications push** : optionnel (permission OS)
**Justification** :
- Opensource, 0€, conformité RGPD garantie
- Historique backend = preuve légale en cas de contrôle
- Granularité conforme recommandations CNIL
---
### 13.2 Anonymisation des données GPS
**Décision** : Geohash après 24h
**Processus** :
1. Données précises conservées **24h** (recommandation personnalisée)
2. Après 24h : conversion en geohash précision 5 (~5km²)
3. Coordonnées originales supprimées définitivement
**Implémentation PostGIS** :
```sql
-- Job quotidien
UPDATE location_history
SET location = ST_SetSRID(ST_GeomFromGeoHash(ST_GeoHash(location::geography, 5)), 4326)::geography,
anonymized = true
WHERE created_at < NOW() - INTERVAL '24 hours' AND anonymized = false;
```
**Exceptions** :
- ✅ Historique personnel visible (liste trajets) : conservation intégrale tant que compte actif
- ❌ Analytics globales : uniquement geohash anonyme
**Justification** :
- Vraie anonymisation RGPD (CNIL compliant)
- Permet analytics agrégées (heatmaps trafic)
- PostGIS natif, 0€
---
### 13.3 Export des données (portabilité)
**Décision** : JSON + HTML + ZIP, génération asynchrone
**Contenu de l'export** :
```
export-roadwave-[user_id]-[date].zip
├── export.json # Machine-readable
├── index.html # Human-readable (stylé)
├── audio/
│ ├── content-123.opus
│ ├── content-456.opus
│ └── ...
└── README.txt # Instructions
```
**Données exportées** :
- Profil utilisateur (email, pseudo, date inscription, bio)
- Historique d'écoute (titres, dates, durées)
- Contenus créés (audio + métadonnées)
- Abonnements et likes
- Centres d'intérêt (jauges)
- Historique consentements
**Processus** :
1. Demande via paramètres compte
2. Génération asynchrone (worker background)
3. Email avec lien download (expire **7 jours**)
4. Délai : **48h maximum** (conformité RGPD)
**Limite** :
- Maximum **1 export/mois** (anti-abus)
**Justification** :
- Conformité article 20 RGPD (portabilité)
- Double format (human + machine)
- Worker asynchrone évite timeout
---
### 13.4 Suppression du compte
**Décision** : Grace period 30j + anonymisation contenus
**Processus** :
1. Utilisateur clique "Supprimer mon compte"
2. Compte désactivé immédiatement (login impossible)
3. Contenus cachés pendant 30 jours (non diffusés)
4. Email confirmation + lien annulation (valide 30j)
5. Après 30j sans annulation : suppression effective
**Suppression effective** :
- ✅ Compte utilisateur supprimé (données personnelles)
- ✅ Historique d'écoute supprimé
- ✅ GPS historique supprimé
- ✅ Sessions et tokens révoqués
- ⚠️ Contenus créés **anonymisés** (créateur = "Utilisateur supprimé")
- ⚠️ Likes et abonnements supprimés (mais compteurs préservés)
**Contenus conservés anonymement** :
- Audio files (CDN)
- Métadonnées (titre, description, tags, géolocalisation)
- Statistiques d'écoute
**Justification** :
- Grace period évite suppressions impulsives
- Anonymisation contenus = intérêt légitime communauté
- Conforme RGPD si créateur = donnée supprimée
---
### 13.5 Mode dégradé (sans GPS précis)
**Décision** : GeoIP par défaut, GPS optionnel
**Niveaux de précision** :
| Niveau | Technologie | Contenus accessibles | Consentement |
|--------|-------------|---------------------|--------------|
| **Pays** | Aucune géoloc | Contenus nationaux uniquement | ❌ Non requis |
| **Ville** | GeoIP (IP2Location) | Contenus régionaux/ville | ❌ Non requis |
| **Précis** | GPS | Tous contenus (hyperlocaux inclus) | ✅ Requis |
**Implémentation** :
- Démarrage app : GeoIP automatique (IP → ville)
- Banner in-app : "Activez la géolocalisation pour découvrir du contenu près de chez vous"
- Upgrade volontaire vers GPS
**API GeoIP** :
- IP2Location Lite (gratuit, self-hosted, voir [ADR-019](../adr/019-geolocalisation-ip.md))
- Update DB mensuelle automatique
- Précision ~80% au niveau ville
**Justification** :
- RGPD : pas de consentement requis pour GeoIP (pas de donnée personnelle)
- UX dégradée acceptable (contenus disponibles)
- Progressive disclosure (upgrade optionnel)
---
### 13.6 Durée de conservation des données
**Décision** : 5 ans inactivité → purge automatique
**Règles** :
| Type de compte | Seuil inactivité | Action |
|----------------|------------------|--------|
| **Auditeur uniquement** | 5 ans sans connexion | Suppression automatique |
| **Créateur avec contenus actifs** | Jamais (tant qu'écoutes) | Conservation indéfinie |
| **Créateur inactif** | 5 ans sans connexion + 2 ans sans écoute | Suppression automatique |
**Notifications avant suppression** :
- Email + push : **90 jours** avant
- Email + push : **30 jours** avant
- Email + push : **7 jours** avant
- Toute connexion = reset compteur inactivité
**Contenu conservé** :
- Contenus créés par comptes supprimés (anonymisés) : conservation indéfinie
**Justification** :
- Conformité principe minimisation RGPD
- 5 ans = équilibre raisonnable (standard industrie)
- Exception créateurs actifs = intérêt légitime plateforme
---
### 13.7 Cookies et trackers web
**Décision** : Matomo self-hosted, zéro cookie tiers
**Cookies utilisés** :
| Cookie | Type | Durée | Finalité | Consentement |
|--------|------|-------|----------|--------------|
| `session` | Technique | 30j | Authentification | ❌ Non requis |
| `refresh_token` | Technique | 30j | Session persistante | ❌ Non requis |
| `_pk_id` | Analytique | 13 mois | Matomo (IP anonyme) | ✅ Requis |
**Analytics : Matomo self-hosted** :
- Hébergé sur nos serveurs (Docker)
- IP anonymisées automatiquement (2 derniers octets)
- Pas de cookie si consentement refusé
- Alternative : Plausible (SaaS EU, 9€/mois)
**Trackers interdits** :
- ❌ Google Analytics
- ❌ Facebook Pixel
- ❌ Hotjar, Mixpanel, etc.
**Justification** :
- Souveraineté données (pas de transfert US)
- Conformité RGPD max (CNIL compatible)
- Matomo = opensource, 0€ infra
---
### 13.8 Registre des traitements
**Décision** : Document Markdown versionné Git (MVP)
**Emplacement** :
- `docs/rgpd/registre-traitements.md`
- Versionné Git (historique modifications)
**Contenu obligatoire par traitement** :
- Nom et finalité du traitement
- Catégories de données collectées
- Base légale (consentement / contrat / intérêt légitime)
- Durée de conservation
- Destinataires (sous-traitants, CDN, etc.)
- Transferts hors UE (aucun prévu)
**Responsable** :
- DPO / Fondateur
- Review trimestrielle obligatoire
- Update immédiate si nouveau traitement
**Migration future** :
- Si > 100K utilisateurs : interface admin PostgreSQL
**Justification** :
- Obligation RGPD Article 30
- Markdown = simple, versionné, auditable
- 0€
---
### 13.9 Notification violations de données (breach)
**Décision** : Monitoring + alertes + runbook
**Détection automatique** :
| Événement | Outil | Alerte |
|-----------|-------|--------|
| Erreurs backend critiques | Sentry | Discord/Slack immédiat |
| Pic requêtes anormal | Grafana | Email équipe |
| Accès non autorisé DB | PostgreSQL logs | SMS fondateur |
| Authentification suspecte | Zitadel alerts | Email équipe |
**Procédure breach** :
- Runbook : `docs/rgpd/procedure-breach.md`
- Checklist 72h CNIL :
1. H+0 : Détection et confinement
2. H+24 : Évaluation gravité (données concernées, utilisateurs impactés)
3. H+48 : Notification CNIL si risque pour utilisateurs
4. H+72 : Notification utilisateurs si risque élevé
**Contact CNIL** :
- Email pré-rédigé (template)
- Formulaire en ligne (account CNIL créé)
**Justification** :
- Obligation RGPD Article 33 (notification 72h)
- Monitoring proactif évite découverte tardive
- Sentry gratuit < 5K events/mois
---
### 13.10 DPO (Délégué à la Protection des Données)
**Décision** : Fondateur = DPO temporaire (MVP)
**Raison légale** :
- Non obligatoire si :
- < 250 employés
- Pas de traitement à grande échelle de données sensibles
- RoadWave : données localisation = sensible MAIS échelle MVP
**Formation** :
- CNIL : formation gratuite en ligne (4h)
- Certification CNIL "Atelier RGPD" (gratuit)
**Contact** :
- Email : dpo@roadwave.fr
- Publié dans CGU et mentions légales
- Délai réponse : **1 mois** (RGPD)
**Migration future** :
- Si > 100K utilisateurs : DPO externe mutualisé (~200€/mois)
- Ou recrutement DPO interne si > 10 employés
**Justification** :
- Conforme RGPD (non obligatoire en phase MVP)
- 0€, contrôle total
- Bonne pratique : avoir un contact identifié
---
## Récapitulatif Section 13
| Mesure | Implémentation | Coût |
|--------|----------------|------|
| **Consentement** | Tarteaucitron.js + PostgreSQL | 0€ |
| **Anonymisation GPS** | Geohash PostGIS (24h) | 0€ |
| **Export données** | JSON+HTML+ZIP asynchrone | 0€ |
| **Suppression compte** | Grace period 30j + anonymisation | 0€ |
| **Mode dégradé** | GeoIP IP2Location + GPS optionnel | 0€ |
| **Conservation** | Purge auto 5 ans inactivité | 0€ |
| **Analytics** | Matomo self-hosted | ~5€/mois |
| **Registre traitements** | Markdown Git | 0€ |
| **Breach detection** | Sentry + Grafana + runbook | 0€ |
| **DPO** | Fondateur formé CNIL | 0€ |
**Coût total RGPD : ~5€/mois**
---
## Points d'attention pour Gherkin
- Tester consentement géolocalisation (accept/refuse → contenus différents)
- Tester anonymisation GPS après 24h (job cron)
- Tester export données (génération complète + vérification contenu)
- Tester grace period suppression (annulation possible)
- Tester mode GeoIP (ville détectée correctement)
- Tester purge automatique (5 ans inactivité)
- Tester notifications avant purge (90j/30j/7j)