commit f99fb3c6145f3e4a764a62a85451c8228b867ced Author: jpgiannetti Date: Sat Jan 31 11:45:11 2026 +0100 Initial commit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3fbf326 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,146 @@ +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + test: + name: Test + runs-on: ubuntu-latest + + services: + postgres: + image: postgis/postgis:16-3.4-alpine + env: + POSTGRES_DB: roadwave_test + POSTGRES_USER: roadwave + POSTGRES_PASSWORD: test_password + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + redis: + image: redis:7-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + + - name: Cache Go modules + uses: actions/cache@v3 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Install dependencies + run: | + go mod download + go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest + go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest + + - name: Generate sqlc code + run: sqlc generate + + - name: Run migrations + env: + DATABASE_URL: postgres://roadwave:test_password@localhost:5432/roadwave_test?sslmode=disable + run: migrate -path migrations -database "$DATABASE_URL" up + + - name: Run unit tests + run: go test -v -race -short -coverprofile=coverage.out ./... + + - name: Run BDD tests + run: | + go install github.com/cucumber/godog/cmd/godog@latest + godog run features/ + + - name: Check coverage + run: | + coverage=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//') + echo "Total coverage: $coverage%" + if (( $(echo "$coverage < 80" | bc -l) )); then + echo "❌ Coverage $coverage% is below 80%" + exit 1 + fi + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + files: ./coverage.out + flags: unittests + name: codecov-umbrella + + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v4 + with: + version: latest + args: --timeout=5m + + build: + name: Build + runs-on: ubuntu-latest + needs: [test, lint] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + + - name: Build binary + run: | + CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o bin/api ./cmd/api + + - name: Build Docker image + run: docker build -f docker/Dockerfile -t roadwave:${{ github.sha }} . + + - name: Log in to Docker registry (main branch only) + if: github.ref == 'refs/heads/main' + run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin + + - name: Push Docker image (main branch only) + if: github.ref == 'refs/heads/main' + run: | + docker tag roadwave:${{ github.sha }} ${{ secrets.DOCKER_REGISTRY }}/roadwave:latest + docker tag roadwave:${{ github.sha }} ${{ secrets.DOCKER_REGISTRY }}/roadwave:${{ github.sha }} + docker push ${{ secrets.DOCKER_REGISTRY }}/roadwave:latest + docker push ${{ secrets.DOCKER_REGISTRY }}/roadwave:${{ github.sha }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3283dfe --- /dev/null +++ b/.gitignore @@ -0,0 +1,67 @@ +# Binaries +*.exe +*.exe~ +*.dll +*.so +*.dylib +/bin/ +/dist/ +api +worker +migrate + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool +*.out +coverage.out + +# Dependency directories +vendor/ + +# Go workspace file +go.work + +# Environment files +.env +.env.local +.env.*.local + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ +.DS_Store + +# Logs +*.log +logs/ + +# Temporary files +tmp/ +temp/ +.air/ + +# Database +*.db +*.sqlite +*.sqlite3 + +# Generated files +internal/database/sqlc/ + +# Docker volumes +postgres_data/ +redis_data/ + +# Config overrides (keep examples) +config/local.yaml +config/*.local.yaml + +# MkDocs +site/ +.cache/ +docs/bdd/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2cbe62f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,260 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +RoadWave is a geo-localized audio social network for road users (drivers, pedestrians, tourists). Users listen to audio content (podcasts, audio guides, ads, live radio) based on their geographic location and interests. + +**Tech Stack**: +- Backend: Go 1.21+ with Fiber framework +- Mobile: Flutter (see [ADR-014](docs/adr/014-frontend-mobile.md)) +- Database: PostgreSQL 16+ with PostGIS extension +- Cache: Redis 7+ with geospatial features +- Auth: Zitadel (self-hosted IAM) +- Data Access: sqlc for type-safe SQL code generation +- Streaming: HLS protocol with Opus codec + +## Monorepo Structure + +This is a monorepo organized as follows: + +``` +/backend → Go backend API (modular monolith) +/mobile → Flutter mobile app +/features → Shared BDD Gherkin features (test specs) +/docs/adr → Architecture Decision Records +/shared → Shared code and API contracts +/docker → Docker configuration files +``` + +**Important**: BDD test features (`.feature` files) are shared in `/features`, but each component implements its own step definitions: +- Backend step definitions: `backend/tests/bdd/` +- Mobile step definitions: `mobile/tests/bdd/` + +See [ADR-016](docs/adr/016-organisation-monorepo.md) for monorepo organization rationale. + +## Backend Architecture + +**Modular monolith** with clear module separation ([ADR-012](docs/adr/012-architecture-backend.md)): + +``` +backend/internal/ +├── auth/ # JWT validation, Zitadel integration +├── user/ # User profiles, interest gauges +├── content/ # Content CRUD, metadata +├── geo/ # Geospatial search, recommendation algorithm +├── streaming/ # HLS generation, transcoding +├── moderation/ # Content moderation, reporting workflow +├── payment/ # Mangopay integration +└── analytics/ # Listening metrics, interest gauge evolution +``` + +**Module pattern**: Each module follows `handler.go` → `service.go` → `repository.go`. + +**Database access**: Uses `sqlc` ([ADR-013](docs/adr/013-orm-acces-donnees.md)) for type-safe Go code generation from SQL queries. This allows writing complex PostGIS spatial queries while maintaining compile-time type safety. + +## Development Commands + +**IMPORTANT**: Always use `docker compose` (not `docker-compose`) as per user preferences. + +All commands must be run from the **monorepo root**: + +### Development +```bash +make init # Initialize project (install tools, setup .env) +make dev # Start backend with hot reload (Air) +make docker-up # Start all services (API, PostgreSQL, Redis, Zitadel, Adminer) +make docker-down # Stop all Docker services +make docker-logs # Show Docker logs +``` + +Services after `make docker-up`: +- API: http://localhost:8080 +- Zitadel: http://localhost:8081 +- Adminer: http://localhost:8082 + +### Testing + +**Test Strategy** ([ADR-015](docs/adr/015-strategie-tests.md)): +- Unit tests: Testify (80%+ coverage target) +- Integration tests: Testcontainers (for PostGIS queries) +- BDD tests: Godog/Gherkin (user stories validation) + +```bash +make test # Run all tests (unit + integration + BDD) +make test-unit # Unit tests only (fast, ~30s) +make test-integration # Integration tests with Testcontainers +make test-bdd # BDD tests with Godog (Gherkin features) +make test-coverage # Generate coverage report (coverage.html) +``` + +**Running a single test**: +```bash +cd backend +go test -v -run TestFunctionName ./path/to/package +``` + +**Running a single BDD feature**: +```bash +godog run features/path/to/feature.feature +``` + +### Database + +```bash +make migrate-up # Apply all migrations +make migrate-down # Rollback last migration +make migrate-create name=add_users # Create new migration +make migrate-version # Show current migration version +make sqlc-generate # Generate Go code from SQL queries +``` + +**After modifying SQL queries** in `backend/queries/*.sql`, always run `make sqlc-generate` to regenerate Go code. + +### Build & Code Quality + +```bash +make build # Build production binary (backend/bin/api) +make lint # Run golangci-lint +make format # Format Go code (gofmt) +make deps # Download and tidy Go dependencies +make clean # Clean build artifacts +``` + +### Documentation + +```bash +make docs-serve # Generate BDD docs and serve MkDocs (http://localhost:8000) +make bdd-docs # Same as docs-serve +make docs-pdf # Generate PDF of all documentation +make docs-clean # Remove generated docs and PDF +``` + +## Working with sqlc + +When adding or modifying database queries: + +1. Write SQL query in `backend/queries/*.sql`: +```sql +-- name: GetContentNearby :many +SELECT id, title, ST_Distance(location, $1::geography) as distance +FROM contents +WHERE ST_DWithin(location, $1::geography, $2) +ORDER BY distance +LIMIT $3; +``` + +2. Run code generation: +```bash +make sqlc-generate +``` + +3. Use generated type-safe Go code: +```go +contents, err := q.GetContentNearby(ctx, location, radius, limit) +``` + +## Writing BDD Tests + +BDD features use Gherkin syntax ([ADR-007](docs/adr/007-tests-bdd.md)): + +1. Write feature in `/features//.feature` +2. Implement step definitions in `backend/tests/bdd/` for API testing +3. Run with `make test-bdd` + +Example feature: +```gherkin +Feature: Geolocalised recommendation + + Scenario: Tourist near a monument + Given a user with "tourism" interest at 80% + And a GPS position 100m from the Eiffel Tower + When the system calculates recommendations + Then the audio guide "Histoire de la Tour Eiffel" is in first position +``` + +## Key Architectural Decisions + +All technical decisions are documented in Architecture Decision Records (ADRs) in `/docs/adr/`: + +- [ADR-001](docs/adr/001-langage-backend.md): Backend language (Go) +- [ADR-002](docs/adr/002-protocole-streaming.md): Streaming protocol (HLS) +- [ADR-005](docs/adr/005-base-de-donnees.md): Database (PostgreSQL + PostGIS) +- [ADR-008](docs/adr/008-authentification.md): Authentication (Zitadel) +- [ADR-012](docs/adr/012-architecture-backend.md): Backend architecture (modular monolith) +- [ADR-013](docs/adr/013-orm-acces-donnees.md): Data access (sqlc) +- [ADR-016](docs/adr/016-organisation-monorepo.md): Monorepo organization + +**When making architectural decisions**, check if there's an existing ADR or create a new one following the established pattern. + +## Recommendation Algorithm + +Core feature combining geolocation and interest matching: + +- **Geographic priority**: GPS point > city > department > region > country +- **Interest gauges**: Dynamic scores per category (automobile, travel, music, etc.) +- **Combined scoring**: Distance + interest matching +- **Cache**: Redis geospatial for performance (`GEORADIUS`) + +The algorithm is implemented in `backend/internal/geo/` and uses PostGIS functions like `ST_DWithin`, `ST_Distance`. + +## Common Patterns + +### Module Structure +Each backend module follows this pattern: +``` +internal/modulename/ +├── handler.go # HTTP handlers (Fiber routes) +├── service.go # Business logic +├── repository.go # Database access (sqlc generated code) +└── models.go # Domain models (if needed) +``` + +### Error Handling +Use the custom error package in `backend/pkg/errors/` for consistent error handling across the application. + +### Configuration +Environment variables are loaded via `backend/pkg/config/`. Development config is in `backend/.env` (copied from `.env.example` during `make init`). + +## PostGIS Spatial Queries + +Example patterns for geospatial operations: + +```sql +-- Find content within radius +WHERE ST_DWithin(location::geography, ST_MakePoint($lon, $lat)::geography, $radius_meters) + +-- Calculate distance +SELECT ST_Distance(location::geography, ST_MakePoint($lon, $lat)::geography) as distance + +-- Order by proximity +ORDER BY location <-> ST_MakePoint($lon, $lat)::geography +``` + +## Testing with Testcontainers + +Integration tests that require PostGIS use Testcontainers to spin up a real PostgreSQL+PostGIS instance: + +```go +// backend/tests/integration/geo_test.go +// Uses testcontainers to validate complex spatial queries +``` + +This ensures PostGIS queries work correctly without mocking. + +## Authentication Flow + +Zitadel handles authentication ([ADR-008](docs/adr/008-authentification.md)): +- Self-hosted on OVH (France data sovereignty) +- OAuth2 PKCE for mobile apps +- JWT validation in backend using zitadel-go SDK +- Shares PostgreSQL database with RoadWave (separate schema) + +## Performance Targets + +See [TECHNICAL.md](TECHNICAL.md) for detailed metrics: +- API latency p99: < 100ms +- Audio start time: < 3s +- Target availability: 99.9% +- Concurrent connections/server: 100K+ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..498fe8f --- /dev/null +++ b/Makefile @@ -0,0 +1,180 @@ +.PHONY: help init dev build test test-unit test-integration test-bdd test-coverage clean docs-clean docker-up docker-down docker-logs migrate-up migrate-down migrate-create migrate-version sqlc-generate lint format docs-serve bdd-docs docs-pdf + +# Colors for terminal output +BLUE := \033[0;34m +GREEN := \033[0;32m +YELLOW := \033[0;33m +RED := \033[0;31m +NC := \033[0m # No Color + +## help: Display this help message +help: + @echo "$(BLUE)RoadWave - Makefile Commands$(NC)" + @echo "" + @grep -E '^## ' $(MAKEFILE_LIST) | sed 's/^## / /' + @echo "" + +## init: Initialize project (install tools, setup env) +init: + @echo "$(GREEN)Initializing project...$(NC)" + @go install github.com/cosmtrek/air@latest + @go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest + @go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest + @go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + @cp backend/.env.example backend/.env + @echo "$(GREEN)✓ Project initialized$(NC)" + +## dev: Start development environment with hot reload +dev: + @echo "$(BLUE)Starting development server...$(NC)" + @cd backend && air -c .air.toml + +## build: Build production binary +build: + @echo "$(BLUE)Building production binary...$(NC)" + @cd backend && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o bin/api ./cmd/api + @echo "$(GREEN)✓ Binary built: backend/bin/api$(NC)" + +## test: Run all tests +test: test-unit test-integration test-bdd + +## test-unit: Run unit tests +test-unit: + @echo "$(BLUE)Running unit tests...$(NC)" + @cd backend && go test -v -race -short ./... + +## test-integration: Run integration tests +test-integration: + @echo "$(BLUE)Running integration tests...$(NC)" + @cd backend && go test -v -race -tags=integration ./... + +## test-bdd: Run BDD tests (Godog) +test-bdd: + @echo "$(BLUE)Running BDD tests...$(NC)" + @godog run features/ + +## test-coverage: Run tests with coverage report +test-coverage: + @echo "$(BLUE)Running tests with coverage...$(NC)" + @cd backend && go test -race -coverprofile=coverage.out -covermode=atomic ./... + @cd backend && go tool cover -html=coverage.out -o coverage.html + @cd backend && go tool cover -func=coverage.out | grep total | awk '{print "Total coverage: " $$3}' + @echo "$(GREEN)✓ Coverage report: backend/coverage.html$(NC)" + +## clean: Clean build artifacts and temporary files +clean: + @echo "$(YELLOW)Cleaning...$(NC)" + @rm -rf bin/ tmp/ coverage.out coverage.html + @echo "$(GREEN)✓ Cleaned$(NC)" + +## docs-clean: Remove generated documentation (BDD docs and PDF) +docs-clean: + @echo "$(YELLOW)Cleaning generated documentation...$(NC)" + @rm -rf docs/bdd/ output/RoadWave_Documentation.pdf + @docker rmi roadwave-pdf-generator 2>/dev/null || true + @echo "$(GREEN)✓ Documentation cleaned$(NC)" + +## docker-up: Start all Docker services +docker-up: + @echo "$(BLUE)Starting Docker services...$(NC)" + @docker compose up -d + @echo "$(GREEN)✓ Services started$(NC)" + @echo "$(YELLOW)API: http://localhost:8080$(NC)" + @echo "$(YELLOW)Zitadel: http://localhost:8081$(NC)" + @echo "$(YELLOW)Adminer: http://localhost:8082$(NC)" + +## docker-down: Stop all Docker services +docker-down: + @echo "$(YELLOW)Stopping Docker services...$(NC)" + @docker compose down + @echo "$(GREEN)✓ Services stopped$(NC)" + +## docker-logs: Show Docker logs +docker-logs: + @docker compose logs -f + +## migrate-up: Apply all migrations +migrate-up: + @echo "$(BLUE)Applying migrations...$(NC)" + @migrate -path backend/migrations -database "postgres://roadwave:dev_password@localhost:5432/roadwave_dev?sslmode=disable" up + @echo "$(GREEN)✓ Migrations applied$(NC)" + +## migrate-down: Rollback last migration +migrate-down: + @echo "$(YELLOW)Rolling back last migration...$(NC)" + @migrate -path backend/migrations -database "postgres://roadwave:dev_password@localhost:5432/roadwave_dev?sslmode=disable" down 1 + @echo "$(GREEN)✓ Migration rolled back$(NC)" + +## migrate-create: Create new migration (usage: make migrate-create name=add_users) +migrate-create: + @if [ -z "$(name)" ]; then \ + echo "$(RED)Error: name parameter required$(NC)"; \ + echo "Usage: make migrate-create name=add_users"; \ + exit 1; \ + fi + @migrate create -ext sql -dir backend/migrations -seq $(name) + @echo "$(GREEN)✓ Migration created$(NC)" + +## migrate-version: Show current migration version +migrate-version: + @migrate -path backend/migrations -database "postgres://roadwave:dev_password@localhost:5432/roadwave_dev?sslmode=disable" version + +## sqlc-generate: Generate Go code from SQL queries +sqlc-generate: + @echo "$(BLUE)Generating Go code from SQL...$(NC)" + @cd backend && sqlc generate + @echo "$(GREEN)✓ Code generated$(NC)" + +## lint: Run linter +lint: + @echo "$(BLUE)Running linter...$(NC)" + @cd backend && golangci-lint run ./... + +## format: Format code +format: + @echo "$(BLUE)Formatting code...$(NC)" + @cd backend && go fmt ./... + @cd backend && gofmt -s -w . + @echo "$(GREEN)✓ Code formatted$(NC)" + +## deps: Download dependencies +deps: + @echo "$(BLUE)Downloading dependencies...$(NC)" + @cd backend && go mod download + @cd backend && go mod tidy + @echo "$(GREEN)✓ Dependencies updated$(NC)" + +## run-api: Run API server (without hot reload) +run-api: + @echo "$(BLUE)Starting API server...$(NC)" + @cd backend && go run cmd/api/main.go + +## docs-serve: Start documentation server (http://localhost:8000) +docs-serve: + @echo "$(BLUE)Generating BDD documentation from Gherkin files...$(NC)" + @python3 scripts/generate-bdd-docs.py + @echo "$(GREEN)✓ BDD documentation generated$(NC)" + @echo "$(BLUE)Starting documentation server...$(NC)" + @echo "$(YELLOW)Documentation available at http://localhost:8000$(NC)" + @docker run --rm -it -p 8000:8000 -v "$(PWD):/docs" squidfunk/mkdocs-material + +## bdd-docs: Generate BDD docs from Gherkin and serve with MkDocs (http://localhost:8000) +bdd-docs: + @echo "$(BLUE)Generating BDD documentation from Gherkin files...$(NC)" + @python3 scripts/generate-bdd-docs.py + @echo "$(GREEN)✓ BDD documentation generated$(NC)" + @echo "$(BLUE)Starting documentation server...$(NC)" + @echo "$(YELLOW)Documentation available at http://localhost:8000$(NC)" + @echo "$(YELLOW)Navigate to 'Tests BDD' section$(NC)" + @docker run --rm -it -p 8000:8000 -v "$(PWD):/docs" squidfunk/mkdocs-material + +## docs-pdf: Generate PDF of all documentation (output/RoadWave_Documentation.pdf) +docs-pdf: + @echo "$(BLUE)Generating BDD documentation from Gherkin files...$(NC)" + @python3 scripts/generate-bdd-docs.py + @echo "$(GREEN)✓ BDD documentation generated$(NC)" + @echo "$(BLUE)Building PDF generator Docker image...$(NC)" + @docker build -t roadwave-pdf-generator -f scripts/Dockerfile.pdf . -q + @echo "$(BLUE)Generating PDF documentation...$(NC)" + @docker run --rm -u $(shell id -u):$(shell id -g) -v "$(PWD):/docs" roadwave-pdf-generator + @echo "$(GREEN)✓ PDF generated: output/RoadWave_Documentation.pdf$(NC)" diff --git a/README.md b/README.md new file mode 100644 index 0000000..6b3996d --- /dev/null +++ b/README.md @@ -0,0 +1,285 @@ +# RoadWave + +Réseau social audio géolocalisé pour les usagers de la route. + +## Concept + +RoadWave permet aux conducteurs d'écouter du contenu audio contextuel pendant leurs trajets. La navigation se fait par commandes au volant (suivant/précédent), inspirée des réseaux à scroll infini. + +Le contenu est diffusé en fonction de la position géographique de l'utilisateur et de ses centres d'intérêt. + +--- + +## Cas d'usage + +| Utilisateur | Scénario | +|-------------|----------| +| **Conducteur** | Écoute contenu audio en conduisant, navigation par commandes au volant (suivant/précédent), reçoit notifications géolocalisées en passant près de points d'intérêt | +| **Routier** | Écoute podcasts et radios live pendant ses trajets longue distance | +| **Touriste à pied** | Visite guidée audio d'un musée, monument ou ville : choisit parmi plusieurs guides, navigue entre séquences à son rythme (tactile/vocal), reçoit notification push quand un audio-guide est disponible à proximité | +| **Commerçant** | Diffuse une publicité audio ciblée GPS devant son commerce | +| **Passionné auto** | Découvre du contenu automobile près de circuits ou concessionnaires | +| **Habitant local** | Partage anecdotes ou bons plans géolocalisés dans son quartier | +| **Média traditionnel** | Le Monde, Le Parisien diffusent actualités géolocalisées ou nationales | + +--- + +## Utilisateurs + +Tout utilisateur peut écouter et créer du contenu (rôle flexible). + +| Rôle | Description | +|------|-------------| +| **Auditeur** | Écoute, like, s'abonne à des créateurs, signale des contenus | +| **Créateur** | Publie du contenu audio géolocalisé (individus, médias traditionnels) | +| **Publicitaire** | Diffuse des publicités ciblées géographiquement | +| **Modérateur** | Valide et modère les contenus signalés | + +--- + +## Types de contenu + +| Type | Description | +|------|-------------| +| **Contenu court** | Audio de quelques secondes à quelques minutes | +| **Podcast** | Épisodes plus longs, séries thématiques | +| **Radio live** | Diffusion en direct avec synchronisation approximative entre auditeurs | +| **Audio-guide** | Visite guidée multiséquence (musée, monument, ville) : plusieurs séquences numérotées, navigation manuelle entre pistes, liste complète visible, guidage vocal entre points d'intérêt | + +--- + +## Géolocalisation + +Le créateur définit la zone de diffusion de son contenu : + +| Niveau | Portée | +|--------|--------| +| **Point GPS** | Rayon précis autour d'une coordonnée | +| **Ville** | Diffusion dans une ville | +| **Département** | Diffusion départementale | +| **Région** | Diffusion régionale | +| **Pays** | Diffusion nationale | + +**Priorité de diffusion** : plus la zone est précise, plus le contenu a de chances d'être diffusé (GPS > ville > département > région > pays). + +--- + +## Algorithme de recommandation + +Le contenu proposé est calculé via un **score combiné** : + +- **Proximité géographique** : distance entre l'utilisateur et la zone du contenu +- **Pertinence des intérêts** : correspondance avec les centres d'intérêt de l'utilisateur + +Lorsque plusieurs contenus sont disponibles dans une zone, **seul le plus pertinent est diffusé**. + +--- + +## Centres d'intérêt + +Chaque utilisateur possède des **jauges d'intérêt** qui évoluent dynamiquement : + +### Catégories +- Automobile +- Voyage +- Famille +- Amour +- Musique +- Économie +- Cryptomonnaie +- Politique +- *... (extensible)* + +### Évolution des jauges + +| Action | Effet | +|--------|-------| +| Temps d'écoute long | Augmente la jauge | +| Like | Augmente la jauge | +| Abonnement | Augmente fortement la jauge | +| Skip rapide | Diminue la jauge | + +Les créateurs taguent leur contenu avec des centres d'intérêt. L'algorithme privilégie les correspondances mais n'exclut pas les utilisateurs sans correspondance. + +--- + +## Interactions + +### Commandes au volant (conduite) + +Interactions simplifiées pour sécurité routière maximale : + +| Commande | Action | +|----------|--------| +| **Suivant** | Passer au contenu suivant | +| **Précédent** | Revenir au contenu précédent | +| **Play/Pause** | Mettre en pause / reprendre la lecture | + +**Like automatique** : Le système détecte automatiquement vos préférences selon votre temps d'écoute : +- Écoute ≥80% du contenu → Like renforcé (+2 points jauge) +- Écoute 30-79% du contenu → Like standard (+1 point jauge) +- Skip après <10s → Signal négatif (-0.5 point) + +> Voir [ADR-010](docs/adr/010-commandes-volant.md) pour les détails techniques + +--- + +## Publicités + +- Insertion **entre deux contenus** uniquement (jamais d'interruption) +- Ciblage géographique : point GPS, ville, département, région ou national +- Interface dédiée pour les publicitaires + +--- + +## Radio live + +- Diffusion en direct par des créateurs +- **Buffering** pour garantir une écoute fluide +- **Synchronisation approximative** entre les auditeurs (quelques secondes de décalage possible) + +--- + +## Modération + +Approche hybride combinant participation communautaire, IA et modérateurs dédiés. + +### Contenus prohibés + +| Catégorie | Description | +|-----------|-------------| +| **Haine et violence** | Incitation à la haine, violence, discrimination | +| **Contenu sexuel** | Pornographie ou contenu sexuellement explicite | +| **Illégalité** | Apologie du terrorisme, actes criminels | +| **Désinformation dangereuse** | Fausses informations sur la santé, sécurité routière | +| **Harcèlement** | Menaces, intimidation, doxxing | +| **Droits d'auteur** | Violation de propriété intellectuelle | +| **Fraude** | Arnaques, escroqueries | + +### Rôles de modération + +| Rôle | Capacités | +|------|-----------| +| **Auditeur lambda** | Signaler un contenu (1 clic) | +| **Auditeur de confiance** | Signalements priorisés après historique positif | +| **Modérateur junior** | Traiter signalements simples (spam, contenu évident) | +| **Modérateur senior** | Cas complexes, appels, décisions de ban | +| **Admin modération** | Définir les règles, superviser l'équipe | + +### Flux de modération + +``` +1. Auditeur signale → File d'attente +2. IA pré-filtre → Cas évidents traités automatiquement +3. Modérateur junior → Traite 80% des cas restants +4. Modérateur senior → Cas complexes + recours +``` + +### Outils de modération automatique + +| Outil | Fonction | +|-------|----------| +| **Transcription audio** | Conversion automatique en texte pour analyse | +| **Analyse vocale IA** | Détection de ton agressif, cris, insultes | +| **Empreinte audio** | Détection de contenus déjà modérés (réupload) | +| **Détection droits d'auteur** | Identification automatique de musique protégée | +| **Filtrage mots-clés** | Liste noire de termes inappropriés | + +### Modération préventive + +- **Nouveaux créateurs** : validation manuelle des 3 premiers contenus +- **Score de confiance** : évolution selon l'historique du créateur +- **Publicités** : validation manuelle obligatoire avant diffusion + +### Système de strikes + +| Strike | Sanction | Durée suspension | +|--------|----------|------------------| +| **Strike 1** | Avertissement + suppression contenu + suspension upload | 3 jours | +| **Strike 2** | Suppression contenu + suspension upload | 7 jours | +| **Strike 3** | Suppression contenu + suspension upload | 30 jours | +| **Strike 4** | Ban définitif du compte créateur | Permanent | + +**Notes** : +- **Tolérance 1ère fois** (droits d'auteur uniquement) : avertissement sans strike +- **Violations graves** (haine, illégalité, violence) : strike immédiat sans tolérance +- **Réhabilitation** : -1 strike tous les 6 mois sans nouvelle violation + +### Priorisation des signalements + +| Priorité | Type de contenu | +|----------|-----------------| +| **CRITIQUE** | Violence, suicide, mise en danger immédiate | +| **HAUTE** | Harcèlement, haine, désinformation | +| **MOYENNE** | Spam, contenu inapproprié | +| **BASSE** | Qualité audio, tags incorrects | + +### Transparence et recours + +- **Notification explicite** lors de suppression (raison détaillée) +- **Processus d'appel** : le créateur peut contester une décision +- **Délai de traitement** : 48-72h pour les recours +- **Historique** : tableau de bord des sanctions pour le créateur + +### Modération communautaire + +- **Utilisateurs de confiance** : signalements priorisés après historique positif +- **Récompenses** : badges, réduction premium pour signalements pertinents +- Lutte contre les signalements abusifs (sanctions possibles) + +--- + +## Modèle économique + +### Offres + +| Formule | Description | +|---------|-------------| +| **Gratuit** | Accès complet avec publicités entre les contenus | +| **Premium** | Sans publicité + accès aux contenus exclusifs | + +### Monétisation créateurs + +- **Partage des revenus pub** : rémunération basée sur le nombre d'écoutes +- **Pourboires** : les auditeurs peuvent faire des dons aux créateurs + +--- + +## Conformité RGPD + +### Permissions GPS + +RoadWave adapte ses permissions selon l'usage : + +| Mode | Permission GPS | Fonctionnalités | +|------|----------------|-----------------| +| **Mode voiture** | "When In Use" uniquement | Écoute contenu géolocalisé pendant conduite | +| **Mode piéton** | "Always Location" optionnelle | Notifications audio-guides en arrière-plan | + +**Important** : La permission "Always Location" est **optionnelle** et réservée au mode piéton pour recevoir des notifications push lorsqu'un audio-guide est disponible à proximité. Elle n'est jamais requise pour l'usage principal en voiture. + +> Voir [ADR-011](docs/adr/011-conformite-stores-carplay-android-auto.md) pour les détails techniques + +### Données collectées + +| Donnée | Finalité | Base légale | +|--------|----------|-------------| +| **Position GPS** | Diffusion de contenu géolocalisé | Consentement | +| **Historique d'écoute** | Personnalisation des recommandations | Intérêt légitime | +| **Centres d'intérêt** | Algorithme de recommandation | Consentement | +| **Identité créateur** | Publication de contenu | Exécution du contrat | + +### Droits des utilisateurs + +- **Accès** : consulter toutes ses données personnelles +- **Rectification** : modifier ses informations +- **Suppression** : supprimer son compte et toutes ses données +- **Portabilité** : exporter ses données dans un format standard +- **Opposition** : désactiver le profilage publicitaire + +### Mesures techniques + +- Consentement explicite requis pour la géolocalisation +- Anonymisation des données de localisation après 24h (sauf historique personnel) +- Possibilité d'utiliser l'app en mode dégradé (sans géolocalisation précise) +- Données hébergées dans l'UE diff --git a/TECHNICAL.md b/TECHNICAL.md new file mode 100644 index 0000000..118e4d4 --- /dev/null +++ b/TECHNICAL.md @@ -0,0 +1,196 @@ +# RoadWave - Architecture Technique + +> Les décisions techniques sont documentées dans [docs/adr/](docs/adr/) + +## Stack Technologique + +| Composant | Technologie | ADR | +|-----------|-------------|-----| +| **Backend** | Go + Fiber | [ADR-001](docs/adr/001-langage-backend.md) | +| **Architecture Backend** | Monolithe Modulaire | [ADR-012](docs/adr/012-architecture-backend.md) | +| **Authentification** | Zitadel (self-hosted OVH) | [ADR-008](docs/adr/008-authentification.md) | +| **Streaming** | HLS | [ADR-002](docs/adr/002-protocole-streaming.md) | +| **Codec** | Opus | [ADR-003](docs/adr/003-codec-audio.md) | +| **CDN** | NGINX Cache (OVH VPS) | [ADR-004](docs/adr/004-cdn.md) | +| **Storage** | OVH Object Storage | [ADR-004](docs/adr/004-cdn.md) | +| **Hébergement MVP** | OVH VPS Essential | [ADR-017](docs/adr/017-hebergement.md) | +| **Organisation** | Monorepo | [ADR-016](docs/adr/016-organisation-monorepo.md) | +| **Base de données** | PostgreSQL + PostGIS | [ADR-005](docs/adr/005-base-de-donnees.md) | +| **ORM/Accès données** | sqlc | [ADR-013](docs/adr/013-orm-acces-donnees.md) | +| **Cache** | Redis Cluster | [ADR-005](docs/adr/005-base-de-donnees.md) | +| **Chiffrement** | TLS 1.3 | [ADR-006](docs/adr/006-chiffrement.md) | +| **Live** | WebRTC | [ADR-002](docs/adr/002-protocole-streaming.md) | +| **Frontend Mobile** | Flutter | [ADR-014](docs/adr/014-frontend-mobile.md) | +| **Tests** | Testify + Godog (Gherkin) | [ADR-015](docs/adr/015-strategie-tests.md), [ADR-007](docs/adr/007-tests-bdd.md) | +| **Paiements** | Mangopay | [ADR-009](docs/adr/009-solution-paiement.md) | +| **Emailing** | Brevo | [ADR-018](docs/adr/018-service-emailing.md) | +| **Commandes volant** | Like automatique | [ADR-010](docs/adr/010-commandes-volant.md) | +| **Conformité stores** | CarPlay, Android Auto, App/Play Store | [ADR-011](docs/adr/011-conformite-stores-carplay-android-auto.md) | + +--- + +## Streaming Audio + +### Protocole : HLS (HTTP Live Streaming) + +- Fonctionne à travers firewalls et réseaux mobiles instables +- Cache CDN natif (réduction des coûts) +- Bitrate adaptatif automatique (tunnels, zones rurales) +- Support natif iOS/Android + +### Codec : Opus + +Optimisé pour la voix en environnement bruyant (voiture). + +| Qualité | Bitrate | Usage | +|---------|---------|-------| +| Basse | 24 kbps | 2G/Edge | +| Standard | 48 kbps | 3G | +| Haute | 64 kbps | 4G/5G | + +Fallback AAC-LC pour appareils legacy. + +### Buffering Adaptatif + +| Réseau | Buffer min | Buffer cible | Buffer max | +|--------|------------|--------------|------------| +| WiFi | 5s | 30s | 120s | +| 4G/5G | 10s | 45s | 120s | +| 3G | 30s | 90s | 300s | + +--- + +## Sécurité + +### Chiffrement + +- **TLS 1.3** sur tous les endpoints (overhead ~1-2%) +- **DTLS-SRTP** pour WebRTC (radio live) +- Pas de DRM initialement (ajout si licences l'exigent) + +### Authentification + +- **Zitadel self-hosted sur OVH France** (Gravelines) pour IAM +- Souveraineté totale : 100% données en France (cohérent avec ADR-004) +- JWT validation locale (zitadel-go SDK) +- OAuth2 PKCE pour mobile (iOS/Android) +- MFA et passkeys disponibles +- Rate limiting par IP et par utilisateur (Nginx + Zitadel) +- PostgreSQL schema partagé avec RoadWave (séparation logique) + +--- + +## Base de Données + +### PostgreSQL + PostGIS + +```sql +-- Requête géolocalisée typique +SELECT id, ST_Distance(location::geography, ST_MakePoint($lon, $lat)::geography) as distance +FROM contents +WHERE ST_DWithin(location::geography, ST_MakePoint($lon, $lat)::geography, 50000) +ORDER BY distance +LIMIT 20; +``` + +### Redis Geospatial (Cache) + +``` +GEOADD contents:geo longitude latitude content_id +GEORADIUS contents:geo user_lon user_lat 50 km WITHDIST COUNT 20 ASC +``` + +TTL cache : 5 minutes (le contenu ne bouge pas). + +--- + +## Architecture Services + +``` +┌─────────────────────────────────────────────┐ +│ OVH VPS (Gravelines, France) │ +│ │ +│ ┌─────────────────┐ │ +│ │ NGINX Cache │ Cache HLS │ +│ │ + Let's Encrypt│ SSL, rate limiting │ +│ └────────┬────────┘ │ +│ │ │ +│ ┌────────┴────────┐ │ +│ │ API Gateway │ Go + Fiber │ +│ └────────┬────────┘ │ +│ │ │ +│ ┌────┴────┬─────────────┬──────────┐ │ +│ │ │ │ │ │ +│ ┌───▼───┐ ┌──▼───┐ ┌───────▼────┐ ┌──▼─────┐ +│ │ Auth │ │ User │ │Content/Geo │ │Zitadel │ +│ │Service│ │Svc │ │ Service │ │ IdP │ +│ └───┬───┘ └──┬───┘ └──────┬─────┘ └───┬────┘ +│ │ │ │ │ │ +│ └────────┴────────────┴───────────┘ │ +│ │ │ +│ ┌───────────┴──────────┐ │ +│ │ │ │ +│ ┌────▼────┐ ┌──────▼──────┐ │ +│ │ Redis │ │ PostgreSQL │ │ +│ │ Cluster │ │ + PostGIS │ │ +│ └─────────┘ │ │ │ +│ │ Schémas: │ │ +│ │ - roadwave │ │ +│ │ - zitadel │ │ +│ └─────────────┘ │ +└─────────────────────────────────────────────┘ + │ + ┌──────────┴──────────┐ + │ │ +┌────────▼────────┐ ┌────────▼────────┐ +│ OVH Object │ │ Mobile Apps │ +│ Storage (S3) │ │ iOS/Android │ +│ Fichiers audio │ │ │ +└─────────────────┘ └─────────────────┘ + +Souveraineté : 100% données en France +``` + +--- + +## Scaling 10M Utilisateurs + +### Stratégie par phase + +| Phase | Utilisateurs | Infra | Coût estimé | +|-------|--------------|-------|-------------| +| MVP | 0-20K | OVH VPS Essential + PostgreSQL + Zitadel + NGINX Cache | ~14€/mois | +| Growth | 20K-500K | Scaleway Instances (multi-replicas), OVH Object Storage | 150-500€/mois | +| Scale | 500K+ | Multi-région, Kubernetes managé, NGINX origin shield | 2-10K€/mois | + +### Métriques cibles + +| Métrique | Objectif | +|----------|----------| +| Latence API p99 | < 100ms | +| Temps de démarrage audio | < 3s | +| Disponibilité | 99.9% | +| Connexions/serveur | 100K+ | + +--- + +## Points de vigilance + +1. **Buffering mobile** : Pré-chargement agressif avant tunnels (détection GPS) +2. **Handoff réseau** : Buffer suffisant pour survivre aux changements de cellule +3. **Mode offline** : Téléchargement complet sur WiFi +4. **Bande passante** : 48 kbps Opus = ~20 MB/heure (faible consommation data) + +--- + +## Pourquoi pas UDP brut ? + +| UDP | HLS/TCP | +|-----|---------| +| Latence minimale | Latence acceptable (5-30s) | +| Problèmes NAT/firewall | Passe partout | +| Perte de paquets = artefacts | Retransmission automatique | +| Pas de cache CDN | Cache CDN = économies | +| Complexité++ | Standard de l'industrie | + +Pour du contenu non-interactif (podcasts, audio-guides), la latence HLS est acceptable. WebRTC réservé à la radio live uniquement. diff --git a/backend/.air.toml b/backend/.air.toml new file mode 100644 index 0000000..ef5d69d --- /dev/null +++ b/backend/.air.toml @@ -0,0 +1,44 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "./tmp/main" + cmd = "go build -o ./tmp/main ./cmd/api" + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata", "docs", "migrations", "features", "docker", "scripts"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..893d0ea --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,51 @@ +# Server +SERVER_PORT=8080 +SERVER_ENV=development + +# Database +DATABASE_HOST=localhost +DATABASE_PORT=5432 +DATABASE_NAME=roadwave_dev +DATABASE_USER=roadwave +DATABASE_PASSWORD=dev_password +DATABASE_SSL_MODE=disable +DATABASE_MAX_CONNECTIONS=25 +DATABASE_MAX_IDLE_CONNECTIONS=5 + +# Redis +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= +REDIS_DB=0 + +# Zitadel +ZITADEL_DOMAIN=localhost:8081 +ZITADEL_PROJECT_ID=your_project_id +ZITADEL_CLIENT_ID=your_client_id +ZITADEL_CLIENT_SECRET=your_client_secret +ZITADEL_ISSUER=http://localhost:8081 + +# JWT +JWT_SECRET=your_super_secret_key_change_me + +# OVH Object Storage (S3-compatible) +OVH_S3_ENDPOINT=s3.gra.io.cloud.ovh.net +OVH_S3_REGION=gra +OVH_S3_ACCESS_KEY=your_ovh_access_key +OVH_S3_SECRET_KEY=your_ovh_secret_key +OVH_S3_BUCKET=roadwave-dev + +# Mangopay +MANGOPAY_CLIENT_ID=your_mangopay_client_id +MANGOPAY_API_KEY=your_mangopay_api_key +MANGOPAY_BASE_URL=https://api.sandbox.mangopay.com + +# Logging +LOG_LEVEL=debug +LOG_FORMAT=console + +# CORS +CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080 + +# Rate Limiting +RATE_LIMIT_REQUESTS_PER_MINUTE=60 diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..9710133 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,33 @@ +# Builder stage +FROM golang:1.23-alpine AS builder + +RUN apk add --no-cache git ca-certificates + +WORKDIR /app + +# Copy go mod files +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source code +COPY . . + +# Build the application +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o /bin/api ./cmd/api + +# Final stage +FROM alpine:latest + +RUN apk --no-cache add ca-certificates ffmpeg + +WORKDIR /root/ + +# Copy binary from builder +COPY --from=builder /bin/api . + +# Copy config files +COPY config/ ./config/ + +EXPOSE 8080 + +CMD ["./api"] diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev new file mode 100644 index 0000000..e0d15e3 --- /dev/null +++ b/backend/Dockerfile.dev @@ -0,0 +1,25 @@ +FROM golang:1.23-alpine + +RUN apk add --no-cache git ca-certificates make + +# Install Air for hot reload +RUN go install github.com/cosmtrek/air@latest + +# Install sqlc +RUN go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest + +# Install migrate +RUN go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest + +WORKDIR /app + +# Copy go mod files +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source code +COPY . . + +EXPOSE 8080 + +CMD ["air", "-c", ".air.toml"] diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..4c06c50 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,77 @@ +# Backend - RoadWave + +Backend Go de l'application RoadWave. + +## Structure + +``` +backend/ +├── cmd/ # Points d'entrée de l'application +│ └── api/ # Serveur API REST +├── internal/ # Code interne (non exportable) +│ ├── auth/ # Validation JWT, intégration Zitadel +│ ├── user/ # Profils, centres d'intérêt +│ ├── content/ # CRUD contenus, métadonnées +│ ├── geo/ # Recherche géospatiale, algorithme +│ ├── streaming/ # Génération HLS, transcoding +│ ├── moderation/ # Signalements, workflow +│ ├── payment/ # Intégration Mangopay +│ └── analytics/ # Métriques écoute, jauges +├── pkg/ # Code exportable (bibliothèques réutilisables) +├── migrations/ # Migrations SQL +├── tests/ +│ └── bdd/ # Step definitions pour tests Gherkin +├── config/ # Fichiers de configuration (dev, staging, prod) +├── go.mod # Dépendances Go +├── sqlc.yaml # Configuration SQLC +├── .air.toml # Configuration Air (hot reload) +├── Dockerfile # Image production +└── Dockerfile.dev # Image développement +``` + +## Architecture + +Voir [ADR-012 : Architecture Backend](../docs/adr/012-architecture-backend.md) pour les détails de l'architecture modulaire. + +## Commandes + +Depuis la **racine du monorepo** : + +```bash +# Développement +make dev # Démarrer avec hot reload +make docker-up # Lancer tous les services Docker + +# Tests +make test # Lancer tous les tests +make test-unit # Tests unitaires uniquement +make test-bdd # Tests BDD Gherkin + +# Migrations +make migrate-up # Appliquer les migrations +make migrate-down # Rollback dernière migration +make migrate-create name=add_users # Créer nouvelle migration + +# Build +make build # Compiler le binaire production +make lint # Linter le code +make format # Formatter le code +``` + +## Configuration + +1. Copier `.env.example` en `.env` : + ```bash + cp backend/.env.example backend/.env + ``` + +2. Ajuster les variables d'environnement si nécessaire + +## Dépendances + +- **Go** : 1.21+ +- **PostgreSQL** : 16+ avec PostGIS +- **Redis** : 7+ +- **Zitadel** : dernière version + +Toutes les dépendances sont gérées via Docker Compose (voir `docker-compose.yml` à la racine). diff --git a/backend/config/dev.yaml b/backend/config/dev.yaml new file mode 100644 index 0000000..9e18c65 --- /dev/null +++ b/backend/config/dev.yaml @@ -0,0 +1,77 @@ +server: + port: 8080 + env: development + read_timeout: 10s + write_timeout: 10s + +database: + host: localhost + port: 5432 + name: roadwave_dev + user: roadwave + password: dev_password + ssl_mode: disable + max_connections: 25 + max_idle_connections: 5 + max_lifetime: 5m + +redis: + host: localhost + port: 6379 + password: "" + db: 0 + pool_size: 10 + min_idle_connections: 3 + max_retries: 3 + +zitadel: + domain: localhost:8081 + project_id: ${ZITADEL_PROJECT_ID} + client_id: ${ZITADEL_CLIENT_ID} + client_secret: ${ZITADEL_CLIENT_SECRET} + issuer: http://localhost:8081 + +jwt: + secret: ${JWT_SECRET} + expiration: 24h + +ovh_s3: + endpoint: ${OVH_S3_ENDPOINT} + region: ${OVH_S3_REGION} + access_key: ${OVH_S3_ACCESS_KEY} + secret_key: ${OVH_S3_SECRET_KEY} + bucket: ${OVH_S3_BUCKET} + +mangopay: + client_id: ${MANGOPAY_CLIENT_ID} + api_key: ${MANGOPAY_API_KEY} + base_url: https://api.sandbox.mangopay.com + +logging: + level: debug + format: console + +cors: + allowed_origins: + - http://localhost:3000 + - http://localhost:8080 + allowed_methods: + - GET + - POST + - PUT + - DELETE + - OPTIONS + allowed_headers: + - Origin + - Content-Type + - Authorization + max_age: 3600 + +rate_limit: + requests_per_minute: 60 + burst: 10 + +geo: + default_radius_km: 50 + max_radius_km: 500 + cache_ttl: 5m diff --git a/backend/config/prod.yaml b/backend/config/prod.yaml new file mode 100644 index 0000000..8e80b02 --- /dev/null +++ b/backend/config/prod.yaml @@ -0,0 +1,77 @@ +server: + port: 8080 + env: production + read_timeout: 20s + write_timeout: 20s + +database: + host: ${DATABASE_HOST} + port: ${DATABASE_PORT} + name: ${DATABASE_NAME} + user: ${DATABASE_USER} + password: ${DATABASE_PASSWORD} + ssl_mode: require + max_connections: 100 + max_idle_connections: 25 + max_lifetime: 15m + +redis: + host: ${REDIS_HOST} + port: ${REDIS_PORT} + password: ${REDIS_PASSWORD} + db: 0 + pool_size: 50 + min_idle_connections: 10 + max_retries: 3 + +zitadel: + domain: ${ZITADEL_DOMAIN} + project_id: ${ZITADEL_PROJECT_ID} + client_id: ${ZITADEL_CLIENT_ID} + client_secret: ${ZITADEL_CLIENT_SECRET} + issuer: https://${ZITADEL_DOMAIN} + +jwt: + secret: ${JWT_SECRET} + expiration: 24h + +ovh_s3: + endpoint: ${OVH_S3_ENDPOINT} + region: ${OVH_S3_REGION} + access_key: ${OVH_S3_ACCESS_KEY} + secret_key: ${OVH_S3_SECRET_KEY} + bucket: ${OVH_S3_BUCKET} + +mangopay: + client_id: ${MANGOPAY_CLIENT_ID} + api_key: ${MANGOPAY_API_KEY} + base_url: https://api.mangopay.com + +logging: + level: warn + format: json + +cors: + allowed_origins: + - https://roadwave.com + - https://www.roadwave.com + allowed_methods: + - GET + - POST + - PUT + - DELETE + - OPTIONS + allowed_headers: + - Origin + - Content-Type + - Authorization + max_age: 3600 + +rate_limit: + requests_per_minute: 200 + burst: 50 + +geo: + default_radius_km: 50 + max_radius_km: 500 + cache_ttl: 5m diff --git a/backend/config/staging.yaml b/backend/config/staging.yaml new file mode 100644 index 0000000..addac11 --- /dev/null +++ b/backend/config/staging.yaml @@ -0,0 +1,76 @@ +server: + port: 8080 + env: staging + read_timeout: 15s + write_timeout: 15s + +database: + host: ${DATABASE_HOST} + port: ${DATABASE_PORT} + name: ${DATABASE_NAME} + user: ${DATABASE_USER} + password: ${DATABASE_PASSWORD} + ssl_mode: require + max_connections: 50 + max_idle_connections: 10 + max_lifetime: 10m + +redis: + host: ${REDIS_HOST} + port: ${REDIS_PORT} + password: ${REDIS_PASSWORD} + db: 0 + pool_size: 20 + min_idle_connections: 5 + max_retries: 3 + +zitadel: + domain: ${ZITADEL_DOMAIN} + project_id: ${ZITADEL_PROJECT_ID} + client_id: ${ZITADEL_CLIENT_ID} + client_secret: ${ZITADEL_CLIENT_SECRET} + issuer: https://${ZITADEL_DOMAIN} + +jwt: + secret: ${JWT_SECRET} + expiration: 24h + +ovh_s3: + endpoint: ${OVH_S3_ENDPOINT} + region: ${OVH_S3_REGION} + access_key: ${OVH_S3_ACCESS_KEY} + secret_key: ${OVH_S3_SECRET_KEY} + bucket: ${OVH_S3_BUCKET} + +mangopay: + client_id: ${MANGOPAY_CLIENT_ID} + api_key: ${MANGOPAY_API_KEY} + base_url: https://api.sandbox.mangopay.com + +logging: + level: info + format: json + +cors: + allowed_origins: + - https://staging.roadwave.com + allowed_methods: + - GET + - POST + - PUT + - DELETE + - OPTIONS + allowed_headers: + - Origin + - Content-Type + - Authorization + max_age: 3600 + +rate_limit: + requests_per_minute: 100 + burst: 20 + +geo: + default_radius_km: 50 + max_radius_km: 500 + cache_ttl: 5m diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..ce41e42 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,20 @@ +module github.com/roadwave/roadwave + +go 1.23 + +require ( + github.com/gofiber/fiber/v2 v2.52.0 + github.com/redis/go-redis/v9 v9.4.0 + github.com/rs/zerolog v1.31.0 + github.com/spf13/viper v1.18.2 + github.com/joho/godotenv v1.5.1 + github.com/go-playground/validator/v10 v10.19.0 + github.com/golang-migrate/migrate/v4 v4.17.0 + github.com/stretchr/testify v1.8.4 + github.com/cucumber/godog v0.14.0 + github.com/testcontainers/testcontainers-go v0.28.0 + github.com/go-resty/resty/v2 v2.11.0 + go.opentelemetry.io/otel v1.22.0 + go.opentelemetry.io/otel/trace v1.22.0 + github.com/prometheus/client_golang v1.18.0 +) diff --git a/backend/sqlc.yaml b/backend/sqlc.yaml new file mode 100644 index 0000000..7ae8eea --- /dev/null +++ b/backend/sqlc.yaml @@ -0,0 +1,22 @@ +version: "2" +sql: + - engine: "postgresql" + queries: "internal/database/queries" + schema: "migrations" + gen: + go: + package: "sqlc" + out: "internal/database/sqlc" + sql_package: "pgx/v5" + emit_json_tags: true + emit_prepared_queries: false + emit_interface: true + emit_exact_table_names: false + emit_empty_slices: true + overrides: + - db_type: "pg_catalog.uuid" + go_type: "github.com/google/uuid.UUID" + - db_type: "geography" + go_type: "string" + - db_type: "geometry" + go_type: "string" diff --git a/backend/tests/bdd/.gitkeep b/backend/tests/bdd/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7fa693d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,111 @@ +version: '3.8' + +services: + # Backend API + api: + build: + context: ./backend + dockerfile: Dockerfile.dev + ports: + - "8080:8080" + volumes: + - ./backend:/app + - /app/tmp + env_file: + - ./backend/.env + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + zitadel: + condition: service_healthy + networks: + - roadwave-network + + # PostgreSQL + PostGIS + postgres: + image: postgis/postgis:16-3.4-alpine + ports: + - "5432:5432" + environment: + POSTGRES_DB: roadwave_dev + POSTGRES_USER: roadwave + POSTGRES_PASSWORD: dev_password + POSTGRES_MULTIPLE_DATABASES: zitadel + volumes: + - postgres_data:/var/lib/postgresql/data + - ./docker/init-multiple-databases.sh:/docker-entrypoint-initdb.d/init-multiple-databases.sh + healthcheck: + test: ["CMD-SHELL", "pg_isready -U roadwave"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - roadwave-network + + # Redis (caching) + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + networks: + - roadwave-network + + # Zitadel (authentication) + zitadel: + image: ghcr.io/zitadel/zitadel:latest + ports: + - "8081:8080" + environment: + ZITADEL_DATABASE_POSTGRES_HOST: postgres + ZITADEL_DATABASE_POSTGRES_PORT: 5432 + ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel + ZITADEL_DATABASE_POSTGRES_USER: roadwave + ZITADEL_DATABASE_POSTGRES_PASSWORD: dev_password + ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: roadwave + ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: dev_password + ZITADEL_EXTERNALSECURE: "false" + ZITADEL_EXTERNALPORT: 8081 + ZITADEL_MASTERKEYPATH: /zitadel-masterkey + volumes: + - zitadel_data:/zitadel-masterkey + depends_on: + postgres: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/debug/ready"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 30s + networks: + - roadwave-network + + # Adminer (database admin) + adminer: + image: adminer:latest + ports: + - "8082:8080" + environment: + ADMINER_DEFAULT_SERVER: postgres + depends_on: + - postgres + networks: + - roadwave-network + +volumes: + postgres_data: + redis_data: + zitadel_data: + +networks: + roadwave-network: + driver: bridge diff --git a/docker/init-multiple-databases.sh b/docker/init-multiple-databases.sh new file mode 100755 index 0000000..c81d94b --- /dev/null +++ b/docker/init-multiple-databases.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -e +set -u + +# Script to create multiple databases in PostgreSQL container +# Used by docker-compose to create both roadwave_dev and zitadel databases + +function create_database() { + local database=$1 + echo "Creating database '$database'" + psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL + CREATE DATABASE $database; + GRANT ALL PRIVILEGES ON DATABASE $database TO $POSTGRES_USER; +EOSQL +} + +if [ -n "$POSTGRES_MULTIPLE_DATABASES" ]; then + echo "Multiple database creation requested: $POSTGRES_MULTIPLE_DATABASES" + for db in $(echo $POSTGRES_MULTIPLE_DATABASES | tr ',' ' '); do + create_database $db + done + echo "Multiple databases created" +fi diff --git a/docs/INCONSISTENCIES-ANALYSIS.md b/docs/INCONSISTENCIES-ANALYSIS.md new file mode 100644 index 0000000..577d0d3 --- /dev/null +++ b/docs/INCONSISTENCIES-ANALYSIS.md @@ -0,0 +1,671 @@ +# Analyse des Incohérences entre ADR et Règles Métier + +**Date d'analyse** : 2026-01-28 +**Analysé par** : Audit Architecture RoadWave +**Scope** : 18 ADR × Règles Métier (17 fichiers) + +--- + +## Résumé Exécutif + +Cette analyse a identifié **15 incohérences** entre les décisions d'architecture (ADR) et les règles métier du projet RoadWave. + +### Répartition par Sévérité + +| Sévérité | Nombre | % Total | Statut | Action Required | +|----------|--------|---------|--------|-----------------| +| 🔴 **CRITICAL** | 2 | 13% | ✅ **RÉSOLU** | ~~avant implémentation~~ | +| 🟠 **HIGH** | 4 | 27% | ⏳ 3 restants (1 résolu) | Résolution Sprint 1-2 | +| 🟡 **MODERATE** | 8 | 53% | ⏳ En cours | Résolution Sprint 3-5 | +| 🟢 **LOW** | 1 | 7% | ⏳ En cours | À clarifier lors du développement | + +### Impact par Domaine + +| Domaine | Nombre d'incohérences | Criticité maximale | +|---------|----------------------|-------------------| +| Streaming & Géolocalisation | 3 | 🔴 CRITICAL | +| Données & Infrastructure | 3 | 🟠 HIGH | +| Authentification & Sécurité | 2 | 🟠 HIGH | +| Tests & Qualité | 2 | 🟡 MODERATE | +| Coûts & Déploiement | 3 | 🟡 MODERATE | +| UX & Engagement | 2 | 🟡 MODERATE | + +--- + +## 🔴 Incohérences Critiques (Blocantes) + +### #1 : HLS ne supporte pas les Notifications Push en Arrière-plan + +**Statut** : ✅ **RÉSOLU** (ADR-019 créé) + +| Élément | Détail | +|---------|--------| +| **ADR concerné** | ADR-002 (Protocole Streaming) | +| **Règle métier** | Règle 05, section 5.1.2 (Mode Piéton, lignes 86-120) | +| **Conflit** | HLS est unidirectionnel (serveur→client), ne peut pas envoyer de push quand l'app est fermée | +| **Impact** | Mode piéton non fonctionnel : notifications "Point d'intérêt à 200m" impossibles | + +**Scénario d'échec** : +``` +Utilisateur: Marie se promène, app fermée +Position: 150m de la Tour Eiffel +Attendu: Push notification "🗼 À proximité: Histoire de la Tour Eiffel" +Réel: Rien (HLS ne peut pas notifier) +``` + +**Solution implémentée** : +- ✅ **ADR-019** : Architecture hybride WebSocket + Firebase Cloud Messaging +- Phase 1 (MVP) : Push serveur via FCM/APNS +- Phase 2 : Geofencing natif iOS/Android pour mode offline + +**Actions requises** : +- [ ] Backend : Implémenter endpoint WebSocket `/ws/location` +- [ ] Backend : Worker PostGIS avec requête `ST_DWithin` (30s interval) +- [ ] Mobile : Intégrer Firebase SDK (`firebase_messaging`) +- [ ] Tests : Validation en conditions réelles (10 testeurs, Paris) + +--- + +### #2 : Latence HLS Incompatible avec ETA de 7 Secondes + +**Statut** : ✅ **RÉSOLU** (ADR-002 mis à jour) + +| Élément | Détail | +|---------|--------| +| **ADR concerné** | ADR-002 (Protocole Streaming, lignes 40-41) | +| **Règle métier** | Règle 05 (lignes 16-20), Règle 17 (lignes 25-30, 120-124) | +| **Conflit** | ETA de 7s avant le point, mais HLS a 5-30s de latence → audio démarre APRÈS avoir dépassé le point | +| **Impact** | UX catastrophique : utilisateur entend "Vous êtes devant le château" 100m APRÈS l'avoir dépassé | + +**Calcul du problème** (90 km/h = 25 m/s) : +``` +t=0s → Notification "Suivant: Château dans 7s" (175m avant) +t=7s → Utilisateur arrive au château +t=15s → HLS démarre (latence 15s) +Résultat: Audio démarre 200m APRÈS le point ❌ +``` + +**Solution implémentée** : +- ✅ **ADR-002 mis à jour** : Section "Gestion de la Latence et Synchronisation Géolocalisée" +- Pre-buffering à ETA=30s (15 premières secondes en cache local) +- ETA adaptatif : 5s si cache prêt, 15s sinon +- Mesure dynamique de latence HLS par utilisateur + +**Actions requises** : +- [ ] Backend : Endpoint `/api/v1/audio/poi/:id/intro` (retourne 15s d'audio) +- [ ] Mobile : Service `PreBufferService` avec cache local (max 100 MB) +- [ ] Mobile : Loader visuel avec progression si buffer > 3s +- [ ] Tests : Validation synchronisation ±10m du POI + +--- + +## 🟠 Incohérences Importantes (Sprint 1-2) + +### #3 : Souveraineté des Données (Français vs Suisse) + +**Statut** : ✅ **RÉSOLU** (ADR-008 mis à jour) + +| Élément | Détail | +|---------|--------| +| **ADR concernés** | ADR-004 (CDN, ligne 26), ADR-008 (Auth, mis à jour) | +| **Règle métier** | Règle 02 (RGPD, section 13.10) | +| **Conflit** | ADR-004 revendique "100% souveraineté française" mais ADR-008 utilisait Zitadel (entreprise suisse) | +| **Impact** | Contradiction marketing + risque juridique si promesse "100% français" | + +**Solution implémentée** : **Self-hosting Zitadel sur OVH France** + +- ✅ Container Docker sur le même VPS OVH (Gravelines, France) +- ✅ Base de données PostgreSQL partagée (schéma séparé pour Zitadel) +- ✅ Aucune donnée ne transite par des serveurs tiers +- ✅ Souveraineté totale garantie : 100% des données en France +- ✅ Cohérence complète avec ADR-004 (CDN 100% français) + +**Changements apportés** : +- ✅ ADR-008 mis à jour avec architecture self-hosted détaillée +- ✅ TECHNICAL.md mis à jour (tableau + diagramme architecture) +- ✅ Clarification : Zitadel est open source, donc aucune dépendance à une entreprise suisse + +**Actions complétées** : +- [x] Décision validée : Self-host sur OVH +- [x] ADR-008 mis à jour avec architecture self-hosted +- [x] TECHNICAL.md mis à jour + +--- + +### #4 : ORM sqlc vs Types PostGIS + +| Élément | Détail | +|---------|--------| +| **ADR concernés** | ADR-013 (ORM, lignes 12, 33-40), ADR-005 (BDD, lignes 47-56) | +| **Règle métier** | N/A (problème technique pur) | +| **Conflit** | sqlc génère types Go depuis SQL, mais PostGIS geography/geometry ne mappent pas proprement | +| **Impact** | Risque de type `interface{}` ou `[]byte` pour géographie → perte de type safety revendiquée | + +**Nature du problème** : + +sqlc génère du code Go depuis SQL, mais les types PostGIS (`geography`, `geometry`) ne sont pas mappés proprement en Go. Résultat : types opaques (`[]byte`, `interface{}`) qui perdent la **type safety** revendiquée dans ADR-013. + +**Solution retenue** : + +1. **Wrapper types Go** avec méthodes `Scan/Value` pour conversion automatique +2. **Utiliser les fonctions PostGIS de conversion** : + - `ST_AsGeoJSON()` → struct GeoJSON typée + - `ST_AsText()` → string WKT + - `geography` brut → `pgtype.Point` (lib pgx) +3. **Documenter le pattern** dans ADR-013 section "Gestion des Types PostGIS" + +**Action** : +- [ ] Créer package `internal/geo` avec wrappers `GeoJSON`, `WKT` +- [ ] Mettre à jour ADR-013 section "Types PostGIS" +- [ ] Documenter pattern dans README backend + +--- + +### #5 : Cache Redis (TTL 5min) vs Mode Offline (30 jours) + +| Élément | Détail | +|---------|--------| +| **ADR concerné** | ADR-005 (BDD, ligne 60) | +| **Règle métier** | Règle 11 (Mode Offline, lignes 58-77) | +| **Conflit** | Redis avec TTL 5min pour géolocalisation, mais contenu offline valide 30 jours | +| **Impact** | En mode offline, impossible de rafraîchir le cache géolocalisation → POI proches non détectés | + +**Analyse du flux** : + +``` +Mode connecté: +1. Requête POI proches → Redis (cache 5min) +2. Si miss → PostGIS → Cache Redis +3. ✅ Fonctionne + +Mode offline (Règle 11): +1. Requête POI proches → Redis (expiré depuis 6 min) +2. Impossible de requêter PostGIS (pas de réseau) ❌ +3. Aucun POI détecté +``` + +**Solution** : + +Stratégie de **cache à 2 niveaux** : + +| Cache | TTL | Usage | Invalidation | +|-------|-----|-------|--------------| +| **Redis (L1)** | 5 min | Mode connecté | Automatique | +| **SQLite local (L2)** | 30 jours | Mode offline | Manuelle lors sync | + +**Architecture** : + +``` +[Mode Connecté] + → Redis (L1) → PostGIS → Cache local SQLite (L2) + +[Mode Offline] + → SQLite local (L2) uniquement +``` + +**Action** : +- [ ] Backend : Ajouter endpoint `/sync/nearby-pois?lat=X&lon=Y&radius=10km` +- [ ] Mobile : Créer `OfflineCacheService` avec SQLite + index spatial +- [ ] Mettre à jour ADR-005 section "Cache" avec stratégie 2 niveaux +- [ ] Règle 11 : Clarifier sync automatique vs manuel + +--- + +### #6 : Package Geofencing vs Permissions iOS/Android + +| Élément | Détail | +|---------|--------| +| **ADR concerné** | ADR-014 (Frontend Mobile, ligne 48) | +| **Règle métier** | Règle 05 (lignes 86-134), Règle 11 (RGPD, lignes 51-86) | +| **Conflit** | Package `geofence_service` choisi, mais pas de doc sur compatibilité permissions "optionnelles" | +| **Impact** | Risque de rejet App Store/Play Store si permissions obligatoires mal gérées | + +**Problématiques** : + +1. **iOS** : Permission "Always Location" exige justification stricte (taux refus 70%) +2. **Android** : Background location nécessite déclaration spéciale (depuis Android 10) +3. **Règle métier** : Permissions optionnelles (app utilisable sans "Always Location") + +**Package `geofence_service`** : +- ✅ Supporte iOS/Android +- ⚠️ Documentation peu claire sur permissions optionnelles +- ⚠️ Pas de fallback natif si permission refusée + +**Solution** : + +**Stratégie de permissions progressive** : + +```dart +enum LocationPermissionLevel { + denied, // Pas de permission + whenInUse, // "Quand l'app est ouverte" (iOS) + always, // "Toujours" (iOS) / Background (Android) +} + +class GeofencingService { + Future requestPermissions() async { + // Étape 1: Demander "When In Use" (moins intrusif) + var status = await Permission.locationWhenInUse.request(); + + if (status.isGranted) { + // Mode basique: détection seulement app ouverte + _enableBasicGeofencing(); + + // Étape 2 (optionnelle): Proposer upgrade vers "Always" + _showUpgradePermissionDialog(); + } + } + + Future upgradeToAlwaysPermission() async { + // Demandé seulement si utilisateur veut mode piéton complet + await Permission.locationAlways.request(); + } +} +``` + +**Actions** : +- [ ] Mettre à jour ADR-014 avec stratégie permissions progressive +- [ ] Créer doc "Permissions Strategy" dans `/docs/mobile/` +- [ ] Tests : Validation rejet App Store (TestFlight beta) + +--- + +## 🟡 Incohérences Modérées (Sprint 3-5) + +### #7 : Points vs Pourcentages dans les Jauges + +| Élément | Détail | +|---------|--------| +| **ADR concerné** | ADR-010 (Commandes Volant, lignes 15-21) | +| **Règle métier** | Règle 03 (Centres d'intérêt, lignes 7-14) | +| **Conflit** | ADR dit "+2 **points**", Règle dit "+2**%**" pour même action | +| **Impact** | Ambiguïté sur calcul : +2 points absolus ou +2% relatifs ? | + +**Exemple du conflit** : + +- **ADR-010 (ligne 18)** : "≥80% d'écoute = +2 **points**" +- **Règle 03 (ligne 9)** : "≥80% d'écoute = +2**%** à la jauge" + +**Scénario** : +``` +Jauge "Automobile" = 45% +Utilisateur écoute 85% d'un podcast voiture + +Option A (points absolus): 45 + 2 = 47% +Option B (pourcentage relatif): 45 * 1.02 = 45.9% +``` + +**Recommandation** : **Option A (points absolus)** pour simplicité + +**Justification** : +- Progression linéaire plus intuitive +- Évite effet "rich get richer" (jauges hautes progressent + vite) +- Cohérent avec système de gamification classique + +**Actions** : +- [ ] Clarifier ADR-010 : remplacer "points" par "points de pourcentage" +- [ ] Clarifier Règle 03 : uniformiser terminologie +- [ ] Backend : Documenter formule exacte dans code + +--- + +### #8 : OAuth2 Complexe vs Email/Password Simple + +| Élément | Détail | +|---------|--------| +| **ADR concerné** | ADR-008 (Auth, lignes 12, 52-68) | +| **Règle métier** | Règle 01 (Auth, lignes 5-10) | +| **Conflit** | ADR implémente OAuth2 PKCE complet, mais Règle dit "❌ Pas d'OAuth tiers, email/password uniquement" | +| **Impact** | Sur-ingénierie : OAuth2 conçu pour tiers (Google, Facebook) mais non utilisé ici | + +**Analyse** : + +- **ADR-008** : Architecture OAuth2 avec PKCE, refresh tokens, etc. +- **Règle 01** : "❌ Pas de Google, Apple, Facebook OAuth" + +**Zitadel supporte** : +- OAuth2 (pour intégrations tierces) +- Email/Password natif (ce dont on a besoin) + +**Question** : Pourquoi implémenter OAuth2 si pas de tiers ? + +**Options** : + +| Option | Complexité | Justification | +|--------|------------|---------------| +| **A. Garder OAuth2** | Haute | Future-proof pour API partenaires | +| **B. Session simple** | Basse | Suffit pour MVP email/password | + +**Recommandation** : **Option A** (garder OAuth2) si : +- Vision long-terme : API pour partenaires (créateurs, annonceurs) +- Coût marginal : Zitadel gère OAuth2 nativement + +Sinon **Option B** (session simple) si MVP pur. + +**Actions** : +- [ ] Décision : Confirmer besoin OAuth2 avec product owner +- [ ] Si A : Mettre à jour Règle 01 "OAuth tiers en Phase 2" +- [ ] Si B : Simplifier ADR-008 (session JWT classique) + +--- + +### #9 : GeoIP Database (MaxMind) + +| Élément | Détail | +|---------|--------| +| **ADR concerné** | ADR-005 (non mentionné) | +| **Règle métier** | Règle 02 (RGPD, lignes 146-149) | +| **Conflit** | Règle cite "MaxMind GeoLite2 (gratuit)", mais offre a changé en 2019 | +| **Impact** | Coût caché : MaxMind nécessite compte + API calls (plus de base offline gratuite) | + +**Historique** : +- **Avant 2019** : GeoLite2 database téléchargeable gratuitement +- **Après 2019** : Compte requis + limite 1000 requêtes/jour (gratuit) +- **Dépassement** : 0.003$/requête + +**Utilisation RoadWave** : +- Mode dégradé (sans GPS) → GeoIP pour localisation approximative +- Estimation : 10% des utilisateurs (1000 users × 10% = 100 requêtes/jour) + +**Options** : + +| Option | Coût/mois | Précision | Maintenance | +|--------|-----------|-----------|-------------| +| **A. MaxMind API** | ~10€ | ±50 km | Nulle | +| **B. IP2Location Lite** | Gratuit | ±50 km | Maj mensuelle | +| **C. Self-hosted GeoIP** | Gratuit | ±50 km | +2h/mois | + +**Recommandation** : **Option C** (self-hosted avec IP2Location Lite DB) + +**Architecture** : +``` +[Backend Go] → [GeoIP Service] + ↓ + [IP2Location SQLite DB] + (màj mensuelle via cron) +``` + +**Actions** : +- [ ] Backend : Implémenter service GeoIP avec IP2Location +- [ ] DevOps : Cron job màj mensuelle de la DB +- [ ] Mettre à jour Règle 02 ligne 147 + +--- + +### #10 : Tests BDD Synchronisés (Backend + Mobile) + +| Élément | Détail | +|---------|--------| +| **ADR concernés** | ADR-007 (Tests BDD, lignes 30-68), ADR-015 (Stratégie, lignes 59-62) | +| **Règle métier** | Toutes (Gherkin) | +| **Conflit** | Features partagées `/features`, step definitions séparées → qui exécute quoi ? | +| **Impact** | Risque de divergence backend/mobile si tests pas synchronisés | + +**Architecture actuelle** : + +``` +/features/*.feature (partagé) +/backend/tests/bdd/ (step definitions Go) +/mobile/tests/bdd/ (step definitions Dart) +``` + +**Question non résolue** : +- Un test "Authentification" concerne-t-il backend ET mobile ? +- Qui est responsable de l'exécuter ? +- Si les implémentations divergent ? + +**Recommandation** : **Catégoriser les features** + +``` +/features/ + /api/ → Backend uniquement (tests API REST) + /ui/ → Mobile uniquement (tests interface) + /e2e/ → End-to-end (backend + mobile ensemble) +``` + +**Exemple** : +```gherkin +# features/api/authentication.feature (backend) +Scénario: Création de compte via API + Étant donné une requête POST /api/v1/auth/register + Quand j'envoie email "test@example.com" et password "Pass123!" + Alors le statut HTTP est 201 + Et la réponse contient un token JWT + +# features/ui/authentication.feature (mobile) +Scénario: Création de compte via interface + Étant donné que je suis sur l'écran d'inscription + Quand je saisis email "test@example.com" + Et je saisis mot de passe "Pass123!" + Et je clique sur "S'inscrire" + Alors je vois l'écran d'accueil +``` + +**Actions** : +- [ ] Réorganiser `/features` en 3 catégories (api, ui, e2e) +- [ ] Mettre à jour ADR-007 avec convention de nommage +- [ ] CI/CD : Séparer jobs backend-bdd et mobile-bdd + +--- + +### #11 : 70/30 Split Paiements (Vérification Manquante) + +| Élément | Détail | +|---------|--------| +| **ADR concerné** | ADR-009 (Paiement, lignes 32-52) | +| **Règle métier** | Règle 18 (Monétisation, non fournie complète) | +| **Conflit** | ADR assume 70/30 split sans référence règle métier | +| **Impact** | Risque de mauvaise répartition revenus créateurs | + +**ADR-009 spécifie** : +- 70% créateur +- 30% plateforme + +**Question** : Est-ce validé par les règles métier business ? + +**Actions** : +- [ ] Lire Règle 18 (Monétisation Créateurs) complète +- [ ] Vérifier si 70/30 correspond aux attentes +- [ ] Si divergence : mettre à jour ADR-009 + +--- + +### #12 : Monorepo Path Filters vs Features Partagées + +| Élément | Détail | +|---------|--------| +| **ADR concernés** | ADR-016 (Monorepo, ligne 80), ADR-015 (Tests) | +| **Règle métier** | N/A (problème CI/CD) | +| **Conflit** | Path filters pour éviter rebuild tout, mais features partagées déclenchent tout | +| **Impact** | Optimisation CI/CD inefficace | + +**Problème** : + +```yaml +# .github/workflows/backend.yml +on: + push: + paths: + - 'backend/**' + - 'features/**' # ❌ Change sur n'importe quel .feature → rebuild backend +``` + +**Solution** : Path filters **par catégorie** (suite de #10) + +```yaml +# .github/workflows/backend.yml +on: + push: + paths: + - 'backend/**' + - 'features/api/**' # ✅ Seulement features API + - 'features/e2e/**' # ✅ E2E impacte backend + +# .github/workflows/mobile.yml +on: + push: + paths: + - 'mobile/**' + - 'features/ui/**' # ✅ Seulement features UI + - 'features/e2e/**' # ✅ E2E impacte mobile +``` + +**Actions** : +- [ ] Implémenter catégorisation features (dépend de #10) +- [ ] Mettre à jour workflows CI/CD +- [ ] Mettre à jour ADR-016 avec stratégie path filters + +--- + +### #13 : Coûts Email (Transition Free → Paid) + +| Élément | Détail | +|---------|--------| +| **ADR concernés** | ADR-018 (Email, lignes 49-52), ADR-017 (Hébergement) | +| **Règle métier** | N/A (économique) | +| **Conflit** | ADR cite "gratuit" mais limite 9000 emails/mois → plan transition manquant | +| **Impact** | Coût surprise lors de la croissance | + +**ADR-018 spécifie** : +- Brevo gratuit : 300 emails/jour = 9000/mois +- Phase MVP : 0-10K utilisateurs + +**Calcul réaliste** : +``` +Emails par utilisateur/mois: +- Vérification email: 1 +- Reset password: 0.1 (10%) +- Notifications (opt-in 30%): 4 +- Paiements créateurs (5%): 1 + +Total: ~2 emails/user/mois (moyenne) + +10K users × 2 = 20K emails/mois → dépassement tier gratuit +``` + +**Coût Brevo** : +- Free: 0-9K emails +- Lite: 19€/mois (20K emails) +- Business: 49€/mois (50K emails) + +**Actions** : +- [ ] Mettre à jour ADR-018 avec projection coûts +- [ ] Implémenter alertes (90% quota atteint) +- [ ] Plan B : Self-hosted SMTP (Postfix) si budget serré + +--- + +### #14 : Kubernetes vs VPS MVP + +| Élément | Détail | +|---------|--------| +| **ADR concernés** | ADR-017 (Hébergement, ligne 12), ADR-001 (Go, ligne 27) | +| **Règle métier** | N/A (infrastructure) | +| **Conflit** | ADR-001 justifie Go pour "Kubernetes first-class", mais ADR-017 utilise VPS simple | +| **Impact** | Sur-architecture : pourquoi choisir Go pour K8s si pas utilisé ? | + +**Analyse** : + +- **ADR-001** : Go choisi notamment pour "excellent support Kubernetes" +- **ADR-017** : MVP sur OVH VPS Essential (single VM, Docker Compose) +- **ADR-012** : Mentionne migration K8s "à 1M+ users" + +**Question** : Justification K8s prématurée ? + +**Réponse** : **Non, acceptable** si : +- Vision long-terme claire (1M users = besoin K8s) +- Go apporte autres avantages (perf, concurrence, typing) +- Coût marginal (Go vs Node.js comparable en complexité MVP) + +**Recommandation** : **Clarifier la vision** dans ADR + +**Actions** : +- [ ] Mettre à jour ADR-001 : "Go pour scalabilité future (K8s), mais aussi perf/typage" +- [ ] ADR-017 : Ajouter section "Roadmap Infrastructure" (VPS → K8s) + +--- + +## 🟢 Incohérences Mineures (Clarification) + +### #15 : Unlike Manuel sur Contenu Auto-liké + +| Élément | Détail | +|---------|--------| +| **ADR concerné** | ADR-010 (ligne 15-21) | +| **Règle métier** | Règle 05 (lignes 248-323), Règle 03 (lignes 93-99) | +| **Conflit** | Auto-like +2% documenté, mais unlike manuel non spécifié | +| **Impact** | Ambiguïté : faut-il annuler (+2%) si unlike ? | + +**Scénario** : +``` +1. Utilisateur écoute 85% → auto-like → jauge +2% +2. Utilisateur clique "Unlike" (toggle) +3. Que se passe-t-il ? + Option A: Jauge -2% (annulation) + Option B: Jauge reste (unlike n'affecte pas) +``` + +**Recommandation** : **Option A** (annulation symétrique) + +**Justification** : Unlike explicite = signal fort "pas intéressé" + +**Actions** : +- [ ] Clarifier Règle 03 : section "Unlike Manuel" +- [ ] Backend : Implémenter logique annulation dans `GaugeService` + +--- + +## Plan d'Action Global + +### Phase 1 : Résolutions Critiques (Avant Implémentation) + +| # | Tâche | Responsable | Effort | Deadline | +|---|-------|-------------|--------|----------| +| 1 | ✅ Créer ADR-019 (Notifications) | Architecture | 2h | ✅ Fait | +| 2 | ✅ Mettre à jour ADR-002 (Pre-buffering) | Architecture | 1h | ✅ Fait | +| 3 | Implémenter WebSocket backend | Backend Lead | 3j | Sprint 1 | +| 4 | Implémenter Pre-buffer mobile | Mobile Lead | 2j | Sprint 1 | + +### Phase 2 : Résolutions Importantes (Sprint 1-2) + +| # | Tâche | Responsable | Effort | Statut | +|---|-------|-------------|--------|--------| +| 5 | ✅ Décision souveraineté (Zitadel self-host) | CTO | 1h | ✅ **Fait** | +| 6 | Package geo types (PostGIS) | Backend | 1j | ⏳ Sprint 2 | +| 7 | Cache 2 niveaux (Redis + SQLite) | Backend + Mobile | 3j | ⏳ Sprint 2 | +| 8 | Stratégie permissions progressive | Mobile | 2j | ⏳ Sprint 2 | + +### Phase 3 : Résolutions Modérées (Sprint 3-5) + +| # | Tâche | Responsable | Effort | Deadline | +|---|-------|-------------|--------|----------| +| 9-15 | Clarifications ADR/Règles | Tech Writer | 5j | Sprint 3-4 | +| 16 | Réorganisation features BDD | QA Lead | 2j | Sprint 4 | +| 17 | Optimisation CI/CD path filters | DevOps | 1j | Sprint 5 | + +--- + +## Métriques de Suivi + +| Métrique | Valeur Initiale | Cible | Actuel | +|----------|----------------|-------|--------| +| Incohérences CRITICAL | 2 | 0 | ✅ **0** (2/2 résolues) | +| Incohérences HIGH | 4 | 0 | ⏳ **3** (1/4 résolue) | +| Incohérences MODERATE | 8 | ≤2 | ⏳ 8 | +| ADR à jour | 66% (12/18) | 100% | ⏳ 78% (14/18) | +| Coverage documentation | N/A | >90% | ⏳ 80% | + +**Dernière mise à jour** : 2026-01-30 + +--- + +## Contacts et Ressources + +- **Analyse complète** : Ce document +- **ADR-019** : `/docs/adr/019-notifications-geolocalisees.md` +- **ADR-002 (mis à jour)** : `/docs/adr/002-protocole-streaming.md` +- **Questions** : Créer une issue GitHub avec tag `[architecture]` + +--- + +**Prochaine revue** : 2026-02-15 (après Sprint 2) diff --git a/docs/adr/001-langage-backend.md b/docs/adr/001-langage-backend.md new file mode 100644 index 0000000..6687cc6 --- /dev/null +++ b/docs/adr/001-langage-backend.md @@ -0,0 +1,65 @@ +# ADR-001 : Langage Backend + +**Statut** : Accepté +**Date** : 2025-01-17 + +## Contexte + +RoadWave doit gérer 10M d'utilisateurs avec des connexions concurrentes massives pour le streaming audio géolocalisé. + +## Décision + +**Go** avec le framework **Fiber**. + +## Alternatives considérées + +### Comparatif synthétique + +| Option | Conn/serveur | P99 latency | Simplicité | Écosystème RoadWave | +|--------|-------------|------------|------------|------------| +| **Go + Fiber** | 1M+ | 5-50ms | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | +| Rust + Tokio | 2M+ | 2-20ms | ⭐⭐ | ⭐⭐⭐ | +| Node.js | 100-500K | 10-100ms | ⭐⭐⭐⭐ | ⭐⭐⭐ | +| Elixir/Phoenix | 2M+ | 5-50ms | ⭐⭐⭐ | ⭐⭐ | + +## Justification + +### Pourquoi Go plutôt que Rust ? + +Rust offre meilleures performances absolues (2M conn/serveur vs 1M, 0 GC pauses) mais **Go gagne sur le plan startup** : + +1. **Time-to-market critique** : MVP en 8 semaines vs 12+ pour Rust + - Courbe d'apprentissage borrow checker = grosse friction pour juniors + - Temps compilation + refactoring : 30-60s vs 1-2s Go + - Recrutement moins onéreux (€35-50K junior Go vs €50-70K Rust) + +2. **Écosystème production-ready pour RoadWave** : + - **WebRTC** : pion/webrtc (mature) vs webrtc.rs (naissant) + - **Tests BDD** : Godog/Gherkin natif en Go, pas d'équivalent Rust + - **PostgreSQL + PostGIS** : pgx (excellent) vs sqlx (bon mais moins mature) + - **Zitadel/OAuth2** : clients Go stables vs Rust émergents + +3. **Performance suffisante pour 10M users distribués** : + - 1M conn/serveur = 10 serveurs max pour pics + - GC pauses (10-100ms) acceptable avec stratégie multi-région + - Scaling horizontal plus simple que vertical + +4. **Tooling natif** : + - pprof intégré (CPU, mémoire) + - race detector systématique + - Kubernetes first-class + - Cold start ~10ms (vs ~50ms Rust) + +### Quand Rust aurait du sens + +- Si concentrations d'1M+ connexions sur serveur unique (cas rare RoadWave) +- Si p99 latencies en prod > 100ms deviennent bottleneck (après Growth phase) +- Si refonte majeure planifiée anyway +- Stratégie possible : réécrire services hot (WebRTC, HLS streaming) en Rust à phase Scale + +## Conséquences + +- Formation équipe sur Go si nécessaire +- Utilisation des bibliothèques : Fiber (HTTP), pgx (PostgreSQL), go-redis +- Monitoring GC pauses en production (cibler < 20ms p95) +- Potential migration partielle à Rust pour services critiques post-Series A diff --git a/docs/adr/002-protocole-streaming.md b/docs/adr/002-protocole-streaming.md new file mode 100644 index 0000000..f7c76f4 --- /dev/null +++ b/docs/adr/002-protocole-streaming.md @@ -0,0 +1,182 @@ +# ADR-002 : Protocole de Streaming + +**Statut** : Accepté +**Date** : 2025-01-17 + +## Contexte + +Streaming audio vers des utilisateurs mobiles en voiture, avec réseaux instables (tunnels, zones rurales, handoff cellulaire). + +## Décision + +**HLS** (HTTP Live Streaming) pour le contenu à la demande. +**WebRTC** réservé à la radio live. + +## Alternatives considérées + +| Option | Latence | Fiabilité mobile | Cache CDN | Complexité | +|--------|---------|------------------|-----------|------------| +| **HLS** | 5-30s | Excellente | Oui | Faible | +| DASH | 5-30s | Bonne | Oui | Moyenne | +| WebRTC | <500ms | Moyenne | Non | Élevée | +| UDP brut | Minimale | Faible | Non | Très élevée | + +## Justification + +- **Réseaux mobiles** : HLS gère les coupures et changements de cellule nativement +- **Cache CDN** : Segments .ts cachables = réduction des coûts +- **Compatibilité** : Support natif iOS/Android +- **Bitrate adaptatif** : Ajustement automatique selon la qualité réseau + +## Pourquoi pas UDP ? + +- Problèmes NAT/firewall sur réseaux mobiles +- Perte de paquets = artefacts audio +- Impossible à cacher sur CDN +- Complexité sans bénéfice pour du contenu non-interactif + +## Conséquences + +- Latence de 5-30s acceptable pour podcasts/audio-guides (avec pré-buffering, voir section 4.3) +- WebRTC à implémenter séparément pour la radio live + +## Gestion de la Latence et Synchronisation Géolocalisée + +### Problème Identifié + +La latence HLS (5-30s) entre en conflit avec les notifications géolocalisées qui doivent déclencher l'audio **au moment précis** où l'utilisateur atteint un point d'intérêt. + +**Exemple critique** : +- Utilisateur en voiture à 90 km/h (25 m/s) +- ETA de 7 secondes avant le point → notification affichée +- Latence HLS de 15 secondes +- Résultat : audio démarre **200 mètres après** le point d'intérêt ❌ + +### Solution : Pre-buffering Anticipé + ETA Adaptatif + +#### 4.3.1 Pre-buffering Automatique + +**Déclenchement** : À ETA = 30 secondes du prochain point d'intérêt + +``` +[App Mobile] + ↓ (ETA=30s, position GPS détectée) +[Cache Manager] + ↓ (télécharge en arrière-plan) +[CDN NGINX] → /audio/poi-{id}/intro.m4a (10-15s d'audio, ~5-8 MB) + ↓ +[Cache Local Mobile] (max 3 POI simultanés) +``` + +**Stratégie de cache** : +- Télécharge les **15 premières secondes** de chaque POI à proximité +- Limite : 3 POI simultanés en cache (max ~25 MB) +- Purge automatique après 200m de distance passée +- Format : M4A haute qualité (128 kbps) pour intro, puis bascule HLS pour la suite + +#### 4.3.2 ETA de Notification Adaptatif + +**Algorithme** : + +```python +def calculate_notification_eta(poi, user_position, user_speed): + distance_to_poi = haversine(user_position, poi.position) + is_cached = cache.has(poi.audio_id) + hls_latency = metrics.get_average_latency(user_id) # 8-18s typique + + if is_cached: + # Cache prêt → notification courte (temps de réaction) + notification_eta = 5 # secondes + else: + # Pas de cache → compenser latence HLS + marge + notification_eta = hls_latency + 3 + 2 # latence + marge + réaction + # Typique: 10s + 3s + 2s = 15s + + time_to_poi = distance_to_poi / user_speed + + if time_to_poi <= notification_eta: + send_notification(poi) + + if time_to_poi <= 30 and not is_cached: + cache.preload_async(poi.audio_id) +``` + +**Résultat** : + +| Situation | ETA Notification | Distance (à 90 km/h) | Expérience | +|-----------|------------------|----------------------|------------| +| Cache prêt | 5s | 125m avant | ✅ Lecture instantanée | +| Cache en cours | 15s | 375m avant | ✅ Lecture à temps | +| 3G lent | 20s | 500m avant | ✅ Compensation latence | + +#### 4.3.3 Mesure Dynamique de Latence + +**Tracking par utilisateur** : + +```go +type HLSMetrics struct { + UserID uuid.UUID + AvgLatency time.Duration // Moyenne glissante 10 lectures + NetworkType string // "4G", "5G", "3G", "wifi" + LastMeasured time.Time +} + +// Mesure lors de chaque lecture +func (m *HLSMetrics) RecordPlaybackStart(requestTime, firstByteTime time.Time) { + latency := firstByteTime.Sub(requestTime) + m.AvgLatency = (m.AvgLatency*9 + latency) / 10 // Moyenne glissante +} +``` + +**Utilisation** : L'ETA adaptatif utilise `AvgLatency` personnalisé par utilisateur au lieu d'une valeur fixe. + +#### 4.3.4 Fallback et Indicateurs Visuels + +Si le pre-buffer échoue (réseau faible, pas de cache), afficher un **loader avec progression** : + +``` +┌─────────────────────────────────┐ +│ 🏰 Château de Fontainebleau │ +│ │ +│ [████████░░] 80% │ +│ Chargement de l'audio... │ +│ │ +│ 📍 Vous arrivez dans 12s │ +└─────────────────────────────────┘ +``` + +- **Feedback visuel** : Utilisateur comprend que ça charge +- **ETA affiché** : Maintient l'attention et réduit la frustration perçue +- **Timeout** : Si > 30s sans succès, proposer mode dégradé (texte seul) + +### Impact sur l'Infrastructure + +#### Backend (Go) +- **Nouveau service** : `audiocache.Service` pour préparer les extraits M4A +- **Endpoint** : `GET /api/v1/audio/poi/:id/intro` (retourne 15s d'audio) +- **CDN** : Cache NGINX avec TTL 7 jours sur `/audio/*/intro.m4a` + +#### Mobile (Flutter) +- **Package** : `just_audio` avec cache local (`flutter_cache_manager`) +- **Stockage** : Max 100 MB de cache audio (auto-purge LRU) +- **Logique** : `PreBufferService` avec scoring de priorité POI + +#### Coûts +- **Bande passante** : +10-15 MB/utilisateur/session (vs streaming pur) +- **Stockage CDN** : +500 MB pour 1000 POI × 5 MB intro (négligeable) +- **Économie** : Cache CDN réduit les requêtes origin (-60% selon tests) + +### Métriques de Succès + +- **Latence perçue** : < 1 seconde dans 95% des cas (cache hit) +- **Synchronisation** : Audio démarre à ±10 mètres du POI (objectif ±50m) +- **Cache hit rate** : > 90% pour utilisateurs en mode navigation +- **Consommation data** : +15% vs HLS pur, mais acceptable pour UX + +### Références + +- [HLS Authoring Specification](https://developer.apple.com/documentation/http_live_streaming/hls_authoring_specification_for_apple_devices) +- [Low-Latency HLS (LL-HLS)](https://developer.apple.com/documentation/http_live_streaming/enabling_low-latency_hls) +- Règle Métier 05 : Section 5.2 (Mode Voiture, lignes 16-84) +- Règle Métier 17 : Section 17.2 (ETA Géolocalisé, lignes 25-65) +- **ADR-019** : Architecture des Notifications Géolocalisées diff --git a/docs/adr/003-codec-audio.md b/docs/adr/003-codec-audio.md new file mode 100644 index 0000000..147d666 --- /dev/null +++ b/docs/adr/003-codec-audio.md @@ -0,0 +1,41 @@ +# ADR-003 : Codec Audio + +**Statut** : Accepté +**Date** : 2025-01-17 + +## Contexte + +Audio diffusé en voiture : environnement bruyant, réseau mobile variable, qualité studio non nécessaire. + +## Décision + +**Opus** comme codec principal, **AAC-LC** en fallback. + +## Profils d'encodage + +| Qualité | Bitrate | Usage | +|---------|---------|-------| +| Basse | 24 kbps | 2G/Edge | +| Standard | 48 kbps | 3G | +| Haute | 64 kbps | 4G/5G | + +## Alternatives considérées + +| Codec | Bitrate | Qualité voix | Support mobile | +|-------|---------|--------------|----------------| +| **Opus** | 24-64 kbps | Excellente | Android natif, iOS via libs | +| AAC-LC | 64-128 kbps | Bonne | Universel | +| AAC-HE v2 | 32-64 kbps | Très bonne | Bon | +| MP3 | 128-320 kbps | Correcte | Universel (legacy) | + +## Justification + +- **Environnement bruyant** : Opus intègre des algorithmes de résilience au bruit +- **Bande passante** : 48 kbps Opus ≈ qualité 96 kbps AAC pour la voix +- **Consommation data** : ~20 MB/heure à 48 kbps +- **Latence** : 2.5-60ms, idéal pour streaming adaptatif + +## Conséquences + +- Fallback AAC-LC pour appareils legacy +- Pipeline d'encodage à prévoir côté ingestion diff --git a/docs/adr/004-cdn.md b/docs/adr/004-cdn.md new file mode 100644 index 0000000..950493d --- /dev/null +++ b/docs/adr/004-cdn.md @@ -0,0 +1,56 @@ +# ADR-004 : CDN + +**Statut** : Accepté +**Date** : 2025-01-25 + +## Contexte + +Distribution audio HLS à 10M d'utilisateurs, besoin de performance, coût maîtrisé, et **souveraineté** : pas de dépendance à un service commercial tiers. + +## Décision + +**NGINX auto-hébergé** sur OVH comme cache HLS, avec OVH Object Storage en origin. + +## Alternatives considérées + +| Solution | Coût MVP/mois | Souveraineté | Setup | Dépendance | +|----------|---------------|--------------|-------|------------| +| **NGINX self-hosted** | ~14€ | 100% FR | 2h | Aucune | +| CDN commercial externe | ~1 000€ | Slovénie | 15 min | Forte | +| OVHcloud CDN | ~50-200€ | 100% FR | 1h | Moyenne | +| Cloudflare | 0-5 000€ | US | 5 min | Forte | +| CloudFront | ~9 750€ | US (AWS) | 1h | Très forte | + +## Justification + +- **Souveraineté** : 100% français (OVH), 100% contrôlé, zéro dépendance commerciale +- **Open-source** : NGINX sous licence BSD, stack entièrement libre +- **Coût** : ~14€/mois MVP (VPS inclus), scaling linéaire et prévisible +- **Performance** : Cache multiplexing 1→1000 (1 Mbps origin → 1 Gbps clients) +- **Conformité RGPD** : Données hébergées en France + +## Architecture + +``` +Clients + ↓ +NGINX Cache Proxy (OVH VPS Essential) +├─ Cache RAM disk (2GB) +├─ TTL .m3u8: 5s +└─ TTL .ts: 7 jours + ↓ +OVH Object Storage (origin) +``` + +## Évolution prévue + +1. **Phase 1** (0-20K users) : NGINX mono-région +2. **Phase 2** (20-100K users) : NGINX multi-région (Gravelines + Strasbourg) + GeoDNS +3. **Phase 3** (100K+) : Évaluation CDN managé européen si ROI justifié + +## Conséquences + +- Configuration NGINX avec `proxy_cache_path` et règles de TTL +- RAM disk `/mnt/ramdisk` pour performance maximale +- Monitoring bande passante et taux de cache hit +- Sécurité : token authentication pour protéger les segments HLS diff --git a/docs/adr/005-base-de-donnees.md b/docs/adr/005-base-de-donnees.md new file mode 100644 index 0000000..897b44f --- /dev/null +++ b/docs/adr/005-base-de-donnees.md @@ -0,0 +1,67 @@ +# ADR-005 : Base de Données + +**Statut** : Accepté +**Date** : 2025-01-17 + +## Contexte + +Requêtes géolocalisées intensives (contenus à proximité), données utilisateurs, historiques d'écoute. + +## Décision + +- **PostgreSQL + PostGIS** : Données persistantes et requêtes géospatiales +- **Redis Cluster** : Cache géolocalisation et sessions + +## Architecture + +``` +Requête → Redis Cache → [HIT] → Réponse + ↓ + [MISS] + ↓ + PostGIS → Cache → Réponse +``` + +## Alternatives considérées + +| Usage | Option choisie | Alternatives | +|-------|---------------|--------------| +| Données utilisateurs | PostgreSQL | MySQL, MongoDB | +| Géolocalisation | PostGIS | MongoDB Geo, Elasticsearch | +| Cache | Redis | Memcached, KeyDB | +| Analytics (futur) | ClickHouse | TimescaleDB | + +## Justification + +### PostgreSQL + PostGIS +- Requêtes géospatiales complexes et précises +- Index GIST pour performance +- ACID, fiabilité éprouvée +- Écosystème mature + +### Redis +- Cache géo natif (`GEORADIUS`) : 100K+ requêtes/sec +- Sessions utilisateurs +- Pub/sub pour temps réel + +## Exemple de requête + +```sql +SELECT id, name, + ST_Distance(location::geography, ST_MakePoint($lon, $lat)::geography) as distance +FROM contents +WHERE ST_DWithin(location::geography, ST_MakePoint($lon, $lat)::geography, 50000) +ORDER BY distance +LIMIT 20; +``` + +## Conséquences + +- TTL cache Redis : 5 minutes (le contenu géolocalisé ne bouge pas) +- Index GIST sur colonnes géométriques +- Réplication read replicas pour scaling lecture + +## Documentation technique détaillée + +- [Diagramme de séquence cache géospatial](../architecture/sequences/cache-geospatial.md) +- [Schéma base de données](../architecture/database/schema.md) diff --git a/docs/adr/006-chiffrement.md b/docs/adr/006-chiffrement.md new file mode 100644 index 0000000..a937800 --- /dev/null +++ b/docs/adr/006-chiffrement.md @@ -0,0 +1,44 @@ +# ADR-006 : Chiffrement + +**Statut** : Accepté +**Date** : 2025-01-17 + +## Contexte + +Streaming audio sur réseaux mobiles, conformité RGPD, protection du contenu. + +## Décision + +- **TLS 1.3** sur tous les endpoints +- **DTLS-SRTP** pour WebRTC (radio live) +- Pas de DRM au lancement + +## Alternatives considérées + +| Méthode | Overhead | Usage | +|---------|----------|-------| +| **TLS 1.3** | ~1-2% CPU | HTTPS streaming | +| DTLS-SRTP | ~3-5% CPU | WebRTC temps réel | +| AES-128-CBC | Minimal | Chiffrement segments HLS | +| Widevine/FairPlay | Modéré | DRM (si licences l'exigent) | + +## Justification + +### Pourquoi chiffrer ? + +- **RGPD** : Protection des données utilisateurs obligatoire +- **Confiance** : Standard attendu en 2025 +- **Intégrité** : Empêche injection de contenu par opérateurs +- **Overhead minimal** : TLS 1.3 optimisé, impact négligeable + +### Pourquoi pas de DRM ? + +- Contenu généré par utilisateurs (pas de licences) +- Complexité et coût d'intégration Widevine/FairPlay +- À reconsidérer si partenariats avec labels/éditeurs + +## Conséquences + +- Certificats SSL gérés par Let's Encrypt (auto-renouvelés via Nginx) +- Configuration TLS 1.3 sur Nginx/API +- DTLS-SRTP à implémenter pour le module radio live diff --git a/docs/adr/007-tests-bdd.md b/docs/adr/007-tests-bdd.md new file mode 100644 index 0000000..05c03f2 --- /dev/null +++ b/docs/adr/007-tests-bdd.md @@ -0,0 +1,62 @@ +# ADR-007 : Tests et Spécifications Exécutables + +**Statut** : Accepté +**Date** : 2025-01-17 + +## Contexte + +RoadWave nécessite une documentation des use cases qui soit à la fois lisible par tous les stakeholders et vérifiable automatiquement. Les scénarios utilisateurs (touriste, routier, commerçant) doivent être validés en continu. + +## Décision + +**Gherkin** pour les spécifications avec **Godog** comme runner de tests. + +## Alternatives considérées + +| Option | Lisibilité | Intégration Go | Maintenance | +|--------|------------|----------------|-------------| +| **Gherkin + Godog** | Excellente | Native | Faible | +| Gauge (Markdown) | Bonne | Plugin | Moyenne | +| Tests Go natifs | Faible (devs only) | Native | Faible | +| Concordion | Bonne | Java-centric | Élevée | + +## Justification + +- **Living Documentation** : Les fichiers `.feature` servent de documentation ET de tests +- **Accessibilité** : Syntaxe Given/When/Then lisible par PO, devs, testeurs +- **Cohérence stack** : Godog est le standard BDD pour Go +- **CI/CD** : Intégration simple dans les pipelines + +## Structure + +``` +features/ +├── recommendation/ +│ ├── geolocalisation.feature +│ └── interets.feature +├── streaming/ +│ ├── lecture.feature +│ └── buffering.feature +├── moderation/ +│ └── signalement.feature +└── steps/ + └── steps.go +``` + +## Exemple + +```gherkin +Feature: Recommandation géolocalisée + + Scenario: Touriste près d'un monument + Given un utilisateur avec l'intérêt "tourisme" à 80% + And une position GPS à 100m de la Tour Eiffel + When le système calcule les recommandations + Then l'audio guide "Histoire de la Tour Eiffel" est en première position +``` + +## Conséquences + +- Dépendance : `github.com/cucumber/godog` +- Les use cases du README doivent être traduits en `.feature` +- CI exécute `godog run` avant chaque merge diff --git a/docs/adr/008-authentification.md b/docs/adr/008-authentification.md new file mode 100644 index 0000000..0058b75 --- /dev/null +++ b/docs/adr/008-authentification.md @@ -0,0 +1,119 @@ +# ADR-008 : Authentification et Gestion d'Identité + +**Statut** : Accepté +**Date** : 2025-01-18 + +## Contexte + +RoadWave nécessite un système d'authentification sécurisé pour mobile (iOS/Android), scalable jusqu'à 10M utilisateurs, avec contraintes de coût réduit et conformité RGPD. + +**Exigence de souveraineté** : En cohérence avec ADR-004 (CDN 100% français), les données d'authentification doivent être hébergées en France pour garantir une souveraineté totale. + +## Décision + +**Zitadel self-hosted sur OVH France** pour l'IAM avec validation JWT locale côté API Go. + +**Architecture de déploiement** : +- Container Docker sur le même VPS OVH (Gravelines, France) que l'API +- Base de données PostgreSQL partagée avec RoadWave (séparation logique par schéma) +- Aucune donnée d'authentification ne transite par des serveurs tiers + +## Alternatives considérées + +| Solution | Coût (10M users) | Performance | Simplicité | Intégration Go | +|----------|------------------|-------------|------------|----------------| +| **Zitadel** | 200-500€/mois | Excellente | Élevée | SDK natif | +| Supabase Auth | 32K€/mois | Excellente | Élevée | REST API | +| Keycloak | 200-800€/mois | Bonne | Faible | Lib tierce | +| Auth0 | 50K€+/mois | Excellente | Élevée | SDK natif | +| JWT Custom | 0€ (dev) | Excellente | Moyenne | Natif | + +## Justification + +- **Souveraineté garantie** : Self-hosting sur OVH France (Gravelines) = 100% des données en France, cohérent avec ADR-004 +- **Coût maîtrisé** : 100x moins cher que Supabase/Auth0 à 10M users (pas de coût par utilisateur actif) +- **Performance** : JWT validation locale = 0 latence auth sur chaque requête API +- **Stack alignée** : Go + PostgreSQL + Redis (déjà dans RoadWave) +- **Scalabilité prouvée** : Clients avec 2.3M tenants, architecture event-sourced +- **RGPD natif** : Open source, contrôle total des données, DPA non nécessaire (pas de sous-traitant) +- **Standards ouverts** : OpenID Connect certifié (pas de vendor lock-in, migration facile si besoin) + +## Architecture + +``` +┌─────────────────┐ +│ Mobile Apps │ OAuth2 PKCE + Refresh tokens +│ (iOS/Android) │ +└────────┬────────┘ + │ HTTPS + │ + ┌────▼─────────────────────────────────┐ + │ OVH VPS Essential (Gravelines, FR) │ + │ │ + │ ┌─────────────────┐ │ + │ │ Zitadel IdP │ Port 8081 │ + │ │ (Docker) │ Self-hosted │ + │ └────────┬────────┘ │ + │ │ JWT token │ + │ ┌────────▼────────┐ │ + │ │ Go + Fiber API │ Port 8080 │ + │ │ (RoadWave) │ Validation │ + │ │ │ JWT locale │ + │ └────────┬────────┘ │ + │ │ │ + │ ┌────────▼────────┐ │ + │ │ PostgreSQL │ Schémas: │ + │ │ + PostGIS │ - roadwave │ + │ │ │ - zitadel │ + │ └─────────────────┘ │ + └───────────────────────────────────────┘ + +Données 100% hébergées en France (souveraineté totale) +``` + +## Exemple d'intégration + +```go +import "github.com/zitadel/zitadel-go/v3/pkg/authorization/oauth" + +// Validation JWT locale haute performance +verifier := oauth.WithJWT(config) +app.Use(verifier.Middleware()) + +// Accès aux claims +userID := ctx.Locals("sub").(string) +``` + +## Conséquences + +### Positives + +- ✅ **Souveraineté totale** : Données 100% en France (OVH Gravelines), contrôle complet +- ✅ **Coût prévisible** : Pas de surprise à la croissance (pas de facturation par utilisateur) +- ✅ **Performance** : Latence minimale (même VPS que l'API) +- ✅ **Fonctionnalités avancées** : MFA, passkeys, SSO disponibles gratuitement +- ✅ **Conformité RGPD** : Pas de DPA nécessaire (pas de sous-traitant externe) +- ✅ **Standards ouverts** : Migration facile vers autre solution si besoin + +### Négatives + +- ❌ **Maintenance** : Nécessite monitoring et mises à jour régulières +- ❌ **Complexité initiale** : Configuration PostgreSQL schema partagé +- ❌ **Backup** : Responsabilité de sauvegarder les données utilisateurs +- ❌ **Scaling** : Migration Kubernetes nécessaire au-delà de 500K utilisateurs + +### Déploiement + +- **MVP (0-20K)** : Docker sur VPS OVH Essential (coût inclus) +- **Growth (20K-500K)** : Même architecture, VPS plus puissant si besoin +- **Scale (500K+)** : Migration Kubernetes managé avec haute disponibilité + +### Coût Estimé + +| Phase | Utilisateurs | Coût Zitadel/mois | +|-------|--------------|-------------------| +| MVP | 0-20K | 0€ (inclus VPS) | +| Growth | 20K-500K | 0€ (inclus VPS) | +| Scale | 500K+ | 50-100€ (instance K8s dédiée) | + +**Comparaison** : Auth0 coûterait 50K€/mois pour 10M utilisateurs vs 100€/mois en self-hosted. diff --git a/docs/adr/009-solution-paiement.md b/docs/adr/009-solution-paiement.md new file mode 100644 index 0000000..c501199 --- /dev/null +++ b/docs/adr/009-solution-paiement.md @@ -0,0 +1,61 @@ +# ADR-009 : Solution de Paiement et Gestion des Abonnements + +**Statut** : Accepté +**Date** : 2025-01-19 + +## Contexte + +RoadWave nécessite une solution de paiement pour gérer les abonnements Premium (4.99€/mois) et reverser 70% des revenus aux créateurs de contenu. Besoin de marketplace natif (split payments), KYC automatique, conformité RGPD, et coûts maîtrisés. + +## Décision + +**Mangopay** (France/Luxembourg) comme solution unique pour paiements, marketplace et abonnements. + +## Alternatives considérées + +| Solution | Coût transaction | Marketplace | KYC | Souveraineté | +|----------|-----------------|-------------|-----|--------------| +| **Mangopay** | 1.8% + 0.18€ | ✅ Natif | ✅ Gratuit | 🇪🇺 France/LU | +| Stripe Connect | 2.9% + 0.30€ | ✅ Natif | ❌ 1.20€ | 🇺🇸 USA | +| Mollie | 2.9% + 0.29€ | ❌ Non | ❌ Non | 🇪🇺 Pays-Bas | +| Paddle | 5% + 0.50€ | ✅ Natif | ✅ Inclus | 🇬🇧 UK | + +## Justification + +- **38% moins cher** que Stripe (1.8% vs 2.9%) +- **Marketplace natif** : E-wallets automatiques, split payments 70/30, payouts SEPA gratuits +- **KYC gratuit** : vérification d'identité incluse (vs 1.20€/créateur chez Stripe) +- **Souveraineté EU** : France/Luxembourg, régulé ACPR, RGPD natif +- **Conformité DAC7** : reporting fiscal automatique +- **Spécialisé marketplace** : utilisé par Vinted, Ulule, ManoMano + +## Architecture + +``` +┌────────────────────────┐ +│ Utilisateurs Premium │ 4.99€/mois +└───────────┬────────────┘ + │ + ┌───────▼───────┐ + │ Mangopay │ - Abonnements récurrents + │ │ - KYC créateurs (gratuit) + │ │ - E-wallets automatiques + └───────┬───────┘ - Payouts SEPA (gratuits) + │ + ┌─────────┼─────────┐ + │ │ │ +┌─▼───┐ ┌─▼───┐ ┌─▼────┐ +│Créa │ │Créa │ │Plate-│ +│teur │ │teur │ │forme │ +│ A │ │ B │ │(30%) │ +│(70%)│ │(70%)│ │ │ +└─────┘ └─────┘ └──────┘ +``` + +## Conséquences + +- Solution tout-en-un : 1 seul prestataire vs 2-3 +- Économie de 2160€/an sur 1000 abonnés (vs Stripe) +- Délai activation compte : 2-5 jours +- Intégration Go via REST API (pas de SDK Go officiel) +- Apple/Google IAP gérés séparément (comme toute solution de paiement) diff --git a/docs/adr/010-commandes-volant.md b/docs/adr/010-commandes-volant.md new file mode 100644 index 0000000..b67692a --- /dev/null +++ b/docs/adr/010-commandes-volant.md @@ -0,0 +1,46 @@ +# ADR-010 : Commandes au volant et likes + +**Statut** : Accepté +**Date** : 2026-01-20 + +## Contexte + +RoadWave est utilisée en conduisant. Les utilisateurs doivent pouvoir liker du contenu pour améliorer les recommandations, mais les commandes au volant ont des limitations : +- 40% des véhicules n'ont que Suivant/Précédent/Mute +- iOS/Android ne supportent pas nativement les appuis longs ou doubles-appuis +- La sécurité impose des interactions minimales + +## Décision + +**Like automatique basé sur le temps d'écoute**. + +Règles : +- ≥80% d'écoute → Like renforcé (+2 points) +- 30-79% d'écoute → Like standard (+1 point) +- <30% d'écoute → Pas de like +- Skip <10s → Signal négatif (-0.5 point) + +## Alternatives considérées + +| Option | Compatibilité | Sécurité | Complexité | +|--------|---------------|----------|------------| +| **Like automatique** | 100% | Maximale | Faible | +| Double-tap Pause | ~80% | Moyenne | Moyenne | +| Appui long Suivant | ~95% | Faible | Élevée | +| Configuration paramétrable | 100% | Variable | Très élevée | + +## Justification + +- **Sécurité maximale** : Aucune action complexe en conduite +- **Compatibilité universelle** : Fonctionne sur 100% des véhicules +- **UX intuitive** : Comportement standard (Spotify, YouTube Music) +- **Engagement** : Tous les contenus génèrent des signaux +- **Simplicité** : Une seule logique à implémenter et maintenir + +## Conséquences + +- Tracking du temps d'écoute via le player audio +- Calcul du score côté backend basé sur `completion_rate` +- Communication onboarding : "Vos likes sont automatiques selon votre temps d'écoute" +- Possibilité de like manuel depuis l'app (à l'arrêt) +- Métriques à suivre : taux de complétion, distribution des scores, feedbacks utilisateurs diff --git a/docs/adr/011-conformite-stores-carplay-android-auto.md b/docs/adr/011-conformite-stores-carplay-android-auto.md new file mode 100644 index 0000000..d2afcd9 --- /dev/null +++ b/docs/adr/011-conformite-stores-carplay-android-auto.md @@ -0,0 +1,137 @@ +# ADR-011 : Conformité App Stores et Plateformes Auto + +**Statut** : Accepté avec actions requises +**Date** : 2026-01-20 + +## Contexte + +RoadWave est une app audio géolocalisée utilisée en conduite (CarPlay/Android Auto) avec : +- Contenu généré par utilisateurs (UGC) +- Monétisation : publicités géolocalisées + Premium (4.99€ web / 5.99€ IAP) +- GPS en arrière-plan +- Partage de revenus avec créateurs (70/30) + +## Décision + +**Stratégie de conformité multi-plateforme** avec : +- Modération UGC robuste (IA + humain) +- Prix différenciés selon région (US/EU/Monde) +- GPS avec disclosure complète +- Paiements créateurs externes (Mangopay) + +## Plateformes analysées + +| Plateforme | Conformité | Points critiques | +|------------|------------|------------------| +| **Android Auto** | ✅ Conforme | API Level 35+ (Android 15+) | +| **CarPlay** | ✅ Conforme | Entitlement audio à demander | +| **Google Play** | ⚠️ Actions requises | Déclaration GPS + UGC modération | +| **App Store** | ⚠️ Actions requises | Prix différenciés US/EU | + +## Conformité détaillée + +### Android Auto / CarPlay ✅ +- 100% audio (pas de vidéo) +- Commandes standard au volant +- Aucun achat in-car +- Like automatique = sécurité maximale +- **Notifications géolocalisées** : sonore uniquement en mode CarPlay/Android Auto (pas d'overlay visuel) +- **Action** : Demander CarPlay Audio Entitlement (Apple) + +### Google Play ⚠️ + +**UGC (critique)** : +- Modération hybride IA + humain ✅ +- 3 premiers contenus validés manuellement ✅ +- Système de strikes (4 = ban) ✅ +- Signalement + blocage utilisateurs ✅ + +**GPS Background (critique)** : +- Permission "Always Location" = **OPTIONNELLE** +- Demandée uniquement pour mode piéton (notifications arrière-plan audio-guides) +- Justification Play Console : + > "RoadWave permet aux utilisateurs de recevoir des alertes audio-guides lorsqu'ils passent à pied près de monuments/musées, même quand l'app est en arrière-plan. Cette fonctionnalité est optionnelle et peut être désactivée dans les paramètres." +- In-app disclosure obligatoire (écran dédié avant demande permission) +- Si refusée : app fonctionne en mode voiture uniquement +- **Action** : Remplir formulaire background location Play Console avec justification + +**Réponses formulaire Play Console** : + +| Question | Réponse | +|----------|---------| +| Why does your app need background location? | "RoadWave offers optional pedestrian mode: users receive push notifications when passing near audio-guide points (museums, monuments) even when app is in background. This feature is opt-in and can be disabled in settings." | +| Is this feature core to your app? | "No. This is an optional feature. Users can use RoadWave without background location permission (in-car mode works with foreground location only)." | +| What user value does this provide? | "Pedestrian users (tourists, museum visitors) can keep phone in pocket and receive audio-guide alerts automatically without opening the app." | +| Does a less invasive alternative exist? | "Yes. Users can use manual navigation (open app, select audio-guide). Background location is a convenience feature for hands-free experience." | + +### App Store ⚠️ + +**Prix différenciés (légaux depuis 2025-2026)** : +- 🇺🇸 US : Lien externe autorisé (0% commission) +- 🇪🇺 EU : Paiement externe DMA (7-20% commission réduite) +- 🌍 Monde : IAP obligatoire (30% commission) + +**UGC** : +- Mode Kids obligatoire (filtrage selon âge) ✅ +- Système de modération + signalement ✅ + +**GPS Background (critique)** : +- Permission "Always Location" = **OPTIONNELLE** +- Deux strings Info.plist requises : + - `NSLocationWhenInUseUsageDescription` : explication mode voiture + - `NSLocationAlwaysAndWhenInUseUsageDescription` : explication mode piéton (optionnel) +- In-app disclosure obligatoire avant demande "Always" +- Flux two-step : When In Use → Always (si user active mode piéton) +- Si refusée : app fonctionne en mode voiture uniquement +- **Action** : Voir strings détaillés dans [05-interactions-navigation.md](../regles-metier/05-interactions-navigation.md#512-mode-piéton-audio-guides) + +### Revenus créateurs + +**Position** : Paiements créateurs = "services" (comme YouTube/Uber), pas IAP +- Paiement via Mangopay Connect (externe) +- Commission stores uniquement sur Premium (IAP) +- Comparables : YouTube AdSense, TikTok Creator Fund, Uber + +## Actions bloquantes avant soumission + +| Action | Plateforme | Deadline | Complexité | +|--------|-----------|----------|------------| +| Demander CarPlay Audio Entitlement | Apple | Avant soumission iOS | Faible | +| Remplir formulaire background location avec justification | Google Play | Avant soumission Android | Faible | +| Implémenter disclosure GPS (écran dédié mode piéton) | iOS + Android | MVP | Moyenne | +| Rendre permission "Always Location" optionnelle | iOS + Android | MVP | Moyenne | +| Désactiver overlay visuel notification en CarPlay/Android Auto | iOS + Android | MVP | Moyenne | +| Mettre à jour strings Info.plist avec justifications détaillées | iOS | MVP | Faible | +| Finaliser système modération UGC | Google + Apple | MVP | Élevée | + +**Estimation totale** : +5 jours développement avant soumission stores + +## Stratégie de lancement + +**Phase 1 - MVP** : +- IAP uniquement (5.99€/mois mondial) +- Modération UGC active +- GPS avec disclosure +- CarPlay/Android Auto basique + +**Phase 2 - Post-validation** : +- Prix différenciés US (lien externe 4.99€) +- Paiement externe EU (DMA) +- Monétisation créateurs (Mangopay) + +## Conséquences + +- Formation équipe sur politiques stores +- Suivi des métriques modération (% rejet, SLA) +- Migration iOS 26 SDK (Avril 2026) +- API Level 35 Android (2026) +- Communication transparente GPS/publicités + +## Sources + +- [Android Auto Media Apps](https://developer.android.com/training/cars/media) +- [CarPlay Developer Guide](https://developer.apple.com/carplay) +- [Google Play UGC Policy](https://support.google.com/googleplay/android-developer/answer/9876937) +- [App Store Guidelines](https://developer.apple.com/app-store/review/guidelines/) +- [Apple DMA Update EU](https://www.revenuecat.com/blog/growth/apple-eu-dma-update-june-2025/) +- [Google Background Location 2026](https://support.google.com/googleplay/android-developer/answer/9799150) diff --git a/docs/adr/012-architecture-backend.md b/docs/adr/012-architecture-backend.md new file mode 100644 index 0000000..43cfb63 --- /dev/null +++ b/docs/adr/012-architecture-backend.md @@ -0,0 +1,50 @@ +# ADR-012 : Architecture Backend + +**Statut** : Accepté +**Date** : 2025-01-20 + +## Contexte + +RoadWave nécessite une architecture backend évolutive tout en gardant la simplicité opérationnelle pour un MVP. Le système doit supporter une croissance progressive de 0 à 10M utilisateurs. + +## Décision + +**Monolithe modulaire** avec séparation claire en modules internes. + +## Alternatives considérées + +| Architecture | Complexité | Coûts infra | Time to market | Évolutivité | +|--------------|------------|-------------|----------------|-------------| +| **Monolithe modulaire** | Faible | Faible | Rapide | 0-1M users | +| Microservices | Élevée | Élevée | Lent | 1M+ users | +| Hybrid (Mono + Workers) | Moyenne | Moyenne | Moyen | 100K-5M users | + +## Justification + +- **Simplicité** : 1 seul binaire Go, déploiement trivial +- **Transactions** : Communications inter-modules en mémoire (pas de latence réseau) +- **Debugging** : Stack traces complètes, profiling unifié +- **Coûts** : 1 serveur suffit pour 100K users (vs N services) +- **Refactoring** : Modules internes bien séparés facilitent migration vers microservices si nécessaire + +## Structure modulaire + +``` +internal/ +├── auth/ # Validation JWT, intégration Zitadel +├── user/ # Profils, centres d'intérêt +├── content/ # CRUD contenus, métadonnées +├── geo/ # Recherche géospatiale, algorithme +├── streaming/ # Génération HLS, transcoding +├── moderation/ # Signalements, workflow +├── payment/ # Intégration Mangopay +└── analytics/ # Métriques écoute, jauges +``` + +Chaque module suit : `handler.go` → `service.go` → `repository.go`. + +## Conséquences + +- Scaling horizontal : réplication complète du binaire (acceptable jusqu'à 1M users) +- Transition vers microservices possible en phase 2 (extraction progressive des modules) +- Importance de maintenir découplage fort entre modules (interfaces claires) diff --git a/docs/adr/013-orm-acces-donnees.md b/docs/adr/013-orm-acces-donnees.md new file mode 100644 index 0000000..7374316 --- /dev/null +++ b/docs/adr/013-orm-acces-donnees.md @@ -0,0 +1,57 @@ +# ADR-013 : ORM et Accès Données + +**Statut** : Accepté +**Date** : 2025-01-20 + +## Contexte + +RoadWave nécessite des requêtes SQL complexes (PostGIS géospatiales) avec performance optimale et type safety. Le choix entre ORM, query builder ou SQL brut impacte maintenabilité et performance. + +## Décision + +**sqlc** pour génération de code Go type-safe depuis SQL. + +## Alternatives considérées + +| Solution | Performance | Type Safety | Contrôle SQL | Courbe apprentissage | +|----------|-------------|-------------|--------------|----------------------| +| **sqlc** | Excellente | Très haute | Total | Faible | +| GORM | Moyenne | Moyenne | Limité | Faible | +| pgx + SQL brut | Excellente | Faible | Total | Moyenne | +| sqlx | Bonne | Faible | Total | Faible | + +## Justification + +- **Performance** : Génération compile-time, zero overhead runtime +- **Type safety** : Structs Go générées automatiquement, erreurs détectées à la compilation +- **Contrôle SQL** : Requêtes PostGIS complexes écrites en pur SQL (pas de limitations ORM) +- **Maintenabilité** : Modifications SQL → `sqlc generate` → code mis à jour +- **Simplicité** : Pas de magic, code généré lisible et debuggable + +## Workflow + +```sql +-- queries/content.sql +-- name: GetContentNearby :many +SELECT id, title, ST_Distance(location, $1::geography) as distance +FROM contents +WHERE ST_DWithin(location, $1::geography, $2) +ORDER BY distance +LIMIT $3; +``` + +```bash +sqlc generate +``` + +```go +// Code Go type-safe généré automatiquement +contents, err := q.GetContentNearby(ctx, location, radius, limit) +``` + +## Conséquences + +- Dépendance : `github.com/sqlc-dev/sqlc` +- Fichier `sqlc.yaml` à la racine pour configuration +- Migrations gérées séparément avec `golang-migrate` +- CI doit exécuter `sqlc generate` pour valider cohérence SQL/Go diff --git a/docs/adr/014-frontend-mobile.md b/docs/adr/014-frontend-mobile.md new file mode 100644 index 0000000..341d7a7 --- /dev/null +++ b/docs/adr/014-frontend-mobile.md @@ -0,0 +1,74 @@ +# ADR-014 : Frontend Mobile + +**Statut** : Accepté +**Date** : 2025-01-20 + +## Contexte + +RoadWave nécessite applications iOS et Android avec support CarPlay/Android Auto, lecture audio HLS avancée, géolocalisation temps réel. Le choix du framework impacte vélocité développement et performances. + +## Décision + +**Flutter** pour iOS et Android avec codebase unique. + +## Alternatives considérées + +| Framework | Codebase | Performance | Audio/CarPlay | Communauté | +|-----------|----------|-------------|---------------|------------| +| **Flutter** | Unique | Native | Excellente | Large | +| React Native | Unique | Bonne | Modules natifs requis | Très large | +| Native (Swift+Kotlin) | Double | Excellente | Native | Large | +| Ionic/Capacitor | Unique | Moyenne | Limitée | Moyenne | + +## Justification + +- **Codebase unique** : iOS + Android maintenus ensemble, vélocité développement x2 +- **Performance** : Dart compilé en code natif (pas de bridge JS) +- **Audio HLS** : Package `just_audio` mature avec support HLS, buffering adaptatif +- **CarPlay/Android Auto** : Support via packages communautaires (`flutter_carplay`, `android_auto_flutter`) +- **Géolocalisation** : `geolocator` robuste avec gestion permissions +- **Écosystème** : Widgets riches (Material/Cupertino), state management mature (Bloc, Riverpod) + +## Packages clés + +```yaml +dependencies: + flutter_bloc: ^8.1.3 # State management + just_audio: ^0.9.36 # Lecture audio HLS + geolocator: ^11.0.0 # GPS temps réel (mode voiture) + geofence_service: ^5.2.0 # Geofencing arrière-plan (mode piéton) + flutter_local_notifications: ^17.0.0 # Notifications géolocalisées + dio: ^5.4.0 # HTTP client + flutter_secure_storage: ^9.0.0 # Tokens JWT + cached_network_image: ^3.3.1 # Cache images +``` + +**Nouveaux packages (contenus géolocalisés)** : + +- **`geofence_service`** : Détection entrée/sortie rayon 200m en arrière-plan (mode piéton) + - Geofencing natif iOS/Android + - Minimise consommation batterie + - Supporte notifications push même app fermée + +- **`flutter_local_notifications`** : Notifications locales avec compteur dynamique + - Notification avec compteur décroissant (7→1) en mode voiture + - Icônes personnalisées selon type contenu + - Désactivation overlay en mode CarPlay/Android Auto (conformité) + +## Structure application + +``` +lib/ +├── core/ # Config, DI, routes +├── data/ # Repositories, API clients +├── domain/ # Models, business logic +├── presentation/ # UI (screens, widgets, blocs) +└── main.dart +``` + +## Conséquences + +- Équipe doit apprendre Dart (syntaxe proche Java/TypeScript) +- Taille binaire : 8-15 MB (acceptable) +- Tests : `flutter_test` pour widgets, `integration_test` pour E2E +- CI/CD : Fastlane pour déploiement stores diff --git a/docs/adr/015-strategie-tests.md b/docs/adr/015-strategie-tests.md new file mode 100644 index 0000000..2bb3e5a --- /dev/null +++ b/docs/adr/015-strategie-tests.md @@ -0,0 +1,84 @@ +# ADR-015 : Stratégie Tests + +**Statut** : Accepté +**Date** : 2025-01-20 + +## Contexte + +RoadWave nécessite une couverture tests robuste avec documentation vivante des use cases. La stratégie doit équilibrer vélocité développement et qualité. + +## Décision + +Approche **multi-niveaux** : unitaires, intégration, BDD (Gherkin), E2E, load testing. + +## Stratégie par type + +| Type | Framework | Cible | Fréquence | +|------|-----------|-------|-----------| +| **Unitaires** | Testify | 80%+ couverture | Chaque commit | +| **Intégration DB** | Testify + Testcontainers | Repositories critiques | Avant merge PR | +| **BDD (Gherkin)** | Godog | User stories | Avant release | +| **E2E Mobile** | Flutter integration_test | Parcours critiques | Nightly | +| **Load** | k6 | N/A | Avant mise en prod | + +## Tests unitaires (Testify) + +- **Framework** : `github.com/stretchr/testify` +- **Couverture minimale** : 80% sur packages `internal/*/service.go` +- **Exécution** : Chaque commit (CI rapide ~30s) + +## Tests BDD (Gherkin + Godog) + +- **Framework** : `github.com/cucumber/godog` +- **Couverture** : Tous les cas d'usage du [README.md](../../README.md) traduits en `.feature` +- **Exécution** : Avant release +- **Détails** : Voir [ADR-007](007-tests-bdd.md) pour contexte complet + +## Tests intégration (Testcontainers) + +- **Framework** : `github.com/testcontainers/testcontainers-go` +- **Scope** : Repositories avec PostGIS/Redis (requêtes spatiales complexes) +- **Exécution** : Avant merge PR +- **Justification** : Validation des requêtes PostGIS impossibles à mocker + +## Tests E2E Mobile (Flutter) + +- **Framework** : `integration_test` (Flutter officiel) +- **Scope** : Parcours critiques (authentification, lecture audio, géolocalisation) +- **Exécution** : Nightly builds +- **Justification** : Validation de l'intégration complète mobile + backend + +## Load testing (k6) + +- **Framework** : `grafana/k6` +- **Objectif** : API p99 < 100ms à 10K RPS +- **Scénarios** : Recommandations géolocalisées, streaming HLS, authentification +- **Exécution** : Avant mise en production + +## Alternatives considérées + +| Approche | Avantages | Inconvénients | Décision | +|----------|-----------|---------------|----------| +| **Tests unitaires uniquement** | Rapides, simples | Pas de validation intégration | ❌ Insuffisant pour PostGIS | +| **TDD strict (100% couverture)** | Qualité maximale | Vélocité réduite (-40%) | ❌ Trop coûteux pour MVP | +| **BDD sans tests unitaires** | Documentation vivante | Feedback lent (3-5min/run) | ❌ Cycle développement trop lent | +| **Stratégie multi-niveaux** | Équilibre qualité/vitesse | Complexité CI/CD | ✅ **Choisi** | + +## Justification + +- **80% unitaires** : Feedback rapide (<30s), détection précoce des régressions +- **BDD** : Documentation vivante alignée avec les règles métier +- **Testcontainers** : Seule façon fiable de tester PostGIS `ST_DWithin`, `ST_Distance` +- **E2E ciblés** : Validation des parcours critiques sans ralentir le développement +- **k6** : Détection des régressions de performance avant production + +## Conséquences + +- Dépendances : + - `github.com/stretchr/testify` + - `github.com/cucumber/godog` + - `github.com/testcontainers/testcontainers-go` + - `grafana/k6` +- Temps CI : ~3-5 min (tests unitaires + BDD) +- Tests intégration/E2E : nightly builds (15-30 min) +- Load tests : avant chaque release majeure diff --git a/docs/adr/016-organisation-monorepo.md b/docs/adr/016-organisation-monorepo.md new file mode 100644 index 0000000..1a30ce8 --- /dev/null +++ b/docs/adr/016-organisation-monorepo.md @@ -0,0 +1,87 @@ +# ADR-016 : Organisation en Monorepo + +**Statut** : Accepté +**Date** : 2025-01-24 + +## Contexte + +RoadWave comprend plusieurs composants (backend Go, mobile React Native/Flutter, documentation, tests BDD). Il faut décider de l'organisation des repositories : monorepo unique, multirepo avec submodules, ou repos totalement séparés. + +Les tests Gherkin existants décrivent des **fonctionnalités bout-en-bout** (front + mobile + back) et doivent rester cohérents entre tous les composants. + +## Décision + +**Monorepo unique** contenant backend, mobile, documentation et features Gherkin. + +## Alternatives considérées + +| Option | Cohérence | Complexité | Onboarding | Déploiements | +|--------|-----------|------------|------------|--------------| +| **Monorepo** | Excellente | Faible | Simple (1 clone) | Couplés | +| Multirepo + submodules | Moyenne | Élevée | Complexe | Indépendants | +| Multirepo séparés | Faible | Moyenne | Moyen | Indépendants | + +## Justification + +- **Source de vérité unique** : documentation et Gherkin partagés, pas de désynchronisation +- **Versionning cohérent** : un tag Git = release complète front + back compatible +- **Tests e2e simplifiés** : tout le code disponible localement pour tests intégrés +- **Expérience développeur** : un seul `git clone`, historique unifié, changements cross-stack facilités +- **Stade du projet** : début de projet où agilité et changements rapides sont critiques + +## Structure + +``` +roadwave/ +├── docs/ # Documentation commune (ADR, règles métier) +├── features/ # Tests BDD Gherkin partagés +│ ├── authentication/ +│ ├── navigation/ +│ └── ... +├── backend/ # Application Go +│ ├── cmd/ +│ ├── internal/ +│ ├── pkg/ +│ ├── migrations/ +│ ├── tests/ +│ │ └── bdd/ # Step definitions Go (API) +│ └── go.mod +├── mobile/ # Application React Native/Flutter +│ ├── src/ +│ ├── tests/ +│ │ └── bdd/ # Step definitions mobile (UI) +│ └── package.json / pubspec.yaml +├── shared/ # Code partagé (types, contrats API) +├── docker-compose.yml # Stack complète locale +└── Makefile # Commandes communes +``` + +## Gestion des tests Gherkin + +**Principe** : Les fichiers `.feature` restent **partagés** dans `/features`, mais chaque projet implémente ses **step definitions**. + +``` +features/authentication/inscription.feature → Scénario fonctionnel + +backend/tests/bdd/inscription_steps.go → Teste l'API +mobile/tests/bdd/inscription_steps.dart → Teste l'UI mobile +``` + +Cela garantit que : +- Les spécifications métier sont uniques et cohérentes +- Chaque couche teste sa responsabilité +- Les tests valident le contrat entre front et back + +## Outillage + +- **Turborepo** ou **Nx** : orchestration des builds/tests, cache intelligent +- **Docker Compose** : environnement de dev local (PostgreSQL, Redis, backend, etc.) +- **Make** : commandes communes (`make test`, `make build`, `make dev`) +- **CI/CD** : GitHub Actions avec path filters (rebuild seulement ce qui change) + +## Conséquences + +- **Positif** : cohérence maximale, onboarding simplifié, changements cross-stack facilités +- **Négatif** : repo plus volumineux, nécessite CI intelligente pour éviter builds lents +- **Migration future** : si le monorepo devient ingérable (>100k LOC), split possible mais improbable à moyen terme +- **Permissions** : tout le monde voit tout (transparence encouragée en phase startup) diff --git a/docs/adr/017-hebergement.md b/docs/adr/017-hebergement.md new file mode 100644 index 0000000..aecafb2 --- /dev/null +++ b/docs/adr/017-hebergement.md @@ -0,0 +1,59 @@ +# ADR-017 : Hébergement + +**Statut** : Accepté +**Date** : 2025-01-25 + +## Contexte + +RoadWave nécessite une infrastructure cloud hébergée dans l'UE (conformité RGPD) avec PostgreSQL+PostGIS, Redis et stockage objet. L'objectif est de minimiser les coûts en phase MVP tout en gardant une évolutivité vers des services managés. + +## Décision + +**OVH VPS** avec services self-hosted via Docker Compose pour le MVP. +Migration vers **Scaleway** (services managés) prévue en phase de croissance. + +## Alternatives considérées + +| Solution | Coût MVP/mois | Simplicité ops | PostGIS | Redis managé | RGPD | +|----------|---------------|----------------|---------|--------------|------| +| **OVH VPS** | ~14€ | Moyenne | Self-hosted | Self-hosted | Oui | +| Scaleway managé | ~67€ | Élevée | Oui | Oui | Oui | +| Clever Cloud | ~120€ | Très élevée | Oui | Oui | Oui | +| Infomaniak | ~50€ | Élevée | Non | Non | Suisse | + +## Justification + +- **Économie** : 5x moins cher que les services managés (~14€ vs ~67€) +- **Suffisant pour MVP** : VPS 4GB RAM supporte facilement 10-20K users +- **Docker Compose** : Stack reproductible, backups automatisables +- **RGPD** : Datacenters OVH en France +- **Migration simple** : Passage vers Scaleway managé sans refonte + +## Stack MVP + +``` +OVH VPS Essential (12.50€/mois) +├── Go + Fiber (app) +├── PostgreSQL 16 + PostGIS 3.4 (Docker) +├── Redis 7 (Docker) +├── NGINX Cache (distribution HLS) +└── Backups quotidiens (cron + pg_dump) + +OVH Object Storage (~1.20€/100GB) +└── Fichiers audio source +``` + +## Évolution prévue + +| Phase | Users | Infrastructure | Coût estimé | +|-------|-------|----------------|-------------| +| MVP | 0-20K | OVH VPS Essential | ~14€/mois | +| Croissance | 20-100K | Scaleway + PostgreSQL managé | ~100€/mois | +| Scale | 100K+ | Scaleway Kubernetes | ~500€/mois | + +## Conséquences + +- Backups PostgreSQL à automatiser (cron + pg_dump) +- Monitoring basique requis (Uptime Kuma ou alertes OVH) +- Sécurité serveur à configurer (UFW, Fail2ban) +- Migration vers services managés à planifier dès revenus stables diff --git a/docs/adr/018-service-emailing.md b/docs/adr/018-service-emailing.md new file mode 100644 index 0000000..0435718 --- /dev/null +++ b/docs/adr/018-service-emailing.md @@ -0,0 +1,62 @@ +# ADR-018 : Service d'Emailing Transactionnel + +**Statut** : Accepté +**Date** : 2026-01-26 + +## Contexte + +RoadWave nécessite un service d'envoi d'emails transactionnels pour notifications utilisateurs, modération (strikes, suspensions), authentification (Zitadel), conformité RGPD et communications créateurs. + +**Volume estimé MVP** : 2000-5000 emails/mois. + +**Contraintes** : Souveraineté préférée (France/UE), RGPD natif, coût maîtrisé, API simple pour Go, capacité SMS future (post-MVP). + +## Décision + +**Brevo (ex-Sendinblue)** comme service d'emailing transactionnel et SMS. + +## Alternatives considérées + +| Service | Localisation | Prix gratuit | Prix 100K emails/mois | SMS disponible | +|---------|--------------|--------------|----------------------|----------------| +| **Brevo** | 🇫🇷 France | 300 emails/jour (~9K/mois) | ~49€ | ✅ Oui | +| Scaleway TEM | 🇫🇷 France | 300 emails/mois | ~25€ | ❌ Non | +| Mailjet | 🇫🇷 France | 200 emails/jour (~6K/mois) | ~50-100€ | ❌ Non | +| Postal | 🏠 Self-hosted | Illimité (coût VPS) | ~20-50€ | ❌ Non | + +## Justification + +- **Plan gratuit généreux** : 300 emails/jour (9000/mois) vs 300/mois chez Scaleway → coût 0 en MVP +- **SMS natif** : Vérification anti-spam post-MVP (emails temporaires) +- **Souverain français** : Hébergé France/UE, RGPD natif +- **CRM intégré** : Gestion contacts, segmentation pour newsletters futures +- **Simplicité** : API REST standard, SDK disponibles, documentation complète + +## Architecture + +``` +Backend Go → Brevo API REST → Utilisateurs + ├── Emails transactionnels + ├── Templates HTML + ├── Webhooks (bounces, opens) + └── [Post-MVP] SMS +``` + +## Estimation coûts + +| Phase | Utilisateurs | Emails/mois | Coût Brevo | +|-------|--------------|-------------|------------| +| MVP | 0-10K | ~5K/jour | **Gratuit** (< 300/jour) | +| Growth | 10K-50K | ~10K/mois | Gratuit ou 19€/mois | +| Scale | 50K-100K | ~100K/mois | 49€/mois | + +**SMS** (post-MVP) : ~0.04€/SMS France, soit ~400€/mois pour 10K inscriptions/mois. + +## Conséquences + +- Coût 0 en MVP (9000 emails/mois gratuits) +- SMS intégré pour vérification anti-spam (post-MVP) +- API REST simple, pas de SDK Go officiel +- Limites quotidiennes strictes (300/jour en gratuit) +- Migration possible vers Scaleway TEM si volume >100K emails/mois (coût optimisé) +- Configuration DNS requise (SPF, DKIM, DMARC) diff --git a/docs/adr/019-notifications-geolocalisees.md b/docs/adr/019-notifications-geolocalisees.md new file mode 100644 index 0000000..2f7d13e --- /dev/null +++ b/docs/adr/019-notifications-geolocalisees.md @@ -0,0 +1,144 @@ +# ADR-019 : Architecture des Notifications Géolocalisées + +**Statut** : Accepté +**Date** : 2026-01-28 +**Supersède** : Résout l'incohérence identifiée entre ADR-002 et Règle Métier 05 (Mode Piéton) + +## Contexte + +Le mode piéton exige des notifications push en temps réel lorsque l'utilisateur approche d'un point d'intérêt (rayon de 200m), **même si l'application est fermée ou en arrière-plan**. + +ADR-002 spécifie HLS pour tout le streaming audio, mais HLS est un protocole unidirectionnel (serveur → client) qui ne permet pas au serveur d'envoyer des notifications push vers un client inactif. + +## Décision + +Architecture hybride en **2 phases** : + +### Phase 1 (MVP) : WebSocket + Firebase Cloud Messaging + +``` +[App Mobile] → [WebSocket] → [Backend Go] + ↓ + [PostGIS Worker] + ↓ + [Firebase FCM / APNS] + ↓ + [Push Notification] +``` + +**Fonctionnement** : +1. L'utilisateur ouvre l'app → connexion WebSocket établie +2. L'app envoie sa position GPS toutes les 30s via WebSocket +3. Un worker backend (goroutine) interroge PostGIS toutes les 30s : + ```sql + SELECT poi.*, users.fcm_token + FROM points_of_interest poi + JOIN user_locations users ON ST_DWithin( + poi.geom, + users.last_position, + 200 -- rayon en mètres + ) + WHERE users.notifications_enabled = true + AND users.last_update > NOW() - INTERVAL '5 minutes' + ``` +4. Si proximité détectée → envoi de push notification via Firebase (Android) ou APNS (iOS) +5. Utilisateur clique → app s'ouvre → HLS démarre l'audio (ADR-002) + +**Limitations MVP** : +- Fonctionne uniquement si l'utilisateur a envoyé sa position < 5 minutes +- En voiture rapide (>80 km/h), possible de "manquer" un POI si position pas mise à jour + +### Phase 2 (Post-MVP) : Ajout du Geofencing Local + +``` +[Mode Connecté] → WebSocket + Push serveur (Phase 1) +[Mode Offline] → Geofencing natif iOS/Android +[Mode Économie] → Geofencing natif (batterie < 20%) +``` + +**Fonctionnement additionnel** : +1. Quand l'utilisateur télécharge du contenu pour mode offline → synchronisation des POI proches (rayon 10 km) +2. Configuration de **geofences locales** sur iOS/Android (limite : 20 sur iOS, 100 sur Android) +3. Sélection intelligente des 20 POI les plus pertinents selon les jauges d'intérêt +4. Système d'exploitation surveille les geofences même app fermée +5. Entrée dans geofence → notification locale (pas de serveur) + +## Alternatives considérées + +| Option | Fonctionne offline | Batterie | Complexité | Limite POI | Précision | +|--------|-------------------|----------|------------|------------|-----------| +| **WebSocket + FCM (Phase 1)** | ❌ Non | ⭐ Optimale | ⭐ Faible | ∞ | ⭐⭐ Bonne | +| Geofencing local seul | ⭐ Oui | ⚠️ Élevée | ⚠️ Moyenne | 20 (iOS) | ⭐⭐⭐ Excellente | +| Polling GPS continu | ⭐ Oui | ❌ Critique | ⭐ Faible | ∞ | ⭐⭐⭐ Excellente | +| **Hybride (Phase 1+2)** | ⭐ Oui | ⭐ Adaptative | ⚠️ Moyenne | ∞/20 | ⭐⭐⭐ Excellente | + +## Justification + +### Pourquoi WebSocket et pas HTTP long-polling ? + +- **Efficacité** : 1 connexion TCP vs multiples requêtes HTTP +- **Batterie** : Connexion persistante optimisée par l'OS mobile +- **Bi-directionnel** : Backend peut envoyer des mises à jour instantanées (ex: "nouveau POI créé par un créateur que tu suis") + +### Pourquoi Firebase FCM et pas implémentation custom ? + +- **Gratuit** : 10M notifications/mois (largement suffisant jusqu'à 100K utilisateurs) +- **Fiabilité** : Infrastructure Google avec 99.95% uptime +- **Batterie** : Utilise les mécanismes système (Google Play Services) +- **Cross-platform** : API unifiée iOS/Android + +### Pourquoi limiter le geofencing local à Phase 2 ? + +- **Complexité** : Permissions "Always Location" difficiles à obtenir (taux d'acceptation ~30%) +- **ROI** : 80% des utilisateurs auront un réseau mobile disponible +- **Priorité** : Livrer le MVP rapidement avec la solution serveur + +## Conséquences + +### Positives + +- ✅ Notifications temps réel en mode piéton (< 1 minute de latence) +- ✅ Fonctionne avec HLS pour l'audio (pas de conflit avec ADR-002) +- ✅ Scalable : Worker backend peut gérer 10K utilisateurs/seconde avec PostGIS indexé +- ✅ Mode offline disponible en Phase 2 sans refonte + +### Négatives + +- ❌ Dépendance à Firebase (vendor lock-in) - mitigée par l'utilisation de l'interface FCM standard +- ❌ WebSocket nécessite maintien de connexion (charge serveur +10-20%) +- ❌ Mode offline non disponible au MVP (déception possible des early adopters) + +### Impact sur les autres ADR + +- **ADR-002 (Streaming)** : Aucun conflit - HLS reste pour l'audio +- **ADR-005 (Base de données)** : Ajouter index PostGIS `GIST (geom)` sur `points_of_interest` +- **ADR-012 (Architecture Backend)** : Ajouter un module `geofencing` avec worker dédié +- **ADR-014 (Frontend Mobile)** : Intégrer `firebase_messaging` (Flutter) et gérer permissions + +## Métriques de Succès + +- Latence notification < 60s après entrée dans rayon 200m +- Taux de livraison > 95% (hors utilisateurs avec notifications désactivées) +- Consommation batterie < 5% / heure en mode piéton +- Coût serveur < 0.01€ / utilisateur / mois + +## Migration et Rollout + +### Phase 1 (MVP - Sprint 3-4) +1. Backend : Implémenter WebSocket endpoint `/ws/location` +2. Backend : Worker PostGIS avec requête ST_DWithin +3. Mobile : Intégrer Firebase SDK + gestion FCM token +4. Test : Validation en conditions réelles (Paris, 10 testeurs) + +### Phase 2 (Post-MVP - Sprint 8-10) +1. Mobile : Implémenter geofencing avec `flutter_background_geolocation` +2. Backend : API `/sync/nearby-pois?lat=X&lon=Y&radius=10km` +3. Mobile : Algorithme de sélection des 20 POI prioritaires +4. Test : Validation mode avion (offline complet) + +## Références + +- [Firebase Cloud Messaging Documentation](https://firebase.google.com/docs/cloud-messaging) +- [PostGIS ST_DWithin Performance](https://postgis.net/docs/ST_DWithin.html) +- [iOS Geofencing Best Practices](https://developer.apple.com/documentation/corelocation/monitoring_the_user_s_proximity_to_geographic_regions) +- Règle Métier 05 : Section 5.1.2 (Mode Piéton, lignes 86-120) diff --git a/docs/architecture/sequences/cache-geospatial.md b/docs/architecture/sequences/cache-geospatial.md new file mode 100644 index 0000000..c1abf1a --- /dev/null +++ b/docs/architecture/sequences/cache-geospatial.md @@ -0,0 +1,319 @@ +# Diagramme de Séquence : Cache Géospatial Redis + +> Architecture du cache Redis Geospatial pour l'optimisation des requêtes de découverte de contenu géolocalisé. + +## Vue d'ensemble + +Le cache Redis Geospatial permet d'accélérer la recherche de contenus audio à proximité d'une position GPS en évitant des calculs PostGIS coûteux sur PostgreSQL à chaque requête. + +**Performance** : +- Sans cache : ~200-500ms (calcul PostGIS sur 100K points) +- Avec cache : ~5-10ms (filtrage Redis en mémoire) + +--- + +## Flux complet : Cold Start → Warm Cache + +```mermaid +sequenceDiagram + participant User as 📱 Utilisateur
(Paris) + participant Backend as 🔧 Backend Go + participant Redis as 🔴 Redis Geospatial
(Cache) + participant PostgreSQL as 🗄️ PostgreSQL
+ PostGIS + participant CDN as 🌐 NGINX Cache (OVH VPS) + + Note over User,CDN: 🥶 Cold Start - Cache vide + + User->>Backend: GET /contents?lat=48.8566&lon=2.3522&radius=50km + Backend->>Redis: EXISTS geo:catalog + Redis-->>Backend: false (cache vide) + + Backend->>PostgreSQL: SELECT id, lat, lon, title, geo_level
FROM contents WHERE active=true + Note over PostgreSQL: Tous les contenus actifs
de la plateforme (métadonnées) + PostgreSQL-->>Backend: 100K contenus (métadonnées) + + Backend->>Redis: GEOADD geo:catalog
lon1 lat1 "content:1"
lon2 lat2 "content:2"
... (100K entrées) + Note over Redis: Stockage index spatial
en mémoire (~20 MB) + Redis-->>Backend: OK (100000) + + Backend->>Redis: EXPIRE geo:catalog 300 + Note over Redis: TTL 5 minutes + + Backend->>Redis: GEORADIUS geo:catalog
2.3522 48.8566 50 km + Note over Redis: Filtrage spatial instantané
(index geohash) + Redis-->>Backend: [content:123, content:456, ...]
(~500 IDs dans rayon) + + Backend->>PostgreSQL: SELECT * FROM contents
WHERE id IN (123, 456, ...)
AND geo_level = 'gps_precise' + Note over PostgreSQL: Récupération détails complets
uniquement contenus proches + PostgreSQL-->>Backend: Détails complets (500 contenus GPS) + + Backend->>PostgreSQL: SELECT * FROM contents
WHERE city='Paris' OR dept='75'
OR region='IDF' OR geo_level='national' + Note over PostgreSQL: Contenus par niveau géographique + PostgreSQL-->>Backend: Contenus ville/région/national + + Backend->>Backend: Scoring & mixage :
- GPS proche : 70%
- Ville : 20%
- Région : 8%
- National : 2% + + Backend-->>User: JSON: [{id, title, creator, audioUrl, score}, ...]
(playlist mixée et scorée) + + Note over User,CDN: 🎵 Lecture audio (requêtes séparées) + + User->>CDN: GET /audio/content-123.m3u8 + CDN-->>User: Playlist HLS + + User->>CDN: GET /audio/content-123-segment-001.ts + CDN-->>User: Segment audio Opus + + Note over User,CDN: 🔥 Warm Cache - Utilisateur 2 à Lyon (45km+) + + participant User2 as 📱 Utilisateur 2
(Lyon) + + User2->>Backend: GET /contents?lat=45.7640&lon=4.8357&radius=50km + Backend->>Redis: EXISTS geo:catalog + Redis-->>Backend: true ✅ (cache chaud) + + Backend->>Redis: GEORADIUS geo:catalog
4.8357 45.7640 50 km + Note over Redis: Filtrage instantané
sur cache existant + Redis-->>Backend: [content:789, content:012, ...]
(~300 IDs différents) + + Backend->>PostgreSQL: SELECT * FROM contents
WHERE id IN (789, 012, ...)
AND geo_level = 'gps_precise' + PostgreSQL-->>Backend: Détails complets + + Backend->>PostgreSQL: SELECT * FROM contents
WHERE city='Lyon' OR dept='69'
OR region='Auvergne-RA' OR geo_level='national' + PostgreSQL-->>Backend: Contenus ville/région/national + + Backend->>Backend: Scoring & mixage + + Backend-->>User2: JSON: [{id, title, creator, audioUrl, score}, ...] +``` + +--- + +## Stratégie de cache + +### Cache du catalogue complet (approche choisie) + +**Principe** : Au premier cache miss, charger **TOUS** les contenus géolocalisés en une seule fois dans Redis. + +**Avantages** : +- ✅ 1 seul cache miss au démarrage de l'instance +- ✅ Toutes les requêtes suivantes servies par Redis (n'importe quelle position GPS) +- ✅ Simple à gérer (1 seule clé Redis : `geo:catalog`) +- ✅ Pas de duplication de données + +**Inconvénients** : +- ⚠️ Premier utilisateur subit le cold start (~500ms-1s) +- ⚠️ Nécessite charger toute la base (acceptable : ~20 MB pour 100K contenus) + +**Alternatives non retenues** : +- Cache par zone géographique → cache miss fréquents, complexité gestion chevauchements +- Cache à la demande → trop de cache miss, pas d'optimisation réelle + +--- + +## Détails techniques + +### 1. Données stockées dans Redis + +**Clé Redis** : `geo:catalog` + +**Structure** : +``` +GEOADD geo:catalog + 2.3522 48.8566 "content:12345" # lon, lat, member + 4.8357 45.7640 "content:67890" + ... +``` + +**Taille mémoire** : +- ~200 bytes par entrée (ID + coordonnées + index geohash) +- 100K contenus = ~20 MB +- Négligeable pour Redis (plusieurs GB RAM disponibles) + +**TTL** : 5 minutes (300 secondes) +- Le contenu géolocalisé est quasi-statique (change peu) +- Rechargement automatique toutes les 5 minutes si cache expiré +- Permet de propager les nouveaux contenus rapidement + +### 2. Niveaux géographiques + +Le cache Redis ne contient que les contenus avec **GPS précis** (`geo_level = 'gps_precise'`). + +Les autres niveaux géographiques sont gérés par filtrage applicatif : + +| Niveau | Stockage | Requête | +|--------|----------|---------| +| **GPS précis** | Redis + PostgreSQL | `GEORADIUS` puis `SELECT WHERE id IN (...)` | +| **Ville** | PostgreSQL uniquement | `SELECT WHERE city = ?` | +| **Département** | PostgreSQL uniquement | `SELECT WHERE department = ?` | +| **Région** | PostgreSQL uniquement | `SELECT WHERE region = ?` | +| **National** | PostgreSQL uniquement | `SELECT WHERE geo_level = 'national'` | + +### 3. Commandes Redis utilisées + +```bash +# Vérifier existence du cache +EXISTS geo:catalog +# Retour : 0 (n'existe pas) ou 1 (existe) + +# Charger le catalogue complet (cold start) +GEOADD geo:catalog 2.3522 48.8566 "content:1" 4.8357 45.7640 "content:2" ... +# Retour : nombre d'éléments ajoutés + +# Définir TTL 5 minutes +EXPIRE geo:catalog 300 + +# Rechercher contenus dans un rayon +GEORADIUS geo:catalog 2.3522 48.8566 50 km +# Retour : ["content:123", "content:456", ...] + +# Optionnel : obtenir distance et coordonnées +GEORADIUS geo:catalog 2.3522 48.8566 50 km WITHDIST WITHCOORD +# Retour : [["content:123", "12.5", ["2.35", "48.85"]], ...] +``` + +### 4. Algorithme de scoring + +Le backend mixe les résultats selon une pondération : + +``` +Score final = + (Pertinence GPS × 0.70) + + (Pertinence Ville × 0.20) + + (Pertinence Région × 0.08) + + (Pertinence National × 0.02) +``` + +**Critères de pertinence** : +- **GPS** : Plus proche = score élevé (distance inversée) +- **Ville/Région** : Matching exact = score maximal +- **National** : Score fixe faible (contenu générique) + +**Mixage playlist** : +1. Trier tous les contenus par score décroissant +2. Appliquer diversité créateurs (pas 3 contenus du même créateur d'affilée) +3. Injecter contenus sponsorisés/mis en avant (futurs) +4. Retourner top 50 pour la session d'écoute + +--- + +## Métriques de performance + +### Temps de réponse typiques + +| Scénario | Latence | Détail | +|----------|---------|--------| +| **Cold start** | 500-1000ms | Chargement 100K contenus dans Redis + requête | +| **Warm cache** | 5-10ms | `GEORADIUS` + `SELECT WHERE id IN (...)` | +| **TTL expiré** | 500-1000ms | Rechargement automatique | + +### Charge serveurs + +| Composant | Sans cache | Avec cache | +|-----------|------------|------------| +| **PostgreSQL CPU** | 60-80% | 10-20% | +| **Redis CPU** | N/A | 5-15% | +| **Throughput** | ~50 req/s | ~500 req/s | + +--- + +## Cas limites et optimisations futures + +### Cas limite 1 : Contenu très dense (Paris intra-muros) + +**Problème** : 10K contenus dans rayon 5km → trop de résultats + +**Solution actuelle** : +- Limiter résultats Redis à 1000 premiers (tri par distance) +- Scorer et filtrer côté application + +**Optimisation future** : +- Ajuster rayon dynamiquement selon densité +- Utiliser `GEORADIUS ... COUNT 500` pour limiter côté Redis + +### Cas limite 2 : Zones rurales (peu de contenu) + +**Problème** : Rayon 50km retourne <10 contenus + +**Solution actuelle** : +- Augmenter poids contenus région/national dans le scoring +- Suggérer contenus nationaux populaires + +**Optimisation future** : +- Augmenter rayon automatiquement jusqu'à obtenir min 20 contenus +- `GEORADIUS ... 100 km` si rayon initial insuffisant + +### Cas limite 3 : Nombreux créateurs actifs (évolutivité) + +**Problème** : Cache 100K → 1M contenus (200 MB Redis) + +**Solution actuelle** : +- 200 MB reste acceptable pour Redis + +**Optimisation future** : +- Sharding géographique : cache Europe, cache USA, etc. +- Limiter cache aux contenus actifs 90 derniers jours + +--- + +## Invalidation du cache + +### Stratégies d'invalidation + +| Événement | Action cache | Détail | +|-----------|--------------|--------| +| **Nouveau contenu publié** | Lazy (TTL) | Visible sous 5 minutes max | +| **Contenu supprimé/modéré** | Lazy (TTL) | Disparaît sous 5 minutes max | +| **Mise à jour GPS contenu** | Lazy (TTL) | Nouvelle position sous 5 minutes | +| **Déploiement backend** | Flush volontaire | `DEL geo:catalog` si schema change | + +**Pas d'invalidation immédiate** pour simplifier l'architecture (cohérence éventuelle acceptable). + +**Alternative future** : +- Pub/Sub Redis : notifier toutes les instances backend lors d'un changement +- `GEOADD geo:catalog 2.35 48.85 "content:new"` pour ajout immédiat + +--- + +## Schéma simplifié + +``` +┌─────────────────────────────────────────────────────────┐ +│ Utilisateur │ +│ (Position GPS actuelle) │ +└────────────────────────┬────────────────────────────────┘ + │ + │ GET /contents?lat=X&lon=Y&radius=Z + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Backend Go (Fiber) │ +│ │ +│ 1. Vérifier cache Redis │ +│ ├─ Cache HIT → GEORADIUS rapide │ +│ └─ Cache MISS → Charger catalogue complet │ +│ │ +│ 2. Filtrer contenus GPS proches (Redis) │ +│ 3. Récupérer contenus ville/région/national (PG) │ +│ 4. Scorer et mixer selon pondération │ +│ 5. Retourner playlist │ +└────────────┬───────────────────────────┬────────────────┘ + │ │ + │ GEORADIUS │ SELECT détails + ▼ ▼ +┌─────────────────────┐ ┌───────────────────────────┐ +│ Redis Geospatial │ │ PostgreSQL + PostGIS │ +│ (Index spatial) │ │ (Données complètes) │ +│ │ │ │ +│ • geo:catalog │ │ • contents (détails) │ +│ • TTL 5 min │ │ • users │ +│ • ~20 MB mémoire │ │ • playlists │ +└─────────────────────┘ └───────────────────────────┘ +``` + +--- + +## Références + +- [ADR-005 : Base de données](../../adr/005-base-de-donnees.md) +- [Redis Geospatial Commands](https://redis.io/docs/data-types/geospatial/) +- [PostGIS Documentation](https://postgis.net/documentation/) +- [Règles métier : Découverte de contenu géolocalisé](../../regles-metier/03-decouverte-contenu.md) diff --git a/docs/index.md b/docs/index.md new file mode 120000 index 0000000..32d46ee --- /dev/null +++ b/docs/index.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/docs/legal/README.md b/docs/legal/README.md new file mode 100644 index 0000000..b4b8ffe --- /dev/null +++ b/docs/legal/README.md @@ -0,0 +1,116 @@ +# Documents juridiques RoadWave + +> **IMPORTANT** : Ces documents sont des **structures types** basées sur les règles métier validées. Ils doivent être **obligatoirement relus et validés par un avocat** avant publication. + +--- + +## Documents requis + +| Document | Fichier | Obligation légale | Révision avocat | +|----------|---------|-------------------|-----------------| +| CGU - Conditions Générales d'Utilisation | [cgu.md](cgu.md) | DSA + Code conso | Requis | +| Politique de confidentialité | [politique-confidentialite.md](politique-confidentialite.md) | RGPD | Requis | +| Charte créateur | [charte-createur.md](charte-createur.md) | DSA (transparence) | Requis | +| CGV Premium | [cgv-premium.md](cgv-premium.md) | Code conso | Requis | +| CGV Publicités | [cgv-publicites.md](cgv-publicites.md) | Code conso | Requis | +| Mentions légales | [mentions-legales.md](mentions-legales.md) | Obligatoire | Requis | + +--- + +## Moments de présentation + +### À l'inscription (consentement explicite requis) + +``` +┌─────────────────────────────────────────┐ +│ Bienvenue sur RoadWave │ +├─────────────────────────────────────────┤ +│ Email : [________] │ +│ Mot de passe : [________] │ +│ │ +│ ☐ J'accepte les CGU et la Politique │ +│ de confidentialité │ +│ [Lire les CGU] [Lire la Politique] │ +│ │ +│ [Créer mon compte] │ +└─────────────────────────────────────────┘ +``` + +- **Checkbox obligatoire** : bloque création compte si non cochée +- **Liens cliquables** : CGU + Politique (ouverture modal ou nouvelle page) +- **Horodatage** : enregistrer date/heure acceptation (preuve RGPD) + +### Avant première publication (créateur) + +``` +┌─────────────────────────────────────────┐ +│ Devenir créateur RoadWave │ +├─────────────────────────────────────────┤ +│ En publiant du contenu, vous acceptez │ +│ la Charte Créateur. │ +│ │ +│ Points clés : │ +│ • Vos 3 premiers contenus seront │ +│ validés manuellement (24-48h) │ +│ • Contenus prohibés : haine, violence, │ +│ musique protégée, spam │ +│ • Système de strikes : 4 strikes = ban │ +│ │ +│ ☐ J'ai lu et j'accepte la Charte │ +│ Créateur │ +│ [Lire la Charte complète] │ +│ │ +│ [Publier mon premier contenu] │ +└─────────────────────────────────────────┘ +``` + +### Avant souscription Premium + +- Affichage CGV Premium +- Checkbox acceptation obligatoire +- Bouton "Souscrire" désactivé tant que non accepté + +### Avant création campagne publicitaire + +- Affichage CGV Publicités +- Checkbox acceptation obligatoire +- Validation manuelle obligatoire (24-48h) + +--- + +## Accessibilité permanente + +### Footer application/site web + +``` +RoadWave © 2026 +[CGU] | [Politique de confidentialité] | [Mentions légales] | [Charte créateur] | [Contact DPO] +``` + +### Menu utilisateur + +``` +Mon compte +├── Paramètres +├── Données personnelles +├── Documents légaux +│ ├── CGU +│ ├── Politique de confidentialité +│ ├── Charte créateur (si créateur) +│ └── CGV Premium (si abonné) +└── Déconnexion +``` + +--- + +## Rappel important + +Ces documents doivent être **obligatoirement relus et validés par un avocat spécialisé** (droit numérique, RGPD) avant publication. + +**Coût avocat estimé** : 1500-3000€ (révision + adaptation contexte) + +**Prochaines étapes** : +1. Révision avocat +2. Ajout informations manquantes (SIRET, adresse, etc.) +3. Intégration technique (pages web, checkboxes, horodatage) +4. Tests parcours utilisateur (inscription, première publication, souscription Premium) diff --git a/docs/legal/cgu.md b/docs/legal/cgu.md new file mode 100644 index 0000000..0c3e754 --- /dev/null +++ b/docs/legal/cgu.md @@ -0,0 +1,513 @@ +# CGU - Conditions Générales d'Utilisation + +**Version** : 1.0 +**Date d'effet** : [DATE DE LANCEMENT MVP] +**Dernière mise à jour** : [DATE] + +--- + +## Article 1 - Définitions + +- **Plateforme** : application mobile et site web RoadWave +- **Utilisateur** : toute personne utilisant la Plateforme (auditeur ou créateur) +- **Créateur** : utilisateur publiant du contenu audio +- **Auditeur** : utilisateur écoutant du contenu audio +- **Contenu** : tout fichier audio publié sur la Plateforme +- **Compte** : espace personnel de l'utilisateur +- **Strike** : sanction appliquée en cas de violation des CGU + +--- + +## Article 2 - Objet + +Les présentes Conditions Générales d'Utilisation (CGU) régissent l'utilisation de la plateforme RoadWave, réseau social audio géolocalisé. + +RoadWave permet aux utilisateurs : +- **Auditeurs** : écouter du contenu audio contextuel géolocalisé +- **Créateurs** : publier du contenu audio géolocalisé + +--- + +## Article 3 - Acceptation des CGU + +### 3.1 Consentement + +L'utilisation de la Plateforme implique l'acceptation pleine et entière des présentes CGU. + +### 3.2 Modification des CGU + +RoadWave se réserve le droit de modifier les CGU à tout moment. + +**Notification** : +- Email à tous les utilisateurs **14 jours avant** l'entrée en vigueur +- Notification in-app au lancement suivant la modification +- Version mise à jour disponible sur roadwave.fr/cgu + +**Refus des nouvelles CGU** : +- Utilisateur peut supprimer son compte dans les 14 jours +- Utilisation continue = acceptation tacite + +--- + +## Article 4 - Inscription et compte utilisateur + +### 4.1 Conditions d'inscription + +- **Âge minimum** : 13 ans (conformément au RGPD) +- **Mineurs 13-15 ans** : autorisation parentale requise +- **Email valide** : requis pour récupération compte +- **Mot de passe sécurisé** : minimum 8 caractères, lettres + chiffres + +### 4.2 Vérification email + +- **Auditeurs** : optionnelle, mais limite 5 contenus écoutés avant vérification +- **Créateurs** : **obligatoire** pour publier du contenu +- Lien de vérification expire après 7 jours + +### 4.3 Responsabilité du compte + +L'utilisateur est responsable de : +- La confidentialité de ses identifiants +- Toutes actions effectuées depuis son compte +- Signalement immédiat en cas d'accès non autorisé + +### 4.4 Un compte par personne + +Création de comptes multiples interdite, sauf : +- Compte test (développeurs autorisés) +- Demande explicite auprès de RoadWave + +--- + +## Article 5 - Utilisation de la Plateforme + +### 5.1 Licence d'utilisation + +RoadWave accorde une licence **non exclusive, révocable, non transférable** pour : +- Accéder à la Plateforme +- Écouter du contenu +- Publier du contenu (créateurs) + +### 5.2 Interdictions générales + +Il est interdit de : +- Utiliser la Plateforme à des fins illégales +- Tenter de contourner les mesures de sécurité +- Utiliser des bots, scripts ou outils automatisés +- Revendre l'accès à la Plateforme +- Extraire massivement des données (scraping) +- Usurper l'identité d'un tiers + +### 5.3 Géolocalisation + +- **Consentement requis** : activation GPS optionnelle +- **Mode dégradé** : utilisation possible sans géolocalisation précise (contenus nationaux uniquement) +- **Révocation** : désactivation GPS possible à tout moment + +--- + +## Article 6 - Publication de contenu (créateurs) + +### 6.1 Règles générales + +Voir **[Charte Créateur](charte-createur.md)** pour règles détaillées. + +**Résumé** : +- Contenus originaux ou droits acquis +- Pas de contenu haineux, violent, illégal +- Pas de musique protégée >30 secondes sans licence +- Métadonnées obligatoires : titre, zone diffusion, tags, classification âge + +### 6.2 Validation des 3 premiers contenus + +- Nouveaux créateurs : validation manuelle sous **24-48h** (jours ouvrés) +- Après 3 validations : statut "Vérifié" → publication immédiate + +### 6.3 Propriété intellectuelle + +- **Créateur conserve** tous les droits sur ses contenus +- **Licence accordée à RoadWave** : diffusion, hébergement, transcoding (licence non exclusive) +- **Suppression** : créateur peut supprimer à tout moment (irréversible) + +### 6.4 Responsabilité du créateur + +Le créateur garantit : +- Détenir tous les droits nécessaires +- Ne pas violer de droits tiers (musique, marques, image) +- Respecter les classifications d'âge +- Exactitude des métadonnées (zone géographique, tags) + +--- + +## Article 7 - Contenus prohibés + +### 7.1 Liste exhaustive + +Sont strictement interdits : + +#### **Haine et violence** (Article 7.1.1) +- Incitation à la haine raciale, ethnique, religieuse +- Discrimination sexiste, homophobe, transphobe +- Apologie de crimes contre l'humanité +- Menaces de violence physique + +#### **Contenu sexuel** (Article 7.1.2) +- Pornographie ou contenu sexuellement explicite +- Contenu impliquant des mineurs (strictement interdit) +- Sollicitation sexuelle + +#### **Illégalité** (Article 7.1.3) +- Apologie du terrorisme +- Incitation aux actes criminels +- Vente de produits illégaux (drogues, armes) +- Pédopornographie → **ban immédiat + signalement autorités** + +#### **Droits d'auteur** (Article 7.1.4) +- Musique protégée en intégrale ou extraits >30 secondes +- Films, séries, livres audio protégés +- Diffusion de concerts, événements sportifs payants (lives) +- **Exception** : extraits ≤30s pour critique/analyse (fair use) + +#### **Désinformation dangereuse** (Article 7.1.5) +- Fausses informations sur la santé (COVID, vaccins, traitements) +- Fausses informations sur la sécurité routière +- Manipulation électorale + +#### **Harcèlement** (Article 7.1.6) +- Menaces répétées envers une personne +- Doxxing (publication informations privées) +- Intimidation, chantage + +#### **Fraude** (Article 7.1.7) +- Arnaques, escroqueries +- Pyramides de Ponzi, MLM illégaux +- Phishing, vol d'identité + +#### **Spam** (Article 7.1.8) +- Publicité non autorisée (hors système pub RoadWave) +- Répétition de contenus identiques +- Liens vers sites externes de spam + +### 7.2 Modération + +**Délais de traitement** : +- CRITIQUE (violence, suicide) : <2h (24/7) +- HAUTE (haine, harcèlement) : <24h +- MOYENNE (spam) : <48h +- BASSE (qualité audio) : <72h + +--- + +## Article 8 - Système de strikes et sanctions + +### 8.1 Grille de sanctions + +| Strike | Sanction | Durée suspension upload | +|--------|----------|-------------------------| +| **Strike 1** | Avertissement + suppression contenu | 3 jours | +| **Strike 2** | Suppression contenu | 7 jours | +| **Strike 3** | Suppression contenu | 30 jours | +| **Strike 4** | **Ban définitif** compte créateur | Permanent | + +### 8.2 Exceptions + +**Tolérance 1ère fois (droits d'auteur uniquement)** : +- Avertissement sans strike si première violation musique protégée +- Explication pédagogique + lien vers règles + +**Violations graves (pas de tolérance)** : +- Haine, violence → Strike 1 immédiat +- Contenu illégal (terrorisme, pédopornographie) → Strike 4 (ban) + signalement autorités + +### 8.3 Strikes immédiats (radio live) + +| Violation | Sanction | +|-----------|----------| +| Concert/événement sportif payant | Strike 2 immédiat (7 jours) | +| Contenu violent en direct | Strike 3 immédiat (30 jours) | +| Contenu illégal en direct | Strike 4 (ban) + signalement | + +### 8.4 Réhabilitation + +- **-1 strike automatique** tous les **6 mois** sans nouvelle violation +- Minimum : 0 strikes (pas de valeur négative) +- Avertissement (sans strike) ne compte pas pour réhabilitation + +**Exemple** : +``` +Créateur a Strike 2 +→ 6 mois sans incident → Strike 1 +→ 6 mois sans incident → Compte propre (0 strike) +``` + +### 8.5 Notification des sanctions + +**Multi-canal** (email + push + in-app) : +- Catégorie violée (référence article CGU) +- Raison détaillée (langage clair) +- Extrait audio concerné (timestamp) +- Transcription du passage problématique +- Strike actuel (ex: "Strike 2/4") +- Lien vers processus d'appel + +--- + +## Article 9 - Processus d'appel + +### 9.1 Droit de contestation + +Tout utilisateur sanctionné peut contester la décision. + +### 9.2 Délai + +- **7 jours** après notification de sanction +- Après 7 jours : appel automatiquement refusé + +### 9.3 Procédure + +1. Bouton "Contester cette décision" dans notification +2. Formulaire d'appel (raison + arguments + preuves optionnelles) +3. Numéro de ticket unique (ex: `#MOD-2026-00142`) +4. Email confirmation : "Votre appel sera traité sous 72h" + +### 9.4 Traitement + +- Délai : **72h maximum** (3 jours ouvrés) +- Examen par modérateur senior +- Décision finale (maintien / annulation / réduction sanction) + +### 9.5 Décision définitive + +- Une seule contestation par sanction +- Décision d'appel = définitive (pas de second appel) + +--- + +## Article 10 - Publicités + +### 10.1 Publicités RoadWave (auditeurs gratuits) + +- Fréquence : **1 publicité / 5 contenus** +- Durée : 10-60 secondes +- Skippable après **5 secondes** +- Pas de publicités pour utilisateurs Premium + +### 10.2 Publicités créateurs (annonceurs) + +Voir **[CGV Publicités](cgv-publicites.md)** + +**Résumé** : +- Interface self-service +- Budget minimum : 50€ +- Validation manuelle obligatoire (24-48h) +- Facturation : écoute complète 0.05€, skip après 5s = 0.02€ + +--- + +## Article 11 - Abonnement Premium + +Voir **[CGV Premium](cgv-premium.md)** + +**Résumé** : +- Prix : 4.99€/mois OU 49.99€/an +- Avantages : 0 pub, contenus exclusifs 👑, qualité 64 kbps, offline illimité +- Résiliation : à tout moment (effet fin période en cours) + +--- + +## Article 12 - Monétisation créateurs + +### 12.1 Conditions d'éligibilité + +- Compte ≥3 mois +- ≥500 abonnés +- ≥10 000 écoutes totales +- 0 strike actif +- ≥5 contenus publiés dans les 90 derniers jours + +### 12.2 KYC (Know Your Customer) + +Vérification obligatoire via Mangopay : +- SIRET, numéro TVA, RIB professionnel +- Pièce d'identité +- Kbis <3 mois (entreprises) + +### 12.3 Revenus + +**Publicités** : +- 3€ / 1000 écoutes complètes +- Représente ~6% du CA publicitaire + +**Premium** : +- 70% créateur, 30% plateforme +- Proportionnel au temps d'écoute + +### 12.4 Paiement + +- Seuil minimum : **50€** +- Fréquence : mensuelle (15 du mois suivant) +- Virement SEPA via Mangopay + +--- + +## Article 13 - Données personnelles + +Voir **[Politique de confidentialité](politique-confidentialite.md)** pour détails complets. + +### 13.1 Données collectées + +- Position GPS (avec consentement explicite) +- Historique d'écoute +- Centres d'intérêt (jauges) +- Identité créateur (KYC si monétisation) + +### 13.2 Droits RGPD + +- **Accès** : consulter toutes ses données +- **Rectification** : modifier ses informations +- **Suppression** : supprimer son compte (grace period 30 jours) +- **Portabilité** : export JSON + HTML + audio +- **Opposition** : désactiver profilage publicitaire + +### 13.3 Contact DPO + +- Email : dpo@roadwave.fr +- Délai réponse : **1 mois** (RGPD) + +--- + +## Article 14 - Propriété intellectuelle + +### 14.1 Propriété de la Plateforme + +RoadWave et tous ses éléments (code, design, logo, marque) sont protégés par : +- Droit d'auteur +- Droit des marques +- Droit sui generis des bases de données + +### 14.2 Propriété des contenus + +- **Créateur conserve** tous les droits sur ses contenus +- **Licence accordée à RoadWave** : + - Non exclusive + - Mondiale + - Gratuite + - Révocable (suppression contenu par créateur) + - Incluant : hébergement, diffusion, transcoding, transcription IA + +### 14.3 Utilisation des contenus par RoadWave + +RoadWave peut : +- Diffuser les contenus aux auditeurs +- Transcrire automatiquement (modération IA) +- Générer segments HLS (streaming) +- Promouvoir la Plateforme (captures d'écran, extraits marketing) + +RoadWave **ne peut pas** : +- Revendre les contenus à des tiers +- Modifier substantiellement les contenus (hors transcoding technique) + +--- + +## Article 15 - Résiliation + +### 15.1 Résiliation par l'utilisateur + +- **Suppression compte** : à tout moment depuis paramètres +- **Grace period** : 30 jours (récupération possible) +- **Après 30 jours** : suppression définitive + +### 15.2 Résiliation par RoadWave + +RoadWave peut suspendre ou supprimer un compte en cas de : +- Violation grave des CGU +- Strike 4 (ban définitif) +- Activité frauduleuse +- Compte inactif >24 mois (après notification) + +### 15.3 Conséquences de la résiliation + +**Contenus créateurs** : +- Dépubliés immédiatement +- Marqués "Utilisateur supprimé" (anonymisation) +- Fichiers audio supprimés sous 48h + +**Abonnement Premium** : +- Pas de remboursement au prorata +- Accès jusqu'à fin période payée + +**Revenus créateurs** : +- Solde <50€ → perdu +- Solde ≥50€ → virement final sous 30 jours + +--- + +## Article 16 - Limitation de responsabilité + +### 16.1 Disponibilité de la Plateforme + +RoadWave s'efforce d'assurer une disponibilité 24/7 mais : +- **Aucune garantie** de disponibilité continue +- Maintenances programmées notifiées 48h avant +- Interruptions d'urgence possibles sans préavis + +### 16.2 Contenus utilisateurs + +RoadWave n'est **pas responsable** des contenus publiés par les créateurs. + +**Modération** : +- Validation 3 premiers contenus (nouveaux créateurs) +- Modération a posteriori (créateurs vérifiés) +- Réactivité <24-48h après signalement + +### 16.3 Limitation de dommages + +RoadWave ne peut être tenu responsable de : +- Perte de données (sauvegardes régulières recommandées) +- Perte de revenus créateurs (indisponibilité temporaire) +- Dommages indirects ou consécutifs + +**Plafond** : +- Responsabilité limitée aux **12 derniers mois d'abonnement Premium** (auditeurs) +- Responsabilité limitée aux **revenus perçus sur 12 derniers mois** (créateurs) + +--- + +## Article 17 - Droit applicable et juridiction + +### 17.1 Droit applicable + +Les présentes CGU sont régies par le **droit français**. + +### 17.2 Médiation + +Avant toute action judiciaire, l'utilisateur doit tenter une **médiation** : +- Médiateur de la consommation : [NOM MÉDIATEUR] +- Plateforme européenne ODR : https://ec.europa.eu/consumers/odr + +### 17.3 Juridiction + +En cas d'échec de la médiation, les tribunaux français sont compétents. + +--- + +## Article 18 - Dispositions diverses + +### 18.1 Intégralité + +Les CGU constituent l'intégralité de l'accord entre RoadWave et l'utilisateur. + +### 18.2 Nullité partielle + +Si une clause est jugée invalide, les autres clauses restent applicables. + +### 18.3 Non-renonciation + +L'absence d'exercice d'un droit par RoadWave ne constitue pas une renonciation. + +--- + +**Contact** : +- Email : support@roadwave.fr +- Adresse : [ADRESSE SIÈGE SOCIAL] +- SIRET : [SIRET] diff --git a/docs/legal/cgv-premium.md b/docs/legal/cgv-premium.md new file mode 100644 index 0000000..9472a46 --- /dev/null +++ b/docs/legal/cgv-premium.md @@ -0,0 +1,181 @@ +# CGV Premium + +**Version** : 1.0 +**Date d'effet** : [DATE] + +--- + +## 1. Objet + +Conditions spécifiques à l'abonnement **RoadWave Premium**. + +Complète les **[CGU](cgu.md)** (Article 11). + +--- + +## 2. Offre Premium + +### 2.1 Tarifs + +| Formule | Prix | Économie | +|---------|------|----------| +| **Mensuel** | 4.99€/mois | - | +| **Annuel** | 49.99€/an | ~17% (4.16€/mois effectif) | + +**Tarif IAP (In-App Purchase iOS/Android)** : +- Mensuel : 5.99€/mois (+20% commission Apple/Google) +- Annuel : 59.99€/an + +### 2.2 Avantages + +- **0 publicité** : aucune pub entre contenus +- **Contenus exclusifs** : accès contenus marqués (créateurs Premium) +- **Qualité audio supérieure** : 64 kbps Opus (vs 48 kbps gratuit) +- **Offline illimité** : téléchargement sans limite (vs 50 max gratuit) + +### 2.3 Pas d'essai gratuit (MVP) + +- Pas de trial 7 jours au MVP (peut être ajouté post-MVP) +- Facturation immédiate à la souscription + +### 2.4 Pas de partage familial (MVP) + +- 1 compte = 1 abonnement +- Partage familial : fonctionnalité post-MVP + +--- + +## 3. Souscription + +### 3.1 Modalités + +**Via web (Mangopay)** : +- Carte bancaire (Visa, Mastercard, Amex) +- Prélèvement SEPA (si mensuel) +- Paiement sécurisé 3D Secure + +**Via mobile (IAP)** : +- App Store (iOS) : Apple Pay, carte bancaire +- Google Play (Android) : Google Pay, carte bancaire + +### 3.2 Renouvellement automatique + +- **Mensuel** : renouvellement le même jour chaque mois +- **Annuel** : renouvellement 1 an après souscription + +**Notification avant renouvellement** : +- Email **7 jours avant** renouvellement +- Rappel : résiliation possible à tout moment + +--- + +## 4. Résiliation + +### 4.1 Par l'utilisateur + +- **À tout moment** : paramètres → "Gérer abonnement" → "Résilier" +- **Effet** : fin période en cours (pas de remboursement au prorata) +- **Accès** : conservé jusqu'à fin période payée + +**Exemple** : +``` +Souscription : 1er janvier (mensuel 4.99€) +Résiliation : 15 janvier +→ Accès Premium jusqu'au 31 janvier +→ Pas de renouvellement 1er février +``` + +### 4.2 Par RoadWave + +RoadWave peut résilier si : +- Échec paiement (3 tentatives) +- Violation grave CGU (Strike 4 = ban) +- Fraude détectée (carte volée, etc.) + +**Notification** : +- Email **48h avant** résiliation +- Possibilité mise à jour moyen paiement + +### 4.3 Remboursement + +**Aucun remboursement** sauf : +- Bug empêchant utilisation service >7 jours consécutifs +- Résiliation par RoadWave (erreur de notre part) + +**Remboursement au prorata** dans ces cas uniquement. + +--- + +## 5. Multi-devices + +### 5.1 Connexion + +- **Illimitée** : connexion sur autant d'appareils que souhaité + +### 5.2 Streaming simultané + +- **1 seul stream actif** à la fois +- Détection connexion simultanée : + - Message : "Compte déjà utilisé sur un autre appareil" + - Bouton "Déconnecter l'autre appareil" + +### 5.3 Offline + +- Téléchargements **illimités** +- Synchronisation entre appareils (contenus téléchargés sur app A = visibles sur app B) + +--- + +## 6. Modifications tarifaires + +- Notification **30 jours avant** augmentation tarif +- Email détaillé : ancien prix → nouveau prix +- **Droit de résiliation** sans frais pendant ces 30 jours + +**Protection abonnés annuels** : +- Tarif gelé jusqu'à fin période annuelle +- Nouveau tarif appliqué au prochain renouvellement annuel + +--- + +## 7. Facturation + +### 7.1 Factures + +- Disponibles : paramètres → "Mes factures" +- Format : PDF +- Envoi email automatique après chaque paiement + +### 7.2 Échec paiement + +**Processus** : +1. Tentative 1 : J+0 (échec) +2. Email notification : "Échec paiement, mise à jour carte" +3. Tentative 2 : J+3 +4. Tentative 3 : J+7 +5. Si échec final → **suspension Premium** + +**Suspension** : +- Accès Premium désactivé +- Passage compte gratuit (avec pubs) +- Réactivation possible sous 30 jours (mise à jour carte) + +--- + +## 8. Rétractation (14 jours) + +**Droit de rétractation UE** : +- **14 jours** après souscription +- Demande via : support@roadwave.fr +- Remboursement intégral sous 14 jours + +**Exception** : +- Si utilisation service pendant 14 jours = renonciation au droit de rétractation +- Acceptation explicite à la souscription + +--- + +## 9. Contact + +- Email : premium@roadwave.fr +- Support : support@roadwave.fr diff --git a/docs/legal/cgv-publicites.md b/docs/legal/cgv-publicites.md new file mode 100644 index 0000000..503a25a --- /dev/null +++ b/docs/legal/cgv-publicites.md @@ -0,0 +1,193 @@ +# CGV Publicités + +**Version** : 1.0 +**Date d'effet** : [DATE] + +--- + +## 1. Objet + +Conditions spécifiques aux **annonceurs publicitaires** RoadWave. + +Complète les **[CGU](cgu.md)** (Article 10). + +--- + +## 2. Accès à la plateforme publicitaire + +### 2.1 Inscription + +- Interface self-service : roadwave.fr/pub +- Email professionnel requis +- SIRET obligatoire (entreprises françaises) + +### 2.2 Validation compte annonceur + +- Vérification SIRET (24-48h) +- Email confirmation activation + +--- + +## 3. Création campagne + +### 3.1 Paramètres campagne + +| Paramètre | Options | Validation | +|-----------|---------|------------| +| **Budget** | Min 50€, max illimité | Obligatoire | +| **Ciblage géo** | Point GPS / Ville / Département / Région / National | Obligatoire | +| **Durée pub** | 10-60 secondes (recommandé 15-30s) | Obligatoire | +| **Étalement** | 1 jour à 365 jours | Obligatoire | + +### 3.2 Upload audio + +- Formats : MP3, AAC +- Taille max : 10 MB +- Qualité recommandée : 128 kbps + +### 3.3 Validation manuelle + +- **Délai** : 24-48h (jours ouvrés) +- **Critères** : conformité CGU, pas de contenu trompeur, respect ARPP + +**Refus si** : +- Contenu trompeur, illégal +- Alcool, tabac (interdits) +- Produits santé non autorisés +- Arnaque évidente + +--- + +## 4. Tarification + +### 4.1 Modèle CPM + +- **Écoute complète** : 0.05€ +- **Skip après 5s** : 0.02€ +- **Skip immédiat (<5s)** : 0€ + +### 4.2 Budget minimum + +- **50€ minimum** par campagne +- Étalement paramétrable (ex: 50€ sur 10 jours = 5€/jour max) + +### 4.3 Prépaiement obligatoire + +- Paiement **avant diffusion** (Mangopay) +- Carte bancaire (Visa, Mastercard, Amex) +- Virement SEPA (délai 1-2 jours) + +--- + +## 5. Diffusion + +### 5.1 Insertion publicitaire + +- **Fréquence** : 1 pub / 5 contenus (auditeurs gratuits uniquement) +- **Skippable** après **5 secondes** +- Entre deux contenus (jamais d'interruption mid-roll) + +### 5.2 Ciblage géographique + +- Diffusion uniquement aux auditeurs **dans la zone ciblée** +- Exemple : pub "Restaurant Paris 15e" → diffusée uniquement dans rayon paramétré + +### 5.3 Priorité diffusion + +- **Enchères automatiques** : plus le budget restant élevé, plus la pub est diffusée +- Pas de manipulation manuelle (algorithme équitable) + +--- + +## 6. Reporting + +### 6.1 Dashboard annonceur + +Accessible 24/7 : roadwave.fr/pub/dashboard + +**Métriques** : +- Impressions totales (nombre diffusions) +- Taux d'écoute complète (%) +- Taux de skip (%) +- Budget consommé / restant +- CPM moyen effectif + +### 6.2 Mise à jour + +- Temps réel (rafraîchissement toutes les 5 minutes) + +--- + +## 7. Facturation + +### 7.1 Factures + +- PDF disponible : dashboard → "Mes factures" +- Envoi email automatique fin campagne + +### 7.2 Budget non consommé + +**Si campagne terminée avec budget restant** : +- <10€ : crédit conservé pour prochaine campagne +- ≥10€ : remboursement automatique sous 14 jours + +--- + +## 8. Modification / Annulation campagne + +### 8.1 Avant validation (24-48h) + +- Modification libre (budget, ciblage, audio) +- Annulation gratuite → remboursement intégral + +### 8.2 Après validation (campagne en cours) + +- **Pause** : possible à tout moment +- **Annulation** : budget consommé non remboursable, budget restant remboursé +- **Modification ciblage** : possible (ex: élargir zone géo) +- **Modification audio** : nouvelle validation requise (24-48h) + +--- + +## 9. Interdictions + +### 9.1 Contenus prohibés + +Strictement interdits : +- Alcool, tabac, cannabis +- Jeux d'argent (casinos, paris sportifs) +- Produits pharmaceutiques sans autorisation +- Arnaques, MLM illégaux +- Contenu haineux, discriminatoire +- Produits contrefaits + +### 9.2 Sanctions + +- **1ère violation** : suppression pub + avertissement +- **2e violation** : suspension compte annonceur 30 jours +- **3e violation** : ban définitif + blacklist + +--- + +## 10. Responsabilité + +### 10.1 Annonceur + +L'annonceur garantit : +- Détenir droits sur contenu publicitaire (musique, voix, marques) +- Conformité légale (ARPP, DGCCRF) +- Véracité des informations (pas de publicité mensongère) + +### 10.2 RoadWave + +RoadWave n'est pas responsable de : +- Performance commerciale de la pub (pas de garantie ventes) +- Avis négatifs utilisateurs sur la pub + +--- + +## 11. Contact + +- Email annonceurs : pub@roadwave.fr +- Support : support@roadwave.fr +- Téléphone : [TÉLÉPHONE] (9h-18h jours ouvrés) diff --git a/docs/legal/charte-createur.md b/docs/legal/charte-createur.md new file mode 100644 index 0000000..6d044b4 --- /dev/null +++ b/docs/legal/charte-createur.md @@ -0,0 +1,299 @@ +# Charte Créateur + +**Version** : 1.0 +**Date d'effet** : [DATE] + +--- + +## Préambule + +Cette Charte complète les **[CGU](cgu.md)** (Articles 6 et 7) et détaille les règles spécifiques aux créateurs de contenu. + +**Acceptation obligatoire** avant première publication. + +--- + +## 1. Validation des 3 premiers contenus + +### 1.1 Processus + +- Upload contenu → **file d'attente modération** +- Modérateur RoadWave écoute 30 secondes +- Vérification : qualité audio, métadonnées, respect règles +- Délai : **24-48h** (jours ouvrés, peut atteindre 72h le weekend) + +### 1.2 Critères de validation + +**Accepté si** : +- Qualité audio compréhensible +- Métadonnées cohérentes (zone géo, tags, classification âge) +- Pas de contenu prohibé évident + +**Refusé si** : +- Audio incompréhensible (grésillement excessif) +- Musique protégée évidente en intégrale +- Contenu haineux, violent, illégal +- Métadonnées incohérentes (ex: "Tour Eiffel" en zone "National") + +### 1.3 Après 3 validations + +- **Statut "Vérifié"** : badge visible sur profil +- **Publication immédiate** : contenus futurs publiés sans délai +- **Modération a posteriori** : uniquement si signalé + +--- + +## 2. Règles de publication + +### 2.1 Formats acceptés + +- **Audio** : MP3, AAC (.mp3, .aac, .m4a) +- **Taille max** : 200 MB (~4h podcast 128 kbps) +- **Durée max** : 4 heures + +### 2.2 Métadonnées obligatoires + +| Champ | Format | Validation | +|-------|--------|------------| +| **Titre** | 5-100 caractères | Obligatoire | +| **Type géo** | Ancré / Contextuel / Neutre | Obligatoire | +| **Zone diffusion** | Point GPS / Ville / Département / Région / National | Obligatoire | +| **Tags** | 1 à 3 parmi 12 catégories | Obligatoire | +| **Classification âge** | Tout public / 13+ / 16+ / 18+ | Obligatoire | + +**Tags disponibles** : +- Automobile, Voyage, Famille, Amour, Musique, Économie, Cryptomonnaie, Politique, Culture générale, Sport, Technologie, Santé + +### 2.3 Cohérence métadonnées + +**Exemples incohérents** : +- "Histoire Tour Eiffel" en zone "National" (devrait être "Point GPS Paris") +- Podcast enfants 3-6 ans classé "18+" (incohérent) +- Contenu politique non tagué "Politique" + +**Exemples cohérents** : +- "Visite château Versailles" → Type Ancré, Point GPS Versailles, Tags Voyage + Culture +- "Podcast auto route A7" → Type Contextuel, Zone Région Auvergne-Rhône-Alpes, Tag Automobile + +--- + +## 3. Droits d'auteur et musique + +### 3.1 Musique autorisée + +**Vous pouvez utiliser** : +- Votre propre musique originale (vous êtes compositeur/interprète) +- Musique libre de droits (Epidemic Sound, Artlist, YouTube Audio Library, Creative Commons CC0) +- Musique domaine public (>70 ans après mort auteur, ex: classique pré-1950) +- **Extraits courts ≤30 secondes** pour critique/analyse (fair use) + +### 3.2 Musique interdite + +**Strictement interdit** : +- Musique protégée en intégrale (titre complet) +- Musique protégée en fond >30 secondes +- Compilation DJ sans droits +- Karaoké (instrumental protégé + voix) + +### 3.3 Fair use (usage transformatif) + +**Conditions cumulatives** pour extraits ≤30s : +1. Durée ≤30 secondes **ET** +2. Usage transformatif : commentaire, critique, analyse **ET** +3. Pas de substitution à l'œuvre originale **ET** +4. Mention titre + artiste recommandée + +**Exemples OK** : +- Review album : extrait 20s + commentaire analyse musicale +- Podcast histoire musique : extrait 25s + contexte historique + +**Exemples KO** : +- Fond musical hit radio 2 minutes (>30s) +- Extrait 15s sans commentaire (pas d'usage transformatif) + +### 3.4 Preuves licence + +Si vous utilisez musique payante (Epidemic Sound, Artlist) : +- Conservez factures/contrats +- En cas de signalement, upload preuve via processus d'appel +- Musique sera ajoutée à whitelist interne (évite futures erreurs) + +--- + +## 4. Radio live + +### 4.1 Règles spécifiques + +**Interdictions strictes en live** : +- Diffusion concert/spectacle depuis la salle → Strike 2 immédiat (7 jours) +- Événement sportif payant (match, compétition droits TV) → Strike 2 immédiat +- Film, série, musique fond sans droits → Strike 1 (3 jours) +- Contenu violent en direct → Strike 3 immédiat (30 jours) +- Contenu illégal (terrorisme, etc.) → Strike 4 (ban) + signalement autorités + +### 4.2 Enregistrement automatique + +- Live enregistré automatiquement +- Replay publié sous **5-10 minutes** après fin +- Option "Ne pas publier replay" désactivable avant démarrage + +--- + +## 5. Classification d'âge + +### 5.1 Règles strictes + +| Classification | Contenu autorisé | Exemple | +|----------------|------------------|---------| +| **Tout public** | Aucune restriction | Contenu familial, éducatif | +| **13+** | Langage léger, thèmes adolescents | Podcast ados, gaming | +| **16+** | Langage grossier, thèmes matures | Politique, économie, débats | +| **18+** | Contenu adulte (non sexuel) | Humour noir, true crime violent | + +### 5.2 Interdit quelle que soit la classification + +- Contenu sexuellement explicite (pornographie) → **Toujours interdit** +- Contenu impliquant mineurs → **Toujours interdit + signalement** + +--- + +## 6. Modification et suppression + +### 6.1 Modification autorisée + +| Élément | Modifiable après publication | +|---------|------------------------------| +| Titre | Oui | +| Description | Oui (si ajoutée) | +| Tags | Oui | +| Image couverture | Oui | +| **Audio** | Non | +| **Zone diffusion** | Non | +| **Type géo** | Non | +| **Classification âge** | Non | + +**Raison** : éviter fraude (uploader contenu validé → remplacer par spam). + +### 6.2 Si besoin de changer audio/zone/classification + +→ **Supprimer contenu + republier** +- Si <3 contenus validés : retourne en file validation +- Si ≥3 contenus validés : publication immédiate + +### 6.3 Suppression + +- **Irréversible** : suppression définitive sous 24h +- Historique auditeurs : marqué "Contenu supprimé par créateur" +- Analytics anonymisés conservés (RGPD compliant) + +--- + +## 7. Monétisation + +### 7.1 Conditions d'éligibilité + +- Compte ≥3 mois +- ≥500 abonnés +- ≥10 000 écoutes totales +- **0 strike actif** (strikes passés effacés OK) +- ≥5 contenus publiés dans les 90 derniers jours + +### 7.2 Suspension monétisation + +**Suspension immédiate si** : +- Strike 3+ actif +- Inactivité (aucun contenu publié depuis 6 mois) +- Échec virement (RIB invalide, 3 tentatives) + +**Réactivation** : +- Après résolution strikes (réhabilitation 6 mois) +- Après mise à jour RIB +- Après publication nouveau contenu + +### 7.3 Fermeture compte monétisé + +**Solde perdu si** : +- <50€ au moment de la fermeture +- Ban définitif (Strike 4) +- Inactivité 24 mois + solde <50€ + +**Virement final si** : +- ≥50€ → virement sous 30 jours après fermeture + +--- + +## 8. Comportement et éthique + +### 8.1 Spam + +**Interdit** : +- Publier 10+ contenus identiques/similaires +- Répéter contenu dans zones différentes (ex: même podcast publié 50 fois dans 50 villes) +- Auto-promotion excessive (ex: "Abonnez-vous !" répété 20 fois) + +### 8.2 Manipulation + +**Interdit** : +- Acheter des abonnés, likes, écoutes +- Utiliser bots pour gonfler métriques +- S'abonner massivement puis se désabonner (follow/unfollow spam) + +### 8.3 Harcèlement + +**Interdit** : +- Créer contenu ciblant nommément une personne (harcèlement) +- Publier informations privées d'un tiers (doxxing) +- Menaces répétées + +--- + +## 9. Sanctions spécifiques créateurs + +### 9.1 Grille sanctions (rappel) + +| Violation | Sanction | +|-----------|----------| +| **1ère fois (droits d'auteur uniquement)** | Avertissement (pas de strike) | +| **1ère violation autre** | Strike 1 (3 jours) | +| **2e violation** | Strike 2 (7 jours) | +| **3e violation** | Strike 3 (30 jours) | +| **4e violation** | Strike 4 (ban définitif) | + +### 9.2 Conséquences strike + +**Suspension upload** : +- Strike 1 : **3 jours** sans possibilité de publier +- Strike 2 : **7 jours** +- Strike 3 : **30 jours** +- Strike 4 : **Permanent** (ban définitif) + +**Pas d'impact sur** : +- Contenus déjà publiés (restent en ligne sauf si violents/illégaux) +- Revenus en cours (virement mensuel maintenu) +- Radio live (suspension upload = suspension live aussi) + +--- + +## 10. Ressources et aide + +### 10.1 Documentation + +- **Page CGU musique** : roadwave.fr/cgu/musique +- **FAQ créateurs** : roadwave.fr/faq-createurs +- **Liste musique libre** : roadwave.fr/musique-libre + +### 10.2 Support + +- **Email** : createurs@roadwave.fr +- **Délai réponse** : 48-72h (jours ouvrés) + +### 10.3 Signalement problème + +- Si contenu validé à tort (erreur modération) +- Si strike injustifié → processus d'appel (7 jours, délai 72h) + +--- + +**Contact** : +- Email créateurs : createurs@roadwave.fr +- Email général : support@roadwave.fr diff --git a/docs/legal/mentions-legales.md b/docs/legal/mentions-legales.md new file mode 100644 index 0000000..fad3af1 --- /dev/null +++ b/docs/legal/mentions-legales.md @@ -0,0 +1,55 @@ +# Mentions légales + +--- + +## Éditeur + +**[NOM SOCIÉTÉ]** +[FORME JURIDIQUE] au capital de [CAPITAL]€ +Siège social : [ADRESSE COMPLÈTE] +SIRET : [SIRET] +RCS : [VILLE] +TVA intracommunautaire : [TVA] + +--- + +## Directeur de la publication + +[NOM FONDATEUR] + +--- + +## Hébergement + +**OVH SAS** +2 rue Kellermann +59100 Roubaix +France +SIRET : 424 761 419 00045 +Site : https://www.ovhcloud.com + +--- + +## Paiements + +**Mangopay SA** +2 Avenue Amélie +L-1126 Luxembourg +Agrément : Établissement de monnaie électronique (EME) +Site : https://www.mangopay.com + +--- + +## Contact + +- Email : contact@roadwave.fr +- Support : support@roadwave.fr +- DPO : dpo@roadwave.fr + +--- + +## Propriété intellectuelle + +Tous droits réservés © RoadWave 2026 +Marque déposée : RoadWave ® +Logo, design, code source protégés par droit d'auteur. diff --git a/docs/legal/politique-confidentialite.md b/docs/legal/politique-confidentialite.md new file mode 100644 index 0000000..d51ef61 --- /dev/null +++ b/docs/legal/politique-confidentialite.md @@ -0,0 +1,305 @@ +# Politique de confidentialité + +**Version** : 1.0 +**Date d'effet** : [DATE] +**Dernière mise à jour** : [DATE] + +Conforme au **Règlement Général sur la Protection des Données (RGPD)** (UE 2016/679). + +--- + +## 1. Responsable de traitement + +**RoadWave** +[ADRESSE SIÈGE SOCIAL] +SIRET : [SIRET] +Email : dpo@roadwave.fr + +**Délégué à la Protection des Données (DPO)** : +[NOM DPO] +Email : dpo@roadwave.fr + +--- + +## 2. Données collectées + +### 2.1 Données d'identification + +| Donnée | Obligatoire | Finalité | Base légale | +|--------|-------------|----------|-------------| +| **Email** | Oui | Connexion, récupération compte | Exécution du contrat | +| **Mot de passe** | Oui | Authentification sécurisée | Exécution du contrat | +| **Pseudo** | Oui | Identification publique créateur | Exécution du contrat | +| **Date de naissance** | Oui | Vérification âge minimum (13 ans) | Obligation légale | + +### 2.2 Données de géolocalisation + +| Donnée | Obligatoire | Finalité | Base légale | +|--------|-------------|----------|-------------| +| **Position GPS précise** | Non | Diffusion contenu géolocalisé | **Consentement explicite** | +| **Geohash 5 (~5km²)** | Auto | Recommandations après 24h | Intérêt légitime | + +**Durée conservation GPS précis** : +- Historique personnel : **24h** puis agrégé en geohash 5 +- Geohash : **conservé** pour analytics (anonymisé) + +**Révocation consentement** : +- Désactivation GPS dans paramètres → effet immédiat +- Mode dégradé : contenus nationaux + neutres uniquement + +### 2.3 Données d'utilisation + +| Donnée | Finalité | Base légale | +|--------|----------|-------------| +| **Historique d'écoute** | Recommandations personnalisées | Intérêt légitime | +| **Centres d'intérêt (jauges)** | Algorithme de recommandation | Consentement | +| **Interactions** (likes, abonnements, skips) | Amélioration UX, analytics | Intérêt légitime | +| **Logs techniques** (IP, user-agent) | Sécurité, anti-fraude | Intérêt légitime | + +**IP anonymisée** : +- Dernier octet masqué (ex: 192.168.1.XXX) +- Conservation : 7 jours (sécurité) puis suppression + +### 2.4 Données créateurs (monétisation) + +| Donnée | Finalité | Base légale | +|--------|----------|-------------| +| **SIRET, numéro TVA** | KYC Mangopay, fiscalité | Obligation légale | +| **RIB professionnel** | Virements revenus | Exécution du contrat | +| **Pièce d'identité** | Vérification identité (KYC) | Obligation légale | +| **Kbis <3 mois** | Vérification entreprise | Obligation légale | + +**Traitement KYC** : délégué à **Mangopay** (sous-traitant RGPD). + +### 2.5 Données analytics + +- **Matomo self-hosted** (pas Google Analytics) +- IP anonymisées +- **0 cookie tiers** +- Données agrégées (pas de tracking individuel hors RoadWave) + +--- + +## 3. Finalités et bases légales + +| Finalité | Base légale RGPD | Opt-out possible | +|----------|------------------|------------------| +| **Création et gestion compte** | Exécution du contrat | Non (essentiel) | +| **Diffusion contenu géolocalisé** | Consentement (GPS) | Oui (désactiver GPS) | +| **Recommandations personnalisées** | Intérêt légitime | Oui (mode anonyme) | +| **Modération contenus** | Obligation légale (DSA) | Non | +| **Analytics plateforme** | Intérêt légitime | Oui (opposition) | +| **Publicité ciblée géo** | Consentement | Oui (désactiver dans paramètres) | +| **Paiements créateurs** | Exécution du contrat | Non (si monétisation) | + +--- + +## 4. Partage des données + +### 4.1 Sous-traitants RGPD + +| Sous-traitant | Finalité | Localisation données | DPA signé | +|---------------|----------|----------------------|-----------| +| **Mangopay** | Paiements, KYC créateurs | UE (Luxembourg) | Oui | +| **OVH** | Hébergement audio, stockage fichiers | France | Oui | +| **Hetzner** | Hébergement serveurs backend | UE (Allemagne) | Oui | + +**DPA** = Data Processing Agreement (accord de sous-traitance RGPD). + +### 4.2 Données publiques + +| Donnée | Visibilité | Modification | +|--------|------------|--------------| +| **Pseudo créateur** | Publique | Modifiable | +| **Bio créateur** | Publique | Modifiable | +| **Stats créateur** (abonnés, écoutes) | Publique (arrondies) | Non modifiable | +| **Badge vérifié** | Publique | Non modifiable | + +### 4.3 Aucune revente + +RoadWave **ne vend jamais** vos données à des tiers. + +### 4.4 Autorités + +Données transmises uniquement si : +- **Obligation légale** (réquisition judiciaire) +- **Signalement contenu illégal** (terrorisme, pédopornographie) → autorités compétentes + +--- + +## 5. Durées de conservation + +| Donnée | Durée | Justification | +|--------|-------|---------------| +| **Compte actif** | Tant que compte existe | Exécution contrat | +| **Compte supprimé** | Grace period **30 jours** puis suppression | Récupération possible | +| **GPS précis** | **24h** puis agrégé geohash 5 | RGPD minimisation | +| **Historique écoute** | Tant que compte existe | Recommandations | +| **Logs modération** | **3 ans** (DSA) | Obligation légale | +| **Logs techniques (IP)** | **7 jours** puis suppression | Sécurité | +| **KYC créateurs** | 5 ans après fin relation (obligation fiscale) | Obligation légale | +| **Contenus supprimés** | Anonymisés immédiatement, analytics conservés | Analytics plateforme | + +--- + +## 6. Droits des utilisateurs (RGPD) + +### 6.1 Droit d'accès + +- **Consulter** toutes ses données personnelles +- Interface dédiée : "Mes données" dans paramètres +- Export automatique (voir 6.4) + +### 6.2 Droit de rectification + +- **Modifier** : pseudo, bio, email, centres d'intérêt +- Demande via : dpo@roadwave.fr +- Délai : **1 mois** (RGPD) + +### 6.3 Droit à l'effacement ("droit à l'oubli") + +- **Suppression compte** : paramètres → "Supprimer mon compte" +- Grace period : **30 jours** (récupération possible) +- Après 30 jours : suppression définitive + +**Exceptions** (conservation nécessaire) : +- Logs modération : 3 ans (obligation DSA) +- KYC créateurs : 5 ans (obligation fiscale) +- Analytics agrégés (anonymisés) + +### 6.4 Droit à la portabilité + +- **Export données** : JSON + HTML + audio +- Générateur asynchrone (délai **48h** si volume important) +- Lien téléchargement expire après **7 jours** + +**Contenu export** : +- Profil (pseudo, bio, stats) +- Historique écoute complet +- Centres d'intérêt (jauges) +- Contenus publiés (métadonnées + fichiers audio) +- Abonnements, likes + +### 6.5 Droit d'opposition + +- **Profilage publicitaire** : désactivation dans paramètres +- **Analytics** : désactivation tracking Matomo +- **Emails marketing** : désinscription lien footer email + +### 6.6 Droit de limitation + +- **Suspension traitement** pendant contestation exactitude données +- Demande via : dpo@roadwave.fr + +### 6.7 Réclamation CNIL + +Si désaccord avec RoadWave : +- **CNIL** (Commission Nationale de l'Informatique et des Libertés) +- Site : https://www.cnil.fr/fr/plaintes +- Adresse : 3 Place de Fontenoy, 75007 Paris + +--- + +## 7. Sécurité des données + +### 7.1 Mesures techniques + +- **Chiffrement** : HTTPS (TLS 1.3) pour toutes communications +- **Stockage** : bases de données chiffrées au repos (AES-256) +- **Mots de passe** : hashage bcrypt (salt + iterations) +- **Backups** : quotidiens, chiffrés, stockés UE + +### 7.2 Mesures organisationnelles + +- Accès données restreint (équipe RoadWave uniquement) +- Authentification 2FA pour admins +- Logs d'accès (audit trail) + +### 7.3 Violation de données + +En cas de fuite de données : +- Notification CNIL sous **72h** +- Notification utilisateurs concernés (email) si risque élevé +- Mesures correctives immédiates + +--- + +## 8. Cookies + +### 8.1 Cookies essentiels (pas de consentement requis) + +| Cookie | Finalité | Durée | +|--------|----------|-------| +| `session_token` | Authentification utilisateur | 30 jours | +| `refresh_token` | Renouvellement session | 30 jours | + +### 8.2 Cookies analytics (consentement requis) + +- **Matomo** : analytics self-hosted +- Bannière **Tarteaucitron.js** au premier lancement +- Refus = aucun cookie analytics + +### 8.3 Aucun cookie tiers + +- Pas de Google Analytics +- Pas de Facebook Pixel +- Pas de trackers publicitaires tiers + +--- + +## 9. Mineurs + +### 9.1 Âge minimum + +- **13 ans minimum** (RGPD) +- Vérification : date de naissance à l'inscription + +### 9.2 Mineurs 13-15 ans + +- **Autorisation parentale requise** +- Email parent vérifié +- Parent peut demander suppression compte mineur + +### 9.3 Mode Kids + +- Activation automatique si <13 ans (détecté via date naissance) +- Contenus filtrés : uniquement "Tout public" + tranches d'âge adaptées +- Pas de publicités ciblées (conformité COPPA US si expansion) + +--- + +## 10. Transferts hors UE + +### 10.1 Principe + +Données hébergées **exclusivement UE** : +- Serveurs : Hetzner (Allemagne) ou OVH (France) +- Storage : OVH Object Storage (France) +- Paiements : Mangopay (Luxembourg) + +### 10.2 Exceptions + +**API tierces potentielles** (post-MVP) : +- Si transfert hors UE → clauses contractuelles types (CCT) +- Notification utilisateurs avant activation + +--- + +## 11. Modifications de la politique + +- Notification **14 jours avant** entrée en vigueur (email) +- Version datée disponible : roadwave.fr/confidentialite +- Changements majeurs → nouveau consentement requis + +--- + +## 12. Contact DPO + +**Délégué à la Protection des Données** : +- Email : dpo@roadwave.fr +- Délai réponse : **1 mois** (RGPD) + +**Pour toute question RGPD** : +1. Email dpo@roadwave.fr +2. Objet : "[RGPD] Votre demande" +3. Joindre : justificatif identité (si accès/suppression données) diff --git a/docs/regles-metier/01-authentification-inscription.md b/docs/regles-metier/01-authentification-inscription.md new file mode 100644 index 0000000..d563c43 --- /dev/null +++ b/docs/regles-metier/01-authentification-inscription.md @@ -0,0 +1,233 @@ +## 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 +- ✅ 2FA (Two-Factor Authentication) disponible +- ✅ Option "Appareil de confiance" (skip 2FA pour 30 jours) + +**Justification** : +- Souveraineté : pas de dépendance externe +- RGPD : données 100% contrôlées +- Coût : 0€ (Zitadel intégré) + +--- + +### 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 🟢 uniquement +- Utilisateur 16-17 ans → contenus 🟢 🟡 +- 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 diff --git a/docs/regles-metier/02-conformite-rgpd.md b/docs/regles-metier/02-conformite-rgpd.md new file mode 100644 index 0000000..5634cf8 --- /dev/null +++ b/docs/regles-metier/02-conformite-rgpd.md @@ -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 (MaxMind) | 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** : +- MaxMind GeoLite2 (gratuit, self-hosted) +- 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 MaxMind + 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) diff --git a/docs/regles-metier/03-centres-interet-jauges.md b/docs/regles-metier/03-centres-interet-jauges.md new file mode 100644 index 0000000..db00bc6 --- /dev/null +++ b/docs/regles-metier/03-centres-interet-jauges.md @@ -0,0 +1,141 @@ +## 3. Centres d'intérêt et jauges + +### 3.1 Évolution des jauges + +**Décision** : Système simple avec valeurs fixes + +| Action | Impact jauge | Justification | +|--------|--------------|---------------| +| **Like automatique renforcé (≥80% écoute)** | +2% | Signal fort d'intérêt (écoute quasi-complète) | +| **Like automatique standard (30-79% écoute)** | +1% | Signal modéré d'intérêt | +| **Like explicite (manuel)** | +2% | Signal fort, cumulable avec auto | +| **Abonnement créateur** | +5% sur tous ses tags | Signal très fort d'affinité | +| **Skip rapide (<10s)** | -0.5% | Désintérêt marqué | +| **Skip tardif (≥30%)** | 0% | Neutre (contenu essayé suffisamment) | + +**Paramètres techniques** : +- Les jauges sont bornées strictement entre **0% et 100%** +- Calcul immédiat à chaque action (pas de batch différé) +- Les tags du contenu sont définis par le créateur à la publication +- Si un contenu a plusieurs tags, chaque jauge correspondante est impactée + +**Exemple de calcul** : +``` +Contenu de 5 minutes tagué "Automobile" + "Voyage" + +Scénario 1 : Écoute 4min30 (90%) +→ Like automatique renforcé (+2%) +→ Jauge Automobile : 45% → 47% +→ Jauge Voyage : 60% → 62% + +Scénario 2 : Écoute 2min30 (50%) +→ Like automatique standard (+1%) +→ Jauge Automobile : 45% → 46% +→ Jauge Voyage : 60% → 61% + +Scénario 3 : Écoute 2min30 (50%) + Like manuel +→ Like auto +1% puis like manuel +2% = +3% total +→ Jauge Automobile : 45% → 48% +→ Jauge Voyage : 60% → 63% + +Scénario 4 : Skip après 5s +→ Signal négatif (-0.5%) +→ Jauge Automobile : 45% → 44.5% +→ Jauge Voyage : 60% → 59.5% +``` + +**Justification** : +- **Like automatique** : Reflète l'engagement réel (voir [ADR-010](../adr/010-commandes-volant.md)) +- **Sécurité routière** : Pas d'action complexe en conduite +- **Prévisibilité** : Règles claires et déterministes +- **Coût minimal** : Calculs simples en backend +- **Fiabilité** : Pas d'edge cases complexes +- **Ajustable** : Valeurs modifiables via dashboard admin si besoin + +--- + +### 3.2 Jauge initiale + +**Décision** : Démarrage neutre à 50%, pas de questionnaire + +**À l'inscription** : +- Toutes les jauges d'intérêt sont initialisées à **50%** +- Pas de questionnaire onboarding (friction zéro) +- L'algorithme apprend naturellement via les premières écoutes + +**Catégories disponibles** : +- Automobile +- Voyage +- Famille +- Amour +- Musique +- Économie +- Cryptomonnaie +- Politique +- Culture générale +- Sport +- Technologie +- Santé +- *... (extensible)* + +**Cold start (premiers jours)** : +1. Nouvel utilisateur s'inscrit → toutes jauges à 50% +2. Écoute premier podcast "Automobile" → jauge Auto monte à 51% +3. Skip un contenu "Économie" → jauge Éco descend à 48% +4. Après 10-15 écoutes, profil commence à se dessiner clairement + +**Alternative optionnelle (post-MVP)** : +- Questionnaire **optionnel** proposé après 3 écoutes (in-app) +- Message : "Améliorez vos recommandations en sélectionnant vos centres d'intérêt" +- Si rempli : jauges sélectionnées passent à 70%, non sélectionnées à 30% +- Si skip : conserve 50% partout + +**Justification** : +- **Inscription ultra-rapide** : pas de questionnaire = moins de churn +- **Découverte naturelle** : l'algorithme apprend en quelques écoutes +- **Équitable** : pas de biais initial vers certains créateurs +- **Comportement déterministe** : facile à tester et débugger +- **Cold start acceptable** : à 50%, tous les contenus ont une chance égale initialement + +--- + +### 3.3 Dégradation temporelle + +**Décision** : Pas de dégradation automatique + +Les jauges **ne diminuent jamais** avec le temps de manière automatique. + +**Règle** : +- Une jauge ne change **que par les actions utilisateur** (like, écoute, skip) +- Pas de cron job de dégradation périodique +- Pas de "rafraîchissement" artificiel + +**Scénario illustratif** : +``` +Utilisateur aimait "Économie" (jauge 80%) il y a 1 an +→ Depuis, skip tous les contenus Éco +→ Jauge descend naturellement à 40% via les skips +→ Pas besoin de dégradation temporelle +``` + +**Si utilisateur inactif longtemps** : +- Utilisateur part en vacances 6 mois → jauges conservées +- Au retour : ses jauges reflètent toujours ses goûts d'avant +- Comportement cohérent et prévisible + +**Alternative utilisateur (contrôle explicite)** : +- Bouton "Réinitialiser mes centres d'intérêt" dans paramètres +- Action manuelle : remet toutes les jauges à 50% +- Permet nouveau départ si souhaité (changement de vie, etc.) + +**Justification** : +- **Principe KISS** (Keep It Simple, Stupid) +- **Coût 0** : pas de batch nocturne, pas de calculs temporels +- **Fiabilité maximale** : pas de bugs de fuseaux horaires, dates, etc. +- **UX prévisible** : jauge = reflet des actions, pas d'automatisme caché +- **Respect historique** : si utilisateur aimait X depuis 2 ans, pourquoi "oublier" ? +- **Évolution naturelle** : les actions récentes suffisent à faire évoluer les jauges + +--- + +## Récapitulatif Section 3 diff --git a/docs/regles-metier/04-algorithme-recommandation.md b/docs/regles-metier/04-algorithme-recommandation.md new file mode 100644 index 0000000..1eb6b59 --- /dev/null +++ b/docs/regles-metier/04-algorithme-recommandation.md @@ -0,0 +1,336 @@ +## 2. Algorithme de recommandation + +### 2.1 Classification de géo-pertinence + +**Décision** : 3 types de contenus selon leur pertinence géographique + +| Type | Description | Exemple | Pondération géo | +|------|-------------|---------|-----------------| +| **Géo-ancré** | Contenu lié à un lieu précis | Audio-guide monument, pub restaurant local | 70% | +| **Géo-contextuel** | Pertinent dans une zone | Actualité régionale, événement local | 50% | +| **Géo-neutre** | Universel, pas de lien géo | Podcast philosophie, musique | 20% | + +**Qui décide** : +- ✅ Créateur choisit le type à la publication +- ✅ Modération peut reclassifier après validation +- ✅ Modification possible après publication (tout le monde a le droit de se tromper) + +**Justification** : +- Différencie audio-guide (hyper-local) des podcasts génériques +- Algorithme adapte automatiquement la pondération +- Coût : champ supplémentaire en DB + règle algo + +--- + +### 2.2 Formule de scoring + +**Décision** : Score combiné dynamique selon type de contenu + +``` +score_final = (score_geo * poids_geo_type) + + (score_interets * poids_interets_type) + + (score_engagement * 0.2) + + (bonus_aleatoire) + +où : +- score_geo = 1 - (distance_km / distance_max_km) +- score_interets = moyenne des jauges utilisateur pour les tags du contenu +- score_engagement = (taux_completion * 0.5) + (ratio_likes * 0.3) + (ratio_abonnements * 0.2) +- bonus_aleatoire = 10% des recommandations tirées aléatoirement +``` + +**Pondérations par type** : + +| Type | Poids géo | Poids intérêts | +|------|-----------|----------------| +| Géo-ancré | 0.7 | 0.1 | +| Géo-contextuel | 0.5 | 0.3 | +| Géo-neutre | 0.2 | 0.6 | + +**Paramètres** : +- Distance max recommandée : **200 km** +- Dégradation : **linéaire** (1 - distance/200km) +- Rayon point GPS : **500m** (adapté au volume de contenu local) + +**Tous ces paramètres sont configurables à chaud via interface admin.** + +**Justification** : +- Flexibilité totale selon type de contenu +- Linéaire = rattrapage naturel du contenu viral ancien +- Auditable via métriques engagement (moyenne/médiane) + +--- + +### 2.3 Score d'engagement et popularité + +**Décision** : Intégration popularité avec poids 0.2 + +**Métriques** : +- **Taux de complétion** : écoutes >80% / total écoutes (poids 0.5) +- **Ratio likes** : likes / écoutes (poids 0.3) +- **Ratio abonnements** : nouveaux abonnés après écoute / écoutes (poids 0.2) + +**Seuil minimum** : +- Minimum **50 écoutes** avant de considérer l'engagement +- Contenu <50 écoutes : score engagement = 0.5 (neutre) + +**Contenu viral** : +- Un contenu viral à Paris **peut** être proposé à Marseille +- Score géo faible compensé par score engagement élevé +- Paramétrable admin + +**Dépréciation temporelle** : +- Pas de dépréciation automatique +- Ratio linéaire = contenu ancien mais toujours apprécié reste pertinent + +**Justification** : +- Équilibre découverte / qualité +- Pas de pénalisation arbitraire des contenus anciens +- Coût : calculs sur métriques existantes + +--- + +### 2.4 Part d'aléatoire (exploration) + +**Décision** : 10% par défaut, paramétrable utilisateur + +**Fonctionnement** : +- 1 contenu sur 10 = tirage aléatoire (hors historique déjà écouté) +- Utilisateur peut ajuster : curseur 0% (aucun aléatoire) à 50% (exploration max) + +**Curseur utilisateur** : +- 🎯 **0%** : Personnalisé max (recommandations strictes) +- ⚖️ **10%** : Équilibré (défaut) +- 🎲 **30%** : Découverte élevée +- 🌍 **50%** : Découverte max (équivaut à national = découverte) + +**Justification** : +- Évite la bulle de filtre +- Laisse l'utilisateur maître de son expérience +- Coût : variable aléatoire en algo + +--- + +### 2.5 Contenu politique (version MVP simplifiée) + +> ⚠️ **Note** : La classification politique avancée (échelle gauche/droite, équilibrage imposé) a été reportée post-MVP. Voir [ANNEXE-POST-MVP.md](ANNEXE-POST-MVP.md) pour la version complète. + +**Décision MVP** : Tag simple "Politique" sans classification idéologique + +**Tagging** : +- Créateur peut taguer son contenu comme "Politique" (optionnel) +- Tag "Politique" au même niveau que "Économie", "Sport", "Culture", etc. +- **Pas de classification gauche/droite** +- **Pas d'équilibrage imposé** + +**Filtrage utilisateur** : +- Option paramètres : **"Masquer contenu politique"** +- Si activé → 0% de contenus tagués "Politique" dans le feed +- Par défaut : désactivé (tous contenus visibles) + +**Justification MVP** : +- **Simplicité** : Pas de modération politique coûteuse (~2000€/mois économisés) +- **Neutralité technique** : Aucun jugement éditorial sur orientation +- **Risque minimal** : Évite controverses et contentieux DSA au lancement +- **Fonctionnel** : Utilisateurs peuvent filtrer si souhaité + +**Post-MVP** : +- Classification avancée possible si forte demande utilisateurs +- Nécessite ressources modération dédiées et audit DSA + +--- + +### 2.6 Mode Kids (13-15 ans) + +**Décision** : Mode optionnel pour adolescents 13-15 ans uniquement + +> ⚠️ **Note** : Âge minimum d'inscription = **13 ans** (obligation légale EU). Pas d'utilisateurs <13 ans sur la plateforme. + +**Tranche concernée** : + +| Tranche | Description | Contenus autorisés | Restrictions | +|---------|-------------|-------------------|--------------| +| **13-15 ans** | Collège | Contenus "Tous publics" uniquement | Filtrage 16+ et 18+ | + +**Activation** : +- ❌ **Pas d'activation automatique** (tous les utilisateurs ont ≥13 ans) +- ✅ **Activation manuelle** via toggle paramètres +- ✅ Parents peuvent activer pour leurs enfants 13-15 ans +- ✅ Utilisateur peut désactiver à tout moment + +**Filtrage quand Mode Kids activé** : +- ✅ Contenus "Tous publics" uniquement +- ❌ Exclusion contenus 16+ et 18+ +- ❌ Pas de contenu politique (automatiquement filtré) +- ❌ Pas de publicité (ou uniquement pub validée manuellement) + +**Interface** : +- Interface standard (pas d'interface dédiée enfants pour MVP) +- Filtrage algorithmique des contenus inappropriés + +**Justification** : +- **Conformité légale** : Âge minimum 13 ans (RGPD, DSA) +- **Simplicité MVP** : Un seul mode optionnel vs 4 tranches d'âge +- **Protection mineurs** : Filtrage contenus adultes pour 13-15 ans +- **Flexibilité** : Parents décident d'activer ou non + +--- + +### 2.7 Déclenchement géographique + +**Décision** : Notification au passage, pas d'anticipation + +**Fonctionnement** : +1. Utilisateur passe à <500m d'un point GPS (contenu géo-ancré) +2. **Notification sonore** (bip court) + **visuelle** (logo selon type) +3. Types de logos : 📍 Info, 🏛️ Culturel, 🍴 Commercial, 🎭 Événement +4. Délai réaction utilisateur : **5 secondes** pour accepter (bouton volant ou commande vocale) +5. Si accepté → lecture immédiate +6. Si ignoré → contenu proposé normalement en file d'attente + +**Publicités** : +- ⚠️ **Jamais d'interruption** de contenu en cours +- Pub s'intercale **entre deux séquences** uniquement +- Notification pub : son différent (facultatif selon paramètres) + +**Gestion demi-tour** : +- Si utilisateur repart du point après notification → pas de nouvelle notification (déjà proposé) +- Réinitialisation après 24h + +**Justification** : +- Respect écoute en cours (pas de coupure brutale) +- UX fluide (utilisateur garde contrôle) +- Simplicité technique (pas de prédiction trajectoire) + +--- + +### 2.8 Historique et repropositon + +**Décision** : Pas de reproposition sauf contenu partiel + +**Règles** : + +| État écoute | Completion | Action | +|-------------|------------|--------| +| **Écouté complètement** | >80% | ❌ Ne jamais reproposer (sauf flag `replayable = true` pour audio-guides) | +| **Skippé rapidement** | <10s | ❌ Ne pas reproposer | +| **Partiellement écouté** | 10-80% | ✅ Reproposer avec reprise position (`last_position_seconds`) | + +**Stockage historique** : +- Table `user_content_history` (user_id, content_id, completion_rate, last_position, listened_at) +- Historique **illimité** (PostgreSQL) +- Algorithme considère les **100 derniers** pour optimisation requêtes +- Export complet disponible (RGPD) + +**Justification** : +- Découverte maximale (pas de redites) +- Respect erreurs de clic (contenu partiel = 2nde chance) +- Coût stockage négligeable (PostgreSQL scalable) + +--- + +### 2.9 Paramétrabilité admin (interface dashboard) + +**Décision** : Tous paramètres scoring exposés + A/B testing + +**Paramètres configurables à chaud** : + +| Paramètre | Plage | Défaut | Unité | +|-----------|-------|--------|-------| +| `poids_geo_ancre` | 0.5 - 1.0 | 0.7 | % | +| `poids_geo_contextuel` | 0.3 - 0.7 | 0.5 | % | +| `poids_geo_neutre` | 0.0 - 0.4 | 0.2 | % | +| `poids_engagement` | 0.0 - 0.5 | 0.2 | % | +| `part_aleatoire_global` | 0.0 - 0.3 | 0.1 | % | +| `distance_max_km` | 50 - 500 | 200 | km | +| `rayon_gps_point_m` | 100 - 2000 | 500 | m | +| `seuil_min_ecoutes_engagement` | 10 - 200 | 50 | nb | + +**Application changements** : +- Immédiat : nouveaux calculs utilisent nouvelle config +- Aucun recalcul batch (coût CPU) +- Version config trackée (git-like) +- Rollback 1 clic + +**A/B Testing** : +- Création variantes (Config A vs Config B) +- Split utilisateurs 50/50 aléatoire +- Métriques comparatives : taux complétion, engagement, session duration +- Dashboard graphique temps réel + +**Audit engagement** : +- Métriques clés : moyenne/médiane temps d'écoute par session +- Graphiques : évolution engagement selon config +- Export CSV pour analyse externe + +**Justification** : +- Optimisation continue sans redéploiement +- Data-driven decisions (métriques objectives) +- Coût : dashboard admin à développer (one-time) + +--- + +### 2.10 Paramétrabilité utilisateur + +**Décision** : Curseurs avancés avec profils sauvegardables + +**Niveaux de personnalisation** : + +**Curseurs disponibles** : +- 📍 **Géolocalisation** : Local ← slider → National (découverte = national) +- 🎲 **Découverte** : 0% ← slider → 50% (part aléatoire) +- ⚖️ **Politique** : Masquer / Équilibré / Mes préférences + +**Profils sauvegardables** : +- 🚗 Trajet quotidien (boulot) : géo local, découverte 5%, politique masqué +- 🛣️ Road trip : géo régional, découverte 30%, politique équilibré +- 👶 Enfants : Mode Kids activé + +**Synchronisation** : +- ✅ Sync profils entre devices (cloud PostgreSQL) +- ❌ Pas de partage profils entre utilisateurs (famille) +- Auto-switch selon context (détection trajet récurrent via GPS) + +**Sécurité conduite** : +- ⚠️ **Blocage modification si vitesse GPS >10 km/h** +- Warning au lancement app : "Configurez avant de prendre la route" +- Modifications uniquement app arrêtée/passager + +**Justification** : +- Utilisateur maître de son expérience +- Contextes d'usage différents (quotidien vs voyage) +- Sécurité routière (pas de distraction) + +--- + +### 2.11 Médias traditionnels + +**Décision** : Ouverture aux médias établis + +**Médias autorisés** : +- Presse nationale : Le Monde, Le Parisien, Libération, Le Figaro, etc. +- Radios : France Inter, RTL, Europe 1, etc. +- Médias régionaux : Ouest-France, Sud-Ouest, etc. + +**Format contenus** : +- Flashs info géolocalisés (actualité régionale) +- Chroniques thématiques (culture, économie, sport) +- Éditos et débats (classification politique appliquée) + +**Validation** : +- Compte média vérifié (badge ✓) +- Pas de validation 3 premiers contenus (confiance établie) +- Modération a posteriori uniquement + +**Monétisation** : +- Partage revenus pub standard (même conditions créateurs) +- Possibilité sponsoring direct (pas via plateforme) + +**Justification** : +- Crédibilité plateforme (contenus professionnels) +- Diversité éditoriale +- Attractivité grand public (noms reconnus) + +--- + +## Récapitulatif Section 2 diff --git a/docs/regles-metier/05-interactions-navigation.md b/docs/regles-metier/05-interactions-navigation.md new file mode 100644 index 0000000..edfdc9d --- /dev/null +++ b/docs/regles-metier/05-interactions-navigation.md @@ -0,0 +1,516 @@ +## 5. Interactions et navigation + +### 5.1 File d'attente et commande "Suivant" + +**Décision** : Pré-calcul 5 contenus avec insertion prioritaire pour points géographiques + +**File d'attente** : +- **5 contenus pré-calculés** en cache (Redis) +- Recalcul automatique si : + - Déplacement >10km + - Toutes les 10 minutes (rafraîchissement contenu) + - File d'attente <3 contenus restants + +**Insertion prioritaire géo-ancrée (mode voiture uniquement)** : + +**Détection** : +- Calcul ETA (Estimated Time of Arrival) via API GPS native iOS/Android +- Notification déclenchée **7 secondes avant** d'arriver au point GPS +- Si vitesse < 5 km/h ET distance < 50m → notification immédiate +- ⚠️ **App doit être ouverte** (pas de détection en arrière-plan en mode voiture) + +**Notification** : +- **Sonore uniquement** : bip court ou son personnalisé RoadWave +- **Visuelle minimale** : icône selon type de contenu (🏛️ culture, 👨‍👩‍👧 famille, 🎵 musique, etc.) +- **Compteur visible** : 7...6...5...4...3...2...1 (décompte des secondes) +- **Pas de texte affiché** (éviter distraction conducteur) +- **Pas de bouton "Annuler"** : seul le bouton "Suivant" permet validation + +**Actions utilisateur** : +1. User entend notification sonore + voit icône et compteur +2. User appuie "Suivant" dans les 7 secondes → décompte 5s démarre +3. Pendant décompte : contenu actuel continue, compteur visible (5...4...3...2...1) +4. Si contenu actuel se termine pendant décompte → contenu suivant du buffer démarre +5. À la fin du décompte → contenu géolocalisé démarre (fade out/in 0.3s) + +**Si user n'appuie pas sur "Suivant"** : +- Notification disparaît après 7 secondes +- Contenu géolocalisé est perdu (pas d'insertion dans file) +- Pas de nouveau contenu géolocalisé pendant **10 minutes** (éviter spam) + +**Limitation anti-spam** : +- Maximum **6 contenus géolocalisés par heure** +- Timer reset toutes les heures (rolling window) +- Exception : séquences d'un même audio-guide multi-séquences (comptent comme 1) +- Si quota atteint : notifications suivantes ignorées jusqu'à libération du quota + +**Invalidation immédiate** : +- Utilisateur change ses préférences (curseurs géo/découverte/politique) + - ⚠️ **Modification bloquée si vitesse GPS >10 km/h** (sécurité routière) +- Live démarre d'un créateur suivi dans la zone + +**Implémentation** : +``` +Redis cache : + - Clé : user:{user_id}:queue + - Structure : [content_1, content_2, ..., content_5] + - Métadonnées : {last_lat, last_lon, computed_at, mode: "voiture"|"pieton"} + - TTL : 15 minutes + +Tracking GPS temps réel (mobile) : + - Vérification toutes les 1 seconde + - Calcul ETA vers points géolocalisés proches (rayon 500m) + - Si ETA ≤ 7s → trigger notification + - Historique GPS : 30 derniers points pour calcul vitesse moyenne + +Quota anti-spam (Redis) : + - Clé : user:{user_id}:geo_quota + - Structure : sorted set avec timestamps des 6 derniers contenus + - TTL : 1 heure + - Vérification avant notification : ZCOUNT pour compter contenus dernière heure + +Cooldown après ignorance (Redis) : + - Clé : user:{user_id}:geo_cooldown + - TTL : 10 minutes + - Set après notification ignorée +``` + +**Justification** : +- **Expérience fluide** : pas de latence au clic "Suivant" +- **Réactivité géo** : contenu local inséré immédiatement +- **Coût optimisé** : recalcul uniquement si nécessaire +- **Sécurité** : pas de modification en conduite + +--- + +### 5.1.2 Mode piéton (audio-guides) + +**Décision** : Notifications push en arrière-plan avec rayon large + +**Contexte** : +- Mode piéton détecté automatiquement si vitesse moyenne < 5 km/h +- Cas d'usage : visites à pied, musées, monuments, quartiers historiques +- User n'a pas besoin d'avoir l'app ouverte +- ⚠️ **Fonctionnalité optionnelle** : requiert permission "localisation en arrière-plan" (activée par user) + +**Détection** : +- App peut être en arrière-plan (si permission accordée) +- Rayon de détection : **200 mètres** autour du point GPS +- Geofencing iOS/Android pour minimiser consommation batterie +- Permission demandée uniquement si user active "Notifications audio-guides piéton" dans settings + +**Notification push système** : + +Format : +``` +Titre : "Audio-guide à proximité" +Body : "[Nom du contenu] - [Nom créateur]" +Action : Tap → ouvre app sur le contenu +``` + +Exemple : +``` +Audio-guide à proximité +Musée du Louvre : La Joconde - @paris_museum +``` + +**Permissions requises** : + +⚠️ **Important** : Permission "Always Location" est **optionnelle** et demandée uniquement si user active le mode piéton dans settings. + +iOS (`Info.plist`) : +```xml +NSLocationWhenInUseUsageDescription +RoadWave utilise votre position pour vous proposer des contenus audio géolocalisés adaptés à votre trajet en temps réel. + +NSLocationAlwaysAndWhenInUseUsageDescription +Si vous activez les notifications audio-guides piéton, RoadWave peut vous alerter lorsque vous passez près d'un monument ou musée, même quand l'app est en arrière-plan. Cette fonctionnalité est optionnelle et peut être désactivée à tout moment dans les réglages. +``` + +Android (`AndroidManifest.xml`) : +```xml + + +``` + +**Disclosure avant demande permission** (Android requis, iOS recommandé) : + +Écran affiché avant demande permission "Always Location" : + +``` +┌────────────────────────────────────────┐ +│ 📍 Notifications audio-guides piéton │ +├────────────────────────────────────────┤ +│ Pour vous alerter d'audio-guides à │ +│ proximité même quand vous marchez avec │ +│ l'app fermée, RoadWave a besoin de │ +│ votre position en arrière-plan. │ +│ │ +│ Votre position sera utilisée pour : │ +│ ✅ Détecter monuments à 200m │ +│ ✅ Vous envoyer une notification │ +│ │ +│ Votre position ne sera jamais : │ +│ ❌ Vendue à des tiers │ +│ ❌ Utilisée pour de la publicité │ +│ │ +│ Cette fonctionnalité est optionnelle. │ +│ Vous pouvez utiliser RoadWave sans │ +│ cette permission. │ +│ │ +│ [Continuer] [Non merci] │ +│ │ +│ Plus d'infos : Politique confidentialité│ +└────────────────────────────────────────┘ +``` + +**Si user refuse** : +- Mode piéton désactivé (uniquement mode voiture disponible) +- App fonctionne normalement avec permission "When In Use" +- Audio-guides accessibles en mode manuel (user ouvre app, sélectionne contenu) + +**Comportement après tap sur notification** : +1. User tap notification push +2. App s'ouvre sur la page du contenu +3. User peut démarrer la lecture manuellement +4. Navigation libre (voir section 16.2 pour audio-guides piéton) + +**Basculement automatique voiture ↔ piéton** : + +Détection par vitesse GPS moyenne sur 30 secondes : +- Vitesse < 5 km/h (stable 10s) → mode piéton +- Vitesse ≥ 5 km/h (stable 10s) → mode voiture + +Changements de mode : + +| Mode actuel | Vitesse détectée | Nouveau mode | Effet | +|-------------|------------------|--------------|-------| +| Piéton | ≥ 5 km/h | Voiture | Notifications push → sonores + icône (app ouverte requise) | +| Voiture | < 5 km/h | Piéton | Notifications sonores → push arrière-plan | + +**Pas de popup confirmation** : +- Basculement transparent et automatique +- User n'a rien à faire +- Hysteresis (10s) pour éviter basculements intempestifs + +**Quota anti-spam mode piéton** : +- Même limitation que mode voiture : **6 contenus/heure** +- Cooldown 10 min si notification ignorée (app pas ouverte après tap) + +**Justification** : +- ✅ Expérience adaptée aux visites à pied (rayon large, pas de timing précis) +- ✅ Économie batterie (geofencing natif iOS/Android) +- ✅ User peut garder téléphone en poche +- ✅ Basculement automatique = pas de friction + +--- + +### 5.2 Commande "Précédent" + +**Décision** : Comportement smart selon progression écoute + +**Règles** : + +| Situation | Temps écouté | Action "Précédent" | +|-----------|--------------|-------------------| +| **Début de contenu** | <10 secondes | Retour au contenu précédent (position exacte) | +| **Milieu/fin** | ≥10 secondes | Replay contenu actuel depuis le début | +| **Premier de session** | N/A | Replay depuis début (rien avant) | + +**Historique de navigation** : +- **10 contenus maximum** en mémoire (Redis List) +- Structure : `[{content_id, position_seconds, listened_at}, ...]` +- FIFO : au-delà de 10, suppression du plus ancien + +**Exemple scénario** : +``` +Utilisateur écoute : +1. Contenu A → écoute 5s → "Suivant" +2. Contenu B → écoute 2min30 → "Suivant" +3. Contenu C → écoute 5s → "Précédent" + → Retour Contenu B à 2min30 (car >10s) +4. Sur Contenu B → "Précédent" + → Retour Contenu A à 5s (position exacte) +``` + +**Interface (responsabilité front)** : +- ❌ Pas de message UI +- ✅ Progress bar revient au début ou à position exacte +- ✅ Animation fluide (transition 0.3s) + +**Justification** : +- **UX intuitive** : comportement standard Spotify/YouTube +- **Pas de frustration** : si début, vraiment revenir en arrière +- **Simplicité** : règle unique (seuil 10s) + +--- + +### 5.3 Interactions au volant : Like automatique et engagement + +> ⚠️ **Architecture Decision Record** : Voir [ADR-010](../adr/010-commandes-volant.md) pour les détails techniques complets + +**Décision** : Like automatique basé sur le temps d'écoute + +**Problème technique identifié** : +- iOS et Android ne supportent **pas nativement** les appuis longs ou doubles-appuis sur les commandes média +- Les commandes physiques au volant varient selon les véhicules (pas de bouton "Pause" dédié sur beaucoup de modèles) +- Système de double-appui/appui long = **non-intuitif** et **risques sécurité** (regarder écran pour feedback) + +--- + +#### Commandes au volant simplifiées + +**Actions disponibles** (100% compatibles tous véhicules) : + +| Commande physique | Action RoadWave | +|-------------------|-----------------| +| **Suivant** | Passer au contenu suivant | +| **Précédent** | Revenir au contenu précédent (règle 10s, voir section 5.2) | +| **Play/Pause** | Pause/reprise lecture (fade out 0.3s) | + +**Aucune action complexe au volant** → Sécurité routière maximale. + +--- + +#### Like automatique implicite + +**Principe** : Le système détecte automatiquement l'intérêt utilisateur selon le temps d'écoute. + +**Règles d'attribution** : + +| Durée écoutée | Action automatique | Points jauge | Justification | +|---------------|-------------------|--------------|---------------| +| **≥ 80% du contenu** | Like renforcé | +2.0 | Écoute quasi-complète = fort intérêt | +| **30-79% du contenu** | Like standard | +1.0 | Écoute significative = intérêt | +| **< 30% du contenu** | Pas de like | 0 | Écoute trop courte | +| **Skip après <10s** | Signal négatif | -0.5 | Désintérêt marqué | + +**Exemples concrets** : +``` +Contenu de 3 minutes (180s) : +- Écoute 2min30 (83%) → Like renforcé (+2 points) +- Écoute 1min15 (42%) → Like standard (+1 point) +- Écoute 30s (17%) puis skip → Pas de like +- Skip après 5s → Signal négatif (-0.5 point) + +Contenu de 15 minutes (900s) : +- Écoute 13min (87%) → Like renforcé (+2 points) +- Écoute 6min (40%) → Like standard (+1 point) +``` + +--- + +#### Actions complémentaires (mode piéton uniquement) + +**Interface mobile** (vitesse < 5 km/h) : + +| Action | Moyen | Effet | +|--------|-------|-------| +| **Like explicite** | Bouton cœur | +2 points jauge (même si déjà liké auto) | +| **Unlike** | Re-clic cœur (toggle) | -2 points jauge | +| **Abonnement** | Bouton "S'abonner" profil créateur | +5 points toutes jauges tags créateur | +| **Désabonnement** | Bouton "Se désabonner" | -5 points | +| **Signalement** | Menu contextuel "⋮" | Ouverture flux modération | + +**Feedback visuel** : +- **Like automatique** : Badge discret "♥ Ajouté à vos favoris" (2s, bas de l'écran) +- **Like explicite** : Animation cœur rouge + vibration courte +- **Abonnement** : Animation étoile dorée + badge "Abonné ✓" + +**Disponibilité** : +- ✅ Mode piéton (vitesse < 5 km/h) : toutes les actions disponibles +- ❌ Mode voiture (vitesse ≥ 5 km/h) : aucune de ces actions (sauf like automatique) + +--- + +#### Gestion impacts jauges (algorithme) + +**Like automatique** : +- Like renforcé (≥80%) → **+2% jauges** de tous les tags du contenu +- Like standard (30-79%) → **+1% jauges** des tags du contenu +- Signal négatif (skip <10s) → **-0.5% jauges** des tags du contenu + +**Actions explicites** : +- Like manuel → **+2% jauges** (cumulable avec like auto) +- Unlike → **-2% jauges** +- Abonnement → **+5% toutes jauges** tags créateur +- Désabonnement → **-5% toutes jauges** + +**Persistance** : +- Événements stockés en base (table `listen_events`) +- Mise à jour jauges : **immédiate** (Redis) + **async batch** (PostgreSQL) + +--- + +#### Implémentation technique + +**Backend** (Go) : + +```go +type ListenEvent struct { + UserID string + ContentID string + StartedAt time.Time + StoppedAt time.Time + Duration int // secondes écoutées + ContentTotal int // durée totale contenu + Percentage float64 // duration / contentTotal * 100 + Action string // "completed", "skipped", "paused" +} + +func ProcessListenEvent(event ListenEvent) { + percentage := event.Percentage + + // Signal négatif fort + if event.Action == "skipped" && event.Duration < 10 { + UpdateJauges(event.UserID, event.ContentID, -0.5) + return + } + + // Like automatique + if percentage >= 80 { + AutoLike(event.UserID, event.ContentID, 2.0) // Renforcé + } else if percentage >= 30 { + AutoLike(event.UserID, event.ContentID, 1.0) // Standard + } + // < 30% : pas de like +} +``` + +**Mobile** (iOS/Android) : + +```swift +// iOS - Tracking écoute +class AudioPlayerManager { + var startTime: Date? + let contentDuration: TimeInterval + + func onPlay() { + startTime = Date() + } + + func onStop(action: String) { // "completed" | "skipped" | "paused" + guard let start = startTime else { return } + let duration = Date().timeIntervalSince(start) + let percentage = (duration / contentDuration) * 100 + + // API call + API.track(ListenEvent( + contentId: currentContentId, + duration: Int(duration), + percentage: percentage, + action: action + )) + } +} +``` + +--- + +#### Justification + +**Avantages** : +- ✅ **Sécurité routière maximale** : aucune action complexe au volant +- ✅ **UX intuitive** : comportement standard industrie (Spotify, YouTube Music, Deezer) +- ✅ **Compatibilité 100%** : fonctionne sur tous véhicules, tous OS +- ✅ **Engagement amélioré** : tous les contenus écoutés génèrent des signaux +- ✅ **Algorithme plus précis** : données granulaires (30%, 50%, 80%, 100%) +- ✅ **Simplicité développement** : pas de workarounds complexes iOS/Android + +**Inconvénients mitigés** : +- ⚠️ Pas de like explicite en conduite → **Mitigation** : like automatique + vocal (CarPlay/Android Auto) +- ⚠️ Pas d'abonnement en conduite → **Mitigation** : liste "Créateurs à découvrir" dans app +- ⚠️ Like automatique peut surprendre → **Mitigation** : onboarding clair + unlike possible + +--- + +#### Communication utilisateurs (onboarding) + +**Écran onboarding 1** : +``` +🚗 Conduite sécurisée + +RoadWave détecte automatiquement vos goûts +selon vos écoutes. + +Plus vous écoutez longtemps, plus +l'algorithme s'améliore ! + +[Suivant] +``` + +**Écran onboarding 2** : +``` +❤️ Likes automatiques + +Pas besoin de liker manuellement : +si vous écoutez >50% d'un contenu, +on comprend que vous aimez ! + +[Suivant] +``` + +**Écran onboarding 3** : +``` +⏸️ Commandes simples + +Utilisez les boutons au volant : +• Suivant → Prochain contenu +• Précédent → Contenu d'avant +• Pause → Mettre en pause + +[Commencer] +``` + +--- + +### 5.4 Lecture en boucle et enchaînement + +**Décision** : Passage automatique après 2s + insertion pub paramétrable + +**Fin de contenu** : +1. Audio termine → **Timer 2 secondes** démarre +2. UI overlay : "Contenu suivant dans 2s..." + barre décompte +3. Possibilité annuler : bouton "Rester sur ce contenu" (optionnel) +4. Timer atteint 0 → passage automatique au contenu suivant + +**Délai selon contexte** : + +| Mode | Délai | Justification | +|------|-------|---------------| +| **Standard** | 2 secondes | Temps réaction confortable | +| **Mode Kids** | 1 seconde | Attention courte enfants | +| **Live** | 0 seconde | Enchaînement immédiat | + +**Insertion publicité** : +- Pub s'insère **pendant le délai de 2s** (transition naturelle) +- Fréquence : **paramétrable admin** (défaut : 1 pub / 5 contenus) +- Message : "Publicité (15s)" puis lecture pub +- ⚠️ **Jamais d'interruption** d'un contenu en cours + +**Publicité skippable** : +- Durée minimale visionnage : **paramétrable** (défaut : 5 secondes) +- Bouton "Passer" apparaît après délai +- Métriques engagement : taux skip, durée écoute moyenne +- **Like et abonnement autorisés sur pub** (engagement créateur pub) + +**Si aucun contenu disponible** : +1. Message : "Aucun contenu disponible dans cette zone" +2. Proposition : "Élargir la zone de recherche ?" (bouton) +3. Si accepté → relance algo avec rayon +50km +4. Sinon → lecture en pause, attente action utilisateur + +**Gestion erreurs** : +- Échec chargement contenu suivant → **retry 3× avec backoff exponentiel** +- Si 3 échecs → message "Connexion instable, basculement mode offline" +- Mode offline → lecture contenus téléchargés uniquement + +**Justification** : +- **Fluidité** : enchaînement naturel sans action utilisateur +- **Contrôle** : possibilité annuler pendant délai +- **Paramétrabilité pub** : évite frustration excès publicité +- **Engagement pub** : like/abonnement autorisé = monétisation créateurs pub + +--- + +## Récapitulatif Section 5 diff --git a/docs/regles-metier/06-audio-guides-multi-sequences.md b/docs/regles-metier/06-audio-guides-multi-sequences.md new file mode 100644 index 0000000..dc771f6 --- /dev/null +++ b/docs/regles-metier/06-audio-guides-multi-sequences.md @@ -0,0 +1,736 @@ +## 16. Audio-guides multi-séquences + +### 16.1 Types d'audio-guides et modes de déplacement + +**Décision** : 4 modes distincts avec détection automatique + +#### 16.1.1 Classification par mode + +| Mode | Vitesse détection | Déclenchement | Use case | +|------|-------------------|---------------|----------| +| **🚶 Piéton** | <5 km/h | Manuel (bouton "Suivant") | Musées, visites urbaines, monuments | +| **🚗 Voiture** | >10 km/h | Auto GPS + Manuel possible | Safari-parc, routes touristiques, circuits auto | +| **🚴 Vélo** | 5-25 km/h | Auto GPS + Manuel possible | Pistes cyclables, circuits vélo, parcours nature | +| **🚌 Transport** | Variable | Auto GPS + Manuel possible | Bus touristiques, trains panoramiques | + +**Détection automatique** : +- Vitesse moyenne calculée sur 30 secondes +- Suggestion mode au démarrage : "Détection : 🚗 Voiture. Est-ce correct ? [Oui] [Changer]" +- User peut forcer mode manuellement (settings) + +**Justification** : +- Flexibilité maximale créateurs et utilisateurs +- Expériences optimisées par type de déplacement +- Gestion cas limites (vélo lent vs piéton rapide) + +--- + +#### 16.1.2 Création d'un audio-guide (côté créateur) + +**Formulaire création** : + +``` +┌────────────────────────────────────────┐ +│ Nouvel audio-guide multi-séquences │ +├────────────────────────────────────────┤ +│ Titre : [Safari du Paugre] │ +│ Description : [Découvrez les animaux │ +│ du parc en voiture...] │ +│ │ +│ Mode de déplacement : *obligatoire │ +│ ○ 🚶 Piéton (navigation manuelle) │ +│ ● 🚗 Voiture (GPS auto + manuel) │ +│ ○ 🚴 Vélo (GPS auto + manuel) │ +│ ○ 🚌 Transport (GPS auto + manuel) │ +│ │ +│ Vitesse recommandée : 30-50 km/h │ +│ (si voiture/vélo/transport) │ +│ │ +│ ──────────────────────────────────── │ +│ │ +│ Séquences (ordre lecture) : │ +│ │ +│ 1. [📍] Introduction - Point d'accueil │ +│ Lat: 43.1234, Lon: 2.5678 │ +│ Rayon déclenchement : 30m │ +│ Durée : 2:15 │ +│ [🎵 Audio uploadé] [✏️] [🗑️] │ +│ │ +│ 2. [📍] Enclos des lions │ +│ Lat: 43.1245, Lon: 2.5690 │ +│ Rayon déclenchement : 30m │ +│ Durée : 3:42 │ +│ [📤 Upload audio] [✏️] [🗑️] │ +│ │ +│ 3. [📍] Enclos des girafes │ +│ [+ Ajouter point GPS] │ +│ │ +│ [+ Ajouter séquence] │ +│ │ +│ 📊 Statistiques : │ +│ · 2 séquences complètes │ +│ · 5:57 durée totale │ +│ · 320m distance totale │ +│ │ +│ [🗺️ Aperçu sur carte] │ +│ [✅ Publier audio-guide] │ +└────────────────────────────────────────┘ +``` + +**Métadonnées obligatoires** : + +| Champ | Requis | Détails | +|-------|--------|---------| +| **Titre audio-guide** | ✅ | 5-100 caractères | +| **Description** | ✅ | 10-500 caractères | +| **Mode déplacement** | ✅ | Piéton / Voiture / Vélo / Transport | +| **Nombre séquences** | ✅ | Minimum 2, maximum 50 | +| **Point GPS par séquence** | ✅ (sauf piéton) | Latitude, longitude (WGS84) | +| **Rayon déclenchement** | ✅ (sauf piéton) | 10-100m selon mode | +| **Vitesse recommandée** | ❌ | Optionnel, affichée utilisateur | +| **Tags** | ✅ | 1-3 parmi liste prédéfinie | +| **Classification âge** | ✅ | Tout public / 13+ / 16+ / 18+ | +| **Zone diffusion** | ✅ | Polygon géographique | + +**Wizard de création** : +- Étape 1 : Infos générales (titre, description, mode) +- Étape 2 : Ajout séquences une par une +- Étape 3 : Preview carte (trace + points) +- Étape 4 : Validation modération (3 premiers audio-guides) + +**Justification** : +- Contrôle total créateur sur expérience +- Carte preview aide visualiser parcours +- Wizard guidé = réduction friction création + +--- + +### 16.2 Mode Piéton (manuel) + +**Décision** : Navigation manuelle avec pub auto-play + +#### 16.2.1 Passage entre séquences + +**Séquence normale (sans pub)** : + +1. Séquence 1 se termine +2. Player se met en **pause automatique** +3. Message affiché : "Séquence 1 terminée. Appuyez sur Suivant quand vous êtes prêt." +4. User appuie sur [▶|] → Séquence 2 démarre immédiatement + +**Séquence avec publicité** (1 pub / 5 séquences) : + +1. Séquence 2 se termine +2. **Publicité s'enchaîne automatiquement** (pas d'attente bouton) +3. Pub se lit (skippable après 5s) +4. Pub se termine → Player se met en **pause automatique** +5. Message : "Séquence 3 prête. Appuyez sur Suivant." +6. User appuie sur [▶|] → Séquence 3 démarre + +**Schéma flux** : +``` +Séquence 1 [fin] → PAUSE → User clique → Séquence 2 [fin] → PUB AUTO-PLAY → PAUSE → User clique → Séquence 3 +``` + +**Fréquence pub** : +- Gratuits : 1 pub toutes les 5 séquences (paramétrable admin 1/3 à 1/10) +- Premium : 0 pub + +**Justification** : +- Pub s'insère naturellement (pas d'attente utilisateur pour déclencher) +- User garde contrôle rythme visite (pause après pub) +- Monétisation effective créateurs +- Premium reste attractif (0 interruption) + +--- + +#### 16.2.2 Navigation et contrôles + +**Décision** : Liberté totale utilisateur + +**Contrôles disponibles** : + +| Bouton | Fonction | Comportement | +|--------|----------|--------------| +| **[▶\|] Suivant** | Passe séquence suivante | Immédiat, même si séquence actuelle pas terminée | +| **[\|◀] Précédent** | Retour séquence précédente | Saut direct séquence avant (pas de logique "replay si >10s") | +| **[⏸️] Pause** | Pause temporaire | Reprend à position exacte | +| **[▶️] Play** | Reprend lecture | Continue position actuelle | +| **Liste séquences** | Navigation libre | Tap séquence → saut direct (même séquences non écoutées) | + +**Interface liste séquences** : + +``` +┌────────────────────────────────────────┐ +│ 🚶 Audio-guide Piéton │ +│ Musée du Louvre │ +├────────────────────────────────────────┤ +│ [Cover image] │ +│ │ +│ ▶️ 0:00 ──●────────── 3:42 │ +│ │ +│ Séquence 3/12 : La Joconde │ +│ │ +│ [|◀] [⏸️] [▶|] │ +│ │ +│ ──────────────────────────────────── │ +│ │ +│ 📋 Liste des séquences │ +│ │ +│ ✅ 1. Introduction (2:15) │ +│ Écouté le 15/01/2026 │ +│ │ +│ ✅ 2. Pyramide du Louvre (1:48) │ +│ Écouté le 15/01/2026 │ +│ │ +│ ▶️ 3. La Joconde (3:42) - EN COURS │ +│ ──●──────────── 1:22/3:42 │ +│ │ +│ ⭕ 4. Vénus de Milo (2:58) │ +│ │ +│ ⭕ 5. Code d'Hammurabi (4:12) │ +│ │ +│ ⭕ 6. Victoire de Samothrace (3:25) │ +│ │ +│ ... +6 séquences │ +│ │ +│ [Tout afficher ▼] │ +└────────────────────────────────────────┘ +``` + +**Navigation libre** : +- User peut sauter séquences déjà connues +- User peut revenir en arrière à tout moment +- User peut aller directement à séquence 8 (même si 4-7 non écoutées) + +**Sauvegarde progression** : +- Checkmarks ✅ sur séquences écoutées >80% +- Position exacte sauvegardée dans séquence en cours + +**Justification** : +- Utilisateur contrôle 100% son rythme +- Adapté musées : visitor peut voir physiquement une œuvre lointaine et vouloir écouter sa description +- Pas de frustration (liberté totale) + +--- + +### 16.3 Mode Voiture (GPS automatique) + +**Décision** : GPS auto avec navigation manuelle conservée + +#### 16.3.1 Déclenchement et contrôles + +**Distinction audio-guides vs contenus géolocalisés simples** : + +⚠️ **Important** : Les audio-guides multi-séquences fonctionnent différemment des contenus géolocalisés simples. + +| Type | Séquences | Déclenchement | Notification | Enchaînement | Comptabilité quota | +|------|-----------|---------------|--------------|--------------|-------------------| +| **Contenu géolocalisé simple** | 1 séquence unique | Notification 7s avant (temps ETA) | Sonore + icône | Fin → retour buffer normal | 1 contenu = 1 quota | +| **Audio-guide multi-séquences** | 2 à 50 séquences | Au point GPS exact (distance 30m) | Ding + toast 2s | Séquences s'enchaînent auto | 1 audio-guide = 1 quota (toutes séquences) | + +**Fonctionnement GPS automatique** : + +1. User démarre audio-guide en voiture (voir section 16.1 pour démarrage) +2. Séquence 1 démarre automatiquement au point GPS défini (rayon 30m) +3. Séquence 1 se termine +4. **Affichage progress bar** : distance temps réel + ETA jusqu'au prochain point +5. User roule vers point GPS suivant +6. Arrivée au point GPS suivant (rayon 30m) → **déclenchement automatique** séquence suivante +7. Notification sonore discrète : "Ding" (0.3s) + toast 2s : "Enclos des girafes" +8. Séquence suivante démarre immédiatement (pas de décompte) + +**Pas de système "7 secondes avant" pour les audio-guides** : +- Contrairement aux contenus géolocalisés simples (voir [05-interactions-navigation.md](05-interactions-navigation.md#511-file-dattente-et-commande-suivant)) +- Les séquences se déclenchent **au point GPS exact** (rayon 30m) +- Raison : expérience guidée continue, user sait qu'il suit un parcours + +**Navigation manuelle CONSERVÉE** : + +| Bouton | État | Comportement | +|--------|------|--------------| +| **[▶\|] Suivant** | ✅ Toujours actif | Passe séquence suivante immédiatement (même hors point GPS) | +| **[\|◀] Précédent** | ✅ Toujours actif | Retour séquence précédente (même hors point GPS) | +| **[⏸️] Pause** | ✅ | Pause temporaire | +| **Liste séquences** | ✅ | Saut direct possible | + +**Use cases navigation manuelle** : + +| Situation | Solution manuelle | +|-----------|-------------------| +| Embouteillage (séquence finie, point GPS loin) | User clique Suivant → avance manuellement | +| Point GPS inaccessible (route fermée) | User clique Suivant → skip point | +| Envie réécouter séquence précédente | User clique Précédent → retour | +| Passager manipule l'app | Passager navigue librement | + +**Avertissement sécurité** : + +- Si vitesse **>10 km/h** ET user clique bouton (Suivant/Précédent) : + - Toast 3 secondes : "⚠️ Manipulation en conduite détectée. Pour votre sécurité, demandez à un passager." + - **Action quand même exécutée** (pas de blocage) +- Justification : sensibilisation sans bloquer (passager peut légitimement manipuler) + +**Schéma flux** : +``` +Point GPS 1 (30m) → Séquence 1 AUTO → User roule → Distance affichée → Point GPS 2 (30m) → Séquence 2 AUTO + ↓ + User clique Suivant (manuel) → Séquence 2 immédiate +``` + +**Justification** : +- Flexibilité maximale : GPS optimise expérience MAIS user garde contrôle +- Gestion cas limites : routes fermées, détours, embouteillages +- Sécurité : warning sensibilise sans bloquer (passager légitime) + +--- + +#### 16.3.2 Affichage distance et guidage + +**Décision** : Distance + direction (PAS de carte miniature) + +**Interface en conduite** : + +``` +┌────────────────────────────────────────┐ +│ 🚗 Audio-guide Voiture │ +│ Safari du Paugre │ +├────────────────────────────────────────┤ +│ │ +│ ▶️ 0:00 ──●────────── 2:15 │ +│ │ +│ Séquence 2/8 : Les lions │ +│ │ +│ ──────────────────────────────────── │ +│ │ +│ 📍 Prochain point │ +│ │ +│ Enclos des girafes │ +│ │ +│ ┌────────────────────────────────┐ │ +│ │ │ │ +│ │ ↗️ │ │ +│ │ (direction) │ │ +│ │ │ │ +│ │ 320 mètres │ │ +│ │ ≈ 40 secondes │ │ +│ │ │ │ +│ └────────────────────────────────┘ │ +│ │ +│ Vitesse actuelle : 28 km/h │ +│ Vitesse recommandée : 20-30 km/h │ +│ │ +│ [|◀] [⏸️] [▶|] [📋 Liste] │ +└────────────────────────────────────────┘ +``` + +**Affichage entre deux séquences** : + +Quand une séquence se termine et qu'il reste un point GPS suivant, l'interface bascule en mode "attente prochain point" : + +``` +┌────────────────────────────────────────┐ +│ 🚗 Audio-guide Voiture │ +│ Safari du Paugre │ +├────────────────────────────────────────┤ +│ │ +│ ✅ Séquence 2/8 terminée │ +│ Les lions │ +│ │ +│ ──────────────────────────────────── │ +│ │ +│ 📍 Prochain point │ +│ │ +│ Enclos des girafes │ +│ │ +│ ┌────────────────────────────────┐ │ +│ │ [Progress bar] │ │ +│ │ ████████░░░░░░░░░ 65% │ │ +│ │ │ │ +│ │ ↗️ │ │ +│ │ (direction) │ │ +│ │ │ │ +│ │ 320 mètres │ │ +│ │ ≈ 40 secondes │ │ +│ │ │ │ +│ └────────────────────────────────┘ │ +│ │ +│ Vitesse actuelle : 28 km/h │ +│ │ +│ [|◀] [▶️ Rejouer séq.] [▶|] │ +└────────────────────────────────────────┘ +``` + +**Progress bar dynamique** : +- Se remplit au fur et à mesure qu'on se rapproche du point +- Calcul : `progress = 100 - (distance_actuelle / distance_initiale * 100)` +- Exemple : distance initiale 500m, distance actuelle 175m → progress = 65% +- Couleur : vert (#4CAF50) pour la partie remplie, gris (#E0E0E0) pour le reste + +**Bouton "Rejouer séq."** : +- Permet de réécouter la séquence qui vient de se terminer +- User clique → séquence actuelle redémarre depuis 0:00 +- Utile si distraction pendant l'écoute + +--- + +**Informations affichées** : + +| Info | Mise à jour | Format | +|------|-------------|--------| +| **Distance** | Chaque seconde | "320 m" / "1.2 km" | +| **ETA** | Chaque seconde | "≈ 40 secondes" / "≈ 2 minutes" | +| **Direction** | Chaque 5s | Flèche indique direction (8 directions : ↑ ↗ → ↘ ↓ ↙ ← ↖) | +| **Vitesse actuelle** | Chaque seconde | "28 km/h" | +| **Vitesse recommandée** | Statique | "20-30 km/h" (définie par créateur) | +| **Progress bar** | Chaque seconde | Pourcentage parcouru vers prochain point | + +**Calcul direction** : + +```javascript +// Calcul angle entre position actuelle et prochain point +const currentGPS = getCurrentLocation(); +const nextPoint = audioGuide.sequences[currentIndex + 1].location; + +const angle = calculateBearing(currentGPS, nextPoint); // 0-360° + +// Conversion en flèche (8 directions) +const arrows = ['↑', '↗', '→', '↘', '↓', '↙', '←', '↖']; +const index = Math.round(angle / 45) % 8; +const direction = arrows[index]; +``` + +**Calcul ETA** : + +```javascript +const distance = calculateDistance(currentGPS, nextPoint); // mètres +const currentSpeed = getCurrentSpeed(); // km/h + +if (currentSpeed > 5) { + const eta = (distance / 1000) / currentSpeed * 3600; // secondes + return formatETA(eta); // "≈ 40 secondes" ou "≈ 2 minutes" +} else { + return "En attente de déplacement"; +} +``` + +**Justification** : +- Distance + ETA = info essentielle sans surcharge visuelle +- Direction (flèche) = aide se repérer sans carte complexe +- Simplicité = moins distraction conducteur +- Économie batterie (pas de rendu carte) + +--- + +#### 16.3.3 Rayon de déclenchement et tolérance + +**Décision** : Rayon configurable créateur avec défauts intelligents + +**Rayons par défaut** : + +| Mode | Rayon déclenchement | Rayon "point manqué" | Justification | +|------|---------------------|----------------------|---------------| +| **🚗 Voiture** | 30 mètres | 100 mètres | Vitesse élevée = anticipation | +| **🚴 Vélo** | 50 mètres | 75 mètres | Vitesse variable, arrêts fréquents | +| **🚌 Transport** | 100 mètres | 150 mètres | Arrêts bus/train, moins précis | + +**Configuration créateur** : + +- Curseur rayon : **10m → 200m** +- Défaut pré-sélectionné selon mode choisi +- Preview visuel : cercle sur carte (lors création) +- Suggestion auto : "Recommandé : 30m pour voiture à 30 km/h" + +**Gestion point manqué** : + +``` +User passe à 110m du point GPS +(hors rayon déclenchement 30m MAIS dans rayon tolérance 100m) + ↓ +Toast : "⚠️ Point manqué : Enclos des girafes" + ↓ +Popup 5 secondes : +┌────────────────────────────────────┐ +│ Point manqué │ +│ │ +│ "Enclos des girafes" │ +│ Vous êtes passé à 110m du point │ +│ │ +│ [🔊 Écouter quand même] │ +│ [⏭️ Passer au suivant] │ +│ [🔙 Faire demi-tour] │ +└────────────────────────────────────┘ +``` + +**Actions popup** : + +| Bouton | Comportement | +|--------|--------------| +| **Écouter quand même** | Lance séquence immédiatement (même hors zone) | +| **Passer au suivant** | Skip séquence, continue vers prochain point | +| **Faire demi-tour** | Lance navigation GPS externe (Google Maps / Waze) vers point manqué | + +**Si user au-delà rayon tolérance (>100m)** : +- Aucun popup (point trop loin, probablement hors itinéraire) +- User peut naviguer manuellement (bouton Suivant) + +**Justification** : +- Flexibilité créateur (ajuste selon terrain, vitesse prévue) +- Gestion intelligente imprévus (détours, routes fermées) +- User pas bloqué (toujours moyen avancer) + +--- + +### 16.4 Modes Vélo et Transport + +**Décision** : Même logique voiture avec tolérances ajustées + +**Différences par rapport à mode voiture** : + +| Paramètre | Voiture | Vélo | Transport | +|-----------|---------|------|-----------| +| **Rayon déclenchement** | 30m | 50m | 100m | +| **Rayon tolérance "point manqué"** | 100m | 75m | 150m | +| **Vitesse recommandée affichée** | 20-50 km/h | 10-25 km/h | Variable (selon ligne) | +| **Warning sécurité** | >10 km/h | >5 km/h | Désactivé | + +**Mode Vélo spécificités** : + +- Rayon plus large : vitesse variable, nombreux arrêts (feux, piétons) +- Warning sécurité dès 5 km/h (vélo en mouvement) +- Tolérance GPS moins stricte (tracé moins prévisible qu'auto) + +**Mode Transport spécificités** : + +- Rayon très large : arrêts fréquents (bus, train), ligne fixe +- Pas de warning sécurité (user = passager, pas conducteur) +- Vitesse recommandée = "Selon ligne" (pas de valeur fixe) +- Tolérance horaire : si bus en retard, point peut se déclencher avec 2-3 min de délai + +**Comportement identique voiture** : + +- Navigation manuelle conservée (boutons actifs) +- Affichage distance + ETA + direction +- Gestion point manqué +- Pub entre séquences + +**Justification** : +- Vélo : moins de contrôle qu'auto (obstacles, arrêts), nécessite tolérance +- Transport : moins de contrôle utilisateur (suit ligne fixe), rayon large compense +- Même UX globale = cohérence + +--- + +### 16.5 Publicités dans audio-guides + +**Décision** : Pub auto-play entre séquences TOUS modes + +#### 16.5.1 Règles universelles + +**Insertion publicité** : + +- Fréquence : **1 pub toutes les 5 séquences** (paramétrable admin 1/3 à 1/10) +- Gratuits uniquement, **Premium 0 pub** +- Pub s'enchaîne **automatiquement** après séquence +- Skippable après **5 secondes** (règle standard RoadWave) +- Volume normalisé -14 LUFS (comme pubs normales) + +**Comportement MODE PIÉTON** : + +``` +Séquence 2 [fin] + → Pub AUTO-PLAY + → Pub se termine + → PAUSE AUTO + → Message "Séquence 3 prête. Appuyez sur Suivant." + → User clique [▶|] + → Séquence 3 démarre +``` + +**Comportement MODE VOITURE/VÉLO/TRANSPORT** : + +``` +Séquence 2 [fin] + → Pub AUTO-PLAY + → Pub se termine + → ATTENTE point GPS suivant OU user clique Suivant + → Séquence 3 démarre +``` + +**Schéma complet** : + +| Mode | Après séquence normale | Après pub | +|------|------------------------|-----------| +| **Piéton** | Pause + attente user | Pause + attente user | +| **Voiture** | Attente GPS OU user clique Suivant | Attente GPS OU user clique Suivant | +| **Vélo** | Attente GPS OU user clique Suivant | Attente GPS OU user clique Suivant | +| **Transport** | Attente GPS OU user clique Suivant | Attente GPS OU user clique Suivant | + +**Justification** : +- Monétisation équitable créateurs (tous modes participent) +- Pub s'insère naturellement (auto-play, pas d'attente utilisateur) +- User garde contrôle : piéton clique Suivant, voiture peut skip manuel +- Premium reste attractif (expérience 0 interruption) +- Modèle économique viable + +--- + +#### 16.5.2 Métriques pub audio-guides + +**Dashboard créateur** : + +| Métrique | Affichage | +|----------|-----------| +| **Impressions pub** | Nombre de pubs insérées dans audio-guides | +| **Écoutes complètes pub** | Nombre de pubs écoutées >80% | +| **Taux skip pub** | % pubs skippées avant 5s vs après | +| **Revenus pub audio-guides** | 3€ / 1000 écoutes complètes (6% CA pub) | + +**Distinction contenus normaux vs audio-guides** : +- Dashboard sépare : "Revenus contenus classiques" / "Revenus audio-guides" +- Permet créateur voir performance par type + +**Justification** : +- Transparence créateur (comprend revenus) +- Incite création audio-guides (nouvelle source revenus) + +--- + +### 16.6 Reprise et sauvegarde progression + +**Décision** : Sauvegarde complète automatique avec popup intelligente + +#### 16.6.1 Sauvegarde automatique + +**Données sauvegardées** : + +| Info | Détail | Utilité | +|------|--------|---------| +| **Audio-guide ID** | Identifiant unique | Retrouver audio-guide | +| **Séquence actuelle** | Index (ex: 3/12) | Reprise position | +| **Position dans séquence** | Timestamp exact (ex: 1:42/3:20) | Reprise exacte | +| **Séquences écoutées** | Liste avec checkmarks ✅ | Historique progression | +| **Date dernière écoute** | Timestamp | Proposer reprise si <30j | +| **GPS dernière position** | Coordonnées optionnelles | Info contextuelle (non utilisée pour reprise) | + +**Stockage** : + +| Environnement | Technologie | Utilité | +|---------------|-------------|---------| +| **Local** | SQLite mobile | Fonctionnement offline | +| **Cloud** | PostgreSQL (sync auto) | Multi-device (reprendre sur autre appareil) | + +**Synchronisation** : +- Sauvegarde locale : chaque fin de séquence + chaque 30s +- Sync cloud : à la reconnexion réseau (batch) + +**Justification** : +- Expérience fluide (pas de perte progression) +- Multi-device (démarrer sur iPhone, continuer sur iPad) +- Offline-first (fonctionne sans réseau) + +--- + +#### 16.6.2 Interface de reprise + +**Conditions popup** : +- Dernière écoute **<30 jours** +- Progression **>0%** et **<100%** (pas terminé) + +**Popup reprise** : + +``` +┌────────────────────────────────────────┐ +│ Reprendre l'audio-guide ? │ +├────────────────────────────────────────┤ +│ 🚗 Safari du Paugre │ +│ @safari_createur │ +│ │ +│ Progression : 3/8 séquences écoutées │ +│ Dernière écoute : il y a 2 jours │ +│ │ +│ Vous étiez à : │ +│ "Les lions" (1:42/3:20) │ +│ │ +│ [▶️ Reprendre] [🔄 Recommencer] │ +│ [📋 Voir toutes les séquences] │ +└────────────────────────────────────────┘ +``` + +**Actions** : + +| Bouton | Comportement | +|--------|--------------| +| **Reprendre** | Continue séquence 3 à position 1:42 exacte | +| **Recommencer** | Reset progression, démarre séquence 1 depuis 0:00 | +| **Voir séquences** | Affiche liste complète, user choisit séquence départ | + +**Expiration progression** : +- Progression conservée **30 jours** +- Après 30j : popup "Audio-guide expiré. Recommencez depuis le début ?" +- Suppression données progression (mais historique "écouté" préservé) + +**Justification** : +- Contexte clair : user sait exactement où il en est +- Flexibilité : reprendre OU recommencer (choix utilisateur) +- 30 jours = raisonnable pour tourisme multi-jours ou retour ultérieur + +--- + +#### 16.6.3 Multi-device + +**Scénario** : + +1. User démarre audio-guide sur iPhone (séquences 1-3) +2. Progression sync cloud +3. Lendemain : user ouvre app sur iPad +4. Popup : "Reprendre Safari du Paugre sur cet appareil ?" +5. User clique Reprendre → continue séquence 4 + +**Conflit de version** : +- Si modifications simultanées 2 appareils (rare) : **dernière modification gagne** +- Toast : "Progression mise à jour depuis votre autre appareil" + +**Justification** : +- Confort utilisateur (change d'appareil librement) +- Use case réel : planning trajet sur tablette, écoute sur smartphone en voiture + +--- + +## Récapitulatif Section 16 + +| Point | Décision | Coût | Complexité | +|-------|----------|------|------------| +| **16.1** Types audio-guides | 4 modes (piéton/voiture/vélo/transport) avec détection auto | 0€ | Moyenne | +| **16.1.2** Création | Formulaire séquences + GPS + rayon + wizard guidé | 0€ | Moyenne | +| **16.2.1** Piéton - Passages | Manuel AVEC pub auto-play entre séquences, pause après | 0€ | Faible | +| **16.2.2** Piéton - Navigation | Liberté totale (skip, retour, saut direct liste) | 0€ | Faible | +| **16.3.1** Voiture - Déclenchement | GPS auto + boutons manuels actifs (warning sécurité si >10 km/h) | 0€ | Moyenne | +| **16.3.2** Voiture - Affichage | Distance + ETA + direction (flèche) + vitesse (PAS de carte) | 0€ | Faible | +| **16.3.3** Voiture - Rayon | Configurable créateur (défauts 30m/50m/100m selon mode) | 0€ | Faible | +| **16.4** Vélo & Transport | Mêmes règles avec tolérances ajustées + warning adapté | 0€ | Faible | +| **16.5** Publicités | 1/5 séquences, auto-play TOUS modes, skippable 5s | 0€ | Faible | +| **16.6.1** Sauvegarde | Complète (séquence + position + historique) local + cloud | 0€ | Faible | +| **16.6.2** Reprise | Popup intelligente avec choix (reprendre/recommencer), expiration 30j | 0€ | Faible | +| **16.6.3** Multi-device | Sync cloud PostgreSQL (reprendre sur autre appareil) | 0€ | Faible | + +**Coût total MVP : 0€** (GPS natif, calcul distance PostGIS) + +--- + +## Points d'attention pour Gherkin + +- Tester 4 modes audio-guides (détection vitesse auto) +- Tester création séquences avec points GPS + rayon configurable +- Tester mode piéton : pause après séquence + pub auto-play + pause après pub + clic Suivant +- Tester navigation libre piéton (skip, retour, saut direct liste) +- Tester mode voiture : déclenchement GPS auto rayon 30m +- Tester navigation manuelle voiture : boutons actifs + warning si vitesse >10 km/h +- Tester affichage distance + ETA + direction (flèche 8 directions) +- Tester rayon tolérance "point manqué" (popup 3 actions) +- Tester mode vélo (rayon 50m) et transport (rayon 100m) +- Tester insertion pub 1/5 séquences tous modes avec auto-play +- Tester sauvegarde progression locale + sync cloud +- Tester popup reprise (3 boutons : reprendre/recommencer/voir liste) +- Tester expiration progression 30 jours +- Tester multi-device : démarrer iPhone, continuer iPad +- Tester gestion conflit progression simultanée 2 appareils diff --git a/docs/regles-metier/07-contenus-geolocalises-voiture.md b/docs/regles-metier/07-contenus-geolocalises-voiture.md new file mode 100644 index 0000000..e8fc656 --- /dev/null +++ b/docs/regles-metier/07-contenus-geolocalises-voiture.md @@ -0,0 +1,757 @@ +## 17. Contenus géolocalisés en mode voiture + +### 17.1 Principe général + +**Objectif** : Proposer des contenus audio au moment précis où l'utilisateur passe devant un point d'intérêt géographique, pour enrichir son trajet avec des informations contextuelles liées au paysage. + +**Contrainte principale** : **Sécurité routière** +- Aucune distraction visuelle (pas de texte à lire) +- Notification sonore uniquement + icône minimale +- Validation par un seul bouton physique au volant ("Suivant") +- App doit être ouverte (premier plan) + +**Distinction avec audio-guides** : + +| Type | Description | Use case | +|------|-------------|----------| +| **Contenu géolocalisé simple** (cette section) | 1 point GPS unique, 1 contenu audio | Monument, panneau historique, point de vue | +| **Audio-guide multi-séquences** (section 16) | Plusieurs points GPS, séquences enchaînées | Safari-parc, circuit touristique, parcours urbain | + +--- + +### 17.2 Détection et notification + +#### 17.2.1 Calcul ETA (Estimated Time of Arrival) + +**Méthode** : API GPS native iOS/Android + +**Principe** : +- Calcul temps d'arrivée au point GPS basé sur vitesse actuelle et distance +- Notification déclenchée **7 secondes avant** d'atteindre le point +- Permet au conducteur d'avoir le temps de réagir (2s) + décompte (5s) + +**Implémentation iOS (Swift)** : + +```swift +import CoreLocation + +class GeoContentDetector { + let locationManager = CLLocationManager() + var geoContents: [GeoContent] = [] // Points GPS proches + + func calculateETA(to targetLocation: CLLocation) -> TimeInterval? { + guard let currentLocation = locationManager.location, + let currentSpeed = currentLocation.speed, // m/s + currentSpeed > 0 else { return nil } + + let distance = currentLocation.distance(from: targetLocation) // mètres + let eta = distance / currentSpeed // secondes + return eta + } + + func checkTriggers() { + // Appelé toutes les 1 seconde + for geoContent in geoContents { + let targetLocation = CLLocation( + latitude: geoContent.latitude, + longitude: geoContent.longitude + ) + + // Cas particulier : vitesse très faible + if let speed = locationManager.location?.speed, + speed < 1.4, // < 5 km/h + let distance = locationManager.location?.distance(from: targetLocation), + distance < 50 { + triggerNotification(for: geoContent) + continue + } + + // Cas normal : calcul ETA + if let eta = calculateETA(to: targetLocation), + eta <= 7.0 && eta > 0 { + triggerNotification(for: geoContent) + } + } + } +} +``` + +**Implémentation Android (Kotlin)** : + +```kotlin +import android.location.Location +import com.google.android.gms.location.FusedLocationProviderClient + +class GeoContentDetector(private val fusedLocationClient: FusedLocationProviderClient) { + private var geoContents: List = emptyList() + + fun calculateETA(targetLocation: Location, currentLocation: Location): Double? { + val speed = currentLocation.speed // m/s + if (speed <= 0) return null + + val distance = currentLocation.distanceTo(targetLocation) // mètres + return distance / speed // secondes + } + + fun checkTriggers(currentLocation: Location) { + for (geoContent in geoContents) { + val targetLocation = Location("").apply { + latitude = geoContent.latitude + longitude = geoContent.longitude + } + + // Cas particulier : vitesse très faible (< 5 km/h) + if (currentLocation.speed < 1.4 && + currentLocation.distanceTo(targetLocation) < 50) { + triggerNotification(geoContent) + continue + } + + // Cas normal : calcul ETA + val eta = calculateETA(targetLocation, currentLocation) + if (eta != null && eta <= 7.0 && eta > 0) { + triggerNotification(geoContent) + } + } + } +} +``` + +**Cas particulier : vitesse nulle ou très faible** : +- Si vitesse < 5 km/h (1.4 m/s) ET distance < 50m → notification immédiate +- Exemple : user arrêté à un feu rouge à 30m du point +- Évite notification trop tardive quand user redémarre + +**Fréquence de vérification** : +- GPS updates : toutes les 1 seconde (balance batterie/précision) +- Calcul ETA : à chaque update GPS +- Notification : déclenchée immédiatement quand ETA ≤ 7s + +--- + +#### 17.2.2 Format de la notification + +**Philosophie** : **Minimalisme absolu** pour sécurité routière + +❌ **Pas de** : +- Titre texte à lire +- Description longue +- Bouton "Annuler" ou "Plus tard" +- Popup bloquante +- Image/cover du contenu + +✅ **Uniquement** : +- Son bref (notification) +- Icône selon tag du contenu +- Compteur chiffres (7→1) + +**Icônes par tag** : + +| Tag | Icône | Couleur | +|-----|-------|---------| +| Culture générale | 🏛️ | Bleu | +| Histoire | 📜 | Marron | +| Voyage | ✈️ | Vert | +| Famille | 👨‍👩‍👧 | Orange | +| Musique | 🎵 | Rose | +| Sport | ⚽ | Rouge | +| Technologie | 💻 | Gris | +| Automobile | 🚗 | Noir | +| Politique | 🏛️ | Bleu foncé | +| Économie | 📈 | Vert foncé | +| Cryptomonnaie | ₿ | Or | +| Santé | 🏥 | Rouge clair | +| Amour | ❤️ | Rose vif | + +**Interface visuelle (minimaliste)** : + +``` +┌────────────────────────────────────────┐ +│ [Player actuel] │ +│ ▶️ Podcast en cours... │ +│ 0:00 ────●──────────── 12:34 │ +│ │ +│ [|◀] [⏸️] [▶|] │ +├────────────────────────────────────────┤ +│ 🏛️ │ +│ 7 │ +└────────────────────────────────────────┘ +``` + +**Évolution du compteur** : +- Affichage pendant 7 secondes +- Compteur décrémente : 7 → 6 → 5 → 4 → 3 → 2 → 1 → disparaît +- Police grande (72pt), bold, couleur blanche +- Background semi-transparent (noir 50% opacity) + +**Notification sonore** : +- **Son** : bip court (0.5s) ou "ding" doux personnalisé RoadWave +- **Volume** : suit le volume système notification (indépendant du volume media) +- **Pas de vibration** : inutile en voiture (téléphone sur support) +- **Configurable** : user peut choisir dans settings : + - Bip système + - Son RoadWave personnalisé + - Muet (visuel uniquement) + +--- + +#### 17.2.2b Conformité CarPlay / Android Auto + +**Décision** : Notification sonore uniquement en mode CarPlay/Android Auto + +**Contexte** : +- [CarPlay Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/carplay) interdisent les overlays qui "takeover" l'écran +- [Android Auto Media Apps Guidelines](https://developer.android.com/training/cars/media) imposent des interactions minimales +- Sécurité routière maximale = pas de distraction visuelle + +**Comportement en mode CarPlay/Android Auto** : + +❌ **Désactivé** : +- Icône overlay +- Compteur visuel (7...6...5...) +- Tout élément graphique supplémentaire + +✅ **Activé uniquement** : +- Notification sonore (bip ou ding) +- Bouton "Suivant" standard (déjà présent) + +**Détection automatique** : + +```swift +// iOS - Détection CarPlay +import MediaPlayer + +class NotificationManager { + var isCarPlayActive: Bool { + if #available(iOS 14.0, *) { + return MPNowPlayingInfoCenter.default().playbackState == .playing && + MPPlayableContentManager.shared().context != nil + } + return false + } + + func showGeoNotification(content: GeoContent) { + if isCarPlayActive { + // CarPlay : sonore uniquement + AudioServicesPlaySystemSound(1052) // Notification beep + // Pas d'overlay visuel + } else { + // Mode normal : sonore + icône + compteur + showFullNotification(content) + } + } +} +``` + +```kotlin +// Android - Détection Android Auto +import android.car.Car + +class NotificationManager(private val context: Context) { + private fun isAndroidAutoActive(): Boolean { + val carManager = context.getSystemService(Context.CAR_SERVICE) as? Car + return carManager?.isConnected == true + } + + fun showGeoNotification(content: GeoContent) { + if (isAndroidAutoActive()) { + // Android Auto : sonore uniquement + val notification = NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notification) + .setSound(defaultSoundUri) + .setCategory(NotificationCompat.CATEGORY_NAVIGATION) + .build() + notificationManager.notify(NOTIFICATION_ID, notification) + } else { + // Mode normal : sonore + overlay visuel + showFullNotification(content) + } + } +} +``` + +**Expérience utilisateur en CarPlay/Android Auto** : + +1. User roule, app connectée à CarPlay/Android Auto +2. ETA 7s atteint → notification sonore uniquement (bip) +3. User entend le bip, sait qu'un contenu géolocalisé est disponible +4. User appuie sur bouton "Suivant" physique au volant +5. Décompte 5s démarre (audio continue normalement) +6. Contenu géolocalisé démarre après 5s + +**Justification** : +- ✅ Conformité maximale CarPlay/Android Auto guidelines +- ✅ Sécurité routière (pas de distraction visuelle) +- ✅ User peut toujours valider via bouton "Suivant" standard +- ✅ Apps comparables (Waze, Apple Maps) utilisent alertes sonores similaires + +**Alternative envisagée** : Mini-badge sur bouton "Suivant" +- Moins invasif qu'un compteur +- Mais toujours considéré comme overlay (zone grise) +- **Décision** : Privilégier conformité maximale (sonore uniquement) + +--- + +#### 17.2.3 Décompte après validation + +**User appuie sur "Suivant"** : + +**Étape 1 : Transition visuelle (0.3s)** +- Icône + compteur "7" disparaissent avec fade out +- Nouveau compteur "5" apparaît (plus grand, centré) + +**Étape 2 : Décompte 5 secondes** +- Compteur décrémente : 5 → 4 → 3 → 2 → 1 +- Contenu actuel continue de jouer **normalement** (pas de baisse volume) +- User entend le contenu en cours pendant le décompte + +**Étape 3 : Fin du décompte** +- Compteur atteint "0" +- Fade out 0.3s du contenu actuel +- Fade in 0.3s du contenu géolocalisé +- Transition audio fluide (pas de silence) + +**Cas particulier : contenu actuel se termine pendant le décompte** + +Exemple : User clique "Suivant", décompte démarre à 5s, mais contenu actuel se termine après 2s. + +``` +Timeline : +T+0s : User clique "Suivant", décompte démarre (5...4...3...) +T+2s : Contenu actuel se termine + → Contenu suivant du buffer démarre immédiatement + → Décompte continue (2...1...) +T+5s : Décompte atteint 0 + → Fade out du contenu buffer (celui qui a démarré à T+2s) + → Fade in du contenu géolocalisé +``` + +**Justification** : +- Évite silence inconfortable pendant décompte +- User continue d'écouter du contenu intéressant +- Transition naturelle + +**Interface pendant décompte** : + +``` +┌────────────────────────────────────────┐ +│ [Player actuel] │ +│ ▶️ Podcast en cours... │ +│ 0:00 ────●──────────── 12:34 │ +│ │ +│ [|◀] [⏸️] [▶|] │ +├────────────────────────────────────────┤ +│ │ +│ 3 │ +│ │ +│ Contenu géolocalisé arrive │ +└────────────────────────────────────────┘ +``` + +--- + +### 17.3 Limitation anti-spam + +**Problème identifié** : +- Routes riches en points d'intérêt (parcours touristiques) +- Risque de notifier toutes les 30 secondes +- Fatigue utilisateur + distraction conducteur + +**Solution : quota horaire + cooldown** + +#### 17.3.1 Quota : 6 contenus géolocalisés par heure + +**Règle** : +- Maximum **6 contenus géolocalisés** notifiés par heure +- Fenêtre glissante : calcul sur les 60 dernières minutes (pas réinitialisation à 0h) +- Si quota atteint : notifications suivantes ignorées silencieusement + +**Exemple** : +``` +10h05 : Contenu 1 notifié ✅ (quota : 1/6) +10h12 : Contenu 2 notifié ✅ (quota : 2/6) +10h18 : Contenu 3 notifié ✅ (quota : 3/6) +10h25 : Contenu 4 notifié ✅ (quota : 4/6) +10h33 : Contenu 5 notifié ✅ (quota : 5/6) +10h41 : Contenu 6 notifié ✅ (quota : 6/6) +10h52 : Contenu 7 détecté mais ignoré ❌ (quota atteint) +11h06 : Contenu 1 expire (>60 min) → quota libéré (5/6) +11h08 : Contenu 8 notifié ✅ (quota : 6/6) +``` + +**Exception : audio-guides multi-séquences** : +- Un audio-guide avec N séquences compte comme **1 seul contenu** dans le quota +- Une fois démarré, toutes ses séquences sont jouables (pas de limite) +- Exemple : audio-guide 8 séquences = 1 quota, contenus simples restants = 5 + +**Implémentation backend (Redis)** : + +```go +// Structure quota (sorted set avec timestamps) +// Clé : user:{user_id}:geo_quota +// Valeur : ZADD avec timestamp + +func CanNotify(userID string) bool { + key := fmt.Sprintf("user:%s:geo_quota", userID) + now := time.Now().Unix() + oneHourAgo := now - 3600 + + // Supprimer entrées > 1h + redis.ZRemRangeByScore(key, "-inf", oneHourAgo) + + // Compter entrées restantes + count := redis.ZCard(key) + + return count < 6 +} + +func RecordNotification(userID string, contentID string) { + key := fmt.Sprintf("user:%s:geo_quota", userID) + now := time.Now().Unix() + + // Ajouter entrée avec timestamp actuel + redis.ZAdd(key, now, contentID) + + // TTL 1h (sécurité) + redis.Expire(key, 3600) +} +``` + +--- + +#### 17.3.2 Cooldown : 10 minutes après notification ignorée + +**Règle** : +- Si user ne clique pas sur "Suivant" pendant les 7 secondes +- → Cooldown de **10 minutes** activé +- → Aucune nouvelle notification pendant ce délai +- → Même si quota non atteint + +**Justification** : +- User a probablement une raison de ne pas vouloir de contenu géolocalisé maintenant +- Évite harcèlement (notifications répétées ignorées) +- Respecte choix implicite de l'utilisateur + +**Timeline exemple** : +``` +10h00 : Notification contenu A (7s) +10h00+7s : User n'a pas cliqué → cooldown activé +10h05 : Contenu B détecté → ignoré (cooldown actif) +10h08 : Contenu C détecté → ignoré (cooldown actif) +10h10 : Cooldown expire +10h11 : Contenu D détecté → notification affichée ✅ +``` + +**Implémentation (Redis)** : + +```go +func IsInCooldown(userID string) bool { + key := fmt.Sprintf("user:%s:geo_cooldown", userID) + exists := redis.Exists(key) + return exists == 1 +} + +func ActivateCooldown(userID string) { + key := fmt.Sprintf("user:%s:geo_cooldown", userID) + redis.Set(key, "1", 10*time.Minute) +} +``` + +**Exception : notification validée (user a cliqué)** : +- Pas de cooldown si user a cliqué sur "Suivant" +- Même si user skip le contenu ensuite (pendant le décompte ou après) +- Cooldown = pénalité uniquement pour notification complètement ignorée + +--- + +### 17.4 Navigation avec contenus géolocalisés + +#### 17.4.1 Historique de navigation + +**Structure** (voir [05-interactions-navigation.md](05-interactions-navigation.md#52-commande-précédent)) : + +- **10 contenus maximum** en mémoire (Redis List) +- Structure : `[{content_id, position_seconds, listened_at, type}, ...]` +- FIFO : au-delà de 10, suppression du plus ancien + +**Comportement avec contenus géolocalisés** : + +Les contenus géolocalisés s'intègrent dans l'historique comme des contenus normaux : + +``` +Historique : +[ + {id: "buffer_1", position: 180, type: "contextuel"}, + {id: "geo_tour_eiffel", position: 42, type: "geo_anchored"}, + {id: "buffer_2", position: 90, type: "neutre"}, + {id: "buffer_3", position: 0, type: "contextuel"} +] +``` + +--- + +#### 17.4.2 Commande "Précédent" avec contenu géolocalisé + +**Cas A : User écoute contenu géolocalisé, puis skip** + +Timeline : +1. Contenu buffer_1 joue (position 3:00) +2. Notification contenu géolocalisé +3. User clique "Suivant" → décompte 5s → contenu géolocalisé démarre +4. User écoute 42 secondes du contenu géolocalisé +5. User appuie "Suivant" (skip) → contenu buffer_2 démarre +6. User appuie "Précédent" → **retour au contenu géolocalisé à 42s** + +**Règle** : Comme décrit dans [05-interactions-navigation.md](05-interactions-navigation.md#52-commande-précédent) : +- Si temps écouté ≥ 10 secondes → replay contenu actuel depuis début +- Si temps écouté < 10 secondes → retour contenu précédent (position exacte) + +Dans ce cas : 42s ≥ 10s → le contenu géolocalisé reprend à 42s (où user l'avait arrêté) + +--- + +**Cas B : User ne clique pas pendant notification (ignore)** + +Timeline : +1. Contenu buffer_1 joue +2. Notification contenu géolocalisé (7s) +3. User ne clique pas → notification disparaît +4. Contenu buffer_1 continue +5. User appuie "Précédent" → **retour contenu avant buffer_1** + +**Règle** : Contenu géolocalisé ignoré n'entre PAS dans l'historique. + +--- + +**Cas C : User skip pendant le décompte** + +Timeline : +1. Notification contenu géolocalisé +2. User clique "Suivant" → décompte 5s démarre (5...4...3...) +3. User re-clique "Suivant" pendant le décompte → **annule le décompte** +4. Contenu suivant du buffer démarre +5. Contenu géolocalisé n'entre PAS dans l'historique + +**Justification** : User a explicitement annulé, pas d'intérêt pour ce contenu. + +--- + +### 17.5 Basculement automatique voiture ↔ piéton + +**Principe** : Détection automatique du mode selon vitesse GPS, sans action utilisateur. + +#### 17.5.1 Calcul vitesse moyenne + +**Méthode** : Moyenne glissante sur 30 secondes + +```javascript +// Historique GPS : 30 derniers points (1 point/seconde) +const gpsHistory = [ + {lat: 48.8584, lon: 2.2945, speed: 8.3, timestamp: 1642861200}, + {lat: 48.8585, lon: 2.2946, speed: 8.5, timestamp: 1642861201}, + // ... 28 autres points +]; + +// Calcul vitesse moyenne +const avgSpeed = gpsHistory + .reduce((sum, point) => sum + point.speed, 0) / gpsHistory.length; + +// km/h = m/s × 3.6 +const avgSpeedKmh = avgSpeed * 3.6; + +if (avgSpeedKmh < 5) { + mode = "piéton"; +} else { + mode = "voiture"; +} +``` + +**Hysteresis (éviter basculements intempestifs)** : +- Nouvelle vitesse doit être stable pendant **10 secondes** avant basculement +- Exemple : user passe de 20 km/h à 3 km/h (arrêt feu rouge) + - Si vitesse remonte à 20 km/h après 8s → pas de basculement + - Si vitesse reste à 3 km/h pendant 10s → basculement piéton + +--- + +#### 17.5.2 Effets du basculement + +**Passage voiture → piéton** : + +| Paramètre | Avant (voiture) | Après (piéton) | +|-----------|----------------|----------------| +| **App requise** | Premier plan | Arrière-plan OK | +| **Notification** | Sonore + icône + compteur | Push système standard | +| **Rayon détection** | ETA 7s (distance variable) | 200 mètres fixes | +| **Type contenu** | Tous contenus géolocalisés | Audio-guides uniquement | + +**Passage piéton → voiture** : + +| Paramètre | Avant (piéton) | Après (voiture) | +|-----------|---------------|----------------| +| **App requise** | Arrière-plan OK | Premier plan | +| **Notification** | Push système standard | Sonore + icône + compteur | +| **Rayon détection** | 200 mètres fixes | ETA 7s (distance variable) | +| **Type contenu** | Audio-guides uniquement | Tous contenus géolocalisés | + +**Transition fluide** : +- Pas de popup ou message à l'utilisateur +- Basculement invisible et automatique +- Permissions ajustées automatiquement (si déjà accordées) + +--- + +### 17.6 Edge cases + +#### 17.6.1 User passe trop vite devant le point (>130 km/h) + +**Scénario** : Autoroute A6, vitesse 130 km/h, contenu géolocalisé détecté. + +**Calcul** : +- Vitesse : 130 km/h = 36.1 m/s +- ETA 7s → distance notification : 7 × 36.1 = **252 mètres** avant le point +- User a 7s pour cliquer "Suivant" +- Décompte 5s démarre +- À 130 km/h, user parcourt : 5 × 36.1 = 180m pendant décompte +- Distance restante au démarrage contenu : 252 - 180 = **72 mètres avant le point** + +**Conclusion** : Le système fonctionne même à très haute vitesse ✅ + +**Cas extrême : 180 km/h** (illégal mais théoriquement possible) : +- Vitesse : 180 km/h = 50 m/s +- ETA 7s → distance notification : 350m avant le point +- Décompte 5s : user parcourt 250m +- Distance restante : 350 - 250 = **100m avant le point** +- Contenu démarre encore avant le point ✅ + +--- + +#### 17.6.2 Multiples points géolocalisés proches + +**Scénario** : Route départementale avec 3 châteaux espacés de 800m chacun. + +``` +Château A ----800m---- Château B ----800m---- Château C +``` + +À 50 km/h (13.9 m/s), user met 57 secondes pour parcourir 800m. + +**Timeline** : +``` +T+0s : Notification Château A (7s avant) +T+7s : User clique "Suivant" → décompte 5s +T+12s : Contenu Château A démarre +T+30s : Contenu Château A se termine (durée 18s) +T+30s : Retour buffer normal (contenu contextuel) +T+50s : Notification Château B devrait se déclencher + MAIS : quota 1/6 utilisé, pas de cooldown (user a validé A) + → Notification affichée ✅ +T+57s : User clique "Suivant" → contenu Château B démarre +... +``` + +**Problème** : Si user ignore une notification, cooldown 10 min bloque les suivantes. + +**Solution proposée** : **Ajuster le cooldown selon validation précédente** + +Nouvelle règle : +- Notification validée (user a cliqué) : pas de cooldown +- Notification ignorée (user n'a pas cliqué) : cooldown 10 min +- Exception : si 2+ notifications validées consécutives, cooldown réduit à 5 min + +**Implémentation** : + +```go +func CalculateCooldown(userID string) time.Duration { + // Récupérer historique dernières notifications + history := getNotificationHistory(userID, 3) // 3 dernières + + validatedCount := 0 + for _, notif := range history { + if notif.Validated { + validatedCount++ + } + } + + if validatedCount >= 2 { + return 5 * time.Minute // Cooldown réduit + } + return 10 * time.Minute // Cooldown standard +} +``` + +--- + +#### 17.6.3 User arrêté longtemps près d'un point (parking) + +**Scénario** : User se gare à 30m d'un château, sort de voiture, visite 1h, revient. + +**Problème** : +- Vitesse < 5 km/h + distance < 50m → notification immédiate +- Mais user en mode "stationnement", pas en mode "conduite" + +**Solution** : **Détection mode stationnement** + +Règle : +- Si vitesse < 1 km/h pendant **2 minutes** consécutives +- → Mode "stationnement" activé +- → Pas de notification de contenus géolocalisés +- → Basculement automatique en mode piéton (push arrière-plan) + +**Reprise conduite** : +- Vitesse > 5 km/h pendant 10s +- → Mode "voiture" réactivé +- → Notifications reprennent (si quota non atteint) + +--- + +### 17.7 Récapitulatif + +| Point | Décision | Justification | +|-------|----------|---------------| +| **Détection** | ETA 7s avant point (API GPS native) | Précis, fiable, adapté à toutes vitesses | +| **Notification** | Sonore + icône + compteur (pas de texte) | Sécurité routière, minimalisme | +| **Validation** | Bouton "Suivant" uniquement | Un seul geste, simple, pas de distraction | +| **Décompte** | 5s avec contenu actuel qui continue | Évite silence, fluidité | +| **Quota** | 6 contenus/heure, fenêtre glissante | Anti-spam efficace | +| **Cooldown** | 10 min après notification ignorée | Respecte choix utilisateur | +| **Exception audio-guides** | Toutes séquences comptent comme 1 | Encourage création audio-guides | +| **Basculement auto** | Vitesse < 5 km/h = piéton, ≥ 5 km/h = voiture | Transparent, pas de friction | +| **Mode stationnement** | Vitesse < 1 km/h pendant 2 min → piéton | Évite notifications inutiles | + +--- + +## Récapitulatif Section 17 + +| Point | Décision | Coût | Complexité | +|-------|----------|------|------------| +| **17.1** Principe général | Notification 7s avant, app ouverte requise | 0€ | Faible | +| **17.2.1** Calcul ETA | API GPS native iOS/Android | 0€ | Faible | +| **17.2.2** Format notification | Sonore + icône + compteur (minimaliste) | 0€ | Faible | +| **17.2.3** Décompte 5s | Contenu actuel continue, transition fluide | 0€ | Faible | +| **17.3.1** Quota 6/h | Redis sorted set, fenêtre glissante | 0€ | Moyenne | +| **17.3.2** Cooldown 10 min | Redis TTL après ignorance | 0€ | Faible | +| **17.4** Historique navigation | 10 contenus max, FIFO, comme contenus normaux | 0€ | Faible | +| **17.5** Basculement auto | Vitesse moyenne 30s, hysteresis 10s | 0€ | Moyenne | +| **17.6** Edge cases | Haute vitesse, points multiples, stationnement | 0€ | Moyenne | + +**Coût total MVP : 0€** (GPS natif, Redis existant) + +--- + +## Points d'attention pour Gherkin + +- Tester calcul ETA à différentes vitesses (10 km/h, 50 km/h, 130 km/h) +- Tester cas vitesse < 5 km/h ET distance < 50m (notification immédiate) +- Tester affichage notification (icône + compteur, pas de texte) +- Tester validation "Suivant" → décompte 5s → contenu démarre +- Tester contenu actuel se termine pendant décompte → buffer démarre +- Tester quota 6/h : 7e notification ignorée +- Tester cooldown 10 min après notification ignorée +- Tester exception audio-guides (toutes séquences = 1 quota) +- Tester navigation "Précédent" après skip contenu géolocalisé (position sauvegardée) +- Tester basculement voiture → piéton (vitesse < 5 km/h stable 10s) +- Tester basculement piéton → voiture (vitesse ≥ 5 km/h stable 10s) +- Tester mode stationnement (vitesse < 1 km/h pendant 2 min) +- Tester multiples points proches (notifications successives si quota OK) +- Tester haute vitesse (130 km/h) : notification 252m avant, contenu démarre au bon moment diff --git a/docs/regles-metier/08-mode-offline.md b/docs/regles-metier/08-mode-offline.md new file mode 100644 index 0000000..613b368 --- /dev/null +++ b/docs/regles-metier/08-mode-offline.md @@ -0,0 +1,150 @@ +## 11. Mode offline + +### 11.1 Téléchargement + +**Zone géographique** : Choix manuel utilisateur + +**Options prédéfinies** : +- "Autour de moi" (rayon 50 km position actuelle) +- "Ma ville" (limite administrative détectée) +- "Mon département" (sélection liste) +- "Ma région" (sélection liste) +- Recherche manuelle : "Paris", "Lyon", "Marseille", etc. + +**Nombre de contenus téléchargeables** : + +| Statut | Limite | Affichage | +|--------|--------|-----------| +| **Gratuit** | 50 contenus max | "12/50 contenus téléchargés" | +| **Premium** | Illimité | "245 contenus (3.2 GB)" | + +**Calcul temps disponible** : +- 50 contenus × 5 min moyenne = 250 min = **4h d'écoute** (suffisant pour gratuits) +- Premium illimité = limité uniquement par espace disque device + +**Connexion WiFi/Mobile** : + +**Par défaut** : WiFi uniquement + +**Sur données mobiles** : +1. User clique "Télécharger" +2. Détection : pas de WiFi +3. Popup : "Vous n'êtes pas connecté en WiFi. Télécharger via données mobiles consommera environ **X MB**. Continuer ?" +4. Boutons : "Attendre WiFi" / "Continuer" + +**Calcul estimation** : +``` +Nombre contenus × durée moyenne × bitrate qualité +Exemple : 20 contenus × 5 min × 48 kbps = ~72 MB +``` + +**Qualité audio téléchargement** : + +| Qualité | Bitrate | Taille | Disponibilité | +|---------|---------|--------|---------------| +| **Basse** | 24 kbps | ~10 MB/h | Gratuit + Premium | +| **Standard** | 48 kbps | ~20 MB/h | Gratuit + Premium (défaut) | +| **Haute** | 64 kbps | ~30 MB/h | **Premium uniquement** | + +**Justification** : +- Standard = bon compromis qualité/taille (Opus 48 kbps = très correct pour voix) +- Haute réservée Premium = incitation upgrade +- User peut réduire à "basse" si espace limité + +--- + +### 11.2 Validité et renouvellement + +**Durée de validité** : 30 jours après téléchargement + +**Standard industrie** : +- Spotify : 30 jours +- YouTube Music : 30 jours +- Deezer : 30 jours + +**Renouvellement automatique** : + +``` +App détecte WiFi + contenus >25 jours +→ Requête API : GET /offline/contents/refresh +→ Backend vérifie pour chaque contenu : + - Abonnement Premium toujours actif ? + - Contenu pas modéré/supprimé ? + - Métadonnées à jour ? +→ Renouvelle validité à 30 jours supplémentaires +→ Mise à jour métadonnées (titre, créateur, statut) +→ Pas de re-téléchargement audio (sauf si fichier corrompu) +``` + +**Notification avant expiration** : +- **J-3** : "X contenus expirent dans 3 jours. Connectez-vous en WiFi pour les renouveler" +- **J-0** : Suppression automatique +- **J+0** : Toast "15 contenus expirés ont été supprimés" + +**Justification** : +- **Force reconnexion** : vérifier abonnement actif, contenus légaux +- **Évite stockage obsolète** : contenus supprimés/modérés ne restent pas +- **UX transparente** : renouvellement silencieux si WiFi régulier + +--- + +### 11.3 Synchronisation actions offline + +**Actions stockées localement (SQLite)** : +- Likes/unlikes +- Abonnements/désabonnements +- Signalements +- Progression audio-guides + +**Sync automatique à la reconnexion** : + +``` +1. App détecte reconnexion Internet +2. Récupération queue locale : SELECT * FROM pending_actions ORDER BY created_at +3. Envoi batch API : POST /sync/actions +4. Backend traite chaque action +5. Confirmation réception : DELETE FROM pending_actions WHERE id IN (...) +6. Toast : "3 likes et 1 abonnement synchronisés" +``` + +**Gestion erreurs sync** : +- Si échec après 3 tentatives → notification : "Impossible de synchroniser. Réessayez plus tard" +- Actions conservées jusqu'à sync réussie (pas de perte) +- **Rétention max 7 jours** : après = purge (évite queue infinie) + +**Conflits contenus supprimés** : + +``` +Backend retourne : {deleted_content_ids: [123, 456]} +→ App supprime fichiers locaux +→ Si contenu 123 en cours d'écoute : + - Attendre fin lecture actuelle + - Passage auto suivant après 2s +→ Toast : "1 contenu téléchargé a été retiré (violation règles)" +``` + +**Justification** : +- **Pas de conflit possible** : actions unilatérales user (likes/abonnements) +- **UX fluide** : pas de blocage offline +- **Batch = économie** : requêtes HTTP groupées +- **Conformité modération** : contenu illégal disparaît même offline + +--- + +## Récapitulatif Section 11 + +| Aspect | Décision | Valeur | +|--------|----------|--------| +| **Zone téléchargement** | Choix | Manuel (autour/ville/département/région/recherche) | +| **Limite gratuit** | Contenus | 50 max | +| **Limite Premium** | Contenus | Illimité (espace disque) | +| **Connexion** | Par défaut | WiFi (mobile avec confirmation) | +| **Qualité Standard** | Bitrate | 48 kbps Opus | +| **Qualité Haute** | Bitrate | 64 kbps (Premium uniquement) | +| **Validité** | Durée | 30 jours | +| **Renouvellement** | Mode | Automatique si WiFi | +| **Notification expiration** | Délai | J-3 | +| **Sync actions** | Mode | Batch automatique reconnexion | +| **Rétention queue** | Durée | 7 jours max | + +--- diff --git a/docs/regles-metier/09-abonnements-notifications.md b/docs/regles-metier/09-abonnements-notifications.md new file mode 100644 index 0000000..68c3639 --- /dev/null +++ b/docs/regles-metier/09-abonnements-notifications.md @@ -0,0 +1,319 @@ +## 8. Abonnements et notifications + +### 8.1 Impact sur l'algorithme + +**Décision** : Boost +30% au score + reste dans le mix + +**Boost de score abonnements** : +- **+30% au score final** pour contenus d'un créateur suivi +- Application : multiplicateur sur le score calculé + +``` +score_final_avec_boost = score_final × 1.3 +``` + +**Reste dans le mix** : +- ❌ **Pas de priorité absolue** (pas de file dédiée abonnements) +- ✅ Contenu suivi entre en **compétition avec autres contenus** +- ✅ Si créateur suivi publie contenu faible engagement → peut être battu par contenu viral non-suivi + +**Exemple concret** : +``` +Utilisateur à Paris, 2 contenus disponibles : + +Contenu A (créateur NON suivi) : +- Score géo : 0.9 (très proche) +- Score intérêts : 0.8 +- Score engagement : 0.7 +→ Score final : 0.80 + +Contenu B (créateur suivi) : +- Score géo : 0.5 (moyennement proche) +- Score intérêts : 0.6 +- Score engagement : 0.5 +→ Score final : 0.53 +→ Score avec boost : 0.53 × 1.3 = 0.69 + +→ Contenu A proposé en premier (0.80 > 0.69) +``` + +**Cas où abonnement fait la différence** : +``` +Contenu A (non suivi) : score 0.70 +Contenu B (suivi) : score 0.60 → avec boost 0.78 +→ Contenu B proposé (boost fait pencher la balance) +``` + +**Justification** : +- **Équilibre** : valorise abonnements sans enfermer utilisateur +- **Découverte** : contenus viraux/locaux peuvent toujours émerger +- **Prévisible** : boost fixe, pas de logique opaque +- **Coût 0** : multiplicateur simple dans l'algo + +--- + +### 8.2 Notifications contextuelles + +**Décision** : Push adapté selon contexte (voiture vs à pied) + limite 10/jour + +**Détection contexte utilisateur** : + +| Contexte | Détection | Comportement | +|----------|-----------|--------------| +| **En voiture** | Vitesse GPS >10 km/h | Notifications silencieuses (in-app uniquement) + commandes volant | +| **À pied** | Vitesse GPS <5 km/h | Notifications push actives + interface tactile/vocale | + +**Notifications activées** : + +#### En voiture (mode conduite) + +| Événement | Notification | Comportement | +|-----------|--------------|--------------| +| **Nouveau contenu créateur suivi** | In-app uniquement | Badge compteur, pas de push (sécurité) | +| **Live créateur suivi** | In-app uniquement | Badge compteur, pas de push | +| **Point d'intérêt proche** | Audio notification | Bip + annonce vocale : "Audio-guide disponible" | + +#### À pied (mode piéton) + +| Événement | Notification | Comportement | +|-----------|--------------|--------------| +| **Nouveau contenu créateur suivi** | ✅ Push | Si utilisateur dans zone géo du contenu | +| **Live créateur suivi** | ✅ Push | Si utilisateur dans zone géo | +| **Audio-guide disponible** | ✅ Push | "📍 Audio-guide disponible : [Lieu]" | +| **Séquence suivante suggérée** | Audio notification | Annonce vocale : "Pièce suivante disponible" | + +**Format notifications** : + +**Nouveau contenu** : +``` +🎧 [Nom créateur] a publié : "[Titre contenu]" +Tap pour écouter +``` + +**Live en direct** : +``` +🔴 [Nom créateur] est en direct : "[Titre live]" +Tap pour rejoindre +``` + +**Audio-guide à pied** : +``` +📍 Audio-guide disponible : [Nom du lieu] +Choisissez parmi 3 guides pour [Musée du Louvre] +Tap pour explorer +``` + +**Filtrage géographique** : +- Si contenu/live hors zone utilisateur → **pas de notification** +- Évite frustration : "notification pour contenu que je ne peux pas écouter" +- Exception : contenu national → notifie tous les abonnés + +**Fréquence maximale** : +- **Maximum 10 notifications push/jour** par utilisateur (tous types confondus) +- Si dépassement : notifications regroupées +- Message groupé : "🎧 3 nouveaux contenus de créateurs suivis" + +**Plages horaires** : +- **Mode silencieux** : 22h-8h (pas de push, sauf live) +- Paramétrable utilisateur (désactivation totale possible) +- Option "Notifications importantes uniquement" (lives uniquement) + +**Gestion préférences** : + +| Préférence | Défaut | Description | +|------------|--------|-------------| +| **Nouveaux contenus** | ✅ Activé | Push à chaque nouveau contenu (à pied uniquement) | +| **Lives** | ✅ Activé | Push au démarrage live (à pied uniquement) | +| **Audio-guides proximité** | ✅ Activé | Push quand audio-guide détecté à <100m | +| **Mode silencieux** | ✅ Activé (22h-8h) | Pas de push nocturne | +| **Limite quotidienne** | 10 | Modifiable 5-20 | + +**Justification** : +- **Sécurité routière** : pas de push en conduite (distraction) +- **Engagement piéton** : push actifs pour audio-guides (valeur ajoutée tourisme) +- **Pas de spam** : limite 10/jour + mode silencieux +- **Filtrage géo** : pertinence maximale (pas de notif inutiles) +- **Coût** : Firebase Cloud Messaging (gratuit jusqu'à volume élevé) + +--- + +### 8.3 Mode Audio-guide (piéton) + +**Décision** : Navigation manuelle multiséquence + choix parmi plusieurs guides + +**Fonctionnement** : + +#### Détection et proposition + +1. Utilisateur à pied (<5 km/h) passe à <**100m** d'un lieu avec audio-guides +2. **Notification push** : "📍 Audio-guide disponible : [Musée du Louvre]" +3. Tap notification → **Page de sélection** audio-guides + +#### Page de sélection + +**Affichage** : +``` +📍 Musée du Louvre + +Choisissez votre guide : + +┌─────────────────────────────────┐ +│ 🎨 Visite complète (45 min) │ +│ Par [Créateur A] • 12 séquences│ +│ ⭐ 4.8 • 1.2K écoutes │ +└─────────────────────────────────┘ + +┌─────────────────────────────────┐ +│ 🏛️ Œuvres majeures (20 min) │ +│ Par [Créateur B] • 5 séquences │ +│ ⭐ 4.9 • 3.5K écoutes │ +└─────────────────────────────────┘ + +┌─────────────────────────────────┐ +│ 👶 Visite famille (30 min) │ +│ Par [Créateur C] • 8 séquences │ +│ ⭐ 4.7 • 850 écoutes │ +└─────────────────────────────────┘ +``` + +#### Interface audio-guide + +**Après sélection** : +``` +🎨 Visite complète • Musée du Louvre + +Piste actuelle : 2/12 +"La Joconde - Histoire et mystères" +[████████────────────] 3:24 / 6:50 + +Liste des séquences : +✅ 1. Introduction et architecture +▶️ 2. La Joconde - Histoire et mystères +⏸️ 3. Vénus de Milo +⏸️ 4. Victoire de Samothrace +⏸️ 5. Peintures Renaissance +... +⏸️ 12. Conclusion et boutique +``` + +**Navigation** : + +| Action | Geste | Effet | +|--------|-------|-------| +| **Séquence suivante** | Tap "Suivant" ou commande vocale "Suivant" | Passe à séquence N+1 | +| **Séquence précédente** | Tap "Précédent" ou commande vocale "Précédent" | Revient à séquence N-1 | +| **Saut direct** | Tap séquence dans liste | Lecture séquence choisie | +| **Pause** | Tap bouton pause | Met en pause, reprise position exacte | +| **Quitter** | Tap "×" | Sauvegarde progression, sortie guide | + +**Guidage vocal automatique** : +- Entre 2 séquences : "Vous avez terminé la séquence 2. Dirigez-vous vers la Vénus de Milo pour la séquence 3." +- Si utilisateur s'éloigne (>50m de la prochaine pièce) : "Vous vous éloignez de la prochaine étape. Consultez le plan." + +**Sauvegarde progression** : +- Position dans guide sauvegardée automatiquement +- Retour ultérieur : "Reprendre à la séquence 5 ?" ou "Recommencer depuis le début" +- Historique : guide marqué "Terminé" si toutes séquences écoutées + +**Création audio-guide multiséquence** : + +**Processus créateur** : +1. Créateur upload **plusieurs fichiers audio** (1 par séquence) +2. Numérote les séquences : "Séquence 1", "Séquence 2", etc. +3. Titre chaque séquence : "Introduction", "La Joconde", etc. +4. Définit **point GPS unique** pour tout le guide (centre du lieu) +5. Métadonnées : durée totale calculée automatiquement + +**Format stockage** : +```json +{ + "guide_id": "abc123", + "title": "Visite complète Musée du Louvre", + "location": {"lat": 48.8606, "lon": 2.3376, "radius": 200}, + "sequences": [ + { + "sequence_number": 1, + "title": "Introduction et architecture", + "audio_url": "https://cdn.../seq1.mp3", + "duration_seconds": 180 + }, + { + "sequence_number": 2, + "title": "La Joconde - Histoire et mystères", + "audio_url": "https://cdn.../seq2.mp3", + "duration_seconds": 410 + }, + ... + ], + "total_duration_seconds": 2700, + "creator_id": "creator_xyz" +} +``` + +**Justification** : +- **UX piéton** : navigation tactile adaptée (pas de commandes volant) +- **Autonomie** : utilisateur maître de son rythme (pas d'enchaînement forcé) +- **Choix** : plusieurs guides = diversité styles (famille, expert, rapide) +- **Engagement** : sauvegarde progression = incitation terminer +- **Coût** : réutilise infra contenu standard (juste métadonnées séquences) + +--- + +### 8.4 Limites et désabonnement + +**Décision** : 200 abonnements max + désabonnement -5% jauges + +**Nombre maximum d'abonnements** : +- **200 créateurs maximum** par utilisateur +- Raisons : + - **Évite spam** : au-delà de 200, notifications ingérables + - **Usage réaliste** : 200 créateurs = déjà énorme (vs 100-150 sur YouTube/Twitter) + - **Performance** : requêtes SQL optimisées (index sur 200 max) + +**Si limite atteinte** : +- Message : "Vous suivez déjà 200 créateurs. Désabonnez-vous d'un créateur pour en suivre un nouveau." +- Liste triable : par date abonnement, nb contenus écoutés, dernière activité +- Suggestion : "Vous n'avez pas écouté [Créateur X] depuis 6 mois, le désabonner ?" + +**Abonnement initial** : +- Impact : **+5% toutes jauges tags du créateur** (défini en [ADR-010](../adr/010-commandes-volant.md)) +- Action : Bouton "S'abonner" dans profil créateur (interface mobile) +- Immédiat à l'action + +**Désabonnement** : +- Impact : **-5% toutes jauges tags du créateur** (symétrique) +- Action : Bouton "Se désabonner" dans profil créateur +- Immédiat à l'action +- Pas de confirmation (action réversible) + +**Exemple** : +``` +Créateur tague ses contenus : Automobile, Voyage + +Abonnement : +→ Jauge Automobile : 60% → 65% (+5%) +→ Jauge Voyage : 55% → 60% (+5%) + +3 mois plus tard, désabonnement : +→ Jauge Automobile : 65% → 60% (-5%) +→ Jauge Voyage : 60% → 55% (-5%) +``` + +**Gestion multi-tags** : +- Si créateur a 3 tags → **+5% sur chacun des 3 tags** +- Logique : abonnement = signal fort d'affinité à TOUS les sujets du créateur + +**Abonnements réciproques** : +- ❌ **Pas d'abonnement mutuel visible** +- Créateur ne voit pas qui est abonné (privacy) +- Créateur voit uniquement : nombre total abonnés (métrique globale) + +**Justification** : +- **Limite 200** : équilibre entre liberté et gestion spam +- **Symétrie +5%/-5%** : cohérence mathématique, prévisibilité +- **Privacy** : pas de liste publique abonnés (évite stalking) +- **Coût** : table abonnements PostgreSQL standard + +--- + +## Récapitulatif Section 8 diff --git a/docs/regles-metier/10-gestion-erreurs.md b/docs/regles-metier/10-gestion-erreurs.md new file mode 100644 index 0000000..cb5ddcc --- /dev/null +++ b/docs/regles-metier/10-gestion-erreurs.md @@ -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 diff --git a/docs/regles-metier/11-creation-publication-contenu.md b/docs/regles-metier/11-creation-publication-contenu.md new file mode 100644 index 0000000..51fee6c --- /dev/null +++ b/docs/regles-metier/11-creation-publication-contenu.md @@ -0,0 +1,285 @@ +## 4. Création et publication de contenu + +### 4.1 Upload et encodage + +**Décision** : Formats universels avec encodage asynchrone + +**Formats acceptés** : +- ✅ MP3 (`.mp3`) +- ✅ AAC (`.aac`, `.m4a`) +- ❌ WAV, FLAC (trop lourds, inutiles en voiture) + +**Limites** : + +| Paramètre | Valeur | Justification | +|-----------|--------|---------------| +| **Taille maximale** | 200 MB | ~4h de podcast à 128 kbps | +| **Durée maximale** | 4 heures | Suffisant pour podcasts longs | +| **Validation format** | Client + backend | Double sécurité | + +**Pipeline d'encodage** : + +``` +1. Upload fichier (MP3/AAC) → OVH Object Storage temporaire +2. Job asynchrone (worker Go + FFmpeg) : + - Validation format et intégrité + - Réencodage Opus 3 profils (24/48/64 kbps) + - Génération segments HLS (.m3u8 + .ts) + - Génération image couverture par défaut +3. Suppression fichier original (économie stockage) +4. Notification créateur : "Contenu prêt à publier" +``` + +**Temps d'encodage estimé** : +- Contenu 5 min → ~30 secondes +- Podcast 1h → ~5 minutes +- Podcast 4h → ~20 minutes + +**Profils Opus générés** : + +| Qualité | Bitrate | Usage | +|---------|---------|-------| +| Basse | 24 kbps | 2G/Edge | +| Standard | 48 kbps | 3G (défaut) | +| Haute | 64 kbps | 4G/5G | + +**Écoute accélérée** : + +| Vitesse | Usage | +|---------|-------| +| 0.75x | Compréhension difficile (accent, technique) | +| 1.0x | Normal (défaut) | +| 1.25x | Gain léger | +| 1.5x | Podcasts longs | +| 2.0x | Survol rapide (modérateurs) | + +**Disponible pour** : +- ✅ Modérateurs (validation rapide : 30s → 15s à 2x) +- ✅ Auditeurs (tous les contenus) +- ✅ Standard industrie (YouTube, Spotify, Apple Podcasts) + +**Justification** : +- **Simplicité** : 2 formats couvrent 95% des cas d'usage +- **Coût optimisé** : pas de conversion WAV/FLAC lourds +- **Stockage réduit** : suppression original après encodage +- **Scalabilité** : workers horizontalement (Kubernetes jobs) +- **Productivité** : écoute accélérée = double productivité modération + +--- + +### 4.2 Métadonnées obligatoires + +**Décision** : Minimaliste pour réduire friction + +**Champs obligatoires** : + +| Champ | Format | Validation | +|-------|--------|------------| +| **Titre** | 5-100 caractères | Alphanumérique + ponctuation basique | +| **Type géo** | Enum | Ancré / Contextuel / Neutre | +| **Zone diffusion** | Composite | Voir détails ci-dessous | +| **Tags** | Enum | 1 à 3 parmi liste prédéfinie | +| **Classification âge** | Enum | Tout public / 13+ / 16+ / 18+ | + +**Zone de diffusion (obligatoire)** : + +Options mutuellement exclusives : +- **Point GPS** : latitude + longitude + rayon (100m à 10km) +- **Ville** : sélection dans référentiel INSEE +- **Département** : sélection liste +- **Région** : sélection liste +- **National** : France entière + +**Tags disponibles** (1 à 3 obligatoires) : +- Automobile +- Voyage +- Famille +- Amour +- Musique +- Économie +- Cryptomonnaie +- Politique +- Culture générale +- Sport +- Technologie +- Santé + +**Champs optionnels** : +- ❌ Description (ajout ultérieur) +- ❌ Image couverture (génération auto) + +**Image de couverture par défaut** : + +Génération automatique selon règles : +- Icône selon type géo : 📍 Ancré / 🌍 Contextuel / 🎧 Neutre +- Couleur selon tag principal : bleu (Auto), vert (Voyage), rouge (Musique), etc. +- Format 800×800px, PNG +- Personnalisable ultérieurement (post-MVP) + +**Exemple de publication** : +``` +Titre : "Histoire de la Tour Eiffel" +Type géo : Ancré +Zone : Point GPS (48.8584, 2.2945, rayon 500m) +Tags : Voyage, Culture générale +Classification : Tout public +→ Image auto : 📍 fond bleu-vert (Voyage) +``` + +**Justification** : +- **Friction minimale** : 5 champs max = 2 min de publication +- **Publication rapide** : pas de blocage sur description/image +- **Coût 0** : pas de génération IA au MVP +- **Évolutif** : champs optionnels ajoutables ultérieurement + +--- + +### 4.3 Validation des 3 premiers contenus + +**Décision** : Validation manuelle par équipe modération RoadWave + +**Processus nouveau créateur** : + +1. Créateur upload ses 3 premiers contenus +2. Contenus passent en **file d'attente modération** +3. Modérateur junior RoadWave : + - Écoute 30 secondes (ou 15s à 2x) + - Vérifie métadonnées + - Valide ou rejette avec raison +4. Si accepté : contenu publié + notification créateur +5. Si refusé : notification avec raison détaillée + lien vers règles +6. Après 3 contenus validés : créateur passe en **statut vérifié** + +**Critères de validation** : + +| Critère | Détails | +|---------|---------| +| **Qualité audio** | Compréhensible (pas de grésillement excessif) | +| **Respect règles** | Pas de contenu prohibé évident (haine, spam, illégal) | +| **Classification âge** | Cohérente avec contenu écouté | +| **Tags pertinents** | Correspondance minimale avec contenu | +| **Zone diffusion** | Cohérente (pas "Tour Eiffel" avec zone "National") | + +**Délai de validation** : +- Objectif : **24-48h** (jours ouvrés) +- Priorité : FIFO (First In First Out) +- Weekend : délai peut atteindre 72h +- Message au créateur : "Validation en cours, délai estimé 24-48h" + +**Notification créateur** : + +**Si accepté** : +- Email + push : "✅ Votre contenu '[Titre]' est en ligne !" +- Lien direct vers le contenu +- Compteur : "2/3 contenus validés pour devenir créateur vérifié" + +**Si refusé** : +- Email + push : "❌ Contenu '[Titre]' refusé" +- Raison détaillée : "Qualité audio insuffisante" / "Tags non pertinents" / "Classification incorrecte" / etc. +- Lien vers règles de publication +- Possibilité de correction + resoumission + +**Après 3 validations** : + +Créateur obtient **statut "Vérifié"** : +- Badge ✓ visible sur profil +- Contenus futurs publiés **immédiatement** (modération a posteriori uniquement) +- Modération seulement si signalé par utilisateurs + +**Outils modérateur** : +- Écoute accélérée (1.5x ou 2x) = double productivité +- Interface dédiée : queue de contenus à valider +- Raccourcis clavier : A (Accepter), R (Rejeter), Espace (Pause) +- Historique créateur visible (si déjà 1-2 contenus validés) + +**Modération communautaire (post-MVP)** : + +⚠️ **Non implémenté au MVP** (complexité juridique) + +Vision future (envisageable) : +- Créateurs établis peuvent opt-in "Modérateur communautaire" +- Formation obligatoire (30 min) + quiz (80%) +- Pré-validation uniquement (validation finale toujours par équipe RoadWave) +- Compensation : badges, premium offert +- Attribution aléatoire (pas de collusion) + +**Justification décision MVP** : +- **Responsabilité juridique** : plateforme reste responsable (DSA EU) +- **Qualité garantie** : modérateurs formés et mandatés +- **Anti-spam efficace** : bloque 95% des abus dès le début +- **Coût raisonnable** : 30s × 3 contenus = 1.5 min/créateur +- **UX acceptable** : délai 24-48h expliqué clairement +- **Pas de validation par pairs** au MVP = évite risques juridiques (collusion, compétence, conflits) + +--- + +### 4.4 Modification et suppression + +**Décision** : Modification métadonnées uniquement, suppression immédiate + +**Modification autorisée** : + +| Élément | Modifiable | Justification | +|---------|------------|---------------| +| **Titre** | ✅ | Correction coquilles | +| **Description** | ✅ | Si ajoutée ultérieurement | +| **Tags** | ✅ | Ajustement pertinence | +| **Image couverture** | ✅ | Personnalisation | +| **Audio** | ❌ | Intégrité contenu | +| **Zone diffusion** | ❌ | Évite manipulation algo | +| **Type géo** | ❌ | Évite manipulation algo | +| **Classification âge** | ❌ | Sécurité mineurs | + +**Raisons restrictions** : + +**Audio non modifiable** : +- Évite fraude : uploader contenu validé → remplacer par spam +- Intégrité : auditeurs doivent écouter ce qui a été validé + +**Zone/Type non modifiables** : +- Évite manipulation : créer "Local Paris" → changer en "National" pour boost visibilité +- Évite abus : créer "Neutre" (faible pondération géo) → changer en "Ancré" (forte pondération) + +**Classification non modifiable** : +- Évite contournement : uploader "Tout public" → passer en "18+" sans revalidation +- Sécurité : garantit que classification a été vérifiée + +**Si besoin de changer audio/zone/classification** : +- Action : **Supprimer contenu + republier** +- Si créateur <3 contenus validés : retourne en file validation +- Si créateur ≥3 contenus validés : publication immédiate + +**Suppression de contenu** : + +| Aspect | Comportement | +|--------|--------------| +| **Délai** | Immédiat | Suppression BDD + cache sous 5 min | +| **Réversibilité** | Non | Suppression définitive | +| **Historique auditeurs** | Marqué "Contenu supprimé par créateur" | Conserve écoute dans historique | +| **Analytics plateforme** | Anonymisé et conservé | Métriques globales (RGPD compliant) | +| **Fichiers cache** | Supprimés sous 24h | Purge NGINX Cache (OVH VPS) et OVH Object Storage | + +**Exemple scénario suppression** : +``` +Créateur supprime podcast écouté par 1000 personnes +→ Cache/Storage : fichiers purgés sous 24h (NGINX Cache + OVH Object Storage) +→ BDD : entrée marquée "deleted", auteur anonymisé +→ Historique auditeurs : "Contenu supprimé" (conserve durée écoute pour stats) +→ Analytics : métriques globales conservées (anonymes, RGPD OK) +``` + +**Notifications suppression** : +- Pas de notification aux auditeurs (pour éviter effet Streisand) +- Historique reste consultable : "Vous avez écouté ce contenu le [date]" +- Si auditeur tente de réécouter : "Ce contenu n'est plus disponible" + +**Justification** : +- **Simplicité** : règles claires et non-ambiguës +- **Sécurité** : évite manipulations algorithme et contournements modération +- **Contrôle créateur** : liberté totale de supprimer (RGPD) +- **Traçabilité** : historique conservé pour analytics (anonymisé) +- **Coût 0** : pas de revalidation métadonnées + +--- + +## Récapitulatif Section 4 diff --git a/docs/regles-metier/12-radio-live.md b/docs/regles-metier/12-radio-live.md new file mode 100644 index 0000000..b4653ab --- /dev/null +++ b/docs/regles-metier/12-radio-live.md @@ -0,0 +1,254 @@ +## 7. Radio live + +### 7.1 Démarrage d'un live + +**Décision** : Buffer 15s + notification abonnés + limite 8h + +**Processus de démarrage** : + +1. Créateur appuie "Démarrer live" dans l'app +2. **Vérification pré-live** : + - Connexion ≥1 Mbps upload (warning si insuffisant) + - Micro autorisé + - Zone diffusion déjà définie (ville, département, région, national) +3. **Buffer initial 15 secondes** avant diffusion publique + - Créateur parle pendant 15s → accumulation buffer serveur + - Message créateur : "Live démarre dans 15s... Testez votre micro" + - Permet vérifier qualité audio avant diffusion +4. Après 15s → **Live public**, auditeurs peuvent rejoindre + +**Notification abonnés** : +- ✅ **Push notification immédiate** à tous les abonnés dans la zone géographique +- Message : "🔴 [Nom créateur] est en direct : [Titre live]" +- Tap notification → ouverture app + lecture live immédiate +- **Filtrage géographique** : si abonné hors zone, pas de notif (évite frustration) + +**Limite de durée** : +- **Maximum 8 heures** par session live +- Warning créateur à 7h30 : "Votre live se terminera dans 30 min" +- Si besoin continuer → arrêt + redémarrage nouveau live (évite abus ressources serveur) + +**Métadonnées obligatoires** : + +| Champ | Format | Validation | +|-------|--------|------------| +| **Titre** | 5-100 caractères | Ex: "Discussion politique en direct" | +| **Tags** | 1-3 centres d'intérêt | Sélection liste prédéfinie | +| **Classification âge** | Enum | Tout public / 13+ / 16+ / 18+ | +| **Zone diffusion** | Geo | Ville / Département / Région / National | + +**Contenus interdits en live** : + +| Type | Description | Sanction | +|------|-------------|----------| +| **Concert/spectacle** | Diffusion concert en direct depuis la salle | Strike 2 immédiat + suspension 7 jours | +| **Événement sportif payant** | Match, compétition avec droits TV | Strike 2 immédiat + suspension 7 jours | +| **Œuvre protégée** | Film, série, musique en fond sans droits | Strike 1 + suspension 3 jours + suppression live | +| **Contenu violent** | Agression, violence physique | Strike 3 immédiat + suspension 30 jours | +| **Contenu illégal** | Apologie terrorisme, pédopornographie | Strike 4 (ban définitif) + signalement autorités | + +**Exemple usecase interdit** : +``` +❌ Utilisateur dans salle de concert diffuse live performance +→ Violation droits d'auteur + droits de diffusion +→ Détection : modération réactive (signalements) + IA audio fingerprint +→ Sanction : Strike 2 (suspension 7 jours) + suppression live + suppression replay +``` + +**Détection violations** : +- **Signalement utilisateurs** : bouton "Signaler" accessible pendant live +- **IA audio fingerprint** : détection musique protégée en arrière-plan (post-MVP, voir [Section 18](18-detection-contenu-protege.md)) +- **Modération réactive** : modérateurs peuvent écouter lives signalés en temps réel +- **Coupure immédiate** : modérateur peut arrêter live si contenu illégal évident + +**Justification** : +- **Buffer 15s** : équilibre entre test qualité et friction minimale +- **Notification abonnés** : engagement maximal, valeur ajoutée live +- **8h max** : couvre 99% cas usage (podcasts longs, émissions radio) sans abus +- **Interdictions strictes** : protection juridique plateforme (DSA EU, droits d'auteur) +- **Coût** : WebRTC ingestion + HLS distribution (réutilise infra existante) + +--- + +### 7.2 Arrêt du live + +**Décision** : Compte à rebours 5s + tolérance déconnexion 60s + enregistrement auto + +**Fin manuelle créateur** : + +1. Créateur appuie "Arrêter live" +2. **Compte à rebours 5 secondes** affiché + - Message audio : "Ce live se termine dans 5... 4... 3... 2... 1" + - Permet au créateur de faire un outro propre + - Annulable pendant décompte (bouton "Annuler") +3. Timer atteint 0 → arrêt diffusion +4. **Traitement post-live automatique** démarre (voir ci-dessous) + +**Fin automatique si déconnexion** : + +| Durée coupure | Comportement | +|---------------|--------------| +| **<60 secondes** | Message auditeurs : "Connexion créateur perdue, reconnexion en cours..." | +| **≥60 secondes** | Arrêt automatique live + message : "Le live est terminé suite à une coupure de connexion" | + +**Enregistrement automatique** : + +✅ **Obligatoire et automatique** (valeur ajoutée énorme) + +**Processus** : +1. Pendant live : enregistrement continu serveur (format Opus raw) +2. Fin live → **job asynchrone** (worker Go + FFmpeg) : + - Conversion MP3 256 kbps (qualité optimale) + - Génération segments HLS (comme contenu classique) + - Normalisation volume -14 LUFS + - Détection silences prolongés (nettoyage) +3. **Publication automatique** du replay : + - Titre : "[REPLAY] [Titre live original]" + - Même zone diffusion, tags, classification + - Disponible sous **5-10 minutes** après fin live + - Type géo : automatiquement "Géo-neutre" (replay = contenu pérenne) + +**Options créateur** : + +| Option | Défaut | Description | +|--------|--------|-------------| +| **Publier replay automatiquement** | ✅ OUI | Désactivable avant démarrage live | +| **Supprimer replay après coup** | ✅ Possible | Suppression standard contenu | +| **Modifier replay** | ❌ Non | Intégrité enregistrement | + +**Conservation fichier source** : +- Opus raw conservé **7 jours** après fin live (backup) +- Suppression automatique après 7j (économie stockage) +- Si replay supprimé par créateur → fichier raw supprimé immédiatement + +**Justification** : +- **Compte à rebours 5s** : outro propre, pas de coupure brutale +- **Tolérance 60s** : évite arrêts intempestifs (tunnel, changement cellule) +- **Enregistrement auto** : valorisation contenu éphémère, génération contenu pérenne +- **MP3 256 kbps** : qualité optimale pour replay (vs 48 kbps live) +- **Coût** : stockage minimal (Opus → MP3 1× par live, puis suppression raw après 7j) + +--- + +### 7.3 Comportement auditeur + +**Décision** : Buffer 15s + continuation hors zone + reconnexion au live actuel + écoute passive uniquement + +**Buffer de synchronisation** : + +- **15 secondes** entre créateur et auditeurs +- Raisons : + - Stabilité réseau mobile (3G/4G fluctuant) + - Synchronisation approximative acceptable (pas besoin temps réel strict) + - Permet buffering anticiper coupures courtes (tunnels) + +**Comparaison buffers** : + +| Buffer | Avantages | Inconvénients | Décision | +|--------|-----------|---------------|----------| +| 5s | Quasi temps réel | Instable 3G, coupures fréquentes | ❌ | +| 10s | Bon compromis | Légèrement juste pour 3G | ❌ | +| **15s** | **Stabilité optimale 3G/4G** | Léger décalage acceptable | ✅ | +| 20s+ | Très stable | Décalage trop perceptible | ❌ | + +**Zone géographique pendant live** : + +- ✅ **Continuation si sortie de zone** +- Scénario : auditeur écoute live régional → sort du département → **live continue** +- Raisons : + - Pas de coupure brutale (mauvaise UX) + - Écoute engagée = terminer naturellement + - Après fin live → algo normal (pas de contenus hors zone) + +**Reconnexion après coupure réseau** : + +| Durée coupure | Comportement | +|---------------|--------------| +| **<90 secondes** | Reprend au live actuel (pas au buffer ancien) + saut temporel transparent | +| **≥90 secondes** | Message : "Live en cours perdu, passage au contenu suivant" + algo propose contenu normal | + +**Interactions disponibles** : + +**Décision ferme** : ❌ **Aucun chat en direct, ni maintenant ni dans le futur** + +**Raisons** : +- **Sécurité routière** : pas de distraction en voiture (focus UX) +- **Harcèlement** : évite contenu haineux, insultes, trolling +- **Modération** : pas de coût modération temps réel (impossible à scale) +- **Simplicité** : écoute passive = expérience uniforme + +**Actions autorisées pendant live** : + +| Action | Disponible | Effet | +|--------|------------|-------| +| **Like** | ✅ | Bouton cœur interface mobile (véhicule arrêté) | +| **Abonnement créateur** | ✅ | Bouton profil créateur (interface mobile) | +| **Skip** | ✅ | Passe au contenu suivant, sort du live | +| **Précédent** | ❌ | Pas de sens sur live (flux temps réel) | +| **Chat** | ❌ | Jamais implémenté (décision définitive) | +| **Réactions emoji** | ❌ | Jamais implémenté (décision définitive) | + +**Messages utilisateur** : +- "💬 Les discussions ne sont pas disponibles sur RoadWave pour garantir votre sécurité en voiture et éviter le harcèlement." + +**Justification décision définitive** : +- **UX cohérente** : RoadWave = écoute en conduisant, pas réseau social interactif +- **Bien-être** : évite toxicité, harcèlement, haine (fléau réseaux sociaux) +- **Juridique** : pas de risque contentieux modération chat (DSA EU) +- **Coût** : 0€ infra chat, 0€ modération temps réel +- **Différenciation** : positionnement "audio safe" vs plateformes toxiques + +--- + +### 7.4 Architecture technique + +**Stack** : + +``` +Créateur (App mobile) + ↓ WebRTC (OPUS 48 kbps) +Serveur Ingestion (Go + Pion WebRTC) + ↓ Conversion temps réel +Serveur HLS (segments .ts) + ↓ NGINX Cache (OVH VPS) +Auditeurs (App mobile, HLS natif) +``` + +**Flux détaillé** : +1. **Créateur** → WebRTC OPUS 48 kbps vers serveur Go +2. **Serveur Go** → Conversion temps réel OPUS → segments HLS (.m3u8 + .ts) +3. **NGINX Cache (OVH VPS)** → Distribution HLS avec cache +4. **Auditeurs** → Lecture HLS native iOS/Android (buffer 15s) +5. **Enregistrement parallèle** → Opus raw stocké temporairement +6. **Post-live** → Job async : Opus → MP3 256 kbps → Publication replay + +**Dépendances** : +- ✅ **Pion WebRTC** (Go library, open source, MIT license) +- ✅ **FFmpeg** (conversion audio, LGPL/GPL) +- ✅ **NGINX** (cache et distribution HLS, open source) +- ✅ **OVH Object Storage** (stockage origin, compatible S3) +- ✅ **PostgreSQL + Redis** (métadonnées live + cache) + +**Avantages** : +- ✅ Pas de dépendance Google/Facebook/Cloudflare (souveraineté) +- ✅ WebRTC standard ouvert (Pion = lib Go pure) +- ✅ Réutilise infra HLS existante (pas de doublon) +- ✅ NGINX Cache (OVH VPS) optimise la distribution (coût réduit) +- ✅ Scalable horizontalement (workers Go) + +**Coût estimé infrastructure** : + +| Phase | Utilisateurs | Infra live | Coût/mois | +|-------|--------------|------------|-----------| +| **MVP** | 0-100K | 1 instance Go (ingestion 100 lives simultanés) | +50€ (serveur) + bande passante | +| **Growth** | 100K-1M | 3-5 instances Go (500 lives simultanés) | +200€ + bande passante | +| **Scale** | 1M-10M | Kubernetes auto-scale (2000+ lives) | +1K€ + bande passante | + +**Bande passante** : +- Live : 48 kbps × nb_auditeurs (via NGINX Cache, segments) +- Exemple : 100 auditeurs = 4.8 Mbps = ~2 Go/heure via cache +- Coût estimé : ~0.02€/heure pour 100 auditeurs + +--- + +## Récapitulatif Section 7 diff --git a/docs/regles-metier/13-detection-contenu-protege.md b/docs/regles-metier/13-detection-contenu-protege.md new file mode 100644 index 0000000..71afc5f --- /dev/null +++ b/docs/regles-metier/13-detection-contenu-protege.md @@ -0,0 +1,411 @@ +## 18. Détection de contenu protégé par droits d'auteur + +### 18.1 Périmètre et objectifs + +**Décision** : Focus musique uniquement, approche minimaliste MVP + +**Contenu protégé couvert** : + +| Type | Couvert MVP | Justification | +|------|-------------|---------------| +| **Musique** | ✅ OUI | Risque principal (80% violations), détectable auditivement | +| **Films/séries** | ❌ NON | Rare dans podcasts audio, complexité technique élevée | +| **Livres audio** | ❌ NON | Difficile à distinguer de lecture légitime | +| **Jingles/pubs** | ❌ NON | Usage souvent transformatif, faible risque juridique | + +**Objectifs** : + +1. **Juridique** : Conformité droits d'auteur UE (directive 2019/790) +2. **Protection plateforme** : Éviter contentieux avec ayants droit (SACEM, labels) +3. **Qualité** : Encourager contenu original créateurs +4. **Pragmatisme** : Coût 0€ au MVP, scalable post-MVP + +**Non-objectifs MVP** : + +- ❌ Détection automatisée (fingerprinting audio) +- ❌ Intégration bases de données commerciales (ACRCloud, etc.) +- ❌ Détection temps réel sur lives (voir section 7) +- ❌ Système de licences musicales + +--- + +### 18.2 Règles d'utilisation de musique + +**Décision** : Tolérance 30 secondes pour extraits (fair use) + +**Cas autorisés** : + +| Cas d'usage | Durée max | Condition | Exemple | +|-------------|-----------|-----------|---------| +| **Citation/critique** | 30 secondes | Commentaire ajouté, contexte éditorial | Review album, analyse musicale | +| **Musique libre de droits** | Illimitée | Preuve licence si demandée | Epidemic Sound, Artlist, CC0 | +| **Musique originale** | Illimitée | Créateur = compositeur/interprète | Podcast musical créateur | +| **Domaine public** | Illimitée | Œuvre >70 ans après mort auteur | Classique pré-1950 | + +**Cas interdits** : + +| Violation | Description | Détection | +|-----------|-------------|-----------| +| **Musique intégrale** | Titre complet en fond ou standalone | Écoute modérateur (évident) | +| **Compilation DJ** | Mix de titres protégés sans droits | Écoute modérateur | +| **Extrait >30s** | Citation longue sans transformation | Chronométrage manuel si signalé | +| **Karaoké** | Instrumental protégé + voix créateur | Écoute modérateur (reconnaissable) | + +**Exception fair use (30 secondes)** : + +**Conditions cumulatives** : +1. Extrait ≤30 secondes **ET** +2. Usage transformatif : commentaire, critique, analyse **ET** +3. Pas de substitution à l'œuvre originale **ET** +4. Mention titre + artiste dans métadonnées (recommandé) + +**Justification juridique** : +- Directive UE 2019/790 : exception citation à des fins de critique +- Jurisprudence FR : citation courte autorisée si justifiée +- 30s = standard industrie (YouTube, TikTok) + +--- + +### 18.3 Processus de détection MVP + +**Décision** : Validation manuelle lors des 3 premiers contenus uniquement + +**Workflow intégré à la validation existante** (voir section 4.3) : + +``` +Upload contenu (créateur <3 validés) + ↓ +File d'attente modération + ↓ +Modérateur écoute 30s (déjà existant) + ↓ +Vérification AJOUTÉE : + ├─ Musique en fond détectée ? + │ ├─ OUI → Musique reconnaissable (titre connu) ? + │ │ ├─ OUI → Durée >30s ? + │ │ │ ├─ OUI → REFUS (violation droits) + │ │ │ └─ NON → ACCEPTÉ (fair use) + │ │ └─ NON → ACCEPTÉ (musique libre probable) + │ └─ NON → ACCEPTÉ (pas de musique) + └─ +Validation normale continue +``` + +**Critères de détection manuelle** : + +| Indicateur | Action modérateur | +|------------|-------------------| +| **Musique reconnaissable** (hit radio, classique célèbre) | Chronométrer l'extrait | +| **Extrait >30s** | Refus automatique | +| **Extrait ≤30s** | Vérifier usage transformatif (commentaire ?) | +| **Musique d'ambiance inconnue** | Accepter (probable musique libre) | +| **Doute** | Demander preuve licence au créateur | + +**Après 3 contenus validés** : + +- ✅ Créateur = statut "Vérifié" +- ✅ Publication immédiate sans validation préalable +- ✅ Modération **a posteriori uniquement** (si signalé) + +**Justification** : +- **Coût 0€** : réutilise écoute 30s déjà effectuée +- **Scalable** : pas de validation pour créateurs établis +- **Pragmatique** : détecte violations évidentes (90% des cas) +- **Humain nécessaire** : fair use impossible à automatiser fiablement + +--- + +### 18.4 Signalement et modération a posteriori + +**Décision** : Réutilise système existant section 14 + +**Signalement utilisateur** : + +- Catégorie existante : **🎵 Droits d'auteur** +- Formulaire : identique autres signalements +- Commentaire optionnel : "Quelle musique ? À quel timestamp ?" + +**Traitement modérateur** : + +1. Signalement reçu → file d'attente priorité MOYENNE (délai 24-48h) +2. Modérateur écoute le timestamp indiqué +3. Vérification : + - Musique identifiable ? → Recherche Shazam/SoundHound (outil externe) + - Durée extrait ? + - Usage transformatif (commentaire/critique) ? +4. Décision : + - **Violation confirmée** → Application sanctions (voir 18.5) + - **Fair use** → Rejet signalement + - **Doute** → Escalade modérateur senior + +**Outils modérateur** : + +| Outil | Usage | Coût | +|-------|-------|------| +| **Écoute manuelle** | Détection présence musique | 0€ | +| **Shazam/SoundHound** | Identification titre (externe, outil perso modérateur) | 0€ | +| **Chronomètre** | Mesure durée extrait | 0€ | +| **Notes** | Documentation décision (audit DSA) | 0€ | + +**Priorité traitement** : + +- Score IA : NON APPLICABLE (pas d'IA au MVP) +- Priorité : **MOYENNE** (délai 24-48h jours ouvrés) +- Escalade senior si : + - Créateur conteste avec preuve licence + - Doute fair use complexe + - Récidive (>2 violations) + +--- + +### 18.5 Sanctions + +**Décision** : Échelle progressive avec tolérance première violation + +**Grille de sanctions** : + +| Occurrence | Sanction | Durée | Justification | +|------------|----------|-------|---------------| +| **1ère violation** | ⚠️ Avertissement + Suppression contenu | - | Tolérance erreur bonne foi | +| **2e violation** | 🟡 Strike 1 + Suppression + Suspension upload | 3 jours | Négligence confirmée | +| **3e violation** | 🟠 Strike 2 + Suppression + Suspension upload | 7 jours | Récidive caractérisée | +| **4e violation** | 🟠 Strike 3 + Suppression + Suspension upload | 30 jours | Abus répété | +| **5e violation** | 🔴 Strike 4 + Ban définitif compte créateur | Permanent | Abus délibéré | + +**Détail sanctions** : + +**Avertissement (1ère fois)** : +- Suppression contenu immédiate +- Email + push + in-app : "⚠️ Contenu retiré pour violation droits d'auteur" +- Explication pédagogique : règles musique, lien vers CGU +- **Pas de strike** (tolérance) +- Créateur peut republier version corrigée + +**Strike 1 (2e fois)** : +- Suppression contenu +- Strike ajouté au compteur (visible profil créateur) +- Suspension upload **3 jours** +- Email détaillé : titre détecté, timestamp, règle violée + +**Strike 2 (3e fois)** : +- Idem Strike 1 +- Suspension upload **7 jours** +- Warning : "Strike 2/4 - Vous approchez du seuil critique" + +**Strike 3 (4e fois)** : +- Idem Strike 2 +- Suspension upload **30 jours** +- Warning : "Strike 3/4 - Prochaine violation = ban définitif" + +**Strike 4 - Ban définitif (5e fois)** : +- Désactivation compte créateur +- Tous contenus dépubliés +- Pas de création nouveau compte (email/téléphone blacklisté) + +**Exceptions - Pas de sanction si** : + +- ✅ Créateur prouve licence acquise (facture, abonnement Epidemic Sound, etc.) +- ✅ Musique = domaine public vérifié +- ✅ Musique = œuvre originale créateur (preuve registre SACEM si demandée) +- ✅ Extrait ≤30s + usage transformatif évident (critique musicale) + +**Réhabilitation** : + +- **-1 strike automatique** tous les **6 mois** sans nouvelle violation +- Conditions : aucun signalement validé pendant la période +- Minimum : 0 strikes (pas de valeur négative) +- Avertissement (1ère fois) ne compte pas pour la réhabilitation + +**Exemple** : +``` +Créateur a Strike 2 (7 jours de suspension) +→ 6 mois sans incident +→ Strike 2 devient Strike 1 +→ 6 mois additionnels sans incident +→ Strike 1 effacé, compte propre +``` + +**Justification** : +- **Tolérance 1ère fois** : évite punir erreurs honnêtes +- **Escalade progressive** : dissuasion sans brutalité +- **4 strikes avant ban** : cohérent avec système global (sections 9, 14) +- **Réhabilitation 6 mois** : encourage bon comportement long terme +- **Conforme DSA** : sanctions proportionnées + droit d'appel + +--- + +### 18.6 Processus d'appel + +**Décision** : Réutilise système existant section 14.3.3 + +**Accès** : +- Bouton "Contester cette décision" dans notification sanction +- Délai : **7 jours** après notification + +**Formulaire d'appel** : + +Champs standards (voir section 14) **+** champs spécifiques : + +| Champ additionnel | Type | Obligatoire | +|-------------------|------|-------------| +| **Preuve licence** | Upload PDF/image (facture, contrat) | ❌ Optionnel | +| **Lien source musique libre** | URL (YouTube Audio Library, etc.) | ❌ Optionnel | +| **Déclaration œuvre originale** | Checkbox | ❌ Optionnel | + +**Traitement appel** : + +1. Modérateur senior examine : + - Preuves fournies (licence, facture) + - Réécoute contenu (usage transformatif ?) + - Recherche musique (domaine public ?) +2. Délai : **72h** (standard section 14) +3. Décision finale : + - **Appel accepté** → Strike retiré + Contenu rétabli + Excuse formelle + - **Appel rejeté** → Sanction maintenue + Explication détaillée + +**Cas particulier : Musique libre mal détectée** + +Si créateur prouve musique = licence Epidemic Sound / Artlist : +- ✅ Appel automatiquement accepté +- ✅ Ajout titre à **whitelist interne** (évite futures erreurs) +- ✅ Excuse + compensation (ex: 1 mois Premium offert) + +**Justification** : +- Réutilise processus éprouvé (section 14) +- Preuve licence = résout 90% des cas +- Délai 72h acceptable (pas de suspension immédiate sur appel) + +--- + +### 18.7 Éducation créateurs + +**Décision** : Prévention via documentation + tooltips + +**Ressources disponibles** : + +| Ressource | Contenu | Accès | +|-----------|---------|-------| +| **Page CGU dédiée** | Règles musique détaillées + exemples | Lien dans CGU + FAQ | +| **Tooltip upload** | "⚠️ Pas de musique protégée >30s" | Interface upload contenu | +| **Liste musique libre** | Liens Epidemic, Artlist, YouTube Audio Library | Page aide créateurs | +| **Exemples fair use** | Cas OK : review 20s + commentaire / Cas KO : hit complet en fond | FAQ illustrée | + +**Messages préventifs** : + +**Lors du premier upload** (popup) : +``` +🎵 Attention aux droits d'auteur + +Vous pouvez utiliser : +✅ Votre propre musique originale +✅ Musique libre de droits (Epidemic Sound, etc.) +✅ Extraits courts ≤30s pour critique/analyse + +Interdit : +❌ Musique populaire en intégrale ou fond prolongé +❌ Compilation de hits sans droits + +[J'ai compris] [En savoir plus] +``` + +**Justification** : +- Prévention > sanction (économie modération) +- Créateurs informés = moins de violations +- Coût : 0€ (documentation statique) + +--- + +### 18.8 Évolution post-MVP + +**Décision** : Audio fingerprinting open-source si besoin scalabilité + +**Déclencheurs réintégration** : + +1. Volume signalements "Droits d'auteur" >50/mois +2. Temps modération musique >20h/mois +3. Contentieux avec ayants droit (SACEM, labels) + +**Solution technique prévue** : + +| Composant | Technologie | Fonction | Coût | +|-----------|-------------|----------|------| +| **Fingerprinting** | Chromaprint (open-source) | Génération empreinte audio | 0€ | +| **Base de référence** | MusicBrainz + AcoustID | Comparaison empreintes | 0€ | +| **Matching** | Python + PostgreSQL | Détection similarité >85% | 0€ | +| **Infrastructure** | VPS 8GB RAM + 4 vCPU | Processing async | 50-100€/mois | + +**Workflow automatisé** : + +``` +Upload contenu + ↓ +Job async : extraction empreinte audio (Chromaprint) + ↓ +Comparaison avec base MusicBrainz + ↓ +Match >95% → FLAG automatique "Musique détectée : [Titre]" + ↓ +Modérateur humain vérifie : + ├─ Durée extrait ? + ├─ Usage transformatif ? + └─ Décision finale (accept/reject) +``` + +**Limites connues** : + +- ❌ Précision ~70% (vs 95%+ API commerciales) +- ❌ Base MusicBrainz incomplète (surtout hits récents) +- ❌ Faux positifs possibles (musique libre similaire) +- ✅ Mais : **gratuit** + **self-hosted** + **scalable** + +**Alternative commerciale** (si budget disponible) : + +- ACRCloud : 300-800€/mois, précision 95%+, base exhaustive +- Audible Magic : 500-1000€/mois, musique + films/TV +- **Décision différée** post-MVP selon besoins réels + +--- + +## Récapitulatif Section 18 + +| Point | Décision | Coût | +|-------|----------|------| +| **Périmètre MVP** | Musique uniquement (80% des violations) | 0€ | +| **Tolérance** | 30 secondes pour extraits (fair use) | 0€ | +| **Détection MVP** | Manuelle lors des 3 premiers contenus | 0€ | +| **Outils modérateur** | Écoute + Shazam externe + Chronomètre | 0€ | +| **Sanctions** | Progressive : Avertissement → Strike 1 (3j) → Strike 2 (7j) → Strike 3 (30j) → Strike 4 (ban) | 0€ | +| **Appel** | Réutilise processus section 14 (délai 72h) | 0€ | +| **Éducation** | CGU + Tooltips + FAQ musique libre | 0€ | +| **Post-MVP** | Chromaprint + MusicBrainz (si >50 signalements/mois) | 50-100€/mois | + +**Coût total MVP** : **0€** (validation manuelle intégrée) + +**Conformité juridique** : +- ✅ Directive UE 2019/790 (droit d'auteur + exception citation) +- ✅ DSA (Digital Services Act) : modération réactive + droit d'appel +- ✅ SACEM/SDRM : protection ayants droit + processus contentieux +- ✅ Fair use : tolérance 30s conforme jurisprudence FR/UE + +**Scalabilité** : +- 0-1000 contenus/mois : validation manuelle suffisante (3h/mois modération) +- 1000-10K contenus/mois : fingerprinting open-source requis +- 10K+ contenus/mois : API commerciale à considérer (ACRCloud) + +**Risques identifiés** : + +| Risque | Probabilité | Impact | Mitigation | +|--------|-------------|--------|------------| +| **Contentieux ayant droit** | Faible | Élevé | Réactivité suppression (<24h) + CGU claires | +| **Faux négatifs** (violation non détectée) | Moyenne | Moyen | Signalements utilisateurs + modération a posteriori | +| **Faux positifs** (musique libre bloquée) | Faible | Faible | Processus d'appel 72h + whitelist | +| **Volume signalements** | Faible | Moyen | Évolution fingerprinting si >50/mois | + +--- + +**Lien avec autres sections** : +- Section 4.3 : Validation des 3 premiers contenus (workflow intégré) +- Section 7.2 : Interdictions lives + fingerprinting post-MVP +- Section 14 : Système modération (signalements, sanctions, appels) + +**Prochaine section à clarifier** : Section 11 (Mode offline) ou Section 12 (Gestion des erreurs) diff --git a/docs/regles-metier/14-moderation-flows.md b/docs/regles-metier/14-moderation-flows.md new file mode 100644 index 0000000..a945752 --- /dev/null +++ b/docs/regles-metier/14-moderation-flows.md @@ -0,0 +1,393 @@ +## 14. Modération - Flows opérationnels + +### 14.1 Signalement + +**Décision** : Formulaire simple avec 7 catégories prédéfinies + +#### 14.1.1 Catégories de signalement + +Liste déroulante avec 7 options : + +| Catégorie | Description | +|-----------|-------------| +| 🚫 **Haine & violence** | Incitation à la haine, discrimination, menaces | +| 🔞 **Contenu sexuel** | Pornographie, contenu explicite | +| ⚖️ **Illégalité** | Terrorisme, apologie de crimes | +| 🎵 **Droits d'auteur** | Musique/contenu protégé non autorisé (voir [Section 18](18-detection-contenu-protege.md) pour règles détaillées) | +| 📧 **Spam** | Publicité non sollicitée, répétition | +| ❌ **Fausse information** | Désinformation sur santé, sécurité routière | +| 🔧 **Autre** | Champ texte obligatoire si sélectionné | + +**Justification** : +- Équilibre entre simplicité (pas trop de choix) et précision (aide les modérateurs) +- Coût : 0€ (liste déroulante standard) + +--- + +#### 14.1.2 Commentaire du signaleur + +**Décision** : Optionnel avec incitation + +- Champ texte libre (0-500 caractères) +- Placeholder : "Décrivez le problème (optionnel mais recommandé)" +- Non bloquant : le signalement peut être envoyé sans commentaire + +**Justification** : +- Encourage la qualité des signalements sans créer de friction +- Aide les modérateurs à comprendre le contexte +- Pas de risque d'abandon du processus + +--- + +#### 14.1.3 Confirmation après signalement + +**Décision** : Toast in-app avec lien historique + +**Affichage** : +- Toast notification : "✓ Signalement envoyé. Nous l'examinerons sous 24-48h." +- Durée affichage : 5 secondes +- Bouton optionnel "Voir mes signalements" (accès historique) + +**Historique personnel** : +- Liste des signalements envoyés par l'utilisateur +- Statut : En cours / Traité / Rejeté +- Notification in-app si action prise (contenu retiré, signalement rejeté) + +**Justification** : +- Transparence maximale +- Coût : 0€ (aucun email automatique) +- Bonne UX + +--- + +### 14.2 Traitement des signalements + +#### 14.2.1 IA pré-filtre (transcription + analyse) + +**Décision** : OpenAI Whisper open source + NLP + +**Stack technique** : + +| Composant | Technologie | Hébergement | +|-----------|-------------|-------------| +| **Transcription** | Whisper large-v3 | Self-hosted (CPU MVP, GPU scale) | +| **Analyse sentiment** | distilbert-base-uncased | Self-hosted | +| **Détection haine** | facebook/roberta-hate-speech | Self-hosted | +| **Mots-clés** | Liste noire FR/EN + regex | PostgreSQL | + +**Processus** : +1. Signalement reçu → ajout file d'attente asynchrone +2. Transcription audio (1-10 minutes selon durée) +3. Analyse automatique : + - Score de confiance : 0-100% + - Catégorie détectée + - Timestamps des passages problématiques +4. Priorisation automatique selon score + +**Délais** : +- Audio <5 min : 1-3 minutes +- Audio 5-30 min : 3-10 minutes +- Audio >30 min : 10-20 minutes + +**Coût** : +- **MVP** : 0€ (CPU standard, processing asynchrone) +- **Scale** : 50-200€/mois (GPU VPS si >1000 signalements/jour) + +**Justification** : +- 100% open source, pas de dépendance GAFAM +- Coût maîtrisé (scaling progressif) +- Gain productivité modérateurs ×3-5 + +--- + +#### 14.2.2 Délais de traitement (SLA) + +**Décision** : SLA progressif selon priorité + +| Priorité | Délai cible | Traitement | +|----------|-------------|------------| +| **CRITIQUE** | <2h (24/7) | Violence, suicide, mise en danger → Astreinte modérateur senior | +| **HAUTE** | <24h (jours ouvrés) | Haine, harcèlement, désinformation → Modérateur junior/senior | +| **MOYENNE** | <24h (jours ouvrés) | Spam, contenu inapproprié → Modérateur junior | +| **BASSE** | <72h (jours ouvrés) | Qualité audio, tags incorrects → Modérateur junior | + +**Traitement automatique** : +- Score IA >95% + catégorie évidente (ex: spam répété) → Action automatique immédiate +- Notification créateur + possibilité d'appel + +**Justification** : +- Réaliste et conforme DSA (Digital Services Act) +- Scalable : priorisation automatique +- Ressources humaines optimisées + +--- + +#### 14.2.3 Priorisation automatique + +**Décision** : File d'attente intelligente basée sur score IA + +**Calcul de priorité** : + +``` +Priorité = (Score_IA × 0.7) + (Signalements_cumulés × 0.2) + (Fiabilité_signaleur × 0.1) +``` + +**Détails** : +- **Score_IA** : 0-100% (confiance analyse automatique) +- **Signalements_cumulés** : nombre de signalements du même contenu (boost priorité) +- **Fiabilité_signaleur** : score utilisateur (historique signalements pertinents) + +**Classification résultante** : +- Priorité ≥90 → **CRITIQUE** (traitement immédiat) +- Priorité 70-89 → **HAUTE** (file prioritaire) +- Priorité 40-69 → **MOYENNE** (file normale) +- Priorité <40 → **BASSE** (file différée) + +**Justification** : +- Optimise le temps des modérateurs +- Traite les cas graves en priorité +- Coût : 0€ (algorithme simple) + +--- + +### 14.3 Sanctions + +#### 14.3.1 Notification au créateur + +**Décision** : Multi-canal (email + push + in-app) + +**Canaux utilisés** : + +| Canal | Timing | Contenu | +|-------|--------|---------| +| **Push notification** | Immédiat | Alerte courte : "Votre contenu a été modéré" | +| **In-app** | Au prochain lancement | Popup détaillée avec bouton "Voir détails" | +| **Email** | Dans l'heure | Notification complète avec lien vers formulaire d'appel | + +**Contenu email** : +``` +Objet : Modération de votre contenu "[Titre du contenu]" + +Bonjour [Pseudo], + +Votre contenu "[Titre]" publié le [Date] a été modéré. + +Catégorie violée : [Catégorie] +Raison : [Explication détaillée] +Sanction : [Strike X / Suspension X jours / Suppression contenu] + +Extrait audio concerné : [Timestamp] +Transcription : "[Passage problématique surligné]" + +Vous pouvez contester cette décision sous 7 jours : +[Lien formulaire d'appel] + +L'équipe RoadWave +``` + +**Coût** : +- Email : ~0.001€/notification (Brevo, Resend) +- Push : 0€ (Firebase Cloud Messaging / APNs) +- In-app : 0€ + +**Justification** : +- Conformité DSA (transparence obligatoire) +- Multi-canal garantit réception +- Coût négligeable + +--- + +#### 14.3.2 Détail de la sanction + +**Décision** : Notification complète avec preuves + +**Éléments inclus obligatoirement** : + +1. **Catégorie violée** : référence précise CGU (ex: "Article 3.2 - Haine & violence") +2. **Raison détaillée** : explication en langage clair (non juridique) +3. **Extrait audio** : timestamp exact du passage problématique (ex: "3:42-4:15") +4. **Transcription** : texte problématique surligné en rouge +5. **Gravité** : Strike actuel + conséquences (ex: "Strike 2/4 - Suspension 7 jours") +6. **Recours** : lien direct vers formulaire d'appel + délai (7 jours) + +**Exemple visuel in-app** : +``` +┌─────────────────────────────────────┐ +│ ⚠️ Contenu modéré │ +├─────────────────────────────────────┤ +│ Titre : "Mon podcast #42" │ +│ Publié le : 15/01/2026 │ +│ │ +│ Catégorie violée : │ +│ 🚫 Haine & violence (Article 3.2) │ +│ │ +│ Passage problématique : 3:42-4:15 │ +│ "[Transcription surlignée]" │ +│ │ +│ Sanction : Strike 2/4 │ +│ Suspension : 7 jours │ +│ │ +│ [Contester cette décision] │ +└─────────────────────────────────────┘ +``` + +**Justification** : +- Transparence maximale (obligation DSA) +- Créateur comprend l'erreur → amélioration future +- Réduit les appels non fondés + +--- + +#### 14.3.3 Processus d'appel + +**Décision** : Formulaire in-app structuré + +**Accès** : +- Bouton "Contester cette décision" dans notification +- Section "Mes sanctions" dans profil créateur + +**Formulaire d'appel** : + +| Champ | Type | Obligatoire | +|-------|------|-------------| +| **Sanction contestée** | Pré-rempli (non modifiable) | ✅ | +| **Raison de l'appel** | Texte libre (50-1000 caractères) | ✅ | +| **Arguments** | Zone texte enrichie | ✅ | +| **Preuves** | Upload fichiers (max 5, 10 MB total) | ❌ | + +**Après soumission** : +- Génération numéro de ticket unique (ex: `#MOD-2026-00142`) +- Email confirmation : "Votre appel sera traité sous 72h" +- Statut visible dans l'app : "En cours d'examen" + +**Délai de soumission** : +- Maximum **7 jours** après notification de sanction +- Après 7 jours : appel automatiquement refusé + +**Justification** : +- Professionnel et traçable +- Intégration complète avec système modération +- Coût : 0€ (formulaire custom backend) + +--- + +#### 14.3.4 Délai de réponse pour appel + +**Décision** : SLA 72h garanti + +**Délais** : + +| Type d'appel | Délai | Responsable | +|--------------|-------|-------------| +| **Standard** | 72h max (3 jours ouvrés) | Modérateur senior | +| **Complexe** | 5 jours ouvrés + notification intermédiaire J+3 | Modérateur senior + Admin modération | +| **Critique** | 24h (cas suspension longue/ban) | Admin modération | + +**Notification intermédiaire** (si délai >72h) : +- Email J+3 : "Votre appel #MOD-XXX est en cours d'examen approfondi. Réponse sous 2 jours." + +**Réponse finale** : + +Email détaillé avec : +1. **Décision** : Maintien / Annulation / Réduction de sanction +2. **Justification** : explication de la décision d'appel +3. **Actions** : Strike retiré / Suspension annulée / Contenu rétabli (si applicable) +4. **Définitif** : mention "Cette décision est définitive" (pas de second appel) + +**Suivi in-app** : +- Mise à jour statut : "Appel accepté ✓" ou "Appel rejeté ✗" +- Badge notification + +**Justification** : +- Équilibre entre rapidité et qualité de traitement +- Conforme pratiques industrie (YouTube, TikTok : 5-7 jours) +- Ressources humaines réalistes + +--- + +### 14.4 Outils modérateurs + +**Stack technique complète** : + +| Outil | Technologie | Fonction | +|-------|-------------|----------| +| **Dashboard** | React + TanStack Table | Interface modération | +| **File signalements** | PostgreSQL + Redis | Priorisation temps réel | +| **Player audio** | Wavesurfer.js | Lecture avec waveform + annotations | +| **Transcription** | Whisper large-v3 | Conversion audio → texte | +| **Historique créateur** | Vue 360° | Contenus, strikes, appels, métriques | +| **Actions rapides** | Shortcuts clavier | Approuver (A), Rejeter (R), Escalade (E) | +| **Logs audit** | PostgreSQL + export | Traçabilité complète (DSA) | +| **Collaboration** | Système de commentaires | Modérateurs peuvent s'entraider sur cas complexes | + +**Fonctionnalités clés** : + +1. **Lecture accélérée** : 0.75x à 2x (gain productivité) +2. **Marqueurs temporels** : annotation directe sur waveform +3. **Historique créateur** : vue rapide contenus précédents + strikes +4. **Statistiques** : signalements traités/jour, temps moyen, précision +5. **Fil d'activité** : actions récentes équipe (temps réel) + +**Coût infrastructure** : +- MVP : 0-50€/mois (serveur CPU) +- Scale : 50-200€/mois (GPU + Redis Cluster) + +--- + +### 14.5 Modération préventive (rappel) + +**Nouveaux créateurs** : +- Validation manuelle des **3 premiers contenus** +- Délai : 24-48h (jours ouvrés) +- Transcription automatique pour aide modérateur + +**Score de confiance** : +- Évolution dynamique selon historique +- Créateur fiable (0 strike depuis 6 mois) → validation automatique +- Créateur suspect (strikes récents) → validation manuelle systématique + +**Publicités** : +- Validation manuelle obligatoire 24-48h (responsabilité juridique) +- Transcription + analyse métadonnées (ciblage, durée, volume) + +**Justification** : +- Prévention > réaction (économie modération) +- Qualité plateforme préservée dès le début + +--- + +## Récapitulatif Section 14 + +| Point | Décision | Coût | +|-------|----------|------| +| **Catégories signalement** | 7 catégories prédéfinies + champ libre | 0€ | +| **Commentaire signaleur** | Optionnel avec incitation | 0€ | +| **Confirmation** | Toast in-app + historique personnel | 0€ | +| **IA pré-filtre** | Whisper (CPU MVP, GPU scale) + NLP open source | 0-200€/mois | +| **Délais traitement** | SLA progressif : 2h/24h/72h selon priorité | Dépend équipe | +| **Priorisation** | File intelligente basée score IA | 0€ | +| **Notification sanction** | Email + push + in-app (multi-canal) | ~0.001€/notif | +| **Détail sanction** | Complet : raison + extrait + transcription | 0€ | +| **Processus appel** | Formulaire in-app structuré | 0€ | +| **Délai appel** | 72h garanti (standard) | Dépend équipe | +| **Outils modérateurs** | Dashboard React + Whisper + Wavesurfer.js | 0-200€/mois | + +**Coût total MVP** : **0-200€/mois** (infrastructure IA optionnelle) + +**Conformité** : +- ✅ DSA (Digital Services Act) : transparence, traçabilité, délais +- ✅ RGPD : données modération anonymisées après 3 ans +- ✅ Logs audit : toutes actions tracées (obligation légale plateforme) + +**Scalabilité** : +- 0-1000 signalements/mois : équipe 1-2 modérateurs junior + 1 senior +- 1000-10K signalements/mois : équipe 5-10 modérateurs + IA GPU +- 10K+ signalements/mois : équipe dédiée + IA optimisée + modération communautaire + +--- + +**🎯 Modération communautaire** : Voir [Section 19](19-moderation-communautaire.md) pour le système complet de badges, récompenses et priorisation des signalements pertinents. + +--- + +**Section suivante** : [Section 19 - Modération Communautaire](19-moderation-communautaire.md) diff --git a/docs/regles-metier/15-moderation-communautaire.md b/docs/regles-metier/15-moderation-communautaire.md new file mode 100644 index 0000000..b460a46 --- /dev/null +++ b/docs/regles-metier/15-moderation-communautaire.md @@ -0,0 +1,430 @@ +## 19. Modération Communautaire - Badges et Récompenses + +**Contexte** : Système de gamification pour encourager les utilisateurs à signaler du contenu inapproprié de manière pertinente et qualitative. + +**Objectifs** : +- Améliorer la qualité des signalements (réduire les signalements abusifs) +- Réduire la charge de travail des modérateurs (priorisation automatique) +- Récompenser les contributeurs actifs et fiables + +--- + +### 19.1 Système de badges + +**Décision** : 3 niveaux de badges selon l'historique de signalements validés + +#### 19.1.1 Badges et critères + +| Badge | Nom | Critères | Avantages | +|-------|-----|----------|-----------| +| 🥉 | **Contributeur Bronze** | 5 signalements validés + 70% taux pertinence | Signalements prioritaires (+10 points algorithme) | +| 🥈 | **Contributeur Argent** | 20 signalements validés + 80% taux pertinence | Signalements prioritaires (+20 points) + Badge visible profil | +| 🥇 | **Contributeur Or** | 50 signalements validés + 90% taux pertinence | Signalements prioritaires (+30 points) + Badge visible + Réduction Premium | + +**Règles d'éligibilité** : +- Minimum **10 signalements envoyés** pour être éligible aux badges +- Les signalements "En cours" ne comptent pas dans le calcul +- Les signalements rejetés font baisser le taux de pertinence + +**Calcul du taux de pertinence** : +``` +Taux de pertinence = (Signalements validés / Total signalements envoyés) × 100 +``` + +**Période de calcul** : +- Seuls les **6 derniers mois** comptent (période glissante) +- Évite que les utilisateurs se reposent sur leurs lauriers + +**Justification** : +- **Simple** : 3 niveaux seulement (pas d'over-engineering) +- **Gratuit** : logique backend + affichage frontend +- **Efficace** : incite la qualité plutôt que la quantité + +--- + +#### 19.1.2 Délai entre obtention badges + +**Décision** : Délai minimum entre niveaux + +| Transition | Délai minimum | +|------------|---------------| +| **Bronze → Argent** | 30 jours | +| **Argent → Or** | 60 jours | + +**Justification** : +- Évite la montée en badge trop rapide (anti-farming) +- Force une contribution régulière sur la durée +- Détecte les patterns suspects (audit modérateur si trop rapide) + +--- + +#### 19.1.3 Découverte du système + +**IMPORTANT** : L'utilisateur doit être informé du système de récompenses **dès son premier signalement**. + +**Moment d'affichage** : +- Après avoir envoyé le **premier signalement** +- Juste après le toast de confirmation standard +- **2 secondes de délai** avant affichage de la modal + +**Modal d'information** (affichage unique, ne se réaffiche jamais) : + +``` +┌─────────────────────────────────────────────┐ +│ 🎯 Bravo ! Vous contribuez à une │ +│ communauté plus saine │ +├─────────────────────────────────────────────┤ +│ │ +│ En signalant ce contenu, vous participez │ +│ activement à améliorer l'expérience de │ +│ tous les utilisateurs RoadWave. │ +│ │ +│ Vos contributions de qualité sont │ +│ valorisées et récompensées : │ +│ │ +│ 🥉 Bronze : 5 signalements validés │ +│ → Signalements prioritaires │ +│ │ +│ 🥈 Argent : 20 signalements validés │ +│ → Badge visible + priorité accrue │ +│ │ +│ 🥇 Or : 50 signalements validés │ +│ → Réduction Premium -50% pendant 3 mois │ +│ │ +│ 💡 Votre taux de pertinence compte ! │ +│ Signalements validés ÷ Total × 100 │ +│ │ +│ Continuez à nous aider, chaque │ +│ signalement pertinent compte ! 🙏 │ +│ │ +│ [En savoir plus] [J'ai compris] │ +└─────────────────────────────────────────────┘ +``` + +**Lien "En savoir plus"** : +- Redirection vers page dédiée expliquant : + - Calcul détaillé du taux de pertinence + - Critères d'obtention pour chaque badge + - Avantages détaillés de chaque niveau + - Règles anti-abus (limite 10 signalements/24h) + - Durée de validité des badges (audit trimestriel) + +**Coût** : **0€** (modal one-time, logique backend simple) + +--- + +#### 19.1.4 Affichage badges et statistiques + +**Badge visible** : +- **Profil utilisateur** : visible par tous les autres utilisateurs +- **Historique signalements** : visible uniquement par l'utilisateur lui-même +- **Toast après obtention** : + - 🥉 Bronze : "🎉 Félicitations ! Vous êtes désormais Contributeur Bronze. Merci de rendre RoadWave meilleur !" + - 🥈 Argent : "🎉 Impressionnant ! Badge Contributeur Argent obtenu. Votre engagement fait la différence !" + - 🥇 Or : "🎉 Exceptionnel ! Vous êtes Contributeur Or. La communauté vous remercie pour votre aide précieuse !" + +**Statistiques personnelles** (page Profil > Mes signalements) : +``` +📊 Vos statistiques de modération +Signalements envoyés : 27 +Validés : 23 ✅ +Rejetés : 4 ❌ +Taux de pertinence : 85% + +Badge actuel : 🥈 Contributeur Argent +Prochain palier : 🥇 Contributeur Or (30 signalements validés restants) +``` + +**Toast après traitement signalement** : +- Si validé : "✅ Bravo ! Votre signalement a aidé la communauté. Progression : 3/5 pour badge Bronze 🥉" +- Si rejeté : "❌ Signalement non retenu. Taux de pertinence : 60%. Continuez vos efforts !" + +**Justification** : +- **Transparence totale** : l'utilisateur voit sa progression en temps réel +- **Motivation** : gamification saine (pas de pression, juste encouragement) +- **Gratifiant** : messages positifs valorisant la contribution + +--- + +### 19.2 Score de fiabilité + +**Décision** : Score interne utilisé pour prioriser les signalements dans l'algorithme + +#### 19.2.1 Formule + +``` +Score fiabilité = min(100, (Validés × 10 - Rejetés × 5 + Bonus_Or × 20)) +``` + +**Détails** : +- **Validés** : nombre de signalements validés par modérateurs +- **Rejetés** : nombre de signalements rejetés +- **Bonus_Or** : +20 points si badge Or actif +- **Plafond** : maximum 100 points + +**Exemples** : + +| Cas | Validés | Rejetés | Badge | Score | +|-----|---------|---------|-------|-------| +| Nouvel utilisateur fiable | 10 | 1 | Aucun | 95 | +| Utilisateur moyen | 15 | 8 | Bronze | 70 | +| Contributeur Or | 50 | 3 | Or | 100 (plafonné) | +| Signaleur abusif | 2 | 20 | Aucun | 0 (min 0) | + +--- + +#### 19.2.2 Utilisation dans l'algorithme de priorisation + +**Rappel formule Section 14.2.3** : +``` +Priorité = (Score_IA × 0.7) + (Signalements_cumulés × 0.2) + (Fiabilité_signaleur × 0.1) +``` + +**Intégration** : +- **Fiabilité_signaleur** = Score fiabilité / 100 (normalisé 0-1) +- Les signalements des utilisateurs avec badge Argent/Or passent automatiquement devant les autres (même score IA) + +**Affichage à l'utilisateur** : +- **NON affiché** publiquement (risque de gamification abusive) +- **Visible** uniquement dans les stats personnelles : + ``` + Votre score de fiabilité : 85/100 + → Vos signalements sont traités en priorité + ``` + +**Coût** : **0€** (calcul automatique lors du traitement) + +--- + +### 19.3 Statut "Utilisateur de confiance" + +**Décision** : Statut automatique pour utilisateurs Badge Argent ou Or + +**Critère** : +- Badge **Argent OU Or** actif = automatiquement "Utilisateur de confiance" + +**Avantages** : +1. **Priorisation signalements** : traités avant signalements standards (même score IA) +2. **Badge visible** : affichage "Utilisateur de confiance" sur profil public +3. **Notification différée** : résultats sous 12h (au lieu de 24-48h standards) + +**Révocation** : +- Perte badge Argent/Or → perte statut confiance automatique +- Retour au statut normal, aucune sanction supplémentaire + +**Coût** : **0€** (simple flag booléen calculé automatiquement) + +--- + +### 19.4 Réduction Premium pour badge Or + +**Décision** : Seuls les utilisateurs **Contributeur Or** (top contributeurs) obtiennent une réduction Premium + +#### 19.4.1 Conditions + +| Critère | Valeur | +|---------|--------| +| **Montant** | **-50% pendant 3 mois** (2.49€/mois au lieu de 4.99€) | +| **Éligibilité** | Badge Or actif + pas déjà Premium | +| **Durée** | 3 mois à partir de l'activation | +| **Renouvellement** | Prix normal (4.99€/mois) après 3 mois | +| **Cumul** | Non cumulable avec offre annuelle | +| **Délai activation** | 30 jours après obtention badge Or | + +--- + +#### 19.4.2 Notification + +**Email + Push + In-app** dès obtention badge Or : + +``` +🎉 Exceptionnel ! Vous avez obtenu le badge Contributeur Or + +Vous faites partie des meilleurs contributeurs de RoadWave. +Grâce à votre engagement et votre vigilance, vous aidez +des milliers d'utilisateurs à profiter d'une expérience +audio de qualité. + +La communauté vous remercie ! 🙏 + +En reconnaissance de votre aide précieuse, vous bénéficiez de : + +✨ 3 mois d'abonnement Premium à -50% +→ 2.49€/mois au lieu de 4.99€ + +[Profiter de l'offre] + +Cette offre est valable pendant 30 jours. +Merci de contribuer à rendre RoadWave meilleur chaque jour ! +``` + +**Rappels** : +- Email + Push J-7 : "Il vous reste 7 jours pour profiter de votre réduction Premium -50%" +- Email + Push J-1 : "Dernière chance ! Votre réduction Premium -50% expire demain" + +**Si non activée après 30 jours** : +- Offre expirée (notification : "Votre offre Premium -50% a expiré") +- Badge Or conservé (seule l'offre expire, pas le badge) + +--- + +#### 19.4.3 Perte du badge Or + +**Conditions** : +- Taux de pertinence descend sous **90%** après audit trimestriel +- Signalements abusifs détectés (voir Section 19.5) + +**Conséquences** : +- **Badge Or révoqué** immédiatement +- **Abonnement Premium en cours** reste actif jusqu'à sa fin normale +- **Nouvelle souscription** à prix normal (4.99€/mois) +- **Pas de nouvelle offre -50%** même si badge Or réobtenu ultérieurement + +**Justification** : +- Maintien qualité badges sur le long terme +- Évite abus système + +--- + +#### 19.4.4 ROI et justification + +**Coût maximum** : **200€/mois** (si 50 utilisateurs Or simultanés avec réduction active) + +**ROI attendu** : +- **1 utilisateur Or** = économie ~5-10h modération/mois = 75-150€ économisés (taux horaire modérateur ~15€/h) +- **10 utilisateurs Or actifs** = 750-1500€ économisés > 200€ coût réductions +- **ROI positif dès 2-3 utilisateurs Or actifs** + +**Justification** : +- **Incitation forte** pour meilleurs contributeurs uniquement +- **Conversion** : utilisateurs gratuits très engagés → Premium payant après 3 mois +- **Risque limité** : coût max plafonné, largement compensé par économie modération + +--- + +### 19.5 Anti-abus + +**CRITIQUE** : Système de protection contre les comportements abusifs + +#### 19.5.1 Limite temporelle + +**Décision** : Maximum 10 signalements / 24h par utilisateur + +**Règles** : +- Au-delà de 10 signalements/24h → signalements automatiquement rejetés +- Alerte modérateur automatique (enquête manuelle) +- Message utilisateur : "Limite quotidienne atteinte (10 signalements/24h). Réessayez demain." + +**Justification** : +- Évite signalement massif (farming) +- 10/jour = largement suffisant pour usage légitime +- Coût : 0€ + +--- + +#### 19.5.2 Détection patterns suspects + +**Audit automatique hebdomadaire** : + +```sql +-- Détection signaleurs suspects +SELECT user_id, COUNT(*) as total +FROM reports +WHERE created_at > NOW() - INTERVAL '7 days' +GROUP BY user_id +HAVING COUNT(*) > 30 +``` + +**Action** : +- Enquête manuelle modérateur +- Révocation badge si abus confirmé + +**Patterns détectés** : +- Signalement massif (>30/semaine) +- Taux de pertinence <50% malgré volume élevé +- Signalements tous rejetés sur période 7 jours +- Signalements ciblant toujours même créateur (harcèlement) + +--- + +#### 19.5.3 Audit trimestriel automatique + +**Décision** : Tous les 3 mois, recalcul badges et révocation si critères non respectés + +**Processus** : +1. Recalcul taux pertinence sur période glissante 6 mois +2. Vérification critères : + - Bronze : minimum 70% pertinence + - Argent : minimum 80% pertinence + - Or : minimum 90% pertinence +3. Révocation badge si taux insuffisant +4. Email notification **7 jours avant audit** : + ``` + Votre badge Contributeur Argent sera audité dans 7 jours. + Taux de pertinence actuel : 78% + Minimum requis : 80% + + Continuez à signaler du contenu pertinent pour conserver votre badge ! + ``` + +**Après audit** : +- Email résultat : "Badge conservé ✓" ou "Badge révoqué ✗" +- Possibilité de réobtenir badge ultérieurement (pas de ban) + +**Justification** : +- Maintien qualité badges sur le long terme +- Évite repos sur lauriers +- Coût : 0€ (script automatique) + +--- + +#### 19.5.4 Sanctions abus détecté + +| Gravité | Abus détecté | Sanction | +|---------|--------------|----------| +| **Mineur** | >10 signalements/jour pendant 1 jour | Avertissement + limite 5/jour pendant 7 jours | +| **Modéré** | >30 signalements/semaine avec <50% pertinence | Révocation badge + interdiction signalement 30 jours | +| **Grave** | Signalements massifs coordonnés (farming) | Ban permanent fonctionnalité signalement + révocation tous badges | + +**Notification sanction** : +- Email + Push + In-app +- Explication détaillée de l'abus détecté +- Durée sanction +- **Pas de recours** pour sanctions graves (farming confirmé) + +--- + +## Récapitulatif Section 19 + +| Point | Décision | Coût | +|-------|----------|------| +| **Badges (3 niveaux)** | Bronze (5), Argent (20), Or (50) signalements validés | 0€ | +| **Modal découverte** | Affichage unique au 1er signalement | 0€ | +| **Score fiabilité** | Calcul automatique pour priorisation | 0€ | +| **Utilisateurs de confiance** | Statut auto Argent/Or | 0€ | +| **Réduction Premium Or** | -50% pendant 3 mois (Post-MVP) | 0-200€/mois | +| **Limite temporelle** | Max 10 signalements/24h | 0€ | +| **Audit trimestriel** | Révocation si critères non respectés | 0€ | +| **Sanctions abus** | Mineur/Modéré/Grave selon pattern | 0€ | + +**Coût total MVP** : **0€** + +**Coût total Post-MVP** : **0-200€/mois** (réductions Premium Or) + +**ROI** : **Positif dès 2-3 utilisateurs Or actifs** (économie modération > coût réductions) + +--- + +**Conformité** : +- ✅ RGPD : données modération anonymisées après 3 ans +- ✅ Transparence : utilisateur informé dès le 1er signalement +- ✅ Anti-discrimination : système accessible à tous, basé uniquement sur pertinence + +**Scalabilité** : +- 0-100 utilisateurs actifs : système automatique, 0€ +- 100-1000 utilisateurs actifs : 0-50€/mois (quelques badges Or) +- 1000+ utilisateurs actifs : 50-200€/mois (max 50 badges Or simultanés) + +--- + +**Prochaine section à clarifier** : Section 20 (si nécessaire) ou validation complète règles métier diff --git a/docs/regles-metier/16-publicites.md b/docs/regles-metier/16-publicites.md new file mode 100644 index 0000000..dc7f63a --- /dev/null +++ b/docs/regles-metier/16-publicites.md @@ -0,0 +1,167 @@ +## 6. Publicités + +### 6.1 Système de campagnes publicitaires + +**Décision** : Interface self-service avec maîtrise budget et métriques détaillées + +**Fonctionnalités publicitaire** : + +#### Création de campagne + +**Paramètres configurables** : + +| Paramètre | Options | Justification | +|-----------|---------|---------------| +| **Budget total** | Montant libre (min 50€) | Maîtrise coût total | +| **Durée campagne** | Date début/fin + étalement | Ex: 300€ sur 2 semaines | +| **Ciblage géographique** | Point GPS / Ville / Département / Région / National | Précision selon besoin | +| **Ciblage horaire** | Plages horaires (ex: 7h-9h, 17h-19h) | Optimisation trajet domicile-travail | +| **Centres d'intérêt** | Tags (ex: Automobile, Voyage) | Ciblage thématique | +| **Tranche d'âge** | Tout public / 13+ / 16+ / 18+ | Respect classifications | + +**Étalement budget** : +``` +Exemple campagne : +- Budget : 300€ +- Durée : 14 jours +- Zone : Département du Var +- Horaires : 7h-9h + 17h-19h (rush) + +Calcul automatique : +→ Budget/jour = 300€ / 14 = 21.43€/jour +→ Diffusions/jour estimées : ~430 (0.05€/écoute) +→ Alerte si budget épuisé avant fin (réajustement possible) +``` + +**Mode de paiement** : +- ✅ Prépaiement obligatoire (évite impayés) +- ✅ Carte bancaire uniquement (Mangopay) +- ✅ Recharge automatique optionnelle (si budget <10%) + +#### Validation et modération + +**Processus** : +1. Publicitaire upload audio pub (formats : MP3, AAC) +2. **Validation manuelle obligatoire** (modérateur RoadWave) + - Délai : 24-48h ouvrées + - Critères : respect réglementation, qualité audio, classification correcte +3. Si accepté → campagne démarre à la date choisie +4. Si refusé → email avec raison + remboursement automatique + +**Contenus interdits en pub** : +- ❌ Alcool, tabac (réglementation française) +- ❌ Jeux d'argent +- ❌ Contenu politique (pendant campagnes électorales) +- ❌ Contenu sexuel ou violence +- ✅ Tous commerces/services légaux + +#### Dashboard métriques engagement + +**Indicateurs temps réel** : + +| Métrique | Description | Utilité | +|----------|-------------|---------| +| **Impressions** | Nombre de diffusions | Volume exposition | +| **Écoutes complètes** | Pub écoutée >80% | Engagement réel | +| **Taux de skip** | % skip après délai min | Qualité contenu | +| **Durée moyenne écoute** | Secondes écoutées | Rétention attention | +| **Likes** | Nombre de likes | Appréciation contenu | +| **Abonnements** | Abonnements au créateur pub | Conversion forte | +| **Coût par écoute** | Budget / écoutes complètes | ROI campagne | +| **Répartition géographique** | Heatmap diffusions | Validation ciblage | +| **Répartition horaire** | Graphique par heure | Optimisation horaires | + +**Métriques engagement avancées** : +- **Taux complétion par tranche d'âge** : identifier audience réceptive +- **Carte de chaleur GPS** : visualiser zones forte écoute +- **Comparatif campagnes** : A/B testing créatifs publicitaires + +**Export données** : +- ✅ CSV/Excel pour analyse externe +- ✅ Graphiques interactifs (Chart.js) +- ✅ Rapport PDF automatique fin de campagne + +#### Gestion budget et alertes + +**Suivi temps réel** : +- Dashboard : Budget restant, % consommé, jours restants +- Projection : "À ce rythme, budget épuisé dans X jours" +- Alerte email/push si : + - Budget consommé à 80% + - Budget consommé à 90% + - Budget épuisé + - Campagne terminée (rapport final) + +**Ajustements en cours** : +- ✅ Pause campagne (budget conservé) +- ✅ Prolonger campagne (recharge budget) +- ✅ Modifier ciblage horaire/géo (si <50% budget consommé) +- ❌ Modifier audio (nécessite nouvelle validation) + +#### Système d'enchères (post-MVP) + +**Optionnel future** : +- Enchère au CPM (coût pour 1000 impressions) +- Priorité selon prix : pub prix élevé → diffusion privilégiée +- Floor price : 2€ CPM minimum +- Évite surcharge pub : max 1 pub / 5 contenus stricte + +**Justification décision MVP** : +- Tarif fixe simple : 0.05€/écoute complète +- Pas de complexité enchères immédiatement +- Scalable : passage enchères ultérieur si demande forte + +--- + +### 6.2 Insertion et fréquence + +**Décision** : Paramétrable admin + respect expérience utilisateur + +**Fréquence d'insertion** : +- **Défaut : 1 pub / 5 contenus** (utilisateurs gratuits) +- **Paramétrable admin** : curseur 1/3 à 1/10 +- **Utilisateurs Premium** : 0 pub (modèle sans publicité) + +**Règles strictes** : +- ⚠️ **Jamais d'interruption** contenu en cours +- Pub s'insère uniquement **entre deux contenus** (pendant délai 2s) +- Rotation : même pub max **3 fois/jour** par utilisateur (évite saturation) +- Limite : max **6 pubs/heure** par utilisateur (évite spam) + +**Ciblage intelligent** : +- Géolocalisation prioritaire (point GPS > ville > département > région > national) +- Centres d'intérêt secondaires (tags utilisateur) +- Horaire (campagne 7h-9h → diffusion uniquement pendant plage) + +**Volume audio normalisé** : +- Pub normalisée à **-14 LUFS** (standard broadcast) +- Évite effet "pub trop forte" (frustration utilisateur) +- Validation automatique via FFmpeg lors encodage + +--- + +### 6.3 Caractéristiques publicités + +**Durée** : +- Minimum : **10 secondes** +- Maximum : **60 secondes** +- Recommandé : **15-30 secondes** (sweet spot engagement) + +**Skippable** : +- Délai minimum obligatoire : **5 secondes** (paramétrable admin : 3-10s) +- Bouton "Passer la publicité" apparaît après délai +- Durée minimale comptabilisée pour facturation + +**Facturation** : +- **Écoute complète** (>80%) : 0.05€ facturé publicitaire +- **Skip après délai min** : 0.02€ (exposition partielle) +- **Skip immédiat** (<5s) : 0€ (pas d'engagement) + +**Justification modèle tarif** : +- Incitatif qualité : pub engageante = coût réduit +- Équitable : publicitaire paie pour attention réelle +- Transparent : dashboard montre écoutes complètes vs skips + +--- + +## Récapitulatif Section 6 diff --git a/docs/regles-metier/17-premium.md b/docs/regles-metier/17-premium.md new file mode 100644 index 0000000..9c2aa13 --- /dev/null +++ b/docs/regles-metier/17-premium.md @@ -0,0 +1,176 @@ +## 10. Premium + +### 10.1 Offre et tarification + +**Décision** : Deux formules sans essai gratuit + +| Formule | Prix | Économie | Prix effectif | +|---------|------|----------|---------------| +| **Mensuel** | 4.99€/mois | - | 4.99€/mois | +| **Annuel** | 49.99€/an | 2 mois offerts | 4.16€/mois | + +**❌ Pas d'essai gratuit** + +**Raisons** : +- **Anti-abus vacances** : évite inscriptions opportunistes (essai 14j avant road trip vacances, puis annulation) +- **Protection revenus créateurs** : les écoutes Premium rémunèrent créateurs dès jour 1 +- **Simplicité** : pas de gestion période trial + conversion +- **Engagement** : utilisateur qui paie dès début = plus engagé + +**❌ Pas de partage familial (MVP)** + +**Raisons** : +- Complexité technique (gestion invitations, validation liens, limite devices) +- Risque abus ("familles" de 6 inconnus) +- Coût dev/support élevé pour ROI incertain +- La plupart des users RoadWave sont individuels (conducteurs) +- **Post-MVP** : Si forte demande, offre "Famille" à 9.99€/mois pour 5 comptes + +**Justification tarif** : +- **Aligné marché bas** : Spotify = 10.99€, YouTube Premium = 11.99€, Apple Music = 10.99€ +- **Prix accessible** : cible conducteurs quotidiens (budget raisonnable) +- **Incitation annuel** : 2 mois offerts = engagement long terme + réduction churn + +--- + +### 10.2 Multi-devices et détection simultanée + +**Décision** : 1 seul stream actif par compte à tout moment + +**Détection connexion simultanée** : + +``` +User A écoute sur iPhone +→ User A lance sur iPad +→ Détection : session active iPhone existe +→ Action : Arrêt lecture iPhone (WebSocket close) +→ Message iPhone : "Lecture interrompue : votre compte est utilisé sur un autre appareil" +→ Lecture démarre iPad +``` + +**Implémentation technique** : + +``` +Redis : active_streams:{user_id} → {device_id, started_at} +TTL : 5 minutes (refresh à chaque heartbeat) + +Heartbeat toutes les 30s depuis app : +→ Si autre device détecté : kill session actuelle +→ Si pas de heartbeat pendant 5 min : considérer session morte +``` + +**Exceptions** : +- Contenus téléchargés (offline) ne comptent pas comme stream actif +- Transition rapide device (<10s) tolérée (changement voiture → maison) + +**Justification** : +- **Anti-partage compte** : empêche 2 personnes d'utiliser même compte Premium +- **Protection revenus créateurs** : 1 abonnement = 1 personne = 1 écoute +- **UX claire** : message explicite, pas de coupure brutale + +--- + +### 10.3 Contenus exclusifs Premium + +**Décision** : Créateur décide (déjà couvert section 9.6) + +**Rappel règles** : +- Toggle "Réservé Premium" par contenu +- Aucune limite de ratio gratuit/premium +- Badge 👑 visible +- Users gratuits : lecture bloquée avec CTA "Passez Premium" + +**Impact algorithme** : +- Contenus premium inclus dans recommandations +- Si user gratuit → skip automatique (ne consomme pas slot) +- Si user premium → diffusé normalement selon score + +--- + +### 10.4 Avantages Premium + +**Inclus dans l'abonnement** : + +| Avantage | Gratuit | Premium | +|----------|---------|---------| +| **Publicités** | 1/5 contenus | 0 (aucune) | +| **Contenus exclusifs** | ❌ Bloqués | ✅ Accès complet | +| **Qualité audio** | 48 kbps Opus | 64 kbps Opus | +| **Mode offline** | 50 contenus max | Illimité | +| **Historique écoute** | 100 derniers | Illimité | + +**Qualité audio** : +- Gratuit : 48 kbps Opus (~20 MB/h) = très correct pour voix +- Premium : 64 kbps Opus (~30 MB/h) = excellente qualité + +**Justification différences** : +- **0 pub** = argument principal (confort écoute) +- **Qualité audio** = avantage tangible audiophiles +- **Offline illimité** = use case road trips longs +- **Pas d'over-engineering** : pas de badges cosmétiques, fonctionnalités sociales, etc. (focus essentiel) + +--- + +### 10.5 Gestion abonnement + +**Souscription** : + +| Canal | Prestataire | Prix | Commission | +|-------|-------------|------|------------| +| **Web (desktop/mobile)** | Mangopay | 4.99€ | 1.8% + 0.18€ = 0.27€ | +| **iOS App** | Apple In-App Purchase | 5.99€ | 30% (Apple) | +| **Android App** | Google Play Billing | 5.99€ | 30% (Google) | + +**Majoration mobile (5.99€)** : +- Apple/Google prennent 30% de commission +- RoadWave majore prix de 20% pour compenser +- **Incitation web** : Email aux users "Abonnez-vous sur roadwave.com pour 4.99€/mois" (38% moins cher en frais !) + +**Renouvellement automatique** : +- Email rappel **7 jours avant** renouvellement +- Email confirmation **après** renouvellement réussi +- Retry automatique si échec paiement (3 tentatives sur 7 jours) +- Annulation automatique après 3 échecs + +**Annulation** : +- Self-service dans Settings app : "Abonnement > Annuler" +- Accès Premium maintenu jusqu'à **fin période payée** +- Pas de remboursement prorata (standard industrie) +- Email confirmation annulation avec date fin d'accès + +**Réabonnement** : +- Possibilité immédiate +- ❌ Pas de nouvelle période d'essai (pas d'essai du tout) + +**Architecture données** : + +```sql +CREATE TABLE subscriptions ( + id UUID PRIMARY KEY, + user_id UUID NOT NULL REFERENCES users(id) UNIQUE, + mangopay_recurring_payin_id VARCHAR(255), -- Null si IAP + mangopay_user_id VARCHAR(255), -- Null si IAP + apple_transaction_id VARCHAR(255), -- Null si Mangopay + google_purchase_token VARCHAR(255), -- Null si Mangopay + status VARCHAR(50) NOT NULL, -- 'active', 'cancelled', 'expired', 'past_due' + plan VARCHAR(50) NOT NULL, -- 'monthly', 'yearly' + current_period_start TIMESTAMP NOT NULL, + current_period_end TIMESTAMP NOT NULL, + cancelled_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); +``` + +**Vérification Premium en temps réel** : + +``` +Cache Redis : premium:{user_id} → boolean (TTL 1h) +Refresh via webhooks : +- Mangopay : PAYIN_NORMAL_SUCCEEDED, PAYIN_NORMAL_FAILED +- Apple : App Store Server Notifications +- Google : Real-time Developer Notifications +``` + +--- + +## Récapitulatif Section 10 diff --git a/docs/regles-metier/18-monetisation-createurs.md b/docs/regles-metier/18-monetisation-createurs.md new file mode 100644 index 0000000..ce63b02 --- /dev/null +++ b/docs/regles-metier/18-monetisation-createurs.md @@ -0,0 +1,308 @@ +## 9. Monétisation créateurs + +### 9.1 Pourboires + +**Décision** : ❌ Fonctionnalité abandonnée pour le MVP + +**Raisons** : +- Complexité juridique (collecte pour compte de tiers, TVA variable) +- Frais de transaction élevés sur petits montants (Mangopay ~1.8% + 0.18€) +- UX additionnelle à développer (wallet, transactions, confirmations) +- Charge comptable importante pour la plateforme + +**Post-MVP** : Possible réintégration avec crypto (Bitcoin/Lightning Network) si législation UE l'autorise clairement (régulation MiCA en cours). + +--- + +### 9.2 Conditions d'activation de la monétisation + +**Décision** : 5 critères cumulatifs obligatoires + +| Critère | Seuil | Justification | +|---------|-------|---------------| +| **Ancienneté** | Compte créé depuis ≥ 3 mois | Anti-fraude : temps de détecter comportements suspects | +| **Popularité** | ≥ 500 abonnés | Garantit audience réelle et engagée | +| **Engagement** | ≥ 10 000 écoutes complètes cumulées | Créateurs produisant du contenu de qualité | +| **Fiabilité** | Aucun strike actif, 0 contenu modéré dans les 6 derniers mois | Historique propre requis | +| **Régularité** | ≥ 5 contenus publiés dans les 90 derniers jours | Activité constante | + +**Vérification** : Automatique via requêtes SQL lors de la demande d'activation + +**Affichage** : +- Bouton "Demander la monétisation" dans profil créateur +- Si critères non remplis → affichage progression vers objectifs +- Si critères remplis → redirection vers KYC Mangopay + +**Justification** : +- **Anti-fraude** : Le délai de 3 mois permet de détecter les comptes suspects +- **Qualité** : Seuls les créateurs sérieux avec audience réelle sont monétisés +- **Coût administratif** : Réduit le nombre de comptes à gérer (KYC, comptabilité, virements) +- **Légitimité** : Audience organique prouvée + +--- + +### 9.3 KYC (Know Your Customer) et inscription + +**Décision** : Statut juridique professionnel obligatoire + +**Statuts acceptés** : +- Auto-entrepreneur (micro-BNC pour artistes/créateurs de contenu) +- SARL/SAS/SASU (sociétés) + +**Documents requis** : + +| Document | Obligatoire | Format | Validité | +|----------|-------------|--------|----------| +| **SIRET** | ✅ | 14 chiffres | Permanent | +| **RIB professionnel** | ✅ | IBAN FR | Permanent | +| **Pièce d'identité** | ✅ | CNI/Passeport | En cours de validité | +| **Numéro TVA intracommunautaire** | ⚠️ Si applicable | FR + 11 chiffres | Permanent | +| **Kbis <3 mois** | ⚠️ Si société | PDF | <3 mois | + +**Vérification** : Via Mangopay (KYC intégré + vérification bancaire) + +**Délai** : 24-72h si documents conformes + +**Rejet possible si** : +- Documents invalides/illisibles +- Identité ne correspond pas au compte RoadWave +- Liste noire anti-blanchiment (vérification automatique Mangopay) +- RIB non professionnel (particulier) + +**Base légale** : +- **Conformité fiscale** : L'État français impose déclaration revenus >1200€/an (DAS2) +- **Anti-blanchiment** : Directive EU 2018/843 (5ème directive LCB-FT) +- **RGPD** : Données hébergées EU via Mangopay (conforme) + +**Justification** : +- **Responsabilité légale** : RoadWave doit pouvoir prouver identité réelle créateurs monétisés +- **Automatisation** : Mangopay gère tout (KYC, vérifications, conformité, e-wallets) +- **KYC gratuit** : inclus dans l'offre Mangopay (vs 1.20€ chez Stripe) +- **Souveraineté EU** : Mangopay est européen (France/Luxembourg), régulé ACPR + +--- + +### 9.4 Sources de revenus créateurs + +#### A) Publicités (utilisateurs gratuits) + +**Formule** : **3€ / 1000 écoutes complètes** (CPM créateur) + +**Répartition économique** : + +``` +Publicité facturée par RoadWave : 0.05€/écoute complète = 50€ CPM +├─ Créateur touche : 3€ (6% du CA pub) +└─ Plateforme garde : 47€ (94%) + ├─ CDN + infrastructure : ~10-15€ + ├─ Modération + support : ~5-10€ + ├─ Développement + R&D : ~10-15€ + └─ Marge opérationnelle : ~10-15€ +``` + +**Exemple concret** : +- 10 000 écoutes/mois → créateur touche **30€** +- 50 000 écoutes/mois → créateur touche **150€** +- 100 000 écoutes/mois → créateur touche **300€** + +**Comparaison industrie** : +- YouTube : 3-5€/1000 vues +- Spotify : 3-4€/1000 écoutes +- RoadWave : 3€/1000 écoutes (aligné) + +**Règles comptabilisation** : +- ✅ Écoute complète = ≥80% du contenu écouté +- ✅ Utilisateur gratuit uniquement +- ❌ Écoutes Premium ne comptent pas ici (autre système) +- ❌ Bots détectés exclus (rate limiting + analyse patterns) + +--- + +#### B) Abonnés Premium + +**Formule** : **70% au créateur, 30% à la plateforme** + +**Répartition proportionnelle au temps d'écoute effectif** : + +``` +Utilisateur Premium = 4.99€/mois +├─ 3.49€ reversés aux créateurs (70%) +└─ 1.50€ gardés par plateforme (30%) + +Si l'utilisateur écoute 3 créateurs ce mois : +- Créateur A : 10h d'écoute (50%) → 1.75€ +- Créateur B : 6h d'écoute (30%) → 1.05€ +- Créateur C : 4h d'écoute (20%) → 0.70€ +``` + +**Calcul technique** : + +```sql +-- Pour chaque utilisateur Premium +SELECT + creator_id, + SUM(listen_duration_seconds) AS total_seconds, + (SUM(listen_duration_seconds) / total_user_seconds) AS ratio, + (4.99 * 0.70 * ratio) AS revenue_euros +FROM premium_listens +WHERE user_id = :user_id + AND month = :current_month +GROUP BY creator_id; +``` + +**Comparaison industrie** : +- YouTube Premium : 70/30 +- Spotify : 70/30 +- Apple Music : 52/48 (moins avantageux) +- RoadWave : 70/30 (standard) + +**Justification** : +- **Standard industrie** : ratio équitable éprouvé +- **Incitation qualité** : créateurs les plus écoutés gagnent plus +- **Équité** : pas de "winner takes all", chaque créateur écouté reçoit sa part +- **Marge plateforme** : 30% couvre absence revenus pub sur Premium + +--- + +### 9.5 Paiement des créateurs + +**Seuil minimum** : 50€ + +- En dessous → solde reporté mois suivant +- Évite frais bancaires sur micro-sommes +- Standard industrie (YouTube/Twitch/Spotify = 50-100€) + +**Fréquence** : Mensuelle + +| Date | Action | +|------|--------| +| **Dernier jour du mois** (ex: 31 janvier) | Calcul revenus du mois via SQL | +| **1-14 du mois suivant** | Traitement contestations/fraudes éventuelles | +| **15 du mois suivant** (ex: 15 février) | Virement SEPA via Mangopay (Payout) | +| **16-18 du mois suivant** | Réception virement (1-3 jours ouvrés SEPA) | + +**Virement via Mangopay** : +- SEPA pour comptes EU (gratuit, 1-3 jours) +- Virement international hors EU (frais variables selon pays, rare en pratique) +- **E-wallets automatiques** : chaque créateur possède un wallet Mangopay où ses revenus sont transférés automatiquement + +**Tableau de bord créateur** (temps réel) : + +| Métrique | Description | Mise à jour | +|----------|-------------|-------------| +| **Revenus pub** | Écoutes × CPM | Temps réel | +| **Revenus premium** | Abonnés actifs × ratio écoute | Temps réel | +| **Solde disponible** | Total revenus mois en cours | Temps réel | +| **Solde en attente** | Revenus mois précédent (paiement le 15) | Figé fin de mois | +| **Historique virements** | Liste des paiements reçus | Permanent | +| **Export comptable CSV** | Données pour expert-comptable | Téléchargement | + +**Gestion échecs virement** : +1. Tentative 1 (15 du mois) → échec +2. Retry automatique J+3 +3. Retry automatique J+7 +4. Si 3 échecs → suspension monétisation + email créateur (RIB invalide) + +--- + +### 9.6 Contenus Premium exclusifs + +**Décision** : Créateur décide individuellement pour chaque contenu + +**Fonctionnement** : +- Toggle "Réservé Premium" lors création/édition contenu +- **Aucune limite imposée** : créateur peut mettre 0%, 50% ou 100% en premium +- Badge 👑 visible sur interface utilisateur + +**Comportement utilisateurs gratuits** : +- Contenu premium visible dans liste/algo +- Tentative lecture → overlay bloquant +- Message : "Ce contenu est réservé aux abonnés Premium" +- CTA : "Passez Premium pour 4.99€/mois" + +**Comportement algorithme** : +- Contenus premium inclus dans recommandations +- Si user gratuit → contenu skippé automatiquement (ne consomme pas de slot) +- Si user premium → diffusé normalement + +**Métadonnées** : +- Champ `is_premium` (boolean) en base +- Index sur ce champ pour requêtes rapides +- Cache Redis : `content:{id}:premium` (TTL 1h) + +**Justification** : +- **Liberté créateur** : chaque créateur choisit sa stratégie (freemium, tout gratuit, tout premium) +- **Incitation Premium** : contenu exclusif = argument fort pour s'abonner +- **Équité** : un petit créateur peut tout mettre en premium, un gros peut tout offrir gratuitement + +--- + +### 9.7 Obligations fiscales + +**RoadWave génère automatiquement** : + +| Document | Fréquence | Destinataire | Base légale | +|----------|-----------|--------------|-------------| +| **Relevé mensuel PDF** | Chaque mois | Créateur | Transparence | +| **Export CSV comptable** | À la demande | Créateur + expert-comptable | Facilitation déclarations | +| **DAS2 annuel** | Si >1200€/an | Impôts (DGFIP) | Obligation légale France | + +**Créateur responsable de** : +- Déclarer ses revenus à l'URSSAF (cotisations sociales auto-entrepreneur ou IS/IR) +- Déclarer ses revenus aux impôts (IR ou IS selon statut) +- Gérer sa TVA si applicable (franchise en base jusqu'à ~37K€/an en micro-BNC) +- Conserver justificatifs **10 ans** (obligation légale comptable) + +**Mangopay transmet automatiquement** : +- Données aux autorités fiscales EU via **DAC7** (directive 2021/514) +- Justificatif de chaque virement (preuve bancaire pour comptabilité créateur) + +**Exemple DAS2** : +``` +Si créateur a touché 2500€ en 2026 : +→ RoadWave envoie DAS2 aux impôts en janvier 2027 +→ Créateur reçoit copie par email +→ Créateur doit déclarer ces 2500€ dans sa déclaration annuelle +``` + +**Justification** : +- **Conformité légale** : RoadWave doit déclarer revenus versés (DAS2, DAC7) +- **Responsabilité fiscale** : Le créateur reste responsable de sa déclaration (impossible de gérer pour lui) +- **Automatisation** : Minimise charge administrative côtés créateur et plateforme + +--- + +### 9.8 Désactivation et suspension monétisation + +**Créateur peut** : +- Désactiver temporairement (vacances, pause création) +- Réactiver sans refaire KYC si données à jour (<2 ans) +- Solde conservé pendant désactivation + +**Plateforme suspend automatiquement si** : + +| Motif | Action | Réversible | +|-------|--------|------------| +| **Strike 3+ actif** | Suspension immédiate | Oui, après résolution strikes | +| **Compte bancaire invalide** | Suspension après 3 échecs virement | Oui, après mise à jour RIB | +| **Documents KYC expirés** | Suspension avec préavis 30j | Oui, après renouvellement docs | +| **Fraude détectée** | Suspension immédiate + enquête | Cas par cas | + +**Suppression définitive si** : +- Demande du créateur (solde versé sous 30 jours) +- Inactivité 24 mois + solde <50€ (purge RGPD) +- Ban définitif compte (Strike 4) + +**Notification** : +- Email + in-app pour toute suspension +- Raison explicite fournie +- Procédure de réactivation indiquée + +**Justification** : +- **Flexibilité** : créateur peut faire pause sans perdre statut +- **Sécurité** : plateforme doit pouvoir suspendre en cas problème légal/technique +- **RGPD** : suppression auto données inactives après délai raisonnable + +--- + +## Récapitulatif Section 9 diff --git a/docs/regles-metier/19-autres-comportements.md b/docs/regles-metier/19-autres-comportements.md new file mode 100644 index 0000000..59f1a6f --- /dev/null +++ b/docs/regles-metier/19-autres-comportements.md @@ -0,0 +1,633 @@ +## 15. Autres comportements + +### 15.1 Partage de contenu + +**Décision** : Système de partage complet avec web player + +#### 15.1.1 Bouton "Partager" + +**Disponibilité** : Partout dans l'application + +**Emplacements** : +- Player en lecture (bouton dans contrôles) +- Page profil créateur (sur chaque contenu) +- Liste de recherche (menu contextuel) +- Historique personnel + +**Icône** : ⬆️ (universelle iOS/Android) + +**Menu options** : +- Copier le lien +- WhatsApp +- Email +- SMS +- Plus... (sheet natif OS) + +**Justification** : +- Viralité = croissance organique gratuite +- Aucune friction, partage universel + +--- + +#### 15.1.2 Comportement du lien partagé + +**Format URL** : `https://roadwave.fr/share/c/[content_id]` + +**Comportement multi-plateforme** : + +``` +User clique lien partagé + ↓ +Page web responsive + ↓ +┌─────────────────────────────────┐ +│ Si app installée │ +│ → Deep link (ouverture directe) │ +└─────────────────────────────────┘ + ↓ +┌─────────────────────────────────┐ +│ Si app non installée │ +│ → Web player + CTA téléchargement│ +└─────────────────────────────────┘ +``` + +**Contenu de la page web** : + +```html +┌───────────────────────────────────────┐ +│ RoadWave │ +├───────────────────────────────────────┤ +│ [Image cover 16:9] │ +│ │ +│ 📻 Titre du contenu │ +│ Par @créateur · 12 min · 🎧 2.3K │ +│ │ +│ 📍 Paris 5e · Ancré │ +│ 🏷️ #Voyage #Histoire │ +│ │ +│ Description : Lorem ipsum... │ +│ │ +│ [▶️ Écouter maintenant] │ +│ (Player HTML5 si contenu public) │ +│ │ +│ ────────────────────────────────── │ +│ │ +│ 📱 Télécharger l'app RoadWave │ +│ [App Store] [Google Play] │ +│ │ +│ [Voir le profil de @créateur] │ +└───────────────────────────────────────┘ +``` + +**Métadonnées Open Graph (SEO)** : + +```html + + + + + + + + +``` + +**Deep linking** : +- iOS : Universal Links (configuration `apple-app-site-association`) +- Android : App Links (configuration `assetlinks.json`) +- URL scheme : `roadwave://content/[content_id]` + +**Justification** : +- Meilleure viralité (partage social optimisé) +- SEO (contenus indexés Google) +- UX optimale (web + app) +- Coût : 0€ (backend simple + CDN existant) + +--- + +#### 15.1.3 Contenus Premium partagés + +**Décision** : Preview 30 secondes + paywall + +**Comportement** : + +1. User clique lien contenu Premium partagé +2. Page web affiche badge "👑 Contenu Premium" +3. Player démarre automatiquement +4. Après **30 secondes exactement** : + - Fade out audio (2 secondes) + - Overlay apparaît : + +``` +┌─────────────────────────────────┐ +│ 👑 Contenu réservé Premium │ +│ │ +│ Profitez de ce contenu complet │ +│ et de milliers d'autres │ +│ sans publicité │ +│ │ +│ [Passer Premium - 4.99€/mois] │ +│ [Télécharger l'app] │ +└─────────────────────────────────┘ +``` + +5. Utilisateur peut : + - S'abonner Premium (redirection web Mangopay) + - Télécharger l'app (redirection stores) + - Rejouer les 30 premières secondes (illimité) + +**Tracking** : +- Métriques créateur : "Partages Premium" + "Conversions Premium" +- Créateur touche sa part si conversion (70%) + +**Justification** : +- Équilibre viralité / monétisation +- 30s = assez pour donner envie, pas assez pour satisfaire +- Protège revenus créateurs + +--- + +### 15.2 Profil créateur + +**Décision** : Profil public complet et transparent + +#### 15.2.1 Structure de la page profil + +**URL** : `https://roadwave.fr/@[pseudo]` + +**Layout** : + +``` +┌────────────────────────────────────────┐ +│ [Photo profil 120×120] │ +│ @pseudo ✓ │ +│ [Badge vérifié si applicable] │ +│ │ +│ Bio : Lorem ipsum dolor sit amet... │ +│ (300 caractères max) │ +│ │ +│ 🎧 1.2K abonnés │ +│ 📻 42 contenus │ +│ ⏱️ 18h de contenu créé │ +│ 🔊 54K écoutes totales │ +│ │ +│ [S'abonner] [Partager profil] [•••] │ +│ │ +│ ──────────────────────────────────── │ +│ │ +│ Contenus ▼ [Plus récents ▼] │ +│ │ +│ ┌──────────────────────────────────┐ │ +│ │ [Cover] Titre contenu 1 │ │ +│ │ 12 min · 🎧 2.3K · 📍 Paris │ │ +│ │ [▶️] │ │ +│ └──────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────┐ │ +│ │ [Cover] Titre contenu 2 │ │ +│ │ 8 min · 🎧 5.1K · 📍 Lyon │ │ +│ │ [▶️] │ │ +│ └──────────────────────────────────┘ │ +│ │ +│ [Charger plus] │ +└────────────────────────────────────────┘ +``` + +**Informations affichées** : + +| Élément | Visibilité | Détails | +|---------|------------|---------| +| **Photo + pseudo** | ✅ Public | Identité visuelle | +| **Badge vérifié ✓** | ✅ Public (si applicable) | Compte authentique | +| **Bio** | ✅ Public | 0-300 caractères, markdown basique (gras, italique, liens) | +| **Nombre abonnés** | ✅ Public | Arrondi si >1000 (ex: 1.2K, 54K) | +| **Nombre contenus** | ✅ Public | Exact | +| **Durée totale créée** | ✅ Public | Arrondi en heures (ex: 18h, 142h) | +| **Écoutes totales** | ✅ Public | Arrondi (ex: 54K, 1.2M) | +| **Liste abonnés** | ❌ Privé | Protection vie privée (RGPD) | +| **Revenus** | ❌ Privé | Confidentialité financière | +| **Localisation précise** | ❌ Privé | Sécurité | +| **Email** | ❌ Privé | Anti-spam | + +**Tri des contenus** : + +| Option | Comportement | +|--------|--------------| +| **Plus récents** | Date publication DESC (défaut) | +| **Plus populaires** | Écoutes complètes × (1 + (date_publication - now) / 90 jours) | +| **Plus anciens** | Date publication ASC | +| **Par tag** | Filtre multi-sélection tags | + +**Recherche locale** : +- Barre recherche dans profil : "Rechercher dans les contenus de @pseudo" +- Recherche full-text sur titres + descriptions + +**Actions menu [•••]** : +- Partager profil +- Signaler profil (spam, usurpation) +- Bloquer créateur (masque tous ses contenus) + +--- + +#### 15.2.2 Statistiques publiques + +**Décision** : Stats arrondies et motivantes + +**Affichage public** : + +| Métrique | Format affichage | Exemple | +|----------|------------------|---------| +| **Abonnés** | Exact si <1000, arrondi sinon | 342 / 1.2K / 54K / 1.2M | +| **Écoutes totales** | Arrondi dès 1000 | 842 / 5.4K / 142K / 2.1M | +| **Contenus publiés** | Exact | 42 contenus | +| **Durée totale** | Arrondi en heures | 18h / 142h de contenu | + +**Métriques PRIVÉES (créateur uniquement)** : + +| Métrique | Disponible dans dashboard créateur | +|----------|-------------------------------------| +| **Taux complétion moyen** | 78% (écoutes >80% / écoutes totales) | +| **Évolution abonnés** | Graphique 30j / 90j / 1 an | +| **Écoutes par contenu** | Tableau détaillé | +| **Revenus** | Dashboard monétisation dédié | +| **Taux conversion Premium** | Partages → conversions | +| **Démographie** | Âge / zone géo (agrégée, anonymisée) | + +**Justification** : +- Arrondi = évite comparaisons anxiogènes +- Preuve sociale pour nouveaux auditeurs (trust) +- Gamification douce (motivation créateurs) +- Privacy by design + +--- + +#### 15.2.3 Badge vérifié + +**Décision** : Badge unique ✓ (vérifié officiel) + +**Critères d'attribution** (au moins UN des critères) : + +1. **KYC monétisation validé** : identité vérifiée via Mangopay KYC +2. **Célébrité / Média officiel** : validation manuelle équipe RoadWave +3. **Communauté significative** : ≥10K abonnés + compte actif >6 mois + +**Affichage** : +- Badge bleu **✓** accolé au pseudo (partout : profil, player, recherche) +- Tooltip au survol/appui long : "Compte vérifié" + +**Processus d'obtention** : + +| Type | Processus | +|------|-----------| +| **Automatique (KYC)** | Badge attribué dès validation documents Mangopay | +| **Manuel (célébrité)** | Formulaire demande → équipe vérifie identité → validation 48-72h | +| **Automatique (10K)** | Badge attribué automatiquement à 10K abonnés si compte >6 mois | + +**Retrait du badge** : +- Suspension monétisation → badge retiré temporairement +- Strikes multiples → badge retiré définitivement +- Usurpation identité détectée → ban + retrait + +**Justification** : +- Combat usurpations d'identité +- Trust auditeurs (surtout pour médias/personnalités) +- Simplicité (1 seul badge, pas de gamification excessive) +- Coût : 0€ (champ boolean `verified` en DB) + +--- + +### 15.3 Recherche + +**Décision** : Recherche full-text + géo + filtres avancés + +#### 15.3.1 Recherche par mot-clé + +**Implémentation** : PostgreSQL full-text search (français) + +**Configuration technique** : + +```sql +-- Index full-text optimisé français +CREATE INDEX idx_content_search ON contents +USING GIN( + to_tsvector('french', + coalesce(title, '') || ' ' || + coalesce(description, '') || ' ' || + coalesce(creator_pseudo, '') + ) +); + +-- Recherche avec ranking +SELECT + c.*, + ts_rank( + to_tsvector('french', c.title || ' ' || c.description), + plainto_tsquery('french', $search_query) + ) AS rank +FROM contents c +WHERE to_tsvector('french', c.title || ' ' || c.description) + @@ plainto_tsquery('french', $search_query) +ORDER BY rank DESC, listen_count DESC +LIMIT 20; +``` + +**Champs indexés** : +- Titre du contenu (poids × 3) +- Description (poids × 1) +- Pseudo créateur (poids × 2) +- Tags (poids × 1.5) + +**Fonctionnalités** : + +| Feature | Description | +|---------|-------------| +| **Stemming français** | "voyages" trouve "voyage", "voyager", etc. | +| **Correction auto** | Suggestion si 0 résultat | +| **Recherches populaires** | "Essayez plutôt : balade paris, audio-guide louvre" | +| **Historique personnel** | 10 dernières recherches sauvegardées | +| **Autocomplete** | Suggestions pendant frappe (top 5) | + +**Coût** : 0€ (PostgreSQL natif) + +**Migration future** : +- Si >100K contenus : Meilisearch (typo-tolerance avancée, ~20-50€/mois) +- Si >1M contenus : Elasticsearch cluster + +**Justification** : +- PostgreSQL full-text = performant jusqu'à 500K contenus +- Stemming français natif +- 0€, aucune dépendance externe + +--- + +#### 15.3.2 Recherche géographique + +**Décision** : Recherche lieu + rayon paramétrable + +**Interface utilisateur** : + +``` +┌─────────────────────────────────────┐ +│ 🔍 Recherche contenu... │ +├─────────────────────────────────────┤ +│ �� Lieu │ +│ [Paris, France ▼] │ +│ · Autour de moi (GPS actuel) │ +│ · Entrer une adresse/ville │ +│ │ +│ 📏 Rayon de recherche │ +│ [●─────────────────] 50 km │ +│ (curseur 5 km → 500 km) │ +│ │ +│ 🗺️ [Afficher sur carte] │ +└─────────────────────────────────────┘ +``` + +**Géocodage** : + +| Service | Usage | Coût | +|---------|-------|------| +| **Nominatim (OSM)** | MVP (API publique) | 0€ (rate limit 1 req/s) | +| **Nominatim self-hosted** | Scale (Docker) | 20-50€/mois VPS | +| **Mapbox Geocoding** | Fallback premium | 0.50€ / 1000 requêtes | + +**Processus de recherche géo** : + +1. User tape "Louvre" ou "Paris" +2. Autocomplete via Nominatim → liste suggestions +3. User sélectionne → récupération coordonnées (lat, lon) +4. Requête PostGIS : + +```sql +SELECT c.*, + ST_Distance(c.location::geography, ST_Point($lon, $lat)::geography) AS distance +FROM contents c +WHERE ST_DWithin( + c.location::geography, + ST_Point($lon, $lat)::geography, + $radius_meters +) +ORDER BY distance ASC; +``` + +**Affichage résultats** : +- Tri par défaut : distance croissante +- Indication distance : "À 2.3 km" / "À 15 km" / "À 142 km" +- Option carte : markers cliquables (clustering si >50 résultats) + +**Coût** : +- MVP : 0€ (Nominatim public) +- Scale : 20-50€/mois (Nominatim self-hosted Docker) + +**Justification** : +- Essentiel pour tourisme / planification trajet +- OpenStreetMap = pas de dépendance Google +- PostGIS = performant (index GIST natif) + +--- + +#### 15.3.3 Filtres avancés + +**Décision** : 7 catégories de filtres combinables + +**Interface filtres** : + +``` +┌─────────────────────────────────────┐ +│ Filtres [×] │ +├─────────────────────────────────────┤ +│ Type de contenu │ +│ ☐ Contenu court (<5 min) │ +│ ☐ Podcast (>5 min) │ +│ ☐ Radio live │ +│ ☐ Audio-guide │ +│ │ +│ Durée │ +│ ○ Toutes durées │ +│ ○ <5 min │ +│ ○ 5-15 min │ +│ ○ 15-30 min │ +│ ○ >30 min │ +│ │ +│ Classification âge │ +│ ☐ Tout public │ +│ ☐ 13+ │ +│ ☐ 16+ │ +│ ☐ 18+ │ +│ │ +│ Géo-pertinence │ +│ ☐ Ancré (lieu précis) │ +│ ☐ Contextuel (zone large) │ +│ ☐ Neutre (national) │ +│ │ +│ Tags (multi-sélection) │ +│ ☐ Automobile ☐ Voyage │ +│ ☐ Famille ☐ Histoire │ +│ ☐ Économie ☐ Sciences │ +│ ... (liste complète tags) │ +│ │ +│ Date de publication │ +│ ○ Toutes dates │ +│ ○ Dernières 24h │ +│ ○ Cette semaine │ +│ ○ Ce mois │ +│ ○ Cette année │ +│ │ +│ Abonnement │ +│ ○ Tous les contenus │ +│ ○ Gratuits uniquement │ +│ ○ Premium uniquement 👑 │ +│ │ +│ ────────────────────────────── │ +│ [Réinitialiser] [Appliquer] │ +└─────────────────────────────────────┘ +``` + +**Options de tri** : + +| Tri | Algorithme | +|-----|-----------| +| **Pertinence** | Score recherche × (1 + log(listen_count + 1)) | +| **Popularité** | Écoutes complètes derniers 30j DESC | +| **Récent** | Date publication DESC | +| **Proximité** | Distance GPS ASC (si recherche géo active) | +| **Durée** | Durée audio ASC ou DESC | + +**Sauvegarde de recherches** : + +- Bouton "💾 Sauvegarder cette recherche" +- Nom personnalisable : "Podcasts voyage Paris" +- Maximum **5 recherches sauvegardées** +- Accès rapide : onglet "Recherches sauvegardées" dans page recherche +- Notifications optionnelles : "3 nouveaux contenus dans 'Podcasts voyage Paris'" + +**Performances** : + +```sql +-- Index composites pour filtres +CREATE INDEX idx_content_filters ON contents ( + content_type, + duration, + age_rating, + geo_type, + published_at +); + +-- Index GIN pour tags +CREATE INDEX idx_content_tags ON contents USING GIN(tags); +``` + +**Coût** : 0€ (PostgreSQL + index standards) + +**Justification** : +- Filtres essentiels pour découvrabilité +- Combinables = puissance maximale +- Sauvegarde = gain temps utilisateurs réguliers + +--- + +#### 15.3.4 Page de résultats + +**Décision** : Liste avec previews enrichies + +**Layout résultats** : + +``` +┌─────────────────────────────────────────┐ +│ 🔍 "voyage paris" │ +│ 42 résultats · Tri : Pertinence ▼ │ +│ [Filtres] [Carte] │ +├─────────────────────────────────────────┤ +│ ┌─────────────────────────────────────┐ │ +│ │ [Cover ] Balade à Paris │ │ +│ │ [16:9 ] @paris_stories ✓ │ │ +│ │ [Image ] 12 min · 🎧 2.3K │ │ +│ │ 📍 Paris 5e · Ancré │ │ +│ │ 🏷️ #Voyage #Histoire │ │ +│ │ [▶️ Écouter] [⋮] │ │ +│ └─────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ [Cover ] Secrets Montmartre │ │ +│ │ [16:9 ] @explore_paris │ │ +│ │ [Image ] 8 min · 🎧 5.1K │ │ +│ │ 📍 Paris 18e · Guide │ │ +│ │ 🏷️ #Voyage #Art │ │ +│ │ [▶️ Écouter] [⋮] │ │ +│ └─────────────────────────────────────┘ │ +│ │ +│ [Charger plus] (20 suivants) │ +└─────────────────────────────────────────┘ +``` + +**Informations par résultat** : + +| Élément | Affichage | +|---------|-----------| +| **Cover image** | 16:9, 120×68 px, lazy loading | +| **Titre** | Tronqué 2 lignes max | +| **Créateur** | @pseudo + badge ✓ si vérifié, cliquable → profil | +| **Durée** | Format : "3 min" / "12 min" / "1h 24 min" | +| **Écoutes** | Arrondi : "2.3K" / "54K" / "1.2M" | +| **Localisation** | Ville + type géo (Ancré/Contextuel/Neutre) | +| **Tags** | Maximum 3 premiers tags | +| **Badge Premium** | 👑 si contenu premium | +| **Distance** | Si recherche géo : "À 2.3 km" | + +**Actions contextuelles [⋮]** : +- Partager +- Ajouter à une playlist (future feature) +- Télécharger (offline) +- Signaler + +**Pagination** : +- **20 résultats** par page +- Infinite scroll (charger automatiquement si scroll >80%) +- Bouton "Charger 20 suivants" en bas (fallback si scroll auto désactivé) + +**Vue carte (alternative)** : +- Bouton toggle "Liste / Carte" +- Map Leaflet (OpenStreetMap) +- Markers cliquables → popup avec preview +- Clustering si >50 résultats proches + +**Coût** : 0€ (Leaflet open source + OSM tiles gratuit) + +**Justification** : +- Équilibre information / compacité +- Lazy loading = performances +- Infinite scroll = UX moderne + +--- + +## Récapitulatif Section 15 + +| Point | Décision | Coût | Complexité | +|-------|----------|------|------------| +| **15.1.1** Bouton partager | Disponible partout (⬆️), menu natif OS | 0€ | Faible | +| **15.1.2** Lien partagé | Web player + deep link + Open Graph SEO | 0€ | Moyenne | +| **15.1.3** Premium partagé | Preview 30s + paywall overlay | 0€ | Faible | +| **15.2.1** Page profil | Profil public complet (stats + bio + contenus + tri) | 0€ | Faible | +| **15.2.2** Stats publiques | Arrondies (abonnés, écoutes, durée totale) | 0€ | Faible | +| **15.2.3** Badge vérifié | ✓ si KYC/célébrité/>10K abonnés | 0€ | Faible | +| **15.3.1** Recherche texte | PostgreSQL full-text french + stemming | 0€ | Moyenne | +| **15.3.2** Recherche géo | Lieu + rayon (Nominatim OSM) | 0-50€/mois | Moyenne | +| **15.3.3** Filtres | 7 catégories combinables + sauvegarde recherches | 0€ | Moyenne | +| **15.3.4** Page résultats | Liste enrichie + vue carte Leaflet + infinite scroll | 0€ | Moyenne | + +**Coût total MVP : 0-50€/mois** (Nominatim self-hosted optionnel) + +--- + +## Points d'attention pour Gherkin + +- Tester partage contenu public vs Premium (preview 30s) +- Tester deep linking iOS/Android (ouverture app si installée) +- Tester Open Graph (aperçu correct sur WhatsApp, Twitter, Facebook) +- Tester profil public (stats arrondies, badge vérifié) +- Tester recherche full-text français (stemming, accents) +- Tester recherche géo + rayon (PostGIS distance) +- Tester combinaison filtres multiples (AND logic) +- Tester sauvegarde recherches (max 5) +- Tester pagination infinite scroll + fallback bouton +- Tester vue carte Leaflet (clustering, markers cliquables) diff --git a/docs/regles-metier/ANNEXE-POST-MVP.md b/docs/regles-metier/ANNEXE-POST-MVP.md new file mode 100644 index 0000000..7c7ef24 --- /dev/null +++ b/docs/regles-metier/ANNEXE-POST-MVP.md @@ -0,0 +1,687 @@ +# Annexe : Fonctionnalités reportées Post-MVP + +**Date** : 2026-01-19 +**Statut** : Fonctionnalités validées mais reportées après le MVP + +--- + +## Sommaire + +1. [Classification politique et équilibre éditorial](#1-classification-politique-et-équilibre-éditorial) +2. [Système de pourboires créateurs](#2-système-de-pourboires-créateurs) + +--- + +## 1. Classification politique et équilibre éditorial + +> ⚠️ **Reporté post-MVP** pour raisons de coût, complexité et risques juridiques. + +### Contexte du report + +**Raisons** : +- **Coût modération** : Classification manuelle humaine très coûteuse (~2000€/mois pour 1-2 modérateurs senior full-time) +- **Risque juridique** : Accusations de biais éditorial, contentieux DSA +- **Complexité technique** : Dashboard audit, logs 3 ans, alertes déséquilibre +- **Controverse** : Peut créer polémique dès le lancement +- **Pas essentiel MVP** : L'application fonctionne sans ce système + +**Version MVP** (actuelle) : +- Tag "Politique" simple (comme "Économie", "Sport") +- Pas de classification gauche/droite +- Pas d'équilibrage imposé +- Option utilisateur "Masquer politique" → 0% contenus politiques + +--- + +### Spécifications complètes (future implémentation) + +**Échelle de classification** (5 niveaux) : +- 🔴 **Extrême gauche** (anticapitalisme radical, révolution) +- 🟠 **Gauche** (écologie, social, critique capitalisme modérée) +- ⚪ **Centre/Neutre** (pas de positionnement politique clair) +- 🔵 **Droite** (sécurité, tradition, économie libérale) +- 🟣 **Extrême droite** (nationalisme radical, conservatisme extrême) +- 🟢 **Non politique** (enfants, musique, fiction, culture générale) + +**Qui classifie** : +- ❌ Pas de classification automatique IA (outil informatif uniquement, jamais décisionnaire) +- ✅ Modérateurs senior après transcription +- ✅ Créateur peut contester via processus d'appel + +**Affichage** : +- Badge politique visible : **au choix de l'utilisateur** (paramètre "Afficher orientation politique") +- Par défaut : badges masqués (UX neutre) + +**Règles de diffusion (équilibre imposé)** : + +| Préférence utilisateur | Répartition | Justification | +|------------------------|-------------|---------------| +| **Équilibré** (défaut) | 35% gauche / 35% droite / 30% centre-neutre | Neutralité plateforme | +| **Plutôt gauche** | 50% gauche / 20% droite / 30% centre-neutre | Préférence respectée avec minimum opposition | +| **Plutôt droite** | 50% droite / 20% gauche / 30% centre-neutre | Préférence respectée avec minimum opposition | +| **Masquer politique** | 0% gauche / 0% droite / 100% centre-neutre + non politique | Option apolitique | + +**Audit et conformité DSA** : +- Rapport hebdomadaire automatique : % gauche/droite/centre diffusé par utilisateur +- Alerte si déséquilibre global plateforme (>55% d'un bord) +- Logs conservés **3 ans** (exigence Digital Services Act EU) +- Dashboard admin : visualisation répartition temps réel + +**Sanctions mauvaise classification** : +- Classification volontairement incorrecte = Strike 1 +- Récidive = Strike 2 (suspension 7j) +- Détection via signalements utilisateurs + audit modération + +**Justification** : +- **Conformité juridique DSA** (obligation neutralité plateforme EU) +- Protection contre accusations de biais éditorial +- Transparence auditable +- Coût : temps modération humaine (incompressible) + +--- + +### Conditions de réintégration + +**Prérequis** : +1. Base utilisateurs stable et revenus suffisants pour financer modération +2. Équipe modération dédiée (2+ modérateurs senior formés) +3. Dashboard admin audit DSA opérationnel +4. Système de logs et archivage 3 ans en place +5. Validation juridique du processus de classification + +**Chronologie estimée** : +- Phase 1 (Post-MVP+3 mois) : Validation demande utilisateurs via sondages +- Phase 2 (Post-MVP+6 mois) : Recrutement modérateurs + développement dashboard +- Phase 3 (Post-MVP+9 mois) : Tests bêta avec utilisateurs volontaires +- Phase 4 (Post-MVP+12 mois) : Déploiement progressif si résultats positifs + +--- + +## 2. Système de pourboires créateurs + +> ⚠️ **Reporté post-MVP** - Fonctionnalité crypto (Lightning Network) prévue ultérieurement. + +### Contexte du report + +**Raisons** : +- **Complexité technique** : Intégration Lightning Network, gestion wallets crypto +- **Réglementation** : Incertitude juridique crypto en EU (MiCA 2025) +- **Focus MVP** : Priorité sur monétisation via abonnements Premium et publicités +- **Adoption utilisateurs** : Nécessite éducation et adoption crypto préalables + +**Version MVP** (actuelle) : +- Monétisation créateurs via : + - Partage revenus publicités (3€ CPM) + - 70% revenus abonnements Premium + +--- + +### Spécifications complètes (future implémentation) + +**Système prévu** : Micro-dons via Lightning Network (Bitcoin Layer 2) + +**Fonctionnement** : +1. Auditeur peut envoyer pourboire pendant ou après écoute +2. Montants suggérés : 0.10€, 0.50€, 1€, 5€ (personnalisable) +3. Transaction instantanée via Lightning Network (frais <0.01€) +4. Créateur reçoit directement dans wallet Lightning +5. Conversion EUR/BTC automatique (optionnelle) + +**Avantages Lightning Network** : +- ✅ Frais quasi-nuls (<1%) vs 1.8% Mangopay +- ✅ Transactions instantanées (<1 seconde) +- ✅ Micropaiements possibles (dès 0.01€) +- ✅ International sans frais supplémentaires +- ✅ Pas d'intermédiaire (peer-to-peer) + +**Contraintes** : +- ❌ Adoption crypto limitée (2-5% population EU en 2026) +- ❌ Volatilité BTC (nécessite conversion EUR immédiate) +- ❌ UX complexe pour utilisateurs non-crypto +- ❌ Réglementation MiCA en évolution + +**Alternatives étudiées** : +- Ko-fi / Buy Me a Coffee : simple mais frais 5% +- PayPal/Stripe : frais 2.9% + 0.30€ (non viable pour micropaiements) +- Mangopay : déjà utilisé, mais frais élevés pour petits montants + +--- + +### Conditions de réintégration + +**Prérequis** : +1. Réglementation MiCA stabilisée et conforme +2. Adoption crypto suffisante dans la base utilisateurs (>10%) +3. Intégration Lightning Network validée techniquement +4. UX simplifiée pour utilisateurs non-crypto (onboarding dédié) +5. Demande créateurs confirmée via sondages + +**Chronologie estimée** : +- Phase 1 (Post-MVP+6 mois) : Étude de marché et demande utilisateurs +- Phase 2 (Post-MVP+12 mois) : Développement intégration Lightning +- Phase 3 (Post-MVP+15 mois) : Tests bêta avec créateurs volontaires +- Phase 4 (Post-MVP+18 mois) : Déploiement public si résultats positifs + +--- + +## 3. Roulette de connexion live aléatoire + +> ⚠️ **Reporté post-MVP** - Feature sociale avancée nécessitant masse critique d'utilisateurs et infrastructure WebRTC renforcée. + +### Contexte du report + +**Raisons** : +- **Masse critique requise** : Nécessite pool suffisant d'utilisateurs simultanés (>500) pour matching rapide (<30s) +- **Infrastructure WebRTC** : Coût serveurs TURN/STUN supplémentaire (~500€/mois pour 1000 utilisateurs actifs) +- **Complexité modération** : Contenu live non enregistré = risques abus, nécessite système de confiance et signalement robuste +- **Focus MVP** : Priorité sur le contenu asynchrone (radios, POIs audio) avant le live P2P +- **UX conducteur** : Commandes vocales avancées nécessaires pour sécurité routière + +**Version MVP** (actuelle) : +- Radio live créateurs uniquement (1 vers N) +- Pas de connexion P2P entre auditeurs +- Chat textuel limité aux POIs et commentaires + +--- + +### Spécifications complètes (future implémentation) + +**Concept** : Permettre aux utilisateurs (conducteurs ou piétons) de se connecter aléatoirement en live audio avec d'autres utilisateurs pour des conversations spontanées de découverte. + +**Fonctionnement** : + +1. **Matching algorithmique** : + - Pool unifié conducteurs + piétons (pas de séparation) + - Matching pondéré : 70% centres d'intérêt communs + 30% aléatoire + - Proximité géographique : préférence régionale (même région/département) + - Temps d'attente cible : <30 secondes + +2. **Format session** : + - Durée initiale : 5 minutes + - Prolongation par consentement mutuel (5 min supplémentaires, illimitée) + - Skip limité : 3 skips/heure pour éviter abus + - Audio uniquement (pas de vidéo, pas de texte pendant session) + +3. **UX différenciée** : + - **Conducteur** : Commandes vocales uniquement ("Roulette", "Suivant", "Terminer") + - **Piéton** : Bouton dédié "Roulette" dans interface principale + - Badges contextuels : "🚗 En voiture" / "🚶 À pied" visibles dès connexion + - Message pré-session : "Vous parlez avec un conducteur. Soyez concis." + +4. **Sécurité et modération** : + - Score de confiance minimum requis : 50/100 (nouveaux utilisateurs exclus) + - Enregistrement tampon 5 min glissantes (sauvegardé uniquement si signalement) + - Signalement immédiat pendant session → déconnexion + revue modération + - Sanctions progressives : avertissement → suspension 1 semaine → ban définitif + +5. **Transition post-session** : + - Option s'abonner mutuellement après bonne conversation + - Statistiques personnelles : nombre sessions, durée totale, rencontres + - Badge "En roulette" visible sur profil (transparence) + +**Avantages** : +- ✅ Sérendipité et découverte (esprit "Chatroulette audio") +- ✅ Complémentarité conducteur/piéton (récit route vs récit urbain) +- ✅ Fidèle concept RoadWave (usagers de la route connectés) +- ✅ Réutilisation infrastructure WebRTC existante (radio live) + +**Contraintes** : +- ❌ Nécessite pool minimum 500 utilisateurs actifs simultanés +- ❌ Modération temps réel complexe (contenu éphémère) +- ❌ Coût infrastructure TURN/STUN significatif +- ❌ Risque dérive (trolls, contenu inapproprié) +- ❌ Commandes vocales avancées requises pour conducteurs + +**Monétisation** : +- Gratuit avec limitation : 3 sessions/jour de 5 min +- Premium : sessions illimitées + matching prioritaire (moins d'attente) + +**Aspects légaux** : +- Âge minimum : 18 ans pour accès roulette +- Charte d'utilisation spécifique (respect, pas de contenu sexuel/violent, pas de sollicitation commerciale) +- Anonymat relatif : pseudo + ville visible, pas de photo + +--- + +### Conditions de réintégration + +**Prérequis** : +1. Base utilisateurs active : >10 000 MAU (Monthly Active Users) dont >500 utilisateurs simultanés en heures pleines +2. Infrastructure WebRTC stable : serveurs TURN/STUN dimensionnés, latence <500ms +3. Système de confiance opérationnel : score utilisateur basé sur comportement, signalements +4. Équipe modération : capacité traiter signalements en <2h +5. Budget infrastructure : 500-1000€/mois selon volume +6. Commandes vocales avancées implémentées pour conducteurs + +**Chronologie estimée** : +- Phase 1 (Post-MVP+3 mois) : Validation demande utilisateurs via sondages, analyse concurrence (Clubhouse, Twitter Spaces) +- Phase 2 (Post-MVP+6 mois) : Développement matchmaking + WebRTC P2P renforcé +- Phase 3 (Post-MVP+9 mois) : Tests bêta avec 100 utilisateurs volontaires +- Phase 4 (Post-MVP+12 mois) : Déploiement progressif si KPI positifs (>70% satisfaction, <5% signalements) + +**KPI de succès** : +- Temps moyen d'attente matching : <30 secondes +- Taux satisfaction post-session : >70% +- Taux signalement : <5% +- Durée moyenne session : >5 minutes (signe d'engagement) +- Taux conversion abonnements mutuels : >10% + +--- + +## 4. Vérification SMS anti-spam + +> ⚠️ **Reporté post-MVP** - Ajout d'une vérification par SMS pour éviter les comptes créés avec des emails temporaires. + +### Contexte du report + +**Raisons** : +- **Coût SMS** : ~0.04€/SMS en France via Brevo (400€/mois pour 10K inscriptions) +- **Complexité UX** : Étape supplémentaire à l'inscription (friction) +- **Focus MVP** : Priorité sur l'expérience utilisateur fluide +- **Modération suffisante** : Système de strikes et signalements couvre les cas d'abus initiaux + +**Version MVP** (actuelle) : +- Inscription par email uniquement (via Zitadel) +- Confirmation email obligatoire +- Détection basique emails jetables (liste noire publique) +- Modération réactive via signalements + +--- + +### Spécifications complètes (future implémentation) + +**Problématique** : Comptes créés avec emails temporaires (Yopmail, 10minutemail, etc.) pour contourner bans ou spammer du contenu. + +**Solution** : Vérification numéro mobile par SMS lors de l'inscription. + +**Fonctionnement** : + +1. **Inscription initiale** : + - Utilisateur crée compte avec email (Zitadel) + - Email de confirmation envoyé (standard) + +2. **Détection email suspect** : + - Regex patterns emails temporaires (`.disposable.com`, `tempmail`, etc.) + - API externe (kickbox.io, mailcheck.ai) pour validation domaine + - Score confiance email < 50% → vérification SMS obligatoire + +3. **Vérification SMS** : + - Demande numéro mobile (+33, +32, etc.) + - Envoi code 6 chiffres via Brevo SMS API + - Expiration : 10 minutes + - Maximum 3 tentatives/jour/numéro (anti-abus) + +4. **Validation** : + - Code correct → compte activé, badge "Vérifié ✓" + - Code incorrect (3 fois) → blocage temporaire 24h + +5. **Sanctions doublon** : + - Détection numéro déjà utilisé pour autre compte + - Limite : 3 comptes/numéro maximum + - Au-delà → signalement automatique modération + +**Affichage** : +- Badge "Vérifié ✓" visible sur profil créateur +- Non obligatoire pour auditeurs simples (seulement créateurs) +- Option "Vérifier mon compte" dans paramètres + +**Règles de diffusion** : +- Contenus créateurs non-vérifiés : portée limitée à 10 km pendant 30 premiers jours +- Après 30 jours sans signalement : levée restriction +- Créateurs vérifiés : aucune restriction + +**Avantages** : +- ✅ Réduction spam et comptes multiples +- ✅ Amélioration confiance plateforme +- ✅ Conformité anti-fraude (KYC léger) +- ✅ Réutilisation infrastructure Brevo (emails + SMS) + +**Contraintes** : +- ❌ Coût SMS : ~400€/mois pour 10K inscriptions/mois +- ❌ Friction UX (étape supplémentaire) +- ❌ Numéros virtuels (Twilio, etc.) contournent vérification +- ❌ Certains utilisateurs réticents (vie privée) + +**Alternatives étudiées** : +- **Captcha reCAPTCHA v3** : efficace mais contournable, pas de coût +- **Email reputation API** : ~0.01€/vérification (kickbox.io) +- **Vérification bancaire** : trop contraignant pour MVP + +--- + +### Conditions de réintégration + +**Prérequis** : +1. Base utilisateurs >10K avec taux spam/abus significatif (>5% comptes signalés) +2. Budget SMS disponible (~400-800€/mois selon volume) +3. Intégration Brevo SMS API opérationnelle +4. UX optimisée (onboarding fluide) +5. Conformité RGPD : consentement stockage numéro mobile + +**Chronologie estimée** : +- Phase 1 (Post-MVP+3 mois) : Analyse taux spam/abus, validation besoin +- Phase 2 (Post-MVP+4 mois) : Développement détection emails temporaires + API Brevo SMS +- Phase 3 (Post-MVP+5 mois) : Tests bêta avec créateurs volontaires +- Phase 4 (Post-MVP+6 mois) : Déploiement progressif selon catégorie utilisateur (créateurs en priorité) + +**KPI de succès** : +- Réduction comptes spam : >50% +- Taux vérification volontaire (créateurs) : >70% +- Friction UX acceptable : taux abandon inscription <10% +- Coût SMS : <2% revenus utilisateurs vérifiés + +--- + +## 5. Synthèse vocale de documents (Text-to-Speech) + +> ⚠️ **Reporté post-MVP** - Fonctionnalité Premium permettant aux utilisateurs d'écouter des documents (PDF, articles web, ebooks) convertis en audio. + +### Contexte du report + +**Raisons** : +- **Complexité technique** : Intégration API TTS (Text-to-Speech), OCR pour PDF scannés, parsing multi-formats +- **Coût infrastructure** : ~0.016€/1000 caractères (Google Cloud TTS) = ~1.60€ par livre moyen (100K caractères) +- **Conformité droits d'auteur** : Risque juridique si conversion de contenus protégés sans licence +- **Focus MVP** : Priorité sur contenu audio natif géolocalisé (podcasts, audio-guides, radios live) +- **Usage limité** : Cas d'usage minoritaire vs contenu audio créé par la communauté + +**Version MVP** (actuelle) : +- Contenu audio uniquement créé par les créateurs +- Pas de conversion automatique document → audio +- Utilisateurs doivent uploader directement fichiers audio + +--- + +### Spécifications complètes (future implémentation) + +**Problématique** : Utilisateurs Premium veulent écouter des documents (articles, PDF, ebooks) pendant leurs trajets sans les lire. + +**Solution** : Conversion Text-to-Speech (TTS) de documents en audio avec voix neurale haute qualité. + +**Fonctionnement** : + +1. **Upload document** : + - Formats supportés : PDF, EPUB, TXT, DOCX, URLs articles web + - Taille max : 50 MB par fichier + - Détection automatique langue (FR, EN, ES, DE, IT) + - OCR automatique si PDF scanné (Tesseract ou Google Vision API) + +2. **Traitement et conversion** : + - Nettoyage texte (enlever headers/footers, numéros page, notes de bas de page) + - Détection structure (chapitres, sections) pour navigation + - Génération audio via TTS (Google Cloud TTS ou AWS Polly) + - Voix neurale professionnelle (qualité proche voix humaine) + - Génération chapitres audio distincts (navigation facilitée) + +3. **Stockage et synchronisation** : + - Audio généré stocké sur OVH Object Storage (comme autres contenus) + - Ajouté automatiquement à la bibliothèque utilisateur + - Synchronisation multi-device (reprendre écoute où elle s'est arrêtée) + - Conservation : 90 jours après génération, puis suppression automatique + +4. **Lecture en voiture** : + - Navigation par chapitres (suivant/précédent) + - Vitesse de lecture ajustable (0.75x, 1x, 1.25x, 1.5x, 2x) + - Signets audio (marquer un passage pour y revenir) + - Reprise automatique au dernier point d'écoute + +5. **Limitations et quotas** : + - **Premium uniquement** (pas disponible en gratuit) + - Quota mensuel : 10 documents ou 500K caractères/mois (soit ~5 livres moyens) + - File d'attente : traitement sous 5-10 minutes selon longueur + - Conservation temporaire (90 jours) pour limiter coûts stockage + +**Formats supportés** : + +| Format | Support | Limitations | +|--------|---------|-------------| +| **PDF texte** | ✅ Natif | Max 50 MB, extraction texte directe | +| **PDF scanné** | ✅ OCR | Max 50 MB, nécessite OCR (plus lent) | +| **EPUB** | ✅ Natif | Ebooks sans DRM uniquement | +| **TXT** | ✅ Natif | UTF-8, max 10 MB | +| **DOCX** | ✅ Natif | Max 20 MB | +| **URLs web** | ✅ Parsing | Articles uniquement (pas de paywall) | + +**Voix TTS disponibles** : + +| Langue | Voix | Fournisseur | +|--------|------|-------------| +| **Français** | Léa (féminine), Thomas (masculine) | Google Cloud TTS WaveNet | +| **Anglais** | Emily, James | Google Cloud TTS WaveNet | +| **Espagnol** | Carmen, Diego | Google Cloud TTS WaveNet | +| **Allemand** | Anna, Max | Google Cloud TTS WaveNet | + +**Avantages** : +- ✅ Différenciation Premium forte (feature exclusive) +- ✅ Fidélisation utilisateurs (consommation contenu personnel) +- ✅ Réutilisation infrastructure audio existante (HLS, NGINX Cache) +- ✅ Cas d'usage trajets longs (livres, articles longs) + +**Contraintes** : +- ❌ Coût TTS : ~1.60€/livre moyen (Google Cloud TTS WaveNet) +- ❌ Coût stockage : ~0.01€/GB/mois (temporaire 90 jours) +- ❌ Risque juridique : conversion contenus protégés (livres, articles premium) +- ❌ Qualité variable selon format source (PDF mal structurés) +- ❌ Pas de géolocalisation (contenu personnel, pas communautaire) + +**Conformité droits d'auteur** : + +| Contenu | Autorisé | Restrictions | +|---------|----------|--------------| +| **Documents personnels** | ✅ Oui | Aucun problème légal | +| **Articles web publics** | ✅ Oui | Fair use personnel uniquement | +| **Ebooks DRM-free** | ✅ Oui | Usage privé uniquement (pas de partage) | +| **Ebooks DRM** | ❌ Non | Violation DRM interdite | +| **Livres sous copyright** | ⚠️ Tolérance | Usage strictement privé, pas de redistribution | +| **Articles paywall** | ❌ Non | Bypass paywall interdit | + +**Disclaimer utilisateur** : +> "La conversion de documents en audio est réservée à un usage privé uniquement. Vous êtes responsable de vous assurer que vous possédez les droits nécessaires pour convertir et écouter ce contenu. RoadWave ne peut être tenu responsable de toute violation de droits d'auteur." + +**Alternatives étudiées** : + +| Solution | Coût | Qualité voix | Contraintes | +|----------|------|--------------|-------------| +| **Google Cloud TTS WaveNet** | 0.016€/1K caractères | ⭐⭐⭐⭐⭐ Excellente | API stable, voix neurales | +| **AWS Polly Neural** | 0.016€/1K caractères | ⭐⭐⭐⭐⭐ Excellente | Similar à Google | +| **Azure Cognitive Services** | 0.014€/1K caractères | ⭐⭐⭐⭐ Très bonne | Moins cher, voix correctes | +| **Elevenlabs** | 0.30€/1K caractères | ⭐⭐⭐⭐⭐ Ultra-réaliste | Trop cher pour MVP | +| **OpenAI TTS** | 0.015€/1K caractères | ⭐⭐⭐⭐ Très bonne | Nouveau (2024), à tester | + +**Recommandation** : Google Cloud TTS WaveNet (équilibre coût/qualité, voix neurales professionnelles). + +--- + +### Conditions de réintégration + +**Prérequis** : +1. Base utilisateurs Premium >1000 abonnés (justifier développement feature) +2. Demande utilisateurs confirmée via sondages (>40% intérêt) +3. Budget TTS + stockage disponible (~500-1000€/mois selon volume) +4. Validation juridique : conformité droits d'auteur, disclaimer clair +5. Infrastructure existante stable (HLS, CDN, backend Go) + +**Chronologie estimée** : +- Phase 1 (Post-MVP+6 mois) : Étude de marché, sondage utilisateurs Premium, validation juridique +- Phase 2 (Post-MVP+9 mois) : Développement MVP TTS (PDF texte uniquement, FR/EN) +- Phase 3 (Post-MVP+10 mois) : Tests bêta avec 100 utilisateurs Premium volontaires +- Phase 4 (Post-MVP+12 mois) : Déploiement progressif si KPI positifs + ajout formats (EPUB, OCR) + +**KPI de succès** : +- Adoption feature : >30% utilisateurs Premium l'utilisent au moins 1 fois/mois +- Satisfaction : >75% note positive (4-5/5) +- Rétention Premium : augmentation >10% grâce à cette feature +- Coût TTS : <5% revenus Premium +- Taux d'erreur conversion : <5% (PDF mal structurés, OCR raté) + +**Budget estimé** : + +| Composant | Coût mensuel (1000 utilisateurs Premium actifs) | +|-----------|--------------------------------------------------| +| **Google Cloud TTS** | ~500€ (10 documents/user/mois, 30K caractères/document) | +| **OCR (PDF scannés)** | ~100€ (30% documents nécessitent OCR) | +| **Stockage** | ~50€ (documents audio temporaires 90 jours) | +| **Bande passante** | Inclus dans infrastructure existante | +| **Total** | **~650€/mois** | + +**Rentabilité** : +- Revenus Premium 1000 users : 4990€/mois (4.99€/mois × 1000) +- Coût TTS : 650€/mois (13% revenus) +- Marge après TTS : 4340€/mois (87%) +- **Rentable si** adoption >30% et rétention +10% (soit +100 abonnés = +499€/mois) + +--- + +## 6. Commandes vocales (CarPlay / Android Auto) + +> ⚠️ **Reporté post-MVP** - Permettre aux conducteurs d'utiliser les actions complémentaires via assistants vocaux. + +### Contexte du report + +**Raisons** : +- **Couverture limitée** : ~30-40% du parc automobile EU en 2026 (CarPlay/Android Auto) +- **Complexité technique** : Intégration Siri Intents (iOS) + Google Actions (Android) +- **Modération vocale** : Signalements vocaux nécessitent enregistrement + transcription audio +- **Focus MVP** : Priorité sur like automatique et mode piéton avec actions manuelles +- **Accessibilité secondaire** : Like automatique couvre déjà engagement conducteurs + +**Version MVP** (actuelle) : +- ❌ Pas de commandes vocales +- ✅ Like automatique basé sur temps d'écoute (en voiture) +- ✅ Actions manuelles disponibles seulement en mode piéton + +--- + +### Spécifications complètes (future implémentation) + +**Objectif** : Permettre conducteurs d'effectuer actions complémentaires via commandes vocales sans regarder écran. + +**Commandes vocales supportées** : + +**iOS (Apple Siri)** : +``` +"Hey Siri, like ce contenu" +→ Ajoute +2% jauge (like explicite) + +"Hey Siri, abonne-moi à ce créateur" +→ Ajoute +5% toutes jauges du créateur + +"Hey Siri, signale ce contenu" +→ Siri demande catégorie vocalement ("Spam", "Haine", etc.) + +"Hey Siri, passe au contenu suivant" +→ Même que bouton physique (déjà supporté) +``` + +**Android (Google Assistant)** : +``` +"OK Google, like ce podcast" +→ Ajoute +2% jauge + +"OK Google, abonne-moi au créateur" +→ Ajoute +5% toutes jauges du créateur + +"OK Google, signale ce contenu" +→ Assistant demande catégorie vocalement + +"OK Google, passe au suivant" +→ Même que bouton physique +``` + +**Implémentation technique** : + +**iOS** : +- Siri Intents (framework iOS 12+) +- Clés Intent à ajouter dans `Info.plist` : + ```xml + INUserConfirmationConfiguration + + + INIntentClassName + RoadWaveAddLikeIntent + + + ``` +- Paramètres vocaux : détection "like", "abonne", "signale" + +**Android** : +- Google Actions on Google Assistant (via assistant voice queries) +- Intégration avec Android App Actions +- Paremeters: Intent extras pour passer contenu actuel +- Fallback : repérer contenu par titre + créateur + +**Limitation importante** : +- ⚠️ **CarPlay/Android Auto requis** : Fonctionalité non disponible sur interface mobile +- ⚠️ **Reconnaissance vocale réseau** : Nécessite connexion data +- ⚠️ **Latence acceptable** : <2 secondes entre commande et confirmation + +**UX - Feedback utilisateur** : +- Siri : "✓ J'ai ajouté ce contenu à vos favoris" +- Google Assistant : "✓ Vous êtes maintenant abonné à [Créateur]" +- Confirmation audio pour signalement : "Signalement envoyé. Catégorie : Spam" + +**Signalements vocaux** : +- Enregistrement automatique de la voix (tampon 30 secondes) +- Transcription audio → texte (via Google Cloud Speech ou similaire) +- Catégorie pré-remplie selon réponse vocale ("Spam" → catégorie Spam) +- Commentaire optionnel supplémentaire (enregistrement audio conservé) + +--- + +### Conditions de réintégration + +**Prérequis** : +1. MVP stabilis en production avec base utilisateurs +2. Données télémétrie : >10K utilisateurs actifs CarPlay/Android Auto +3. Intégration Siri Intents iOS et Google Actions validée +4. Transcription vocale fiable (coût ~0.01€/minute) +5. Système de confiance utilisateur en place (éviter abus signalements) + +**Chronologie estimée** : +- Phase 1 (Post-MVP+2 mois) : Validation demande utilisateurs (CarPlay/Android Auto) +- Phase 2 (Post-MVP+4 mois) : Développement Siri Intents + Google Actions +- Phase 3 (Post-MVP+6 mois) : Tests bêta avec conducteurs volontaires +- Phase 4 (Post-MVP+8 mois) : Déploiement progressif si KPI positifs + +**KPI de succès** : +- Adoption commandes vocales : >30% utilisateurs CarPlay/Android Auto +- Taux erreur reconnaissance vocale : <10% +- Satisfaction utilisateurs : >75% (4-5/5) +- Taux signalements abusifs : <2% (via détection anomalies) + +**Budget estimé** : +| Composant | Coût mensuel (10K utilisateurs actifs) | +|-----------|----------------------------------------| +| **Siri Intents** | Inclus iOS SDK | +| **Google Actions** | Inclus Android SDK | +| **Transcription vocale** | ~300€ (30K minutes/mois) | +| **Modération signalements audio** | ~500€ (équipe part-time) | +| **Total** | **~800€/mois** | + +--- + +## Autres fonctionnalités candidates Post-MVP + +Liste non exhaustive de fonctionnalités évoquées mais non encore spécifiées : + +- **Mode offline avancé** : Téléchargement automatique zones fréquentes +- **Playlists collaboratives** : Co-création de playlists géolocalisées +- **API publique créateurs** : Intégration RSS, podcasts existants +- **Gamification** : Badges, défis géolocalisés, leaderboards +- **Mode nuit** : Interface sombre automatique +- **Statistiques avancées créateurs** : Démographie, retention, heatmaps GPS + +Ces fonctionnalités seront spécifiées et priorisées selon les retours utilisateurs MVP. + +--- + +## Suivi et validation + +**Responsable** : Product Owner +**Révision** : Trimestrielle +**Critères de priorisation** : +1. Demande utilisateurs (votes, sondages) +2. Impact business (revenus, rétention) +3. Faisabilité technique (complexité, ressources) +4. Conformité légale (RGPD, DSA, MiCA) +5. Différenciation concurrentielle diff --git a/docs/regles-metier/README.md b/docs/regles-metier/README.md new file mode 100644 index 0000000..af43bf6 --- /dev/null +++ b/docs/regles-metier/README.md @@ -0,0 +1,296 @@ +# Règles métier RoadWave + +> Documentation complète des règles métier validées pour l'application RoadWave. +> Chaque section détaille les comportements, flux et décisions techniques. + +--- + +## 📋 Table des matières + +--- + +## 🏗️ Fondations & Compte utilisateur + +### [01. Authentification & Inscription](01-authentification-inscription.md) + +**Contenu** : Inscription, connexion, récupération de compte + +- Inscription : email/password uniquement (pas d'OAuth tiers) +- Vérification email : optionnelle auditeurs (limite 5 contenus), obligatoire créateurs (lien expire 7j) +- Connexion : 5 tentatives max, blocage 15 min, refresh token 30j +- Récupération mot de passe : email, lien expire 1h + +--- + +### [02. Conformité RGPD](02-conformite-rgpd.md) + +**Contenu** : Consentements, anonymisation, export, suppression + +- Consentement : Tarteaucitron.js + PostgreSQL versioning +- GPS précis : 24h puis geohash 5 (~5km²) +- Export : JSON + HTML + audio → ZIP, génération asynchrone sous 48h, expire 7j +- Suppression : grace period 30j, contenus créés anonymisés (créateur = "Utilisateur supprimé") +- Analytics : Matomo self-hosted, IP anonymisées, 0 cookie tiers +- DPO : fondateur formé CNIL (non obligatoire <250 employés) + +--- + +## 🎧 Consommation de contenu (Auditeur) + +### [03. Centres d'intérêt et jauges](03-centres-interet-jauges.md) + +**Contenu** : Évolution jauges, valeurs initiales + +- Like automatique : écoute ≥80% → +2%, écoute 30-79% → +1% +- Like explicite (manuel) : +2% (cumulable avec auto) +- Abonnement : +5% +- Skip rapide (<10s) : -0.5% +- Valeur initiale : 50% (neutre) +- Limites : 0-100% stricte, pas de dégradation temporelle + +--- + +### [04. Algorithme de recommandation](04-algorithme-recommandation.md) + +**Contenu** : Scoring, géolocalisation, orientation politique, mode Kids + +- Classification géo : Ancré (70%) / Contextuel (50%) / Neutre (20%) +- Engagement : 20%, Aléatoire : 10% +- Orientation politique : 5 niveaux, équilibre imposé (40/40/20) +- Mode Kids : 4 tranches (3-6 / 6-9 / 9-12 / 13-15 ans), activation auto <13 ans +- Historique : >80% jamais reproposer, <10s ne pas reproposer + +--- + +### [05. Interactions et navigation](05-interactions-navigation.md) + +**Contenu** : Commandes Suivant/Précédent, interactions volant, lecture en boucle + +- Suivant : pré-calcul 5 contenus, recalcul >10km ou 10 min +- Précédent : <10s → contenu avant, ≥10s → replay début +- Commandes volant : Suivant, Précédent, Play/Pause uniquement +- Like automatique : ≥80% écoute → +2 points, 30-79% → +1 point +- Actions manuelles : bouton cœur (arrêt véhicule) ou vocal (CarPlay/Android Auto) +- Passage auto après 2s (1s mode Kids) + +--- + +### [06. Audio-guides multi-séquences](06-audio-guides-multi-sequences.md) + +**Contenu** : Modes déplacement, navigation, déclenchement GPS, publicités + +- **4 modes** : 🚶 Piéton (manuel) / 🚗 Voiture (GPS auto + manuel) / 🚴 Vélo / 🚌 Transport +- **Mode Piéton** : pause auto après chaque séquence, user clique Suivant, navigation libre +- **Mode Voiture** : déclenchement GPS auto (rayon 30m), boutons manuels actifs, warning sécurité >10 km/h +- **Affichage voiture** : distance temps réel + ETA + direction (flèche) + vitesse +- **Rayons** : Voiture 30m, Vélo 50m, Transport 100m (configurable créateur 10-200m) +- **Publicités** : 1/5 séquences tous modes, auto-play, skippable 5s +- **Reprise** : sauvegarde auto (séquence + position exacte), popup si <30j, multi-device (sync cloud) + +--- + +### [07. Contenus géolocalisés voiture](07-contenus-geolocalises-voiture.md) + +**Contenu** : Expérience voiture, UI minimaliste, sécurité + +- Interface voiture : minimaliste, boutons larges (80×80px min), lecture auto-play +- Commandes vocales : OK Google/Siri + CarPlay/Android Auto +- Notifications : in-app uniquement (pas de push), badge discret +- Sécurité : warning si interaction manuelle >10 km/h +- Mode nuit : automatique selon luminosité GPS ou heure (22h-6h) + +--- + +### [08. Mode offline](08-mode-offline.md) + +**Contenu** : Téléchargement, validité, synchronisation + +- Zone géographique : choix manuel (autour de moi / ville / département / région) +- Nombre contenus : gratuit 50 max, Premium illimité +- WiFi par défaut, mobile avec confirmation + estimation volume +- Validité : 30 jours, renouvellement auto si WiFi (contenus >25 jours) +- Sync : likes/abonnements batch auto à reconnexion, queue actions 7j max + +--- + +### [09. Abonnements et notifications](09-abonnements-notifications.md) + +**Contenu** : Impact algorithme, notifications, audio-guides, limites + +- Boost +30% au score final (pas priorité absolue) +- Détection contexte : <5 km/h piéton, >10 km/h voiture +- Voiture : in-app uniquement, Piéton : push actives +- Limite 10 notifications push/jour (5-20), mode silencieux 22h-8h +- Audio-guide piéton : détection <100m lieu, page sélection, navigation manuelle +- Max 200 abonnements, +5% jauges tous tags créateur + +--- + +### [10. Gestion des erreurs](10-gestion-erreurs.md) + +**Contenu** : Aucun contenu, contenu supprimé, perte réseau, GPS désactivé + +- Aucun contenu : élargissement auto 50km → 100km → département → région → national +- Contenu supprimé : laisser terminer, passage auto suivant après 2s +- Perte réseau : buffer adaptatif (WiFi 5-120s, 4G 10-120s, 3G 30-300s), retry 5s max 6× +- GPS désactivé : mode dégradé (contenu national + neutre + téléchargé) + +--- + +## 🎙️ Création de contenu (Créateur) + +### [11. Création et publication de contenu](11-creation-publication-contenu.md) + +**Contenu** : Upload, métadonnées, validation, modification + +- Formats : MP3, AAC (.mp3, .aac, .m4a), max 200 MB, 4h +- Métadonnées obligatoires : titre, type géo, zone, tags (1-3), classification âge +- Validation 3 premiers contenus : 24-48h (modération RoadWave) +- Modification : métadonnées uniquement, pas audio/zone/classification + +--- + +### [12. Radio live](12-radio-live.md) + +**Contenu** : Démarrage, arrêt, comportement auditeur + +- Buffer 15s avant diffusion publique, durée max 8h +- Notification push abonnés dans zone géo uniquement +- Arrêt : compte à rebours 5s (manuel) ou auto si déco ≥60s +- Enregistrement auto MP3 256 kbps → replay sous 5-10 min +- Auditeur : buffer 15s, continuation si sortie zone, AUCUN chat + +--- + +### [13. Détection de contenu protégé](13-detection-contenu-protege.md) + +**Contenu** : Droits d'auteur, détection musique, sanctions, fair use + +- Périmètre MVP : musique uniquement (films/livres exclus) +- Fair use : extraits ≤30s autorisés si usage transformatif (critique/analyse) +- Détection : manuelle lors des 3 premiers contenus (coût 0€) +- Sanctions progressives : Avertissement → Strike 1 (3j) → Strike 2 (7j) → Strike 3 (30j) → Strike 4 (ban) +- Réhabilitation : -1 strike tous les 6 mois sans incident +- Appel : réutilise processus section 14, délai 72h, preuve licence acceptée +- Post-MVP : Chromaprint + MusicBrainz si >50 signalements/mois (50-100€/mois) + +--- + +## 🛡️ Modération & Sécurité + +### [14. Modération - Flows opérationnels](14-moderation-flows.md) + +**Contenu** : Signalement, traitement, sanctions + +- Signalement : 7 catégories (haine, sexuel, illégalité, droits auteur, spam, fake news, autre) +- IA pré-filtre : Whisper large-v3 (transcription) + NLP open source (1-10 min) +- SLA : Critique <2h (24/7), Haute/Moyenne <24h, Basse <72h +- Notification sanction : email + push + in-app (détail complet : catégorie, timestamp, transcription) +- Appel : formulaire in-app, délai 7j max, réponse 72h garanti (standard) + +--- + +### [15. Modération Communautaire - Badges et Récompenses](15-moderation-communautaire.md) + +**Contenu** : Système badges, score fiabilité, récompenses, anti-abus + +- **3 niveaux badges** : 🥉 Bronze (5 validés), 🥈 Argent (20 validés), 🥇 Or (50 validés) +- **Modal découverte** : affichage unique au 1er signalement, message gratifiant +- **Score fiabilité** : calcul auto pour priorisation algorithme (0-100 points) +- **Utilisateurs de confiance** : statut auto Argent/Or, traitement prioritaire <12h +- **Réduction Premium Or** : -50% pendant 3 mois (2.49€ au lieu de 4.99€), Post-MVP +- **Anti-abus** : limite 10 signalements/24h, audit trimestriel, révocation badges +- **ROI** : positif dès 2-3 utilisateurs Or (économie modération > coût réductions) +- **Coût MVP** : 0€ / **Coût Post-MVP** : 0-200€/mois + +--- + +## 💰 Monétisation & Business + +### [16. Publicités](16-publicites.md) + +**Contenu** : Campagnes, fréquence, insertion, facturation + +- Interface self-service, budget min 50€, étalement paramétrable +- Fréquence : 1/5 contenus (gratuits uniquement) +- Durée : 10-60s (recommandé 15-30s), skippable après 5s +- Validation manuelle 24-48h, prépaiement Mangopay +- Facturation : écoute complète 0.05€, skip après 5s : 0.02€, skip immédiat : 0€ + +--- + +### [17. Premium](17-premium.md) + +**Contenu** : Offre, multi-devices, avantages, gestion abonnement + +- Prix : 4.99€/mois OU 49.99€/an (4.16€/mois effectif) +- Pas d'essai gratuit, pas de partage familial (MVP) +- Multi-devices : 1 seul stream actif, détection connexion simultanée +- Avantages : 0 pub, contenus exclusifs 👑, qualité 64 kbps Opus, offline illimité +- Paiement : Mangopay (web) ou IAP iOS/Android 5.99€/mois (+30% commission) + +--- + +### [18. Monétisation créateurs](18-monetisation-createurs.md) + +**Contenu** : Activation, KYC, sources revenus, paiement + +- Conditions : compte ≥3 mois, ≥500 abonnés, ≥10K écoutes, 0 strike, ≥5 contenus/90j +- KYC via Mangopay Connect : SIRET, TVA, RIB pro, pièce ID, Kbis <3 mois +- Revenus pub : 3€ / 1000 écoutes complètes (6% CA pub) +- Revenus Premium : 70% créateur, 30% plateforme (proportionnel temps écoute) +- Paiement : seuil 50€, mensuel (15 du mois suivant), SEPA Mangopay + +--- + +## 🔧 Fonctionnalités transverses + +### [19. Autres comportements](19-autres-comportements.md) + +**Contenu** : Partage, profil créateur, recherche + +- Partage : bouton partout, lien `roadwave.fr/share/c/[id]`, web player + deep link +- Profil créateur : @pseudo, bio (300 car), stats publiques arrondies, badge vérifié ✓ +- Badge vérifié : KYC validé OU célébrité OU >10K abonnés +- Recherche : full-text PostgreSQL (français, stemming), recherche géo (Nominatim OSM) +- Filtres : type, durée, âge, géo, tags, date, premium (combinables) +- Affichage : liste enrichie (20/page, infinite scroll) + vue carte Leaflet + +--- + +## 🗂️ Organisation + +Chaque fichier de règles métier suit la structure : + +1. **Décisions** : choix validés avec justifications +2. **Comportements détaillés** : flux utilisateur, cas limites +3. **Paramètres** : valeurs exactes, seuils, durées +4. **Points d'attention Gherkin** : éléments à tester + +--- + +## 🚀 Utilisation + +Ces documents servent de **référence unique** pour : + +- ✅ Développement backend/frontend +- ✅ Écriture des tests Gherkin (BDD) +- ✅ Validation QA +- ✅ Documentation produit + +**Prochaine étape** : Création des fichiers `.feature` Gherkin dans `features/` basés sur ces règles. + +--- + +## 📊 Statistiques + +- **19 sections** validées +- **~14 500 lignes** de spécifications détaillées +- **Coût infrastructure MVP** : ~50-250€/mois (hors salaires) +- **Coût Post-MVP** : +0-200€/mois (réductions Premium contributeurs Or) +- **Technologies** : 100% open source (sauf Mangopay paiements) + +--- + +**Dernière mise à jour** : Janvier 2026 +**Statut** : ✅ Toutes sections validées + Modération communautaire ajoutée diff --git a/docs/technical.md b/docs/technical.md new file mode 120000 index 0000000..e6e8483 --- /dev/null +++ b/docs/technical.md @@ -0,0 +1 @@ +../TECHNICAL.md \ No newline at end of file diff --git a/features/abonnements/audio-guides-pieton.feature b/features/abonnements/audio-guides-pieton.feature new file mode 100644 index 0000000..5de8b9e --- /dev/null +++ b/features/abonnements/audio-guides-pieton.feature @@ -0,0 +1,263 @@ +# language: fr +Fonctionnalité: Audio-guides multi-séquences pour piétons + En tant qu'auditeur à pied + Je veux profiter d'audio-guides structurés lors de mes visites + Afin de découvrir des lieux de manière autonome et à mon rythme + + Contexte: + Étant donné que l'API RoadWave est disponible + Et que je suis connecté en tant qu'auditeur + Et que je suis en mode piéton (vitesse <5 km/h) + + Scénario: Détection d'audio-guide à proximité + Étant donné que je me trouve à 80 mètres du Musée du Louvre + Et que 3 audio-guides sont disponibles pour ce lieu + Quand le système détecte ma position + Alors je reçois une notification push: + """ + 📍 Audio-guide disponible : Musée du Louvre + Choisissez parmi 3 guides pour Musée du Louvre + Tap pour explorer + """ + + Scénario: Rayon de détection de 100m + Étant donné qu'un audio-guide est centré aux coordonnées GPS du Louvre + Quand je suis à exactement 100m du centre + Alors la notification est déclenchée + Et quand je suis à 101m, aucune notification n'est envoyée + + Scénario: Page de sélection des audio-guides + Étant donné que j'ai tapé sur la notification audio-guide + Quand la page de sélection s'affiche + Alors je vois une liste de guides disponibles: + | titre | créateur | nb_sequences | durée | note | écoutes | + | Visite complète | Créateur A | 12 | 45 min | 4.8 | 1.2K | + | Œuvres majeures | Créateur B | 5 | 20 min | 4.9 | 3.5K | + | Visite famille | Créateur C | 8 | 30 min | 4.7 | 850 | + + Scénario: Sélection d'un audio-guide + Étant donné que je suis sur la page de sélection + Quand je tape sur "Visite complète (45 min)" + Alors l'interface de lecture d'audio-guide s'ouvre + Et la séquence 1 commence automatiquement + Et je vois la liste complète des 12 séquences + + Scénario: Interface de lecture audio-guide + Étant donné que j'ai sélectionné un audio-guide de 12 séquences + Quand l'interface s'affiche + Alors je vois: + | élément | exemple | + | Titre guide | 🎨 Visite complète • Musée du Louvre | + | Piste actuelle | Piste 2/12 | + | Titre séquence | "La Joconde - Histoire et mystères" | + | Barre de progression | 3:24 / 6:50 | + | Liste séquences | ✅ 1. Intro, ▶️ 2. Joconde, ⏸️ 3. Vénus... | + | Boutons navigation | Précédent, Play/Pause, Suivant | + + Scénario: Navigation vers séquence suivante + Étant donné que j'écoute la séquence 2 + Quand je tape sur "Suivant" + Alors la séquence 3 commence immédiatement + Et le titre de la séquence s'affiche: "Vénus de Milo" + Et la barre de progression se réinitialise + + Scénario: Navigation vers séquence précédente + Étant donné que j'écoute la séquence 5 + Quand je tape sur "Précédent" + Alors la séquence 4 recommence depuis le début + Et je peux réécouter cette séquence + + Scénario: Saut direct à une séquence spécifique + Étant donné que j'écoute la séquence 2 + Et que la liste des séquences est affichée + Quand je tape sur "7. Peintures Renaissance" + Alors la séquence 7 démarre immédiatement + Et je passe directement de la séquence 2 à la 7 + + Scénario: Commande vocale "Suivant" + Étant donné que j'écoute la séquence 3 + Quand je dis "Suivant" via la commande vocale + Alors la séquence 4 démarre + Et la commande vocale fonctionne même si l'écran est verrouillé + + Scénario: Commande vocale "Précédent" + Étant donné que j'écoute la séquence 6 + Quand je dis "Précédent" via la commande vocale + Alors la séquence 5 démarre depuis le début + + Scénario: Pause et reprise à la position exacte + Étant donné que j'écoute la séquence 4 à la position 2:30 + Quand je mets en pause + Et que j'attends 5 minutes + Et que je reprends la lecture + Alors la séquence reprend exactement à 2:30 + Et aucune donnée n'est perdue + + Scénario: Guidage vocal automatique entre séquences + Étant donné que la séquence 2 se termine + Quand la transition vers la séquence 3 se produit + Alors j'entends un message vocal: + """ + Vous avez terminé la séquence 2. Dirigez-vous vers la Vénus de Milo pour la séquence 3. + """ + Et la séquence 3 ne démarre pas automatiquement (navigation manuelle) + + Scénario: Avertissement si éloignement du point d'intérêt + Étant donné que je suis dans le guide du Louvre + Et que je devrais être devant la Vénus de Milo (séquence 3) + Quand je m'éloigne de plus de 50m de ce point + Alors j'entends un message vocal: + """ + Vous vous éloignez de la prochaine étape. Consultez le plan. + """ + Et un bouton "Voir le plan" apparaît dans l'interface + + Scénario: Sauvegarde automatique de la progression + Étant donné que j'écoute la séquence 5 à la position 1:45 + Quand je ferme l'application brutalement + Et que je la rouvre 10 minutes plus tard + Alors je vois une popup "Reprendre la visite du Musée du Louvre ?" + Et si je choisis "Reprendre", je retourne à la séquence 5 à 1:45 + + Scénario: Option de recommencer depuis le début + Étant donné que j'ai une progression sauvegardée à la séquence 7 + Quand je rouvre le guide + Alors je vois 2 options: + | option | action | + | Reprendre à la séquence 7 | Reprend à la position exacte | + | Recommencer depuis le début | Retourne à la séquence 1 | + + Scénario: Expiration de la sauvegarde après 30 jours + Étant donné que j'ai une progression sauvegardée depuis 30 jours + Quand j'essaie de reprendre le guide + Alors la sauvegarde est considérée comme expirée + Et je recommence depuis la séquence 1 + Et je vois le message "Votre précédente visite date de plus de 30 jours. Recommençons depuis le début." + + Scénario: Synchronisation multi-device de la progression + Étant donné que j'écoute un guide sur mon iPhone à la séquence 4 + Quand je ferme l'app et ouvre sur mon iPad + Alors je vois la progression synchronisée + Et je peux reprendre à la séquence 4 sur l'iPad + + Scénario: Marquage "Terminé" après toutes les séquences + Étant donné que j'écoute la dernière séquence (12/12) + Quand cette séquence se termine + Alors le guide est marqué "✅ Terminé" dans mon historique + Et je vois un message de félicitation: + """ + Félicitations ! Vous avez terminé la visite complète du Musée du Louvre. + """ + Et le créateur gagne les statistiques d'écoute complète + + Scénario: Création d'audio-guide par un créateur + Étant donné que je suis un créateur + Quand je crée un nouvel audio-guide + Alors je dois: + | étape | détail | + | Uploader plusieurs fichiers | 1 fichier MP3 par séquence | + | Numéroter les séquences | Séquence 1, Séquence 2, etc. | + | Titrer chaque séquence | "Introduction", "La Joconde", etc. | + | Définir point GPS unique | Centre du lieu (ex: Louvre) | + | Définir rayon de détection | Par défaut 100m | + Et la durée totale est calculée automatiquement + + Scénario: Structure JSON de stockage audio-guide + Étant donné qu'un créateur publie un audio-guide du Louvre + Quand les métadonnées sont stockées en base + Alors le format JSON contient: + ```json + { + "guide_id": "abc123", + "title": "Visite complète Musée du Louvre", + "location": {"lat": 48.8606, "lon": 2.3376, "radius": 200}, + "sequences": [ + { + "sequence_number": 1, + "title": "Introduction et architecture", + "audio_url": "https://cdn.../seq1.mp3", + "duration_seconds": 180 + }, + { + "sequence_number": 2, + "title": "La Joconde", + "audio_url": "https://cdn.../seq2.mp3", + "duration_seconds": 410 + } + ], + "total_duration_seconds": 2700, + "creator_id": "creator_xyz" + } + ``` + + Scénario: Limitation du nombre de séquences + Étant donné que je crée un audio-guide + Quand j'essaie d'ajouter plus de 50 séquences + Alors je vois le message "Maximum 50 séquences par audio-guide" + Et je dois structurer mon contenu différemment ou créer plusieurs guides + + Scénario: Quitter le guide et sauvegarder + Étant donné que j'écoute la séquence 6 + Quand je tape sur le bouton "×" (fermer) + Alors je vois une confirmation: + """ + Voulez-vous quitter ce guide ? + Votre progression sera sauvegardée. + """ + Et si je confirme, la progression est enregistrée + Et je retourne à l'écran principal + + Scénario: Statistiques créateur pour audio-guides + Étant donné que je suis créateur d'un audio-guide + Quand je consulte mes statistiques + Alors je vois: + | métrique | exemple valeur | + | Nombre de démarrages | 1250 | + | Nombre de complétions (100%) | 387 (31%) | + | Séquence la plus skippée | Séquence 8 | + | Durée moyenne d'écoute | 28 min (sur 45) | + + Scénario: Audio-guide multilingue (post-MVP) + Étant donné qu'un créateur peut publier plusieurs versions linguistiques + Quand un touriste anglophone visite le Louvre + Alors il voit les guides disponibles en anglais + Et peut choisir parmi les guides traduits + Mais cette fonctionnalité n'est pas disponible en MVP + + Scénario: Publicité entre séquences d'audio-guide + Étant donné que je suis un utilisateur gratuit + Et que j'écoute un audio-guide + Quand je passe de la séquence 5 à la séquence 6 + Alors une publicité peut être insérée (1 pub toutes les 5 séquences) + Et la publicité est skippable après 5 secondes + Et les utilisateurs Premium ne voient pas de publicité + + Scénario: Audio-guide en mode offline + Étant donné que j'ai téléchargé un audio-guide complet + Quand je visite le lieu sans connexion internet + Alors toutes les séquences sont disponibles hors ligne + Et la navigation fonctionne normalement + Et seule la sauvegarde cloud est différée jusqu'à reconnexion + + Scénario: Notation d'un audio-guide après écoute + Étant donné que j'ai terminé un audio-guide + Quand je ferme l'interface + Alors je vois une popup "Notez cette visite" + Et je peux donner une note de 1 à 5 étoiles + Et cette note contribue à la note globale visible par les autres utilisateurs + + Scénario: Filtrage par langue dans la page de sélection + Étant donné que plusieurs audio-guides sont disponibles en différentes langues + Quand j'accède à la page de sélection + Alors je peux filtrer par langue + Et par défaut, les guides dans ma langue système sont affichés en premier + + Scénario: Réutilisation de l'infrastructure existante + Étant donné qu'un audio-guide est techniquement un contenu structuré + Alors il réutilise: + | composant | usage | + | OVH Object Storage | Hébergement fichiers MP3 séquences | + | Streaming HLS | Diffusion audio adaptative | + | Cache Redis | Métadonnées guides + progressions | + | PostgreSQL | Stockage structure JSON guides | + Et aucune infrastructure dédiée n'est nécessaire diff --git a/features/abonnements/impact-algorithme.feature b/features/abonnements/impact-algorithme.feature new file mode 100644 index 0000000..29ad381 --- /dev/null +++ b/features/abonnements/impact-algorithme.feature @@ -0,0 +1,146 @@ +# language: fr +Fonctionnalité: Impact des abonnements sur l'algorithme + En tant qu'auditeur + Je veux que les contenus de mes créateurs suivis soient favorisés + Afin de ne pas rater leurs publications tout en découvrant de nouveaux contenus + + Contexte: + Étant donné que l'API RoadWave est disponible + Et que je suis connecté en tant qu'auditeur + Et que je suis abonné au créateur "JeanDupont" + + Scénario: Boost de +30% appliqué au score final + Étant donné un contenu du créateur "JeanDupont" avec: + | score_geo | 0.5 | + | score_interet | 0.6 | + | score_engage | 0.5 | + Quand le score final est calculé + Alors le score de base est 0.53 + Et le boost abonnement de +30% est appliqué + Et le score final avec boost est 0.69 + + Scénario: Contenu non-suivi peut battre contenu suivi + Étant donné que je suis à Paris + Et que 2 contenus sont disponibles: + | contenu | createur_suivi | score_geo | score_interet | score_engage | score_final_base | score_avec_boost | + | Contenu A | Non | 0.9 | 0.8 | 0.7 | 0.80 | 0.80 | + | Contenu B | Oui | 0.5 | 0.6 | 0.5 | 0.53 | 0.69 | + Quand l'algorithme sélectionne le prochain contenu + Alors le Contenu A est proposé en premier + Car son score (0.80) est supérieur au Contenu B avec boost (0.69) + + Scénario: Contenu suivi remporte grâce au boost + Étant donné que je suis à Paris + Et que 2 contenus sont disponibles: + | contenu | createur_suivi | score_final_base | score_avec_boost | + | Contenu A | Non | 0.70 | 0.70 | + | Contenu B | Oui | 0.60 | 0.78 | + Quand l'algorithme sélectionne le prochain contenu + Alors le Contenu B est proposé en premier + Car le boost fait pencher la balance (0.78 > 0.70) + + Scénario: Contenu suivi avec faible engagement ne domine pas + Étant donné que je suis abonné au créateur "CreateurMoyen" + Et qu'il publie un contenu avec très faible engagement (score 0.30) + Et qu'un contenu viral d'un créateur non-suivi a un score de 0.85 + Quand l'algorithme sélectionne le prochain contenu + Alors le contenu viral est proposé en premier (0.85) + Car même avec boost, le contenu suivi obtient seulement 0.39 (0.30 × 1.3) + + Scénario: Pas de file dédiée aux abonnements + Étant donné que je suis abonné à 50 créateurs + Quand l'algorithme génère ma file d'attente de 5 contenus + Alors les contenus suivis et non-suivis sont mélangés + Et tous entrent en compétition selon leurs scores (avec boost si abonnement) + Et il n'y a pas de section séparée "Contenus de vos abonnements" + + Scénario: Vérification du calcul du boost + Étant donné un contenu d'un créateur suivi + Et que le score final de base est calculé à 0.65 + Quand le boost abonnement est appliqué + Alors le multiplicateur utilisé est exactement 1.3 + Et le score final avec boost est 0.845 (0.65 × 1.3) + Et le résultat est arrondi à 2 décimales: 0.85 + + Scénario: Boost appliqué à tous les contenus du créateur suivi + Étant donné que je suis abonné au créateur "JeanDupont" + Et qu'il a publié 10 contenus différents + Quand l'algorithme évalue chacun de ces contenus + Alors le boost de +30% est appliqué à tous les 10 contenus + Et chaque contenu bénéficie du même multiplicateur 1.3 + + Scénario: Plusieurs créateurs suivis en compétition + Étant donné que je suis abonné à "Créateur A" et "Créateur B" + Et que les 2 ont des contenus disponibles dans ma zone: + | createur | score_base | score_avec_boost | + | Créateur A | 0.70 | 0.91 | + | Créateur B | 0.65 | 0.85 | + Quand l'algorithme sélectionne le prochain contenu + Alors le contenu du Créateur A est proposé en premier (0.91 > 0.85) + Et les 2 bénéficient du boost, mais le meilleur score gagne + + Scénario: Contenu national d'un créateur suivi + Étant donné que je suis abonné à "MediaNational" + Et qu'il publie un contenu de type "National" (score_geo 0.2) + Quand le score est calculé avec: + | score_geo | score_interet | score_engage | + | 0.2 | 0.7 | 0.6 | + Alors le score de base est environ 0.50 + Et avec le boost abonnement, le score devient 0.65 + Et le contenu peut être proposé malgré son score géo faible + + Scénario: Transparence du boost dans les paramètres + Quand j'accède aux paramètres de l'algorithme de recommandation + Alors je vois l'information: "Les contenus de vos créateurs suivis bénéficient d'un boost de +30%" + Et je comprends que ce n'est pas une priorité absolue + Et que la découverte de nouveaux contenus reste possible + + Scénario: Boost désactivé si désabonnement + Étant donné que je suis abonné au créateur "JeanDupont" + Et qu'un de ses contenus bénéficiait du boost +30% + Quand je me désabonne de "JeanDupont" + Alors ses contenus n'ont plus le boost + Et leur score revient au score de base sans multiplicateur + + Scénario: Contenu d'un créateur nouvellement suivi + Étant donné que je viens de m'abonner à "NouveauCreateur" + Et qu'il a publié un contenu il y a 2 jours + Quand l'algorithme recalcule les scores + Alors le boost de +30% est immédiatement appliqué à ce contenu + Et il peut apparaître dans ma prochaine file d'attente + + Scénario: Impact sur le taux de contenu suivi dans le feed + Étant donné que je suis abonné à 30 créateurs + Et que j'écoute 100 contenus sur une semaine + Quand j'analyse la répartition + Alors environ 40-50% des contenus proviennent de créateurs suivis + Et 50-60% proviennent de créateurs non-suivis (découverte) + Car le boost favorise sans dominer + + Scénario: Contenu suivi hors zone géographique + Étant donné que je suis à Paris + Et que je suis abonné à un créateur de Marseille + Et qu'il publie un contenu ancré à Marseille (hors de portée) + Quand l'algorithme évalue ce contenu + Alors le score géo est quasi nul (0.05) + Et même avec boost +30%, le score reste très faible + Et le contenu n'est probablement pas proposé + + Scénario: Performance de calcul du boost + Étant donné que je suis abonné à 100 créateurs + Et que l'algorithme évalue 1000 contenus potentiels + Quand le calcul des scores avec boost est effectué + Alors le temps de calcul reste inférieur à 50ms + Car le boost est une simple multiplication (opération O(1)) + Et la requête SQL utilise un JOIN sur la table abonnements + + Scénario: Boost combiné avec d'autres facteurs + Étant donné un contenu d'un créateur suivi + Et que le contenu bénéficie aussi de: + | facteur | impact | + | Score d'engagement élevé | +20% | + | Contenu récent (<24h) | +10% | + | Boost abonnement | +30% | + Quand le score final est calculé + Alors le boost abonnement s'applique au score final (après tous les autres calculs) + Et les boosts ne s'additionnent pas, le boost abonnement est un multiplicateur final diff --git a/features/abonnements/limites-desabonnement.feature b/features/abonnements/limites-desabonnement.feature new file mode 100644 index 0000000..d358367 --- /dev/null +++ b/features/abonnements/limites-desabonnement.feature @@ -0,0 +1,244 @@ +# language: fr +Fonctionnalité: Limites d'abonnements et désabonnement + En tant qu'auditeur + Je veux gérer mes abonnements de manière équilibrée + Afin de suivre mes créateurs préférés sans être submergé + + Contexte: + Étant donné que l'API RoadWave est disponible + Et que je suis connecté en tant qu'auditeur + + Scénario: Limite maximale de 200 abonnements + Étant donné que je suis abonné à 199 créateurs + Quand j'essaie de m'abonner à un 200ème créateur + Alors l'abonnement réussit + Et je suis maintenant abonné à 200 créateurs + + Scénario: Impossible de dépasser 200 abonnements + Étant donné que je suis déjà abonné à 200 créateurs + Quand j'essaie de m'abonner à un nouveau créateur + Alors l'action échoue + Et je vois le message: + """ + Vous suivez déjà 200 créateurs. Désabonnez-vous d'un créateur pour en suivre un nouveau. + """ + + Scénario: Suggestion de désabonnement de créateurs inactifs + Étant donné que je suis abonné à 200 créateurs + Et que j'essaie de m'abonner à un nouveau créateur + Quand je vois le message de limite atteinte + Alors je vois aussi une suggestion: + """ + Vous n'avez pas écouté [Créateur X] depuis 6 mois, le désabonner ? + """ + Et un bouton "Désabonner" est proposé pour ce créateur + + Scénario: Liste triable des abonnements + Étant donné que je suis abonné à 150 créateurs + Quand j'accède à ma liste d'abonnements + Alors je peux trier par: + | critère | ordre | + | Date d'abonnement | Plus récent / Plus ancien | + | Nombre de contenus écoutés| Plus écoutés / Moins écoutés | + | Dernière activité créateur| Plus récent / Plus ancien | + | Ordre alphabétique | A-Z / Z-A | + + Scénario: Abonnement initial augmente les jauges de +5% + Étant donné que mes jauges d'intérêt sont: + | catégorie | valeur initiale | + | Automobile | 60% | + | Voyage | 55% | + Et qu'un créateur tague ses contenus "Automobile" et "Voyage" + Quand je m'abonne à ce créateur + Alors mes jauges évoluent: + | catégorie | nouvelle valeur | + | Automobile | 65% (+5%) | + | Voyage | 60% (+5%) | + + Scénario: Abonnement avec créateur ayant 3 tags + Étant donné qu'un créateur tague ses contenus: + | tags | + | Automobile, Voyage, Technologie | + Et que mes jauges sont toutes à 50% + Quand je m'abonne à ce créateur + Alors les 3 jauges augmentent de +5%: + | catégorie | nouvelle valeur | + | Automobile | 55% | + | Voyage | 55% | + | Technologie | 55% | + + Scénario: Désabonnement diminue les jauges de -5% + Étant donné que je suis abonné à un créateur avec tags "Politique" et "Économie" + Et que mes jauges sont: + | catégorie | valeur actuelle | + | Politique | 70% | + | Économie | 65% | + Quand je me désabonne de ce créateur + Alors mes jauges évoluent: + | catégorie | nouvelle valeur | + | Politique | 65% (-5%) | + | Économie | 60% (-5%) | + + Scénario: Désabonnement sans confirmation + Étant donné que je consulte le profil d'un créateur suivi + Quand je clique sur "Se désabonner" + Alors le désabonnement est immédiat + Et aucune popup de confirmation n'apparaît + Car l'action est réversible (je peux me réabonner) + + Scénario: Réabonnement possible immédiatement + Étant donné que je viens de me désabonner d'un créateur + Quand je consulte à nouveau son profil + Alors le bouton "S'abonner" est affiché + Et je peux me réabonner immédiatement + Et mes jauges augmentent à nouveau de +5% + + Scénario: Effet symétrique abonnement/désabonnement + Étant donné qu'un créateur a les tags "Musique" et "Culture" + Et que ma jauge Musique est à 50% + Quand je m'abonne puis me désabonne immédiatement + Alors ma jauge revient exactement à 50% + Et il n'y a pas de perte ou gain net + + Scénario: Abonnement ne dépasse pas 100% de jauge + Étant donné que ma jauge Automobile est à 97% + Et qu'un créateur tague ses contenus "Automobile" + Quand je m'abonne à ce créateur + Alors ma jauge Automobile passe à 100% (limite max) + Et l'augmentation effective est de +3% seulement + + Scénario: Désabonnement ne descend pas sous 0% + Étant donné que ma jauge Politique est à 3% + Et que je suis abonné à un créateur avec tag "Politique" + Quand je me désabonne de ce créateur + Alors ma jauge Politique passe à 0% (limite min) + Et la diminution effective est de -3% seulement + + Scénario: Créateur ne voit pas qui est abonné (privacy) + Étant donné que je suis abonné au créateur "JeanDupont" + Quand "JeanDupont" consulte ses statistiques + Alors il voit le nombre total d'abonnés (ex: "1,247 abonnés") + Mais il ne voit pas la liste des utilisateurs abonnés + Et mon identité reste privée + + Scénario: Créateur voit uniquement le nombre total d'abonnés + Étant donné que je suis créateur + Et que j'ai 523 abonnés + Quand je consulte mes statistiques + Alors je vois "523 abonnés" + Mais je ne peux pas: + | action interdite | + | Voir la liste des abonnés | + | Contacter mes abonnés individuellement| + | Voir leurs profils | + + Scénario: Pas d'abonnement mutuel visible + Étant donné que je suis abonné au créateur "Alice" + Et qu'"Alice" est abonnée à mon compte créateur + Quand je consulte le profil d'"Alice" + Alors je ne vois pas d'indication qu'elle est abonnée à moi + Et il n'y a pas de badge "Abonné mutuellement" + + Scénario: Performance avec 200 abonnements + Étant donné que je suis abonné à 200 créateurs + Quand l'algorithme calcule ma recommandation + Alors la requête SQL utilise un JOIN sur la table abonnements + Et la table est indexée sur user_id et creator_id + Et le temps de calcul reste inférieur à 50ms + + Scénario: Impact sur la recommandation avec beaucoup d'abonnements + Étant donné que je suis abonné à 150 créateurs très actifs + Et qu'ils publient collectivement 100 contenus par jour + Quand l'algorithme génère ma file de 5 contenus + Alors environ 60-70% des contenus proviennent de créateurs suivis (grâce au boost +30%) + Mais 30-40% proviennent de nouveaux créateurs (découverte) + + Scénario: Notification de désabonnement au créateur (non implémenté) + Étant donné que je me désabonne d'un créateur + Alors le créateur ne reçoit aucune notification + Et il ne peut pas savoir qui s'est désabonné + Car cela préserve la privacy et évite le harcèlement + + Scénario: Statistiques d'abonnements pour l'utilisateur + Étant donné que je suis abonné à 87 créateurs + Quand j'accède à mes statistiques d'abonnements + Alors je vois: + | métrique | exemple valeur | + | Nombre total d'abonnements | 87 / 200 | + | Créateurs les plus écoutés | Top 10 avec % écoute | + | Créateurs non écoutés depuis 6 mois | 12 créateurs | + | Nouveaux contenus non écoutés | 23 contenus | + + Scénario: Recherche dans la liste d'abonnements + Étant donné que je suis abonné à 120 créateurs + Quand j'accède à ma liste d'abonnements + Alors je peux chercher par nom de créateur + Et les résultats sont filtrés en temps réel + Et je trouve rapidement un créateur spécifique + + Scénario: Export de la liste d'abonnements (RGPD) + Étant donné que je demande l'export de mes données + Quand l'export est généré + Alors la liste de mes abonnements est incluse: + ```json + { + "subscriptions": [ + { + "creator_name": "JeanDupont", + "creator_id": "abc123", + "subscribed_at": "2025-06-15T10:30:00Z" + }, + ... + ] + } + ``` + + Scénario: Suppression compte utilisateur et impact sur abonnements + Étant donné que je suis abonné à 50 créateurs + Quand je supprime définitivement mon compte + Alors tous mes abonnements sont supprimés + Et le compteur d'abonnés de chaque créateur est décrémenté de -1 + Et les jauges n'existent plus (données supprimées) + + Scénario: Suppression compte créateur et impact sur abonnés + Étant donné que je suis abonné au créateur "Bob" + Quand "Bob" supprime son compte créateur + Alors je suis automatiquement désabonné + Et mes jauges diminuent de -5% pour les tags de "Bob" + Et je ne vois plus "Bob" dans ma liste d'abonnements + + Scénario: Limite 200 justifiée par usage réaliste + Étant donné que la moyenne d'abonnements sur YouTube est de ~50-100 chaînes + Et que Twitter limite à 5000 follows (mais moyenne ~150) + Quand RoadWave fixe la limite à 200 + Alors cela couvre largement 99% des utilisateurs + Et évite les abus (comptes spam suivant tout le monde) + + Scénario: Table PostgreSQL optimisée pour abonnements + Étant donné la structure de table subscriptions: + ```sql + CREATE TABLE subscriptions ( + user_id UUID, + creator_id UUID, + subscribed_at TIMESTAMP, + PRIMARY KEY (user_id, creator_id) + ); + CREATE INDEX idx_user_subscriptions ON subscriptions(user_id); + CREATE INDEX idx_creator_subscribers ON subscriptions(creator_id); + ``` + Alors les requêtes d'abonnements sont O(1) avec index + Et le count d'abonnés par créateur est rapide + Et la vérification "est abonné ?" est instantanée + + Scénario: Détection d'abonnements abusifs + Étant donné qu'un utilisateur s'abonne à 200 créateurs en moins de 5 minutes + Quand le système détecte cette activité suspecte + Alors un rate limiting est appliqué (max 10 abonnements/minute) + Et l'utilisateur voit "Trop d'actions rapides. Veuillez réessayer dans 1 minute" + Et cela prévient les bots de spam + + Scénario: Badge créateur vérifié visible dans abonnements + Étant donné que je suis abonné à 3 créateurs dont 1 vérifié + Quand je consulte ma liste d'abonnements + Alors le créateur vérifié a un badge ✓ bleu + Et les créateurs non vérifiés n'ont pas de badge diff --git a/features/abonnements/notifications-contextuelles.feature b/features/abonnements/notifications-contextuelles.feature new file mode 100644 index 0000000..28ca42b --- /dev/null +++ b/features/abonnements/notifications-contextuelles.feature @@ -0,0 +1,239 @@ +# language: fr +Fonctionnalité: Notifications contextuelles selon le mode de déplacement + En tant qu'auditeur + Je veux recevoir des notifications adaptées à mon contexte + Afin d'être informé sans être distrait en conduisant + + Contexte: + Étant donné que l'API RoadWave est disponible + Et que je suis connecté en tant qu'auditeur + Et que j'ai activé les notifications + + Scénario: Détection automatique du contexte en voiture + Étant donné que ma vitesse GPS est de 50 km/h + Quand le système détecte mon contexte + Alors je suis identifié comme "En voiture" + Et les notifications push sont désactivées + Et seules les notifications in-app sont actives + + Scénario: Détection automatique du contexte à pied + Étant donné que ma vitesse GPS est de 3 km/h + Quand le système détecte mon contexte + Alors je suis identifié comme "À pied" + Et les notifications push sont activées + Et l'interface tactile et vocale sont disponibles + + Scénario: Zone de transition 5-10 km/h + Étant donné que ma vitesse GPS varie entre 5 et 10 km/h + Quand le système détecte mon contexte + Alors un algorithme de lissage est appliqué sur 30 secondes + Et le mode est déterminé selon la vitesse moyenne + Et les changements de mode ne sont pas trop fréquents + + Scénario: Nouveau contenu créateur suivi - Mode voiture + Étant donné que je suis en voiture (vitesse >10 km/h) + Et que je suis abonné au créateur "JeanDupont" + Quand "JeanDupont" publie un nouveau contenu dans ma zone + Alors je ne reçois pas de notification push + Mais je vois un badge compteur in-app + Et le contenu apparaît dans ma file avec boost +30% + + Scénario: Nouveau contenu créateur suivi - Mode piéton + Étant donné que je suis à pied (vitesse <5 km/h) + Et que je suis abonné au créateur "JeanDupont" + Et que je suis situé en Île-de-France + Quand "JeanDupont" publie un contenu géolocalisé en Île-de-France + Alors je reçois une notification push: + """ + 🎧 JeanDupont a publié : "Titre du contenu" + Tap pour écouter + """ + + Scénario: Live créateur suivi - Mode voiture + Étant donné que je suis en voiture + Et que je suis abonné au créateur "RadioLive" + Quand "RadioLive" démarre un live dans ma zone + Alors je ne reçois pas de notification push + Mais je vois un badge compteur in-app + Et le live peut apparaître dans ma recommandation automatiquement + + Scénario: Live créateur suivi - Mode piéton + Étant donné que je suis à pied + Et que je suis abonné au créateur "RadioLive" + Et que je suis situé dans la zone du live + Quand "RadioLive" démarre un live + Alors je reçois une notification push: + """ + 🔴 RadioLive est en direct : "Titre du live" + Tap pour rejoindre + """ + + Scénario: Audio-guide disponible à proximité - Mode piéton + Étant donné que je suis à pied + Quand je passe à moins de 100m d'un lieu avec audio-guides + Alors je reçois une notification push: + """ + 📍 Audio-guide disponible : Musée du Louvre + Choisissez parmi 3 guides pour Musée du Louvre + Tap pour explorer + """ + + Scénario: Audio-guide disponible à proximité - Mode voiture + Étant donné que je suis en voiture + Quand je passe à moins de 100m d'un lieu avec audio-guides + Alors je reçois une notification audio (bip) + Et une annonce vocale: "Audio-guide disponible" + Mais pas de notification push (sécurité) + + Scénario: Filtrage géographique des notifications + Étant donné que je suis abonné au créateur "CreateurMarseille" + Et que je suis situé à Paris + Quand "CreateurMarseille" publie un contenu ancré à Marseille + Alors je ne reçois pas de notification + Car le contenu est hors de ma zone géographique + Et cela évite la frustration de contenus non écoutables + + Scénario: Contenu national notifie tous les abonnés + Étant donné que je suis abonné au créateur "MediaNational" + Et que je suis situé n'importe où en France + Quand "MediaNational" publie un contenu de type "National" + Alors je reçois une notification (si mode piéton) + Car les contenus nationaux ne sont pas filtrés géographiquement + + Scénario: Limite de 10 notifications push par jour + Étant donné que je suis abonné à 50 créateurs actifs + Et que j'ai déjà reçu 10 notifications push aujourd'hui + Quand un 11ème contenu est publié + Alors je ne reçois pas de notification push individuelle + Mais une notification groupée: "🎧 3 nouveaux contenus de créateurs suivis" + + Scénario: Paramétrage de la limite quotidienne + Étant donné que la limite par défaut est de 10 notifications/jour + Quand j'accède aux paramètres de notifications + Alors je peux modifier la limite entre 5 et 20 + Et si je choisis 15, je recevrai jusqu'à 15 notifications/jour + + Scénario: Mode silencieux nocturne par défaut + Étant donné que le mode silencieux est activé de 22h à 8h par défaut + Et qu'il est 23h30 + Quand un créateur suivi publie un contenu + Alors je ne reçois pas de notification push + Mais les notifications sont empilées + Et je les vois le lendemain matin à 8h01 + + Scénario: Exception du mode silencieux pour les lives + Étant donné que le mode silencieux est activé (22h-8h) + Et qu'il est 23h00 + Et que j'ai activé "Notifications importantes uniquement" (lives uniquement) + Quand un créateur suivi démarre un live + Alors je reçois quand même la notification push du live + Car les lives sont des événements temps réel prioritaires + + Scénario: Désactivation complète des notifications + Étant donné que j'accède aux paramètres de notifications + Quand je désactive toutes les notifications + Alors je ne reçois plus aucune notification push + Et les badges in-app sont également désactivés + Et seule la recommandation algorithmique reste active + + Scénario: Notification "Nouveaux contenus" activée par défaut + Étant donné que je crée un nouveau compte + Et que je m'abonne à mon premier créateur + Quand je consulte les préférences de notifications + Alors "Nouveaux contenus" est activé par défaut + Et "Lives" est activé par défaut + Et "Audio-guides proximité" est activé par défaut + + Scénario: Désactivation sélective par type de notification + Étant donné que j'ai activé toutes les notifications + Quand je désactive uniquement "Nouveaux contenus" + Alors je ne reçois plus de notifications pour nouveaux contenus + Mais je reçois toujours les notifications de lives + Et les notifications d'audio-guides restent actives + + Scénario: Notification groupée après limite dépassée + Étant donné que j'ai reçu 10 notifications push aujourd'hui + Et que 5 nouveaux contenus sont publiés dans l'heure suivante + Quand la 11ème notification devrait être envoyée + Alors les 5 contenus sont regroupés en une seule notification: + """ + 🎧 5 nouveaux contenus de créateurs suivis + Tap pour voir la liste + """ + + Scénario: Détail de la notification groupée + Étant donné que j'ai reçu une notification groupée "3 nouveaux contenus" + Quand je tape sur la notification + Alors l'app s'ouvre sur une liste des 3 contenus: + | créateur | titre | + | JeanDupont | "Actualité du jour" | + | MarieDurand | "Podcast économie" | + | PaulMartin | "Anecdote historique" | + Et je peux choisir lequel écouter en premier + + Scénario: Personnalisation des plages horaires du mode silencieux + Étant donné que le mode silencieux est 22h-8h par défaut + Quand j'accède aux paramètres + Alors je peux modifier les heures: par exemple 23h-7h + Et le mode silencieux s'applique dans la nouvelle plage horaire + + Scénario: Format notification nouveau contenu complet + Étant donné que je suis à pied + Et qu'un créateur suivi publie un contenu + Quand je reçois la notification push + Alors elle contient: + | élément | exemple | + | Emoji | 🎧 | + | Créateur | JeanDupont | + | Action | a publié | + | Titre | "Les secrets du Louvre" | + | CTA | Tap pour écouter | + + Scénario: Format notification live complet + Étant donné que je suis à pied + Et qu'un créateur suivi démarre un live + Quand je reçois la notification push + Alors elle contient: + | élément | exemple | + | Emoji | 🔴 | + | Créateur | RadioLive | + | Action | est en direct | + | Titre | "Débat politique ce soir" | + | CTA | Tap pour rejoindre | + + Scénario: Notification disparaît si contenu supprimé + Étant donné que j'ai reçu une notification pour un contenu + Et que je n'ai pas encore tapé dessus + Quand le créateur supprime le contenu + Alors la notification est automatiquement retirée de mon centre de notifications + Et si je tape dessus par erreur, je vois "Contenu non disponible" + + Scénario: Badge compteur in-app en mode voiture + Étant donné que je suis en voiture + Et que 5 créateurs suivis publient des contenus + Quand j'ouvre l'application + Alors je vois un badge "5" sur l'onglet "Nouveautés" + Et en consultant l'onglet, je vois les 5 nouveaux contenus + Et le badge disparaît après consultation + + Scénario: Coût des notifications push Firebase + Étant donné que je reçois 10 notifications push par jour + Et que je suis actif 365 jours par an + Quand le système calcule le coût + Alors 3650 notifications/an sont envoyées + Et Firebase Cloud Messaging est gratuit jusqu'à plusieurs millions de notifications + Et le coût reste 0€ pour le volume MVP/Growth + + Scénario: Deep link depuis notification push + Étant donné que je reçois une notification push pour un contenu + Quand je tape sur la notification + Alors l'app s'ouvre directement sur le contenu + Et la lecture démarre automatiquement (si j'étais à pied) + Ou le contenu est ajouté en première position dans la file (si je suis en voiture) + + Scénario: Notification refusée si permissions désactivées au niveau OS + Étant donné que j'ai désactivé les notifications dans les paramètres iOS/Android + Quand un créateur suivi publie un contenu + Alors aucune notification push n'est envoyée + Et l'app propose de réactiver les permissions dans les paramètres + Mais les badges in-app continuent de fonctionner diff --git a/features/audio-guides/creation-audio-guide.feature b/features/audio-guides/creation-audio-guide.feature new file mode 100644 index 0000000..3764ba6 --- /dev/null +++ b/features/audio-guides/creation-audio-guide.feature @@ -0,0 +1,333 @@ +# language: fr + +Fonctionnalité: Création d'audio-guide multi-séquences + En tant que créateur de contenu + Je veux créer des audio-guides avec plusieurs séquences géolocalisées + Afin d'offrir des expériences guidées adaptées aux différents modes de déplacement + + Contexte: + Étant donné que l'application RoadWave est démarrée + Et que le créateur "guide@example.com" est connecté + Et que son compte est vérifié + + # 16.1 - Types d'audio-guides et modes de déplacement + + Plan du Scénario: Détection automatique du mode selon la vitesse + Étant donné que l'utilisateur se déplace à km/h + Quand la vitesse est calculée sur 30 secondes + Alors le mode est suggéré automatiquement + + Exemples: + | vitesse | mode | + | 3 | Piéton | + | 15 | Vélo | + | 35 | Voiture | + | 50 | Voiture | + + Scénario: Suggestion de mode au démarrage avec confirmation + Étant donné qu'un audio-guide "Safari du Paugre" est disponible + Et que l'utilisateur se déplace à 35 km/h + Quand l'audio-guide démarre + Alors une popup s'affiche: + """ + Détection : 🚗 Voiture. Est-ce correct ? + [Oui] [Changer] + """ + + Scénario: Changement manuel du mode détecté + Étant donné que le mode "Voiture" est suggéré automatiquement + Quand l'utilisateur clique sur "Changer" + Alors les 4 modes sont proposés: + | mode | emoji | + | Piéton | 🚶 | + | Voiture | 🚗 | + | Vélo | 🚴 | + | Transport | 🚌 | + + Plan du Scénario: Caractéristiques par mode de déplacement + Étant donné un audio-guide configuré en mode + Alors les paramètres suivants sont appliqués: + | paramètre | valeur | + | Vitesse détection | | + | Déclenchement | | + + Exemples: + | mode | vitesse_detection | declenchement | + | Piéton | <5 km/h | Manuel (bouton Suivant) | + | Voiture | >10 km/h | Auto GPS + Manuel | + | Vélo | 5-25 km/h | Auto GPS + Manuel | + | Transport | Variable | Auto GPS + Manuel | + + # 16.1.2 - Création d'un audio-guide (côté créateur) + + Scénario: Accès au formulaire de création d'audio-guide + Étant donné que le créateur est sur son dashboard + Quand il clique sur "Créer un audio-guide" + Alors le formulaire de création s'affiche + Et le wizard guidé en 4 étapes est visible: + | étape | description | + | 1 | Infos générales | + | 2 | Ajout séquences | + | 3 | Preview carte | + | 4 | Validation modération | + + Scénario: Étape 1 - Informations générales obligatoires + Étant donné que le créateur est sur l'étape 1 du wizard + Quand il complète le formulaire + Alors les champs suivants sont obligatoires: + | champ | contrainte | + | Titre | 5-100 caractères | + | Description | 10-500 caractères | + | Mode de déplacement | Choix parmi 4 | + | Tags | 1-3 tags | + | Classification âge | Tout public/13+/16+/18+ | + + Scénario: Sélection du mode de déplacement + Étant donné que le créateur crée un audio-guide + Quand il sélectionne le mode "🚗 Voiture (GPS auto + manuel)" + Alors le champ "Vitesse recommandée" s'affiche + Et la plage suggérée est "30-50 km/h" + + Scénario: Validation du titre + Étant donné que le créateur entre un titre + Quand le titre contient moins de 5 caractères + Alors un message d'erreur "Minimum 5 caractères" s'affiche + Et le bouton "Suivant" est désactivé + + Scénario: Validation de la description + Étant donné que le créateur entre une description + Quand la description contient 520 caractères + Alors un message d'erreur "Maximum 500 caractères" s'affiche + Et les 20 caractères en trop sont surlignés en rouge + + Scénario: Étape 2 - Ajout de la première séquence + Étant donné que le créateur est sur l'étape 2 "Ajout séquences" + Quand il clique sur "Ajouter séquence" + Alors le formulaire de séquence s'affiche avec: + | champ | requis | note | + | Titre séquence | ✅ | 5-80 caractères | + | Audio | ✅ | Upload MP3/AAC, max 200 MB | + | Point GPS | ✅* | *Sauf mode piéton | + | Rayon déclenchement | ✅* | *Sauf mode piéton, 10-200m | + + Scénario: Ajout du point GPS pour une séquence + Étant donné que le créateur ajoute une séquence en mode "Voiture" + Quand il clique sur "📍 Ajouter point GPS" + Alors une carte s'affiche + Et il peut: + | action | + | Cliquer sur la carte | + | Entrer coordonnées manuelles | + | Utiliser sa position actuelle | + + Scénario: Configuration du rayon de déclenchement avec preview + Étant donné qu'un point GPS est défini à (43.1234, 2.5678) + Quand le créateur ajuste le curseur de rayon + Alors le rayon varie de 10m à 200m + Et un cercle visuel est affiché sur la carte + Et la valeur actuelle s'affiche "30m" + + Plan du Scénario: Rayon par défaut selon le mode + Étant donné un audio-guide en mode + Quand le créateur ajoute un point GPS + Alors le rayon par défaut est + + Exemples: + | mode | rayon_defaut | + | Voiture | 30m | + | Vélo | 50m | + | Transport | 100m | + + Scénario: Suggestion intelligente du rayon + Étant donné un audio-guide en mode "Voiture" avec vitesse recommandée 30 km/h + Quand le créateur ajoute un point GPS + Alors une suggestion s'affiche: "Recommandé : 30m pour voiture à 30 km/h" + + Scénario: Upload audio pour une séquence + Étant donné que le créateur crée une séquence "Introduction" + Quand il upload un fichier audio de 5 MB + Alors le fichier est vérifié: + | vérification | règle | + | Format | MP3, AAC, M4A | + | Taille max | 200 MB | + | Durée max | 15 minutes | + + Scénario: Ordre des séquences modifiable + Étant donné un audio-guide avec 5 séquences: + | ordre | titre | + | 1 | Introduction | + | 2 | Les lions | + | 3 | Les girafes | + | 4 | Les éléphants | + | 5 | Conclusion | + Quand le créateur glisse "Les éléphants" en position 2 + Alors l'ordre devient: + | ordre | titre | + | 1 | Introduction | + | 2 | Les éléphants | + | 3 | Les lions | + | 4 | Les girafes | + | 5 | Conclusion | + + Scénario: Nombre minimum de séquences requis + Étant donné un audio-guide avec seulement 1 séquence + Quand le créateur tente de passer à l'étape suivante + Alors un message d'erreur s'affiche: "Minimum 2 séquences requis" + Et le bouton "Suivant" est désactivé + + Scénario: Nombre maximum de séquences + Étant donné un audio-guide avec 50 séquences + Quand le créateur tente d'ajouter une 51ème séquence + Alors un message d'erreur s'affiche: "Maximum 50 séquences par audio-guide" + Et le bouton "+ Ajouter séquence" est désactivé + + Scénario: Étape 3 - Preview carte avec tracé et points + Étant donné un audio-guide avec 5 séquences géolocalisées + Quand le créateur accède à l'étape 3 "Preview carte" + Alors une carte Leaflet s'affiche + Et les éléments suivants sont visibles: + | élément | description | + | Markers numérotés | 1, 2, 3, 4, 5 sur chaque point | + | Tracé entre points | Ligne pointillée connectant les points | + | Cercles de déclenchement | Rayon visuel autour de chaque point | + + Scénario: Statistiques du parcours + Étant donné un audio-guide avec les séquences suivantes: + | séquence | durée | distance_au_suivant | + | 1 | 2:15 | 150m | + | 2 | 3:42 | 200m | + | 3 | 4:10 | 320m | + Quand les statistiques sont calculées + Alors le résumé suivant est affiché: + | métrique | valeur | + | Séquences | 3 complètes | + | Durée totale | 10:07 | + | Distance totale | 670m | + + Scénario: Modification d'une séquence depuis la carte + Étant donné que la preview carte est affichée + Quand le créateur clique sur le marker "2" + Alors une popup s'affiche avec: + | information | + | Titre: "Les lions" | + | Durée: 3:42 | + | Rayon: 30m | + | [✏️ Modifier] | + | [🗑️ Supprimer] | + + Scénario: Zone de diffusion géographique + Étant donné un audio-guide avec des points dans Paris + Quand le créateur définit la zone de diffusion + Alors il peut choisir: + | type | exemple | + | Polygon | Tracé manuel sur carte | + | Ville | Paris (API Nominatim) | + | Département | 75 - Paris | + | Région | Île-de-France | + + Scénario: Étape 4 - Publication et validation modération + Étant donné un créateur qui publie ses 3 premiers audio-guides + Quand il clique sur "✅ Publier audio-guide" + Alors un message s'affiche: + """ + Votre audio-guide est en cours de validation. + Notre équipe le vérifiera sous 24-48h. + Vous recevrez une notification dès validation. + """ + + Scénario: Publication directe pour créateurs expérimentés + Étant donné un créateur ayant publié 5 audio-guides validés + Et aucun strike actif + Quand il publie un nouvel audio-guide + Alors l'audio-guide est publié immédiatement + Et il devient visible pour les utilisateurs + Et aucune validation manuelle n'est requise + + Scénario: Mode piéton sans points GPS obligatoires + Étant donné un audio-guide en mode "🚶 Piéton" + Quand le créateur ajoute une séquence + Alors le champ "Point GPS" est optionnel + Et le champ "Rayon déclenchement" est masqué + Et un message info s'affiche: "Mode manuel : les séquences se déclenchent au clic utilisateur" + + Scénario: Sauvegarde brouillon automatique + Étant donné que le créateur édite un audio-guide depuis 5 minutes + Quand il ajoute une nouvelle séquence + Alors l'audio-guide est sauvegardé en brouillon automatiquement + Et un toast "Brouillon sauvegardé" s'affiche brièvement + + Scénario: Reprise d'un brouillon + Étant donné un audio-guide en brouillon "Safari du Paugre" + Et qu'il contient 3 séquences complètes + Quand le créateur retourne sur son dashboard + Alors le brouillon est visible avec le statut "📝 Brouillon" + Et un bouton "Continuer" est disponible + Et la progression "3/5 séquences" est affichée + + Scénario: Suppression d'un brouillon + Étant donné un audio-guide en brouillon + Quand le créateur clique sur "🗑️ Supprimer" + Alors une confirmation s'affiche: + """ + Supprimer ce brouillon ? + Toutes les séquences seront perdues. + [Annuler] [Supprimer définitivement] + """ + + Scénario: Modification d'un audio-guide publié + Étant donné un audio-guide publié "Safari du Paugre" + Quand le créateur clique sur "✏️ Modifier" + Alors il peut modifier: + | élément modifiable | élément non modifiable | + | Titre | Mode de déplacement | + | Description | Points GPS | + | Tags | Rayons déclenchement | + | Séquences (ordre) | | + Et un avertissement s'affiche: "Les modifications structurelles nécessitent une nouvelle publication" + + Scénario: Duplication d'un audio-guide existant + Étant donné un audio-guide publié "Visite Paris" + Quand le créateur clique sur "📋 Dupliquer" + Alors une copie est créée avec le titre "Visite Paris (copie)" + Et toutes les séquences sont copiées + Et le statut est "📝 Brouillon" + Et le créateur peut modifier avant publication + + # Cas d'erreur + + Scénario: Upload audio échoue (format non supporté) + Étant donné que le créateur upload un fichier "audio.wav" + Quand le format est vérifié + Alors un message d'erreur s'affiche: "Format non supporté. Utilisez MP3, AAC ou M4A" + Et le fichier est rejeté + + Scénario: Upload audio échoue (taille trop grande) + Étant donné que le créateur upload un fichier de 250 MB + Quand la taille est vérifiée + Alors un message d'erreur s'affiche: "Fichier trop volumineux. Maximum 200 MB" + Et le fichier est rejeté + + Scénario: Points GPS trop éloignés (alerte cohérence) + Étant donné un audio-guide en mode "Piéton" + Et une séquence au Louvre (Paris) + Quand le créateur ajoute une séquence à Lyon + Alors un avertissement s'affiche: + """ + ⚠️ Attention : distance importante entre points (465 km) + Vérifiez que le mode "Piéton" est approprié. + [Modifier le mode] [Continuer] + """ + + Scénario: Pas de connexion lors de la sauvegarde + Étant donné que le créateur édite un audio-guide + Et que la connexion réseau est perdue + Quand il tente de sauvegarder + Alors le brouillon est sauvegardé localement + Et un message s'affiche: "Sauvegarde locale. Sera synchronisée à la reconnexion" + Et une icône "☁️ Hors ligne" s'affiche + + Scénario: Reprise après perte de connexion + Étant donné un brouillon sauvegardé localement + Quand la connexion réseau est rétablie + Alors le brouillon est synchronisé automatiquement + Et un toast "✅ Audio-guide synchronisé" s'affiche diff --git a/features/audio-guides/integration-fonctionnalites.feature b/features/audio-guides/integration-fonctionnalites.feature new file mode 100644 index 0000000..891955e --- /dev/null +++ b/features/audio-guides/integration-fonctionnalites.feature @@ -0,0 +1,349 @@ +# language: fr + +Fonctionnalité: Intégration audio-guides avec autres fonctionnalités + En tant qu'utilisateur + Je veux utiliser les audio-guides avec toutes les fonctionnalités de l'app + Afin d'avoir une expérience complète et cohérente + + Contexte: + Étant donné que l'application RoadWave est démarrée + Et que l'utilisateur "jean@example.com" est connecté + + # 16.14 - Téléchargement offline + + Scénario: Téléchargement complet d'un audio-guide + Étant donné un audio-guide "Visite du Louvre" avec 12 séquences + Quand l'utilisateur clique sur "⬇️ Télécharger pour écouter hors ligne" + Alors toutes les 12 séquences sont téléchargées + Et les métadonnées (titres, descriptions, GPS) sont sauvegardées + Et les images (cover, miniatures) sont mises en cache + + Scénario: Affichage de la progression du téléchargement + Étant donné qu'un téléchargement d'audio-guide est en cours + Quand l'utilisateur consulte l'état + Alors la progression s'affiche: + """ + ⬇️ Téléchargement en cours... + Séquence 7/12 • 245 MB / 380 MB + ──────●──────── 64% + """ + + Scénario: Téléchargement uniquement en WiFi (par défaut) + Étant donné que l'option "Télécharger uniquement en WiFi" est activée + Quand l'utilisateur lance un téléchargement sur réseau mobile + Alors un avertissement s'affiche: + """ + ⚠️ Téléchargement nécessite WiFi + Cet audio-guide pèse 380 MB. + [Attendre WiFi] [Télécharger quand même] + """ + + Scénario: Gestion de l'espace de stockage + Étant donné que l'appareil a 500 MB d'espace libre + Et qu'un audio-guide pèse 380 MB + Quand l'utilisateur lance le téléchargement + Alors un avertissement s'affiche: + """ + ⚠️ Espace de stockage limité + Après téléchargement : 120 MB restants + [Continuer] [Gérer stockage] [Annuler] + """ + + Scénario: Liste des audio-guides téléchargés + Étant donné que l'utilisateur a téléchargé 3 audio-guides + Quand il accède à "Bibliothèque > Téléchargés" + Alors il voit: + | audio_guide | taille | date_telechargement | + | Visite du Louvre | 380 MB | 2026-01-20 | + | Safari du Paugre | 245 MB | 2026-01-18 | + | Circuit Loire à Vélo | 520 MB | 2026-01-15 | + + Scénario: Lecture hors connexion complète + Étant donné qu'un audio-guide est téléchargé + Et que l'utilisateur active le mode avion + Quand il lance l'audio-guide + Alors toutes les séquences sont lisibles + Et les métadonnées sont accessibles + Et les images s'affichent normalement + Et la progression est sauvegardée localement + + Scénario: GPS fonctionne en mode avion (mode voiture) + Étant donné qu'un audio-guide voiture est téléchargé + Et que le mode avion est activé (avec GPS actif) + Quand l'utilisateur se déplace + Alors les déclenchements GPS fonctionnent normalement + Et la distance/ETA sont calculés + Parce que le GPS ne nécessite pas de connexion internet + + Scénario: Suppression d'audio-guide téléchargé + Étant donné qu'un audio-guide téléchargé pèse 380 MB + Quand l'utilisateur clique sur "🗑️ Supprimer téléchargement" + Alors une confirmation s'affiche + Et si confirmé, les 380 MB sont libérés + Et l'audio-guide reste accessible en streaming + + Scénario: Mise à jour automatique si nouvelle version + Étant donné qu'un audio-guide téléchargé a été mis à jour par le créateur + Quand l'utilisateur se connecte en WiFi + Alors une notification s'affiche: + """ + 🔄 Mise à jour disponible + "Visite du Louvre" - Nouvelle version + [Mettre à jour] [Plus tard] + """ + + # 16.15 - Playlists et collections + + Scénario: Ajout d'audio-guide à une playlist + Étant donné que l'utilisateur consulte un audio-guide + Quand il clique sur "➕ Ajouter à une playlist" + Alors ses playlists s'affichent: + | playlist | + | 🗺️ Voyages en France | + | 🏛️ Musées parisiens | + | + Créer nouvelle playlist | + + Scénario: Comportement audio-guide dans une playlist + Étant donné une playlist contenant 2 audio-guides et 1 podcast + Quand la lecture atteint un audio-guide + Alors l'audio-guide démarre à la séquence 1 (ou progression sauvegardée) + Et les séquences se jouent normalement + Quand l'audio-guide se termine (dernière séquence) + Alors le contenu suivant de la playlist démarre + + Scénario: Audio-guide marqué comme "Favori" + Étant donné qu'un utilisateur aime un audio-guide + Quand il clique sur "⭐ Ajouter aux favoris" + Alors l'audio-guide est ajouté à la section "Favoris" + Et il est facilement accessible depuis le menu principal + + Scénario: Collections thématiques d'audio-guides + Étant donné que RoadWave propose des collections éditoriales + Quand l'utilisateur accède à "Collections" + Alors il voit des collections comme: + | collection | nombre_audio_guides | + | 🏛️ Musées de France | 12 | + | 🦁 Parcs animaliers | 8 | + | 🚴 Circuits vélo | 15 | + | 🚗 Routes touristiques | 10 | + + # 16.16 - Partage d'audio-guide + + Scénario: Bouton partager sur page audio-guide + Étant donné qu'un utilisateur consulte un audio-guide + Quand il clique sur "⬆️ Partager" + Alors le menu de partage natif s'ouvre + Et le lien généré est "https://roadwave.fr/share/ag/louvre_123" + + Scénario: Page web de partage pour audio-guide + Étant donné qu'un lien d'audio-guide partagé est ouvert sur le web + Quand la page se charge + Alors elle affiche: + | élément | exemple | + | Cover image 16:9 | Photo du Louvre | + | Titre | "Visite du Louvre" | + | Créateur | "@art_guide ✓" | + | Badge type | "🎧 Audio-guide • 12 séquences" | + | Durée totale | "45 minutes" | + | Mode | "🚶 Piéton" | + | Description | Texte complet | + | Preview séquence 1 | Player HTML5 (séquence intro) | + | Carte avec points GPS | Leaflet avec 12 markers | + | CTA téléchargement | Boutons App Store / Google Play | + + Scénario: Deep link vers audio-guide spécifique + Étant donné que l'app est installée + Et qu'un lien "https://roadwave.fr/share/ag/louvre_123" est cliqué + Quand le système détecte l'app + Alors l'app s'ouvre directement sur l'audio-guide + Et l'utilisateur peut démarrer immédiatement + + Scénario: Partage avec séquence spécifique + Étant donné qu'un utilisateur est sur la séquence 5 "La Joconde" + Quand il partage l'audio-guide + Alors le lien généré est "https://roadwave.fr/share/ag/louvre_123?seq=5" + Et le destinataire est dirigé vers la séquence 5 directement + + # 16.17 - Notations et commentaires + + Scénario: Note globale de l'audio-guide + Étant donné qu'un utilisateur termine un audio-guide + Quand la dernière séquence se termine + Alors une popup de notation s'affiche: + """ + Comment avez-vous trouvé cet audio-guide ? + ⭐⭐⭐⭐⭐ + [Ajouter un commentaire (optionnel)] + """ + + Scénario: Note moyenne affichée sur la page + Étant donné qu'un audio-guide a reçu 150 notes + Et que la moyenne est 4.3/5 + Quand la page est affichée + Alors la note "⭐ 4.3 (150 avis)" est visible + + Scénario: Commentaires triés par pertinence + Étant donné qu'un audio-guide a 50 commentaires + Quand l'utilisateur consulte les avis + Alors les commentaires sont triés par défaut selon: + | critère | poids | + | Note élevée | 30% | + | Récent | 30% | + | Likes reçus | 40% | + + Scénario: Réponse du créateur aux commentaires + Étant donné qu'un utilisateur laisse un commentaire négatif + Quand le créateur consulte son dashboard + Alors il peut répondre au commentaire + Et sa réponse apparaît en dessous avec badge "Créateur" + + # 16.18 - Recommandations intelligentes + + Scénario: Audio-guides similaires recommandés + Étant donné qu'un utilisateur termine "Visite du Louvre" + Quand il consulte les recommandations + Alors l'algorithme suggère des audio-guides basés sur: + | critère | exemple | + | Tags similaires | #Art #Histoire #Musée | + | Créateur identique | Autres audio-guides de @art_guide | + | Localisation proche | Autres musées parisiens | + | Mode de déplacement | Autres audio-guides piéton | + + Scénario: Suggestion géographique contextuelle + Étant donné qu'un utilisateur est à Paris (GPS détecté) + Quand il ouvre l'onglet "Audio-guides" + Alors les audio-guides parisiens sont mis en avant + Et un filtre "🗺️ Autour de moi" est pré-appliqué + + Scénario: Badge "Populaire dans votre région" + Étant donné qu'un audio-guide a >100 écoutes dans la région Île-de-France + Et que l'utilisateur est en Île-de-France + Quand l'audio-guide est affiché + Alors un badge "🔥 Populaire près de chez vous" est visible + + # 16.19 - Optimisations techniques + + Scénario: Préchargement de la séquence suivante + Étant donné que la séquence 3 est en cours à 2:30/3:42 + Quand il reste 60 secondes de lecture + Alors la séquence 4 est préchargée en arrière-plan + Et la transition est instantanée (0 latence) + + Scénario: Buffer adaptatif selon connexion + Étant donné qu'un utilisateur est sur réseau 4G + Quand la séquence démarre + Alors 30 secondes d'audio sont bufferisées initialement + Et le buffering continue en arrière-plan + + Plan du Scénario: Buffer selon qualité réseau + Étant donné qu'un utilisateur est sur réseau + Quand une séquence démarre + Alors secondes sont bufferisées + + Exemples: + | reseau | buffer_secondes | + | WiFi | 60 | + | 5G | 45 | + | 4G | 30 | + | 3G | 20 | + + Scénario: Compression audio adaptative + Étant donné qu'un utilisateur est sur connexion lente (3G) + Quand une séquence est streamée + Alors le CDN sert la version 64 kbps (au lieu de 128 kbps) + Et la qualité reste acceptable pour la voix + + Scénario: Cache intelligent des séquences jouées + Étant donné qu'un utilisateur a écouté les séquences 1-5 + Quand il clique sur "Précédent" pour réécouter la séquence 4 + Alors la séquence 4 est chargée depuis le cache local + Et le chargement est instantané (pas de stream) + + Scénario: Nettoyage automatique du cache + Étant donné que le cache audio occupe 500 MB + Et que la limite configurée est 300 MB + Quand le nettoyage automatique s'exécute + Alors les séquences les plus anciennes (non téléchargées) sont supprimées + Et le cache revient à 280 MB + + # 16.20 - Analytics et tracking + + Scénario: Tracking des événements clés + Étant donné qu'un utilisateur écoute un audio-guide + Quand il interagit avec l'application + Alors les événements suivants sont trackés: + | é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 | + | paywall_displayed | audio_guide_id, sequence_number | + | premium_conversion | source: audio_guide_paywall | + + Scénario: Heatmap des abandons par séquence + Étant donné qu'un audio-guide a été écouté 1000 fois + Quand le créateur consulte la heatmap + Alors il voit pour chaque séquence: + | sequence | starts | completions | abandon_rate | + | 1 | 1000 | 950 | 5% | + | 2 | 950 | 920 | 3% | + | 3 | 920 | 850 | 8% | + | ... | ... | ... | ... | + | 12 | 650 | 580 | 11% | + + Scénario: Attribution GPS auto vs manuel + Étant donné un audio-guide voiture avec 8 points GPS + Quand les statistiques sont calculées + Alors le créateur voit: + | mode_declenchement | nombre | + | GPS automatique | 542 | + | Manuel | 123 | + | Point manqué | 89 | + + # Cas d'erreur et edge cases + + Scénario: Audio-guide avec une seule séquence (edge case) + Étant donné un audio-guide avec seulement 1 séquence + Quand il est publié + Alors un avertissement s'affiche: + """ + ⚠️ Un audio-guide doit contenir au minimum 2 séquences + Ajoutez au moins 1 séquence supplémentaire avant publication + """ + + Scénario: Séquence manquante ou corrompue + Étant donné qu'une séquence 5 a un fichier audio corrompu + Quand l'utilisateur tente de la lire + Alors un message d'erreur s'affiche + Et un bouton "⏭️ Passer à la suivante" est disponible + Et le créateur reçoit une notification de l'erreur + + Scénario: GPS désactivé puis réactivé en cours de route + Étant donné un audio-guide voiture en cours + Et que l'utilisateur désactive le GPS + Quand il le réactive 10 minutes plus tard + Alors le déclenchement automatique reprend + Et les points GPS manqués entre-temps ne déclenchent pas de popup + + Scénario: Modification d'audio-guide avec utilisateurs en cours + Étant donné qu'un audio-guide a 50 utilisateurs en cours d'écoute + Quand le créateur modifie une séquence + Alors les utilisateurs actuels conservent l'ancienne version + Et les nouveaux utilisateurs obtiennent la nouvelle version + Et un message informe les utilisateurs lors de la prochaine ouverture + + Scénario: Suppression d'audio-guide par le créateur + Étant donné qu'un audio-guide a 20 utilisateurs avec progression + Quand le créateur supprime l'audio-guide + Alors une confirmation stricte est demandée + Et si confirmé, les progressions utilisateurs sont archivées (30 jours) + Et l'audio-guide devient inaccessible + + Scénario: Signalement d'audio-guide pour contenu inapproprié + Étant donné qu'un utilisateur signale un audio-guide + Quand le signalement est modéré + Et jugé valide + Alors l'audio-guide est dépublié temporairement + Et le créateur reçoit une notification d'explication + Et il peut corriger puis republier diff --git a/features/audio-guides/mode-pieton.feature b/features/audio-guides/mode-pieton.feature new file mode 100644 index 0000000..6c28947 --- /dev/null +++ b/features/audio-guides/mode-pieton.feature @@ -0,0 +1,243 @@ +# language: fr + +Fonctionnalité: Audio-guide mode piéton (navigation manuelle) + En tant qu'utilisateur à pied + Je veux naviguer manuellement entre les séquences d'un audio-guide + Afin de contrôler mon rythme de visite + + Contexte: + Étant donné que l'application RoadWave est démarrée + Et que l'utilisateur "jean@example.com" est connecté (gratuit) + Et qu'un audio-guide piéton "Visite du Louvre" est disponible avec 12 séquences + + # 16.2.1 - Passage entre séquences + + Scénario: Fin de séquence normale avec pause automatique + Étant donné que la séquence 1 "Introduction" est en cours de lecture + Quand la séquence se termine à 2:15 + Alors le player se met en pause automatiquement + Et le message suivant s'affiche: "Séquence 1 terminée. Appuyez sur Suivant quand vous êtes prêt." + Et la barre de progression indique "1/12 complétée" + + Scénario: Passage manuel à la séquence suivante + Étant donné que la séquence 1 est terminée et le player en pause + Quand l'utilisateur appuie sur le bouton [▶|] "Suivant" + Alors la séquence 2 "Pyramide du Louvre" démarre immédiatement + Et aucune latence n'est observée + + Scénario: Séquence avec publicité (1/5 séquences) + Étant donné que la séquence 5 se termine + Et que c'est la 5ème séquence (1 pub toutes les 5) + Quand la séquence se termine + Alors la publicité s'enchaîne automatiquement (sans attente bouton) + Et la publicité se lit normalement + Et elle est skippable après 5 secondes + + Scénario: Fin de publicité avec pause automatique + Étant donné qu'une publicité est en cours de lecture + Quand la publicité se termine + Alors le player se met en pause automatiquement + Et le message suivant s'affiche: "Séquence 6 prête. Appuyez sur Suivant." + Et l'utilisateur doit cliquer sur [▶|] pour continuer + + Scénario: Flux complet séquence → pub → séquence + Étant donné que la séquence 5 démarre + Quand la séquence 5 se termine + Alors la publicité démarre automatiquement + Quand la publicité se termine + Alors le player se met en pause + Quand l'utilisateur clique sur [▶|] + Alors la séquence 6 démarre + + Plan du Scénario: Fréquence de publicité configurable + Étant donné que l'utilisateur gratuit écoute un audio-guide + Et que la fréquence pub est configurée à + Quand il termine la séquence + Alors une publicité est insérée : + + Exemples: + | frequence | numero_sequence | pub_inseree | + | 1/5 | 5 | Oui | + | 1/5 | 10 | Oui | + | 1/5 | 4 | Non | + | 1/3 | 3 | Oui | + | 1/3 | 6 | Oui | + + Scénario: Utilisateur Premium sans publicités + Étant donné que l'utilisateur "premium@example.com" est abonné Premium + Et qu'il écoute un audio-guide piéton + Quand il termine la séquence 5 + Alors aucune publicité n'est insérée + Et le player se met en pause immédiatement + Et le message "Séquence 6 prête. Appuyez sur Suivant." s'affiche + + # 16.2.2 - Navigation et contrôles + + Scénario: Boutons de contrôle disponibles en mode piéton + Étant donné qu'un audio-guide piéton est en lecture + Quand l'utilisateur consulte les contrôles + Alors les boutons suivants sont visibles: + | bouton | fonction | + | [▶\|] Suivant | Passe à la séquence suivante | + | [\|◀] Précédent | Retour à la séquence précédente | + | [⏸️] Pause | Pause temporaire | + | [▶️] Play | Reprend la lecture | + | [📋] Liste | Affiche toutes les séquences | + + Scénario: Passage à la séquence suivante pendant la lecture + Étant donné que la séquence 3 "La Joconde" est en cours à 1:42/3:42 + Quand l'utilisateur clique sur [▶|] "Suivant" + Alors la séquence 4 "Vénus de Milo" démarre immédiatement + Et la séquence 3 n'est pas marquée comme écoutée (car <80%) + + Scénario: Retour à la séquence précédente (saut direct) + Étant donné que la séquence 5 est en cours de lecture + Quand l'utilisateur clique sur [|◀] "Précédent" + Alors la séquence 4 démarre depuis le début (0:00) + Et il n'y a pas de logique "replay si >10s" (contrairement au contenu classique) + + Scénario: Pause et reprise pendant une séquence + Étant donné que la séquence 2 est en cours à 1:15/1:48 + Quand l'utilisateur clique sur [⏸️] "Pause" + Alors la lecture se met en pause + Et la position 1:15 est conservée + Quand l'utilisateur clique sur [▶️] "Play" + Alors la lecture reprend exactement à 1:15 + + Scénario: Interface liste des séquences + Étant donné qu'un audio-guide de 12 séquences est en cours + Quand l'utilisateur clique sur [📋] "Liste séquences" + Alors une liste complète s'affiche avec: + | élément | exemple | + | Numéro et titre | "3. La Joconde" | + | Durée | (3:42) | + | État | ✅ Écouté / ▶️ En cours / ⭕ À écouter | + | Date écoute (si écouté) | "Écouté le 15/01/2026" | + + Scénario: Séquence en cours dans la liste + Étant donné que la séquence 3 est en cours à 1:22/3:42 + Quand la liste des séquences est affichée + Alors la séquence 3 affiche: + """ + ▶️ 3. La Joconde (3:42) - EN COURS + ──●──────────── 1:22/3:42 + """ + + Scénario: Navigation libre vers séquence non encore écoutée + Étant donné que l'utilisateur est sur la séquence 3 + Et que les séquences 4 à 12 n'ont pas été écoutées + Quand l'utilisateur clique sur "8. Les Appartements de Napoléon" + Alors la séquence 8 démarre immédiatement depuis 0:00 + Et les séquences 4 à 7 restent marquées ⭕ "À écouter" + + Scénario: Retour à une séquence déjà écoutée + Étant donné que la séquence 2 "Pyramide du Louvre" a été écoutée à 100% + Et qu'elle est marquée ✅ "Écouté" + Quand l'utilisateur clique dessus dans la liste + Alors la séquence 2 démarre depuis 0:00 + Et le statut ✅ est conservé + + Scénario: Checkmarks sur séquences écoutées >80% + Étant donné que l'utilisateur écoute la séquence 2 de durée 1:48 + Quand il écoute jusqu'à 1:30 (83% de complétion) + Et qu'il passe à la séquence suivante + Alors la séquence 2 est marquée ✅ "Écouté" + Et la date d'écoute est enregistrée + + Scénario: Pas de checkmark si séquence écoutée <80% + Étant donné que l'utilisateur écoute la séquence 3 de durée 3:42 + Quand il écoute jusqu'à 1:30 (40% de complétion) + Et qu'il passe à la séquence suivante + Alors la séquence 3 reste marquée ⭕ "À écouter" + + Scénario: Bouton "Tout afficher" si plus de 6 séquences + Étant donné un audio-guide avec 12 séquences + Quand la liste est affichée + Alors seules les 6 premières séquences sont visibles initialement + Et un bouton "Tout afficher ▼" est présent + Quand l'utilisateur clique sur "Tout afficher ▼" + Alors les 6 séquences restantes sont affichées + + Scénario: Saut vers séquence spécifique depuis la barre de progression + Étant donné qu'un audio-guide est en cours + Quand l'utilisateur clique sur "3/12" dans la barre de progression + Alors la liste des séquences s'ouvre + Et la séquence en cours (3) est mise en surbrillance + + # Sauvegarde progression + + Scénario: Position exacte sauvegardée automatiquement + Étant donné que la séquence 5 est en cours à 2:34/4:10 + Quand l'utilisateur quitte l'application + Alors la position 2:34 dans la séquence 5 est sauvegardée + Et la sauvegarde est effectuée localement (SQLite) + Et la sauvegarde est synchronisée sur le cloud (PostgreSQL) + + Scénario: Reprise après fermeture de l'application + Étant donné que l'utilisateur a quitté l'app à la séquence 5 position 2:34 + Quand il rouvre l'audio-guide + Alors une popup de reprise s'affiche + Quand il clique sur "▶️ Reprendre" + Alors la lecture reprend à la séquence 5 position 2:34 exacte + + # Cas d'usage réels + + Scénario: Visiteur qui connaît déjà certaines œuvres + Étant donné qu'un visiteur du Louvre démarre l'audio-guide + Et qu'il connaît déjà "La Joconde" (séquence 3) + Quand il arrive à la séquence 3 + Et qu'il clique sur [▶|] "Suivant" après 10 secondes + Alors la séquence 4 démarre immédiatement + Et la séquence 3 n'est pas marquée comme écoutée + + Scénario: Visiteur qui veut voir une œuvre éloignée + Étant donné qu'un visiteur est à la séquence 2 + Et qu'il aperçoit "La Victoire de Samothrace" (séquence 8) physiquement + Quand il ouvre la liste et clique sur la séquence 8 + Alors la séquence 8 démarre immédiatement + Et il peut écouter la description même si les séquences 3-7 ne sont pas écoutées + + Scénario: Visiteur qui prend une pause café + Étant donné qu'un visiteur écoute la séquence 6 + Quand il clique sur [⏸️] "Pause" + Et qu'il ferme l'application pendant 30 minutes + Quand il rouvre l'application + Alors la séquence 6 reprend à la position exacte où il s'était arrêté + + Scénario: Visiteur qui revient le lendemain + Étant donné qu'un visiteur a écouté les séquences 1-5 hier + Et qu'il revient au musée aujourd'hui + Quand il ouvre l'audio-guide + Alors une popup propose "▶️ Reprendre" (séquence 6) + Et les séquences 1-5 sont marquées ✅ "Écouté" + + # Cas d'erreur + + Scénario: Séquence audio corrompue ou indisponible + Étant donné que la séquence 7 a un fichier audio corrompu + Quand l'utilisateur tente de la lire + Alors un message d'erreur s'affiche: + """ + ⚠️ Cette séquence est temporairement indisponible. + [⏭️ Passer à la suivante] [🔄 Réessayer] + """ + + Scénario: Perte de connexion pendant le chargement + Étant donné que l'utilisateur lance la séquence 4 + Et que la connexion réseau est perdue + Quand le chargement échoue + Alors un message s'affiche: "Connexion perdue. Vérifiez votre réseau." + Et un bouton "🔄 Réessayer" est disponible + + Scénario: Batterie faible en cours de visite + Étant donné que la batterie de l'appareil est à 5% + Quand l'utilisateur écoute une séquence + Alors une notification système s'affiche: "Batterie faible. Progression sauvegardée." + Et la position est sauvegardée localement toutes les 10 secondes + + Scénario: Mode piéton sans points GPS (pas d'alerte localisation) + Étant donné un audio-guide en mode piéton + Et que le GPS est désactivé + Quand l'utilisateur démarre l'audio-guide + Alors aucune alerte GPS ne s'affiche + Et l'audio-guide fonctionne normalement (navigation 100% manuelle) diff --git a/features/audio-guides/mode-voiture.feature b/features/audio-guides/mode-voiture.feature new file mode 100644 index 0000000..5c0939d --- /dev/null +++ b/features/audio-guides/mode-voiture.feature @@ -0,0 +1,400 @@ +# language: fr + +Fonctionnalité: Audio-guide mode voiture (GPS automatique) + En tant qu'utilisateur en voiture + Je veux que les séquences se déclenchent automatiquement selon ma position GPS + Afin de profiter d'une expérience guidée hands-free + + Contexte: + Étant donné que l'application RoadWave est démarrée + Et que l'utilisateur "jean@example.com" est connecté (gratuit) + Et qu'un audio-guide voiture "Safari du Paugre" est disponible avec 8 séquences + Et que le GPS est activé + + # 16.3.1 - Déclenchement et contrôles + + Scénario: Distinction audio-guides vs contenus géolocalisés simples + Étant donné que l'utilisateur est en mode voiture + Quand il écoute un contenu géolocalisé simple (1 séquence unique) + Alors une notification avec compteur 7→1 est affichée 7s avant le point + Et il doit valider avec "Suivant" + décompte 5s + Et ce contenu compte 1/6 dans le quota horaire + Quand il démarre un audio-guide multi-séquences + Alors les séquences se déclenchent au point GPS exact (rayon 30m) + Et aucun compteur 7s n'est affiché (juste notification "Ding" + toast 2s) + Et l'audio-guide entier compte 1/6 dans le quota + + Scénario: Démarrage automatique au premier point GPS + Étant donné que l'utilisateur démarre l'audio-guide "Safari du Paugre" + Et que le point de départ est à (43.1234, 2.5678) avec rayon 30m + Quand l'utilisateur entre dans le rayon de 30m + Alors la séquence 1 "Introduction - Point d'accueil" démarre automatiquement + Et une notification sonore "Ding" est jouée (non intrusif) + Et un toast s'affiche brièvement pendant 2s: "Introduction - Point d'accueil" + Et aucun compteur 7→1 n'est affiché (contrairement aux contenus géolocalisés simples) + + Scénario: Déclenchement automatique séquence suivante + Étant donné que la séquence 1 est terminée + Et que l'utilisateur se déplace vers le point GPS 2 (43.1245, 2.5690) + Quand l'utilisateur entre dans le rayon de 30m du point 2 + Alors la séquence 2 "Enclos des lions" démarre automatiquement + Et une notification "Ding" + toast "Enclos des lions" s'affiche + + Scénario: Navigation manuelle conservée (bouton Suivant actif) + Étant donné que la séquence 1 est en cours + Et que l'utilisateur est encore loin du point GPS 2 (distance 500m) + Quand l'utilisateur clique sur [▶|] "Suivant" + Alors la séquence 2 démarre immédiatement + Et aucune vérification GPS n'est effectuée + + Scénario: Navigation manuelle conservée (bouton Précédent actif) + Étant donné que la séquence 3 est en cours + Quand l'utilisateur clique sur [|◀] "Précédent" + Alors la séquence 2 démarre depuis le début + Et aucune vérification GPS n'est effectuée + + Scénario: Tous les boutons de contrôle restent actifs + Étant donné qu'un audio-guide voiture est en cours + Quand l'utilisateur consulte les contrôles + Alors les boutons suivants sont actifs: + | bouton | état | comportement | + | [▶\|] Suivant | ✅ | Passe séquence suivante immédiate | + | [\|◀] Précédent | ✅ | Retour séquence précédente | + | [⏸️] Pause | ✅ | Pause temporaire | + | [📋] Liste | ✅ | Saut direct possible | + + Scénario: Use case - Embouteillage (séquence finie, point GPS loin) + Étant donné que la séquence 3 "Enclos des girafes" est terminée + Et que le point GPS 4 est à 2 km de distance (embouteillage) + Quand l'utilisateur clique manuellement sur [▶|] "Suivant" + Alors la séquence 4 démarre immédiatement + Et l'utilisateur peut continuer l'expérience sans attendre d'atteindre le point GPS + + Scénario: Use case - Route fermée (point GPS inaccessible) + Étant donné que le point GPS 5 est sur une route fermée + Et que l'utilisateur ne peut pas s'en approcher + Quand l'utilisateur clique sur [▶|] "Suivant" + Alors la séquence 5 démarre quand même + Et l'audio-guide continue normalement + + Scénario: Use case - Passager manipule l'application + Étant donné que l'utilisateur est passager (non conducteur) + Et que la vitesse du véhicule est 45 km/h + Quand le passager clique sur [▶|] "Suivant" + Alors la séquence suivante démarre + Et un avertissement s'affiche pendant 3 secondes + + Scénario: Avertissement sécurité si vitesse >10 km/h + Étant donné que la vitesse actuelle est 35 km/h + Quand l'utilisateur clique sur un bouton (Suivant ou Précédent) + Alors l'action est exécutée immédiatement (pas de blocage) + Et un toast s'affiche pendant 3 secondes: + """ + ⚠️ Manipulation en conduite détectée. + Pour votre sécurité, demandez à un passager. + """ + + Plan du Scénario: Avertissement selon la vitesse + Étant donné que la vitesse actuelle est km/h + Quand l'utilisateur clique sur un bouton de navigation + Alors l'avertissement est affiché : + + Exemples: + | vitesse | avertissement | + | 5 | Non | + | 10 | Non | + | 11 | Oui | + | 35 | Oui | + | 90 | Oui | + + # 16.3.2 - Affichage distance et guidage + + Scénario: Affichage entre deux séquences avec progress bar + Étant donné que la séquence 2 "Les lions" vient de se terminer + Et que le prochain point GPS 3 "Enclos des girafes" est à 500m + Quand l'interface bascule en mode "attente prochain point" + Alors l'écran affiche: + | élément | description | + | Statut séquence | "✅ Séquence 2/8 terminée" | + | Nom séquence | "Les lions" | + | Progress bar | Barre dynamique remplie selon distance (0%) | + | Distance prochain point| "500 mètres" | + | ETA | "≈ 1 minute 30" | + | Direction | ↗️ | + | Vitesse actuelle | "28 km/h" | + | Bouton "Rejouer séq." | Permet de réécouter la séquence qui vient de finir | + + Scénario: Progress bar dynamique vers le prochain point + Étant donné que la distance initiale vers le prochain point était 500m + Et que la séquence précédente est terminée + Quand l'utilisateur se rapproche du prochain point + Et que la distance actuelle est 175m + Alors la progress bar affiche "65%" remplie + Et le calcul est: 100 - (175 / 500 * 100) = 65% + Et la barre se met à jour chaque seconde + + Scénario: Bouton "Rejouer séq." pour réécouter + Étant donné que la séquence 3 vient de se terminer + Et que l'interface "attente prochain point" est affichée + Quand l'utilisateur clique sur [▶️ Rejouer séq.] + Alors la séquence 3 redémarre depuis 0:00 + Et l'utilisateur peut la réécouter (utile si distraction) + + Scénario: Interface en conduite avec distance et ETA + Étant donné que la séquence 2 est en cours + Et que le prochain point GPS 3 "Enclos des girafes" est à 320m + Et que la vitesse actuelle est 28 km/h + Quand l'interface est affichée + Alors les informations suivantes sont visibles: + | information | valeur | + | Nom prochain point | "Enclos des girafes" | + | Distance | "320 mètres" | + | ETA | "≈ 40 secondes" | + | Direction | ↗️ (flèche direction) | + | Vitesse actuelle | "28 km/h" | + | Vitesse recommandée | "20-30 km/h" | + + Scénario: Mise à jour de la distance en temps réel + Étant donné que la distance au prochain point est 500m + Quand 10 secondes s'écoulent et que l'utilisateur se rapproche + Alors la distance est mise à jour chaque seconde + Et la nouvelle distance "450m" s'affiche + + Scénario: Mise à jour de l'ETA en temps réel + Étant donné que l'ETA est "≈ 2 minutes" + Et que la vitesse est constante à 30 km/h + Quand l'utilisateur se rapproche du point + Alors l'ETA est recalculé chaque seconde + Et il diminue progressivement: "≈ 1 minute 50", "≈ 1 minute 40", etc. + + Plan du Scénario: Format d'affichage de la distance + Étant donné que la distance au prochain point est + Quand l'interface est mise à jour + Alors la distance affichée est "" + + Exemples: + | distance_metres | affichage | + | 50 | 50 m | + | 320 | 320 m | + | 980 | 980 m | + | 1200 | 1.2 km | + | 5400 | 5.4 km | + + Plan du Scénario: Format d'affichage de l'ETA + Étant donné que l'ETA calculé est secondes + Quand l'interface est mise à jour + Alors l'ETA affiché est "" + + Exemples: + | secondes | affichage | + | 30 | ≈ 30 secondes | + | 75 | ≈ 1 minute | + | 150 | ≈ 2 minutes | + | 400 | ≈ 6 minutes | + + Scénario: Calcul de la direction (flèche 8 directions) + Étant donné que la position actuelle est (43.1234, 2.5678) + Et que le prochain point est au nord-est (angle 45°) + Quand la direction est calculée + Alors la flèche "↗" est affichée + + Plan du Scénario: Flèches de direction selon l'angle + Étant donné que l'angle vers le prochain point est ° + Quand la direction est calculée + Alors la flèche "" est affichée + + Exemples: + | angle | fleche | + | 0 | ↑ | + | 45 | ↗ | + | 90 | → | + | 135 | ↘ | + | 180 | ↓ | + | 225 | ↙ | + | 270 | ← | + | 315 | ↖ | + + Scénario: Mise à jour de la direction toutes les 5 secondes + Étant donné que la direction actuelle est ↑ (nord) + Et que l'utilisateur tourne vers l'est + Quand 5 secondes s'écoulent + Alors la direction est recalculée + Et la nouvelle flèche ↗ (nord-est) s'affiche + + Scénario: Message "En attente de déplacement" si vitesse <5 km/h + Étant donné que la vitesse actuelle est 2 km/h (arrêté) + Quand l'ETA est calculé + Alors le message "En attente de déplacement" s'affiche + Et l'ETA n'est pas calculé (car vitesse insuffisante) + + Scénario: Simplicité de l'interface (pas de carte miniature) + Étant donné qu'un audio-guide voiture est en cours + Quand l'interface est affichée + Alors aucune carte miniature n'est présente + Et seuls les éléments essentiels sont affichés: + | élément | + | Distance | + | ETA | + | Direction (flèche) | + | Vitesse | + | Contrôles audio | + + # 16.3.3 - Rayon de déclenchement et tolérance + + Scénario: Rayon de déclenchement par défaut en mode voiture + Étant donné un audio-guide voiture + Quand un point GPS est défini + Alors le rayon de déclenchement est 30 mètres par défaut + Et le rayon de tolérance "point manqué" est 100 mètres + + Scénario: Déclenchement dans le rayon (30m) + Étant donné que le point GPS 3 est défini avec rayon 30m + Quand l'utilisateur entre à 25m du point + Alors la séquence 3 se déclenche automatiquement + + Scénario: Pas de déclenchement hors rayon + Étant donné que le point GPS 3 a un rayon de 30m + Quand l'utilisateur passe à 45m du point + Alors la séquence 3 ne se déclenche pas automatiquement + + Scénario: Point manqué dans rayon de tolérance (100m) + Étant donné que l'utilisateur passe à 60m du point GPS 4 (hors rayon 30m) + Et que 60m < 100m (rayon tolérance) + Quand le point est détecté comme manqué + Alors un toast s'affiche: "⚠️ Point manqué : Enclos des éléphants" + Et une popup s'affiche pendant 5 secondes avec 3 options + + Scénario: Popup "Point manqué" avec 3 actions + Étant donné qu'un point GPS a été manqué (distance 60m) + Quand la popup s'affiche + Alors les options suivantes sont disponibles: + | bouton | icône | comportement | + | Écouter quand même | 🔊 | Lance séquence immédiatement (même hors zone) | + | Passer au suivant | ⏭️ | Skip séquence, continue vers prochain point | + | Faire demi-tour | 🔙 | Ouvre GPS externe (Google Maps/Waze) vers point | + + Scénario: Action "Écouter quand même" + Étant donné qu'un point GPS est manqué + Quand l'utilisateur clique sur "🔊 Écouter quand même" + Alors la séquence correspondante démarre immédiatement + Et l'utilisateur peut continuer sa route + + Scénario: Action "Passer au suivant" + Étant donné qu'un point GPS 5 est manqué + Quand l'utilisateur clique sur "⏭️ Passer au suivant" + Alors la séquence 5 est ignorée (non écoutée) + Et l'application attend le point GPS 6 + Et la distance vers le point 6 s'affiche + + Scénario: Action "Faire demi-tour" + Étant donné qu'un point GPS est manqué à (43.1250, 2.5700) + Quand l'utilisateur clique sur "🔙 Faire demi-tour" + Alors l'application détecte l'app GPS installée (Google Maps ou Waze) + Et ouvre la navigation GPS externe vers (43.1250, 2.5700) + + Scénario: Point manqué au-delà du rayon de tolérance (>100m) + Étant donné que l'utilisateur passe à 150m du point GPS 6 + Quand la distance est détectée + Alors aucune popup ne s'affiche (point trop loin) + Et l'utilisateur peut naviguer manuellement avec [▶|] + + Plan du Scénario: Gestion selon la distance au point + Étant donné un point GPS avec rayon 30m et tolérance 100m + Quand l'utilisateur passe à du point + Alors le comportement est + + Exemples: + | distance | comportement | + | 20m | Déclenchement automatique séquence | + | 40m | Rien (hors rayon, pas encore tolérance) | + | 60m | Popup "Point manqué" avec 3 options | + | 110m | Rien (trop loin, hors tolérance) | + + Scénario: Configuration rayon personnalisé par le créateur + Étant donné qu'un créateur définit un rayon de 50m (au lieu de 30m) + Quand un utilisateur entre à 45m du point + Alors la séquence se déclenche automatiquement + Et le rayon personnalisé est respecté + + Scénario: Rayon minimum et maximum configurables + Étant donné qu'un créateur configure un rayon + Quand il ajuste le curseur + Alors les valeurs disponibles sont de 10m à 200m + Et le rayon par défaut suggéré est 30m pour la voiture + + # Cas d'usage réels + + Scénario: Safari-parc avec déclenchement automatique fluide + Étant donné qu'un utilisateur roule dans un safari à 20 km/h + Quand il passe devant "Enclos des lions" (point GPS 2) + Alors la séquence 2 démarre automatiquement sans intervention + Et il peut se concentrer sur la conduite et l'observation + + Scénario: Détour imprévu (travaux sur la route) + Étant donné qu'un utilisateur prend un détour à cause de travaux + Et que le point GPS 4 devient inaccessible + Quand il est loin du point (>100m) + Et qu'il clique manuellement sur [▶|] + Alors la séquence 4 démarre quand même + Et l'expérience continue sans blocage + + Scénario: Passager qui navigue librement + Étant donné qu'un passager utilise l'application + Et que le conducteur roule à 50 km/h + Quand le passager clique sur "Précédent" pour réécouter + Alors l'action est exécutée immédiatement + Et un warning apparaît brièvement (sensibilisation) + + Scénario: Embouteillage prolongé + Étant donné que la séquence 3 est terminée depuis 10 minutes + Et que l'utilisateur est bloqué dans un embouteillage + Et que le point GPS 4 est encore à 1.5 km + Quand l'utilisateur clique sur [▶|] + Alors la séquence 4 démarre immédiatement + Et l'utilisateur peut passer le temps en écoutant + + # Cas d'erreur + + Scénario: GPS désactivé en mode voiture + Étant donné qu'un audio-guide voiture est démarré + Et que le GPS est désactivé + Quand l'application détecte l'absence de GPS + Alors une alerte s'affiche: + """ + ⚠️ GPS requis pour le mode Voiture + Activez la localisation pour profiter du déclenchement automatique. + [Activer GPS] [Passer en mode Manuel] + """ + + Scénario: Action "Passer en mode Manuel" + Étant donné que le GPS est désactivé + Quand l'utilisateur clique sur "Passer en mode Manuel" + Alors l'audio-guide bascule en navigation 100% manuelle + Et les boutons [▶|] et [|◀] permettent de naviguer + Et aucun déclenchement GPS n'est tenté + + Scénario: Précision GPS insuffisante + Étant donné que le signal GPS a une précision de ±150m + Quand l'utilisateur approche d'un point GPS avec rayon 30m + Alors un avertissement s'affiche: + """ + ⚠️ Signal GPS imprécis (±150m) + Le déclenchement automatique peut être perturbé. + Utilisez les boutons manuels si nécessaire. + """ + + Scénario: Perte signal GPS en cours de route + Étant donné qu'un audio-guide voiture est en cours + Quand le signal GPS est perdu (tunnel, parking souterrain) + Alors un toast s'affiche: "Signal GPS perdu. Navigation manuelle active." + Et les boutons de navigation restent actifs + Quand le signal GPS revient + Alors un toast s'affiche: "Signal GPS rétabli" + Et le déclenchement automatique est réactivé + + Scénario: Dépassement de la vitesse recommandée + Étant donné qu'un audio-guide recommande 20-30 km/h + Et que l'utilisateur roule à 65 km/h + Quand la vitesse est détectée + Alors l'affichage vitesse est en orange: "⚠️ 65 km/h" + Et un message info s'affiche: "Vitesse élevée. Risque de manquer des points." diff --git a/features/audio-guides/modes-velo-transport.feature b/features/audio-guides/modes-velo-transport.feature new file mode 100644 index 0000000..dc7fa3c --- /dev/null +++ b/features/audio-guides/modes-velo-transport.feature @@ -0,0 +1,274 @@ +# language: fr + +Fonctionnalité: Audio-guides modes vélo et transport + En tant qu'utilisateur à vélo ou en transport en commun + Je veux profiter d'un guidage GPS adapté à mon mode de déplacement + Afin d'avoir une expérience optimisée avec tolérances appropriées + + Contexte: + Étant donné que l'application RoadWave est démarrée + Et que l'utilisateur "jean@example.com" est connecté + Et que le GPS est activé + + # 16.4 - Modes Vélo et Transport + + Plan du Scénario: Paramètres par mode de déplacement + Étant donné un audio-guide configuré en mode + Alors les paramètres suivants sont appliqués: + | paramètre | valeur | + | Rayon déclenchement | | + | Rayon tolérance "point manqué" | | + | Vitesse recommandée | | + | Seuil warning sécurité | | + + Exemples: + | mode | rayon_declenchement | rayon_tolerance | vitesse_recommandee | seuil_warning | + | Voiture | 30m | 100m | 20-50 km/h | >10 km/h | + | Vélo | 50m | 75m | 10-25 km/h | >5 km/h | + | Transport | 100m | 150m | Variable | Désactivé | + + # Mode Vélo + + Scénario: Déclenchement automatique avec rayon 50m (mode vélo) + Étant donné un audio-guide vélo "Circuit des châteaux de la Loire" + Et que le point GPS 3 a un rayon de 50m + Quand l'utilisateur à vélo entre à 45m du point + Alors la séquence 3 "Château de Chambord" se déclenche automatiquement + + Scénario: Rayon plus large justifié pour le vélo + Étant donné qu'un cycliste roule sur piste cyclable + Et que sa vitesse varie entre 8 et 22 km/h (arrêts fréquents) + Et que le tracé est moins prévisible qu'en voiture + Quand un point GPS avec rayon 50m est défini + Alors le rayon plus large compense la variabilité de trajectoire + + Scénario: Warning sécurité dès 5 km/h en vélo + Étant donné un audio-guide vélo en cours + Et que la vitesse actuelle est 12 km/h + Quand l'utilisateur clique sur [▶|] "Suivant" + Alors l'action est exécutée + Et un warning s'affiche: "⚠️ Manipulation en déplacement détecté. Pour votre sécurité, arrêtez-vous." + + Plan du Scénario: Warning vélo selon la vitesse + Étant donné que la vitesse actuelle à vélo est km/h + Quand l'utilisateur clique sur un bouton de navigation + Alors le warning est affiché : + + Exemples: + | vitesse | warning | + | 0 | Non | + | 4 | Non | + | 6 | Oui | + | 15 | Oui | + | 25 | Oui | + + Scénario: Tolérance GPS moins stricte en vélo + Étant donné qu'un cycliste passe à 65m du point GPS 4 + Et que le rayon de déclenchement est 50m + Et que le rayon de tolérance est 75m + Quand la distance est détectée + Alors la popup "Point manqué" s'affiche avec 3 options + Et le système tolère l'écart (trajectoire vélo moins prévisible) + + Scénario: Affichage adapté au vélo + Étant donné un audio-guide vélo en cours + Quand l'interface est affichée + Alors les informations suivantes sont visibles: + | information | valeur | + | Icône mode | 🚴 | + | Distance prochain point| "450 m" | + | ETA | "≈ 2 minutes" | + | Direction | ↗️ | + | Vitesse actuelle | "18 km/h" | + | Vitesse recommandée | "10-25 km/h" | + + Scénario: Cas d'usage - Piste cyclable avec arrêts fréquents + Étant donné qu'un cycliste suit un circuit nature + Et qu'il s'arrête régulièrement (feux, photos, fatigue) + Quand il s'arrête à 40m d'un point GPS (rayon 50m) + Alors la séquence se déclenche automatiquement + Et le rayon large permet le déclenchement malgré l'arrêt + + Scénario: Cas d'usage - Circulation mixte piétons/vélos + Étant donné qu'un cycliste roule sur voie partagée + Et qu'il doit ralentir fréquemment pour éviter les piétons + Quand sa vitesse varie entre 5 et 20 km/h + Alors le système s'adapte avec le rayon 50m + Et le déclenchement reste fiable + + # Mode Transport + + Scénario: Déclenchement automatique avec rayon 100m (mode transport) + Étant donné un audio-guide transport "Ligne touristique Paris" + Et que le point GPS "Tour Eiffel" a un rayon de 100m + Quand le bus touristique entre à 85m du point + Alors la séquence "Tour Eiffel" se déclenche automatiquement + + Scénario: Rayon très large justifié pour le transport + Étant donné qu'un bus touristique suit une ligne fixe + Et qu'il effectue des arrêts fréquents (stations) + Et que l'utilisateur n'a aucun contrôle sur la trajectoire + Quand un point GPS avec rayon 100m est défini + Alors le rayon large compense les arrêts et la ligne fixe + + Scénario: Pas de warning sécurité en mode transport + Étant donné un audio-guide transport en cours + Et que le bus roule à 50 km/h + Quand l'utilisateur clique sur [▶|] "Suivant" + Alors l'action est exécutée immédiatement + Et aucun warning n'est affiché + Parce que l'utilisateur est passager (pas conducteur) + + Scénario: Vitesse recommandée "Selon ligne" + Étant donné un audio-guide transport + Quand l'interface est affichée + Alors la vitesse recommandée indique "Selon ligne" + Et aucune valeur fixe n'est affichée (car ligne de transport varie) + + Scénario: Tolérance horaire pour retards + Étant donné qu'un bus touristique est en retard de 3 minutes + Et qu'il arrive au point GPS "Musée du Louvre" avec retard + Quand il entre dans le rayon de 100m + Alors la séquence se déclenche normalement + Et le système tolère le retard (pas de pénalité temporelle) + + Scénario: Tolérance spatiale très large (150m) + Étant donné qu'un bus passe à 120m du point GPS "Arc de Triomphe" + Et que le rayon de déclenchement est 100m + Et que le rayon de tolérance est 150m + Quand la distance est détectée + Alors la popup "Point manqué" s'affiche avec 3 options + + Scénario: Affichage adapté au transport + Étant donné un audio-guide transport en cours + Quand l'interface est affichée + Alors les informations suivantes sont visibles: + | information | valeur | + | Icône mode | 🚌 | + | Distance prochain point| "1.2 km" | + | ETA | "≈ 3 minutes" | + | Direction | → | + | Vitesse actuelle | "35 km/h" | + | Vitesse recommandée | "Selon ligne" | + + Scénario: Cas d'usage - Bus touristique hop-on hop-off + Étant donné un bus touristique "Paris Open Tour" + Et qu'il suit un circuit fixe avec 15 arrêts + Quand il approche de chaque arrêt + Alors la séquence correspondante se déclenche automatiquement + Et l'utilisateur n'a rien à faire (expérience passive) + + Scénario: Cas d'usage - Train panoramique + Étant donné un train touristique "Ligne des Alpes" + Et qu'il roule à vitesse variable (20-80 km/h) + Quand il passe près de points d'intérêt + Alors les séquences se déclenchent avec rayon 100m + Et le système compense la vitesse élevée + + # Comportements identiques à la voiture + + Scénario: Navigation manuelle conservée (vélo et transport) + Étant donné un audio-guide en mode + Quand l'utilisateur clique sur [▶|] ou [|◀] + Alors les boutons manuels fonctionnent normalement + Et aucune vérification GPS n'est effectuée + + Exemples: + | mode | + | Vélo | + | Transport | + + Scénario: Affichage distance + ETA + direction (tous modes) + Étant donné un audio-guide en mode + Quand l'interface est affichée + Alors les informations distance, ETA et direction sont affichées + Et le format est identique au mode voiture + + Exemples: + | mode | + | Vélo | + | Transport | + + Scénario: Gestion "Point manqué" identique + Étant donné un audio-guide en mode + Quand un point GPS est manqué (dans rayon tolérance) + Alors la popup avec 3 options s'affiche: + | option | + | 🔊 Écouter quand même | + | ⏭️ Passer au suivant | + | 🔙 Faire demi-tour | + + Exemples: + | mode | + | Vélo | + | Transport | + + # Publicités (identique tous modes) + + Plan du Scénario: Insertion publicité dans tous les modes + Étant donné un utilisateur gratuit écoute un audio-guide en mode + Quand la séquence 5 se termine (1 pub / 5 séquences) + Alors la publicité s'enchaîne automatiquement + Et elle est skippable après 5 secondes + + Exemples: + | mode | + | Voiture | + | Vélo | + | Transport | + | Piéton | + + # Cas d'erreur + + Scénario: GPS imprécis en forêt (vélo) + Étant donné un cycliste dans une forêt dense + Et que la précision GPS est ±80m + Quand il approche d'un point GPS avec rayon 50m + Alors un avertissement s'affiche: + """ + ⚠️ Signal GPS imprécis (±80m) + Le déclenchement peut être perturbé. + Utilisez les boutons manuels si nécessaire. + """ + + Scénario: Bus dévié de son itinéraire (transport) + Étant donné un bus touristique avec déviation + Et que plusieurs points GPS deviennent inaccessibles + Quand l'utilisateur est informé + Alors un message s'affiche: + """ + ⚠️ Itinéraire modifié + Certains points ne seront pas atteints. + Utilisez la navigation manuelle. + """ + + Scénario: Changement de mode en cours de route + Étant donné un audio-guide démarré en mode "Vélo" + Quand l'utilisateur décide de continuer à pied + Et qu'il ouvre les paramètres + Alors il peut changer le mode vers "Piéton" + Et les rayons sont reconfigurés automatiquement + Et une confirmation s'affiche: + """ + Mode changé : 🚶 Piéton + Navigation manuelle activée. + """ + + Scénario: Détection automatique incohérente + Étant donné qu'un utilisateur marche rapidement (7 km/h) + Et que le système détecte "Vélo" par erreur + Quand la suggestion s'affiche + Alors l'utilisateur peut cliquer sur "Changer" + Et sélectionner manuellement "Piéton" + + Scénario: Batterie en mode vélo longue distance + Étant donné un circuit vélo de 50 km avec 20 séquences + Et que l'utilisateur roule pendant 3 heures + Quand la batterie atteint 15% + Alors une notification suggère: + """ + 🔋 Batterie à 15% + Recommandé : activer mode économie d'énergie + (Désactive affichage continu distance) + [Activer] [Ignorer] + """ diff --git a/features/audio-guides/premium-monetisation.feature b/features/audio-guides/premium-monetisation.feature new file mode 100644 index 0000000..3efe8ae --- /dev/null +++ b/features/audio-guides/premium-monetisation.feature @@ -0,0 +1,307 @@ +# language: fr + +Fonctionnalité: Audio-guides Premium et monétisation + En tant que créateur + Je veux pouvoir proposer des audio-guides Premium + Afin de monétiser mon contenu de qualité + + Contexte: + Étant donné que l'application RoadWave est démarrée + Et que le créateur "guide@example.com" est connecté et vérifié + + # 16.9 - Audio-guides Premium + + Scénario: Création d'un audio-guide Premium + Étant donné que le créateur crée un audio-guide "Visite VIP Versailles" + Quand il accède aux paramètres de monétisation (étape 4) + Alors il peut choisir: + | option | description | + | Gratuit | Accessible à tous (avec pubs) | + | Premium | Réservé abonnés Premium | + + Scénario: Badge Premium visible sur l'audio-guide + Étant donné un audio-guide configuré en Premium + Quand il est affiché dans les résultats de recherche + Alors un badge "👑 Premium" est visible + Et la cover image a un cadre doré subtil + + Scénario: Preview 3 premières séquences pour utilisateurs gratuits + Étant donné un audio-guide Premium "Visite VIP Versailles" avec 15 séquences + Et qu'un utilisateur gratuit ouvre l'audio-guide + Quand il consulte la liste des séquences + Alors les séquences affichent: + | séquence | état | + | 1 | ✅ Accessible (preview) | + | 2 | ✅ Accessible (preview) | + | 3 | ✅ Accessible (preview) | + | 4 | 🔒 Réservé Premium | + | 5-15 | 🔒 Réservé Premium | + + Scénario: Écoute des 3 premières séquences sans blocage + Étant donné un utilisateur gratuit + Et un audio-guide Premium avec preview + Quand il écoute les séquences 1, 2 et 3 + Alors aucune publicité n'est insérée (preview = teasing) + Et l'écoute est fluide + + Scénario: Paywall après la 3ème séquence + Étant donné qu'un utilisateur gratuit termine la séquence 3 + Quand la séquence se termine + Alors un overlay paywall s'affiche immédiatement: + """ + 👑 Contenu réservé Premium + + Continuez cette expérience exclusive + et accédez à 12 séquences supplémentaires + + ✓ Sans publicité + ✓ Accès illimité à tous les contenus Premium + ✓ Téléchargement offline + ✓ Audio haute qualité + + [Passer Premium - 4.99€/mois] + [Découvrir d'autres audio-guides gratuits] + """ + + Scénario: Bouton "Passer Premium" vers tunnel d'abonnement + Étant donné que l'overlay paywall Premium est affiché + Quand l'utilisateur clique sur "Passer Premium" + Alors il est redirigé vers la page d'abonnement Mangopay + Et l'audio-guide actuel est marqué en "pending" (reprise après souscription) + + Scénario: Reprise automatique après souscription Premium + Étant donné qu'un utilisateur s'est abonné Premium depuis un paywall audio-guide + Quand l'abonnement est activé + Alors il est redirigé vers l'audio-guide automatiquement + Et la séquence 4 démarre immédiatement + Et un toast de bienvenue s'affiche: "✨ Bienvenue Premium ! Profitez de votre audio-guide" + + Scénario: Utilisateur Premium - Accès complet immédiat + Étant donné qu'un utilisateur Premium ouvre un audio-guide Premium + Quand il consulte la liste des séquences + Alors toutes les 15 séquences sont accessibles + Et aucun paywall ne s'affiche + Et aucune publicité n'est insérée + + Scénario: Pas de preview si l'audio-guide a <3 séquences + Étant donné un audio-guide Premium avec seulement 2 séquences + Quand un utilisateur gratuit tente de l'ouvrir + Alors un paywall s'affiche immédiatement (avant lecture) + Et aucune preview n'est disponible + + # 16.10 - Revenus créateur + + Scénario: Rémunération créateur pour audio-guide Premium + Étant donné un créateur avec un audio-guide Premium + Et que 50 utilisateurs Premium ont écouté l'audio-guide ce mois + Quand la répartition des revenus est calculée + Alors le créateur reçoit 70% des revenus proportionnels + Et la formule est: (Écoutes créateur / Total écoutes Premium) × 70% pool Premium + + Scénario: Dashboard revenus par audio-guide + Étant donné qu'un créateur a 3 audio-guides Premium publiés + Quand il consulte son dashboard revenus + Alors il voit pour chaque audio-guide: + | audio_guide | ecoutes_mois | revenus_estime | + | Visite VIP Versailles | 142 | 45.20 € | + | Secrets du Louvre | 89 | 28.50 € | + | Châteaux de la Loire | 203 | 64.80 € | + + Scénario: Comparaison gratuit vs Premium + Étant donné qu'un créateur a publié 2 audio-guides: + | titre | type | ecoutes_mois | revenus | + | Tour de Paris | Gratuit | 1200 | 12.50 € | + | Visite VIP Versailles| Premium | 142 | 45.20 € | + Quand il consulte son dashboard + Alors il peut comparer les performances + Et constater que Premium génère plus de revenus par écoute + + Scénario: Seuil minimum de paiement (20€) + Étant donné qu'un créateur a généré 18€ de revenus ce mois + Quand le paiement mensuel est traité + Alors le montant est reporté au mois suivant + Et un message s'affiche: "Seuil minimum non atteint (20€). Montant reporté." + + Scénario: Paiement automatique mensuel + Étant donné qu'un créateur a généré 138.50€ de revenus en janvier + Quand le 5 février arrive + Alors le paiement est initié automatiquement via Mangopay + Et le créateur reçoit une notification: "Paiement de 138.50€ en cours" + Et les fonds arrivent sous 2-3 jours ouvrés + + # 16.11 - Publicités dans audio-guides gratuits + + Scénario: Insertion publicité toutes les 5 séquences (gratuit) + Étant donné un audio-guide gratuit avec 12 séquences + Et un utilisateur gratuit + Quand il termine la séquence 5 + Alors une publicité démarre automatiquement + Quand il termine la séquence 10 + Alors une deuxième publicité démarre + + Scénario: Publicité après séquence en mode piéton (avec pause) + Étant donné un audio-guide piéton gratuit + Quand la séquence 5 se termine + Alors la publicité démarre automatiquement (pas d'attente bouton) + Et la pub est skippable après 5 secondes + Quand la publicité se termine + Alors le player se met en pause + Et l'utilisateur doit cliquer sur [▶|] pour continuer + + Scénario: Publicité en mode voiture/vélo/transport (automatique) + Étant donné un audio-guide voiture gratuit + Quand la séquence 5 se termine + Alors la publicité démarre automatiquement + Quand la publicité se termine + Alors la séquence 6 démarre automatiquement (pas de pause) + Parce que l'utilisateur est en conduite (mode hands-free) + + Scénario: Publicités géolocalisées dans audio-guides + Étant donné un audio-guide dans la région "Île-de-France" + Quand une publicité doit être insérée + Alors l'API publicitaire filtre par: + | critère | valeur | + | Géolocalisation | Île-de-France | + | Catégorie | Tourisme, Culture | + | Langue | Français | + + Scénario: Comptabilisation des impressions pub pour créateur + Étant donné qu'un audio-guide gratuit génère 200 écoutes complètes + Et que chaque écoute complète = 2 publicités (séq. 5 et 10) + Quand les revenus pub sont calculés + Alors 400 impressions sont comptabilisées + Et le créateur reçoit 0.80€ (400 × 0.002€) + + # 16.12 - Stratégies de conversion + + Scénario: CTA Premium après audio-guide gratuit complété + Étant donné qu'un utilisateur gratuit complète un audio-guide gratuit + Quand il termine la dernière séquence + Alors un overlay s'affiche: + """ + 🎉 Audio-guide complété ! + + Vous avez aimé cette expérience ? + Découvrez nos audio-guides Premium pour aller plus loin + + [Découvrir Premium] [Fermer] + """ + + Scénario: Recommandations d'audio-guides Premium après gratuit + Étant donné qu'un utilisateur termine un audio-guide gratuit "Tour de Paris" + Quand l'overlay de fin s'affiche + Alors 3 audio-guides Premium similaires sont suggérés: + | titre | type | créateur | + | Secrets de Montmartre | Premium | @paris_stories | + | Visite VIP Musée d'Orsay | Premium | @art_guide | + | Paris hors des sentiers | Premium | @explore_paris | + + Scénario: Badge "Premium recommandé" sur audio-guides populaires + Étant donné un audio-guide Premium avec >500 écoutes et note >4.5/5 + Quand il est affiché dans les résultats de recherche + Alors un badge "⭐ Premium recommandé" est visible + Et il est mis en avant dans les résultats + + Scénario: Conversion tracking pour attribution créateur + Étant donné qu'un utilisateur découvre Premium via un audio-guide créateur + Quand il s'abonne + Alors la conversion est trackée: + | donnée | valeur | + | source_conversion | audio_guide_paywall | + | audio_guide_id | visite_vip_versailles_123 | + | creator_id | guide_versailles_456 | + Et le créateur bénéficie d'un bonus de conversion + + # 16.13 - Offres spéciales + + Scénario: Essai gratuit 7 jours Premium via audio-guide + Étant donné qu'un utilisateur gratuit atteint le paywall d'un audio-guide Premium + Et qu'il n'a jamais essayé Premium + Quand l'overlay s'affiche + Alors une offre d'essai est proposée: + """ + 👑 Essayez Premium gratuitement pendant 7 jours + + ✓ Accès complet à cet audio-guide + ✓ Tous les contenus Premium débloqués + ✓ Sans engagement, annulable à tout moment + + [Démarrer l'essai gratuit] [Plus tard] + """ + + Scénario: Activation immédiate après essai gratuit + Étant donné qu'un utilisateur démarre un essai gratuit 7 jours + Quand l'essai est activé + Alors l'audio-guide Premium démarre immédiatement + Et toutes les séquences sont débloquées + Et aucune publicité n'est insérée + + Scénario: Rappel 2 jours avant fin d'essai + Étant donné qu'un utilisateur a démarré un essai gratuit le 15/01 + Quand le 20/01 arrive (J-2) + Alors une notification est envoyée: + """ + ⏰ Votre essai Premium se termine dans 2 jours + + Continuez à profiter de tous les audio-guides Premium + pour seulement 4.99€/mois + + [Rester Premium] [Gérer abonnement] + """ + + # Cas d'usage + + Scénario: Créateur mix gratuit + Premium + Étant donné qu'un créateur a publié 5 audio-guides: + | titre | type | + | Découverte de Paris | Gratuit | + | Visite VIP Louvre | Premium | + | Balade Montmartre | Gratuit | + | Secrets Versailles | Premium | + | Visite express Orsay | Gratuit | + Quand un utilisateur découvre son profil + Alors les audio-guides gratuits servent de teasing + Et les audio-guides Premium sont mis en avant avec badge + + Scénario: Utilisateur hésite à s'abonner + Étant donné qu'un utilisateur atteint le paywall d'un audio-guide Premium + Et qu'il clique sur "Découvrir d'autres audio-guides gratuits" + Quand il revient 2 jours plus tard sur le même audio-guide + Alors le paywall s'affiche à nouveau + Et une réduction temporaire est proposée: "Offre spéciale : -20% premier mois" + + # Cas d'erreur + + Scénario: Échec du paiement Premium via paywall + Étant donné qu'un utilisateur tente de s'abonner Premium + Quand le paiement Mangopay échoue + Alors un message d'erreur s'affiche: + """ + ❌ Paiement refusé + Vérifiez vos informations bancaires ou contactez votre banque. + [Réessayer] [Annuler] + """ + + Scénario: Abonnement Premium expiré pendant écoute + Étant donné qu'un utilisateur Premium écoute un audio-guide Premium + Et que son abonnement expire pendant l'écoute (séquence 8/15) + Quand l'expiration est détectée + Alors l'écoute continue jusqu'à la fin de la séquence en cours + Et un overlay s'affiche ensuite: + """ + ⚠️ Votre abonnement Premium a expiré + + Renouvelez pour continuer à profiter des contenus exclusifs + [Renouveler - 4.99€/mois] [Plus tard] + """ + + Scénario: Créateur change audio-guide de gratuit à Premium + Étant donné qu'un audio-guide gratuit a 50 utilisateurs avec progression + Quand le créateur le passe en Premium + Alors les utilisateurs ayant déjà commencé gardent l'accès complet + Et seuls les nouveaux utilisateurs sont soumis au paywall + Et un message de transparence s'affiche: + """ + ℹ️ Cet audio-guide est maintenant Premium + Vous conservez votre accès car vous l'aviez démarré avant le changement. + """ diff --git a/features/audio-guides/progression-sauvegarde.feature b/features/audio-guides/progression-sauvegarde.feature new file mode 100644 index 0000000..a720b81 --- /dev/null +++ b/features/audio-guides/progression-sauvegarde.feature @@ -0,0 +1,310 @@ +# language: fr + +Fonctionnalité: Sauvegarde et reprise de progression audio-guide + En tant qu'utilisateur + Je veux que ma progression soit sauvegardée automatiquement + Afin de pouvoir reprendre mon audio-guide là où je me suis arrêté + + Contexte: + Étant donné que l'application RoadWave est démarrée + Et que l'utilisateur "jean@example.com" est connecté + + # 16.5 - Sauvegarde de progression + + Scénario: Sauvegarde automatique toutes les 10 secondes + Étant donné qu'un audio-guide "Visite du Louvre" est en cours + Et que la séquence 3 est à la position 1:24 + Quand 10 secondes s'écoulent + Alors la progression est sauvegardée automatiquement: + | donnée | valeur | + | audio_guide_id | louvre_123 | + | sequence_actuelle | 3 | + | position_audio | 1:24 | + | timestamp | 2026-01-22 14:35:42 | + | sequences_ecoutees | [1, 2] | + + Scénario: Sauvegarde locale (SQLite) pour rapidité + Étant donné qu'une sauvegarde est déclenchée + Quand la progression est enregistrée + Alors les données sont écrites en SQLite local + Et l'écriture prend moins de 50ms + Et l'application reste fluide + + Scénario: Synchronisation cloud en arrière-plan + Étant donné qu'une sauvegarde locale est effectuée + Quand 30 secondes s'écoulent + Alors la progression est synchronisée vers PostgreSQL cloud + Et la synchronisation s'effectue en arrière-plan + Et elle n'impacte pas les performances + + Scénario: Sauvegarde immédiate lors de la fermeture + Étant donné qu'un audio-guide est en cours à la séquence 4 position 2:15 + Quand l'utilisateur ferme l'application + Alors la progression est sauvegardée immédiatement (local + cloud) + Et les données sont écrites avant la fermeture complète + + Scénario: Sauvegarde des séquences complétées + Étant donné qu'un audio-guide de 12 séquences est en cours + Et que les séquences 1, 2, 4, 5 ont été écoutées à >80% + Quand la progression est sauvegardée + Alors les séquences complétées sont enregistrées: + """json + { + "audio_guide_id": "louvre_123", + "completed_sequences": [1, 2, 4, 5], + "current_sequence": 6, + "current_position": "0:42", + "last_played_at": "2026-01-22T14:35:42Z" + } + """ + + Scénario: Historique des écoutes pour statistiques + Étant donné qu'un utilisateur a écouté 3 séquences d'un audio-guide + Quand les données sont sauvegardées + Alors l'historique d'écoute inclut: + | sequence_id | started_at | completed_at | completion_rate | + | 1 | 2026-01-22 14:10:00 | 2026-01-22 14:12:15 | 100% | + | 2 | 2026-01-22 14:12:20 | 2026-01-22 14:14:08 | 100% | + | 3 | 2026-01-22 14:14:15 | 2026-01-22 14:17:45 | 92% | + + # 16.6 - Reprise de progression + + Scénario: Popup de reprise au redémarrage + Étant donné que l'utilisateur a quitté l'app à la séquence 6 position 2:34 + Quand il rouvre l'audio-guide "Visite du Louvre" + Alors une popup s'affiche: + """ + Continuer où vous vous étiez arrêté ? + + 📍 Séquence 6/12 : "Vénus de Milo" + ⏱️ Position : 2:34 / 4:10 + + [▶️ Reprendre] [🔄 Recommencer] + """ + + Scénario: Action "Reprendre" - Position exacte restaurée + Étant donné qu'une popup de reprise est affichée + Quand l'utilisateur clique sur "▶️ Reprendre" + Alors la séquence 6 "Vénus de Milo" se charge + Et la position exacte 2:34 est restaurée + Et la lecture démarre automatiquement après 1 seconde + + Scénario: Action "Recommencer" - Réinitialisation complète + Étant donné qu'une popup de reprise est affichée + Quand l'utilisateur clique sur "🔄 Recommencer" + Alors l'audio-guide redémarre depuis la séquence 1 position 0:00 + Et toutes les séquences sont marquées ⭕ "À écouter" + Et l'historique d'écoute est réinitialisé pour cette session + + Scénario: Reprise après 7 jours d'inactivité + Étant donné qu'un utilisateur a arrêté un audio-guide le 15/01/2026 + Et qu'il le rouvre le 22/01/2026 (7 jours plus tard) + Quand l'audio-guide se charge + Alors la popup de reprise s'affiche normalement + Et toutes les données de progression sont conservées + + Scénario: Reprise sur un autre appareil (synchronisation cloud) + Étant donné qu'un utilisateur écoute un audio-guide sur iPhone + Et qu'il quitte à la séquence 4 position 1:20 + Quand il ouvre le même audio-guide sur iPad + Alors la popup de reprise s'affiche avec la progression iPhone + Et il peut reprendre exactement où il s'était arrêté + + Scénario: Conflit de synchronisation (dernier appareil gagne) + Étant donné qu'un utilisateur écoute sur iPhone à la séquence 3 + Et simultanément sur iPad à la séquence 7 + Quand les deux appareils synchronisent + Alors la progression la plus récente (timestamp) est conservée + Et l'appareil avec ancienne progression affiche une notification: + """ + ℹ️ Progression mise à jour + Une écoute plus récente a été détectée. + Séquence 7 restaurée. + """ + + Scénario: Mode hors-ligne - Sauvegarde locale uniquement + Étant donné qu'un utilisateur écoute un audio-guide hors connexion + Et qu'il atteint la séquence 5 + Quand la progression est sauvegardée + Alors les données sont écrites localement (SQLite) + Et une icône "☁️ Non synchronisé" s'affiche discrètement + + Scénario: Synchronisation automatique à la reconnexion + Étant donné que l'utilisateur a écouté hors ligne jusqu'à la séquence 8 + Et que 5 progressions locales ne sont pas synchronisées + Quand la connexion réseau est rétablie + Alors les 5 progressions sont synchronisées automatiquement + Et un toast s'affiche brièvement: "✅ Progression synchronisée" + + Scénario: Suppression de la progression (recommencer proprement) + Étant donné qu'un utilisateur est à la séquence 10/12 + Quand il ouvre les paramètres de l'audio-guide + Et qu'il clique sur "🔄 Réinitialiser progression" + Alors une confirmation s'affiche: + """ + Réinitialiser cet audio-guide ? + Toutes les séquences seront marquées comme non écoutées. + [Annuler] [Réinitialiser] + """ + Et si confirmé, la progression est effacée + + # 16.7 - Statistiques d'écoute + + Scénario: Taux de complétion global de l'audio-guide + Étant donné un audio-guide de 12 séquences + Et que l'utilisateur a écouté complètement 8 séquences + Et partiellement 1 séquence (45%) + Quand les statistiques sont calculées + Alors le taux de complétion affiché est "67%" (8/12) + + Scénario: Badge "Audio-guide complété" à 100% + Étant donné un audio-guide de 12 séquences + Quand l'utilisateur écoute la 12ème séquence à 100% + Alors un badge "✅ Audio-guide complété" s'affiche + Et une notification de félicitations est envoyée + Et le statut "Complété le 22/01/2026" est visible dans l'historique + + Scénario: Temps total passé sur l'audio-guide + Étant donné qu'un utilisateur a écouté un audio-guide sur 2 sessions: + | session | durée | + | 1 | 25 min | + | 2 | 18 min | + Quand les statistiques sont calculées + Alors le temps total est "43 minutes" + Et il est affiché dans l'historique personnel + + Scénario: Liste des audio-guides "En cours" dans le profil + Étant donné qu'un utilisateur a 3 audio-guides en cours: + | audio_guide | progression | + | Visite du Louvre | 6/12 | + | Safari du Paugre | 3/8 | + | Circuit Loire à Vélo | 12/15 | + Quand il consulte son profil "Audio-guides" + Alors la section "📍 En cours" affiche les 3 audio-guides + Et chaque élément montre la progression sous forme de barre + + Scénario: Liste des audio-guides "Complétés" dans le profil + Étant donné qu'un utilisateur a complété 2 audio-guides: + | audio_guide | date_completion | + | Tour de Paris | 2026-01-15 | + | Découverte de Lyon | 2026-01-20 | + Quand il consulte son profil "Audio-guides" + Alors la section "✅ Complétés" affiche les 2 audio-guides + Et la date de complétion est visible + + Scénario: Badge "Complétiste" pour 10 audio-guides complétés + Étant donné qu'un utilisateur complète son 10ème audio-guide + Quand la complétion est enregistrée + Alors un badge "🏆 Complétiste" est débloqué + Et il apparaît sur son profil + Et une notification est envoyée: + """ + 🎉 Badge débloqué : Complétiste + Vous avez complété 10 audio-guides ! + """ + + Plan du Scénario: Niveaux de badges selon nombre d'audio-guides complétés + Étant donné qu'un utilisateur complète audio-guides + Quand le badge est attribué + Alors il reçoit le badge "" + + Exemples: + | nombre | badge | + | 1 | 🎧 Premier audio-guide | + | 5 | 🗺️ Explorateur | + | 10 | 🏆 Complétiste | + | 25 | 🌟 Expert | + | 50 | 💎 Maître audio-guideur | + + # 16.8 - Métriques créateur + + Scénario: Dashboard créateur - Statistiques par audio-guide + Étant donné qu'un créateur a publié l'audio-guide "Visite du Louvre" + Quand il consulte son dashboard + Alors les métriques suivantes sont affichées: + | métrique | valeur | + | Écoutes totales | 1542 | + | Écoutes complètes (>80%) | 892 | + | Taux de complétion moyen | 58% | + | Temps d'écoute total | 423h | + | Séquence la plus écoutée | Séq. 3 | + | Séquence la moins écoutée | Séq. 11 | + + Scénario: Graphique de complétion par séquence + Étant donné un audio-guide de 12 séquences + Quand le créateur consulte les statistiques détaillées + Alors un graphique en barres affiche: + | séquence | taux_completion | + | 1 | 100% | + | 2 | 95% | + | 3 | 89% | + | ... | ... | + | 12 | 58% | + + Scénario: Détection des points d'abandon + Étant donné qu'un audio-guide a un taux de complétion de 58% + Et que 35% des utilisateurs abandonnent à la séquence 7 + Quand le créateur consulte les insights + Alors un avertissement s'affiche: + """ + ⚠️ Point d'abandon détecté + 35% des utilisateurs abandonnent à la séquence 7 "Aile Richelieu" + Durée : 8 min + Suggestion : Réduire la durée ou rendre plus captivant + """ + + Scénario: Heatmap géographique des écoutes + Étant donné un audio-guide géolocalisé + Quand le créateur consulte la heatmap + Alors une carte affiche: + | élément | description | + | Densité d'écoutes | Zones rouge/orange/jaune selon écoutes | + | Points GPS | Marqueurs sur chaque point | + | Statistiques par point | Nombre d'écoutes par zone | + + Scénario: Temps moyen par séquence + Étant donné qu'un créateur analyse son audio-guide + Quand il consulte les statistiques temporelles + Alors il voit pour chaque séquence: + | séquence | durée_audio | temps_ecoute_moyen | ecart | + | 1 | 2:15 | 2:10 | -5s | + | 2 | 1:48 | 1:30 | -18s | + | 3 | 3:42 | 3:40 | -2s | + + Scénario: Notification créateur pour milestone + Étant donné qu'un audio-guide atteint 1000 écoutes + Quand le seuil est franchi + Alors une notification est envoyée au créateur: + """ + 🎉 Félicitations ! + Votre audio-guide "Visite du Louvre" a atteint 1000 écoutes ! + Taux de complétion : 58% + """ + + # Cas d'erreur et limites + + Scénario: Corruption de données de sauvegarde + Étant donné qu'une sauvegarde locale (SQLite) est corrompue + Quand l'application tente de charger la progression + Alors une récupération depuis le cloud est tentée + Et si réussie, les données cloud sont restaurées + Et la base locale est reconstruite + + Scénario: Échec de synchronisation cloud + Étant donné que l'API cloud est indisponible + Quand une tentative de synchronisation est effectuée + Alors l'application continue avec sauvegarde locale uniquement + Et un retry automatique est programmé dans 5 minutes + Et l'icône "☁️ Non synchronisé" reste affichée + + Scénario: Suppression accidentelle de progression (récupération) + Étant donné qu'un utilisateur réinitialise un audio-guide par erreur + Quand il contacte le support dans les 7 jours + Alors l'équipe peut restaurer la progression depuis les backups + Et les données sont récupérables (backup quotidien conservé 30 jours) + + Scénario: Nettoyage automatique des vieilles progressions + Étant donné qu'une progression n'a pas été mise à jour depuis 6 mois + Quand le nettoyage automatique s'exécute + Alors la progression est archivée (mais pas supprimée) + Et l'utilisateur peut la restaurer via l'historique diff --git a/features/authentication/classification-age.feature b/features/authentication/classification-age.feature new file mode 100644 index 0000000..28da8a7 --- /dev/null +++ b/features/authentication/classification-age.feature @@ -0,0 +1,119 @@ +# language: fr +Fonctionnalité: Classification des contenus par âge + En tant que plateforme responsable + Je veux classifier les contenus par tranche d'âge + Afin de protéger les mineurs et respecter les obligations légales + + Contexte: + Étant donné que l'API RoadWave est disponible + + Scénario: Créateur doit classifier son contenu à la publication + Étant donné que je suis un créateur connecté + Quand je crée un nouveau contenu audio + Alors je dois obligatoirement choisir une classification d'âge parmi: + | classification | description | + | Tout public | Contenu adapté à tous les âges | + | 13+ | Contenu mature léger | + | 16+ | Contenu mature | + | 18+ | Contenu adulte | + + Scénario: Publication impossible sans classification + Étant donné que je crée un contenu audio + Quand j'essaie de publier sans sélectionner de classification + Alors la publication échoue + Et je vois le message "Vous devez sélectionner une classification d'âge" + + Scénario: Utilisateur 13-15 ans voit uniquement du contenu "Tout public" + Étant donné que je suis un utilisateur de 14 ans + Et qu'il existe des contenus avec les classifications suivantes: + | classification | nombre | + | Tout public | 20 | + | 13+ | 15 | + | 16+ | 10 | + | 18+ | 5 | + Quand je demande des recommandations + Alors je vois uniquement les 20 contenus "Tout public" + Et les autres contenus ne sont jamais proposés + + Scénario: Utilisateur 16-17 ans voit "Tout public" et "13+" + Étant donné que je suis un utilisateur de 17 ans + Et qu'il existe des contenus avec les classifications suivantes: + | classification | nombre | + | Tout public | 20 | + | 13+ | 15 | + | 16+ | 10 | + | 18+ | 5 | + Quand je demande des recommandations + Alors je vois 35 contenus (Tout public + 13+) + Et les contenus 16+ et 18+ ne sont pas proposés + + Scénario: Utilisateur 18+ voit tous les contenus + Étant donné que je suis un utilisateur de 25 ans + Et qu'il existe des contenus avec toutes les classifications + Quand je demande des recommandations + Alors je vois tous les contenus sans restriction + Et aucun filtre d'âge n'est appliqué + + Scénario: Mode Kids activé automatiquement pour les moins de 13 ans + Étant donné que je m'inscris avec une date de naissance "2013-01-21" + Alors le mode Kids est activé automatiquement + Et je vois uniquement du contenu "Tout public" + Et des protections supplémentaires sont appliquées + + Scénario: Modérateur reclassifie un contenu mal catégorisé + Étant donné qu'un contenu est publié avec la classification "Tout public" + Et que ce contenu contient du langage inapproprié détecté en modération + Quand le modérateur reclassifie ce contenu en "16+" + Alors la nouvelle classification est appliquée immédiatement + Et le contenu n'est plus visible pour les utilisateurs de moins de 16 ans + Et le créateur reçoit une notification de reclassification + + Scénario: Strike si classification volontairement incorrecte + Étant donné qu'un créateur a publié un contenu "18+" classifié comme "Tout public" + Et que ce contenu a été signalé + Quand le modérateur confirme la mauvaise classification volontaire + Alors le créateur reçoit 1 strike + Et le contenu est reclassifié en "18+" + Et le créateur reçoit une notification explicative + + Scénario: Créateur peut voir la distribution d'âge de son audience + Étant donné que je suis un créateur + Et que j'ai publié des contenus avec différentes classifications + Quand je consulte mes statistiques + Alors je vois la répartition des âges de mes auditeurs: + | tranche_age | pourcentage | + | 13-15 ans | 15% | + | 16-17 ans | 20% | + | 18+ ans | 65% | + + Scénario: Recherche filtrée par classification d'âge + Étant donné que je suis un utilisateur de 16 ans + Quand je recherche des contenus + Alors les résultats incluent uniquement: + | classification | + | Tout public | + | 13+ | + Et je ne vois pas les contenus 16+ et 18+ dans les résultats + + Scénario: Notification si tentative d'accès à contenu non autorisé + Étant donné que je suis un utilisateur de 14 ans + Et qu'un contenu "16+" est partagé avec moi via un lien direct + Quand j'essaie d'accéder au contenu + Alors l'accès est refusé + Et je vois le message "Ce contenu est réservé aux utilisateurs de 16 ans et plus" + + Scénario: Validation obligatoire des 3 premiers contenus inclut la classification + Étant donné que je suis un nouveau créateur + Et que je publie mon premier contenu classifié "18+" + Quand le modérateur valide mon contenu + Alors il vérifie que la classification "18+" est appropriée + Et peut la modifier si nécessaire avant validation + + Scénario: Statistiques de classification dans l'interface créateur + Étant donné que je suis un créateur + Quand je consulte mes contenus publiés + Alors je vois pour chaque contenu: + | information | exemple | + | Classification actuelle | 13+ | + | Nombre de signalements | 2 | + | Reclassifications | Aucune / 1× par modérateur | diff --git a/features/authentication/connexion.feature b/features/authentication/connexion.feature new file mode 100644 index 0000000..0616deb --- /dev/null +++ b/features/authentication/connexion.feature @@ -0,0 +1,84 @@ +# language: fr +Fonctionnalité: Connexion utilisateur + En tant qu'utilisateur existant + Je veux me connecter à mon compte + Afin d'accéder à mes contenus et paramètres + + Contexte: + Étant donné que l'API RoadWave est disponible + Et qu'un utilisateur existe avec: + | email | mot_de_passe | + | user@test.fr | Password123 | + + Scénario: Connexion réussie avec identifiants valides + Quand je me connecte avec: + | email | mot_de_passe | + | user@test.fr | Password123 | + Alors je suis connecté avec succès + Et je reçois un access token valide pour 15 minutes + Et je reçois un refresh token valide pour 30 jours + + Scénario: Connexion échouée avec email inexistant + Quand je me connecte avec l'email "inexistant@test.fr" + Alors la connexion échoue + Et je vois le message "Email ou mot de passe incorrect" + + Scénario: Connexion échouée avec mot de passe incorrect + Quand je me connecte avec: + | email | mot_de_passe | + | user@test.fr | MauvaisPass1 | + Alors la connexion échoue + Et je vois le message "Email ou mot de passe incorrect" + + Scénario: Blocage après 5 tentatives échouées + Étant donné que j'ai échoué 4 tentatives de connexion + Quand j'échoue une 5ème tentative de connexion + Alors mon compte est temporairement bloqué + Et je vois le message "Compte bloqué pour 15 minutes après 5 tentatives échouées" + Et je reçois un email de notification de blocage + + Scénario: Tentative de connexion pendant le blocage + Étant donné que mon compte est bloqué suite à 5 tentatives échouées + Et que seulement 5 minutes se sont écoulées + Quand j'essaie de me connecter avec les bons identifiants + Alors la connexion échoue + Et je vois le message "Compte bloqué. Réessayez dans 10 minutes" + + Scénario: Déblocage automatique après 15 minutes + Étant donné que mon compte est bloqué suite à 5 tentatives échouées + Et que 15 minutes se sont écoulées + Quand je me connecte avec les bons identifiants + Alors je suis connecté avec succès + Et le compteur de tentatives est réinitialisé + + Scénario: Reset du compteur après connexion réussie + Étant donné que j'ai échoué 3 tentatives de connexion + Quand je me connecte avec les bons identifiants + Alors je suis connecté avec succès + Et le compteur de tentatives est remis à 0 + + Scénario: Reset automatique du compteur après 15 minutes sans blocage + Étant donné que j'ai échoué 3 tentatives de connexion + Et que 15 minutes se sont écoulées sans nouvelle tentative + Quand je consulte mon compteur de tentatives + Alors le compteur est réinitialisé à 0 + + Scénario: Déblocage via lien "Mot de passe oublié" + Étant donné que mon compte est bloqué suite à 5 tentatives échouées + Quand j'utilise la fonction "Mot de passe oublié" + Et que je réinitialise mon mot de passe + Alors le blocage est levé immédiatement + Et je peux me connecter avec le nouveau mot de passe + + Scénario: Email de notification lors d'un blocage + Étant donné que j'ai échoué 5 tentatives de connexion + Alors je reçois un email avec: + | sujet | Tentatives de connexion suspectes détectées | + | contenu_contient | Votre compte a été temporairement bloqué | + | lien_mot_de_passe | présent | + + Scénario: Connexion multi-device simultanée autorisée + Étant donné que je suis connecté sur un appareil iOS + Quand je me connecte également sur un appareil Android + Alors les deux sessions sont actives simultanément + Et je peux utiliser l'application sur les deux appareils diff --git a/features/authentication/inscription.feature b/features/authentication/inscription.feature new file mode 100644 index 0000000..6b90006 --- /dev/null +++ b/features/authentication/inscription.feature @@ -0,0 +1,112 @@ +# language: fr +Fonctionnalité: Inscription utilisateur + En tant que nouvel utilisateur + Je veux créer un compte avec email et mot de passe + Afin d'accéder à l'application RoadWave + + Contexte: + Étant donné que l'API RoadWave est disponible + Et que Zitadel est configuré + + Scénario: Inscription réussie avec données valides + Étant donné que l'email "nouveau@example.com" n'existe pas + Quand je m'inscris avec les données suivantes: + | champ | valeur | + | email | nouveau@example.com | + | mot_de_passe | Password123 | + | pseudo | nouveau_user | + | date_naissance | 1995-06-15 | + Alors mon compte est créé avec succès + Et je reçois un email de vérification + Et le lien de vérification expire dans 7 jours + Et je suis redirigé vers l'application + + Scénario: Inscription avec email déjà existant + Étant donné qu'un utilisateur existe avec l'email "existant@example.com" + Quand je m'inscris avec l'email "existant@example.com" + Alors l'inscription échoue + Et je vois le message "Cet email est déjà utilisé" + + Scénario: Inscription avec mot de passe invalide - trop court + Quand je m'inscris avec un mot de passe de moins de 8 caractères "Pass1" + Alors l'inscription échoue + Et je vois le message "Le mot de passe doit contenir au moins 8 caractères" + + Scénario: Inscription avec mot de passe invalide - sans majuscule + Quand je m'inscris avec un mot de passe sans majuscule "password123" + Alors l'inscription échoue + Et je vois le message "Le mot de passe doit contenir au moins une majuscule" + + Scénario: Inscription avec mot de passe invalide - sans chiffre + Quand je m'inscris avec un mot de passe sans chiffre "Password" + Alors l'inscription échoue + Et je vois le message "Le mot de passe doit contenir au moins un chiffre" + + Scénario: Inscription avec pseudo invalide - trop court + Quand je m'inscris avec un pseudo de 2 caractères "ab" + Alors l'inscription échoue + Et je vois le message "Le pseudo doit contenir entre 3 et 30 caractères" + + Scénario: Inscription avec pseudo invalide - caractères spéciaux + Quand je m'inscris avec un pseudo contenant des caractères spéciaux "user@123" + Alors l'inscription échoue + Et je vois le message "Le pseudo ne peut contenir que des lettres, chiffres et underscores" + + Scénario: Inscription avec email invalide + Quand je m'inscris avec un email invalide "email.invalide" + Alors l'inscription échoue + Et je vois le message "Format d'email invalide" + + Plan du Scénario: Inscription avec âge minimum non respecté + Étant donné la date du jour est "2026-01-21" + Quand je m'inscris avec une date de naissance "" + Alors l'inscription échoue + Et je vois le message "Vous devez avoir au moins 13 ans pour créer un compte" + + Exemples: + | date_naissance | age | + | 2013-01-22 | 12 | + | 2015-06-15 | 10 | + | 2020-01-01 | 6 | + + Scénario: Inscription avec âge limite acceptable (13 ans) + Étant donné la date du jour est "2026-01-21" + Quand je m'inscris avec une date de naissance "2013-01-21" + Alors mon compte est créé avec succès + Et le mode Kids est activé automatiquement + + Scénario: Inscription avec âge supérieur à 18 ans + Étant donné la date du jour est "2026-01-21" + Quand je m'inscris avec une date de naissance "1990-06-15" + Alors mon compte est créé avec succès + Et j'ai accès à tous les contenus sans restriction d'âge + + Scénario: Données minimales requises à l'inscription + Quand je m'inscris sans fournir de nom complet + Et sans fournir de photo de profil + Et sans fournir de bio + Alors mon compte est créé avec succès + Et un avatar par défaut est généré + Et les champs optionnels sont vides + + Scénario: Renvoyer l'email de vérification + Étant donné que je me suis inscrit avec l'email "nouveau@example.com" + Et que je n'ai pas vérifié mon email + Quand je demande à renvoyer l'email de vérification + Alors un nouvel email de vérification est envoyé + Et le précédent lien est invalidé + + Scénario: Limite de renvoi d'email de vérification + Étant donné que je me suis inscrit avec l'email "nouveau@example.com" + Et que j'ai déjà renvoyé l'email de vérification 3 fois aujourd'hui + Quand je demande à renvoyer l'email de vérification une 4ème fois + Alors la demande échoue + Et je vois le message "Vous avez atteint la limite de 3 renvois par jour" + + Scénario: Expiration du lien de vérification + Étant donné que je me suis inscrit il y a 8 jours + Et que je n'ai pas vérifié mon email + Quand j'essaie d'utiliser le lien de vérification + Alors la vérification échoue + Et je vois le message "Ce lien a expiré" + Et je peux demander un nouveau lien diff --git a/features/authentication/recuperation-compte.feature b/features/authentication/recuperation-compte.feature new file mode 100644 index 0000000..133f63c --- /dev/null +++ b/features/authentication/recuperation-compte.feature @@ -0,0 +1,107 @@ +# language: fr +Fonctionnalité: Récupération de compte + En tant qu'utilisateur ayant oublié son mot de passe + Je veux pouvoir réinitialiser mon mot de passe via email + Afin de récupérer l'accès à mon compte + + Contexte: + Étant donné que l'API RoadWave est disponible + Et qu'un utilisateur existe avec l'email "user@test.fr" + + Scénario: Demander la réinitialisation du mot de passe + Quand je clique sur "Mot de passe oublié" + Et que je saisis mon email "user@test.fr" + Alors je reçois un email avec un lien de réinitialisation + Et le lien expire dans 1 heure + Et je vois le message "Email de réinitialisation envoyé" + + Scénario: Email inexistant lors de la demande de réinitialisation + Quand je demande une réinitialisation pour l'email "inexistant@test.fr" + Alors je vois le même message "Email de réinitialisation envoyé" + Mais aucun email n'est envoyé (sécurité - pas d'énumération d'emails) + + Scénario: Réinitialiser le mot de passe avec un lien valide + Étant donné que j'ai demandé une réinitialisation de mot de passe + Et que j'ai reçu le lien de réinitialisation + Quand je clique sur le lien + Et que je saisis un nouveau mot de passe "NouveauPass123" + Et que je confirme le nouveau mot de passe "NouveauPass123" + Alors mon mot de passe est modifié avec succès + Et je suis déconnecté de tous mes appareils sauf celui en cours + Et je reçois un email de confirmation de changement + + Scénario: Lien de réinitialisation expiré + Étant donné que j'ai demandé une réinitialisation il y a 2 heures + Quand j'essaie d'utiliser le lien + Alors je vois le message "Ce lien a expiré" + Et je peux demander un nouveau lien + + Scénario: Nouveau mot de passe ne respecte pas les règles + Étant donné que j'ai un lien de réinitialisation valide + Quand je saisis un nouveau mot de passe "faible" + Alors la réinitialisation échoue + Et je vois le message "Le mot de passe doit contenir au moins 8 caractères, 1 majuscule et 1 chiffre" + + Scénario: Confirmation du mot de passe ne correspond pas + Étant donné que j'ai un lien de réinitialisation valide + Quand je saisis un nouveau mot de passe "NouveauPass123" + Et que je confirme avec un mot de passe différent "AutrePass123" + Alors la réinitialisation échoue + Et je vois le message "Les mots de passe ne correspondent pas" + + Scénario: Limite de demandes de réinitialisation + Étant donné que j'ai déjà demandé 3 réinitialisations dans la dernière heure + Quand je demande une 4ème réinitialisation + Alors la demande échoue + Et je vois le message "Maximum 3 demandes par heure. Réessayez plus tard." + + Scénario: Compteur de demandes se réinitialise après 1 heure + Étant donné que j'ai demandé 3 réinitialisations + Et que 1 heure s'est écoulée + Quand je demande une nouvelle réinitialisation + Alors la demande réussit + Et je reçois un email avec un nouveau lien + + Scénario: Email de notification de changement de mot de passe + Étant donné que je viens de réinitialiser mon mot de passe + Alors je reçois un email de confirmation avec: + | sujet | Votre mot de passe a été modifié | + | contenu_contient | Votre mot de passe a été modifié | + | date_heure | présente | + | appareil | présent | + | localisation | présente | + | action_urgence | Lien si ce n'était pas vous | + + Scénario: Notification push si changement depuis appareil non reconnu + Étant donné que je me suis toujours connecté depuis mon iPhone + Et que je réinitialise mon mot de passe depuis un PC Windows + Alors je reçois une notification push sur mon iPhone avec: + | titre | Mot de passe modifié | + | message | Depuis Windows - Paris, France | + | action | Sécuriser le compte si ce n'est pas vous | + + Scénario: Déconnexion de tous les appareils après réinitialisation + Étant donné que je suis connecté sur 4 appareils différents + Et que je réinitialise mon mot de passe depuis un navigateur web + Alors les 3 autres appareils sont déconnectés immédiatement + Et seule la session du navigateur web reste active + Et je vois le message "Vous avez été déconnecté des autres appareils par sécurité" + + Scénario: Lien de réinitialisation invalide si déjà utilisé + Étant donné que j'ai réinitialisé mon mot de passe avec un lien + Quand j'essaie de réutiliser le même lien + Alors je vois le message "Ce lien a déjà été utilisé" + Et je peux demander un nouveau lien si nécessaire + + Scénario: Nouveau lien invalide l'ancien + Étant donné que j'ai demandé une réinitialisation et reçu un lien + Quand je demande une nouvelle réinitialisation + Alors l'ancien lien est invalidé + Et seul le nouveau lien fonctionne + + Scénario: Réinitialisation débloque un compte bloqué + Étant donné que mon compte est bloqué après 5 tentatives de connexion + Quand je réinitialise mon mot de passe via email + Alors le blocage est levé immédiatement + Et je peux me connecter avec le nouveau mot de passe + Et le compteur de tentatives est remis à 0 diff --git a/features/authentication/sessions-tokens.feature b/features/authentication/sessions-tokens.feature new file mode 100644 index 0000000..e23927a --- /dev/null +++ b/features/authentication/sessions-tokens.feature @@ -0,0 +1,114 @@ +# language: fr +Fonctionnalité: Gestion des sessions et tokens + En tant qu'utilisateur connecté + Je veux que mes sessions soient sécurisées et gérées automatiquement + Afin de maintenir l'accès à l'application sans friction + + Contexte: + Étant donné que l'API RoadWave est disponible + Et que je suis connecté avec succès + + Scénario: Access token expire après 15 minutes + Étant donné que j'ai reçu un access token + Et que 15 minutes se sont écoulées + Quand je fais une requête API avec cet access token + Alors la requête échoue avec le code 401 + Et je vois le message "Token expiré" + + Scénario: Refresh automatique du token avec refresh token + Étant donné que mon access token a expiré + Et que mon refresh token est valide + Quand l'application demande un nouveau access token + Alors je reçois un nouvel access token valide pour 15 minutes + Et je reçois un nouveau refresh token (rotation) + Et l'ancien refresh token est invalidé + + Scénario: Refresh token expire après 30 jours d'inactivité + Étant donné que je me suis connecté il y a 30 jours + Et que je n'ai pas utilisé l'application depuis + Quand j'essaie d'utiliser mon refresh token + Alors la requête échoue + Et je dois me reconnecter avec email/password + + Scénario: Prolongation automatique de la session si l'app est utilisée + Étant donné que je me suis connecté il y a 25 jours + Et que j'utilise l'application régulièrement + Quand je fais une requête API + Alors ma session est automatiquement prolongée + Et mon refresh token reste valide + + Scénario: Détection de token replay attack + Étant donné que j'ai rafraîchi mon token + Et que j'ai reçu un nouveau refresh token + Quand j'essaie de réutiliser l'ancien refresh token + Alors la requête échoue + Et je vois le message "Token invalide ou révoqué" + Et toutes mes sessions sont révoquées par sécurité + + Scénario: Voir la liste des appareils connectés + Étant donné que je suis connecté sur 3 appareils différents + Quand je consulte la liste de mes appareils connectés + Alors je vois 3 appareils avec les informations suivantes: + | information | exemple | + | OS | iOS 17.1 | + | Navigateur | Safari | + | Dernière connexion | Il y a 2 heures | + | Localisation | Paris, France (IP visible) | + + Scénario: Révoquer un appareil spécifique + Étant donné que je suis connecté sur mon iPhone et mon iPad + Quand je révoque la session de mon iPad depuis les paramètres + Alors la session iPad est immédiatement déconnectée + Et ma session iPhone reste active + + Scénario: Déconnecter tous les appareils sauf celui en cours + Étant donné que je suis connecté sur 4 appareils + Quand je clique sur "Déconnecter tous les appareils" + Alors les 3 autres appareils sont déconnectés + Et seul l'appareil actuel reste connecté + + Scénario: Alerte de connexion depuis nouveau device + Étant donné que je me suis toujours connecté depuis Paris + Quand je me connecte depuis un nouvel appareil à Lyon + Alors je reçois une notification push sur mes autres appareils + Et je reçois un email avec: + | sujet | Nouvelle connexion détectée | + | localisation | Lyon, France | + | appareil | Android 14 - Chrome | + | action | Lien pour révoquer la session | + + Scénario: Alerte de connexion suspecte depuis pays différent + Étant donné que je me suis toujours connecté depuis la France + Quand je me connecte depuis un appareil aux États-Unis + Alors je reçois une notification push immédiate + Et je reçois un email d'alerte de sécurité + Et la nouvelle session nécessite une validation 2FA même si désactivée + + Scénario: Déconnexion après 30 jours d'inactivité totale + Étant donné que je ne me suis pas connecté depuis 30 jours + Quand j'ouvre l'application + Alors je suis automatiquement déconnecté + Et je dois me reconnecter avec email/password + Et je vois le message "Session expirée après 30 jours d'inactivité" + + Scénario: Sessions multiples simultanées autorisées + Étant donné que je suis connecté sur: + | appareil | + | iPhone | + | iPad | + | PC Windows (Web) | + Quand je fais des actions sur les 3 appareils simultanément + Alors toutes les sessions fonctionnent sans conflit + Et chaque appareil maintient sa propre session + + Scénario: Validation de JWT via Zitadel + Étant donné que j'ai reçu un access token JWT + Quand l'API RoadWave valide le token + Alors la validation est faite localement avec la clé publique Zitadel + Et aucune requête externe n'est effectuée (performance) + Et le token contient les claims suivants: + | claim | valeur_exemple | + | sub | user-id-123 | + | email | user@test.fr | + | exp | timestamp + 15 minutes | + | iss | zitadel.roadwave.com | diff --git a/features/authentication/two-factor-authentication.feature b/features/authentication/two-factor-authentication.feature new file mode 100644 index 0000000..70f8c23 --- /dev/null +++ b/features/authentication/two-factor-authentication.feature @@ -0,0 +1,132 @@ +# language: fr +Fonctionnalité: Authentification à deux facteurs (2FA) + En tant qu'utilisateur soucieux de sécurité + Je veux activer la 2FA sur mon compte + Afin de protéger mon accès même si mon mot de passe est compromis + + Contexte: + Étant donné que l'API RoadWave est disponible + Et que je suis connecté à mon compte + + Scénario: Activer la 2FA TOTP (Time-based One-Time Password) + Étant donné que la 2FA n'est pas activée sur mon compte + Quand je choisis d'activer la 2FA TOTP + Alors je vois un QR code à scanner + Et je vois le secret partagé en texte clair (backup) + Et je dois entrer un code de vérification depuis mon app authenticator + Quand je saisis un code TOTP valide + Alors la 2FA TOTP est activée avec succès + Et je reçois des codes de backup (10 codes) + + Scénario: Connexion avec 2FA TOTP activée + Étant donné que la 2FA TOTP est activée sur mon compte + Quand je me connecte avec email/password + Alors je suis redirigé vers la page de saisie du code 2FA + Quand je saisis un code TOTP valide de mon authenticator + Alors je suis connecté avec succès + + Scénario: Connexion échouée avec code TOTP invalide + Étant donné que la 2FA TOTP est activée + Quand je me connecte avec email/password + Et que je saisis un code TOTP invalide "000000" + Alors la connexion échoue + Et je vois le message "Code d'authentification invalide" + Et je peux réessayer + + Scénario: Utiliser un code de backup pour 2FA + Étant donné que la 2FA TOTP est activée + Et que j'ai perdu l'accès à mon authenticator + Quand je me connecte avec email/password + Et que je clique sur "Utiliser un code de backup" + Et que je saisis un code de backup valide + Alors je suis connecté avec succès + Et le code de backup utilisé est invalidé + Et il me reste 9 codes de backup + + Scénario: Activer la 2FA par email + Étant donné que la 2FA n'est pas activée + Quand je choisis d'activer la 2FA par email + Alors la 2FA email est activée immédiatement + Et je vois le message "2FA email activée. Vous recevrez un code à chaque connexion" + + Scénario: Connexion avec 2FA email + Étant donné que la 2FA email est activée + Quand je me connecte avec email/password + Alors je reçois un email avec un code à 6 chiffres + Et le code expire dans 10 minutes + Et je dois saisir ce code pour terminer la connexion + + Scénario: Code 2FA email expiré + Étant donné que la 2FA email est activée + Et que je me suis connecté avec email/password + Et que j'ai reçu un code 2FA par email il y a 11 minutes + Quand je saisis ce code + Alors la connexion échoue + Et je vois le message "Code expiré. Demandez un nouveau code." + + Scénario: Renvoyer le code 2FA email + Étant donné que la 2FA email est activée + Et que je suis sur la page de saisie du code 2FA + Quand je clique sur "Renvoyer le code" + Alors je reçois un nouveau code par email + Et l'ancien code est invalidé + + Scénario: Ajouter un appareil de confiance (skip 2FA pendant 30 jours) + Étant donné que la 2FA TOTP est activée + Quand je me connecte avec email/password et code TOTP + Et que je coche "Ne plus demander sur cet appareil" + Alors je suis connecté avec succès + Et cet appareil est enregistré comme "appareil de confiance" + Quand je me reconnecte dans les 30 jours suivants sur ce même appareil + Alors je ne dois pas saisir de code 2FA + + Scénario: Appareil de confiance expire après 30 jours + Étant donné que j'ai enregistré un appareil de confiance il y a 31 jours + Quand je me connecte depuis cet appareil + Alors je dois saisir un code 2FA + Et je vois le message "Appareil de confiance expiré. Veuillez vous authentifier" + + Scénario: Voir la liste des appareils de confiance + Étant donné que j'ai enregistré 3 appareils de confiance + Quand je consulte mes paramètres de sécurité + Alors je vois la liste de mes 3 appareils de confiance avec: + | information | exemple | + | Nom | iPhone 13 - Safari | + | Date ajout | 15 janvier 2026 | + | Dernière vue | Il y a 2 heures | + | Expire le | 14 février 2026 | + + Scénario: Révoquer un appareil de confiance + Étant donné que j'ai un iPhone enregistré comme appareil de confiance + Quand je révoque cet appareil depuis les paramètres + Alors l'appareil est supprimé de la liste + Quand je me reconnecte depuis cet iPhone + Alors je dois saisir un code 2FA + + Scénario: Révoquer tous les appareils de confiance + Étant donné que j'ai 5 appareils de confiance enregistrés + Quand je clique sur "Révoquer tous les appareils de confiance" + Alors tous les appareils sont révoqués + Et je vois le message "Tous les appareils de confiance ont été révoqués" + + Scénario: 2FA forcée pour connexion suspecte malgré appareil de confiance + Étant donné que j'ai un appareil de confiance enregistré en France + Et que je me connecte depuis ce même appareil mais avec une IP américaine + Quand je tente de me connecter + Alors la 2FA est requise malgré l'appareil de confiance + Et je vois le message "Connexion suspecte détectée. Authentification requise." + + Scénario: Désactiver la 2FA + Étant donné que la 2FA TOTP est activée + Quand je désactive la 2FA depuis mes paramètres + Et que je confirme avec mon mot de passe + Alors la 2FA est désactivée + Et tous les codes de backup sont invalidés + Et tous les appareils de confiance sont révoqués + + Scénario: Régénérer les codes de backup + Étant donné que la 2FA est activée + Et que j'ai utilisé 8 codes de backup sur 10 + Quand je demande à régénérer les codes de backup + Alors je reçois 10 nouveaux codes + Et tous les anciens codes (utilisés ou non) sont invalidés diff --git a/features/authentication/verification-email.feature b/features/authentication/verification-email.feature new file mode 100644 index 0000000..da13818 --- /dev/null +++ b/features/authentication/verification-email.feature @@ -0,0 +1,79 @@ +# language: fr +Fonctionnalité: Vérification d'email + En tant qu'utilisateur inscrit + Je veux vérifier mon adresse email + Afin d'accéder à toutes les fonctionnalités selon mon rôle + + Contexte: + Étant donné que l'API RoadWave est disponible + + Scénario: Auditeur avec email non vérifié - lecture illimitée + Étant donné que je suis un auditeur avec email non vérifié + Quand j'essaie d'écouter du contenu + Alors je peux écouter tous les contenus sans limite + + Scénario: Auditeur avec email non vérifié - création limitée à 5 contenus + Étant donné que je suis un auditeur avec email non vérifié + Et que j'ai créé 4 contenus + Quand je crée un 5ème contenu + Alors le contenu est créé avec succès + Mais quand j'essaie de créer un 6ème contenu + Alors la création échoue + Et je vois le message "Vérifiez votre email pour créer plus de contenus" + + Scénario: Rappel de vérification après le 3ème contenu créé + Étant donné que je suis un auditeur avec email non vérifié + Et que j'ai créé 2 contenus + Quand je crée mon 3ème contenu + Alors le contenu est créé avec succès + Et je vois une notification in-app "Vérifiez votre email pour débloquer la création illimitée" + + Scénario: Auditeur vérifie son email + Étant donné que je suis un auditeur avec email non vérifié + Et que j'ai reçu un lien de vérification + Quand je clique sur le lien de vérification dans l'email + Alors mon email est marqué comme vérifié + Et je vois le message "Email vérifié avec succès" + Et toutes les fonctionnalités sont débloquées + + Scénario: Créateur doit vérifier son email sous 7 jours pour monétisation + Étant donné que je suis inscrit comme créateur + Et que mon email n'est pas vérifié + Et que je remplis les conditions de monétisation + Quand j'essaie d'accéder au programme de monétisation + Alors l'accès est refusé + Et je vois le message "Vérifiez votre email pour accéder à la monétisation" + + Scénario: Créateur ne peut pas publier de contenus illimités sans vérification + Étant donné que je suis un créateur avec email non vérifié + Et que j'ai créé 5 contenus + Quand j'essaie de créer un 6ème contenu + Alors la création échoue + Et je vois le message "Vérifiez votre email pour publier des contenus illimités" + + Scénario: Créateur vérifie son email et déboque tout + Étant donné que je suis un créateur avec email non vérifié + Et que j'ai reçu un lien de vérification + Quand je clique sur le lien de vérification + Alors mon email est marqué comme vérifié + Et je peux publier des contenus illimités + Et je peux accéder au programme de monétisation si j'en remplis les conditions + + Scénario: KYC impossible sans email vérifié + Étant donné que je suis un créateur avec email non vérifié + Quand j'essaie de compléter le KYC via Mangopay + Alors l'accès au KYC est refusé + Et je vois le message "Vérifiez votre email avant de procéder au KYC" + + Scénario: Tentative de vérification avec un lien déjà utilisé + Étant donné que j'ai déjà vérifié mon email avec un lien + Quand j'essaie de réutiliser le même lien de vérification + Alors la vérification échoue + Et je vois le message "Ce lien a déjà été utilisé" + + Scénario: Auditeur vérifié peut créer plus de 5 contenus + Étant donné que je suis un auditeur avec email vérifié + Et que j'ai créé 10 contenus + Quand je crée un 11ème contenu + Alors le contenu est créé avec succès + Et il n'y a pas de limite de création diff --git a/features/content-creation/metadonnees-publication.feature b/features/content-creation/metadonnees-publication.feature new file mode 100644 index 0000000..69c4a30 --- /dev/null +++ b/features/content-creation/metadonnees-publication.feature @@ -0,0 +1,232 @@ +# language: fr +Fonctionnalité: Métadonnées et publication de contenu + En tant que créateur + Je veux remplir les métadonnées de mon contenu + Afin de le publier sur RoadWave + + Contexte: + Étant donné que l'API RoadWave est disponible + Et que je suis un créateur connecté + Et que mon fichier audio est encodé et prêt + + Scénario: Publication avec toutes les métadonnées obligatoires + Quand je remplis les métadonnées suivantes: + | champ | valeur | + | Titre | Histoire de la Tour Eiffel | + | Type géo | Ancré | + | Zone | Point GPS (48.8584, 2.2945, 500m) | + | Tags | Voyage, Culture générale | + | Classification âge | Tout public | + Alors la publication réussit + Et mon contenu est soumis pour validation + + Scénario: Titre valide entre 5 et 100 caractères + Quand je saisis un titre de 50 caractères + Alors le titre est accepté + Et la validation passe + + Scénario: Titre trop court (<5 caractères) + Quand je saisis un titre de 4 caractères "Test" + Alors la publication échoue + Et je vois le message "Le titre doit contenir entre 5 et 100 caractères" + + Scénario: Titre trop long (>100 caractères) + Quand je saisis un titre de 101 caractères + Alors la publication échoue + Et je vois le message "Le titre doit contenir entre 5 et 100 caractères" + + Scénario: Titre à exactement 5 caractères accepté + Quand je saisis un titre de exactement 5 caractères "Titre" + Alors le titre est accepté + + Scénario: Titre à exactement 100 caractères accepté + Quand je saisis un titre de exactement 100 caractères + Alors le titre est accepté + + Scénario: Sélectionner type géo "Ancré" + Quand je sélectionne le type géo "Ancré" + Alors le système applique une pondération géo de 0.7 + Et je dois définir une zone de diffusion précise + + Scénario: Sélectionner type géo "Contextuel" + Quand je sélectionne le type géo "Contextuel" + Alors le système applique une pondération géo de 0.5 + Et je peux définir une zone ville/département/région + + Scénario: Sélectionner type géo "Neutre" + Quand je sélectionne le type géo "Neutre" + Alors le système applique une pondération géo de 0.2 + Et je peux définir une zone nationale + + Scénario: Zone diffusion - Point GPS avec rayon + Quand je choisis "Point GPS" + Et que je définis les coordonnées (48.8584, 2.2945) + Et que je définis un rayon de 500 mètres + Alors la zone est validée + Et le contenu sera diffusé dans un rayon de 500m autour du point + + Scénario: Zone diffusion - Rayon minimum 100m + Quand je définis un rayon de 50 mètres (< 100m) + Alors la validation échoue + Et je vois le message "Le rayon doit être entre 100m et 10km" + + Scénario: Zone diffusion - Rayon maximum 10km + Quand je définis un rayon de 15 km (> 10km) + Alors la validation échoue + Et je vois le message "Le rayon doit être entre 100m et 10km" + + Scénario: Zone diffusion - Ville depuis référentiel INSEE + Quand je choisis "Ville" + Alors je vois une liste de villes du référentiel INSEE + Quand je sélectionne "Paris (75000)" + Alors la zone est définie sur toute la ville de Paris + + Scénario: Zone diffusion - Département + Quand je choisis "Département" + Et que je sélectionne "Ille-et-Vilaine (35)" + Alors la zone couvre tout le département 35 + + Scénario: Zone diffusion - Région + Quand je choisis "Région" + Et que je sélectionne "Bretagne" + Alors la zone couvre toute la région Bretagne + + Scénario: Zone diffusion - National + Quand je choisis "National" + Alors la zone couvre toute la France + Et aucune restriction géographique n'est appliquée + + Scénario: Zones mutuellement exclusives + Étant donné que j'ai sélectionné "Point GPS" + Quand j'essaie de sélectionner également "Ville" + Alors la première sélection est remplacée + Et seule "Ville" reste active + + Scénario: Sélectionner 1 tag minimum + Quand je sélectionne 1 tag "Voyage" + Alors la validation passe + Et le contenu est tagué "Voyage" + + Scénario: Sélectionner 3 tags maximum + Quand je sélectionne 3 tags "Automobile", "Technologie", "Sport" + Alors la validation passe + Et le contenu est tagué avec les 3 tags + + Scénario: Impossible de sélectionner 0 tag + Quand j'essaie de publier sans sélectionner de tag + Alors la publication échoue + Et je vois le message "Vous devez sélectionner entre 1 et 3 tags" + + Scénario: Impossible de sélectionner 4 tags + Quand j'essaie de sélectionner 4 tags + Alors le 4ème tag ne peut pas être ajouté + Et je vois le message "Maximum 3 tags" + + Scénario: Tags disponibles dans la liste + Quand je consulte la liste des tags + Alors je vois les tags suivants: + | tag | + | Automobile | + | Voyage | + | Famille | + | Amour | + | Musique | + | Économie | + | Cryptomonnaie | + | Politique | + | Culture générale | + | Sport | + | Technologie | + | Santé | + + Scénario: Classification âge obligatoire + Quand j'essaie de publier sans classification âge + Alors la publication échoue + Et je vois le message "Vous devez sélectionner une classification d'âge" + + Plan du Scénario: Sélectionner classification âge + Quand je sélectionne la classification "" + Alors le contenu sera visible pour "" + + Exemples: + | classification | public_cible | + | Tout public | Tous les utilisateurs | + | 13+ | Utilisateurs 13 ans et plus | + | 16+ | Utilisateurs 16 ans et plus | + | 18+ | Utilisateurs 18 ans et plus | + + Scénario: Image de couverture auto-générée selon type géo + Étant donné que je choisis le type géo "Ancré" + Et que mon tag principal est "Voyage" + Quand la publication est soumise + Alors une image de couverture est générée automatiquement: + | paramètre | valeur | + | Icône | 📍 (Ancré) | + | Couleur | Bleu-vert (Voyage) | + | Format | 800×800px PNG | + + Scénario: Image de couverture type Contextuel + Étant donné que je choisis "Contextuel" + Quand l'image est générée + Alors l'icône est 🌍 (Contextuel) + + Scénario: Image de couverture type Neutre + Étant donné que je choisis "Neutre" + Quand l'image est générée + Alors l'icône est 🎧 (Neutre) + + Plan du Scénario: Couleur selon tag principal + Étant donné que mon tag principal est "" + Quand l'image est générée + Alors la couleur de fond est "" + + Exemples: + | tag | couleur | + | Automobile | Bleu | + | Voyage | Vert | + | Musique | Rouge | + | Économie | Gris | + | Sport | Orange | + + Scénario: Champs optionnels non obligatoires + Quand je publie sans description + Et sans image de couverture personnalisée + Alors la publication réussit + Et les champs optionnels restent vides + Et une image par défaut est générée + + Scénario: Temps de publication estimé 2 minutes + Étant donné que mon fichier audio est prêt + Quand je commence à remplir les métadonnées + Alors je peux publier en environ 2 minutes + Car il n'y a que 5 champs obligatoires + + Scénario: Publication rapide sans friction + Quand je publie mon premier contenu + Alors aucun champ complexe n'est demandé + Et je ne suis pas bloqué sur description ou image + Et la publication est fluide + + Scénario: Prévisualisation avant publication + Étant donné que j'ai rempli toutes les métadonnées + Quand je clique sur "Prévisualiser" + Alors je vois un aperçu de mon contenu: + | élément | affiché | + | Titre | ✅ | + | Image couverture | ✅ | + | Tags | ✅ | + | Zone diffusion | ✅ | + | Durée audio | ✅ | + | Classification | ✅ | + + Scénario: Enregistrer brouillon + Étant donné que j'ai commencé à remplir les métadonnées + Quand je clique sur "Enregistrer brouillon" + Alors mes métadonnées sont sauvegardées + Et je peux reprendre la publication plus tard + + Scénario: Reprendre brouillon + Étant donné que j'ai un brouillon sauvegardé + Quand j'accède à mes contenus + Alors je vois le brouillon avec statut "📝 Brouillon" + Et je peux reprendre la publication diff --git a/features/content-creation/modification-suppression.feature b/features/content-creation/modification-suppression.feature new file mode 100644 index 0000000..0b111eb --- /dev/null +++ b/features/content-creation/modification-suppression.feature @@ -0,0 +1,221 @@ +# language: fr +Fonctionnalité: Modification et suppression de contenu + En tant que créateur + Je veux pouvoir modifier ou supprimer mes contenus + Afin de garder le contrôle sur mes publications + + Contexte: + Étant donné que l'API RoadWave est disponible + Et que je suis un créateur connecté + Et que j'ai publié un contenu + + Scénario: Modifier le titre d'un contenu + Étant donné que mon contenu a le titre "Histoire de Paris" + Quand je modifie le titre en "Histoire fascinante de Paris" + Alors la modification est enregistrée immédiatement + Et je vois le message "Titre modifié avec succès" + + Scénario: Correction de coquilles dans le titre + Étant donné que mon titre contient une faute "Histoore de Paris" + Quand je corrige en "Histoire de Paris" + Alors la modification est acceptée + Et le titre corrigé est affiché + + Scénario: Ajouter une description ultérieurement + Étant donné que j'ai publié sans description + Quand j'ajoute une description "Découvrez l'histoire de la capitale" + Alors la description est enregistrée + Et elle est visible sur la page du contenu + + Scénario: Modifier la description existante + Étant donné que mon contenu a déjà une description + Quand je modifie la description + Alors la nouvelle description remplace l'ancienne + Et la modification est immédiate + + Scénario: Modifier les tags pour ajuster pertinence + Étant donné que mon contenu est tagué "Sport", "Musique" + Quand je change les tags en "Sport", "Santé" + Alors les nouveaux tags sont appliqués + Et l'algorithme utilise les nouveaux tags pour recommandations + + Scénario: Personnaliser l'image de couverture + Étant donné que mon contenu a une image auto-générée + Quand j'uploade une image personnalisée 800×800px + Alors l'image personnalisée remplace l'image par défaut + Et elle est visible sur le contenu + + Scénario: Impossible de modifier l'audio + Étant donné que mon contenu audio est publié + Quand j'essaie de remplacer le fichier audio + Alors la modification est refusée + Et je vois le message "L'audio ne peut pas être modifié après publication" + + Scénario: Raison - Éviter fraude audio + Étant donné que je veux changer l'audio après validation + Quand j'essaie de modifier + Alors le système refuse pour éviter: + | risque | + | Uploader contenu validé puis remplacer spam | + | Fraude sur l'intégrité du contenu | + + Scénario: Impossible de modifier la zone de diffusion + Étant donné que mon contenu est diffusé à Paris + Quand j'essaie de changer la zone en "National" + Alors la modification est refusée + Et je vois le message "La zone de diffusion ne peut pas être modifiée" + + Scénario: Raison - Éviter manipulation algorithme + Étant donné que je veux changer ma zone + Quand j'essaie de modifier + Alors le système refuse pour éviter: + | manipulation | + | Créer "Local Paris" puis changer en "National" | + | Boost artificiel de visibilité | + + Scénario: Impossible de modifier le type géo + Étant donné que mon contenu est type "Neutre" (pondération 0.2) + Quand j'essaie de changer en "Ancré" (pondération 0.7) + Alors la modification est refusée + Et je vois le message "Le type géographique ne peut pas être modifié" + + Scénario: Raison - Éviter abus de pondération + Étant donné que je veux changer le type géo + Quand j'essaie de modifier + Alors le système refuse pour éviter: + | abus | + | Créer "Neutre" puis passer en "Ancré" | + | Manipulation de la pondération algorithme | + + Scénario: Impossible de modifier la classification âge + Étant donné que mon contenu est classé "Tout public" + Quand j'essaie de changer en "18+" + Alors la modification est refusée + Et je vois le message "La classification d'âge ne peut pas être modifiée" + + Scénario: Raison - Sécurité mineurs + Étant donné que je veux changer la classification + Quand j'essaie de modifier + Alors le système refuse pour garantir: + | protection | + | Classification vérifiée en modération | + | Pas de contournement validation | + | Sécurité des mineurs | + + Scénario: Solution si besoin de changer audio/zone/classification + Étant donné que je veux absolument changer l'audio + Quand je consulte les options + Alors je vois "Supprimer et republier le contenu" + Et c'est la seule solution disponible + + Scénario: Republication après suppression - créateur <3 validations + Étant donné que je suis un nouveau créateur (2 contenus validés) + Et que je supprime puis republie un contenu + Quand je republie avec les modifications + Alors le contenu repasse en file de validation + Et une nouvelle validation est effectuée + + Scénario: Republication après suppression - créateur vérifié + Étant donné que je suis créateur vérifié (≥3 contenus validés) + Et que je supprime puis republie un contenu + Quand je republie avec les modifications + Alors le contenu est publié immédiatement + Et aucune validation préalable n'est requise + + Scénario: Suppression de contenu immédiate + Quand je clique sur "Supprimer le contenu" + Et que je confirme la suppression + Alors le contenu est supprimé immédiatement + Et disparaît de la liste publique + + Scénario: Confirmation avant suppression + Quand je clique sur "Supprimer" + Alors je vois un message de confirmation: + | titre | Êtes-vous sûr ? | + | message | Cette action est définitive | + | warning | Le contenu sera supprimé définitivement | + | actions | Confirmer / Annuler | + + Scénario: Suppression définitive et non réversible + Étant donné que j'ai supprimé un contenu + Quand j'essaie de le récupérer + Alors la récupération est impossible + Et le contenu est définitivement perdu + + Scénario: Suppression BDD + CDN sous 5 minutes + Quand je supprime un contenu + Alors l'entrée en base de données est marquée "deleted" + Et les fichiers CDN sont marqués pour suppression + Et la suppression effective a lieu sous 5 minutes + + Scénario: Historique auditeurs conservé anonymisé + Étant donné que 1000 personnes ont écouté mon contenu + Quand je supprime le contenu + Alors leur historique est conservé + Mais marqué "Contenu supprimé par créateur" + Et la durée d'écoute est conservée pour leurs stats + + Scénario: Analytics plateforme anonymisées conservées + Étant donné que mon contenu a généré 10K écoutes + Quand je supprime le contenu + Alors les métriques globales sont conservées anonymement: + | métrique | conservée | + | Total écoutes | ✅ (anonyme) | + | Durée totale | ✅ (anonyme) | + | Catégorie | ✅ (anonyme) | + | Auteur | ❌ (anonymisé) | + Et c'est conforme RGPD + + Scénario: Fichiers CDN supprimés sous 24h + Étant donné que mon contenu est supprimé + Quand 24 heures s'écoulent + Alors tous les fichiers audio sont purgés du NGINX Cache + Et l'espace de stockage est libéré + + Scénario: Pas de notification aux auditeurs + Étant donné que 500 utilisateurs ont écouté mon contenu + Quand je supprime le contenu + Alors aucune notification n'est envoyée aux auditeurs + Et il n'y a pas d'effet Streisand + + Scénario: Auditeur tente de réécouter contenu supprimé + Étant donné qu'un auditeur a écouté mon contenu + Et que j'ai supprimé ce contenu + Quand l'auditeur tente de le réécouter depuis son historique + Alors il voit le message "Ce contenu n'est plus disponible" + Et la lecture est impossible + + Scénario: Historique auditeur conserve trace + Étant donné qu'un auditeur a écouté mon contenu le 15 janvier + Et que je supprime le contenu le 20 janvier + Quand l'auditeur consulte son historique + Alors il voit "Vous avez écouté ce contenu le 15 janvier 2026" + Et le titre est remplacé par "Contenu supprimé" + Et la date d'écoute est conservée + + Scénario: Statistiques créateur après suppression + Étant donné que j'ai publié 10 contenus + Et que je supprime 2 contenus + Quand je consulte mes statistiques globales + Alors je vois: + | métrique | valeur | + | Contenus publiés | 8 (actifs) | + | Total historique | 10 | + | Suppressions | 2 | + Et l'historique des suppressions est visible + + Scénario: Limite de modifications par contenu + Étant donné que j'ai modifié un titre 10 fois + Quand j'essaie de modifier une 11ème fois + Alors la modification est acceptée + Car il n'y a pas de limite de modifications pour champs autorisés + + Scénario: Historique des modifications visible + Étant donné que j'ai modifié un contenu plusieurs fois + Quand je consulte l'historique + Alors je vois: + | date | modification | + | 21/01/2026 | Titre changé | + | 20/01/2026 | Tags modifiés | + | 19/01/2026 | Description ajoutée | + Et je peux tracer toutes les modifications diff --git a/features/content-creation/upload-encodage.feature b/features/content-creation/upload-encodage.feature new file mode 100644 index 0000000..d5e33ac --- /dev/null +++ b/features/content-creation/upload-encodage.feature @@ -0,0 +1,193 @@ +# language: fr +Fonctionnalité: Upload et encodage de contenu audio + En tant que créateur + Je veux uploader mon contenu audio + Afin qu'il soit encodé et disponible pour les auditeurs + + Contexte: + Étant donné que l'API RoadWave est disponible + Et que je suis un créateur connecté + + Scénario: Upload fichier MP3 valide + Quand j'uploade un fichier MP3 de 50 MB et 30 minutes + Alors l'upload réussit + Et le fichier est envoyé vers OVH Object Storage temporaire + Et un job d'encodage asynchrone est lancé + + Scénario: Upload fichier AAC valide (.aac) + Quand j'uploade un fichier AAC de 80 MB et 1 heure + Alors l'upload réussit + Et le fichier est accepté + Et l'encodage démarre + + Scénario: Upload fichier M4A valide + Quand j'uploade un fichier M4A de 100 MB et 2 heures + Alors l'upload réussit + Et le fichier est traité comme AAC + Et l'encodage démarre + + Scénario: Rejet fichier WAV (non supporté) + Quand j'essaie d'uploader un fichier WAV + Alors l'upload échoue + Et je vois le message "Format non supporté. Utilisez MP3 ou AAC (.mp3, .aac, .m4a)" + + Scénario: Rejet fichier FLAC (non supporté) + Quand j'essaie d'uploader un fichier FLAC + Alors l'upload échoue + Et je vois le message "Format non supporté. Utilisez MP3 ou AAC (.mp3, .aac, .m4a)" + + Scénario: Validation taille maximale 200 MB + Quand j'essaie d'uploader un fichier MP3 de 201 MB + Alors l'upload échoue + Et je vois le message "Fichier trop volumineux (max 200 MB)" + + Scénario: Upload à la limite de 200 MB accepté + Quand j'uploade un fichier MP3 de exactement 200 MB + Alors l'upload réussit + Et le fichier est accepté + + Scénario: Validation durée maximale 4 heures + Quand j'essaie d'uploader un fichier de 4h 10min + Alors l'upload échoue + Et je vois le message "Durée trop longue (max 4 heures)" + + Scénario: Upload à la limite de 4h accepté + Quand j'uploade un fichier de exactement 4 heures + Alors l'upload réussit + Et le fichier est accepté + + Scénario: Validation format côté client + Quand je sélectionne un fichier dans l'interface + Alors la validation du format est faite immédiatement côté client + Et je suis informé avant même de lancer l'upload si le format est invalide + + Scénario: Double validation côté backend + Étant donné qu'un fichier a passé la validation client + Quand le backend reçoit le fichier + Alors une validation supplémentaire est effectuée + Et le format et l'intégrité sont vérifiés + + Scénario: Pipeline d'encodage - étape 1 upload + Quand j'uploade un fichier MP3 valide + Alors le fichier est stocké temporairement dans OVH Object Storage + Et un job d'encodage est mis en file d'attente + + Scénario: Pipeline d'encodage - validation format + Étant donné qu'un job d'encodage est lancé + Quand le worker Go traite le fichier + Alors le format est validé avec FFmpeg + Et l'intégrité du fichier est vérifiée + + Scénario: Pipeline d'encodage - génération 3 profils Opus + Étant donné qu'un fichier audio est validé + Quand l'encodage démarre + Alors 3 profils Opus sont générés: + | qualité | bitrate | usage | + | Basse | 24 kbps | 2G/Edge | + | Standard | 48 kbps | 3G | + | Haute | 64 kbps | 4G/5G | + + Scénario: Pipeline d'encodage - génération segments HLS + Étant donné que les profils Opus sont générés + Quand l'encodage continue + Alors un fichier manifest .m3u8 est créé + Et des segments .ts sont générés + Et le contenu est prêt pour streaming HLS + + Scénario: Pipeline d'encodage - génération image par défaut + Étant donné que l'encodage est en cours + Quand les métadonnées sont traitées + Alors une image de couverture par défaut est générée + Et l'image fait 800×800px au format PNG + + Scénario: Pipeline d'encodage - suppression fichier original + Étant donné que l'encodage est terminé avec succès + Quand tous les fichiers de sortie sont générés + Alors le fichier original MP3/AAC est supprimé + Et seuls les profils Opus et HLS sont conservés + Et l'espace de stockage est économisé + + Scénario: Temps d'encodage contenu 5 minutes + Étant donné qu'un fichier de 5 minutes est uploadé + Quand l'encodage démarre + Alors l'encodage prend environ 30 secondes + Et je reçois une notification "Contenu prêt à publier" + + Scénario: Temps d'encodage podcast 1 heure + Étant donné qu'un fichier de 1 heure est uploadé + Quand l'encodage démarre + Alors l'encodage prend environ 5 minutes + Et une barre de progression est affichée + + Scénario: Temps d'encodage podcast 4 heures + Étant donné qu'un fichier de 4 heures est uploadé + Quand l'encodage démarre + Alors l'encodage prend environ 20 minutes + Et je peux fermer l'app (traitement asynchrone) + + Scénario: Notification "Contenu prêt à publier" + Étant donné que mon contenu est en cours d'encodage + Quand l'encodage se termine avec succès + Alors je reçois une notification push "✅ Votre contenu est prêt à publier" + Et je peux accéder à l'interface de publication + + Scénario: Échec d'encodage - fichier corrompu + Étant donné qu'un fichier MP3 corrompu est uploadé + Quand l'encodage démarre + Alors l'encodage échoue + Et je reçois une notification "❌ Erreur d'encodage: fichier corrompu" + Et le fichier temporaire est supprimé + + Scénario: Écoute accélérée - vitesses disponibles + Étant donné qu'un contenu est publié + Quand un auditeur écoute le contenu + Alors il peut choisir parmi les vitesses: + | vitesse | usage | + | 0.75x | Compréhension difficile | + | 1.0x | Normal (défaut) | + | 1.25x | Gain léger | + | 1.5x | Podcasts longs | + | 2.0x | Survol rapide | + + Scénario: Écoute accélérée pour modérateurs + Étant donné que je suis un modérateur + Et qu'un contenu de 30 secondes est à valider + Quand je l'écoute à 2.0x + Alors je termine l'écoute en 15 secondes + Et ma productivité est doublée + + Scénario: Écoute accélérée pour auditeurs + Étant donné que je suis un auditeur + Et qu'un podcast de 1 heure est disponible + Quand je configure la vitesse à 1.5x + Alors j'écoute le podcast en 40 minutes + Et je gagne 20 minutes + + Scénario: Sauvegarde préférence vitesse d'écoute + Étant donné que je configure la vitesse à 1.5x + Quand j'écoute plusieurs contenus + Alors tous les contenus sont lus à 1.5x par défaut + Et ma préférence est sauvegardée + + Scénario: Scalabilité horizontale des workers + Étant donné que 100 contenus sont uploadés simultanément + Quand les jobs d'encodage sont distribués + Alors plusieurs workers Go traitent les jobs en parallèle + Et Kubernetes scale automatiquement les pods + Et tous les contenus sont encodés sans délai excessif + + Scénario: Statut d'encodage visible + Étant donné que mon contenu est en cours d'encodage + Quand je consulte mes contenus + Alors je vois le statut: + | état | affichage | + | En attente | ⏳ File d'attente | + | En cours | ⚙️ Encodage en cours (45%) | + | Terminé | ✅ Prêt à publier | + | Échec | ❌ Erreur - Réessayer | + + Scénario: Réessayer après échec d'encodage + Étant donné que l'encodage de mon contenu a échoué + Quand je clique sur "Réessayer" + Alors un nouveau job d'encodage est lancé + Et je peux tenter à nouveau diff --git a/features/content-creation/validation-premiers-contenus.feature b/features/content-creation/validation-premiers-contenus.feature new file mode 100644 index 0000000..6f4fdc8 --- /dev/null +++ b/features/content-creation/validation-premiers-contenus.feature @@ -0,0 +1,211 @@ +# language: fr +Fonctionnalité: Validation des 3 premiers contenus + En tant que nouveau créateur + Je veux que mes 3 premiers contenus soient validés + Afin de devenir créateur vérifié + + Contexte: + Étant donné que l'API RoadWave est disponible + Et que je suis un nouveau créateur + + Scénario: Premier contenu passe en file de validation + Quand je publie mon premier contenu + Alors le contenu passe en file d'attente modération + Et je vois le message "Votre contenu est en cours de validation (24-48h)" + Et le contenu n'est pas encore visible publiquement + + Scénario: Deuxième contenu passe également en validation + Étant donné que mon premier contenu a été validé + Quand je publie mon deuxième contenu + Alors le contenu passe en file d'attente modération + Et le délai estimé est 24-48h + + Scénario: Troisième contenu - dernière validation + Étant donné que mes 2 premiers contenus ont été validés + Quand je publie mon troisième contenu + Alors le contenu passe en file d'attente modération + Et je vois "Dernière validation avant statut vérifié ✓" + + Scénario: Modérateur écoute 30 secondes du contenu + Étant donné qu'un contenu est en file de validation + Quand le modérateur junior l'examine + Alors il écoute les 30 premières secondes + Ou 15 secondes à vitesse 2x + Et il vérifie les métadonnées + + Scénario: Validation - Qualité audio acceptable + Étant donné qu'un contenu a une qualité audio claire + Quand le modérateur l'écoute + Alors il vérifie que l'audio est compréhensible + Et qu'il n'y a pas de grésillement excessif + + Scénario: Rejet - Qualité audio insuffisante + Étant donné qu'un contenu a un audio très grésillant + Quand le modérateur l'écoute + Alors le contenu est rejeté + Et la raison est "Qualité audio insuffisante" + + Scénario: Validation - Respect des règles + Étant donné qu'un contenu respecte les règles + Quand le modérateur l'examine + Alors il vérifie qu'il n'y a pas de contenu prohibé: + | type prohibé | + | Haine | + | Violence | + | Spam | + | Illégalité | + + Scénario: Rejet - Contenu haineux détecté + Étant donné qu'un contenu contient des propos haineux + Quand le modérateur l'écoute + Alors le contenu est rejeté immédiatement + Et la raison est "Contenu haineux (violation des règles)" + Et le créateur peut recevoir un strike + + Scénario: Validation - Classification âge cohérente + Étant donné qu'un contenu familial est classé "Tout public" + Quand le modérateur l'écoute + Alors il vérifie que la classification correspond au contenu + Et le contenu est accepté + + Scénario: Rejet - Classification incorrecte + Étant donné qu'un contenu adulte est classé "Tout public" + Quand le modérateur détecte l'incohérence + Alors le contenu est rejeté + Et la raison est "Classification d'âge incorrecte" + + Scénario: Validation - Tags pertinents + Étant donné qu'un contenu sur l'automobile est tagué "Automobile", "Technologie" + Quand le modérateur vérifie les tags + Alors il confirme que les tags correspondent au contenu + Et le contenu est accepté + + Scénario: Rejet - Tags non pertinents + Étant donné qu'un contenu musical est tagué "Automobile", "Sport" + Quand le modérateur détecte l'incohérence + Alors le contenu est rejeté + Et la raison est "Tags non pertinents avec le contenu" + + Scénario: Validation - Zone diffusion cohérente + Étant donné qu'un audio-guide de la Tour Eiffel est en "Point GPS" Paris + Quand le modérateur vérifie la cohérence + Alors la zone est appropriée + Et le contenu est accepté + + Scénario: Rejet - Zone incohérente + Étant donné qu'un audio-guide de la Tour Eiffel est en zone "National" + Quand le modérateur détecte l'incohérence + Alors le contenu est rejeté + Et la raison est "Zone de diffusion incohérente (devrait être Point GPS)" + + Scénario: Délai de validation 24-48h jours ouvrés + Étant donné que je publie un contenu un lundi + Quand le contenu entre en file de validation + Alors le délai estimé est 24-48h (mercredi maximum) + + Scénario: Délai étendu le weekend + Étant donné que je publie un contenu un vendredi soir + Quand le contenu entre en file de validation + Alors le délai peut atteindre 72h (lundi) + Et je vois "Validation en cours, délai 24-72h (weekend)" + + Scénario: Priorité FIFO (First In First Out) + Étant donné que 10 contenus sont en file de validation + Quand les modérateurs traitent la file + Alors les contenus sont traités dans l'ordre d'arrivée + Et pas de traitement prioritaire + + Scénario: Notification acceptation + Étant donné que mon contenu est validé et accepté + Alors je reçois un email "✅ Votre contenu '[Titre]' est en ligne !" + Et je reçois une notification push + Et je vois un lien direct vers le contenu + + Scénario: Compteur de validation + Étant donné que mon premier contenu est accepté + Alors je vois "1/3 contenus validés pour devenir créateur vérifié" + Quand mon deuxième contenu est accepté + Alors je vois "2/3 contenus validés pour devenir créateur vérifié" + + Scénario: Notification refus avec raison détaillée + Étant donné que mon contenu est rejeté + Alors je reçois un email "❌ Contenu '[Titre]' refusé" + Et je reçois une notification push + Et je vois la raison exacte: "Qualité audio insuffisante" + Et je vois un lien vers les règles de publication + + Scénario: Possibilité de correction et resoumission + Étant donné que mon contenu a été rejeté pour "Tags non pertinents" + Quand je corrige les tags + Et que je resoumets le contenu + Alors le contenu repasse en file de validation + Et une nouvelle validation est effectuée + + Scénario: Après 3 validations - Statut vérifié obtenu + Étant donné que mes 3 premiers contenus ont été validés + Alors j'obtiens le statut "Créateur Vérifié" + Et je reçois une notification "🎉 Vous êtes maintenant créateur vérifié !" + Et un badge ✓ apparaît sur mon profil + + Scénario: Badge vérifié visible publiquement + Étant donné que j'ai le statut vérifié + Quand un utilisateur consulte mon profil + Alors il voit le badge ✓ à côté de mon pseudo + Et une mention "Créateur vérifié" + + Scénario: Contenus futurs publiés immédiatement + Étant donné que je suis créateur vérifié + Quand je publie un 4ème contenu + Alors le contenu est publié immédiatement + Et il n'y a pas de validation préalable + Et je vois "✅ Contenu publié" + + Scénario: Modération a posteriori uniquement + Étant donné que je suis créateur vérifié + Et que je publie un contenu + Quand le contenu est en ligne + Alors il peut être signalé par les utilisateurs + Et sera modéré uniquement si signalé + + Scénario: Interface modérateur - Queue de contenus + Étant donné que je suis un modérateur junior + Quand j'accède à l'interface de modération + Alors je vois la file des contenus à valider + Et je vois le nombre total en attente + Et les contenus sont triés par ordre FIFO + + Scénario: Interface modérateur - Écoute accélérée + Étant donné que je suis un modérateur + Quand j'écoute un contenu de 30 secondes + Alors je peux choisir la vitesse 1.5x ou 2.0x + Et je termine l'écoute en 15 secondes à 2x + Et ma productivité est doublée + + Scénario: Interface modérateur - Raccourcis clavier + Étant donné que je modère un contenu + Quand j'utilise les raccourcis clavier + Alors je peux: + | touche | action | + | A | Accepter | + | R | Rejeter | + | Espace | Play/Pause | + Et la modération est accélérée + + Scénario: Historique créateur visible + Étant donné qu'un créateur soumet son 2ème contenu + Quand le modérateur examine le contenu + Alors il voit l'historique: + | contenu | statut | + | Contenu 1 | Validé | + | Contenu 2 | En cours| + Et il peut juger la cohérence du créateur + + Scénario: Temps de modération estimé 1.5 min/créateur + Étant donné qu'un créateur soumet 3 contenus + Quand les modérateurs traitent ces contenus + Alors le temps total est environ: + | action | temps | + | Écoute 30s × 3 | 90s | + | Vérification metadata| 15s | + | Décision | 5s | + | Total | 110s | diff --git a/features/error-handling/aucun-contenu-disponible.feature b/features/error-handling/aucun-contenu-disponible.feature new file mode 100644 index 0000000..bef5ca5 --- /dev/null +++ b/features/error-handling/aucun-contenu-disponible.feature @@ -0,0 +1,115 @@ +# language: fr + +@error-handling @no-content +Fonctionnalité: Élargissement automatique de zone quand aucun contenu n'est disponible + + Contexte: + Étant donné que je suis un utilisateur connecté + Et que la géolocalisation est activée + Et que je suis en mode écoute + + # 12.1 - Élargissement progressif + + Scénario: Aucun contenu dans rayon 50km - élargissement à 100km + Étant donné que je suis situé à la position GPS 48.8566, 2.3522 + Et qu'aucun contenu n'existe dans un rayon de 50 km autour de ma position + Mais qu'au moins 1 contenu existe dans un rayon de 100 km + Quand le système recherche du contenu à me proposer + Alors le système élargit automatiquement la zone de recherche à 100 km + Et je reçois un message "Aucun contenu dans votre zone immédiate. Voici du contenu à proximité (100 km)" + Et un contenu dans le rayon de 100 km m'est proposé + + Scénario: Aucun contenu dans rayon 100km - élargissement au département + Étant donné que je suis situé dans le département "75" (Paris) + Et qu'aucun contenu n'existe dans un rayon de 100 km autour de ma position + Mais qu'au moins 1 contenu existe avec la zone "département" pour "75" + Quand le système recherche du contenu à me proposer + Alors le système élargit automatiquement la zone de recherche au département + Et je reçois un message "Aucun contenu local disponible. Voici du contenu dans votre département" + Et un contenu départemental m'est proposé + + Scénario: Aucun contenu départemental - élargissement à la région + Étant donné que je suis situé dans la région "Île-de-France" + Et qu'aucun contenu n'existe dans un rayon de 100 km autour de ma position + Et qu'aucun contenu départemental n'existe pour mon département + Mais qu'au moins 1 contenu existe avec la zone "région" pour "Île-de-France" + Quand le système recherche du contenu à me proposer + Alors le système élargit automatiquement la zone de recherche à la région + Et je reçois un message "Aucun contenu local disponible. Voici du contenu dans votre région" + Et un contenu régional m'est proposé + + Scénario: Aucun contenu régional - basculement sur contenu national + Étant donné que je suis situé en France + Et qu'aucun contenu n'existe dans un rayon de 100 km autour de ma position + Et qu'aucun contenu départemental n'existe pour mon département + Et qu'aucun contenu régional n'existe pour ma région + Quand le système recherche du contenu à me proposer + Alors le système bascule automatiquement sur du contenu national + Et je reçois un message "Aucun contenu local disponible. Voici du contenu national qui pourrait vous intéresser" + Et un contenu national m'est proposé + Et je ne reste jamais sans contenu disponible + + Scénario: Élargissement progressif avec plusieurs étapes + Étant donné que je suis situé dans une zone rurale isolée + Et qu'aucun contenu n'existe dans un rayon de 50 km + Et qu'aucun contenu n'existe dans un rayon de 100 km + Et qu'aucun contenu départemental n'existe + Et qu'aucun contenu régional n'existe + Quand le système recherche du contenu à me proposer + Alors le système essaie d'abord 50 km + Puis essaie 100 km + Puis essaie le département + Puis essaie la région + Puis bascule sur le contenu national + Et tout ce processus se fait de manière transparente et automatique + Et je reçois le message correspondant au dernier niveau trouvé + + # Messages adaptatifs selon le niveau d'élargissement + + Scénario: Message personnalisé selon la distance trouvée + Étant donné que je suis situé à la position GPS 43.6047, 1.4442 + Et que contenu(s) est/sont trouvé(s) + Quand le système me propose du contenu + Alors je reçois le message "" + + Exemples: + | niveau_geo | message_attendu | + | 100 km | Aucun contenu dans votre zone immédiate. Voici du contenu à proximité (100 km) | + | département | Aucun contenu local disponible. Voici du contenu dans votre département | + | région | Aucun contenu local disponible. Voici du contenu dans votre région | + | national | Aucun contenu local disponible. Voici du contenu national qui pourrait vous intéresser| + + # Filet de sécurité - toujours du contenu disponible + + Scénario: Le contenu national sert de filet de sécurité + Étant donné que le système a épuisé toutes les zones géographiques locales + Quand le système bascule sur du contenu national + Alors je dois toujours avoir au moins 1 contenu disponible + Et ce contenu peut être: + | type_contenu | + | Actualités Le Monde | + | Podcasts génériques | + | Contenu éducatif national | + | Contenu culturel national | + + # Pas de message d'erreur bloquant + + Scénario: Pas d'écran d'erreur "Aucun contenu" + Étant donné que je lance l'application + Et qu'aucun contenu local n'est disponible dans ma zone + Quand le système recherche du contenu + Alors je ne dois jamais voir un message d'erreur "Aucun contenu disponible" + Et je ne dois jamais voir un écran vide + Et un contenu doit toujours m'être proposé, même si c'est du contenu national + + # Comportement avec historique d'écoute + + Scénario: Élargissement avec prise en compte des centres d'intérêt + Étant donné que je suis situé dans une zone rurale + Et qu'aucun contenu n'existe dans un rayon de 50 km + Et que mes centres d'intérêt incluent "Automobile" à 80% et "Voyage" à 70% + Et qu'un contenu national existe avec le tag "Automobile" + Et qu'un contenu national existe avec le tag "Politique" + Quand le système bascule sur du contenu national + Alors le contenu national proposé prend en compte mes centres d'intérêt + Et le contenu "Automobile" a un score supérieur au contenu "Politique" diff --git a/features/error-handling/contenu-supprime-pendant-ecoute.feature b/features/error-handling/contenu-supprime-pendant-ecoute.feature new file mode 100644 index 0000000..1769f4c --- /dev/null +++ b/features/error-handling/contenu-supprime-pendant-ecoute.feature @@ -0,0 +1,116 @@ +# language: fr + +@error-handling @content-removal +Fonctionnalité: Gestion d'un contenu supprimé pendant l'écoute + + Contexte: + Étant donné que je suis un utilisateur connecté + Et que je suis en mode écoute + Et qu'un contenu "C123" est en cours de lecture + + # 12.2 - Pas d'interruption brutale + + Scénario: Contenu supprimé pendant lecture - fin de lecture sans interruption + Étant donné que j'écoute le contenu "C123" depuis 30 secondes + Et que la durée totale du contenu est de 120 secondes + Quand le contenu est supprimé par la modération côté backend + Alors la lecture du contenu continue sans interruption + Et je peux écouter le contenu jusqu'à la fin + Et aucune interruption brutale ne se produit + + Scénario: Passage automatique après fin du contenu supprimé + Étant donné que le contenu "C123" a été supprimé pendant ma lecture + Et que j'ai écouté le contenu jusqu'à la fin + Quand le contenu se termine + Alors le système attend 2 secondes + Et passe automatiquement au contenu suivant + Et je reçois une notification toast discrète "Contenu précédent retiré (violation règles)" + + Scénario: Bouton Précédent désactivé après suppression + Étant donné que le contenu "C123" a été supprimé pendant ma lecture + Et que je suis passé au contenu suivant "C456" + Quand j'essaie d'appuyer sur le bouton "Précédent" + Alors le bouton "Précédent" ne me ramène pas au contenu supprimé + Et je reçois un message "Ce contenu n'est plus disponible" + Et la lecture du contenu actuel "C456" continue + + Scénario: Tentative de retour manuel au contenu supprimé + Étant donné que je suis sur le contenu "C456" + Et que le contenu précédent "C123" a été supprimé + Quand j'appuie sur le bouton "Précédent" pour revenir au contenu supprimé + Alors je reçois un message "Ce contenu n'est plus disponible" + Et la lecture reste sur le contenu actuel "C456" + Et aucune action n'est effectuée + + # Sécurité routière - pas d'alerte intrusive + + Scénario: Notification discrète pendant la conduite + Étant donné que je conduis à une vitesse de 60 km/h + Et que le contenu "C123" est supprimé pendant ma lecture + Quand le contenu se termine + Alors la notification "Contenu précédent retiré (violation règles)" s'affiche en toast discret + Et la notification disparaît automatiquement après 5 secondes + Et aucune popup modale n'interrompt ma conduite + Et le contenu suivant démarre automatiquement après 2 secondes + + Scénario: Message informatif mais non alarmiste + Étant donné que le contenu "C123" a été supprimé + Et que je passe au contenu suivant + Quand la notification s'affiche + Alors le message doit être informatif: "Contenu précédent retiré (violation règles)" + Et le ton ne doit pas être alarmiste + Et le message doit être bref et compréhensible + Et aucun détail technique n'est affiché pendant la conduite + + # Empêche la réécoute du contenu modéré + + Scénario: Contenu supprimé retiré de l'historique + Étant donné que le contenu "C123" a été supprimé + Quand je consulte mon historique d'écoute + Alors le contenu "C123" n'apparaît plus dans mon historique + Et je ne peux pas relancer la lecture de ce contenu + Et l'historique affiche "[Contenu retiré]" à la place du titre + + Scénario: Contenu supprimé non accessible via lien direct + Étant donné que le contenu "C123" a été supprimé + Et que j'ai un lien de partage "roadwave.fr/share/c/C123" + Quand je clique sur le lien de partage + Alors je reçois un message "Ce contenu a été retiré pour violation des règles de la communauté" + Et je suis redirigé vers l'accueil de l'application + Et aucune lecture n'est possible + + # Gestion de plusieurs contenus supprimés consécutivement + + Scénario: Plusieurs contenus supprimés dans l'historique récent + Étant donné que j'ai écouté les contenus suivants: + | id | statut | + | C123 | supprimé | + | C456 | actif | + | C789 | supprimé | + Et que je suis actuellement sur le contenu "C456" + Quand j'appuie plusieurs fois sur "Précédent" + Alors je ne peux pas revenir aux contenus "C123" ou "C789" + Et le système saute automatiquement les contenus supprimés + Et je reviens au dernier contenu actif disponible avant "C456" + + # Comportement à l'arrêt du véhicule + + Scénario: Consultation détaillée du contenu supprimé à l'arrêt + Étant donné que je suis à l'arrêt + Et que le contenu "C123" a été supprimé pendant ma session + Quand j'ouvre les détails de la notification de suppression + Alors je peux voir les informations suivantes: + | information | + | Titre du contenu | + | Créateur | + | Raison de suppression | + | Date de suppression | + Et je peux signaler une erreur de modération si je pense qu'elle est injustifiée + + Scénario: Pas d'impact sur les jauges d'intérêt lors de la suppression + Étant donné que j'ai écouté le contenu "C123" pendant 80 secondes (66%) + Et que mes jauges d'intérêt ont été mises à jour pendant l'écoute + Quand le contenu est supprimé après mon écoute + Alors les modifications de mes jauges d'intérêt sont conservées + Et l'écoute déjà effectuée reste comptabilisée + Et seules les futures écoutes de ce contenu sont bloquées diff --git a/features/error-handling/geolocalisation-desactivee.feature b/features/error-handling/geolocalisation-desactivee.feature new file mode 100644 index 0000000..9a67640 --- /dev/null +++ b/features/error-handling/geolocalisation-desactivee.feature @@ -0,0 +1,210 @@ +# language: fr + +@error-handling @gps-disabled +Fonctionnalité: Mode dégradé sans géolocalisation + + Contexte: + Étant donné que je suis un utilisateur connecté + Et que j'ai refusé ou désactivé l'accès à la géolocalisation + + # 12.4 - Mode dégradé automatique + + Scénario: Types de contenu disponibles sans géolocalisation + Étant donné que la géolocalisation est désactivée + Quand j'ouvre l'application + Alors les types de contenu suivants sont disponibles: + | type_contenu | disponible | + | Contenu national | oui | + | Contenu téléchargé (offline) | oui | + | Contenus "Neutre" géographiquement | oui | + | Contenu géolocalisé Ancré | non | + | Contenu géolocalisé Contextuel | non | + | Audio-guides | non | + | Notifications push géo-déclenchées | non | + + # Popup au premier lancement + + Scénario: Popup d'information au premier lancement sans GPS + Étant donné que c'est mon premier lancement de l'application + Et que j'ai refusé l'accès à la géolocalisation + Quand l'application détecte que le GPS est désactivé + Alors une popup s'affiche avec le message: + """ + RoadWave fonctionne mieux avec la géolocalisation activée. Sans elle, seul le contenu national sera disponible. + """ + Et la popup contient les boutons suivants: + | bouton | action | + | Activer | Redirection vers paramètres OS | + | Continuer sans | Ferme popup et lance en mode dégradé | + Et une checkbox "Ne plus me demander" est disponible + + Scénario: Popup non affichée si case "Ne plus me demander" cochée + Étant donné que j'ai déjà vu la popup de géolocalisation + Et que j'ai coché "Ne plus me demander" + Quand je lance l'application avec le GPS désactivé + Alors la popup de géolocalisation ne s'affiche pas + Et l'application démarre directement en mode dégradé + Et le banner permanent de rappel s'affiche + + Scénario: Redirection vers paramètres OS lors du clic sur "Activer" + Étant donné que la popup de géolocalisation est affichée + Quand je clique sur "Activer" + Alors je suis redirigé vers les paramètres de géolocalisation de mon OS + Et sur iOS, j'arrive dans "Réglages > RoadWave > Localisation" + Et sur Android, j'arrive dans "Paramètres > Applications > RoadWave > Autorisations > Position" + + # Banner permanent en mode dégradé + + Scénario: Banner de rappel permanent sans GPS + Étant donné que j'ai cliqué sur "Continuer sans" géolocalisation + Quand l'application s'affiche + Alors un bandeau s'affiche en haut de l'écran + Et le bandeau contient le texte: "Mode limité : géolocalisation désactivée. [Activer]" + Et le bandeau a un fond de couleur avertissement (jaune/orange) + Et le bandeau n'est pas intrusif mais reste visible + Et le bandeau reste affiché sur toutes les pages de l'application + + Scénario: Clic sur le bouton "Activer" du banner + Étant donné que le banner "Mode limité" est affiché + Quand je clique sur le lien "[Activer]" dans le banner + Alors je suis redirigé vers les paramètres de géolocalisation de mon OS + + Scénario: Disparition du banner après activation GPS + Étant donné que le banner "Mode limité" est affiché + Et que je reviens dans l'application après avoir activé le GPS dans les paramètres + Quand l'application détecte que la géolocalisation est maintenant active + Alors le banner disparaît automatiquement + Et l'application bascule en mode normal avec contenu géolocalisé + Et un toast de confirmation s'affiche: "Géolocalisation activée" + + # Contenu disponible en mode dégradé + + Scénario: Lecture de contenu national sans GPS + Étant donné que la géolocalisation est désactivée + Et que du contenu national existe (actualités Le Monde, podcasts génériques) + Quand je lance la lecture + Alors je peux écouter le contenu national sans restriction + Et l'algorithme de recommandation se base uniquement sur: + | critère | + | Mes centres d'intérêt | + | Mon historique d'écoute | + | Popularité générale | + Et la proximité géographique n'est pas prise en compte + + Scénario: Lecture de contenu téléchargé sans GPS + Étant donné que la géolocalisation est désactivée + Et que j'ai téléchargé 30 contenus quand j'avais le GPS activé + Quand j'accède à mes contenus téléchargés + Alors je peux lire tous mes contenus téléchargés normalement + Et les contenus géolocalisés téléchargés restent accessibles + Et le filtre géographique n'est pas appliqué pour les contenus offline + + Scénario: Contenu "Neutre" géographiquement disponible + Étant donné que la géolocalisation est désactivée + Et qu'un créateur a publié du contenu avec la classification géographique "Neutre" + Quand je recherche du contenu + Alors les contenus "Neutre" sont inclus dans les résultats + Et ils sont mélangés avec le contenu national + Et l'algorithme les priorise selon mes centres d'intérêt + + # Restrictions de contenu sans GPS + + Scénario: Audio-guides inaccessibles sans GPS + Étant donné que la géolocalisation est désactivée + Quand je recherche un audio-guide spécifique + Alors les audio-guides apparaissent dans les résultats de recherche + Mais un badge "GPS requis" est affiché sur chaque audio-guide + Et quand je clique sur un audio-guide, un message s'affiche: + """ + Les audio-guides nécessitent la géolocalisation pour fonctionner. Voulez-vous activer le GPS ? + """ + Et je peux choisir "Activer" ou "Annuler" + + Scénario: Notifications push géo-déclenchées désactivées + Étant donné que la géolocalisation est désactivée + Et que je suis abonné à un créateur qui diffuse du contenu géolocalisé + Quand le créateur publie un nouveau contenu géolocalisé + Alors je ne reçois pas de notification push géo-déclenchée + Mais je reçois une notification push standard (non géo-déclenchée) si le créateur publie du contenu national + Et la notification précise: "Nouveau contenu national de [Créateur]" + + Scénario: Contenu géolocalisé non proposé dans le feed + Étant donné que la géolocalisation est désactivée + Quand le système génère mon feed de contenu + Alors aucun contenu "Ancré" ou "Contextuel" n'est inclus + Et seuls les contenus "Neutre" et "National" sont proposés + Et mon feed contient au minimum 20 contenus disponibles + + # RGPD: Respect du consentement libre + + Scénario: Application fonctionnelle sans GPS (pas de blocage) + Étant donné que la géolocalisation est désactivée + Quand j'utilise l'application + Alors je ne suis jamais bloqué par un écran "GPS requis" + Et toutes les fonctionnalités non-géolocalisées restent accessibles: + | fonctionnalité | + | Écoute contenu national | + | Gestion profil | + | Abonnements créateurs | + | Recherche textuelle | + | Historique d'écoute | + | Paramètres | + | Mode offline | + Et je peux créer et publier du contenu national + + Scénario: Respect du choix utilisateur de ne pas activer GPS + Étant donné que j'ai coché "Ne plus me demander" pour la géolocalisation + Quand j'utilise l'application pendant plusieurs semaines + Alors la popup de demande GPS ne s'affiche plus jamais automatiquement + Et seul le banner permanent reste affiché + Et l'application ne force jamais l'activation du GPS + + # Réactivation GPS et bascule en mode normal + + Scénario: Bascule automatique en mode normal après activation GPS + Étant donné que j'utilise l'application en mode dégradé depuis 1 semaine + Et que je décide d'activer la géolocalisation + Quand l'application détecte que le GPS est maintenant actif + Alors le mode dégradé est désactivé automatiquement + Et le banner "Mode limité" disparaît + Et le contenu géolocalisé devient disponible immédiatement + Et mon feed se rafraîchit avec du contenu local pertinent + Et un toast de confirmation s'affiche: "Géolocalisation activée - Contenu local disponible" + + Scénario: Demande de permission GPS lors de l'utilisation d'une fonctionnalité géo + Étant donné que la géolocalisation est désactivée + Quand j'essaie d'accéder à une fonctionnalité nécessitant le GPS (ex: audio-guide) + Alors une popup contextuelle s'affiche: + """ + Cette fonctionnalité nécessite la géolocalisation. Voulez-vous l'activer ? + """ + Et je peux accepter ou refuser + Et si j'accepte, je suis redirigé vers les paramètres OS + Et si je refuse, je reste en mode dégradé sans message d'erreur répétitif + + # Incitation à activer GPS sans être intrusif + + Scénario: Statistiques de contenu local disponible non affiché + Étant donné que la géolocalisation est désactivée + Quand je navigue dans l'application + Alors le banner peut afficher occasionnellement: + """ + Mode limité : géolocalisation désactivée. Plus de 500 contenus locaux disponibles près de vous. [Activer] + """ + Et ce message incitatif change tous les 3 jours + Et il reste non intrusif (pas de popup, juste le banner) + + Scénario: Onboarding différent pour utilisateurs sans GPS + Étant donné que c'est ma première utilisation de RoadWave + Et que j'ai refusé la géolocalisation + Quand l'onboarding se termine + Alors un écran explicatif s'affiche: + """ + Vous utilisez RoadWave sans géolocalisation. Voici ce qui est disponible : + - Contenu national (actualités, podcasts) + - Contenus téléchargés + - Tous les créateurs et abonnements + + Pour profiter pleinement de RoadWave (contenu local, audio-guides), activez la géolocalisation à tout moment. + """ + Et je peux continuer avec un bouton "Compris" diff --git a/features/error-handling/perte-reseau.feature b/features/error-handling/perte-reseau.feature new file mode 100644 index 0000000..03b09f6 --- /dev/null +++ b/features/error-handling/perte-reseau.feature @@ -0,0 +1,179 @@ +# language: fr + +@error-handling @network-loss +Fonctionnalité: Gestion de la perte de réseau et buffering adaptatif + + Contexte: + Étant donné que je suis un utilisateur connecté + Et que je suis en mode écoute + Et qu'un contenu est en cours de lecture + + # 12.3 - Buffer adaptatif selon type de réseau (cf. TECHNICAL.md et ADR-002) + + Plan du Scénario: Paramètres de buffer selon le type de réseau + Étant donné que je suis connecté en "" + Quand le système initialise le buffer audio + Alors le buffer minimum est de secondes + Et le buffer cible est de secondes + Et le buffer maximum est de secondes + + Exemples: + | type_reseau | buffer_min | buffer_cible | buffer_max | + | WiFi | 5 | 30 | 120 | + | 4G | 10 | 45 | 120 | + | 5G | 10 | 45 | 120 | + | 3G | 30 | 90 | 300 | + + # Phase 1: Connexion instable + + Scénario: Connexion instable avec latence élevée - aucun message immédiat + Étant donné que je suis connecté en 4G + Et que le buffer contient 45 secondes de contenu + Quand la latence réseau dépasse 500ms + Alors aucun message n'est affiché immédiatement + Et la lecture continue normalement sur le buffer + Et le système tente de continuer le téléchargement en arrière-plan + + Scénario: Connexion instable pendant plus de 10 secondes - toast discret + Étant donné que je suis connecté en 4G + Et que la latence réseau dépasse 500ms depuis 10 secondes + Quand le système détecte la latence prolongée + Alors un toast discret s'affiche: "Connexion instable" + Et le toast disparaît automatiquement après 3 secondes + Et la lecture continue normalement + + # Phase 2: Perte totale réseau + + Scénario: Perte totale de réseau - lecture sur buffer + Étant donné que je suis connecté en WiFi + Et que le buffer contient 30 secondes de contenu + Quand je perds totalement la connexion réseau + Alors la lecture continue sur le buffer disponible + Et un toast s'affiche: "Hors ligne, lecture sur buffer (30s restantes)" + Et un compte à rebours du temps de buffer restant est visible + + Scénario: Buffer qui s'épuise pendant la perte réseau + Étant donné que je suis hors ligne + Et que le buffer contient 30 secondes de contenu + Quand le contenu continue de jouer + Alors le compte à rebours diminue en temps réel + Et le toast affiche "Hors ligne, lecture sur buffer (15s restantes)" après 15 secondes + Et le toast affiche "Hors ligne, lecture sur buffer (5s restantes)" après 25 secondes + + # Phase 3: Buffer épuisé sans reconnexion + + Scénario: Pause automatique après épuisement du buffer + Étant donné que je suis hors ligne depuis 30 secondes + Et que le buffer est complètement épuisé + Quand il n'y a plus de contenu audio à lire + Alors la lecture se met en pause automatiquement + Et un overlay s'affiche: "Connexion perdue. Reconnexion en cours..." + Et le système tente de se reconnecter automatiquement + + Scénario: Tentatives de reconnexion automatique + Étant donné que la lecture est en pause suite à l'épuisement du buffer + Quand le système tente de se reconnecter + Alors une tentative de reconnexion est effectuée toutes les 5 secondes + Et un maximum de 6 tentatives sont effectuées (30 secondes au total) + Et l'overlay affiche "Tentative de reconnexion... (X/6)" + + # Phase 4: Basculement mode offline après échec reconnexion + + Scénario: Proposition du mode offline après 30 secondes d'échec + Étant donné que 6 tentatives de reconnexion ont échoué + Et que cela fait 30 secondes que je suis déconnecté + Quand la 6ème tentative échoue + Alors une popup s'affiche: "Voulez-vous continuer avec vos contenus téléchargés ?" + Et la popup contient deux boutons: + | bouton | action | + | Réessayer | Nouvelle série de 6 tentatives | + | Mode offline | Bascule sur contenus téléchargés | + + Scénario: Basculement réussi vers le mode offline + Étant donné que la popup de mode offline est affichée + Et que j'ai téléchargé 20 contenus dans ma zone géographique + Quand je clique sur "Mode offline" + Alors le système bascule sur les contenus téléchargés + Et un nouveau contenu téléchargé démarre automatiquement + Et un bandeau permanent indique "Mode hors ligne - Contenus téléchargés" + + Scénario: Aucun contenu téléchargé disponible + Étant donné que la popup de mode offline est affichée + Et que je n'ai aucun contenu téléchargé + Quand je clique sur "Mode offline" + Alors un message s'affiche: "Aucun contenu téléchargé disponible" + Et je suis invité à me connecter en WiFi pour télécharger du contenu + Et le bouton "Réessayer" reste la seule option + + # Reconnexion réussie + + Scénario: Reprise automatique après reconnexion + Étant donné que la lecture est en pause depuis 15 secondes + Et que j'étais à 02:35 du contenu en cours + Quand la connexion réseau est rétablie + Alors la lecture reprend automatiquement au point d'arrêt exact (02:35) + Et un toast s'affiche: "Connexion rétablie" + Et le toast disparaît après 3 secondes + Et le buffer se remplit progressivement selon le type de réseau + + Scénario: Reconnexion avec changement de type de réseau + Étant donné que j'étais connecté en WiFi + Et que j'ai perdu la connexion + Quand je me reconnecte en 4G + Alors le système ajuste automatiquement les paramètres de buffer + Et le buffer minimum passe de 5s à 10s + Et le buffer cible passe de 30s à 45s + Et la lecture reprend normalement + + # Cas spécifique: tunnel routier + + Scénario: Passage dans un tunnel avec perte de signal + Étant donné que je conduis à 90 km/h sur autoroute + Et que je suis connecté en 4G avec un buffer de 45 secondes + Quand j'entre dans un tunnel et perds le signal + Alors la lecture continue sur le buffer pendant 45 secondes maximum + Et aucune notification n'est affichée pendant les 10 premières secondes + Et un toast discret s'affiche après 10 secondes: "Connexion instable" + + Scénario: Sortie du tunnel avant épuisement du buffer + Étant donné que je suis dans un tunnel depuis 30 secondes + Et qu'il reste 15 secondes de buffer + Quand je sors du tunnel et récupère le signal 4G + Alors la lecture continue sans interruption + Et le buffer se remplit à nouveau + Et un toast s'affiche: "Connexion rétablie" + + # Handoff réseau (changement de cellule mobile) + + Scénario: Changement de cellule 4G pendant la lecture + Étant donné que je conduis et change de cellule mobile toutes les 5-10 minutes + Et que le buffer contient 45 secondes de contenu + Quand un handoff de cellule se produit + Alors la lecture continue sans interruption grâce au buffer + Et la connexion à la nouvelle cellule se fait de manière transparente + Et aucune notification n'est affichée si le handoff réussit en moins de 5 secondes + + # Mode offline préventif avant perte réseau + + Scénario: Téléchargement préventif en WiFi avant trajet + Étant donné que je suis connecté en WiFi + Et que j'ai activé le téléchargement automatique + Quand le système détecte que je suis à l'arrêt en WiFi + Alors le système me propose de télécharger du contenu pour mon trajet + Et je peux sélectionner une zone géographique à télécharger + Et le téléchargement se fait en arrière-plan + + # Métriques et monitoring + + Scénario: Tracking des événements de perte réseau pour amélioration + Étant donné que je perds la connexion réseau + Quand l'événement de perte est détecté + Alors le système enregistre les métriques suivantes: + | métrique | + | Type de réseau avant perte | + | Durée de la coupure | + | Buffer disponible | + | Position GPS approximative | + | Heure de la journée | + Et ces métriques sont anonymisées et envoyées en batch lors de la prochaine connexion WiFi + Et les données servent à améliorer les paramètres de buffer diff --git a/features/interest-gauges/degradation-temporelle.feature b/features/interest-gauges/degradation-temporelle.feature new file mode 100644 index 0000000..e572496 --- /dev/null +++ b/features/interest-gauges/degradation-temporelle.feature @@ -0,0 +1,142 @@ +# language: fr +Fonctionnalité: Pas de dégradation temporelle des jauges + En tant que système de recommandation + Je veux que les jauges n'évoluent que par les actions utilisateur + Afin d'avoir un comportement prévisible et fiable + + Contexte: + Étant donné que l'API RoadWave est disponible + Et qu'un utilisateur est connecté + + Scénario: Aucune dégradation automatique avec le temps + Étant donné que ma jauge "Économie" est à 80% + Et que je n'écoute aucun contenu pendant 30 jours + Quand je me reconnecte après 30 jours + Alors ma jauge "Économie" est toujours à 80% + Et aucune dégradation temporelle n'a été appliquée + + Scénario: Jauges conservées après 6 mois d'inactivité + Étant donné que mes jauges sont: + | catégorie | niveau | + | Automobile | 75% | + | Voyage | 60% | + | Musique | 45% | + Et que je pars en vacances pendant 6 mois sans utiliser l'app + Quand je me reconnecte après 6 mois + Alors mes jauges sont exactement les mêmes: + | catégorie | niveau | + | Automobile | 75% | + | Voyage | 60% | + | Musique | 45% | + + Scénario: Évolution naturelle par les actions + Étant donné que j'aimais "Économie" il y a 1 an (jauge 80%) + Et que depuis, je skip tous les contenus "Économie" + Et que j'ai skippé 50 contenus "Économie" en 1 an + Alors ma jauge "Économie" descend naturellement via les skips + Et atteint environ 55% (80% - 50 × 0.5% = 55%) + Et la dégradation vient des actions, pas du temps + + Scénario: Pas de cron job de dégradation + Étant donné que le système vérifie les jauges quotidiennement + Quand un utilisateur n'a pas d'activité depuis 90 jours + Alors aucun job de dégradation n'est exécuté + Et les jauges restent inchangées + Et aucune ressource CPU n'est consommée pour la dégradation + + Scénario: Comportement prévisible après absence + Étant donné que ma jauge "Sport" était à 70% + Et que je n'utilise pas l'app pendant 1 an + Quand je reviens et demande des recommandations + Alors mes recommandations reflètent toujours mes goûts d'avant + Et je reçois du contenu "Sport" prioritaire + Et le comportement est cohérent et prévisible + + Scénario: Réinitialiser manuellement mes centres d'intérêt + Étant donné que je veux repartir de zéro + Quand je vais dans les paramètres + Et que je clique sur "Réinitialiser mes centres d'intérêt" + Et que je confirme l'action + Alors toutes mes jauges reviennent à 50% + Et je vois le message "Vos centres d'intérêt ont été réinitialisés" + + Scénario: Confirmation avant réinitialisation + Étant donné que je suis dans les paramètres + Quand je clique sur "Réinitialiser mes centres d'intérêt" + Alors je vois un message de confirmation: + | titre | Êtes-vous sûr ? | + | message | Cette action remettra toutes vos jauges à 50% | + | actions | Confirmer / Annuler | + + Scénario: Annuler la réinitialisation + Étant donné que j'ai cliqué sur "Réinitialiser mes centres d'intérêt" + Et que la confirmation est affichée + Quand je clique sur "Annuler" + Alors mes jauges ne sont pas modifiées + Et je reviens aux paramètres + + Scénario: Raison de réinitialisation - changement de vie + Étant donné que j'utilisais RoadWave pour mes trajets professionnels + Et que mes jauges reflétaient "Économie" (85%) et "Technologie" (75%) + Et que je change de vie et deviens musicien + Quand je réinitialise mes centres d'intérêt + Alors je peux repartir avec toutes les jauges à 50% + Et découvrir du contenu "Musique" et "Culture" sans biais + + Scénario: Pas de suggestion automatique de réinitialisation + Étant donné que je n'ai pas utilisé l'app depuis 1 an + Quand je me reconnecte + Alors aucune suggestion de réinitialisation n'est affichée + Et mes jauges sont conservées telles quelles + Et je garde le contrôle total + + Scénario: Historique conservé après réinitialisation + Étant donné que j'ai écouté 500 contenus + Quand je réinitialise mes centres d'intérêt + Alors mes jauges reviennent à 50% + Mais mon historique d'écoute est conservé + Et je peux toujours consulter mes anciens contenus écoutés + + Scénario: Évolution future basée sur nouvelles actions + Étant donné que j'ai réinitialisé mes jauges à 50% + Quand j'écoute 5 contenus "Voyage" à >80% + Alors ma jauge "Voyage" monte à 60% (50% + 5 × 2%) + Et l'algorithme recommence à apprendre mes nouvelles préférences + + Scénario: Respect de l'historique utilisateur + Étant donné qu'un utilisateur aime "Cryptomonnaie" depuis 2 ans + Et que sa jauge est à 90% + Quand 2 ans s'écoulent sans dégradation temporelle + Alors sa jauge reste à 90% + Car l'historique de ses goûts est respecté + Et le système ne fait pas d'"oubli" artificiel + + Scénario: Coût infrastructure zéro + Étant donné qu'aucune dégradation temporelle n'existe + Quand le système calcule les jauges + Alors aucun calcul de date n'est nécessaire + Et aucun batch nocturne ne tourne + Et aucun bug de fuseau horaire ne peut survenir + Et le coût CPU est minimal + + Scénario: UX prévisible - jauge = actions + Étant donné qu'un utilisateur consulte sa jauge "Sport" à 65% + Quand il se demande pourquoi elle est à 65% + Alors il peut retracer ses actions: + | action | impact | + | 10 likes automatiques | +10% | + | 3 abonnements Sport | +15% | + | 5 skips de contenu non-Sport| 0% | + Et il comprend que c'est le reflet exact de ses actions + Et il n'y a pas de mystère ou automatisme caché + + Scénario: Statistiques affichées sans date + Étant donné que je consulte mes centres d'intérêt + Quand je vois mes jauges + Alors je vois: + | information | affiché | + | Niveau actuel | ✅ 75% | + | Évolution depuis début | ✅ +25% | + | Dernière mise à jour | ❌ | + Et aucune date n'est affichée car non pertinente + Et seules les actions comptent diff --git a/features/interest-gauges/evolution-jauges.feature b/features/interest-gauges/evolution-jauges.feature new file mode 100644 index 0000000..1f26cd9 --- /dev/null +++ b/features/interest-gauges/evolution-jauges.feature @@ -0,0 +1,193 @@ +# language: fr +Fonctionnalité: Évolution des jauges d'intérêt + En tant que système de recommandation + Je veux faire évoluer les jauges d'intérêt selon les actions utilisateur + Afin d'affiner les recommandations personnalisées + + Contexte: + Étant donné que l'API RoadWave est disponible + Et qu'un utilisateur est connecté + + Scénario: Like automatique renforcé après écoute ≥80% + Étant donné qu'un contenu de 5 minutes est tagué "Automobile" + Et que ma jauge "Automobile" est à 45% + Quand j'écoute le contenu pendant 4 minutes 30 secondes (90%) + Alors je reçois un like automatique renforcé + Et ma jauge "Automobile" augmente de 2% + Et ma jauge "Automobile" est maintenant à 47% + + Scénario: Like automatique renforcé exactement à 80% + Étant donné qu'un contenu de 10 minutes est tagué "Voyage" + Et que ma jauge "Voyage" est à 60% + Quand j'écoute le contenu pendant exactement 8 minutes (80%) + Alors je reçois un like automatique renforcé + Et ma jauge "Voyage" augmente de 2% + Et ma jauge "Voyage" est maintenant à 62% + + Scénario: Like automatique standard après écoute 30-79% + Étant donné qu'un contenu de 5 minutes est tagué "Automobile" + Et que ma jauge "Automobile" est à 45% + Quand j'écoute le contenu pendant 2 minutes 30 secondes (50%) + Alors je reçois un like automatique standard + Et ma jauge "Automobile" augmente de 1% + Et ma jauge "Automobile" est maintenant à 46% + + Scénario: Like automatique standard à 30% exactement + Étant donné qu'un contenu de 10 minutes est tagué "Musique" + Et que ma jauge "Musique" est à 40% + Quand j'écoute le contenu pendant exactement 3 minutes (30%) + Alors je reçois un like automatique standard + Et ma jauge "Musique" augmente de 1% + + Scénario: Like automatique standard à 79% + Étant donné qu'un contenu de 10 minutes est tagué "Sport" + Et que ma jauge "Sport" est à 55% + Quand j'écoute le contenu pendant 7 minutes 54 secondes (79%) + Alors je reçois un like automatique standard + Et ma jauge "Sport" augmente de 1% + Et ma jauge "Sport" est maintenant à 56% + + Scénario: Like explicite (manuel) +2% + Étant donné qu'un contenu est tagué "Économie" + Et que ma jauge "Économie" est à 70% + Quand j'écoute le contenu partiellement + Et que je clique manuellement sur le bouton "Like" + Alors ma jauge "Économie" augmente de 2% + Et ma jauge "Économie" est maintenant à 72% + + Scénario: Like manuel cumulable avec like automatique + Étant donné qu'un contenu de 5 minutes est tagué "Automobile" + Et que ma jauge "Automobile" est à 45% + Quand j'écoute le contenu pendant 2 minutes 30 secondes (50%) + Alors je reçois un like automatique standard (+1%) + Quand je clique ensuite sur le bouton "Like" + Alors ma jauge augmente encore de 2% (like manuel) + Et ma jauge "Automobile" a augmenté de 3% au total + Et ma jauge "Automobile" est maintenant à 48% + + Scénario: Abonnement créateur impacte tous ses tags + Étant donné qu'un créateur publie des contenus tagués "Automobile" et "Technologie" + Et que mes jauges sont: + | catégorie | niveau | + | Automobile | 50% | + | Technologie | 45% | + Quand je m'abonne à ce créateur + Alors ma jauge "Automobile" augmente de 5% + Et ma jauge "Technologie" augmente de 5% + Et mes nouvelles jauges sont: + | catégorie | niveau | + | Automobile | 55% | + | Technologie | 50% | + + Scénario: Skip rapide (<10s) diminue la jauge + Étant donné qu'un contenu est tagué "Économie" + Et que ma jauge "Économie" est à 45% + Quand je skip le contenu après 5 secondes + Alors ma jauge "Économie" diminue de 0.5% + Et ma jauge "Économie" est maintenant à 44.5% + + Scénario: Skip à exactement 10s ne diminue pas la jauge + Étant donné qu'un contenu est tagué "Politique" + Et que ma jauge "Politique" est à 50% + Quand je skip le contenu après exactement 10 secondes + Alors ma jauge "Politique" ne change pas + Et reste à 50% + + Scénario: Skip tardif (≥30%) est neutre + Étant donné qu'un contenu de 10 minutes est tagué "Musique" + Et que ma jauge "Musique" est à 60% + Quand j'écoute pendant 3 minutes (30%) + Et que je skip ensuite + Alors ma jauge "Musique" ne diminue pas (signal neutre) + Et ma jauge reste à 60% (plus le +1% de like auto si applicable) + + Scénario: Contenu avec plusieurs tags impacte toutes les jauges + Étant donné qu'un contenu est tagué "Automobile" et "Voyage" + Et que mes jauges sont: + | catégorie | niveau | + | Automobile | 45% | + | Voyage | 60% | + Quand j'écoute le contenu à 90% + Alors les deux jauges augmentent de 2% + Et mes nouvelles jauges sont: + | catégorie | niveau | + | Automobile | 47% | + | Voyage | 62% | + + Scénario: Contenu avec 3 tags impacte les 3 jauges + Étant donné qu'un contenu est tagué "Sport", "Santé" et "Technologie" + Et que mes jauges sont à 50% pour chaque catégorie + Quand je skip rapidement après 5 secondes + Alors les 3 jauges diminuent de 0.5% + Et toutes passent à 49.5% + + Scénario: Jauges bornées - ne peut pas dépasser 100% + Étant donné que ma jauge "Cryptomonnaie" est à 99% + Et qu'un contenu tagué "Cryptomonnaie" est disponible + Quand j'écoute le contenu à 95% (like auto renforcé +2%) + Alors ma jauge "Cryptomonnaie" passe à 100% (maximum) + Et ne dépasse pas 100% + + Scénario: Jauges bornées - ne peut pas descendre sous 0% + Étant donné que ma jauge "Politique" est à 0.3% + Et qu'un contenu tagué "Politique" est disponible + Quand je skip rapidement après 3 secondes (-0.5%) + Alors ma jauge "Politique" passe à 0% (minimum) + Et ne devient pas négative + + Scénario: Calcul immédiat à chaque action + Étant donné que ma jauge "Voyage" est à 50% + Quand j'écoute un contenu "Voyage" à 85% + Alors la jauge est mise à jour immédiatement (pas de batch) + Et passe à 52% + Quand je demande mes recommandations dans la seconde suivante + Alors l'algorithme utilise déjà la valeur 52% + + Scénario: Like manuel après écoute <30% (pas de like auto) + Étant donné qu'un contenu de 10 minutes est tagué "Culture" + Et que ma jauge "Culture" est à 60% + Quand j'écoute pendant 2 minutes (20%) + Alors je ne reçois pas de like automatique + Quand je clique sur le bouton "Like" + Alors ma jauge "Culture" augmente de 2% uniquement + Et ma jauge "Culture" est maintenant à 62% + + Scénario: Unlike retire le like manuel + Étant donné que j'ai liké manuellement un contenu "Sport" + Et que ma jauge "Sport" est passée de 55% à 57% (+2%) + Quand je clique sur "Unlike" + Alors ma jauge "Sport" diminue de 2% + Et ma jauge "Sport" revient à 55% + + Scénario: Unlike ne peut pas retirer un like automatique + Étant donné que j'ai écouté un contenu "Musique" à 90% + Et que j'ai reçu un like automatique renforcé (+2%) + Et que ma jauge "Musique" est à 52% + Quand j'essaie de faire "Unlike" + Alors l'action n'est pas disponible + Et ma jauge reste à 52% + Car les likes automatiques ne peuvent pas être retirés + + Scénario: Tags définis par créateur à la publication + Étant donné que je suis un créateur + Quand je publie un contenu + Alors je dois sélectionner 1 à 3 tags + Et ces tags sont fixés après publication + Et impacteront les jauges de tous les auditeurs + + Plan du Scénario: Calculs avec différentes durées d'écoute + Étant donné qu'un contenu de 10 minutes est tagué "Voyage" + Et que ma jauge "Voyage" est à 50% + Quand j'écoute pendant () + Alors ma jauge évolue de + Et ma nouvelle jauge est à + + Exemples: + | duree | pourcentage | impact | nouveau_niveau | + | 1 min | 10% | 0% | 50% | + | 3 min | 30% | +1% | 51% | + | 5 min | 50% | +1% | 51% | + | 7.9 min | 79% | +1% | 51% | + | 8 min | 80% | +2% | 52% | + | 9.5 min | 95% | +2% | 52% | + | 5 sec | <1% | -0.5% | 49.5% | diff --git a/features/interest-gauges/jauge-initiale.feature b/features/interest-gauges/jauge-initiale.feature new file mode 100644 index 0000000..11a69d6 --- /dev/null +++ b/features/interest-gauges/jauge-initiale.feature @@ -0,0 +1,147 @@ +# language: fr +Fonctionnalité: Jauge initiale et cold start + En tant que nouvel utilisateur + Je veux que mes jauges d'intérêt démarrent de manière neutre + Afin de découvrir du contenu sans biais initial + + Contexte: + Étant donné que l'API RoadWave est disponible + + Scénario: Inscription - toutes les jauges à 50% + Quand je m'inscris sur RoadWave + Alors toutes mes jauges d'intérêt sont initialisées à 50% + Et je ne dois pas remplir de questionnaire + Et l'inscription est ultra-rapide + + Scénario: Liste des catégories disponibles + Étant donné que je suis un nouvel utilisateur + Quand je consulte mes centres d'intérêt + Alors je vois les catégories suivantes à 50%: + | catégorie | + | Automobile | + | Voyage | + | Famille | + | Amour | + | Musique | + | Économie | + | Cryptomonnaie | + | Politique | + | Culture générale | + | Sport | + | Technologie | + | Santé | + + Scénario: Cold start - premier contenu écouté + Étant donné que je viens de m'inscrire + Et que toutes mes jauges sont à 50% + Quand j'écoute mon premier podcast "Automobile" à 90% + Alors ma jauge "Automobile" monte à 52% (+2%) + Et toutes les autres jauges restent à 50% + + Scénario: Cold start - premier skip + Étant donné que je viens de m'inscrire + Et que toutes mes jauges sont à 50% + Quand je skip rapidement un contenu "Économie" + Alors ma jauge "Économie" descend à 49.5% (-0.5%) + Et toutes les autres jauges restent à 50% + + Scénario: Après 10 écoutes, profil commence à se dessiner + Étant donné que je suis un nouvel utilisateur + Et que j'ai écouté: + | contenu | tags | completion | + | Contenu 1 | Automobile | 90% | + | Contenu 2 | Automobile, Sport | 85% | + | Contenu 3 | Voyage | 75% | + | Contenu 4 | Économie | skip 5s | + | Contenu 5 | Automobile | 95% | + | Contenu 6 | Sport | 80% | + | Contenu 7 | Politique | skip 8s | + | Contenu 8 | Voyage | 88% | + | Contenu 9 | Automobile | 92% | + | Contenu 10 | Technologie | 40% | + Alors mes jauges reflètent mes préférences: + | catégorie | tendance | + | Automobile | Forte hausse (>55%) | + | Voyage | Hausse modérée (~53%) | + | Sport | Hausse modérée (~53%) | + | Économie | Baisse légère (~49.5%) | + | Politique | Baisse légère (~49.5%) | + | Technologie | Neutre (~51%) | + + Scénario: Pas de questionnaire onboarding par défaut + Quand je termine l'inscription + Alors aucun questionnaire de centres d'intérêt n'est affiché + Et je peux commencer à écouter immédiatement + Et l'algorithme apprend naturellement + + Scénario: Algorithme avec jauges à 50% - chances égales + Étant donné que toutes mes jauges sont à 50% + Quand l'algorithme calcule les recommandations + Alors tous les types de contenus ont une chance égale + Et aucun biais initial n'est appliqué + Et la géolocalisation prime sur les intérêts + + Scénario: Questionnaire optionnel après 3 écoutes (post-MVP) + Étant donné que j'ai écouté 3 contenus + Quand je termine ma 3ème écoute + Alors je vois une notification in-app optionnelle: + | titre | Améliorez vos recommandations | + | message | Sélectionnez vos centres d'intérêt | + | actions | Configurer maintenant / Plus tard | + + Scénario: Remplir le questionnaire optionnel (post-MVP) + Étant donné que le questionnaire optionnel est affiché + Quand je sélectionne les centres d'intérêt suivants: + | catégorie | + | Automobile | + | Voyage | + | Sport | + Alors les jauges sélectionnées passent à 70% + Et les jauges non sélectionnées passent à 30% + Et je vois le message "Vos préférences ont été enregistrées" + + Scénario: Skipper le questionnaire optionnel (post-MVP) + Étant donné que le questionnaire optionnel est affiché + Quand je clique sur "Plus tard" + Alors toutes mes jauges conservent 50% + Et l'algorithme continue d'apprendre naturellement + Et je ne suis plus sollicité + + Scénario: Comportement déterministe et testable + Étant donné deux nouveaux utilisateurs A et B + Quand les deux s'inscrivent au même moment + Alors leurs jauges sont identiques (toutes à 50%) + Et leurs recommandations initiales sont identiques (basées sur géo uniquement) + + Scénario: Équité entre créateurs au cold start + Étant donné qu'un nouvel utilisateur s'inscrit + Et qu'il existe 1000 contenus de catégories variées dans sa zone + Quand l'algorithme génère les premières recommandations + Alors tous les contenus ont une pondération intérêts identique (50%) + Et seuls la géolocalisation et l'engagement différencient les contenus + Et aucun créateur n'a d'avantage initial + + Scénario: Catégories extensibles + Étant donné que RoadWave ajoute une nouvelle catégorie "Gastronomie" + Quand je consulte mes centres d'intérêt + Alors je vois la nouvelle catégorie "Gastronomie" à 50% + Et je peux commencer à l'explorer normalement + + Scénario: Voir l'évolution de mes jauges + Étant donné que je suis un utilisateur avec historique + Quand je consulte mes centres d'intérêt dans les paramètres + Alors je vois mes jauges actuelles: + | catégorie | niveau | evolution | + | Automobile | 67% | +17% | + | Voyage | 82% | +32% | + | Économie | 34% | -16% | + | Sport | 50% | 0% | + Et je comprends mes préférences actuelles + + Scénario: Friction zéro à l'inscription + Étant donné que je veux m'inscrire rapidement + Quand je remplis les 4 champs obligatoires + Et que je clique sur "S'inscrire" + Alors mon compte est créé immédiatement + Et je peux commencer à écouter dans les 30 secondes + Et aucune configuration supplémentaire n'est requise diff --git a/features/mode-offline/synchronisation-actions.feature b/features/mode-offline/synchronisation-actions.feature new file mode 100644 index 0000000..416a6dd --- /dev/null +++ b/features/mode-offline/synchronisation-actions.feature @@ -0,0 +1,425 @@ +# language: fr +Fonctionnalité: Synchronisation actions offline + En tant qu'utilisateur + Je veux que mes actions offline soient synchronisées quand je me reconnecte + Afin de ne perdre aucune interaction même sans connexion + + Contexte: + Étant donné que j'utilise l'application RoadWave + + # ===== ACTIONS STOCKÉES LOCALEMENT ===== + + Scénario: Like d'un contenu en mode offline + Étant donné que je n'ai aucune connexion Internet + Quand je like un contenu téléchargé + Alors l'action est enregistrée localement dans SQLite: + ```sql + INSERT INTO pending_actions (type, content_id, created_at) + VALUES ('like', 'abc123', '2025-06-15 14:30:00'); + ``` + Et l'UI affiche immédiatement le like (optimistic update) + + Scénario: Unlike d'un contenu en mode offline + Étant donné que je n'ai aucune connexion Internet + Et que j'avais liké un contenu + Quand je retire mon like + Alors l'action est enregistrée localement: + ```sql + INSERT INTO pending_actions (type, content_id, created_at) + VALUES ('unlike', 'abc123', '2025-06-15 14:35:00'); + ``` + Et l'UI retire immédiatement le like + + Scénario: Abonnement à un créateur en mode offline + Étant donné que je n'ai aucune connexion Internet + Quand je m'abonne à un créateur + Alors l'action est enregistrée localement: + ```sql + INSERT INTO pending_actions (type, creator_id, created_at) + VALUES ('subscribe', 'creator456', '2025-06-15 14:40:00'); + ``` + Et l'UI affiche immédiatement "Abonné ✓" + + Scénario: Désabonnement d'un créateur en mode offline + Étant donné que je n'ai aucune connexion Internet + Et que j'étais abonné à un créateur + Quand je me désabonne + Alors l'action est enregistrée localement: + ```sql + INSERT INTO pending_actions (type, creator_id, created_at) + VALUES ('unsubscribe', 'creator456', '2025-06-15 14:45:00'); + ``` + Et l'UI affiche "S'abonner" + + Scénario: Signalement d'un contenu en mode offline + Étant donné que je n'ai aucune connexion Internet + Quand je signale un contenu pour "Contenu inapproprié" + Alors l'action est enregistrée localement: + ```sql + INSERT INTO pending_actions (type, content_id, reason, created_at) + VALUES ('report', 'abc123', 'Contenu inapproprié', '2025-06-15 14:50:00'); + ``` + Et je vois "Signalement enregistré. Sera envoyé à la reconnexion." + + Scénario: Progression audio-guide en mode offline + Étant donné que je n'ai aucune connexion Internet + Et que j'écoute un audio-guide multi-séquences + Quand je termine la séquence 3/10 + Alors la progression est enregistrée localement: + ```sql + INSERT INTO pending_actions (type, guide_id, sequence_id, created_at) + VALUES ('guide_progress', 'guide789', 'seq003', '2025-06-15 15:00:00'); + ``` + Et ma progression est sauvegardée + + Scénario: Multiple actions offline stockées en queue + Étant donné que je n'ai aucune connexion Internet pendant 2 jours + Quand j'effectue plusieurs actions: + | action | cible | + | like | contenu A | + | like | contenu B | + | subscribe | créateur X | + | unlike | contenu C | + | report | contenu D | + Alors les 5 actions sont stockées dans pending_actions + Et elles seront synchronisées dans l'ordre à la reconnexion + + # ===== SYNCHRONISATION AUTOMATIQUE ===== + + Scénario: Détection reconnexion Internet + Étant donné que j'étais en mode offline + Quand l'app détecte une reconnexion Internet + Alors le processus de synchronisation démarre automatiquement + Et je vois une notification "Synchronisation en cours..." + + Scénario: Récupération queue locale pendant sync + Étant donné que la synchronisation démarre + Quand l'app récupère les actions en attente + Alors une requête SQL est exécutée: + ```sql + SELECT * FROM pending_actions ORDER BY created_at ASC; + ``` + Et toutes les actions sont récupérées dans l'ordre chronologique + + Scénario: Envoi batch API des actions + Étant donné que 15 actions sont en attente + Quand le batch est envoyé au backend + Alors une requête POST /sync/actions est faite: + ```json + { + "actions": [ + {"type": "like", "content_id": "abc123", "timestamp": "2025-06-15T14:30:00Z"}, + {"type": "subscribe", "creator_id": "creator456", "timestamp": "2025-06-15T14:40:00Z"}, + {"type": "unlike", "content_id": "def789", "timestamp": "2025-06-15T14:50:00Z"}, + ... + ] + } + ``` + Et toutes les actions sont groupées en une seule requête + + Scénario: Backend traite chaque action + Étant donné que le backend reçoit le batch d'actions + Quand il traite chaque action + Alors pour chaque action: + | étape | détail | + | Validation | Vérifier user_id, content_id valides | + | Vérification existence | Contenu/créateur existe toujours ? | + | Application action | INSERT/UPDATE/DELETE en base | + | Mise à jour compteurs | Likes, abonnés, etc. | + | Impact sur algorithme | Mise à jour jauges si nécessaire | + + Scénario: Confirmation réception et suppression queue locale + Étant donné que le backend a traité toutes les actions avec succès + Quand la confirmation est reçue par l'app + Alors les actions sont supprimées de la queue locale: + ```sql + DELETE FROM pending_actions WHERE id IN (1, 2, 3, ..., 15); + ``` + Et la table pending_actions est vidée + + Scénario: Toast confirmation synchronisation + Étant donné que 15 actions ont été synchronisées + Quand la synchronisation se termine + Alors je vois un toast: + """ + ✅ Synchronisation réussie + + 3 likes, 1 abonnement et 1 signalement synchronisés. + """ + + Scénario: Synchronisation silencieuse si peu d'actions + Étant donné que j'ai seulement 2 actions en attente + Quand la synchronisation se termine + Alors aucun toast n'est affiché (sync silencieuse) + Et l'expérience reste fluide + Mais je peux voir le détail dans l'historique des syncs + + # ===== GESTION ERREURS SYNC ===== + + Scénario: Échec synchronisation - Retry automatique + Étant donné que la synchronisation échoue (erreur réseau) + Quand l'échec est détecté + Alors un retry automatique est programmé dans 30 secondes + Et les actions restent dans pending_actions + + Scénario: 3 tentatives échouées - Notification utilisateur + Étant donné que 3 tentatives de synchronisation ont échoué + Quand la 3ème tentative échoue + Alors je reçois une notification: + """ + ⚠️ Impossible de synchroniser vos actions + + 15 actions en attente de synchronisation. + Vérifiez votre connexion et réessayez. + + [Réessayer maintenant] [Plus tard] + ``` + + Scénario: Actions conservées jusqu'à sync réussie + Étant donné que la synchronisation échoue plusieurs fois + Quand les tentatives continuent d'échouer + Alors les actions restent dans pending_actions + Et aucune action n'est perdue + Et elles seront envoyées dès que la connexion sera stable + + Scénario: Rétention max 7 jours - Purge automatique + Étant donné qu'une action est en attente depuis 7 jours + Quand le système détecte cette ancienneté + Alors l'action est automatiquement supprimée de la queue + Et je vois "1 action trop ancienne supprimée (>7 jours)" + Et cela évite une queue infinie + + Scénario: Justification rétention 7 jours + Étant donné qu'un utilisateur ne se connecte jamais pendant 2 semaines + Quand ses actions ont >7 jours + Alors elles sont purgées automatiquement + Car après 7 jours, l'action perd sa pertinence + Et évite une queue qui grandit indéfiniment + + Scénario: Retry manuel après échec + Étant donné que la synchronisation a échoué + Quand je clique sur "Réessayer maintenant" + Alors une nouvelle tentative de synchronisation est lancée immédiatement + Et si elle réussit, les actions sont synchronisées + + # ===== CONFLITS CONTENUS SUPPRIMÉS ===== + + Scénario: Backend retourne contenus supprimés + Étant donné que j'ai liké un contenu offline + Mais que le contenu a été supprimé entre temps + Quand le backend traite la synchronisation + Alors il retourne: + ```json + { + "status": "partial_success", + "deleted_content_ids": [123, 456], + "failed_actions": [ + {"type": "like", "content_id": "123", "reason": "content_deleted"} + ] + } + ``` + + Scénario: App supprime fichiers locaux contenus supprimés + Étant donné que le backend retourne deleted_content_ids: [123, 456] + Quand l'app traite la réponse + Alors elle supprime les fichiers locaux des contenus 123 et 456 + Et libère l'espace disque + Et les actions associées sont retirées de la queue + + Scénario: Contenu supprimé en cours d'écoute + Étant donné que j'écoute le contenu 123 en offline + Et que la sync détecte que le contenu a été supprimé + Quand la lecture actuelle se termine + Alors l'app attend 2 secondes + Et passe automatiquement au contenu suivant + Et le fichier du contenu 123 est supprimé en arrière-plan + + Scénario: Toast notification contenu retiré + Étant donné que 2 contenus téléchargés ont été supprimés + Quand la synchronisation se termine + Alors je vois un toast: + """ + 🗑️ 2 contenus téléchargés ont été retirés + + Raison: Violation des règles de la plateforme + """ + + Scénario: Contenu modéré après téléchargement + Étant donné que j'ai téléchargé un contenu qui est ensuite modéré + Quand la synchronisation détecte la modération + Alors le contenu est immédiatement supprimé du device + Et je ne peux plus l'écouter + Et cela garantit la conformité même offline + + # ===== JUSTIFICATIONS ===== + + Scénario: Justification pas de conflit possible + Étant donné que les actions offline sont unilatérales (likes, abonnements) + Quand elles sont synchronisées + Alors il n'y a pas de conflit de version possible + Car l'utilisateur ajoute/retire simplement des préférences + Et pas de merge complexe nécessaire + + Scénario: Justification UX fluide offline + Étant donné que toutes les actions fonctionnent offline + Quand l'utilisateur interagit sans connexion + Alors l'expérience est identique au mode online + Et l'utilisateur n'est pas bloqué + Et peut utiliser l'app normalement + + Scénario: Justification batch = Économie requêtes + Étant donné que 15 actions sont en attente + Quand elles sont synchronisées en batch + Alors 1 seule requête HTTP est envoyée (vs 15 si individuelles) + Et cela économise la bande passante et la batterie + Et réduit la charge serveur + + Scénario: Justification conformité modération offline + Étant donné qu'un contenu illégal est modéré pendant qu'un user est offline + Quand le user se reconnecte + Alors le contenu est immédiatement supprimé de son device + Et cela garantit que les contenus illégaux disparaissent même offline + + # ===== STATISTIQUES ET MONITORING ===== + + Scénario: Historique synchronisations + Étant donné que j'accède à "Paramètres > Synchronisation" + Quand je consulte l'historique + Alors je vois: + | date | actions sync | statut | + | 15/06/2025 14:30:00 | 15 | Réussi ✅ | + | 14/06/2025 09:15:00 | 7 | Réussi ✅ | + | 13/06/2025 18:45:00 | 3 | Échec ❌ | + + Scénario: Détail d'une synchronisation + Étant donné que je clique sur une ligne de l'historique + Quand le détail s'affiche + Alors je vois: + ``` + Synchronisation du 15/06/2025 14:30:00 + + Actions synchronisées: + • 3 likes + • 1 abonnement + • 1 signalement + • 10 progressions audio-guides + + Durée: 1.2s + Statut: Réussi ✅ + ``` + + Scénario: Compteur actions en attente visible + Étant donné que j'ai 12 actions en attente de synchronisation + Quand j'accède à l'onglet Profil + Alors je vois un badge "12" sur l'icône de synchronisation + Et je sais qu'il y a des actions en attente + + Scénario: Synchronisation manuelle forcée + Étant donné que je veux forcer une synchronisation immédiate + Quand je vais dans "Paramètres > Synchronisation" + Et que je clique sur "Synchroniser maintenant" + Alors la synchronisation démarre immédiatement + Et toutes les actions en attente sont envoyées + + Scénario: Statistiques utilisateur - Syncs effectuées + Étant donné que j'accède à mes statistiques + Quand je consulte la section Synchronisation + Alors je vois: + | métrique | valeur | + | Synchronisations depuis début | 87 | + | Actions synchronisées total | 1,234 | + | Taux de succès | 94% | + | Dernière sync | Il y a 2h| + + Scénario: Statistiques admin - Volume synchronisations + Étant donné qu'un admin consulte les métriques de synchronisation + Quand il accède au dashboard + Alors il voit: + | métrique | valeur | + | Synchronisations/jour | 45,678 | + | Actions synchronisées/jour | 234,567 | + | Taux succès sync | 96.5% | + | Temps moyen traitement batch | 0.8s | + | Actions en attente (global) | 12,345 | + + Scénario: Alerte admin si taux échec sync >10% + Étant donné que le taux d'échec sync dépasse 10% + Quand le système détecte cette anomalie + Alors une alerte est envoyée: + """ + ⚠️ Taux échec synchronisation anormal: 12.3% + + Échecs aujourd'hui: 5,621 / 45,678 syncs + Causes principales: + - Timeout serveur: 3,245 + - Erreur réseau client: 1,876 + - Données invalides: 500 + + Action recommandée: Vérifier charge serveur + logs erreurs + """ + + # ===== TESTS PERFORMANCE ===== + + Scénario: Synchronisation rapide <2s + Étant donné que j'ai 20 actions en attente + Quand la synchronisation démarre + Alors le traitement prend <2 secondes + Et je ne remarque aucun ralentissement de l'app + + Scénario: Synchronisation de gros batch (100 actions) + Étant donné que je n'ai pas synchronisé pendant 1 semaine + Et que j'ai 100 actions en attente + Quand la synchronisation démarre + Alors le batch de 100 actions est traité en <5 secondes + Et toutes les actions sont synchronisées avec succès + + Scénario: Gestion charge serveur - 10 000 syncs simultanées + Étant donné que 10 000 utilisateurs se reconnectent simultanément + Quand chacun envoie un batch de 20 actions + Alors le serveur traite 200 000 actions + Et grâce au traitement asynchrone (queue Redis), le temps de réponse reste <3s + Et aucun timeout n'est constaté + + Scénario: Stockage SQLite optimisé + Étant donné que la table pending_actions stocke des centaines d'actions + Quand des requêtes sont exécutées + Alors la table est indexée sur created_at + Et les requêtes SELECT et DELETE sont instantanées (<10ms) + Et l'expérience utilisateur reste fluide + + Scénario: Nettoyage automatique table pending_actions + Étant donné que la table pending_actions grossit avec le temps + Quand les actions sont synchronisées et supprimées + Alors la table est automatiquement optimisée (VACUUM sur SQLite) + Et l'espace disque est libéré + Et les performances restent optimales + + # ===== EDGE CASES ===== + + Scénario: Action dupliquée - Idempotence + Étant donné que j'ai liké un contenu offline + Et que la sync échoue et retry + Quand le backend reçoit 2 fois le même like + Alors il applique l'idempotence (1 seul like enregistré) + Et le compteur de likes n'est pas faussé + + Scénario: Séquence like/unlike offline + Étant donné que j'ai liké puis unliké un contenu offline + Quand les 2 actions sont synchronisées + Alors le backend applique les 2 actions dans l'ordre + Et le résultat final est "pas de like" (état correct) + + Scénario: Abonnement puis désabonnement offline + Étant donné que je me suis abonné puis désabonné d'un créateur offline + Quand les 2 actions sont synchronisées + Alors le backend applique les 2 actions dans l'ordre + Et le résultat final est "pas abonné" + Et les jauges évoluent correctement (+5% puis -5% = 0% net) + + Scénario: Créateur supprimé pendant offline + Étant donné que je me suis abonné à un créateur offline + Mais que le créateur a supprimé son compte entre temps + Quand la sync traite l'abonnement + Alors le backend retourne "creator_deleted" + Et l'action est ignorée silencieusement + Et aucune erreur n'est affichée à l'utilisateur diff --git a/features/mode-offline/telechargement.feature b/features/mode-offline/telechargement.feature new file mode 100644 index 0000000..86a9dd5 --- /dev/null +++ b/features/mode-offline/telechargement.feature @@ -0,0 +1,409 @@ +# language: fr +Fonctionnalité: Téléchargement de contenus offline + En tant qu'utilisateur + Je veux télécharger des contenus pour les écouter sans connexion + Afin de profiter de RoadWave même dans les zones sans réseau + + Contexte: + Étant donné que je suis connecté à l'application RoadWave + + # ===== SÉLECTION ZONE GÉOGRAPHIQUE ===== + + Scénario: Option "Autour de moi" - Rayon 50 km + Étant donné que je suis à Paris (position GPS détectée) + Quand je sélectionne "Télécharger > Autour de moi" + Alors l'app recherche tous les contenus géolocalisés dans un rayon de 50 km + Et je vois une liste de contenus de Paris et banlieue proche + Et l'estimation affiche "~150 contenus disponibles" + + Scénario: Option "Ma ville" - Limite administrative détectée + Étant donné que je suis à Lyon (position GPS détectée) + Quand je sélectionne "Télécharger > Ma ville" + Alors l'app détecte automatiquement "Lyon" comme ville + Et recherche tous les contenus géolocalisés "Lyon" + Et je vois uniquement les contenus de la ville de Lyon (pas banlieue) + + Scénario: Option "Mon département" - Sélection dans liste + Étant donné que je veux télécharger des contenus pour un département + Quand je sélectionne "Télécharger > Mon département" + Alors je vois une liste de tous les départements français: + | département | + | 01 - Ain | + | 02 - Aisne | + | 75 - Paris | + | 69 - Rhône | + | ... | + Et je peux choisir un département + + Scénario: Sélection département et téléchargement contenus + Étant donné que je sélectionne "75 - Paris" dans la liste des départements + Quand la sélection est confirmée + Alors l'app recherche tous les contenus géolocalisés "Paris" + Et je vois "~234 contenus disponibles pour Paris" + + Scénario: Option "Ma région" - Sélection dans liste + Étant donné que je veux télécharger des contenus pour une région + Quand je sélectionne "Télécharger > Ma région" + Alors je vois une liste de toutes les régions françaises: + | région | + | Auvergne-Rhône-Alpes | + | Bretagne | + | Île-de-France | + | Nouvelle-Aquitaine | + | Occitanie | + | ... | + Et je peux choisir une région + + Scénario: Sélection région et téléchargement contenus + Étant donné que je sélectionne "Bretagne" dans la liste des régions + Quand la sélection est confirmée + Alors l'app recherche tous les contenus géolocalisés des départements bretons: + | département | + | Côtes-d'Armor (22) | + | Finistère (29) | + | Ille-et-Vilaine (35) | + | Morbihan (56) | + Et je vois "~487 contenus disponibles pour Bretagne" + + Scénario: Recherche manuelle ville + Étant donné que je veux télécharger des contenus pour une ville spécifique + Quand je tape "Marseille" dans la barre de recherche + Alors l'app propose des suggestions: + | suggestion | + | Marseille (13) | + | Marseille-en-Beauvaisis | + Et je peux sélectionner "Marseille (13)" + + Scénario: Recherche manuelle avec autocomplétion + Étant donné que je tape "Ly" dans la barre de recherche + Quand l'autocomplétion s'active + Alors je vois des suggestions: + | suggestion | + | Lyon (69) | + | Lys-lez-Lannoy | + Et je peux affiner ma recherche + + # ===== LIMITES TÉLÉCHARGEMENT ===== + + Scénario: Utilisateur gratuit - Limite 50 contenus max + Étant donné que je suis un utilisateur gratuit + Et que j'ai déjà téléchargé 45 contenus + Quand j'accède à la page Téléchargements + Alors je vois "45 / 50 contenus téléchargés" + Et je peux télécharger 5 contenus supplémentaires maximum + + Scénario: Utilisateur gratuit - Tentative dépasser limite 50 + Étant donné que je suis gratuit et j'ai déjà 50 contenus téléchargés + Quand j'essaie de télécharger un 51ème contenu + Alors le téléchargement est refusé + Et je vois le message: + """ + 📥 Limite atteinte (50 contenus) + + Vous avez atteint la limite de téléchargements gratuits. + + Options: + • Supprimez des contenus existants pour en télécharger de nouveaux + • Passez Premium pour des téléchargements illimités + + [Gérer mes téléchargements] [Découvrir Premium] + """ + + Scénario: Utilisateur Premium - Téléchargements illimités + Étant donné que je suis un utilisateur Premium + Et que j'ai déjà téléchargé 245 contenus + Quand j'accède à la page Téléchargements + Alors je vois "245 contenus (3.2 GB)" + Et aucune limite n'est affichée + Et je peux télécharger autant de contenus que je veux + + Scénario: Limite Premium = Espace disque disponible + Étant donné que je suis Premium + Et que mon device a 500 MB d'espace disque disponible + Quand j'essaie de télécharger 100 contenus (2 GB) + Alors le téléchargement échoue après ~50 contenus (500 MB) + Et je vois "Espace disque insuffisant. Libérez de l'espace pour continuer." + + Scénario: Calcul temps écoute disponible gratuit + Étant donné que je suis gratuit avec 50 contenus téléchargés + Et que la durée moyenne d'un contenu est 5 minutes + Quand je calcule le temps d'écoute disponible + Alors 50 contenus × 5 min = 250 minutes = 4h10 d'écoute + Et cela suffit pour un trajet quotidien ou road trip court + + Scénario: Calcul temps écoute disponible Premium illimité + Étant donné que je suis Premium avec 300 contenus téléchargés + Et que la durée moyenne est 5 minutes + Quand je calcule le temps d'écoute disponible + Alors 300 contenus × 5 min = 1500 minutes = 25h d'écoute + Et cela suffit pour un road trip de plusieurs jours + + # ===== CONNEXION WIFI / MOBILE ===== + + Scénario: Téléchargement par défaut en WiFi uniquement + Étant donné que je suis connecté en WiFi + Quand je clique sur "Télécharger 20 contenus" + Alors le téléchargement démarre immédiatement + Et aucune popup de confirmation n'apparaît + + Scénario: Tentative téléchargement en données mobiles - Popup confirmation + Étant donné que je suis connecté en 4G (pas de WiFi) + Quand je clique sur "Télécharger 20 contenus" + Alors une popup apparaît: + """ + 📡 Vous n'êtes pas connecté en WiFi + + Télécharger via données mobiles consommera environ 72 MB. + + [Attendre WiFi] [Continuer quand même] + """ + + Scénario: Calcul estimation consommation data mobile + Étant donné que je veux télécharger 20 contenus + Et que la durée moyenne est 5 minutes + Et que la qualité Standard est 48 kbps Opus + Quand l'estimation est calculée + Alors consommation = 20 contenus × 5 min × 48 kbps / 8 = 72 MB + Et ce montant est affiché dans la popup + + Scénario: Confirmation téléchargement en données mobiles + Étant donné que je vois la popup de confirmation données mobiles + Quand je clique sur "Continuer quand même" + Alors le téléchargement démarre immédiatement via 4G + Et la consommation data est comptabilisée sur mon forfait mobile + + Scénario: Refus téléchargement données mobiles - Attendre WiFi + Étant donné que je vois la popup de confirmation données mobiles + Quand je clique sur "Attendre WiFi" + Alors les téléchargements sont mis en file d'attente + Et ils démarreront automatiquement quand le WiFi sera détecté + + Scénario: Détection automatique WiFi et reprise téléchargements + Étant donné que j'ai mis 20 contenus en file d'attente (attente WiFi) + Quand l'app détecte une connexion WiFi + Alors les téléchargements démarrent automatiquement + Et je reçois une notification "Téléchargements en cours via WiFi" + + # ===== QUALITÉ AUDIO ===== + + Scénario: Qualité Standard (48 kbps) par défaut + Étant donné que je configure mes téléchargements + Quand j'accède aux paramètres de qualité + Alors la qualité "Standard (48 kbps - ~20 MB/h)" est sélectionnée par défaut + Et elle est disponible pour tous (gratuit + Premium) + + Scénario: Qualité Basse (24 kbps) disponible pour tous + Étant donné que j'ai peu d'espace disque disponible + Quand je sélectionne qualité "Basse (24 kbps - ~10 MB/h)" + Alors mes prochains téléchargements seront en 24 kbps + Et l'espace utilisé sera divisé par 2 par rapport à Standard + Et cette option est disponible pour gratuit + Premium + + Scénario: Qualité Haute (64 kbps) réservée Premium + Étant donné que je suis un utilisateur gratuit + Quand je consulte les options de qualité + Alors l'option "Haute (64 kbps - ~30 MB/h)" est grisée + Et je vois "👑 Premium uniquement" + Et je ne peux pas la sélectionner + + Scénario: Utilisateur Premium peut choisir qualité Haute + Étant donné que je suis un utilisateur Premium + Quand je consulte les options de qualité + Alors l'option "Haute (64 kbps - ~30 MB/h)" est disponible + Et je peux la sélectionner pour mes téléchargements + Et la qualité audio sera excellente (meilleure restitution voix et ambiances) + + Scénario: Comparaison taille fichiers selon qualité + Étant donné que je veux télécharger 50 contenus de 5 min chacun + Quand je compare les qualités + Alors les tailles totales sont: + | qualité | bitrate | taille totale | + | Basse | 24 kbps | ~250 MB | + | Standard | 48 kbps | ~500 MB | + | Haute | 64 kbps | ~650 MB | + + Scénario: Justification Standard = Bon compromis + Étant donné que le contenu RoadWave est principalement de la voix + Quand la qualité Standard (48 kbps Opus) est utilisée + Alors la qualité est très correcte pour la voix + Et équivalente à la radio FM + Et le compromis qualité/taille est optimal + + Scénario: Justification Haute réservée Premium = Incitation upgrade + Étant donné qu'un utilisateur gratuit veut la meilleure qualité + Quand il voit que Haute est réservée Premium + Alors cela l'incite à passer Premium pour 4.99€/mois + Et c'est un avantage tangible supplémentaire de Premium + + Scénario: Changement qualité après téléchargements existants + Étant donné que j'ai déjà téléchargé 30 contenus en qualité Standard + Quand je change la qualité vers Haute (si Premium) + Alors les 30 contenus existants restent en Standard + Et seuls les nouveaux téléchargements seront en Haute + Et je peux manuellement re-télécharger les 30 contenus pour les avoir en Haute + + # ===== PROCESSUS DE TÉLÉCHARGEMENT ===== + + Scénario: Téléchargement individuel d'un contenu + Étant donné que je consulte la page d'un contenu + Quand je clique sur l'icône de téléchargement 📥 + Alors le téléchargement démarre + Et une barre de progression apparaît + Et l'icône devient ✅ quand terminé + + Scénario: Téléchargement batch de contenus sélectionnés + Étant donné que je consulte une liste de contenus pour "Paris" + Quand je sélectionne 15 contenus manuellement + Et que je clique sur "Télécharger la sélection" + Alors les 15 contenus sont téléchargés en parallèle (max 3 simultanés) + Et une notification affiche "15 contenus téléchargés" + + Scénario: Téléchargement automatique recommandations zone + Étant donné que je sélectionne "Autour de moi" (Paris) + Quand je clique sur "Télécharger les 50 meilleurs contenus" + Alors l'algorithme sélectionne automatiquement les 50 contenus les mieux notés/récents + Et les télécharge tous + Et je n'ai pas besoin de choisir manuellement + + Scénario: Barre de progression téléchargement global + Étant donné que je télécharge 20 contenus + Quand les téléchargements sont en cours + Alors je vois une barre de progression globale: + """ + 📥 Téléchargement en cours... + 7 / 20 contenus (35%) + ~45 MB restants + Temps estimé: 2 min + """ + + Scénario: Téléchargements en tâche de fond + Étant donné que je lance le téléchargement de 30 contenus + Quand je ferme l'app ou passe à une autre activité + Alors les téléchargements continuent en arrière-plan + Et je reçois une notification quand tous sont terminés + + Scénario: Pause et reprise téléchargements + Étant donné que je télécharge 20 contenus + Quand je clique sur "Pause" + Alors les téléchargements en cours se terminent + Et les téléchargements en attente sont mis en pause + Et je peux cliquer sur "Reprendre" plus tard + + Scénario: Annulation téléchargements + Étant donné que je télécharge 20 contenus + Quand je clique sur "Annuler" + Alors tous les téléchargements sont arrêtés + Et les fichiers partiels sont supprimés + Et l'espace disque est libéré + + Scénario: Gestion erreurs téléchargement + Étant donné que je télécharge un contenu + Mais que la connexion Internet coupe au milieu + Quand la connexion revient + Alors le téléchargement reprend automatiquement où il s'était arrêté + Et aucune perte de progression n'a lieu + + Scénario: Retry automatique après échec + Étant donné qu'un téléchargement échoue 3 fois consécutives + Quand l'échec est détecté + Alors le contenu est marqué "Échec" + Et je vois une notification "3 contenus n'ont pas pu être téléchargés" + Et je peux retry manuellement en cliquant sur "Réessayer" + + # ===== GESTION CONTENUS TÉLÉCHARGÉS ===== + + Scénario: Liste contenus téléchargés + Étant donné que j'ai téléchargé 45 contenus + Quand j'accède à "Téléchargements" + Alors je vois la liste complète de mes 45 contenus + Et pour chaque contenu: titre, créateur, durée, taille, date téléchargement + + Scénario: Tri contenus téléchargés + Étant donné que je consulte ma liste de téléchargements + Quand je clique sur "Trier par" + Alors je peux trier par: + | critère | ordre | + | Date téléchargement | Plus récent / Plus ancien| + | Titre | A-Z / Z-A | + | Créateur | A-Z / Z-A | + | Durée | Plus long / Plus court | + | Taille | Plus gros / Plus petit | + + Scénario: Recherche dans contenus téléchargés + Étant donné que j'ai 200 contenus téléchargés + Quand je tape "Tesla" dans la barre de recherche + Alors seuls les contenus contenant "Tesla" s'affichent + Et je peux rapidement trouver un contenu spécifique + + Scénario: Suppression individuelle contenu téléchargé + Étant donné que je veux supprimer un contenu téléchargé + Quand je swipe left (iOS) ou long press (Android) sur le contenu + Et que je clique sur "Supprimer" + Alors le fichier est supprimé du device + Et l'espace disque est libéré + Et le compteur est décrémenté (ex: 45/50 → 44/50) + + Scénario: Suppression batch contenus téléchargés + Étant donné que je veux supprimer plusieurs contenus + Quand je sélectionne 10 contenus + Et que je clique sur "Supprimer la sélection" + Alors les 10 fichiers sont supprimés + Et ~100 MB d'espace disque sont libérés + Et une notification confirme "10 contenus supprimés" + + Scénario: Suppression tous les contenus téléchargés + Étant donné que j'ai 45 contenus téléchargés + Quand je clique sur "Supprimer tout" + Et que je confirme l'action + Alors tous les 45 contenus sont supprimés + Et l'espace disque total est libéré (~450 MB) + Et le compteur repasse à 0/50 + + Scénario: Espace disque utilisé visible + Étant donné que j'ai téléchargé 45 contenus + Quand j'accède à la page Téléchargements + Alors je vois l'espace disque utilisé: + """ + 📥 Téléchargements + 45 / 50 contenus + Espace utilisé: 478 MB + """ + + Scénario: Statistiques téléchargements + Étant donné que j'accède à mes statistiques + Quand je consulte la section Téléchargements + Alors je vois: + | métrique | valeur | + | Contenus actuellement téléchargés | 45 | + | Espace disque utilisé | 478 MB | + | Contenus téléchargés depuis début | 287 | + | Total data téléchargée | 3.2 GB | + | Téléchargements via WiFi | 92% | + | Téléchargements via mobile | 8% | + + # ===== LECTURE OFFLINE ===== + + Scénario: Lecture contenu téléchargé sans connexion + Étant donné que je n'ai aucune connexion Internet (mode avion) + Et que j'ai des contenus téléchargés + Quand je lance un contenu téléchargé + Alors la lecture démarre normalement depuis le fichier local + Et aucune erreur de connexion n'apparaît + + Scénario: Badge "Téléchargé" sur contenus offline + Étant donné que j'ai téléchargé certains contenus + Quand je consulte une liste de contenus + Alors les contenus téléchargés ont un badge ✅ "Offline" + Et je sais immédiatement lesquels sont disponibles sans connexion + + Scénario: Filtre "Téléchargés uniquement" + Étant donné que je veux voir uniquement mes contenus offline + Quand j'active le filtre "Téléchargés uniquement" + Alors seuls les contenus téléchargés s'affichent + Et je peux facilement naviguer dans mon catalogue offline + + Scénario: Playlist offline automatique + Étant donné que j'ai téléchargé 45 contenus + Quand j'accède à "Téléchargements" + Alors je peux lancer une playlist aléatoire de mes 45 contenus + Et profiter d'une écoute continue offline diff --git a/features/mode-offline/validite-renouvellement.feature b/features/mode-offline/validite-renouvellement.feature new file mode 100644 index 0000000..e642373 --- /dev/null +++ b/features/mode-offline/validite-renouvellement.feature @@ -0,0 +1,335 @@ +# language: fr +Fonctionnalité: Validité et renouvellement contenus offline + En tant qu'utilisateur + Je veux que mes contenus téléchargés restent valides un certain temps + Afin de garantir la légalité et la fraîcheur du contenu + + Contexte: + Étant donné que je suis connecté à l'application RoadWave + Et que j'ai des contenus téléchargés + + # ===== DURÉE DE VALIDITÉ ===== + + Scénario: Validité de 30 jours après téléchargement + Étant donné que je télécharge un contenu le 1er juin 2025 + Quand le téléchargement est terminé + Alors le contenu est valide jusqu'au 1er juillet 2025 (30 jours) + Et la date d'expiration est stockée en local + + Scénario: Affichage date expiration sur contenu téléchargé + Étant donné que j'ai téléchargé un contenu il y a 20 jours + Quand je consulte les détails du contenu + Alors je vois "Expire dans 10 jours" + Et je sais combien de temps il reste avant expiration + + Scénario: Standard industrie aligné (Spotify, YouTube, Deezer) + Étant donné que Spotify, YouTube Music et Deezer utilisent 30 jours + Quand RoadWave fixe également 30 jours + Alors c'est le standard accepté par les utilisateurs + Et il n'y a pas de confusion avec les autres plateformes + + Scénario: Justification 30 jours - Force reconnexion régulière + Étant donné qu'un utilisateur ne se connecte jamais + Quand ses contenus expirent après 30 jours + Alors il est obligé de se reconnecter pour les renouveler + Et le système peut vérifier: + | vérification | + | Abonnement Premium toujours actif| + | Contenus non modérés/supprimés | + | Métadonnées à jour | + + Scénario: Justification 30 jours - Évite stockage obsolète + Étant donné qu'un contenu a été modéré après téléchargement + Quand le contenu expire après 30 jours maximum + Alors le contenu illégal est automatiquement supprimé + Et ne reste pas indéfiniment sur le device + + # ===== RENOUVELLEMENT AUTOMATIQUE ===== + + Scénario: Détection WiFi et contenus >25 jours + Étant donné que j'ai des contenus téléchargés il y a 26 jours + Quand l'app détecte une connexion WiFi + Alors une requête GET /offline/contents/refresh est envoyée + Et le backend vérifie chaque contenu + + Scénario: Vérification abonnement Premium toujours actif + Étant donné qu'un contenu téléchargé en Premium est à renouveler + Quand le backend vérifie le statut + Et que l'abonnement Premium est toujours actif + Alors la validité est renouvelée à 30 jours supplémentaires + + Scénario: Abonnement Premium expiré - Contenu non renouvelé + Étant donné qu'un contenu Premium téléchargé est à renouveler + Quand le backend vérifie le statut + Et que l'abonnement Premium a expiré + Alors le contenu n'est pas renouvelé + Et il sera supprimé à l'expiration (J-0) + Et l'utilisateur voit "Contenu Premium expiré (abonnement inactif)" + + Scénario: Vérification contenu pas modéré/supprimé + Étant donné qu'un contenu téléchargé est à renouveler + Quand le backend vérifie le statut + Et que le contenu a été modéré ou supprimé entre temps + Alors le contenu n'est pas renouvelé + Et sera supprimé immédiatement du device + Et l'utilisateur voit "1 contenu retiré (violation règles)" + + Scénario: Mise à jour métadonnées lors du renouvellement + Étant donné qu'un contenu téléchargé est renouvelé + Quand le backend traite le renouvellement + Alors les métadonnées sont mises à jour: + | métadonnée | mise à jour si changée | + | Titre | ✅ | + | Nom créateur | ✅ | + | Description | ✅ | + | Tags | ✅ | + | Statut Premium | ✅ | + Et l'utilisateur voit les infos à jour + + Scénario: Pas de re-téléchargement audio si fichier OK + Étant donné qu'un contenu est renouvelé + Quand le fichier audio local est intact + Alors seules les métadonnées sont mises à jour + Et le fichier audio n'est pas re-téléchargé + Et cela économise la bande passante + + Scénario: Re-téléchargement audio si fichier corrompu + Étant donné qu'un contenu est renouvelé + Quand le fichier audio local est corrompu (checksum invalide) + Alors le fichier audio est re-téléchargé entièrement + Et le nouveau fichier remplace le corrompu + + Scénario: Renouvellement silencieux si WiFi régulier + Étant donné que je me connecte en WiFi tous les jours + Quand mes contenus atteignent 25-30 jours + Alors ils sont automatiquement renouvelés en arrière-plan + Et je ne vois aucune notification (processus transparent) + Et mes contenus restent valides indéfiniment + + Scénario: Renouvellement batch de plusieurs contenus + Étant donné que j'ai 30 contenus à renouveler + Quand le renouvellement automatique se déclenche + Alors une requête batch est envoyée: + ```json + POST /offline/contents/refresh + { + "content_ids": ["abc123", "def456", "ghi789", ...] + } + ``` + Et le backend traite les 30 contenus en une seule requête + Et cela économise les requêtes HTTP + + Scénario: Temps de traitement renouvellement + Étant donné que 30 contenus sont à renouveler + Quand la requête batch est traitée + Alors le backend répond en <2 secondes + Et les métadonnées sont mises à jour localement + Et l'utilisateur ne remarque aucun ralentissement + + # ===== NOTIFICATIONS EXPIRATION ===== + + Scénario: Notification J-3 avant expiration + Étant donné que j'ai 15 contenus qui expirent dans 3 jours + Quand le système vérifie les expirations + Alors je reçois une notification: + """ + ⚠️ 15 contenus expirent dans 3 jours + + Connectez-vous en WiFi pour les renouveler automatiquement. + """ + Et je peux agir avant l'expiration + + Scénario: Pas de notification si connexion WiFi régulière + Étant donné que je me connecte en WiFi tous les jours + Et que mes contenus sont automatiquement renouvelés + Quand le système vérifie les expirations + Alors aucune notification J-3 n'est envoyée + Car les contenus sont déjà renouvelés silencieusement + + Scénario: Notification uniquement si contenus non renouvelés + Étant donné que j'ai 20 contenus dont 15 renouvelés et 5 non renouvelés + Quand le J-3 arrive pour les 5 non renouvelés + Alors je reçois "5 contenus expirent dans 3 jours" + Et seuls les contenus à risque sont mentionnés + + Scénario: Action utilisateur après notification J-3 + Étant donné que je reçois la notification J-3 + Quand je clique sur la notification + Alors l'app s'ouvre sur la page Téléchargements + Et je vois les contenus qui vont expirer en rouge + Et je peux me connecter en WiFi pour les renouveler + + Scénario: Suppression automatique J-0 (expiration) + Étant donné qu'un contenu n'a pas été renouvelé + Quand le jour d'expiration arrive (J-0) + Alors le fichier est automatiquement supprimé du device + Et l'espace disque est libéré + Et le compteur est décrémenté (ex: 45/50 → 44/50) + + Scénario: Toast après suppression automatique J-0 + Étant donné que 15 contenus viennent d'expirer + Quand l'utilisateur ouvre l'app + Alors il voit un toast: + """ + 🗑️ 15 contenus expirés ont été supprimés + + Reconnectez-vous en WiFi régulièrement pour éviter les expirations. + """ + + Scénario: Liste contenus supprimés après expiration + Étant donné que 15 contenus ont expiré + Quand je consulte l'historique des suppressions + Alors je vois la liste des 15 contenus supprimés: + | titre | créateur | date expiration | + | Mon épisode préféré | JeanDupont | 15 juin 2025 | + | Road trip Bretagne | MarieLambert| 15 juin 2025 | + | ... | ... | ... | + Et je peux les re-télécharger si je veux + + Scénario: Re-téléchargement après expiration + Étant donné qu'un contenu a expiré et été supprimé + Quand je retrouve ce contenu dans l'app + Alors le badge ✅ "Offline" n'est plus affiché + Et je peux le re-télécharger normalement + Et la validité repart à 30 jours + + # ===== CAS PARTICULIERS ===== + + Scénario: Utilisateur ne se connecte jamais pendant 30 jours + Étant donné que je télécharge 50 contenus le 1er juin + Mais que je ne me connecte jamais en WiFi pendant 30 jours + Quand le 1er juillet arrive + Alors tous les 50 contenus expirent + Et sont automatiquement supprimés + Et je n'ai plus aucun contenu offline + + Scénario: Utilisateur en zone blanche 30+ jours + Étant donné que je télécharge 50 contenus avant de partir en zone sans réseau + Et que je reste 45 jours sans connexion + Quand les contenus expirent après 30 jours + Alors ils sont supprimés même si je ne peux pas me connecter + Et je perds l'accès à mes contenus offline + + Scénario: Recommandation téléchargement avant zone blanche longue + Étant donné que je prépare un road trip de 60 jours + Quand je consulte la FAQ + Alors je vois la recommandation: + """ + ⚠️ Road trips >30 jours + + Les contenus téléchargés expirent après 30 jours. + Pour les longs voyages sans connexion: + • Téléchargez de nouveaux contenus tous les 25 jours si possible + • Ou planifiez une reconnexion WiFi tous les 25 jours + """ + + Scénario: Changement statut Premium en gratuit pendant validité + Étant donné que je suis Premium et j'ai téléchargé 200 contenus + Quand mon abonnement Premium expire + Et que je repasse en gratuit + Alors au prochain renouvellement, seulement 50 contenus sont conservés + Et les 150 autres sont supprimés (limite gratuit) + Et je vois "Limite gratuit (50 contenus) appliquée. 150 contenus supprimés." + + Scénario: Sélection automatique 50 meilleurs contenus si passage gratuit + Étant donné que je repasse en gratuit avec 200 contenus téléchargés + Quand le système applique la limite de 50 + Alors les 50 contenus les plus récemment écoutés sont conservés + Et les 150 autres sont supprimés + Et cela maximise les chances de garder les contenus que j'aime + + Scénario: Contenus Premium exclusifs supprimés si abonnement expire + Étant donné que j'ai téléchargé 20 contenus Premium exclusifs + Quand mon abonnement Premium expire + Alors les 20 contenus Premium sont immédiatement supprimés + Car ils ne sont accessibles qu'aux abonnés Premium actifs + Et je vois "20 contenus Premium supprimés (abonnement expiré)" + + # ===== STATISTIQUES ET MONITORING ===== + + Scénario: Affichage temps restant avant expiration + Étant donné que j'ai 45 contenus téléchargés + Quand je consulte la page Téléchargements + Alors je vois pour chaque contenu: + | contenu | temps restant | + | Mon épisode (récent)| Expire dans 28 jours | + | Road trip (ancien) | Expire dans 3 jours | + Et je sais lesquels sont prioritaires pour renouvellement + + Scénario: Tri par date expiration + Étant donné que j'ai 45 contenus avec différentes dates d'expiration + Quand je trie par "Expiration" + Alors les contenus qui expirent le plus tôt apparaissent en premier + Et je peux voir rapidement lesquels nécessitent une reconnexion urgente + + Scénario: Badge rouge si expiration <3 jours + Étant donné qu'un contenu expire dans 2 jours + Quand je consulte la liste des téléchargements + Alors le contenu a un badge rouge "⚠️ Expire bientôt" + Et il est visuellement mis en avant + + Scénario: Statistiques utilisateur - Taux de renouvellement + Étant donné que j'accède à mes statistiques + Quand je consulte la section Téléchargements + Alors je vois: + | métrique | valeur | + | Contenus actuels | 45 | + | Contenus expirés depuis début | 87 | + | Contenus renouvelés (auto) | 234 | + | Taux renouvellement automatique | 73% | + + Scénario: Statistiques admin - Taux expiration global + Étant donné qu'un admin consulte les métriques offline + Quand il accède au dashboard + Alors il voit: + | métrique | valeur | + | Contenus téléchargés actifs | 1,234,567 | + | Expirations ce mois | 45,678 | + | Taux expiration | 3.7% | + | Renouvellements automatiques/mois | 234,567 | + + Scénario: Alerte admin si taux expiration >10% + Étant donné que le taux d'expiration mensuel dépasse 10% + Quand le système détecte cette anomalie + Alors une alerte est envoyée: + """ + ⚠️ Taux d'expiration anormal: 12.3% + + Nombre expirations ce mois: 152,345 + Causes possibles: + - Utilisateurs ne se connectent plus en WiFi + - Problème renouvellement automatique ? + - Churn utilisateurs augmenté ? + + Action recommandée: Enquête technique + email rappel utilisateurs + """ + + Scénario: Email rappel si pas de connexion WiFi depuis 20 jours + Étant donné que je n'ai pas connecté l'app en WiFi depuis 20 jours + Et que j'ai 45 contenus téléchargés + Quand le système détecte cette inactivité WiFi + Alors je reçois un email: + """ + 📡 Connectez-vous en WiFi pour conserver vos téléchargements + + Vous n'avez pas connecté RoadWave en WiFi depuis 20 jours. + Vos 45 contenus téléchargés expireront dans 10 jours si non renouvelés. + + Connectez-vous en WiFi avant le 30 juin pour les renouveler automatiquement. + """ + + Scénario: Performance renouvellement avec 10 000 utilisateurs simultanés + Étant donné que 10 000 utilisateurs se connectent en WiFi simultanément + Quand chacun demande le renouvellement de 50 contenus + Alors le serveur traite 500 000 vérifications + Et grâce au cache Redis et index PostgreSQL, le temps de réponse reste <3s + Et les serveurs gèrent la charge sans problème + + Scénario: Logs audit renouvellements + Étant donné qu'un contenu est renouvelé + Quand l'opération se termine + Alors un log est enregistré: + | timestamp | user_id | content_id | action | résultat | + | 2025-06-15 14:30:00 | abc123 | xyz789 | renew | success (+30d) | + | 2025-06-15 14:30:01 | abc123 | def456 | renew | failed (deleted)| + Et ces logs aident à débugger les problèmes diff --git a/features/moderation/moderation-preventive.feature b/features/moderation/moderation-preventive.feature new file mode 100644 index 0000000..17136e0 --- /dev/null +++ b/features/moderation/moderation-preventive.feature @@ -0,0 +1,284 @@ +# language: fr + +@moderation @preventive +Fonctionnalité: Modération préventive + + # 14.5 - Modération préventive (rappel) + + Contexte: + Étant donné que le système de modération préventive est actif + + # Nouveaux créateurs - Validation manuelle des 3 premiers contenus + + Scénario: Créateur nouvellement inscrit + Étant donné que je viens de créer un compte créateur + Et que je n'ai jamais publié de contenu + Quand j'examine mon statut de créateur + Alors mon compte est marqué comme "Nouveau créateur" + Et mes 3 premiers contenus devront être validés manuellement + Et je suis informé de ce processus lors de l'onboarding + + Scénario: Publication du premier contenu par un nouveau créateur + Étant donné que je suis un nouveau créateur + Et que je n'ai publié aucun contenu auparavant + Quand je publie mon premier contenu + Alors le contenu entre en file d'attente de validation manuelle + Et le statut du contenu est "En attente de validation" + Et le contenu n'est pas diffusé sur la plateforme + Et je reçois une notification: + """ + Votre contenu est en cours de validation. + Les premiers contenus sont vérifiés manuellement pour garantir la qualité de la plateforme. + Délai: 24-48h (jours ouvrés). + """ + + Scénario: Validation manuelle par un modérateur + Étant donné que j'ai publié mon premier contenu + Et que le contenu est en attente de validation + Quand un modérateur examine mon contenu + Alors le modérateur utilise la transcription automatique Whisper + Et le modérateur vérifie: + | critère | conforme | + | Respect des règles communauté | oui | + | Pas de contenu inapproprié | oui | + | Qualité audio acceptable | oui | + | Métadonnées cohérentes | oui | + | Tags appropriés | oui | + Et si tout est conforme, le contenu est validé + + Scénario: Délai de validation de 24-48h jours ouvrés + Étant donné que j'ai publié mon premier contenu lundi à 10:00 + Quand le contenu entre en file de validation + Alors le contenu est validé avant mercredi 10:00 (48h jours ouvrés) + Et dans la plupart des cas, la validation est effectuée sous 24h + Et je reçois une notification dès que le contenu est validé + + Scénario: Notification de validation réussie + Étant donné que mon premier contenu a été validé par un modérateur + Quand la validation est approuvée + Alors je reçois une notification: + """ + ✓ Votre contenu "Mon premier podcast" a été validé! + Il est maintenant diffusé sur RoadWave. + """ + Et le statut du contenu passe à "Publié" + Et le contenu devient visible pour tous les utilisateurs + Et il entre dans l'algorithme de recommandation + + Scénario: Refus de validation si contenu non conforme + Étant donné que mon premier contenu viole les règles de la communauté + Quand le modérateur examine le contenu + Alors le contenu est refusé + Et je reçois une notification détaillée: + """ + ⚠️ Votre contenu "Mon premier podcast" n'a pas été validé. + + Raison: Contenu inapproprié détecté + Passage problématique: 3:15-3:45 + Transcription: "[passage surligné]" + + Que faire? + - Corrigez le problème et republiez + - Consultez les règles de la communauté + - Contactez le support si vous avez des questions + """ + Et le contenu reste en statut "Refusé" + Et je peux modifier et republier + + Scénario: Les 3 premiers contenus sont validés manuellement + Étant donné que je suis un nouveau créateur + Quand je publie mes contenus + Alors les contenus suivants nécessitent une validation manuelle: + | contenu | validation manuelle | + | 1er | oui | + | 2ème | oui | + | 3ème | oui | + | 4ème | non (auto) | + Et après 3 contenus validés, mes futurs contenus sont publiés automatiquement + + Scénario: Passage en mode automatique après 3 validations + Étant donné que mes 3 premiers contenus ont été validés avec succès + Quand je publie mon 4ème contenu + Alors le contenu est publié automatiquement + Et aucune validation manuelle n'est requise + Et le statut passe directement à "Publié" + Et je reçois une notification: + """ + ✓ Votre contenu a été publié automatiquement. + Vos 3 premiers contenus ayant été conformes, vos futurs contenus sont publiés instantanément. + """ + + # Score de confiance dynamique + + Scénario: Évolution du score de confiance + Étant donné que je suis un créateur établi + Quand le système évalue mon historique + Alors un score de confiance est calculé basé sur: + | critère | poids | + | Nombre de contenus publiés | 20% | + | Strikes reçus | 40% | + | Signalements infondés | 20% | + | Ancienneté du compte | 10% | + | Taux d'engagement positif | 10% | + Et le score évolue dynamiquement + + Scénario: Créateur fiable - Publication automatique + Étant donné que je suis un créateur + Et que j'ai 0 strike depuis 6 mois + Et que tous mes contenus précédents ont été conformes + Quand mon score de confiance est calculé + Alors je suis classé comme "Créateur fiable" + Et tous mes nouveaux contenus sont publiés automatiquement + Et aucune validation manuelle n'est nécessaire + Et je bénéficie d'une publication instantanée + + Scénario: Créateur suspect - Validation manuelle systématique + Étant donné que je suis un créateur + Et que j'ai reçu 2 strikes récents (< 3 mois) + Quand mon score de confiance est recalculé + Alors je suis classé comme "Créateur suspect" + Et tous mes nouveaux contenus nécessitent une validation manuelle + Et chaque contenu est examiné avant publication + Et je suis notifié de ce changement de statut: + """ + En raison de sanctions récentes, vos contenus nécessitent une validation manuelle. + Respectez les règles de la communauté pour retrouver la publication automatique. + """ + + Scénario: Réhabilitation après période sans incident + Étant donné que j'étais un "Créateur suspect" + Et que je publie 10 contenus conformes sur 6 mois + Et que je ne reçois aucun nouveau strike + Quand le système réévalue mon score de confiance + Alors je passe en "Créateur fiable" + Et la publication automatique est rétablie + Et je reçois une notification de réhabilitation: + """ + ✓ Félicitations! Vous êtes de nouveau un créateur fiable. + Vos contenus seront publiés automatiquement. + """ + + # Publicités - Validation manuelle obligatoire + + Scénario: Toute publicité nécessite validation manuelle + Étant donné qu'un annonceur soumet une publicité audio + Quand la publicité est créée + Alors elle entre automatiquement en file de validation manuelle + Et aucune publicité n'est diffusée sans validation préalable + Et cela est obligatoire pour des raisons de responsabilité juridique + + Scénario: Validation d'une publicité - Processus complet + Étant donné qu'une publicité est en attente de validation + Quand un modérateur senior examine la publicité + Alors le modérateur vérifie: + | critère | conforme | + | Transcription automatique Whisper | effectuée| + | Contenu conforme aux règles | oui | + | Pas de fausse publicité / arnaque | oui | + | Respect du ciblage géographique | oui | + | Durée conforme (10-60s) | oui | + | Volume audio acceptable (pas trop fort)| oui | + | Métadonnées correctes | oui | + Et si tout est conforme, la publicité est validée + + Scénario: Délai de validation d'une publicité - 24-48h + Étant donné qu'un annonceur soumet une publicité lundi à 10:00 + Quand la publicité entre en file de validation + Alors la publicité est validée avant mercredi 10:00 (48h jours ouvrés) + Et l'annonceur est notifié dès la validation + Et la campagne publicitaire peut alors démarrer + + Scénario: Refus de validation d'une publicité + Étant donné qu'une publicité contient des éléments non conformes + Quand le modérateur examine la publicité + Alors la publicité est refusée + Et l'annonceur reçoit une notification détaillée: + """ + ⚠️ Votre publicité n'a pas été validée. + + Raison: Contenu trompeur détecté + Passage problématique: 0:15-0:25 + Transcription: "[promesse irréaliste surlignée]" + + Que faire? + - Modifiez votre publicité + - Soumettez-la à nouveau pour validation + - Consultez les règles publicitaires de RoadWave + """ + Et l'annonceur peut modifier et resoumettre la publicité + Et aucun remboursement n'est effectué pour une publicité refusée + + # Prévention > Réaction + + Scénario: Économie de modération grâce à la prévention + Étant donné que la modération préventive est active + Quand on analyse l'efficacité du système + Alors 80% des contenus inappropriés sont détectés avant publication + Et cela réduit le nombre de signalements de 70% + Et les ressources de modération sont optimisées + Et la qualité de la plateforme est préservée dès le début + + Scénario: Qualité de la plateforme maintenue + Étant donné que tous les nouveaux créateurs sont vérifiés + Quand on analyse la qualité globale des contenus + Alors le taux de contenus inappropriés est <1% + Et les utilisateurs font confiance à la plateforme + Et la réputation de RoadWave est préservée + Et l'expérience utilisateur est optimale + + # Transparence envers les créateurs + + Scénario: Information claire sur le processus de validation + Étant donné que je suis un nouveau créateur + Quand je consulte la page d'aide "Validation des contenus" + Alors j'apprends que: + """ + Validation des contenus + + Nouveaux créateurs: + - Vos 3 premiers contenus sont validés manuellement + - Délai: 24-48h (jours ouvrés) + - Après 3 contenus conformes: publication automatique + + Créateurs établis: + - Publication automatique si historique conforme + - Validation manuelle si strikes récents + + Publicités: + - Validation manuelle obligatoire pour toutes les publicités + - Délai: 24-48h (jours ouvrés) + """ + Et le processus est clair et transparent + + Scénario: Badge "Créateur vérifié" après validation + Étant donné que mes 3 premiers contenus ont été validés avec succès + Quand je consulte mon profil créateur + Alors un badge discret "✓ Créateur vérifié" s'affiche + Et ce badge rassure les auditeurs sur la qualité de mes contenus + Et il améliore ma crédibilité sur la plateforme + + # Conformité et justification + + Scénario: Justification de la modération préventive + Étant donné que la modération préventive est en place + Quand on évalue les bénéfices + Alors les avantages suivants sont constatés: + | bénéfice | + | Prévention meilleure que réaction | + | Économie de ressources de modération (×3-5) | + | Qualité de la plateforme préservée dès le début | + | Confiance des utilisateurs renforcée | + | Moins de contenus inappropriés signalés | + | Réputation de la plateforme protégée | + Et l'investissement dans la prévention est rentable + + Scénario: Coût de la modération préventive + Étant donné que 100 nouveaux créateurs publient 3 contenus chacun + Et que 50 publicités sont soumises par mois + Quand on calcule le coût de modération préventive + Alors le coût en temps modérateur est: + | type | nombre | temps/contenu | total | + | Nouveaux créateurs| 300 | 5 min | 25h | + | Publicités | 50 | 10 min | 8.3h | + Et le coût total est d'environ 33h de modération/mois + Et c'est largement compensé par la réduction des signalements réactifs diff --git a/features/moderation/sanctions-notifications.feature b/features/moderation/sanctions-notifications.feature new file mode 100644 index 0000000..e17713e --- /dev/null +++ b/features/moderation/sanctions-notifications.feature @@ -0,0 +1,327 @@ +# language: fr + +@moderation @sanctions +Fonctionnalité: Sanctions et notifications de modération + + # 14.3 - Sanctions + + Contexte: + Étant donné que je suis un créateur de contenu + Et que j'ai publié un contenu + + # 14.3.1 - Notification au créateur (multi-canal) + + Scénario: Notification multi-canal après sanction + Étant donné que mon contenu a été modéré + Quand la sanction est appliquée + Alors je reçois une notification sur 3 canaux: + | canal | timing | contenu | + | Push | Immédiat | "Votre contenu a été modéré" | + | In-app | Au prochain lancement| Popup détaillée avec bouton "Voir détails" | + | Email | Dans l'heure | Notification complète avec lien d'appel | + Et chaque canal contient un lien vers les détails complets + + Scénario: Notification push immédiate + Étant donné que mon contenu vient d'être modéré + Quand la sanction est appliquée + Alors je reçois une notification push immédiate + Et le message est court: "⚠️ Votre contenu a été modéré" + Et je peux cliquer pour voir les détails + Et la notification utilise Firebase Cloud Messaging (Android) ou APNs (iOS) + Et le coût est de 0€ + + Scénario: Popup in-app au prochain lancement + Étant donné que mon contenu a été modéré + Quand j'ouvre l'application + Alors une popup détaillée s'affiche automatiquement + Et la popup contient: + | élément | description | + | Titre du contenu | "Mon podcast #42" | + | Icône d'avertissement | ⚠️ | + | Catégorie violée | 🚫 Haine & violence | + | Sanction | Strike 2/4 - Suspension 7 jours | + | Bouton "Voir détails" | Redirige vers page détaillée | + | Bouton "Compris" | Ferme la popup | + Et je ne peux pas fermer la popup sans l'avoir vue + + Scénario: Email de notification complet dans l'heure + Étant donné que mon contenu a été modéré à 14:00 + Quand la sanction est appliquée + Alors je reçois un email avant 15:00 (dans l'heure) + Et l'objet de l'email est "Modération de votre contenu \"[Titre du contenu]\"" + Et l'email contient toutes les informations détaillées + Et le coût est d'environ 0.001€ par email (Brevo, Resend) + + # 14.3.2 - Détail de la sanction + + Scénario: Email de notification complet et structuré + Étant donné que mon contenu "Mon podcast #42" a été modéré + Quand je reçois l'email de notification + Alors l'email contient la structure suivante: + """ + Objet : Modération de votre contenu "Mon podcast #42" + + Bonjour @mon_pseudo, + + Votre contenu "Mon podcast #42" publié le 15/01/2026 a été modéré. + + Catégorie violée : 🚫 Haine & violence (Article 3.2 CGU) + Raison : Propos discriminatoires envers un groupe de personnes + + Extrait audio concerné : 3:42-4:15 + Transcription : "[passage problématique surligné en rouge]" + + Sanction : Strike 2/4 + Conséquence : Suspension de votre compte pendant 7 jours + + Vous pouvez contester cette décision sous 7 jours : + [Lien formulaire d'appel] + + L'équipe RoadWave + """ + + Scénario: Page détaillée de la sanction in-app + Étant donné que je clique sur "Voir détails" dans la notification + Quand la page détaillée s'affiche + Alors je vois les 6 éléments obligatoires: + | élément | contenu | + | 1. Catégorie violée | 🚫 Haine & violence (Article 3.2 CGU) | + | 2. Raison détaillée | Explication claire et non juridique | + | 3. Extrait audio | Timestamp exact: 3:42-4:15 | + | 4. Transcription | Texte problématique surligné en rouge | + | 5. Gravité | Strike actuel + conséquences (Strike 2/4, 7j susp) | + | 6. Recours | Lien formulaire d'appel + délai 7j | + + Scénario: Affichage du passage problématique avec timestamp + Étant donné que la page détaillée de la sanction est affichée + Quand je consulte l'extrait audio concerné + Alors le timestamp exact est affiché: "3:42-4:15" + Et je peux écouter uniquement cette portion de l'audio + Et un player audio intégré permet l'écoute du passage + Et la transcription correspondante est affichée en dessous + Et les mots/phrases problématiques sont surlignés en rouge + + Scénario: Référence précise aux CGU + Étant donné que la sanction fait référence à l'Article 3.2 des CGU + Quand je clique sur "Article 3.2" + Alors je suis redirigé vers la section correspondante des CGU + Et la section "Haine & violence" est mise en évidence + Et je peux lire exactement ce qui est interdit + Et cela m'aide à comprendre mon erreur + + Scénario: Gravité de la sanction avec système de strikes + Étant donné que c'est mon 2ème strike + Quand je consulte les détails de la sanction + Alors je vois clairement "Strike 2/4" + Et les conséquences sont explicitées: + """ + Strike 2/4 - Suspension 7 jours + + Que se passe-t-il ensuite? + - Strike 3: Suspension 30 jours + - Strike 4: Ban définitif + + Comment éviter les strikes? + - Lire et respecter les règles de la communauté + - Réhabilitation: -1 strike tous les 6 mois sans incident + """ + Et je comprends l'escalade des sanctions + + # 14.3.3 - Processus d'appel + + Scénario: Accès au formulaire d'appel depuis la notification + Étant donné que j'ai reçu une notification de modération + Quand je clique sur "Contester cette décision" + Alors je suis redirigé vers le formulaire d'appel + Et le formulaire est pré-rempli avec les informations de la sanction + Et je peux commencer à rédiger mon appel + + Scénario: Accès au formulaire d'appel depuis "Mes sanctions" + Étant donné que j'ai reçu une sanction il y a 2 jours + Quand j'ouvre "Profil créateur > Mes sanctions" + Alors je vois la liste de mes sanctions + Et chaque sanction a un bouton "Faire appel" (si délai <7j) + Et je peux accéder au formulaire d'appel + + Scénario: Structure du formulaire d'appel + Étant donné que j'ouvre le formulaire d'appel + Quand le formulaire s'affiche + Alors je vois les champs suivants: + | champ | type | obligatoire | description | + | Sanction contestée | Pré-rempli (readonly) | oui | "Strike 2 - Podcast #42" | + | Raison de l'appel | Texte (50-1000 car) | oui | Explication courte de la contestation | + | Arguments détaillés | Zone texte enrichie | oui | Arguments complets | + | Preuves | Upload fichiers | non | Max 5 fichiers, 10 MB total | + Et tous les champs obligatoires sont marqués d'un astérisque + + Scénario: Validation du formulaire d'appel + Étant donné que je remplis le formulaire d'appel + Quand je clique sur "Soumettre l'appel" + Alors le système valide les champs obligatoires + Et si un champ obligatoire est vide, une erreur s'affiche + Et si la raison fait moins de 50 caractères, une erreur s'affiche + Et si tout est valide, l'appel est soumis + + Scénario: Confirmation après soumission de l'appel + Étant donné que j'ai soumis un appel valide + Quand l'appel est enregistré + Alors un numéro de ticket unique est généré: "#MOD-2026-00142" + Et un email de confirmation est envoyé: + """ + Votre appel #MOD-2026-00142 a été reçu. + Nous l'examinerons sous 72h maximum. + Vous serez notifié de la décision par email et in-app. + """ + Et le statut de l'appel est "En cours d'examen" + Et je peux suivre le statut dans "Mes sanctions" + + Scénario: Délai de soumission de 7 jours maximum + Étant donné que j'ai reçu une sanction le 2026-01-15 + Quand j'essaie de faire appel le 2026-01-25 (10 jours plus tard) + Alors le formulaire d'appel est désactivé + Et un message s'affiche: + """ + Le délai de 7 jours pour faire appel est dépassé. + Cette décision est désormais définitive. + """ + Et je ne peux plus contester la sanction + + Scénario: Bouton "Faire appel" visible si délai respecté + Étant donné que j'ai reçu une sanction il y a 3 jours + Quand je consulte "Mes sanctions" + Alors le bouton "Faire appel" est actif + Et un compteur indique "4 jours restants pour faire appel" + Et je peux cliquer pour soumettre un appel + + # 14.3.4 - Délai de réponse pour appel + + Scénario: SLA de 72h garanti pour appel standard + Étant donné que j'ai soumis un appel standard le lundi à 10:00 + Quand l'appel est en cours de traitement + Alors un modérateur senior est assigné + Et l'appel doit être traité avant jeudi 10:00 (72h - 3 jours ouvrés) + Et je reçois une réponse dans ce délai + + Scénario: Appel complexe avec notification intermédiaire + Étant donné que j'ai soumis un appel complexe + Et que le traitement nécessite plus de 72h + Quand 3 jours se sont écoulés + Alors je reçois un email de notification intermédiaire: + """ + Votre appel #MOD-2026-00142 est en cours d'examen approfondi. + Nous vous répondrons sous 2 jours maximum. + """ + Et l'appel est traité sous 5 jours ouvrés au total + Et un modérateur senior + admin modération examinent le cas + + Scénario: Appel CRITIQUE traité en 24h + Étant donné que j'ai reçu une suspension longue ou un ban + Et que je soumets un appel + Quand l'appel est classé en priorité CRITIQUE + Alors l'admin modération traite l'appel sous 24h + Et je reçois une réponse rapide + Et le cas est examiné en priorité absolue + + Scénario: Réponse finale détaillée - Appel accepté + Étant donné que mon appel est accepté + Quand je reçois la réponse finale + Alors l'email contient: + | élément | contenu | + | Décision | Annulation de la sanction | + | Justification | Explication de pourquoi l'appel est accepté | + | Actions | Strike retiré, suspension annulée, contenu rétabli | + | Définitif | "Cette décision est définitive" | + Et le strike est retiré de mon compte + Et le contenu est rétabli sur la plateforme + Et je peux continuer normalement + + Scénario: Réponse finale détaillée - Appel rejeté + Étant donné que mon appel est rejeté + Quand je reçois la réponse finale + Alors l'email contient: + | élément | contenu | + | Décision | Maintien de la sanction | + | Justification | Explication de pourquoi l'appel est rejeté | + | Actions | Sanction maintenue, strike conservé | + | Définitif | "Cette décision est définitive" | + Et la sanction reste active + Et je ne peux pas faire de second appel + Et je dois respecter la suspension + + Scénario: Réponse finale - Réduction de sanction + Étant donné que mon appel est partiellement accepté + Quand je reçois la réponse finale + Alors la décision est "Réduction de sanction" + Et l'email explique: + """ + Après examen de votre appel, nous reconnaissons des circonstances atténuantes. + La sanction est réduite: + - Strike 2 → Strike 1 + - Suspension 7 jours → Suspension 3 jours + + Cette décision est définitive. + """ + Et le strike est réduit + Et la suspension est raccourcie + Et je suis notifié de la nouvelle date de fin + + Scénario: Suivi du statut de l'appel in-app + Étant donné que j'ai soumis un appel + Quand je consulte "Mes sanctions" + Alors je vois le statut actuel de l'appel: + | statut | badge | couleur | + | En cours d'examen | En cours 🔍 | orange | + | Appel accepté | Accepté ✓ | vert | + | Appel rejeté | Rejeté ✗ | rouge | + | Sanction réduite | Partiellement accepté| bleu | + Et une notification badge m'alerte quand le statut change + + # Transparence et traçabilité + + Scénario: Historique complet des sanctions visible + Étant donné que je suis un créateur + Quand j'ouvre "Profil créateur > Mes sanctions" + Alors je vois la liste complète de mes sanctions passées: + | colonne | description | + | Date | 15/01/2026 | + | Contenu | "Mon podcast #42" | + | Catégorie | 🚫 Haine & violence | + | Sanction | Strike 2 - Suspension 7j | + | Statut | Active / Terminée / Annulée | + | Appel | Aucun / Accepté / Rejeté | + Et les sanctions sont triées par date décroissante + + Scénario: Conformité DSA - Transparence obligatoire + Étant donné que le système de sanction est en place + Quand un audit DSA est effectué + Alors chaque sanction contient: + | élément DSA | présent | + | Référence précise à la règle violée | oui | + | Explication claire et compréhensible | oui | + | Preuve (extrait + transcription) | oui | + | Possibilité de recours (appel) | oui | + | Délai de recours clairement indiqué | oui | + | Réponse motivée au recours | oui | + Et le système est conforme au Digital Services Act + + # Pas de second appel + + Scénario: Décision définitive après premier appel + Étant donné que mon premier appel a été rejeté + Quand j'essaie de faire un second appel + Alors le bouton "Faire appel" est désactivé + Et un message s'affiche: "Cette décision est définitive. Aucun second appel n'est possible." + Et je ne peux plus contester la sanction + Et je dois respecter la décision finale + + # Coût des notifications + + Scénario: Coût des notifications multi-canal + Étant donné que 100 sanctions sont appliquées en un mois + Quand on calcule le coût des notifications + Alors le coût total est d'environ 0.10€: + | canal | coût unitaire | coût pour 100 | + | Email | 0.001€ | 0.10€ | + | Push | 0€ | 0€ | + | In-app | 0€ | 0€ | + Et le coût est négligeable même à grande échelle diff --git a/features/moderation/signalement.feature b/features/moderation/signalement.feature new file mode 100644 index 0000000..0da0aef --- /dev/null +++ b/features/moderation/signalement.feature @@ -0,0 +1,224 @@ +# language: fr + +@moderation @reporting +Fonctionnalité: Signalement de contenu inapproprié + + # 14.1 - Signalement utilisateur + + Contexte: + Étant donné que je suis un utilisateur connecté + Et que je suis en train d'écouter un contenu + + # 14.1.1 - Catégories de signalement + + Scénario: Affichage du formulaire de signalement + Étant donné que j'écoute un contenu inapproprié + Quand j'ouvre le menu du contenu + Et que je clique sur "Signaler" + Alors un formulaire de signalement s'affiche + Et le formulaire contient une liste déroulante "Catégorie du problème" + Et le formulaire contient un champ texte "Commentaire (optionnel)" + Et le formulaire contient un bouton "Envoyer le signalement" + + Scénario: Liste des 7 catégories prédéfinies + Étant donné que le formulaire de signalement est affiché + Quand je clique sur la liste déroulante "Catégorie du problème" + Alors je vois les 7 catégories suivantes: + | icône | catégorie | description | + | 🚫 | Haine & violence | Incitation à la haine, discrimination, menaces | + | 🔞 | Contenu sexuel | Pornographie, contenu explicite | + | ⚖️ | Illégalité | Terrorisme, apologie de crimes | + | 🎵 | Droits d'auteur | Musique/contenu protégé non autorisé | + | 📧 | Spam | Publicité non sollicitée, répétition | + | ❌ | Fausse information | Désinformation sur santé, sécurité routière | + | 🔧 | Autre | Champ texte obligatoire si sélectionné | + Et chaque catégorie a une description claire + + Scénario: Sélection de la catégorie "Haine & violence" + Étant donné que le formulaire de signalement est affiché + Quand je sélectionne la catégorie "🚫 Haine & violence" + Alors la catégorie est sélectionnée + Et la description "Incitation à la haine, discrimination, menaces" s'affiche + Et je peux passer au champ commentaire + + Scénario: Catégorie "Autre" nécessite un commentaire obligatoire + Étant donné que le formulaire de signalement est affiché + Quand je sélectionne la catégorie "🔧 Autre" + Alors le champ "Commentaire" devient obligatoire + Et un message s'affiche: "Veuillez décrire le problème (obligatoire)" + Et le placeholder change en "Décrivez le problème rencontré" + Et je ne peux pas envoyer le signalement sans commentaire + + # 14.1.2 - Commentaire du signaleur + + Scénario: Champ commentaire optionnel avec incitation + Étant donné que le formulaire de signalement est affiché + Et que j'ai sélectionné une catégorie autre que "Autre" + Quand je consulte le champ "Commentaire" + Alors le champ est optionnel (pas d'astérisque rouge) + Et le placeholder indique "Décrivez le problème (optionnel mais recommandé)" + Et la limite de caractères est de 500 + Et un compteur affiche "0/500" + + Scénario: Envoi de signalement sans commentaire + Étant donné que j'ai sélectionné la catégorie "📧 Spam" + Et que je n'ai pas rempli le champ commentaire + Quand je clique sur "Envoyer le signalement" + Alors le signalement est envoyé avec succès + Et aucune erreur de validation ne s'affiche + Et le commentaire est enregistré comme vide + + Scénario: Envoi de signalement avec commentaire + Étant donné que j'ai sélectionné la catégorie "🚫 Haine & violence" + Et que j'ai saisi le commentaire "Le créateur tient des propos discriminatoires à 2:30" + Quand je clique sur "Envoyer le signalement" + Alors le signalement est envoyé avec succès + Et le commentaire est enregistré avec le signalement + Et il sera visible par les modérateurs + + Scénario: Limite de 500 caractères pour le commentaire + Étant donné que le formulaire de signalement est affiché + Quand je saisis un commentaire de 501 caractères + Alors le champ limite automatiquement à 500 caractères + Et le compteur affiche "500/500" + Et les caractères supplémentaires ne sont pas acceptés + + # 14.1.3 - Confirmation après signalement + + Scénario: Toast de confirmation après signalement + Étant donné que j'ai envoyé un signalement + Quand le signalement est enregistré + Alors un toast notification s'affiche + Et le toast contient le message "✓ Signalement envoyé. Nous l'examinerons sous 24-48h." + Et le toast s'affiche pendant 5 secondes + Et le toast contient un bouton "Voir mes signalements" + Et je peux fermer le toast manuellement avec un bouton X + + Scénario: Accès à l'historique des signalements via le toast + Étant donné que le toast de confirmation est affiché + Quand je clique sur "Voir mes signalements" + Alors je suis redirigé vers la page "Mes signalements" + Et je vois la liste de tous mes signalements + Et le signalement que je viens d'envoyer apparaît en premier + + Scénario: Historique personnel des signalements + Étant donné que j'ai envoyé 3 signalements précédemment + Quand j'ouvre "Profil > Mes signalements" + Alors je vois la liste de mes 3 signalements + Et chaque signalement affiche: + | information | description | + | Titre du contenu | "Podcast #42" | + | Créateur | @pseudo_createur | + | Catégorie | 🚫 Haine & violence | + | Date | 15/01/2026 | + | Statut | En cours / Traité / Rejeté | + | Mon commentaire | Texte que j'ai saisi | + Et les signalements sont triés par date décroissante + + Plan du Scénario: Statuts possibles d'un signalement + Étant donné que j'ai envoyé un signalement + Quand le statut du signalement est "" + Alors le badge affiché est "" + Et la couleur du badge est "" + + Exemples: + | statut | badge | couleur | + | En cours | En cours | orange | + | Traité | Traité ✓ | vert | + | Rejeté | Rejeté ✗ | rouge | + + Scénario: Notification in-app si action prise + Étant donné que j'ai signalé un contenu il y a 24h + Quand le modérateur traite mon signalement + Et que le contenu est effectivement retiré + Alors je reçois une notification in-app + Et la notification indique "Votre signalement a été traité. Le contenu a été retiré." + Et le statut de mon signalement passe à "Traité ✓" + Et je peux voir les détails de l'action prise + + Scénario: Notification si signalement rejeté + Étant donné que j'ai signalé un contenu + Quand le modérateur rejette mon signalement + Alors je reçois une notification in-app + Et la notification indique "Votre signalement a été examiné. Le contenu ne viole pas les règles de la communauté." + Et le statut de mon signalement passe à "Rejeté ✗" + Et je peux voir la raison du rejet + + # Signalements multiples du même contenu + + Scénario: Un contenu peut être signalé plusieurs fois + Étant donné qu'un contenu a déjà été signalé par 5 autres utilisateurs + Quand je signale le même contenu + Alors mon signalement est enregistré indépendamment + Et le compteur de signalements du contenu passe à 6 + Et mon signalement rejoint la file d'attente de modération + Et les signalements cumulés augmentent la priorité de traitement + + Scénario: Limite de signalements par utilisateur + Étant donné que j'ai déjà signalé le même contenu il y a 2 jours + Quand j'essaie de signaler à nouveau le même contenu + Alors un message m'informe "Vous avez déjà signalé ce contenu" + Et le formulaire de signalement n'est pas affiché + Et je peux consulter le statut de mon signalement précédent + + # Signalements abusifs + + Scénario: Détection de signalements abusifs répétés + Étant donné que j'ai envoyé 10 signalements ce mois-ci + Et que 8 d'entre eux ont été rejetés comme infondés + Quand j'essaie d'envoyer un nouveau signalement + Alors mon compte est marqué comme "signaleur suspect" + Et un avertissement s'affiche: + """ + Attention: vos signalements récents ont été majoritairement rejetés. + Les signalements abusifs peuvent entraîner des sanctions. + """ + Et je peux toujours envoyer le signalement + Mais mes futurs signalements auront une priorité réduite + + Scénario: Sanction pour signalements abusifs graves + Étant donné que j'ai envoyé 20 signalements abusifs en 1 mois + Et que tous ont été rejetés comme volontairement faux + Quand le modérateur détecte le pattern abusif + Alors mon compte reçoit un avertissement formel + Et je perds la possibilité de signaler pendant 30 jours + Et je reçois un email m'expliquant la sanction + + # Signalement depuis différents points de l'app + + Scénario: Signalement depuis le player audio + Étant donné que j'écoute un contenu + Quand j'ouvre le menu "⋮" du player + Alors je vois l'option "Signaler" + Et je peux ouvrir le formulaire de signalement + + Scénario: Signalement depuis la page de détails du contenu + Étant donné que je consulte la page de détails d'un contenu + Quand je clique sur le bouton "⋮" en haut à droite + Alors je vois l'option "Signaler" + Et je peux ouvrir le formulaire de signalement + + Scénario: Signalement depuis l'historique d'écoute + Étant donné que je consulte mon historique d'écoute + Quand je clique sur "⋮" à côté d'un contenu passé + Alors je vois l'option "Signaler" + Et je peux signaler ce contenu même si je ne l'écoute plus actuellement + + # Anonymat du signaleur + + Scénario: Identité du signaleur anonyme pour le créateur + Étant donné que j'ai signalé un contenu + Quand le créateur est notifié de la modération + Alors mon identité reste anonyme + Et le créateur ne peut pas savoir qui a signalé + Et seuls les modérateurs ont accès à l'identité du signaleur + + # Coût de la solution + + Scénario: Coût du système de signalement + Étant donné que le système de signalement est en place + Quand on calcule le coût + Alors le coût est de 0€ + Et le formulaire est développé en interne + Et aucun service tiers n'est utilisé + Et les notifications in-app sont gratuites diff --git a/features/moderation/traitement-signalements.feature b/features/moderation/traitement-signalements.feature new file mode 100644 index 0000000..05d7fe8 --- /dev/null +++ b/features/moderation/traitement-signalements.feature @@ -0,0 +1,268 @@ +# language: fr + +@moderation @processing +Fonctionnalité: Traitement des signalements par l'IA et les modérateurs + + # 14.2 - Traitement des signalements + + Contexte: + Étant donné que le système de modération est actif + + # 14.2.1 - IA pré-filtre (Whisper + NLP) + + Scénario: Signalement ajouté à la file d'attente asynchrone + Étant donné qu'un utilisateur envoie un signalement pour un contenu audio + Quand le signalement est reçu + Alors le signalement est ajouté à la file d'attente asynchrone + Et un worker de traitement est déclenché + Et le traitement se fait en arrière-plan sans bloquer l'utilisateur + + Scénario: Transcription automatique avec Whisper large-v3 + Étant donné qu'un contenu audio signalé dure 5 minutes + Quand le worker de traitement démarre + Alors le système utilise Whisper large-v3 pour transcrire l'audio + Et la transcription est en self-hosted (pas de service cloud) + Et le texte transcrit est enregistré en base de données + Et le délai de transcription est de 1-3 minutes + + Plan du Scénario: Délai de transcription selon durée audio + Étant donné qu'un contenu audio signalé dure minutes + Quand le système transcrit l'audio + Alors la transcription prend environ + + Exemples: + | duree | delai | + | 2 | 1-3 minutes | + | 10 | 3-10 minutes | + | 45 | 10-20 minutes | + + Scénario: Analyse automatique du contenu transcrit + Étant donné que la transcription audio est terminée + Quand le système analyse le texte transcrit + Alors les analyses suivantes sont effectuées: + | analyse | technologie | + | Analyse de sentiment | distilbert-base-uncased | + | Détection de haine | facebook/roberta-hate-speech | + | Mots-clés interdits | Liste noire FR/EN + regex | + Et chaque analyse génère un score de confiance (0-100%) + + Scénario: Génération du score de confiance IA + Étant donné que toutes les analyses sont terminées + Quand le système calcule le score final + Alors un score de confiance IA entre 0-100% est généré + Et le score indique la probabilité que le contenu viole les règles + Et la catégorie la plus probable est identifiée + Et les timestamps des passages problématiques sont extraits + + Scénario: Détection automatique de contenu clairement inapproprié + Étant donné qu'un contenu contient des insultes graves et répétées + Quand l'IA analyse la transcription + Alors le score de confiance IA est >95% + Et la catégorie détectée est "Haine & violence" + Et les passages problématiques sont identifiés avec timestamps: + | timestamp | texte problématique | + | 02:15 | [insulte discriminatoire] | + | 03:42 | [propos haineux] | + Et le signalement est classé en priorité CRITIQUE + + # 14.2.2 - Délais de traitement (SLA) + + Plan du Scénario: SLA selon priorité du signalement + Étant donné qu'un signalement a une priorité "" + Quand le signalement entre en file d'attente + Alors le délai de traitement cible est "" + Et le responsable du traitement est "" + + Exemples: + | priorite | delai | responsable | + | CRITIQUE | <2h (24/7) | Modérateur senior (astreinte) | + | HAUTE | <24h (jours ouvrés) | Modérateur junior/senior | + | MOYENNE | <24h (jours ouvrés) | Modérateur junior | + | BASSE | <72h (jours ouvrés) | Modérateur junior | + + Scénario: Traitement automatique pour score IA >95% + Étant donné qu'un signalement a un score IA de 97% + Et que la catégorie détectée est "Spam" (évidente) + Quand le système évalue le signalement + Alors une action automatique immédiate est déclenchée + Et le contenu est retiré automatiquement + Et le créateur est notifié de la modération + Et le créateur peut faire appel de la décision + Et un modérateur senior vérifie l'action a posteriori + + Scénario: Signalement CRITIQUE traité en moins de 2h + Étant donné qu'un signalement de priorité CRITIQUE est reçu à 14:00 + Et que le contenu concerne une menace de violence + Quand le signalement est assigné à un modérateur senior d'astreinte + Alors le modérateur est alerté immédiatement (push + SMS) + Et le signalement est traité avant 16:00 (2h) + Et une décision est prise et appliquée + Et les autorités peuvent être contactées si nécessaire + + Scénario: Astreinte modérateur 24/7 pour signalements CRITIQUES + Étant donné qu'un signalement CRITIQUE est reçu un dimanche à 03:00 + Quand le signalement est classé en priorité CRITIQUE + Alors le modérateur senior d'astreinte est alerté + Et le signalement est traité dans les 2h (avant 05:00) + Et le service d'astreinte garantit une disponibilité 24/7 + + Scénario: Signalement HAUTE priorité traité en moins de 24h + Étant donné qu'un signalement de priorité HAUTE est reçu lundi à 10:00 + Et que le contenu concerne du harcèlement + Quand le signalement entre en file d'attente + Alors le signalement est assigné à un modérateur (junior ou senior) + Et le signalement est traité avant mardi 10:00 (24h jours ouvrés) + Et une décision est prise et appliquée + + Scénario: Signalement BASSE priorité traité en moins de 72h + Étant donné qu'un signalement de priorité BASSE est reçu lundi à 10:00 + Et que le contenu concerne des tags incorrects + Quand le signalement entre en file d'attente + Alors le signalement est traité avant jeudi 10:00 (72h jours ouvrés) + Et un modérateur junior peut traiter ce type de signalement + + # 14.2.3 - Priorisation automatique + + Scénario: Calcul du score de priorité + Étant donné qu'un signalement a les caractéristiques suivantes: + | caractéristique | valeur | + | Score IA | 85% | + | Signalements cumulés | 3 | + | Fiabilité du signaleur | 75% | + Quand le système calcule la priorité + Alors la formule appliquée est: + """ + Priorité = (Score_IA × 0.7) + (Signalements_cumulés × 0.2) + (Fiabilité_signaleur × 0.1) + """ + Et le score de priorité est: (85 × 0.7) + (3 × 0.2) + (75 × 0.1) = 67.5 + Et le signalement est classé en priorité MOYENNE + + Plan du Scénario: Classification selon score de priorité + Étant donné qu'un signalement a un score de priorité de + Quand le système classe le signalement + Alors la priorité assignée est "" + Et le signalement entre dans la file "" + + Exemples: + | score | priorite | file | + | 95 | CRITIQUE | Immédiate | + | 82 | HAUTE | Prioritaire | + | 55 | MOYENNE | Normale | + | 25 | BASSE | Différée | + + Scénario: Boost de priorité avec signalements cumulés + Étant donné qu'un contenu a été signalé par 1 utilisateur avec un score IA de 60% + Et que le signalement est classé en priorité MOYENNE (score 42) + Quand 5 autres utilisateurs signalent le même contenu + Alors le nombre de signalements cumulés passe à 6 + Et le score de priorité augmente significativement + Et le signalement peut passer en priorité HAUTE + Et le traitement est accéléré + + Scénario: Impact de la fiabilité du signaleur + Étant donné qu'un utilisateur de confiance (90% fiabilité) envoie un signalement + Et qu'un utilisateur suspect (20% fiabilité) envoie un signalement similaire + Quand le système calcule les priorités + Alors le signalement de l'utilisateur de confiance a un score plus élevé + Et son signalement est traité en priorité + Et le signalement de l'utilisateur suspect est traité plus tard + + Scénario: Évolution du score de fiabilité du signaleur + Étant donné qu'un utilisateur a envoyé 10 signalements + Et que 8 d'entre eux ont été acceptés par les modérateurs + Quand le système calcule son score de fiabilité + Alors le score est de 80% (8 acceptés / 10 total) + Et ses futurs signalements auront plus de poids + Et il peut devenir "utilisateur de confiance" + + # File d'attente intelligente + + Scénario: Files d'attente séparées par priorité + Étant donné que 50 signalements sont en attente + Quand le système organise la file d'attente + Alors les signalements sont répartis dans les files suivantes: + | file | nombre | priorité | + | Immédiate (24/7) | 5 | CRITIQUE | + | Prioritaire | 15 | HAUTE | + | Normale | 20 | MOYENNE | + | Différée | 10 | BASSE | + Et les modérateurs traitent en priorité la file Immédiate + + Scénario: Modérateurs assignés selon compétences + Étant donné qu'un signalement complexe de harcèlement est reçu + Quand le système assigne un modérateur + Alors un modérateur senior est prioritairement assigné + Et les modérateurs juniors peuvent traiter les cas simples (spam, tags) + Et les modérateurs seniors traitent les cas complexes (haine, violence, appels) + + # Technologies opensource + + Scénario: Stack technique 100% opensource + Étant donné que le système de modération IA est déployé + Quand on analyse les technologies utilisées + Alors toutes les technologies sont opensource: + | composant | technologie | hébergement | + | Transcription | Whisper large-v3 | Self-hosted | + | Analyse sentiment | distilbert-base-uncased | Self-hosted | + | Détection haine | facebook/roberta-hate-speech | Self-hosted | + | Mots-clés interdits | Liste noire FR/EN + regex | PostgreSQL | + Et aucune dépendance à Google, AWS, Azure + + # Coût infrastructure + + Plan du Scénario: Coût selon phase du projet + Étant donné que RoadWave est en phase "" + Quand on calcule le coût de l'infrastructure IA + Alors le coût mensuel est "" + + Exemples: + | phase | cout | + | MVP | 0-50€ (CPU) | + | Scale | 50-200€ (GPU VPS) | + + Scénario: Processing asynchrone en MVP avec CPU + Étant donné que RoadWave est en phase MVP + Et que le volume est <1000 signalements/mois + Quand le système traite les signalements + Alors un serveur CPU standard est suffisant + Et le coût est de 0€ (serveur existant) + Et le processing asynchrone absorbe les pics de charge + Et les délais restent acceptables (1-20 minutes) + + Scénario: Scaling avec GPU pour gros volumes + Étant donné que RoadWave reçoit >1000 signalements/jour + Quand le système nécessite un scaling + Alors un VPS avec GPU est requis + Et le coût passe à 50-200€/mois + Et les délais de transcription sont divisés par 5-10 + Et le système peut gérer 10 000+ signalements/mois + + # Logs et audit + + Scénario: Logs d'audit pour chaque traitement + Étant donné qu'un signalement est traité + Quand une action est prise (rejet, acceptation, sanction) + Alors un log d'audit complet est créé: + | champ | description | + | signalement_id | ID unique du signalement | + | content_id | ID du contenu signalé | + | ia_score | Score de confiance IA | + | ia_category | Catégorie détectée par IA | + | priority | CRITIQUE / HAUTE / MOYENNE / BASSE | + | moderator_id | ID du modérateur assigné | + | action_taken | Retiré / Rejeté / Strike | + | processing_time | Durée du traitement | + | timestamp | Date et heure de la décision | + Et le log est conservé pour conformité DSA + Et les logs sont anonymisés après 3 ans (RGPD) + + # Conformité DSA + + Scénario: Traçabilité complète pour conformité DSA + Étant donné que le système de modération est actif + Quand un audit DSA est effectué + Alors toutes les actions de modération sont tracées + Et les délais de traitement sont mesurés et respectés + Et les décisions sont justifiées et documentées + Et la transparence vis-à-vis des utilisateurs est garantie + Et le système est conforme au Digital Services Act diff --git a/features/monetisation/conditions-activation.feature b/features/monetisation/conditions-activation.feature new file mode 100644 index 0000000..a6bc012 --- /dev/null +++ b/features/monetisation/conditions-activation.feature @@ -0,0 +1,263 @@ +# language: fr +Fonctionnalité: Conditions d'activation de la monétisation + En tant que créateur + Je veux pouvoir activer la monétisation quand je remplis les critères + Afin de générer des revenus avec mes contenus + + Contexte: + Étant donné que l'API RoadWave est disponible + Et que je suis connecté en tant que créateur + + Scénario: Critère 1 - Ancienneté de 3 mois validée + Étant donné que mon compte a été créé il y a 91 jours + Quand je consulte les critères de monétisation + Alors le critère "Ancienneté ≥ 3 mois" est validé ✅ + + Scénario: Critère 1 - Ancienneté insuffisante + Étant donné que mon compte a été créé il y a 60 jours + Quand je consulte les critères de monétisation + Alors le critère "Ancienneté ≥ 3 mois" n'est pas validé ❌ + Et je vois "Encore 30 jours avant d'être éligible" + + Scénario: Critère 2 - 500 abonnés atteints + Étant donné que j'ai exactement 500 abonnés + Quand je consulte les critères de monétisation + Alors le critère "≥ 500 abonnés" est validé ✅ + + Scénario: Critère 2 - Pas assez d'abonnés + Étant donné que j'ai 347 abonnés + Quand je consulte les critères de monétisation + Alors le critère "≥ 500 abonnés" n'est pas validé ❌ + Et je vois "Encore 153 abonnés nécessaires" + + Scénario: Critère 3 - 10 000 écoutes complètes atteintes + Étant donné que mes contenus ont cumulé 10 487 écoutes complètes + Quand je consulte les critères de monétisation + Alors le critère "≥ 10 000 écoutes complètes" est validé ✅ + + Scénario: Critère 3 - Écoutes incomplètes non comptabilisées + Étant donné que mes contenus ont: + | type écoute | nombre | + | Écoutes complètes | 8 500 | + | Écoutes <80% | 3 000 | + Quand je consulte les critères de monétisation + Alors seules les 8 500 écoutes complètes comptent + Et je vois "Encore 1 500 écoutes complètes nécessaires" + + Scénario: Critère 4 - Aucun strike actif + Étant donné que je n'ai aucun strike actif + Et que je n'ai eu aucun contenu modéré dans les 6 derniers mois + Quand je consulte les critères de monétisation + Alors le critère "Fiabilité" est validé ✅ + + Scénario: Critère 4 - Strike actif bloque l'éligibilité + Étant donné que j'ai 1 strike actif pour contenu inapproprié + Quand je consulte les critères de monétisation + Alors le critère "Fiabilité" n'est pas validé ❌ + Et je vois "Vous devez résoudre votre strike avant d'être éligible" + + Scénario: Critère 4 - Contenu modéré dans les 6 derniers mois + Étant donné que je n'ai pas de strike actif + Mais qu'un de mes contenus a été modéré il y a 4 mois + Quand je consulte les critères de monétisation + Alors le critère "Fiabilité" n'est pas validé ❌ + Et je vois "Attendre 2 mois après le dernier contenu modéré" + + Scénario: Critère 5 - 5 contenus publiés dans les 90 derniers jours + Étant donné que j'ai publié: + | date de publication | titre | + | Il y a 15 jours | Contenu 1 | + | Il y a 30 jours | Contenu 2 | + | Il y a 45 jours | Contenu 3 | + | Il y a 60 jours | Contenu 4 | + | Il y a 75 jours | Contenu 5 | + Quand je consulte les critères de monétisation + Alors le critère "≥ 5 contenus publiés dans les 90 derniers jours" est validé ✅ + + Scénario: Critère 5 - Contenus trop anciens ne comptent pas + Étant donné que j'ai publié: + | date de publication | titre | + | Il y a 15 jours | Contenu 1 | + | Il y a 30 jours | Contenu 2 | + | Il y a 95 jours | Contenu 3 | + | Il y a 120 jours | Contenu 4 | + Quand je consulte les critères de monétisation + Alors seuls 2 contenus comptent (dans les 90 jours) + Et je vois "Encore 3 contenus à publier dans les 90 prochains jours" + + Scénario: Tous les critères validés - Bouton disponible + Étant donné que tous mes critères sont validés: + | critère | statut | + | Ancienneté ≥ 3 mois | ✅ | + | ≥ 500 abonnés | ✅ | + | ≥ 10 000 écoutes | ✅ | + | Fiabilité | ✅ | + | Régularité (5 contenus) | ✅ | + Quand j'accède à mon profil créateur + Alors le bouton "Demander la monétisation" est actif + Et je peux cliquer pour démarrer le KYC + + Scénario: Critères incomplets - Bouton grisé avec progression + Étant donné que mes critères sont: + | critère | statut | progression | + | Ancienneté ≥ 3 mois | ✅ | 100% | + | ≥ 500 abonnés | ❌ | 347/500 (69%) | + | ≥ 10 000 écoutes | ❌ | 8500/10000 (85%) | + | Fiabilité | ✅ | 100% | + | Régularité (5 contenus) | ✅ | 100% | + Quand j'accède à mon profil créateur + Alors le bouton "Demander la monétisation" est grisé + Et je vois la progression détaillée de chaque critère + + Scénario: Vérification automatique SQL lors de la demande + Étant donné que je clique sur "Demander la monétisation" + Quand le système vérifie mes critères + Alors une requête SQL est exécutée: + ```sql + SELECT + (created_at <= NOW() - INTERVAL '3 months') AS anciennete_ok, + (SELECT COUNT(*) FROM subscriptions WHERE creator_id = :creator_id) >= 500 AS abonnes_ok, + (SELECT SUM(complete_listens) FROM contents WHERE creator_id = :creator_id) >= 10000 AS ecoutes_ok, + (SELECT COUNT(*) FROM strikes WHERE creator_id = :creator_id AND status = 'active') = 0 AS strikes_ok, + (SELECT COUNT(*) FROM moderated_contents WHERE creator_id = :creator_id AND moderated_at >= NOW() - INTERVAL '6 months') = 0 AS moderation_ok, + (SELECT COUNT(*) FROM contents WHERE creator_id = :creator_id AND published_at >= NOW() - INTERVAL '90 days') >= 5 AS regularite_ok + FROM users WHERE id = :creator_id; + ``` + Et si tous les critères sont TRUE, je suis redirigé vers le KYC + + Scénario: Notification par email quand critères atteints + Étant donné que je viens d'atteindre 500 abonnés + Et que c'était mon dernier critère manquant + Quand le système détecte l'éligibilité + Alors je reçois un email: + """ + Félicitations ! Vous êtes maintenant éligible à la monétisation. + Accédez à votre profil pour activer la monétisation et commencer à gagner des revenus. + """ + + Scénario: Badge "Éligible monétisation" dans profil + Étant donné que je remplis tous les critères + Mais que je n'ai pas encore activé la monétisation + Quand un utilisateur consulte mon profil + Alors il voit un badge "Éligible monétisation 💰" + Et cela renforce ma crédibilité de créateur + + Scénario: Justification anti-fraude - Délai 3 mois + Étant donné qu'un compte suspect crée du contenu frauduleux + Quand le compte est détecté dans les 2 premiers mois + Alors le compte est banni avant d'atteindre les 3 mois + Et le créateur n'a jamais été éligible à la monétisation + Et aucun paiement n'a été effectué + + Scénario: Justification qualité - 10 000 écoutes + Étant donné qu'un créateur produit du contenu de mauvaise qualité + Quand ses contenus ne génèrent que 2 000 écoutes complètes + Alors il ne peut pas activer la monétisation + Et seuls les créateurs avec contenu apprécié sont monétisés + + Scénario: Réduction coût administratif plateforme + Étant donné que RoadWave a 10 000 créateurs inscrits + Et que seuls 500 remplissent tous les critères + Quand le système calcule le coût administratif + Alors seulement 500 KYC sont à gérer (vs 10 000) + Et seulement 500 virements mensuels (vs 10 000) + Et la charge comptable est réduite de 95% + + Scénario: Statistiques publiques pour transparence + Quand un utilisateur consulte la page "Devenir créateur" + Alors il voit les statistiques: + | métrique | valeur exemple | + | Nombre créateurs monétisés | 1 247 | + | Revenus moyens par créateur | 127€/mois | + | Top créateur (anonymisé) | 2 450€/mois | + | Critères d'éligibilité à remplir | 5 critères | + Et cela permet de fixer des attentes réalistes + + Scénario: Cache Redis pour calcul rapide critères + Étant donné que je consulte mes critères de monétisation + Quand le système charge la page + Alors les compteurs sont récupérés depuis Redis: + | clé Redis | exemple valeur | + | creator:[id]:subscribers_count | 347 | + | creator:[id]:complete_listens_total | 8500 | + | creator:[id]:recent_contents_count | 7 | + Et le temps de réponse est <50ms + + Scénario: Mise à jour temps réel des compteurs + Étant donné que je viens de publier un nouveau contenu + Quand un utilisateur écoute ce contenu en entier + Alors le compteur "complete_listens_total" est incrémenté immédiatement + Et si je rafraîchis la page critères, je vois la nouvelle valeur + Et cela encourage les créateurs à continuer de produire + + Scénario: Historique des tentatives d'activation + Étant donné que j'ai tenté d'activer la monétisation il y a 2 mois + Mais que les critères n'étaient pas remplis + Quand j'accède à mes logs d'activité + Alors je vois: + | date | action | résultat | raison | + | 2025-11-15 | Demande monétisation | Refusée | Seulement 300 abonnés | + Et cela m'aide à suivre ma progression + + Scénario: Performance avec 100 000 créateurs + Étant donné que RoadWave a 100 000 créateurs + Et que chacun consulte ses critères 1 fois par jour + Quand le système traite ces requêtes + Alors la table users est indexée sur created_at + Et la table subscriptions est indexée sur creator_id + Et la table contents est indexée sur creator_id et published_at + Et chaque requête reste <50ms grâce aux index + + Scénario: Export des critères pour support client + Étant donné que je contacte le support car je pense être éligible + Quand l'agent support consulte mon compte + Alors il voit un export JSON complet: + ```json + { + "creator_id": "abc123", + "monetization_eligible": false, + "criteria": { + "account_age_days": 75, + "account_age_ok": false, + "subscribers_count": 512, + "subscribers_ok": true, + "complete_listens_total": 11234, + "complete_listens_ok": true, + "active_strikes": 0, + "strikes_ok": true, + "moderated_contents_6m": 1, + "moderation_ok": false, + "recent_contents_90d": 8, + "regularity_ok": true + }, + "blocking_criteria": ["account_age", "moderation_history"] + } + ``` + Et l'agent peut expliquer précisément pourquoi je ne suis pas éligible + + Scénario: Notification 30 jours avant éligibilité probable + Étant donné que mes critères sont: + | critère | statut | progression | + | Ancienneté ≥ 3 mois | ❌ | 60/90 jours | + | Tous les autres critères | ✅ | 100% | + Quand il reste exactement 30 jours avant les 90 jours + Alors je reçois une notification: + """ + Plus que 30 jours avant d'être éligible à la monétisation ! + Préparez vos documents KYC (SIRET, RIB, CNI) pour gagner du temps. + """ + + Scénario: Pas de bypass possible pour amis/influenceurs + Étant donné qu'un créateur influent me contacte directement + Et qu'il demande un bypass des critères + Quand je consulte la politique RoadWave + Alors la réponse est "Aucune exception possible, critères automatiques uniquement" + Et cela garantit l'équité pour tous les créateurs + + Scénario: A/B test futur sur seuils (post-MVP) + Étant donné que RoadWave veut tester des seuils différents + Quand un A/B test est lancé en 2027 + Alors groupe A voit: 500 abonnés, 10 000 écoutes + Et groupe B voit: 300 abonnés, 5 000 écoutes + Et les métriques (taux activation, fraude, qualité) sont comparées + Et le meilleur seuil est déployé définitivement diff --git a/features/monetisation/contenus-premium-exclusifs.feature b/features/monetisation/contenus-premium-exclusifs.feature new file mode 100644 index 0000000..2bffc5e --- /dev/null +++ b/features/monetisation/contenus-premium-exclusifs.feature @@ -0,0 +1,286 @@ +# language: fr +Fonctionnalité: Contenus Premium exclusifs + En tant que créateur monétisé + Je veux pouvoir rendre certains contenus exclusifs aux abonnés Premium + Afin d'inciter les utilisateurs à s'abonner + + Contexte: + Étant donné que je suis un créateur avec la monétisation activée + + Scénario: Toggle "Réservé Premium" lors de la création + Étant donné que je crée un nouveau contenu + Quand j'accède aux options de publication + Alors je vois un toggle "Réservé aux abonnés Premium 👑" + Et je peux l'activer ou le désactiver + + Scénario: Contenu marqué Premium lors de la création + Étant donné que je crée un nouveau contenu + Quand j'active le toggle "Réservé Premium" + Et que je publie le contenu + Alors le champ `is_premium` en base est mis à `true` + Et le contenu est visible uniquement pour les utilisateurs Premium + + Scénario: Contenu gratuit par défaut + Étant donné que je crée un nouveau contenu + Quand je ne touche pas au toggle "Réservé Premium" + Et que je publie le contenu + Alors le champ `is_premium` en base est mis à `false` (défaut) + Et le contenu est accessible à tous les utilisateurs + + Scénario: Modification d'un contenu existant en Premium + Étant donné que j'ai publié un contenu gratuit il y a 2 jours + Quand je modifie le contenu et active le toggle "Réservé Premium" + Et que j'enregistre les modifications + Alors le contenu devient immédiatement Premium + Et les utilisateurs gratuits ne peuvent plus y accéder + + Scénario: Passage d'un contenu Premium en gratuit + Étant donné que j'ai publié un contenu Premium il y a 1 mois + Quand je modifie le contenu et désactive le toggle "Réservé Premium" + Et que j'enregistre les modifications + Alors le contenu devient immédiatement gratuit + Et tous les utilisateurs peuvent maintenant y accéder + + Scénario: Aucune limite sur pourcentage de contenus Premium + Étant donné que je publie 10 nouveaux contenus + Quand je décide de rendre les 10 contenus Premium (100%) + Alors le système accepte sans limitation + Et je peux avoir 100% de mon catalogue en Premium + + Scénario: Stratégie freemium - Mix gratuit/premium + Étant donné que je publie 10 nouveaux contenus + Quand je décide de rendre 5 contenus Premium et 5 gratuits (50/50) + Alors le système accepte cette stratégie + Et je peux tester différents mix pour optimiser mes revenus + + Scénario: Stratégie tout gratuit possible + Étant donné que je suis monétisé via publicités + Quand je décide de ne mettre aucun contenu en Premium (0%) + Alors le système accepte cette stratégie + Et je génère des revenus uniquement via les publicités + + Scénario: Badge 👑 visible sur l'interface utilisateur + Étant donné qu'un utilisateur consulte ma liste de contenus + Quand il voit un contenu Premium + Alors un badge 👑 "Premium" est affiché + Et le contenu est clairement identifiable comme réservé + + Scénario: Utilisateur gratuit voit les contenus Premium dans la liste + Étant donné que je suis un utilisateur gratuit + Quand je consulte les contenus d'un créateur + Alors je vois aussi les contenus Premium dans la liste + Et ils sont affichés avec un badge 👑 + Mais je ne peux pas les lire + + Scénario: Tentative de lecture Premium par utilisateur gratuit - Overlay bloquant + Étant donné que je suis un utilisateur gratuit + Quand je clique sur un contenu Premium pour le lire + Alors un overlay bloquant apparaît + Et je vois le message: + """ + 👑 Ce contenu est réservé aux abonnés Premium + + Passez Premium pour 4.99€/mois et accédez à tous les contenus exclusifs ! + """ + Et un bouton "Passer Premium" est affiché + + Scénario: CTA "Passer Premium" redirige vers abonnement + Étant donné que je vois l'overlay de contenu Premium bloqué + Quand je clique sur "Passer Premium" + Alors je suis redirigé vers la page d'abonnement Premium + Et je peux m'abonner pour 4.99€/mois + + Scénario: Utilisateur Premium peut lire tous les contenus Premium + Étant donné que je suis un utilisateur Premium actif + Quand je clique sur un contenu Premium + Alors le contenu se lance immédiatement + Et je n'ai aucun overlay bloquant + Et je peux profiter pleinement du contenu exclusif + + Scénario: Contenus Premium inclus dans les recommandations + Étant donné que l'algorithme génère ma file de 5 contenus + Quand je suis un utilisateur gratuit + Alors les contenus Premium peuvent apparaître dans les recommandations + Et cela me fait découvrir qu'il existe du contenu exclusif + + Scénario: Contenu Premium skippé automatiquement pour utilisateur gratuit + Étant donné que je suis un utilisateur gratuit + Et qu'un contenu Premium apparaît dans ma file de recommandation + Quand j'écoute le contenu précédent jusqu'à la fin + Alors le contenu Premium est automatiquement skippé + Et le contenu suivant (gratuit) est lancé + Et le slot Premium ne compte pas dans ma file de 5 contenus + + Scénario: Contenu Premium diffusé normalement pour utilisateur Premium + Étant donné que je suis un utilisateur Premium + Et qu'un contenu Premium apparaît dans ma file de recommandation + Quand j'écoute le contenu précédent jusqu'à la fin + Alors le contenu Premium est lancé normalement + Et je profite du contenu exclusif sans interruption + + Scénario: Champ `is_premium` boolean en base PostgreSQL + Étant donné qu'un contenu est créé + Quand il est stocké en base de données + Alors la table `contents` contient un champ `is_premium BOOLEAN DEFAULT FALSE` + Et ce champ est indexé pour requêtes rapides + + Scénario: Index PostgreSQL sur `is_premium` + Étant donné que l'algorithme doit filtrer les contenus selon le statut Premium + Quand une requête SQL est exécutée: + ```sql + SELECT * FROM contents + WHERE creator_id = :creator_id + AND (is_premium = FALSE OR :user_is_premium = TRUE) + ORDER BY created_at DESC; + ``` + Alors l'index sur `is_premium` accélère la requête + Et le temps de réponse reste <20ms + + Scénario: Cache Redis pour statut Premium + Étant donné qu'un contenu Premium est consulté fréquemment + Quand l'API vérifie le statut Premium + Alors la valeur est récupérée depuis Redis: + ``` + content:[content_id]:premium = true + ``` + Et le cache a un TTL de 1 heure + Et cela évite des requêtes SQL inutiles + + Scénario: Invalidation cache lors de modification statut Premium + Étant donné qu'un contenu est passé de gratuit à Premium + Quand le créateur enregistre la modification + Alors le cache Redis `content:[id]:premium` est invalidé immédiatement + Et la nouvelle valeur est mise à jour + Et les utilisateurs voient le changement en temps réel + + Scénario: Justification liberté créateur - Stratégie personnalisée + Étant donné que chaque créateur a une audience différente + Quand un créateur décide de sa stratégie Premium + Alors il peut tester différentes approches: + | stratégie | % Premium | objectif | + | Tout gratuit | 0% | Maximiser audience + revenus pub | + | Mix 50/50 | 50% | Équilibrer audience et exclusivité | + | Premium majoritaire | 80% | Cibler abonnés fidèles | + | 100% Premium | 100% | Contenu ultra-exclusif | + + Scénario: Justification incitation Premium - Argument fort pour s'abonner + Étant donné qu'un utilisateur gratuit voit beaucoup de contenus Premium + Quand il consulte les profils de ses créateurs préférés + Alors il voit que 60% de leur contenu est réservé Premium + Et cela l'incite à s'abonner pour 4.99€/mois + Et RoadWave augmente son taux de conversion vers Premium + + Scénario: Justification équité - Petit créateur peut tout mettre en Premium + Étant donné que je suis un petit créateur avec 600 abonnés + Et que 50 sont abonnés Premium + Quand je mets 100% de mon contenu en Premium + Alors je génère des revenus uniquement via mes 50 abonnés Premium + Et cela me permet de vivre de mon contenu malgré une petite audience + + Scénario: Justification équité - Gros créateur peut tout offrir gratuitement + Étant donné que je suis un gros créateur avec 50 000 abonnés + Et que je génère déjà beaucoup de revenus publicitaires + Quand je laisse 100% de mon contenu gratuit + Alors je maximise mon audience et mes revenus pub + Et je n'ai pas besoin de mettre du contenu en Premium + + Scénario: Statistiques créateur - Ratio Premium/Gratuit + Étant donné que j'accède à mon tableau de bord créateur + Quand je consulte mes statistiques de contenus + Alors je vois: + | métrique | valeur | + | Contenus totaux | 47 | + | Contenus gratuits | 32 (68%) | + | Contenus Premium | 15 (32%) | + | Écoutes Premium ce mois | 12,345 | + | Écoutes gratuites ce mois | 28,901 | + + Scénario: Statistiques créateur - Revenus par type + Étant donné que j'ai des contenus gratuits et Premium + Quand je consulte mes revenus détaillés + Alors je vois: + | source | montant | + | Revenus pub (gratuit) | 86.70€ | + | Revenus Premium (exclusifs) | 34.20€ | + | Revenus Premium (tout contenu)| 78.90€ | + Et je peux comparer l'efficacité de chaque stratégie + + Scénario: Notification créateur - Contenu Premium très écouté + Étant donné que j'ai publié un contenu Premium il y a 3 jours + Et qu'il a généré 5 000 écoutes Premium (très élevé) + Quand le système détecte cette performance + Alors je reçois une notification: + """ + 🔥 Votre contenu Premium "[Titre]" cartonne ! + 5 000 écoutes en 3 jours. Continuez à créer du contenu exclusif de qualité ! + """ + + Scénario: A/B test utilisateur - Impact badge Premium sur conversion + Étant donné que RoadWave veut optimiser le taux de conversion Premium + Quand un A/B test est lancé + Alors groupe A voit le badge 👑 "Premium" + Et groupe B voit le badge 💎 "Exclusif" + Et les taux de clic et conversion sont mesurés + Et le badge le plus performant est déployé définitivement + + Scénario: Analytics plateforme - Adoption fonctionnalité Premium + Étant donné que RoadWave suit l'adoption de la fonctionnalité + Quand un admin consulte les métriques + Alors il voit: + | métrique | valeur | + | Créateurs utilisant Premium | 847 (68%) | + | % moyen contenus Premium | 23% | + | Taux conversion vers Premium (users) | 8.5% | + | Revenus Premium/mois | 47,890€ | + + Scénario: Impact sur churn - Contenus Premium réduisent le churn Premium + Étant donné qu'un utilisateur Premium envisage de résilier + Mais qu'il a accès à 150 contenus Premium de ses créateurs préférés + Quand il voit la valeur exclusive qu'il perdrait + Alors il est moins susceptible de résilier (churn réduit de ~30%) + Et les contenus Premium augmentent la rétention + + Scénario: Transparence - Créateur voit combien de contenus Premium il a + Étant donné que j'accède à mon profil créateur + Quand je consulte mes contenus + Alors je peux filtrer par statut: + | filtre | résultats | + | Tous | 47 | + | Gratuits | 32 | + | Premium 👑 | 15 | + Et je peux facilement gérer mon catalogue + + Scénario: Export liste contenus avec statut Premium (RGPD) + Étant donné que je demande l'export de mes données + Quand l'export est généré + Alors la liste de mes contenus inclut le statut Premium: + ```json + { + "contents": [ + { + "title": "Mon épisode exclusif", + "is_premium": true, + "published_at": "2025-06-15T10:30:00Z" + }, + { + "title": "Mon épisode gratuit", + "is_premium": false, + "published_at": "2025-06-14T08:15:00Z" + } + ] + } + ``` + + Scénario: Suppression compte créateur et contenus Premium + Étant donné que je supprime définitivement mon compte créateur + Quand la suppression est confirmée + Alors tous mes contenus (gratuits et Premium) sont supprimés + Et les utilisateurs Premium ne peuvent plus y accéder + Et les fichiers audio sont supprimés du CDN sous 7 jours + + Scénario: Performance avec 1 million de contenus Premium + Étant donné que RoadWave a 1 million de contenus dont 300 000 Premium + Quand l'algorithme génère une recommandation + Alors la requête SQL filtre efficacement avec l'index `is_premium` + Et le temps de réponse reste <50ms + Et la scalabilité est garantie diff --git a/features/monetisation/desactivation-suspension.feature b/features/monetisation/desactivation-suspension.feature new file mode 100644 index 0000000..3d2ed13 --- /dev/null +++ b/features/monetisation/desactivation-suspension.feature @@ -0,0 +1,381 @@ +# language: fr +Fonctionnalité: Désactivation et suspension monétisation + En tant que créateur ou plateforme + Je veux pouvoir désactiver ou suspendre la monétisation selon certaines conditions + Afin de gérer les pauses, problèmes techniques ou violations des règles + + Contexte: + Étant donné que je suis un créateur avec la monétisation activée + + # ===== DÉSACTIVATION VOLONTAIRE PAR LE CRÉATEUR ===== + + Scénario: Désactivation temporaire par le créateur + Étant donné que je veux faire une pause dans ma création de contenu + Quand j'accède à "Paramètres > Monétisation" + Et que je clique sur "Désactiver temporairement la monétisation" + Alors ma monétisation est désactivée immédiatement + Et je ne génère plus de revenus à partir de maintenant + + Scénario: Confirmation avant désactivation + Étant donné que je clique sur "Désactiver temporairement" + Quand une popup de confirmation apparaît + Alors je vois le message: + """ + ⚠️ Désactiver la monétisation ? + + Vous ne générerez plus de revenus à partir de maintenant. + Votre solde actuel sera conservé et versé normalement. + + Vous pourrez réactiver quand vous le souhaitez. + """ + Et je dois confirmer pour continuer + + Scénario: Solde conservé pendant désactivation + Étant donné que mon solde actuel est 87.45€ + Quand je désactive ma monétisation le 15 du mois + Alors mon solde de 87.45€ est conservé + Et il sera reporté au mois suivant + Et si le total dépasse 50€, il sera versé normalement le 15 du mois prochain + + Scénario: Contenus restent accessibles pendant désactivation + Étant donné que j'ai désactivé ma monétisation + Quand des utilisateurs écoutent mes contenus + Alors mes contenus restent accessibles normalement + Mais je ne génère aucun revenu (ni pub ni Premium) + + Scénario: Réactivation sans refaire le KYC si <2 ans + Étant donné que j'ai désactivé ma monétisation il y a 8 mois + Et que mes documents KYC sont toujours valides + Quand je clique sur "Réactiver la monétisation" + Alors la réactivation est immédiate + Et je n'ai pas besoin de refaire le KYC + Et je recommence à générer des revenus dès maintenant + + Scénario: Nouveau KYC requis si inactivité >2 ans + Étant donné que j'ai désactivé ma monétisation il y a 25 mois + Quand j'essaie de réactiver + Alors le système demande un nouveau KYC + Et je vois: + """ + Votre monétisation était désactivée depuis plus de 2 ans. + Veuillez mettre à jour vos documents KYC pour réactiver. + """ + Et je dois soumettre à nouveau mes documents + + Scénario: Historique des désactivations/réactivations + Étant donné que j'ai désactivé et réactivé ma monétisation plusieurs fois + Quand j'accède à "Paramètres > Monétisation > Historique" + Alors je vois la liste complète: + | date | action | raison | + | 15/06/2025 | Réactivation | Reprise création contenu | + | 01/03/2025 | Désactivation | Pause vacances | + | 20/01/2025 | Activation | KYC validé | + + # ===== SUSPENSION AUTOMATIQUE PAR LA PLATEFORME ===== + + Scénario: Suspension si 3+ strikes actifs + Étant donné que je reçois un 3ème strike pour violation des règles + Quand le strike devient actif + Alors ma monétisation est suspendue automatiquement + Et je vois: + """ + ⚠️ Monétisation suspendue + + Votre compte a 3 strikes actifs. + La monétisation est suspendue jusqu'à résolution des violations. + """ + + Scénario: Réactivation après résolution des strikes + Étant donné que ma monétisation est suspendue pour 3 strikes + Quand je résous tous mes strikes (après expiration ou contestation) + Et que mon compteur de strikes passe à 0 + Alors ma monétisation est réactivée automatiquement + Et je reçois un email de confirmation + + Scénario: Suspension si RIB invalide après 3 échecs de virement + Étant donné que 3 tentatives de virement ont échoué (15, 18, 22 du mois) + Quand le 3ème échec est confirmé + Alors ma monétisation est suspendue automatiquement + Et je vois: + """ + ⚠️ Monétisation suspendue + + Vos virements ont échoué 3 fois (RIB invalide ou compte fermé). + Veuillez mettre à jour votre RIB dans "Paramètres > Monétisation". + + Votre solde en attente (150.00€) sera versé dès que le RIB sera valide. + """ + + Scénario: Réactivation après mise à jour RIB valide + Étant donné que ma monétisation est suspendue pour RIB invalide + Quand je mets à jour mon RIB avec un compte bancaire valide + Et que Mangopay valide le nouveau RIB + Alors ma monétisation est réactivée automatiquement + Et un virement est tenté immédiatement pour le solde en attente + + Scénario: Suspension si documents KYC expirés + Étant donné que ma carte d'identité expire dans 30 jours + Quand je reçois un email de rappel de mise à jour + Mais que je ne mets pas à jour mes documents + Et que ma CNI expire + Alors ma monétisation est suspendue automatiquement après 30 jours de grâce + + Scénario: Préavis 30 jours avant suspension pour docs expirés + Étant donné que ma CNI expire le 15 juin 2025 + Quand le 15 mai 2025 arrive (30 jours avant) + Alors je reçois un email d'alerte: + """ + ⚠️ Vos documents KYC expirent dans 30 jours + + Votre carte d'identité expire le 15 juin 2025. + Veuillez mettre à jour vos documents dans "Paramètres > Monétisation > KYC". + + Si vous ne mettez pas à jour, votre monétisation sera suspendue le 16 juin 2025. + """ + + Scénario: Réactivation après renouvellement documents KYC + Étant donné que ma monétisation est suspendue pour CNI expirée + Quand je soumets une nouvelle CNI valide + Et que Mangopay valide le document sous 24-72h + Alors ma monétisation est réactivée automatiquement + Et je recommence à générer des revenus + + Scénario: Suspension si fraude détectée + Étant donné que le système détecte une activité frauduleuse (bots, écoutes artificielles) + Quand l'équipe modération confirme la fraude + Alors ma monétisation est suspendue immédiatement + Et mon compte est mis sous enquête + Et je reçois un email m'informant de la suspension + + Scénario: Enquête fraude - Vérification manuelle + Étant donné que ma monétisation est suspendue pour suspicion de fraude + Quand l'équipe modération enquête + Alors elle analyse: + | élément à vérifier | outil | + | Patterns d'écoute suspects | Analytics + logs | + | Origine géographique | Logs IP | + | Vitesse de croissance anormale | Graphiques statistiques | + | Plaintes utilisateurs | Système de signalement | + + Scénario: Levée suspension si fraude non confirmée + Étant donné que mon compte était suspendu pour suspicion de fraude + Quand l'enquête conclut qu'il n'y a pas eu de fraude + Alors ma monétisation est réactivée + Et les revenus suspendus pendant l'enquête sont versés normalement + Et je reçois un email d'excuses avec explication + + Scénario: Suspension définitive si fraude confirmée + Étant donné que l'enquête confirme une fraude avérée + Quand l'équipe modération prend la décision + Alors ma monétisation est définitivement désactivée + Et mon solde en attente est gelé (non versé) + Et je peux recevoir un strike 4 (ban définitif du compte) + + # ===== SUPPRESSION DÉFINITIVE ===== + + Scénario: Suppression définitive sur demande créateur + Étant donné que je veux arrêter définitivement la monétisation + Quand j'accède à "Paramètres > Monétisation > Supprimer définitivement" + Alors une confirmation stricte est demandée + Et je dois taper "SUPPRIMER" pour confirmer + + Scénario: Solde versé sous 30 jours après suppression + Étant donné que je supprime définitivement ma monétisation + Et que mon solde en attente est 127.45€ + Quand la suppression est confirmée + Alors mon solde sera versé sous 30 jours + Et je reçois un dernier virement de clôture + Et mon e-wallet Mangopay est clôturé + + Scénario: Suppression auto si inactivité 24 mois + solde <50€ + Étant donné que je n'ai plus publié de contenu depuis 24 mois + Et que mon solde en attente est 12.30€ (<50€) + Quand le processus de purge RGPD s'exécute + Alors ma monétisation est automatiquement supprimée + Et mon solde de 12.30€ est perdu (trop faible pour virement) + Et mes données KYC sont archivées puis supprimées selon la législation + + Scénario: Email de préavis 60 jours avant purge RGPD + Étant donné que je suis inactif depuis 22 mois + Quand le système détecte l'inactivité + Alors je reçois un email: + """ + ⚠️ Votre monétisation sera supprimée dans 60 jours + + Vous n'avez pas publié de contenu depuis 22 mois. + Si vous restez inactif 2 mois de plus (24 mois total), votre monétisation sera automatiquement supprimée. + + Solde actuel: 12.30€ + + Pour éviter la suppression, publiez un nouveau contenu ou contactez-nous. + """ + + Scénario: Ban définitif compte - Strike 4 + Étant donné que je reçois un 4ème strike (violation grave ou répétée) + Quand l'équipe modération applique le strike 4 + Alors mon compte est banni définitivement + Et ma monétisation est supprimée définitivement + Et mon solde en attente est gelé (non versé) + Et je ne peux plus créer de nouveau compte (blacklist email/SIRET) + + # ===== NOTIFICATIONS ===== + + Scénario: Email pour toute suspension + Étant donné que ma monétisation est suspendue (quelle qu'en soit la raison) + Quand la suspension devient effective + Alors je reçois immédiatement un email: + """ + ⚠️ Monétisation RoadWave suspendue + + Raison: [Raison explicite] + + Que faire: + [Procédure de réactivation selon la raison] + + Votre solde actuel sera conservé pendant la suspension. + + Questions? Contactez support@roadwave.com + """ + + Scénario: Notification in-app avec raison explicite + Étant donné que ma monétisation est suspendue + Quand je me connecte à l'application + Alors je vois une bannière en haut de mon dashboard: + ``` + ⚠️ MONÉTISATION SUSPENDUE + Raison: 3 strikes actifs + Action requise: Résoudre les violations pour réactiver + [En savoir plus] + ``` + + Scénario: Email de confirmation lors de réactivation + Étant donné que ma monétisation était suspendue + Quand elle est réactivée (automatiquement ou manuellement) + Alors je reçois un email: + """ + ✅ Monétisation RoadWave réactivée + + Votre monétisation a été réactivée avec succès. + Vous recommencez à générer des revenus dès maintenant. + + Tableau de bord: [Lien] + """ + + # ===== STATISTIQUES ET MONITORING ===== + + Scénario: Dashboard admin - Suspensions actives + Étant donné qu'un admin RoadWave consulte les suspensions + Quand il accède au dashboard admin "Monétisation > Suspensions" + Alors il voit: + | raison suspension | nombre actif | taux | + | Strikes (3+) | 23 | 1.8% | + | RIB invalide | 12 | 0.9% | + | Documents KYC expirés | 8 | 0.6% | + | Fraude sous enquête | 3 | 0.2% | + | TOTAL | 46 | 3.7% | + + Scénario: Alertes si taux de suspension >5% + Étant donné que le taux de suspension dépasse 5% + Quand le système détecte cette anomalie + Alors une alerte est envoyée à l'équipe: + """ + ⚠️ Taux de suspension monétisation anormal: 5.8% + + Causes principales: + - RIB invalides: 67 créateurs (3.2%) + - Documents expirés: 34 créateurs (1.6%) + - Strikes: 21 créateurs (1.0%) + + Action recommandée: Campagne email de rappel mise à jour docs + """ + + Scénario: Statistiques personnelles - Temps actif monétisation + Étant donné que j'accède à mon dashboard créateur + Quand je consulte "Statistiques > Monétisation" + Alors je vois: + | métrique | valeur | + | Date activation monétisation | 20 janvier 2025 | + | Temps actif total | 8 mois | + | Périodes de désactivation | 2 (3 mois total)| + | Suspensions subies | 0 | + | Statut actuel | ✅ Actif | + + # ===== RGPD ET CONFORMITÉ ===== + + Scénario: Export données suspension (RGPD) + Étant donné que je demande l'export de mes données + Quand l'export est généré + Alors l'historique des suspensions est inclus: + ```json + { + "monetization_history": [ + { + "event": "suspension", + "reason": "3 active strikes", + "suspended_at": "2025-06-15T10:30:00Z", + "reactivated_at": "2025-07-01T14:20:00Z", + "duration_days": 16 + }, + { + "event": "voluntary_deactivation", + "deactivated_at": "2025-03-01T08:00:00Z", + "reactivated_at": "2025-06-15T10:00:00Z", + "duration_days": 106 + } + ] + } + ``` + + Scénario: Suppression compte et données monétisation + Étant donné que je supprime définitivement mon compte RoadWave + Quand la suppression est confirmée + Alors toutes mes données de monétisation sont supprimées: + | donnée | action | + | Solde en attente | Versé sous 30 jours puis supprimé | + | Historique revenus | Archivé 10 ans (obligation légale) | + | Documents KYC | Archivés 10 ans chez Mangopay puis supprimés | + | E-wallet Mangopay | Clôturé après versement final | + + Scénario: Conservation archives 10 ans obligation légale + Étant donné que je supprime mon compte + Quand mes données sont archivées + Alors RoadWave conserve 10 ans: + | donnée archivée | raison | + | Relevés mensuels PDF | Obligation comptable France | + | Déclarations DAS2 | Obligation fiscale France | + | Justificatifs virements | Preuve paiement en cas d'audit | + Et après 10 ans, tout est supprimé définitivement + + # ===== CAS PARTICULIERS ===== + + Scénario: Suspension temporaire pour maintenance technique + Étant donné que Mangopay effectue une maintenance planifiée + Quand la maintenance est programmée + Alors tous les créateurs reçoivent un email préventif 7 jours avant: + """ + 📅 Maintenance planifiée Mangopay + + Date: 15 juin 2025, 02h00-06h00 (heure de Paris) + Impact: Les virements programmés ce jour seront reportés de 24h + + Aucune action de votre part n'est requise. + Vos revenus seront versés normalement avec 1 jour de retard. + """ + + Scénario: Réactivation progressive après incident majeur + Étant donné qu'un incident technique majeur suspend toutes les monétisations + Quand l'incident est résolu + Alors les réactivations se font progressivement: + | vague | critère | % créateurs | + | 1 | Top 10% créateurs (revenus) | 10% | + | 2 | Créateurs vérifiés | 30% | + | 3 | Tous les autres créateurs | 60% | + Et cela évite une surcharge système lors de la reprise + + Scénario: Support prioritaire pour créateurs suspendus injustement + Étant donné que ma monétisation est suspendue + Et que je pense que c'est une erreur + Quand je contacte le support avec tag "Suspension monétisation" + Alors mon ticket est traité en priorité (SLA 24h) + Et un agent expert examine mon cas + Et si suspension injustifiée, je suis réactivé immédiatement avec excuses diff --git a/features/monetisation/kyc-inscription.feature b/features/monetisation/kyc-inscription.feature new file mode 100644 index 0000000..e116cf8 --- /dev/null +++ b/features/monetisation/kyc-inscription.feature @@ -0,0 +1,293 @@ +# language: fr +Fonctionnalité: KYC et inscription à la monétisation + En tant que créateur éligible + Je veux compléter le KYC pour activer la monétisation + Afin de recevoir des paiements légalement + + Contexte: + Étant donné que je remplis tous les critères de monétisation + Et que j'ai cliqué sur "Demander la monétisation" + + Scénario: Redirection vers formulaire KYC Mangopay + Quand je démarre le processus d'activation + Alors je suis redirigé vers un formulaire KYC + Et le formulaire est fourni par Mangopay (iframe sécurisée) + Et toutes les données sont chiffrées et hébergées en EU + + Scénario: Statut auto-entrepreneur accepté + Étant donné que je suis auto-entrepreneur + Quand je renseigne mon statut juridique + Alors l'option "Auto-entrepreneur (micro-BNC)" est disponible + Et je peux continuer le processus + + Scénario: Statut société SARL/SAS/SASU accepté + Étant donné que j'ai créé une société + Quand je renseigne mon statut juridique + Alors les options suivantes sont disponibles: + | statut juridique | + | SARL | + | SAS | + | SASU | + Et je peux continuer le processus + + Scénario: Statut particulier refusé + Étant donné que je n'ai pas de statut professionnel + Quand j'essaie de m'inscrire en tant que "Particulier" + Alors le formulaire affiche: + """ + Un statut juridique professionnel est obligatoire pour la monétisation. + Créez un statut auto-entrepreneur sur autoentrepreneur.urssaf.fr (gratuit, 15 min). + """ + Et je ne peux pas continuer sans statut professionnel + + Scénario: Document SIRET obligatoire et validé + Étant donné que je renseigne mon SIRET + Quand je saisis "12345678901234" (14 chiffres) + Alors le format est validé + Et Mangopay vérifie l'existence du SIRET auprès du répertoire SIRENE + Et si valide, le document est accepté + + Scénario: SIRET invalide ou inexistant + Étant donné que je renseigne un SIRET inexistant + Quand je saisis "99999999999999" + Alors Mangopay rejette le SIRET + Et je vois "SIRET non trouvé dans le répertoire SIRENE. Vérifiez le numéro." + Et je dois corriger avant de continuer + + Scénario: RIB professionnel obligatoire + Étant donné que j'upload mon RIB + Quand le RIB est scanné par Mangopay + Alors le système vérifie que le titulaire correspond à mon SIRET + Et que l'IBAN commence par "FR" (compte français) + Et si valide, le document est accepté + + Scénario: RIB particulier refusé + Étant donné que j'upload un RIB de compte particulier + Quand Mangopay détecte que le compte n'est pas professionnel + Alors le RIB est rejeté + Et je vois: + """ + Le RIB doit correspondre à un compte professionnel lié à votre SIRET. + Créez un compte professionnel auprès de votre banque. + """ + + Scénario: Pièce d'identité CNI en cours de validité + Étant donné que j'upload ma carte nationale d'identité + Quand Mangopay analyse le document + Alors la date d'expiration est vérifiée + Et si la CNI est valide, le document est accepté + Et mon identité est vérifiée par OCR + vérification manuelle + + Scénario: Pièce d'identité expirée refusée + Étant donné que j'upload une CNI expirée depuis 2 ans + Quand Mangopay analyse le document + Alors le document est rejeté + Et je vois "Pièce d'identité expirée. Veuillez fournir un document en cours de validité." + + Scénario: Passeport accepté comme alternative + Étant donné que je n'ai pas de CNI + Quand j'upload mon passeport en cours de validité + Alors Mangopay accepte le passeport + Et mon identité est vérifiée de la même manière + + Scénario: Numéro TVA intracommunautaire si applicable + Étant donné que mon CA dépasse 37 000€/an + Et que je suis sorti de la franchise en base + Quand je renseigne mon numéro TVA intracommunautaire + Alors le format "FR + 11 chiffres" est validé + Et Mangopay vérifie l'existence auprès de la Commission Européenne (VIES) + + Scénario: TVA non applicable pour micro-BNC sous franchise + Étant donné que je suis auto-entrepreneur sous franchise en base + Et que mon CA est <37 000€/an + Quand je remplis le formulaire KYC + Alors le champ "Numéro TVA" est optionnel + Et je peux continuer sans TVA + + Scénario: Kbis <3 mois pour sociétés + Étant donné que je suis gérant d'une SARL + Quand j'upload mon extrait Kbis + Alors Mangopay vérifie que le Kbis date de moins de 3 mois + Et que le SIRET correspond + Et si valide, le document est accepté + + Scénario: Kbis trop ancien refusé + Étant donné que j'upload un Kbis de 5 mois + Quand Mangopay analyse le document + Alors le Kbis est rejeté + Et je vois "Le Kbis doit dater de moins de 3 mois. Téléchargez un extrait récent sur infogreffe.fr" + + Scénario: Vérification identité ne correspond pas au compte + Étant donné que mon compte RoadWave est au nom de "Jean Dupont" + Mais que ma CNI est au nom de "Pierre Martin" + Quand Mangopay compare les identités + Alors le KYC est rejeté + Et je vois: + """ + L'identité sur votre pièce d'identité ne correspond pas à votre compte RoadWave. + Si vous avez changé de nom, veuillez contacter le support. + """ + + Scénario: Liste noire anti-blanchiment détectée + Étant donné que mon identité apparaît sur une liste anti-blanchiment + Quand Mangopay effectue la vérification AML (Anti-Money Laundering) + Alors le KYC est automatiquement rejeté + Et je vois "Votre demande ne peut être acceptée pour des raisons de conformité légale" + Et mon compte créateur peut être suspendu + + Scénario: Délai de vérification 24-72h si documents conformes + Étant donné que j'ai soumis tous les documents valides + Quand Mangopay traite ma demande + Alors je reçois un email "KYC en cours de vérification (24-72h)" + Et mon statut est "En attente de validation" + Et je peux continuer à publier des contenus en attendant + + Scénario: Validation KYC réussie + Étant donné que mes documents sont conformes + Quand Mangopay valide mon KYC après 48h + Alors je reçois un email "Monétisation activée !" + Et mon statut passe à "Monétisé" + Et je commence à générer des revenus dès maintenant + + Scénario: Rejet KYC pour documents invalides + Étant donné que j'ai soumis une CNI floue et illisible + Quand Mangopay analyse les documents + Alors le KYC est rejeté après 24h + Et je reçois un email détaillant les documents à refournir: + """ + Votre demande de monétisation a été rejetée pour les raisons suivantes: + - Carte d'identité illisible (photo floue) + + Veuillez soumettre à nouveau des documents de meilleure qualité. + """ + + Scénario: E-wallet Mangopay créé automatiquement + Étant donné que mon KYC est validé + Quand Mangopay finalise mon inscription + Alors un e-wallet Mangopay est créé automatiquement à mon nom + Et tous mes futurs revenus seront transférés vers ce wallet + Et les virements SEPA vers mon RIB seront effectués depuis ce wallet + + Scénario: Conformité RGPD - Données hébergées EU + Étant donné que je fournis mes documents KYC + Quand Mangopay stocke mes données + Alors toutes les données sont hébergées en Union Européenne + Et Mangopay est régulé par l'ACPR (Autorité de Contrôle Prudentiel) + Et mes données sont protégées selon le RGPD + + Scénario: KYC gratuit inclus dans Mangopay + Étant donné que je complète le KYC + Quand le processus se termine + Alors aucun frais ne m'est facturé (0€) + Et aucun frais n'est facturé à RoadWave (inclus dans l'offre Mangopay) + Contrairement à Stripe qui facture 1.20€ par KYC + + Scénario: Base légale - Conformité fiscale française + Étant donné que RoadWave est une plateforme française + Quand je génère des revenus >1200€/an + Alors RoadWave doit déclarer ces revenus aux impôts (DAS2) + Et le KYC permet de garantir l'identité réelle du bénéficiaire + Et cela respecte la réglementation fiscale française + + Scénario: Base légale - Directive anti-blanchiment EU 2018/843 + Étant donné que RoadWave verse de l'argent aux créateurs + Quand le KYC est effectué + Alors RoadWave respecte la 5ème directive anti-blanchiment EU + Et Mangopay effectue les vérifications requises (identité, liste noire, origine fonds) + + Scénario: Notification de mise à jour documents expirés + Étant donné que ma CNI va expirer dans 30 jours + Quand le système détecte l'expiration proche + Alors je reçois un email: + """ + Votre pièce d'identité expire dans 30 jours. + Veuillez mettre à jour vos documents KYC pour éviter une suspension de la monétisation. + """ + + Scénario: Suspension monétisation si documents expirés + Étant donné que ma CNI est expirée depuis 10 jours + Et que je n'ai pas mis à jour mes documents + Quand le système vérifie mon statut KYC + Alors ma monétisation est suspendue automatiquement + Et je ne génère plus de revenus jusqu'à mise à jour + + Scénario: Réactivation sans nouveau KYC si données à jour + Étant donné que j'ai désactivé temporairement ma monétisation il y a 6 mois + Et que mes documents KYC sont toujours valides + Quand je réactive la monétisation + Alors je n'ai pas besoin de refaire le KYC + Et la réactivation est immédiate + + Scénario: Nouveau KYC requis après 2 ans d'inactivité + Étant donné que j'ai désactivé ma monétisation il y a 25 mois + Quand j'essaie de réactiver + Alors le système demande un nouveau KYC + Et je dois soumettre des documents à jour (CNI peut avoir changé) + + Scénario: Support créateur pour problèmes KYC + Étant donné que mon KYC est rejeté et je ne comprends pas pourquoi + Quand je contacte le support RoadWave + Alors un agent peut consulter les raisons du rejet Mangopay + Et m'aider à fournir les bons documents + + Scénario: Export données KYC pour RGPD + Étant donné que je demande l'export de mes données personnelles + Quand l'export est généré + Alors les informations KYC sont incluses: + ```json + { + "kyc_status": "validated", + "legal_status": "Auto-entrepreneur", + "siret": "12345678901234", + "iban": "FR76 XXXX XXXX XXXX", + "validated_at": "2025-06-20T14:30:00Z" + } + ``` + Et les documents scannés (CNI, RIB) sont exclus pour sécurité + + Scénario: Suppression compte et données KYC + Étant donné que je supprime définitivement mon compte RoadWave + Quand la suppression est confirmée + Alors mes données KYC chez Mangopay sont archivées 10 ans (obligation légale) + Mais supprimées de la base RoadWave immédiatement + Et mon e-wallet est clôturé après versement du solde final + + Scénario: Statistiques KYC pour monitoring plateforme + Étant donné que RoadWave suit la qualité du processus KYC + Quand un admin consulte les métriques + Alors il voit: + | métrique | valeur exemple | + | Demandes KYC ce mois | 247 | + | Taux de validation | 87% | + | Délai moyen validation | 36h | + | Taux de rejet (documents invalides)| 13% | + Et cela permet d'optimiser le processus + + Scénario: Vérification SIRET via API INSEE + Étant donné que je saisis mon SIRET + Quand le système le valide + Alors une requête est faite à l'API SIRENE de l'INSEE + Et le système vérifie que le SIRET existe et est actif + Et récupère le nom de l'entreprise pour pré-remplir le formulaire + + Scénario: Détection fraude - Même SIRET utilisé par plusieurs comptes + Étant donné qu'un SIRET "12345678901234" est déjà utilisé par un autre créateur + Quand j'essaie d'utiliser le même SIRET + Alors le système détecte la duplication + Et affiche "Ce SIRET est déjà utilisé par un autre compte RoadWave" + Et je dois contacter le support si c'est une erreur + + Scénario: Protection données sensibles - Logs chiffrés + Étant donné que des données KYC sensibles transitent dans le système + Quand les logs sont enregistrés + Alors les numéros SIRET, IBAN et données CNI sont masqués: + """ + [2025-06-20 14:30:00] KYC submitted: creator_id=abc123, siret=1234****1234, iban=FR76**** + """ + Et seule l'équipe sécurité peut accéder aux données complètes + + Scénario: Backup Mangopay des documents KYC + Étant donné que mes documents KYC sont stockés chez Mangopay + Quand un audit est demandé par les autorités + Alors Mangopay peut fournir les documents originaux + Et RoadWave n'a pas besoin de stocker ces documents (réduction risque RGPD) diff --git a/features/monetisation/obligations-fiscales.feature b/features/monetisation/obligations-fiscales.feature new file mode 100644 index 0000000..b613378 --- /dev/null +++ b/features/monetisation/obligations-fiscales.feature @@ -0,0 +1,321 @@ +# language: fr +Fonctionnalité: Obligations fiscales + En tant que créateur monétisé + Je veux que RoadWave génère automatiquement les documents fiscaux requis + Afin de faciliter ma comptabilité et respecter la loi + + Contexte: + Étant donné que je suis un créateur avec la monétisation activée + Et que je génère des revenus sur RoadWave + + # ===== RELEVÉ MENSUEL PDF ===== + + Scénario: Génération automatique relevé mensuel PDF + Étant donné que le mois de janvier se termine + Quand le système calcule mes revenus du mois + Alors un relevé mensuel PDF est généré automatiquement + Et le PDF est disponible dans mon tableau de bord + + Scénario: Contenu du relevé mensuel PDF + Étant donné que mon relevé de janvier est généré + Quand je télécharge le PDF + Alors le document contient: + ``` + ═══════════════════════════════════════════════ + RELEVÉ MENSUEL ROADWAVE + Période: Janvier 2025 + ═══════════════════════════════════════════════ + + CRÉATEUR + Nom: Jean Dupont + SIRET: 12345678901234 + Statut: Auto-entrepreneur + + REVENUS DU MOIS + Revenus publicitaires: 89.50€ + Revenus abonnés Premium: 60.50€ + ───────────────────────────────────────────── + TOTAL REVENUS: 150.00€ + + DÉTAILS + Écoutes complètes (gratuit): 29,833 + Heures d'écoute Premium: 287h + Abonnés Premium actifs: 47 + + PAIEMENT + Statut: Validé + Date de paiement: 15 février 2025 + Référence virement: MANGOPAY-ABC123 + + ═══════════════════════════════════════════════ + Document généré automatiquement par RoadWave + En cas de question: support@roadwave.com + ═══════════════════════════════════════════════ + ``` + + Scénario: Téléchargement relevé depuis tableau de bord + Étant donné que je suis sur mon tableau de bord créateur + Quand j'accède à l'onglet "Revenus > Historique" + Alors je vois la liste de mes relevés mensuels: + | mois | montant | actions | + | Janvier 2025 | 150.00€ | 📄 Télécharger PDF | + | Décembre 2024| 123.50€ | 📄 Télécharger PDF | + | Novembre 2024| 98.75€ | 📄 Télécharger PDF | + + Scénario: Conservation relevés accessibles 10 ans + Étant donné que j'ai commencé la monétisation en janvier 2025 + Quand je consulte mes relevés en janvier 2035 (10 ans plus tard) + Alors tous les relevés depuis 2025 sont toujours accessibles + Et je peux télécharger n'importe quel relevé historique + Et cela respecte l'obligation de conservation comptable de 10 ans + + # ===== EXPORT CSV COMPTABLE ===== + + Scénario: Export CSV à la demande + Étant donné que je clique sur "Exporter pour comptable" + Quand je choisis la période "Année 2025" + Alors un fichier CSV est généré et téléchargé + + Scénario: Contenu export CSV détaillé + Étant donné que j'exporte mes données comptables 2025 + Quand je télécharge le fichier CSV + Alors le fichier contient: + ```csv + Mois,Revenus Pub,Revenus Premium,Total,Référence Virement,Statut,Date Paiement + 2025-01,89.50,60.50,150.00,MANGOPAY-ABC123,Payé,2025-02-15 + 2025-02,78.30,45.20,123.50,MANGOPAY-XYZ789,Payé,2025-03-15 + 2025-03,67.25,31.50,98.75,MANGOPAY-DEF456,Payé,2025-04-15 + 2025-04,45.80,28.90,74.70,MANGOPAY-GHI123,Payé,2025-05-15 + 2025-05,56.40,35.60,92.00,MANGOPAY-JKL456,Payé,2025-06-15 + 2025-06,89.20,67.80,157.00,MANGOPAY-MNO789,Payé,2025-07-15 + 2025-07,91.00,72.50,163.50,MANGOPAY-PQR012,Payé,2025-08-15 + 2025-08,87.60,58.40,146.00,MANGOPAY-STU345,Payé,2025-09-15 + 2025-09,102.30,81.70,184.00,MANGOPAY-VWX678,Payé,2025-10-15 + 2025-10,94.50,69.50,164.00,MANGOPAY-YZA901,Payé,2025-11-15 + 2025-11,88.40,64.60,153.00,MANGOPAY-BCD234,Payé,2025-12-15 + 2025-12,98.70,75.30,174.00,MANGOPAY-EFG567,En attente,2026-01-15 + ``` + + Scénario: Transmission à l'expert-comptable + Étant donné que j'ai téléchargé mon export CSV 2025 + Quand je l'envoie à mon expert-comptable + Alors il peut importer le fichier dans son logiciel comptable + Et il saisit rapidement mes revenus RoadWave + Et cela facilite ma déclaration fiscale annuelle + + # ===== DAS2 ANNUEL ===== + + Scénario: DAS2 généré automatiquement si revenus >1200€/an + Étant donné que mes revenus 2025 totalisent 2,450€ + Quand l'année 2025 se termine + Alors RoadWave génère automatiquement une DAS2 pour les impôts + Et la DAS2 est transmise à la DGFIP en janvier 2026 + + Scénario: Contenu de la DAS2 + Étant donné que RoadWave génère ma DAS2 pour 2025 + Quand la DGFIP reçoit la déclaration + Alors le document contient: + ``` + Déclarant: RoadWave SAS + SIRET RoadWave: 98765432100000 + + Bénéficiaire: Jean Dupont + SIRET bénéficiaire: 12345678901234 + Année: 2025 + + Honoraires versés: 2,450.00€ + Catégorie: BNC (Bénéfices Non Commerciaux) + ``` + + Scénario: Créateur reçoit une copie de la DAS2 + Étant donné que RoadWave transmet ma DAS2 aux impôts + Quand la transmission est confirmée + Alors je reçois un email avec une copie de la DAS2 en pièce jointe + Et je peux consulter le document dans mon tableau de bord + + Scénario: Pas de DAS2 si revenus <1200€/an + Étant donné que mes revenus 2025 totalisent seulement 890€ + Quand l'année 2025 se termine + Alors aucune DAS2 n'est générée car le seuil de 1200€ n'est pas atteint + Mais je dois quand même déclarer mes revenus dans ma déclaration personnelle + + Scénario: Base légale DAS2 - Obligation France + Étant donné que RoadWave verse des honoraires à des prestataires + Quand les revenus dépassent 1200€/an + Alors la déclaration DAS2 est obligatoire selon l'article 87 du Code Général des Impôts + Et le non-respect entraîne une amende de 15€ par bénéficiaire non déclaré + + Scénario: Transmission DAS2 via EDI-TDFC + Étant donné que RoadWave génère 1,247 DAS2 pour l'année 2025 + Quand la transmission aux impôts est effectuée + Alors la transmission se fait via le portail EDI-TDFC de la DGFIP + Et la transmission est automatisée (pas de saisie manuelle) + Et un accusé de réception est reçu sous 48h + + # ===== RESPONSABILITÉS CRÉATEUR ===== + + Scénario: Créateur responsable de déclarer aux impôts + Étant donné que j'ai reçu 2,450€ de revenus RoadWave en 2025 + Quand je fais ma déclaration fiscale en mai 2026 + Alors je dois déclarer ces 2,450€ dans ma déclaration annuelle + Et si je suis auto-entrepreneur, je déclare en BNC (Bénéfices Non Commerciaux) + + Scénario: Créateur responsable des cotisations URSSAF + Étant donné que je suis auto-entrepreneur + Et que j'ai reçu 2,450€ de revenus RoadWave en 2025 + Quand je fais ma déclaration URSSAF trimestrielle + Alors je dois déclarer ces revenus à l'URSSAF + Et je paie ~22% de cotisations sociales (soit ~539€) + + Scénario: TVA non applicable en franchise en base + Étant donné que je suis auto-entrepreneur en micro-BNC + Et que mon chiffre d'affaires est <37,800€/an + Quand je génère des revenus sur RoadWave + Alors je bénéficie de la franchise en base de TVA + Et je ne facture pas de TVA à RoadWave + Et je ne récupère pas la TVA sur mes achats + + Scénario: TVA applicable si CA >37,800€/an + Étant donné que mon chiffre d'affaires total 2025 est 45,000€ + Quand je dépasse le seuil de franchise en base (37,800€) + Alors je dois facturer de la TVA (20%) à RoadWave + Et je dois obtenir un numéro TVA intracommunautaire + Et je dois déclarer ma TVA mensuellement ou trimestriellement + + Scénario: Conservation justificatifs 10 ans - Obligation légale + Étant donné que je génère des revenus sur RoadWave + Quand je télécharge mes relevés mensuels et exports CSV + Alors je dois les conserver 10 ans (obligation comptable France) + Et en cas de contrôle fiscal, je dois pouvoir les fournir + + # ===== MANGOPAY ET CONFORMITÉ EU ===== + + Scénario: Mangopay transmet automatiquement via DAC7 + Étant donné que je suis créateur monétisé sur RoadWave + Quand l'année se termine + Alors Mangopay transmet automatiquement mes revenus aux autorités fiscales EU + Et cela respecte la directive DAC7 (2021/514) sur la transparence fiscale des plateformes + + Scénario: Directive DAC7 - Obligations plateforme + Étant donné que RoadWave est une plateforme facilitant des transactions + Quand Mangopay gère les paiements + Alors Mangopay transmet automatiquement: + | information | destinataire | + | Identité créateur (SIRET) | Autorités fiscales pays EU | + | Revenus annuels | Autorités fiscales pays EU | + | Nombre transactions | Autorités fiscales pays EU | + Et RoadWave n'a pas besoin de faire cette transmission manuellement + + Scénario: Justificatif virement = Preuve bancaire comptable + Étant donné que je reçois un virement de 150.00€ de Mangopay + Quand je consulte mon relevé bancaire + Alors je vois le virement avec la référence MANGOPAY-ABC123 + Et ce relevé bancaire sert de justificatif comptable + Et je peux le fournir à mon expert-comptable ou aux impôts + + # ===== NOTIFICATIONS ET RAPPELS ===== + + Scénario: Notification annuelle rappel déclaration fiscale + Étant donné que je suis créateur monétisé + Quand le mois d'avril 2026 arrive (période déclaration impôts France) + Alors je reçois un email de rappel: + """ + 📋 Rappel: Déclaration fiscale 2025 + + N'oubliez pas de déclarer vos revenus RoadWave 2025 dans votre déclaration annuelle. + + Revenus RoadWave 2025: 2,450.00€ + + Documents disponibles: + - Récapitulatif annuel PDF + - Export CSV pour comptable + - Copie DAS2 (transmise aux impôts) + + Téléchargez vos documents: [Lien dashboard] + + Besoin d'aide? Contactez votre expert-comptable ou nos ressources fiscales. + """ + + Scénario: Page ressources fiscales pour créateurs + Étant donné que je suis créateur monétisé + Quand j'accède à "Aide > Fiscalité" + Alors je vois une page avec: + | ressource | description | + | Guide auto-entrepreneur RoadWave | PDF expliquant démarches et déclarations | + | FAQ fiscalité | Questions fréquentes sur TVA, cotisations, etc.| + | Liens URSSAF et impots.gouv.fr | Portails officiels | + | Contact expert-comptable partenaire | Recommandations d'experts connaissant RoadWave | + + Scénario: Dashboard créateur - Récapitulatif annuel + Étant donné que je consulte mon dashboard en décembre 2025 + Quand j'accède à "Revenus > Récapitulatif annuel" + Alors je vois: + ``` + ===== RÉCAPITULATIF 2025 ===== + + Revenus totaux: 2,450.00€ + - Revenus publicitaires: 1,234.00€ (50%) + - Revenus Premium: 1,216.00€ (50%) + + Paiements reçus: 12 + Mois le plus rentable: Septembre (184.00€) + + À faire pour votre déclaration: + ☐ Télécharger export CSV comptable + ☐ Télécharger copie DAS2 + ☐ Déclarer aux impôts (avant mai 2026) + ☐ Déclarer à l'URSSAF (trimestriel) + ``` + + # ===== AUTOMATISATION ET SÉCURITÉ ===== + + Scénario: Génération automatique minimise erreurs + Étant donné que tous les documents fiscaux sont générés automatiquement + Quand un créateur télécharge ses documents + Alors les montants sont garantis corrects (issus de la base de données) + Et il n'y a pas d'erreur de saisie manuelle + Et cela réduit les risques de contrôle fiscal + + Scénario: Conformité RGPD - Données fiscales chiffrées + Étant donné que les documents fiscaux contiennent des données sensibles (SIRET, revenus) + Quand les documents sont stockés + Alors ils sont chiffrés au repos (encryption AES-256) + Et seul le créateur et les admins autorisés peuvent y accéder + Et les logs d'accès sont conservés pour audit + + Scénario: Backup documents fiscaux 10 ans + Étant donné qu'un document fiscal est généré + Quand il est stocké dans la base de données + Alors une copie est sauvegardée sur S3 (stockage durable) + Et les backups sont répliqués sur 3 zones de disponibilité + Et la conservation est garantie 10 ans minimum + + Scénario: Audit trail génération DAS2 + Étant donné que 1,247 DAS2 sont générées en janvier 2026 + Quand un audit est demandé + Alors tous les événements sont loggés: + | événement | timestamp | détails | + | Calcul revenus annuels | 2025-12-31 23:59:00 | 1,247 créateurs éligibles | + | Génération fichier EDI | 2026-01-10 08:00:00 | Format EDI-TDFC | + | Transmission DGFIP | 2026-01-10 10:30:00 | Via portail EDI-TDFC | + | Accusé réception DGFIP | 2026-01-11 14:20:00 | Transmission confirmée | + | Email créateurs | 2026-01-11 16:00:00 | 1,247 emails envoyés | + + Scénario: Statistiques admin - Conformité fiscale + Étant donné qu'un admin RoadWave consulte les métriques fiscales + Quand il accède au dashboard admin + Alors il voit: + | métrique | valeur 2025 | + | Créateurs monétisés | 1,247 | + | Créateurs éligibles DAS2 (>1200€) | 847 (68%) | + | Revenus totaux versés | 1,890,345€ | + | DAS2 transmises à la DGFIP | 847 | + | Taux conformité | 100% | + + Scénario: Support créateur pour questions fiscales + Étant donné que j'ai une question sur ma déclaration fiscale + Quand je contacte le support RoadWave + Alors l'agent peut consulter mes documents fiscaux + Et m'aider à comprendre ce que je dois déclarer + Mais il ne peut pas me conseiller fiscalement (pas expert-comptable) + Et il me recommande de consulter un expert-comptable si nécessaire diff --git a/features/monetisation/paiement-createurs.feature b/features/monetisation/paiement-createurs.feature new file mode 100644 index 0000000..5c1ddbc --- /dev/null +++ b/features/monetisation/paiement-createurs.feature @@ -0,0 +1,308 @@ +# language: fr +Fonctionnalité: Paiement des créateurs + En tant que créateur monétisé + Je veux recevoir mes paiements mensuels de manière fiable + Afin d'être rémunéré pour mon travail + + Contexte: + Étant donné que je suis un créateur avec la monétisation activée + Et que mon KYC est validé + + Scénario: Seuil minimum de 50€ atteint - Paiement effectué + Étant donné que mes revenus du mois sont 73.45€ + Quand le dernier jour du mois arrive + Alors mon solde de 73.45€ est transféré vers "en attente de paiement" + Et le paiement sera effectué le 15 du mois prochain + + Scénario: Seuil minimum de 50€ non atteint - Report mois suivant + Étant donné que mes revenus du mois sont 32.17€ + Quand le dernier jour du mois arrive + Alors mon solde de 32.17€ est reporté au mois suivant + Et je vois "Solde insuffisant pour paiement (<50€). Report mois prochain." + + Scénario: Cumul sur plusieurs mois jusqu'à atteindre 50€ + Étant donné que mes revenus sont: + | mois | revenus | solde cumulé | + | Janvier | 18.50€ | 18.50€ | + | Février | 22.30€ | 40.80€ | + | Mars | 15.70€ | 56.50€ | + Quand la fin du mois de mars arrive + Alors le solde cumulé de 56.50€ dépasse les 50€ + Et un paiement de 56.50€ est effectué le 15 avril + + Scénario: Calcul des revenus le dernier jour du mois + Étant donné que nous sommes le 31 janvier à 23h59 + Quand le système calcule les revenus du mois + Alors une requête SQL agrège tous les revenus pub et premium + Et le solde final du mois est figé dans monthly_revenues + Et le compteur du mois en cours repart à 0€ le 1er février + + Scénario: Période de traitement contestations 1-14 du mois + Étant donné que mes revenus de janvier sont calculés à 150.00€ + Quand la période du 1-14 février arrive + Alors RoadWave analyse les éventuelles fraudes ou contestations + Et si une fraude est détectée, les revenus concernés sont retirés du solde + Et le solde final est validé le 14 février + + Scénario: Virement SEPA le 15 du mois suivant + Étant donné que mes revenus de janvier validés sont 150.00€ + Quand le 15 février arrive + Alors Mangopay initie un virement SEPA depuis mon e-wallet vers mon RIB + Et le statut du paiement passe à "En cours" + + Scénario: Réception virement 16-18 du mois (1-3 jours SEPA) + Étant donné qu'un virement SEPA a été initié le 15 février + Quand 1-3 jours ouvrés s'écoulent + Alors je reçois le virement sur mon compte bancaire entre le 16 et 18 février + Et je peux consulter l'historique des paiements dans mon dashboard + + Scénario: Virement SEPA gratuit pour comptes EU + Étant donné que mon RIB est français (IBAN FR) + Quand Mangopay effectue le virement + Alors aucun frais n'est prélevé (virement SEPA gratuit) + Et je reçois 100% du montant annoncé + + Scénario: Virement international hors EU avec frais variables + Étant donné que je suis créateur expatrié avec RIB hors Union Européenne + Quand Mangopay effectue le virement international + Alors des frais variables s'appliquent selon le pays + Et les frais sont déduits du montant final + Et je vois le détail des frais dans mon historique + + Scénario: E-wallet Mangopay automatique + Étant donné que mon KYC est validé + Quand mes revenus sont calculés + Alors les revenus sont automatiquement transférés vers mon e-wallet Mangopay + Et l'e-wallet est débité lors du virement SEPA vers mon RIB + Et je n'ai aucune action manuelle à faire + + Scénario: Tableau de bord - Revenus pub temps réel + Étant donné que j'accède à mon tableau de bord créateur + Quand je consulte l'onglet "Revenus" + Alors je vois: + | métrique | valeur exemple | + | Revenus pub ce mois | 123.45€ | + | Revenus premium ce mois | 67.89€ | + | Solde disponible ce mois | 191.34€ | + | Prochain paiement | 15 mars 2025 | + Et ces valeurs sont mises à jour en temps réel (cache Redis, refresh 10 min) + + Scénario: Tableau de bord - Solde en attente de paiement + Étant donné que mes revenus de janvier sont calculés et validés + Et que nous sommes le 10 février + Quand je consulte mon tableau de bord + Alors je vois: + | métrique | valeur exemple | + | Solde en attente | 150.00€ | + | Date de paiement | 15 février 2025 | + | Statut | En attente | + + Scénario: Historique des virements permanents + Étant donné que je suis monétisé depuis 6 mois + Quand je consulte l'historique des paiements + Alors je vois la liste complète: + | date paiement | montant | statut | référence virement | + | 15/02/2025 | 150.00€ | Payé | MANGOPAY-ABC123 | + | 15/01/2025 | 123.50€ | Payé | MANGOPAY-XYZ789 | + | 15/12/2024 | 98.75€ | Payé | MANGOPAY-DEF456 | + | ... | ... | ... | ... | + + Scénario: Export comptable CSV téléchargeable + Étant donné que je clique sur "Télécharger export comptable" + Quand le fichier CSV est généré + Alors je télécharge un fichier contenant: + ```csv + Date,Revenus Pub,Revenus Premium,Total,Référence Virement,Statut + 2025-02-15,89.50,60.50,150.00,MANGOPAY-ABC123,Payé + 2025-01-15,78.30,45.20,123.50,MANGOPAY-XYZ789,Payé + 2024-12-15,67.25,31.50,98.75,MANGOPAY-DEF456,Payé + ``` + Et je peux transmettre ce fichier à mon expert-comptable + + Scénario: Échec virement - Tentative 1 échouée + Étant donné qu'un virement est initié le 15 février + Mais que mon RIB est invalide ou le compte est fermé + Quand Mangopay détecte l'échec + Alors le statut passe à "Échec - Retry programmé le 18 février" + Et je reçois un email m'alertant du problème + + Scénario: Échec virement - Retry automatique J+3 + Étant donné que le virement du 15 février a échoué + Quand le 18 février arrive (J+3) + Alors Mangopay tente automatiquement un nouveau virement + Et si le RIB est toujours invalide, le virement échoue à nouveau + + Scénario: Échec virement - Retry automatique J+7 + Étant donné que les 2 premières tentatives ont échoué + Quand le 22 février arrive (J+7) + Alors Mangopay tente une 3ème et dernière fois + Et si le virement échoue encore, la monétisation est suspendue + + Scénario: Échec virement - Suspension monétisation après 3 échecs + Étant donné que les 3 tentatives de virement ont échoué + Quand le système détecte le 3ème échec + Alors ma monétisation est suspendue automatiquement + Et je reçois un email: + """ + Votre monétisation a été suspendue après 3 échecs de virement. + Veuillez mettre à jour votre RIB dans les paramètres de monétisation. + Votre solde de 150.00€ sera versé dès que le RIB sera valide. + """ + + Scénario: Mise à jour RIB et réactivation paiement + Étant donné que ma monétisation est suspendue pour RIB invalide + Et que mon solde en attente est 150.00€ + Quand je mets à jour mon RIB avec un compte valide + Alors Mangopay tente immédiatement un nouveau virement + Et si le virement réussit, ma monétisation est réactivée automatiquement + + Scénario: Notification email lors de chaque paiement + Étant donné qu'un virement de 150.00€ est effectué le 15 février + Quand le virement est confirmé par Mangopay + Alors je reçois un email: + """ + 💰 Paiement RoadWave effectué + + Montant: 150.00€ + Date: 15 février 2025 + Référence: MANGOPAY-ABC123 + Compte bancaire: FR76 **** **** **** **89 + + Ce virement devrait arriver sur votre compte sous 1-3 jours ouvrés. + Détails dans votre tableau de bord créateur. + """ + + Scénario: Justification seuil 50€ - Éviter frais bancaires micro-sommes + Étant donné que Mangopay facture des frais fixes par virement + Et que les banques peuvent facturer des frais de réception + Quand un créateur génère seulement 5€/mois + Alors un virement mensuel coûterait proportionnellement trop cher + Et le seuil de 50€ garantit des frais proportionnels raisonnables + + Scénario: Comparaison avec YouTube (seuil 100$) + Étant donné que YouTube fixe le seuil à 100$ (~90€) + Quand RoadWave fixe le seuil à 50€ + Alors RoadWave est plus accessible pour petits créateurs + Et les paiements arrivent plus rapidement + + Scénario: Comparaison avec Twitch (seuil 50$) + Étant donné que Twitch fixe le seuil à 50$ (~45€) + Quand RoadWave fixe le seuil à 50€ + Alors le seuil est aligné sur Twitch + Et les créateurs comprennent facilement le système + + Scénario: Comparaison avec Spotify (seuil 10€ mais délais longs) + Étant donné que Spotify a un seuil bas de 10€ mais verse tous les 3 mois + Quand RoadWave a un seuil de 50€ mais verse chaque mois + Alors les créateurs reçoivent leurs paiements plus régulièrement + Et la trésorerie est plus prévisible + + Scénario: Relevé mensuel PDF automatique + Étant donné que mes revenus de janvier sont calculés + Quand le 1er février arrive + Alors un relevé mensuel PDF est généré automatiquement: + ``` + ===== RELEVÉ ROADWAVE - JANVIER 2025 ===== + + Créateur: Jean Dupont + SIRET: 12345678901234 + + Revenus publicitaires: 89.50€ + Revenus Premium: 60.50€ + ───────────────────────────────── + TOTAL: 150.00€ + + Écoutes complètes (gratuit): 29,833 + Abonnés Premium actifs: 47 + + Paiement prévu: 15 février 2025 + ``` + Et le PDF est téléchargeable depuis mon tableau de bord + + Scénario: Conservation relevés 10 ans (obligation comptable) + Étant donné que je génère des revenus sur RoadWave + Quand je télécharge mes relevés mensuels + Alors je dois les conserver 10 ans (obligation légale France) + Et RoadWave conserve également une copie pendant 10 ans pour audit + + Scénario: Dashboard admin - Monitoring paiements + Étant donné qu'un admin RoadWave consulte les paiements du mois + Quand il accède au dashboard admin + Alors il voit: + | métrique | valeur exemple | + | Créateurs payés ce mois | 1,247 | + | Montant total versé | 127,345€ | + | Paiements en attente | 34 | + | Échecs virements | 3 | + | Délai moyen réception (jours) | 1.8 | + + Scénario: Alerte admin si taux échec >5% + Étant donné que 8% des virements du mois ont échoué + Quand le système détecte le taux d'échec élevé + Alors une alerte est envoyée à l'équipe technique: + """ + ⚠️ Taux d'échec virements anormal: 8% (seuil: 5%) + Nombre échecs: 102 / 1,247 virements + Causes principales: + - RIB invalides: 67 + - Comptes fermés: 23 + - Autres erreurs: 12 + """ + + Scénario: Statistiques personnelles - Moyenne revenus sur 6 mois + Étant donné que je suis monétisé depuis 6 mois + Quand je consulte mes statistiques + Alors je vois: + | métrique | valeur | + | Revenus moyens/mois | 134.50€ | + | Meilleur mois | 189.00€ | + | Mois le plus bas | 87.30€ | + | Tendance | +12% ↗ | + Et cela m'aide à suivre ma progression + + Scénario: Projection revenus annuels + Étant donné que mes revenus moyens sont 134.50€/mois + Quand je consulte les projections + Alors le système estime mes revenus annuels à ~1,614€ + Et je peux anticiper mes déclarations fiscales + + Scénario: Notification seuil symbolique 1000€ cumulés + Étant donné que mes revenus cumulés depuis inscription atteignent 1000€ + Quand le paiement qui franchit ce seuil est effectué + Alors je reçois une notification: + """ + 🎉 Félicitations ! Vous venez de dépasser 1000€ de revenus cumulés sur RoadWave ! + Merci de contribuer à la plateforme avec votre contenu de qualité. + """ + + Scénario: Performance calcul avec 100 000 créateurs monétisés + Étant donné que RoadWave a 100 000 créateurs monétisés + Quand le calcul des paiements du 15 du mois est lancé + Alors un job asynchrone traite les paiements par batch de 1000 + Et tous les virements sont initiés en 2-4 heures + Et les serveurs Mangopay gèrent la charge sans problème + + Scénario: Backup des données de paiement + Étant donné que les paiements sont critiques pour les créateurs + Quand un paiement est effectué + Alors les données sont sauvegardées dans PostgreSQL (principal) + Et répliquées vers une base de backup (replica) + Et une copie d'archive est stockée sur S3 (conservation 10 ans) + + Scénario: Audit trail complet des paiements + Étant donné qu'un paiement est initié, traité et complété + Quand un audit est demandé + Alors tous les événements sont loggés: + | événement | timestamp | détails | + | Calcul revenus mois | 2025-01-31 23:59:00 | Montant: 150.00€ | + | Validation période fraude | 2025-02-14 23:59:00 | Aucune fraude détectée | + | Initiation virement | 2025-02-15 09:00:00 | Mangopay ref: ABC123 | + | Confirmation virement | 2025-02-16 14:30:00 | Reçu par banque créateur | + Et ces logs sont conservés 10 ans pour conformité + + Scénario: Protection fraude - Détection pattern suspect + Étant donné qu'un créateur génère subitement 10 000€ de revenus en 1 mois + Alors que sa moyenne est de 50€/mois + Quand le système détecte cette anomalie + Alors le paiement est mis en attente pour vérification manuelle + Et l'équipe modération analyse le compte avant validation diff --git a/features/monetisation/sources-revenus.feature b/features/monetisation/sources-revenus.feature new file mode 100644 index 0000000..827960b --- /dev/null +++ b/features/monetisation/sources-revenus.feature @@ -0,0 +1,307 @@ +# language: fr +Fonctionnalité: Sources de revenus créateurs + En tant que créateur monétisé + Je veux générer des revenus via publicités et abonnés Premium + Afin d'être rémunéré pour mon travail + + Contexte: + Étant donné que je suis un créateur avec la monétisation activée + Et que mon KYC est validé + + # ===== PUBLICITÉS (UTILISATEURS GRATUITS) ===== + + Scénario: CPM créateur de 3€ / 1000 écoutes complètes + Étant donné que mes contenus ont généré 1000 écoutes complètes par des utilisateurs gratuits + Quand le calcul des revenus du mois est effectué + Alors je touche 3.00€ pour ces 1000 écoutes + Et ce montant est ajouté à mon solde disponible + + Scénario: 10 000 écoutes gratuits → 30€ de revenus pub + Étant donné que mes contenus ont généré 10 000 écoutes complètes (utilisateurs gratuits) + Quand le mois se termine + Alors je touche 30.00€ de revenus publicitaires + Et ces revenus sont visibles en temps réel dans mon tableau de bord + + Scénario: 50 000 écoutes gratuits → 150€ de revenus pub + Étant donné que mes contenus ont généré 50 000 écoutes complètes (utilisateurs gratuits) + Quand le mois se termine + Alors je touche 150.00€ de revenus publicitaires + + Scénario: 100 000 écoutes gratuits → 300€ de revenus pub + Étant donné que mes contenus ont généré 100 000 écoutes complètes (utilisateurs gratuits) + Quand le mois se termine + Alors je touche 300.00€ de revenus publicitaires + + Scénario: Répartition économique - Plateforme garde 94% + Étant donné qu'une publicité facturée 0.05€/écoute génère 50€ CPM + Quand la plateforme calcule la répartition + Alors le créateur touche 3€ (6% du CA pub) + Et la plateforme garde 47€ (94%) pour: + | poste budgétaire | coût estimé | + | CDN + infrastructure | 10-15€ | + | Modération + support | 5-10€ | + | Développement + R&D | 10-15€ | + | Marge opérationnelle | 10-15€ | + + Scénario: Écoute complète = ≥80% du contenu écouté + Étant donné qu'un utilisateur gratuit écoute mon contenu de 10 minutes + Quand il écoute 8 minutes (80%) + Alors l'écoute compte comme "complète" + Et je génère 0.003€ de revenus pub (3€/1000) + + Scénario: Écoute incomplète <80% ne compte pas + Étant donné qu'un utilisateur gratuit écoute mon contenu de 10 minutes + Mais il skip après 5 minutes (50%) + Quand le calcul des revenus est effectué + Alors cette écoute ne compte pas comme "complète" + Et je ne génère aucun revenu publicitaire pour cette écoute + + Scénario: Écoutes Premium ne comptent pas pour les revenus pub + Étant donné qu'un utilisateur Premium écoute 100% de mon contenu + Quand le calcul des revenus publicitaires est effectué + Alors cette écoute ne compte pas dans les revenus pub + Car les utilisateurs Premium ne voient pas de publicités + Mais elle compte dans les revenus Premium (système séparé) + + Scénario: Détection bots - Écoutes exclues + Étant donné qu'un bot génère 10 000 écoutes artificielles sur mes contenus + Quand le système détecte le pattern suspect (rate limiting, IP unique, etc.) + Alors ces écoutes sont marquées comme frauduleuses + Et elles sont exclues du calcul des revenus publicitaires + + Scénario: Comparaison avec YouTube (3-5€/1000 vues) + Étant donné que YouTube paie 3-5€/1000 vues + Quand RoadWave fixe le CPM créateur à 3€/1000 écoutes + Alors le tarif est aligné sur le bas de la fourchette YouTube + Et cela est compétitif pour un MVP sans marché publicitaire mature + + Scénario: Comparaison avec Spotify (3-4€/1000 écoutes) + Étant donné que Spotify paie ~3-4€/1000 écoutes + Quand RoadWave fixe le CPM créateur à 3€/1000 écoutes + Alors le tarif est aligné sur l'industrie musicale + Et les créateurs audio peuvent anticiper des revenus similaires + + Scénario: Tableau de bord - Revenus pub temps réel + Étant donné que j'accède à mon tableau de bord créateur + Quand je consulte mes revenus publicitaires + Alors je vois: + | métrique | valeur exemple | + | Écoutes complètes ce mois (gratuit)| 23 456 | + | Revenus pub ce mois | 70.37€ | + | CPM effectif | 3.00€ | + Et ces valeurs sont mises à jour toutes les 10 minutes + + # ===== ABONNÉS PREMIUM ===== + + Scénario: Répartition 70/30 - Créateur touche 70% + Étant donné qu'un utilisateur Premium paie 4.99€/mois + Quand la répartition est calculée + Alors 3.49€ sont reversés aux créateurs écoutés (70%) + Et 1.50€ sont gardés par la plateforme (30%) + + Scénario: Utilisateur écoute 3 créateurs - Répartition proportionnelle + Étant donné qu'un utilisateur Premium paie 4.99€/mois + Et qu'il écoute 3 créateurs ce mois: + | créateur | temps écoute | ratio | + | Créateur A | 10h | 50% | + | Créateur B | 6h | 30% | + | Créateur C | 4h | 20% | + Quand le calcul des revenus Premium est effectué + Alors la répartition est: + | créateur | revenus | + | Créateur A | 1.75€ | + | Créateur B | 1.05€ | + | Créateur C | 0.70€ | + Et la somme totale versée aux créateurs est 3.50€ (70% de 4.99€) + + Scénario: Calcul SQL proportionnel au temps d'écoute + Étant donné qu'un utilisateur Premium a écouté plusieurs créateurs + Quand le système calcule les revenus du mois + Alors la requête SQL suivante est exécutée: + ```sql + SELECT + creator_id, + SUM(listen_duration_seconds) AS total_seconds, + (SUM(listen_duration_seconds) / (SELECT SUM(listen_duration_seconds) FROM premium_listens WHERE user_id = :user_id AND month = :current_month)) AS ratio, + (4.99 * 0.70 * ratio) AS revenue_euros + FROM premium_listens + WHERE user_id = :user_id + AND month = :current_month + GROUP BY creator_id; + ``` + + Scénario: Utilisateur écoute un seul créateur - 100% à ce créateur + Étant donné qu'un utilisateur Premium paie 4.99€/mois + Et qu'il n'écoute qu'un seul créateur (moi) + Quand le mois se termine + Alors je touche 3.49€ (70% de 4.99€) + Et je reçois 100% de la part créateurs + + Scénario: Utilisateur Premium inactif - Aucun revenu généré + Étant donné qu'un utilisateur Premium paie 4.99€/mois + Mais qu'il n'écoute aucun contenu ce mois + Quand le calcul des revenus Premium est effectué + Alors aucun créateur ne reçoit de revenus de cet utilisateur + Et les 3.49€ de la part créateurs restent à la plateforme + Et cela couvre les coûts d'infrastructure + + Scénario: Comparaison avec YouTube Premium (70/30) + Étant donné que YouTube Premium reverse 70% aux créateurs + Quand RoadWave fixe également 70/30 + Alors le modèle est aligné sur le standard industrie + Et les créateurs ont confiance dans l'équité du système + + Scénario: Comparaison avec Spotify (70/30) + Étant donné que Spotify reverse 70% aux artistes + Quand RoadWave fixe également 70/30 + Alors le modèle est identique à Spotify + Et les créateurs audio comprennent facilement le système + + Scénario: Apple Music moins avantageux (52/48) + Étant donné qu'Apple Music ne reverse que 52% aux artistes + Quand RoadWave offre 70% aux créateurs + Alors RoadWave est plus avantageux de 18 points + Et cela devient un argument marketing fort + + Scénario: Justification équité - Créateurs les plus écoutés gagnent plus + Étant donné que 2 créateurs ont le même nombre d'abonnés Premium + Mais que le Créateur A est écouté 20h/mois et le Créateur B seulement 2h/mois + Quand les revenus Premium sont calculés + Alors le Créateur A gagne 10× plus que le Créateur B + Et cela récompense la qualité et l'engagement (pas juste l'abonnement) + + Scénario: Pas de "winner takes all" - Équité totale + Étant donné qu'un utilisateur Premium écoute 10 créateurs différents + Quand les revenus sont calculés + Alors chacun des 10 créateurs reçoit sa part proportionnelle + Et il n'y a pas de système où un seul créateur prend tout + + Scénario: Marge plateforme 30% couvre absence revenus pub Premium + Étant donné qu'un utilisateur Premium ne voit aucune publicité + Quand la plateforme calcule ses revenus + Alors elle ne touche que les 30% de l'abonnement Premium (1.50€) + Et cette marge compense la perte des revenus publicitaires (qui auraient été ~47€/1000 écoutes) + + Scénario: Tableau de bord - Revenus Premium temps réel + Étant donné que j'accède à mon tableau de bord créateur + Quand je consulte mes revenus Premium + Alors je vois: + | métrique | valeur exemple | + | Abonnés Premium actifs ayant écouté | 47 | + | Heures d'écoute Premium ce mois | 234h | + | Revenus Premium ce mois | 89.23€ | + Et ces valeurs sont mises à jour toutes les 10 minutes + + # ===== CUMUL REVENUS PUB + PREMIUM ===== + + Scénario: Revenus cumulés pub + premium + Étant donné que j'ai généré ce mois: + | source | montant | + | Revenus pub | 150.00€ | + | Revenus Premium | 89.23€ | + Quand je consulte mon solde disponible + Alors le total est 239.23€ + Et ce solde sera versé le 15 du mois prochain (si ≥50€) + + Scénario: Dashboard créateur - Vue d'ensemble + Étant donné que j'accède à mon tableau de bord créateur + Quand je consulte la page revenus + Alors je vois: + ``` + ===== REVENUS DU MOIS EN COURS ===== + + 📊 Publicités (utilisateurs gratuits) + - Écoutes complètes: 50,234 + - Revenus pub: 150.70€ + + 👑 Abonnés Premium + - Abonnés actifs: 47 + - Heures d'écoute: 234h + - Revenus Premium: 89.23€ + + 💰 TOTAL CE MOIS: 239.93€ + + ===== HISTORIQUE ===== + Mois dernier: 178.45€ (versé le 15/01) + Il y a 2 mois: 156.78€ (versé le 15/12) + ``` + + Scénario: Export comptable CSV pour expert-comptable + Étant donné que je clique sur "Exporter pour comptable" + Quand l'export est généré + Alors je télécharge un fichier CSV: + ```csv + Mois,Revenus Pub,Revenus Premium,Total,Statut + 2025-01,150.70,89.23,239.93,En attente versement + 2024-12,123.45,55.00,178.45,Versé le 15/01/2025 + 2024-11,100.30,56.48,156.78,Versé le 15/12/2024 + ``` + Et je peux transmettre ce fichier à mon expert-comptable + + Scénario: Notification hebdomadaire progression revenus + Étant donné que je suis créateur monétisé + Quand chaque lundi matin arrive + Alors je reçois un email récapitulatif: + """ + 📊 Vos revenus de la semaine dernière: + - Revenus pub: +23.45€ (3,456 écoutes) + - Revenus Premium: +12.78€ (12 abonnés actifs) + - Total semaine: 36.23€ + - Projection mois: ~150€ + """ + + Scénario: Graphique évolution revenus sur 12 mois + Étant donné que je suis monétisé depuis 12 mois + Quand j'accède à mes statistiques + Alors je vois un graphique en courbes montrant: + | mois | revenus pub | revenus premium | total | + | Jan 25 | 150€ | 89€ | 239€ | + | Déc 24 | 123€ | 55€ | 178€ | + | Nov 24 | 100€ | 56€ | 156€ | + | ... | ... | ... | ... | + Et cela m'aide à suivre ma progression + + Scénario: Top 3 contenus les plus rentables du mois + Étant donné que j'ai publié 20 contenus ce mois + Quand je consulte mes statistiques détaillées + Alors je vois mon top 3 contenus: + | titre | écoutes | revenus pub | revenus premium | total | + | Mon meilleur épisode | 12,345 | 37.04€ | 23.45€ | 60.49€ | + | Discussion tech | 8,901 | 26.70€ | 15.67€ | 42.37€ | + | Road trip Bretagne | 7,234 | 21.70€ | 12.34€ | 34.04€ | + Et cela m'aide à comprendre quel type de contenu plaît le plus + + Scénario: Alertes seuils de revenus + Étant donné que j'ai activé les notifications de seuils + Quand mes revenus du mois dépassent 100€ pour la première fois + Alors je reçois une notification: + """ + 🎉 Félicitations ! Vous venez de dépasser 100€ de revenus ce mois ! + Continuez comme ça ! + """ + + Scénario: Performance calcul avec 100 000 créateurs + Étant donné que RoadWave a 100 000 créateurs monétisés + Quand le calcul des revenus mensuels est lancé le dernier jour du mois + Alors un job asynchrone traite tous les créateurs + Et le calcul prend environ 2-4 heures pour tous les créateurs + Et les résultats sont stockés dans la table monthly_revenues + + Scénario: Cache Redis pour métriques temps réel + Étant donné que je consulte mon dashboard plusieurs fois par jour + Quand la page se charge + Alors les compteurs sont récupérés depuis Redis: + | clé Redis | valeur exemple | + | creator:[id]:complete_listens:202501 | 50234 | + | creator:[id]:premium_hours:202501 | 234 | + | creator:[id]:revenue_ads:202501 | 150.70 | + | creator:[id]:revenue_premium:202501 | 89.23 | + Et le temps de réponse est <30ms + + Scénario: Prévision revenus fin de mois + Étant donné que nous sommes le 20 du mois + Et que mes revenus actuels sont 160€ + Quand le système calcule la projection + Alors il estime les revenus fin de mois à ~240€ (extrapolation linéaire) + Et affiche "Projection fin de mois: ~240€" + Et cela m'aide à anticiper mes revenus diff --git a/features/navigation/actions-mode-pieton.feature b/features/navigation/actions-mode-pieton.feature new file mode 100644 index 0000000..b43f62c --- /dev/null +++ b/features/navigation/actions-mode-pieton.feature @@ -0,0 +1,195 @@ +# language: fr +Fonctionnalité: Actions complémentaires - Mode piéton + En tant qu'auditeur en mode piéton + Je veux accéder à des actions avancées depuis l'application mobile + Afin de liker explicitement, m'abonner ou signaler du contenu + + Contexte: + Étant donné que l'API RoadWave est disponible + Et qu'un utilisateur est connecté + Et qu'il est en mode piéton (vitesse < 5 km/h) + + Scénario: Like explicite avec bouton cœur + Étant donné que j'écoute un contenu tagué "Automobile" + Et que ma jauge "Automobile" est à 60% + Quand je clique sur le bouton cœur "Like" + Alors ma jauge "Automobile" augmente de 2% + Et une animation de cœur rouge s'affiche + Et une vibration courte est déclenchée + Et ma jauge "Automobile" est maintenant à 62% + + Scénario: Like explicite cumulable avec like automatique + Étant donné que j'ai écouté un contenu "Voyage" à 85% + Et que j'ai reçu un like automatique renforcé (+2%) + Et que ma jauge "Voyage" est à 52% + Quand je clique sur le bouton cœur "Like" + Alors ma jauge "Voyage" augmente encore de 2% + Et ma jauge "Voyage" passe à 54% + Et les deux likes sont cumulés + + Scénario: Unlike retire le like manuel uniquement + Étant donné que j'ai liké manuellement un contenu "Sport" + Et que ma jauge "Sport" est à 57% + Quand je clique à nouveau sur le bouton cœur (toggle) + Alors le cœur redevient vide (unlike) + Et ma jauge "Sport" diminue de 2% + Et ma jauge "Sport" revient à 55% + + Scénario: Unlike ne retire pas le like automatique + Étant donné que j'ai écouté un contenu "Musique" à 90% + Et que j'ai reçu un like automatique renforcé (+2%) + Et que ma jauge "Musique" est à 52% + Et que je n'ai PAS liké manuellement + Quand je consulte l'interface + Alors le bouton "Unlike" n'est pas disponible + Et le cœur reste grisé (aucun like manuel) + Et ma jauge reste à 52% + + Scénario: Abonnement à un créateur + Étant donné qu'un créateur publie des contenus tagués "Automobile" et "Technologie" + Et que mes jauges sont: + | catégorie | niveau | + | Automobile | 50% | + | Technologie | 45% | + Quand je clique sur "S'abonner" sur le profil du créateur + Alors ma jauge "Automobile" augmente de 5% + Et ma jauge "Technologie" augmente de 5% + Et une animation d'étoile dorée s'affiche + Et un badge "Abonné ✓" apparaît sur le profil + Et mes nouvelles jauges sont: + | catégorie | niveau | + | Automobile | 55% | + | Technologie | 50% | + + Scénario: Désabonnement d'un créateur + Étant donné que je suis abonné à un créateur + Et que mes jauges "Automobile" et "Technologie" sont à 55% et 50% + Quand je clique sur "Se désabonner" + Alors ma jauge "Automobile" diminue de 5% + Et ma jauge "Technologie" diminue de 5% + Et le badge "Abonné ✓" disparaît + Et mes nouvelles jauges sont: + | catégorie | niveau | + | Automobile | 50% | + | Technologie | 45% | + + Scénario: Signalement d'un contenu inapproprié + Étant donné que j'écoute un contenu + Quand je clique sur le menu contextuel "⋮" + Et que je sélectionne "Signaler" + Alors un formulaire de signalement s'ouvre + Et je dois sélectionner une catégorie: + | Catégorie | + | Haine et violence | + | Contenu sexuel | + | Illégalité | + | Droits d'auteur | + | Spam | + | Désinformation (fake news) | + | Autre | + Et je peux ajouter un commentaire optionnel + Et le signalement est envoyé au flux de modération + + Scénario: Feedback visuel pour like explicite + Étant donné que je clique sur le bouton cœur + Quand le like est enregistré + Alors une animation de cœur rouge se lance (0.5s) + Et le cœur reste rouge plein + Et une vibration haptique courte est déclenchée (iOS: .light, Android: 50ms) + Et un badge "♥ Ajouté à vos favoris" s'affiche 2 secondes + + Scénario: Feedback visuel pour abonnement + Étant donné que je clique sur "S'abonner" + Quand l'abonnement est enregistré + Alors une animation d'étoile dorée se lance (0.8s) + Et le bouton devient "Abonné ✓" avec badge doré + Et une notification "Abonné à [Créateur]" s'affiche + Et les contenus du créateur seront boostés +30% dans l'algo + + Scénario: Menu contextuel avec toutes les options + Étant donné que j'utilise l'app en mode piéton + Quand je clique sur le menu "⋮" (3 points verticaux) + Alors les options disponibles sont: + | Option | + | Like (cœur) | + | S'abonner au créateur | + | Signaler | + | Partager | + | Voir le profil du créateur | + | Télécharger (mode offline) | + Et toutes les options sont cliquables + + Scénario: Persistance des likes manuels en base de données + Étant donné que je like manuellement 5 contenus + Quand je ferme l'application + Et que je me reconnecte plus tard + Alors tous mes likes manuels sont toujours présents + Et les cœurs rouges sont affichés sur les contenus likés + Et mes jauges reflètent toujours l'impact (+2% × 5 likes) + + Scénario: Liste "Mes contenus likés" accessible dans profil + Étant donné que j'ai liké manuellement 10 contenus + Quand j'accède à mon profil utilisateur + Alors je vois une section "❤️ Mes favoris" + Et la liste affiche les 10 contenus likés + Et je peux cliquer pour réécouter + Et je peux retirer un like (unlike) depuis cette liste + + Scénario: Liste "Mes abonnements" accessible dans profil + Étant donné que je suis abonné à 5 créateurs + Quand j'accède à mon profil utilisateur + Alors je vois une section "⭐ Mes abonnements" + Et la liste affiche les 5 créateurs avec leurs avatars + Et je peux accéder au profil de chaque créateur + Et je peux me désabonner depuis cette liste + + Scénario: Impact abonnement sur tous les tags du créateur + Étant donné qu'un créateur a publié des contenus avec ces tags: + | Contenu | Tags | + | C1 | Automobile, Voyage | + | C2 | Automobile, Technologie | + | C3 | Voyage, Famille | + Et que mes jauges sont toutes à 50% + Quand je m'abonne à ce créateur + Alors les jauges impactées sont: + | Tag | Impact | + | Automobile | +5% | + | Voyage | +5% | + | Technologie | +5% | + | Famille | +5% | + Et toutes les autres jauges restent à 50% + + Scénario: Limite d'abonnements (200 maximum) + Étant donné que je suis abonné à 200 créateurs + Quand j'essaie de m'abonner à un 201ème créateur + Alors un message "Limite de 200 abonnements atteinte" s'affiche + Et je dois me désabonner d'un créateur existant pour en ajouter un nouveau + + Scénario: Confirmation avant désabonnement + Étant donné que je suis abonné à un créateur + Quand je clique sur "Se désabonner" + Alors une popup de confirmation s'affiche: + """ + Se désabonner de @CreateurNom ? + + Vous ne recevrez plus de notifications pour ses contenus. + Vos jauges diminueront de 5%. + """ + Et je dois confirmer pour valider + Et je peux annuler pour conserver l'abonnement + + Plan du Scénario: Cumul like automatique + like manuel + Étant donné qu'un contenu est tagué "Sport" + Et que ma jauge "Sport" est à 50% + Quand j'écoute à % (like auto ) + Et que je like manuellement (+2%) + Alors l'impact total est + Et ma nouvelle jauge est + + Exemples: + | pourcentage | auto | total | nouveau_niveau | + | 10 | 0 | +2% | 52% | + | 30 | +1% | +3% | 53% | + | 50 | +1% | +3% | 53% | + | 80 | +2% | +4% | 54% | + | 95 | +2% | +4% | 54% | diff --git a/features/navigation/commande-precedent.feature b/features/navigation/commande-precedent.feature new file mode 100644 index 0000000..073971c --- /dev/null +++ b/features/navigation/commande-precedent.feature @@ -0,0 +1,177 @@ +# language: fr +Fonctionnalité: Commande "Précédent" + En tant qu'auditeur + Je veux que le bouton "Précédent" ait un comportement intelligent + Afin de rejouer le contenu actuel ou revenir au précédent selon la progression + + Contexte: + Étant donné que l'API RoadWave est disponible + Et qu'un utilisateur est connecté + + Scénario: Précédent après <10s revient au contenu précédent + Étant donné que j'ai écouté le contenu "A" pendant 2 minutes + Et que j'écoute maintenant le contenu "B" depuis 5 secondes + Quand j'appuie sur "Précédent" + Alors la lecture revient au contenu "A" + Et la position de lecture est à 2 minutes (position exacte sauvegardée) + Et le contenu "B" reste en historique + + Scénario: Précédent après ≥10s rejoue le contenu actuel + Étant donné que j'écoute le contenu "C" depuis 15 secondes + Quand j'appuie sur "Précédent" + Alors le contenu "C" rejoue depuis le début (position 0:00) + Et la lecture ne revient pas au contenu précédent + Et la progress bar revient à 0% + + Scénario: Précédent exactement à 10s rejoue le contenu actuel + Étant donné que j'écoute le contenu "D" depuis exactement 10 secondes + Quand j'appuie sur "Précédent" + Alors le contenu "D" rejoue depuis le début + Et la lecture ne revient pas au contenu précédent + + Scénario: Précédent sur le premier contenu de session + Étant donné que je viens de démarrer l'application + Et que j'écoute le contenu "Premier" depuis 3 secondes + Quand j'appuie sur "Précédent" + Alors le contenu "Premier" rejoue depuis le début + Et aucun contenu précédent n'existe + + Scénario: Historique de navigation limité à 10 contenus + Étant donné que j'ai écouté 10 contenus [C1, C2, ..., C10] + Et que l'historique Redis contient 10 entrées + Quand je passe au contenu C11 + Alors le contenu C1 est supprimé de l'historique (FIFO) + Et l'historique contient [C2, C3, ..., C10, C11] + Et la taille reste à 10 contenus maximum + + Scénario: Position exacte sauvegardée dans l'historique + Étant donné que j'écoute le contenu "A" (durée 5 minutes) + Quand j'atteins 2 minutes 30 secondes + Et que j'appuie sur "Suivant" + Alors l'historique enregistre: + | content_id | position_seconds | listened_at | + | A | 150 | 2026-01-21T10:30:00 | + Quand je reviens au contenu "A" via "Précédent" + Alors la lecture reprend exactement à 2 minutes 30 secondes + + Scénario: Navigation arrière sur plusieurs contenus + Étant donné que j'ai écouté dans l'ordre: A (2min), B (30s), C (3min) + Et que j'écoute maintenant D depuis 1 seconde + Quand j'appuie sur "Précédent" (1ère fois) + Alors je reviens au contenu C à la position 3 minutes + Quand j'appuie sur "Précédent" (<10s sur C) + Alors je reviens au contenu B à la position 30 secondes + Quand j'appuie sur "Précédent" (<10s sur B) + Alors je reviens au contenu A à la position 2 minutes + + Scénario: Précédent après milieu du contenu rejoue depuis début + Étant donné que j'écoute un contenu de 5 minutes + Quand j'atteins 2 minutes 30 secondes (milieu) + Et que j'appuie sur "Précédent" + Alors le contenu actuel rejoue depuis 0:00 + Et je ne reviens pas au contenu précédent + + Scénario: Enchaînement Suivant puis Précédent rapide + Étant donné que j'écoute le contenu "A" depuis 1 minute + Quand j'appuie sur "Suivant" + Alors le contenu "B" démarre + Quand j'appuie immédiatement sur "Précédent" (2s après) + Alors je reviens au contenu "A" à la position 1 minute + Et le contenu "B" reste dans l'historique + + Scénario: Transition fluide avec animation 0.3s + Étant donné que j'appuie sur "Précédent" + Quand le changement de contenu se produit + Alors la transition audio utilise un fade out/in de 0.3 secondes + Et la progress bar revient avec une animation fluide + Et l'interface ne montre aucun message de confirmation + + Scénario: Historique survit au changement de réseau + Étant donné que j'ai un historique de 5 contenus en cache Redis + Quand je perds la connexion réseau temporairement + Et que je reviens en ligne + Alors l'historique de navigation est toujours disponible + Et je peux toujours utiliser "Précédent" + + Scénario: Historique stocké en Redis avec structure complète + Étant donné que j'ai écouté 3 contenus + Quand je consulte le cache Redis + Alors la structure est: + """ + user:{user_id}:history = [ + {content_id: "C3", position_seconds: 45, listened_at: "2026-01-21T10:33:00Z"}, + {content_id: "C2", position_seconds: 120, listened_at: "2026-01-21T10:30:00Z"}, + {content_id: "C1", position_seconds: 180, listened_at: "2026-01-21T10:27:00Z"} + ] + """ + Et l'ordre est du plus récent au plus ancien + + Scénario: Précédent sur contenu en cours au début (<10s) du premier + Étant donné que je démarre une session avec le contenu "Initial" + Et que j'écoute depuis 3 secondes + Quand j'appuie sur "Précédent" + Alors le contenu "Initial" rejoue depuis le début + Et aucune erreur n'est générée + Et l'historique reste vide + + Scénario: Compteur de temps respecte les seuils exacts + Étant donné que j'écoute un contenu + Quand le temps écoulé est de 9.9 secondes + Et que j'appuie sur "Précédent" + Alors je reviens au contenu précédent + Quand le temps écoulé est de 10.0 secondes + Et que j'appuie sur "Précédent" + Alors le contenu actuel rejoue depuis le début + + Scénario: Progress bar visuelle reflète le retour exact + Étant donné que j'ai écouté le contenu "A" jusqu'à 75% (3min45 sur 5min) + Et que je suis passé au contenu "B" + Quand je reviens au contenu "A" via "Précédent" + Alors la progress bar affiche 75% + Et l'indicateur de temps affiche "3:45 / 5:00" + Et la lecture reprend exactement à cet endroit + + Scénario: Métadonnées d'historique incluent timestamp précis + Étant donné que j'écoute un contenu "X" pendant 45 secondes à 10:30:15 + Quand je passe au contenu suivant + Alors l'historique enregistre: + | content_id | position_seconds | listened_at | + | X | 45 | 2026-01-21T10:30:15Z | + Et le timestamp précis permet l'analyse d'usage + + Scénario: Suppression FIFO respecte l'ordre chronologique + Étant donné un historique de [C1@10:00, C2@10:02, ..., C10@10:20] + Quand j'ajoute C11 à 10:22 + Alors C1 (le plus ancien) est supprimé + Et l'historique contient [C2@10:02, ..., C11@10:22] + Et la taille reste exactement 10 entrées + + Plan du Scénario: Comportement selon temps écouté + Étant donné que j'écoute un contenu depuis secondes + Quand j'appuie sur "Précédent" + Alors l'action est + + Exemples: + | temps | comportement | + | 1 | revenir au contenu précédent | + | 5 | revenir au contenu précédent | + | 9 | revenir au contenu précédent | + | 10 | rejouer le contenu actuel depuis 0:00 | + | 11 | rejouer le contenu actuel depuis 0:00 | + | 30 | rejouer le contenu actuel depuis 0:00 | + | 180 | rejouer le contenu actuel depuis 0:00 | + + Plan du Scénario: Positions de reprise exactes + Étant donné que j'écoute un contenu de 10 minutes + Quand j'atteins et passe au suivant + Et que je reviens via "Précédent" + Alors la lecture reprend exactement à + + Exemples: + | position | + | 0:15 | + | 1:30 | + | 3:45 | + | 5:00 | + | 7:23 | + | 9:50 | diff --git a/features/navigation/commandes-vocales.feature b/features/navigation/commandes-vocales.feature new file mode 100644 index 0000000..fa173c6 --- /dev/null +++ b/features/navigation/commandes-vocales.feature @@ -0,0 +1,238 @@ +# language: fr +Fonctionnalité: Commandes vocales CarPlay et Android Auto + En tant que conducteur avec CarPlay ou Android Auto + Je veux utiliser des commandes vocales pour interagir avec l'application + Afin de garder les mains sur le volant et les yeux sur la route + + Contexte: + Étant donné que l'API RoadWave est disponible + Et qu'un utilisateur est connecté + Et que CarPlay ou Android Auto est activé + + Scénario: Disponibilité des commandes vocales uniquement avec CarPlay/Android Auto + Étant donné que je conduis avec CarPlay activé + Quand je dis "Hey Siri" + Alors Siri est disponible pour les commandes RoadWave + Étant donné que je conduis avec Android Auto activé + Quand je dis "OK Google" + Alors Google Assistant est disponible pour les commandes RoadWave + + Scénario: Parc automobile compatible avec vocal (30-40% en 2026) + Étant donné que nous sommes en 2026 + Quand je consulte les statistiques du parc automobile EU + Alors environ 30-40% des véhicules ont CarPlay ou Android Auto + Et ces utilisateurs peuvent utiliser les commandes vocales + Et les 60-70% restants utilisent les commandes au volant uniquement + + Scénario: Commande vocale "Like ce podcast" avec Siri + Étant donné que j'écoute un contenu tagué "Automobile" + Et que ma jauge "Automobile" est à 60% + Quand je dis "Hey Siri, like ce podcast" + Alors un like explicite (+2%) est enregistré + Et ma jauge "Automobile" passe à 62% + Et Siri confirme vocalement "J'ai ajouté ce contenu à vos favoris" + Et aucune interaction écran n'est requise + + Scénario: Commande vocale "Like ce contenu" avec Google Assistant + Étant donné que j'écoute un contenu tagué "Voyage" + Quand je dis "OK Google, like ce contenu" + Alors un like explicite est enregistré (+2%) + Et Google Assistant confirme "J'ai liké ce contenu pour vous" + Et la commande fonctionne sans toucher l'écran + + Scénario: Commande vocale "Abonne-moi à ce créateur" + Étant donné que j'écoute un contenu d'un créateur tagué "Automobile" et "Technologie" + Et que mes jauges sont à 50% et 45% + Quand je dis "Hey Siri, abonne-moi à ce créateur" + Alors l'abonnement est enregistré + Et mes jauges augmentent de 5% chacune (55% et 50%) + Et Siri confirme "Vous êtes maintenant abonné à [Nom du créateur]" + + Scénario: Commande vocale "Passe au contenu suivant" + Étant donné que j'écoute un contenu "A" + Quand je dis "Hey Siri, passe au contenu suivant" + Alors le contenu "B" démarre immédiatement + Et la commande a le même effet que le bouton physique "Suivant" + + Scénario: Commande vocale "Signale ce contenu" + Étant donné que j'écoute un contenu inapproprié + Quand je dis "OK Google, signale ce contenu" + Alors Google Assistant demande "Quelle catégorie ?" + Et je réponds vocalement "Spam" + Alors le signalement est enregistré avec la catégorie "Spam" + Et Google Assistant confirme "J'ai signalé ce contenu" + + Scénario: Commande vocale avec catégorie de signalement + Étant donné que j'écoute un contenu + Quand je dis "Hey Siri, signale ce contenu pour haine" + Alors le signalement est enregistré avec la catégorie "Haine et violence" + Et Siri confirme "J'ai signalé ce contenu pour haine et violence" + Et le flux de modération reçoit le signalement + + Scénario: Liste des catégories de signalement vocales supportées + Étant donné que je dis "signale ce contenu pour [catégorie]" + Quand la catégorie est: + | Mot-clé vocal | Catégorie mappée | + | "haine" | Haine et violence | + | "sexuel" | Contenu sexuel | + | "illégalité" | Illégalité | + | "droits d'auteur" | Droits d'auteur | + | "spam" | Spam | + | "fake news" | Désinformation | + | "autre" | Autre | + Alors le signalement est enregistré avec la bonne catégorie + + Scénario: Commande vocale non reconnue - fallback + Étant donné que je dis "Hey Siri, super ce podcast" + Quand Siri ne reconnaît pas l'intent RoadWave + Alors Siri répond "Je ne comprends pas cette commande RoadWave" + Et elle suggère "Dites 'like ce podcast' ou 'passe au suivant'" + + Scénario: Commandes vocales disponibles en conduite uniquement + Étant donné que je roule à 50 km/h + Quand j'utilise les commandes vocales + Alors toutes les commandes sont disponibles: + | Commande | Action | + | "Like ce podcast" | Like explicite +2% | + | "Abonne-moi à ce créateur" | Abonnement +5% | + | "Passe au suivant" | Contenu suivant | + | "Reviens au précédent" | Contenu précédent (règle 10s) | + | "Pause" | Pause lecture | + | "Reprends la lecture" | Play | + | "Signale ce contenu" | Signalement | + + Scénario: Intent iOS personnalisé pour RoadWave + Étant donné que l'app iOS implémente les Intents + Quand je configure les Shortcuts iOS + Alors les intents suivants sont disponibles: + | Intent Name | Action | + | LikeCurrentContentIntent | Like explicite | + | SubscribeToCreatorIntent | Abonnement | + | ReportContentIntent | Signalement | + | SkipToNextContentIntent | Suivant | + Et Siri les reconnaît automatiquement + + Scénario: Intent Android personnalisé pour RoadWave + Étant donné que l'app Android implémente les Voice Actions + Quand je configure les actions Google Assistant + Alors les actions suivantes sont disponibles: + | Action Name | Action | + | com.roadwave.LIKE_CONTENT | Like explicite | + | com.roadwave.SUBSCRIBE_CREATOR | Abonnement | + | com.roadwave.REPORT_CONTENT | Signalement | + | com.roadwave.SKIP_NEXT | Suivant | + Et Google Assistant les reconnaît + + Scénario: Confirmation vocale après action réussie + Étant donné que je dis "Hey Siri, like ce podcast" + Quand l'action est enregistrée avec succès + Alors Siri répond immédiatement avec confirmation: + """ + J'ai ajouté ce contenu à vos favoris + """ + Et la réponse est naturelle et concise + Et elle ne distrait pas de la conduite + + Scénario: Gestion d'erreur vocale si action échoue + Étant donné que je dis "Hey Siri, abonne-moi à ce créateur" + Et que j'ai atteint la limite de 200 abonnements + Quand Siri essaie d'enregistrer l'abonnement + Alors l'action échoue + Et Siri répond "Impossible de s'abonner, limite de 200 abonnements atteinte" + Et elle suggère "Désabonnez-vous d'un créateur pour continuer" + + Scénario: Commandes vocales multilingues (français) + Étant donné que mon Siri est configuré en français + Quand je dis "Hey Siri, j'aime ce podcast" + Alors la commande est reconnue (variante de "like ce podcast") + Quand je dis "Hey Siri, mets une étoile" + Alors la commande est reconnue (variante de "like") + + Scénario: Implémentation post-MVP (Sprint 5) + Étant donné que les commandes vocales sont une feature Sprint 5 + Quand le MVP est lancé + Alors seules les commandes au volant physiques sont disponibles + Quand le Sprint 5 est déployé + Alors les intents iOS/Android sont activés + Et les commandes vocales deviennent disponibles + + Scénario: Priorisation commandes vocales vs boutons physiques + Étant donné que je conduis avec CarPlay + Et que j'ai accès aux boutons physiques ET aux commandes vocales + Quand je veux liker un contenu + Alors je peux soit: + - Attendre l'arrêt et cliquer le bouton cœur (recommandé) + - Dire "Hey Siri, like ce podcast" (en conduite) + - Laisser le like automatique se faire (écoute ≥80%) + Et les 3 méthodes sont valides + + Scénario: Statistiques d'usage des commandes vocales + Étant donné que 100 utilisateurs avec CarPlay utilisent RoadWave + Quand je consulte les analytics + Alors je peux voir: + | Métrique | Exemple valeur | + | Taux d'utilisation commandes vocal | 15% | + | Commande la plus utilisée | "Like" | + | Taux de reconnaissance réussie | 92% | + | Taux d'échec / incompréhension | 8% | + + Scénario: Feedback haptique désactivé pour commandes vocales + Étant donné que je like un contenu via commande vocale + Quand l'action est enregistrée + Alors aucune vibration haptique n'est déclenchée + Et seule la confirmation vocale est donnée + Car je n'ai pas le téléphone en main + + Scénario: Badge visuel mis à jour après commande vocale + Étant donné que je dis "Hey Siri, like ce podcast" + Quand l'action est enregistrée + Alors le badge "♥ Ajouté à vos favoris" s'affiche sur l'écran CarPlay + Et le cœur devient rouge plein dans l'interface + Et la mise à jour est visible même sans toucher l'écran + + Scénario: Commandes vocales avec contenu sans créateur + Étant donné que j'écoute un contenu anonyme (créateur supprimé) + Quand je dis "Hey Siri, abonne-moi à ce créateur" + Alors Siri répond "Ce créateur n'est plus disponible" + Et aucun abonnement n'est enregistré + + Scénario: Limitation temporelle des commandes vocales + Étant donné que je dis "Hey Siri, like ce podcast" + Et que le contenu change 1 seconde après + Quand Siri traite la commande 2 secondes plus tard + Alors la commande s'applique au contenu qui était en lecture au moment de la commande + Et non au contenu actuel (système de timestamp) + + Plan du Scénario: Commandes vocales avec différents assistants + Étant donné que j'utilise + Quand je dis + Alors l'action est exécutée + Et la confirmation est + + Exemples: + | assistant | commande | action | confirmation | + | Siri | "Like ce podcast" | Like +2% | "Ajouté à vos favoris" | + | Google Assistant | "Like ce contenu" | Like +2% | "J'ai liké ce contenu" | + | Siri | "Abonne-moi à ce créateur" | Abonnement +5% | "Vous êtes abonné" | + | Google Assistant | "Abonne-moi à ce créateur" | Abonnement +5% | "Abonnement enregistré" | + | Siri | "Signale ce contenu" | Signalement | "J'ai signalé ce contenu" | + | Google Assistant | "Signale ce contenu" | Signalement | "Contenu signalé" | + + Plan du Scénario: Mapping catégories signalement vocal + Étant donné que je dis "signale ce contenu pour " + Quand est reconnu + Alors la catégorie mappée est + + Exemples: + | mot_cle | categorie | + | haine | Haine et violence | + | violence | Haine et violence | + | sexuel | Contenu sexuel | + | porno | Contenu sexuel | + | illégal | Illégalité | + | terrorisme | Illégalité | + | copyright | Droits d'auteur | + | droits auteur | Droits d'auteur | + | spam | Spam | + | fake news | Désinformation | + | fausse info | Désinformation | diff --git a/features/navigation/commandes-volant.feature b/features/navigation/commandes-volant.feature new file mode 100644 index 0000000..2a7f388 --- /dev/null +++ b/features/navigation/commandes-volant.feature @@ -0,0 +1,205 @@ +# language: fr +Fonctionnalité: Commandes au volant et interactions simplifiées + En tant que conducteur en sécurité + Je veux utiliser uniquement les commandes simplifiées au volant + Afin de naviguer sans distraction et en toute sécurité + + Contexte: + Étant donné que l'API RoadWave est disponible + Et qu'un utilisateur est connecté + Et que l'application est connectée via CarPlay ou Android Auto + + Scénario: Trois commandes disponibles au volant uniquement + Étant donné que je conduis à 50 km/h + Quand je consulte les commandes physiques disponibles + Alors seules 3 actions sont disponibles: + | Commande | Action | + | Suivant | Passer au contenu suivant | + | Précédent | Revenir au précédent (règle 10s) | + | Play/Pause | Pause/reprise avec fade 0.3s | + Et aucune commande complexe n'est proposée + + Scénario: Commande "Suivant" au volant + Étant donné que j'écoute un contenu "A" + Quand j'appuie sur le bouton physique "Suivant" au volant + Alors le contenu "B" démarre immédiatement + Et aucune action supplémentaire n'est requise + Et l'interface ne demande aucune confirmation + + Scénario: Commande "Précédent" au volant respecte règle 10s + Étant donné que j'écoute un contenu depuis 5 secondes + Quand j'appuie sur "Précédent" au volant + Alors je reviens au contenu précédent (règle <10s) + Étant donné que j'écoute un contenu depuis 15 secondes + Quand j'appuie sur "Précédent" au volant + Alors le contenu actuel rejoue depuis le début (règle ≥10s) + + Scénario: Commande "Play/Pause" avec fade audio + Étant donné qu'un contenu est en lecture + Quand j'appuie sur "Pause" au volant + Alors la lecture se met en pause avec un fade out de 0.3 secondes + Et la position de lecture est sauvegardée + Quand j'appuie sur "Play" au volant + Alors la lecture reprend avec un fade in de 0.3 secondes + Et la reprise se fait à la position exacte + + Scénario: Aucune commande complexe supportée + Étant donné que je conduis + Quand j'essaie un appui long sur "Suivant" + Alors l'action n'est pas détectée (non supporté iOS/Android) + Quand j'essaie un double-appui sur "Pause" + Alors l'action n'est pas détectée + Et seules les actions simples (clic simple) fonctionnent + + Scénario: Compatibilité 100% tous véhicules + Étant donné que je conduis une voiture avec commandes basiques + Et que mon véhicule a seulement Suivant/Précédent/Pause + Quand j'utilise RoadWave + Alors toutes les fonctions essentielles sont accessibles + Et je n'ai pas besoin de boutons supplémentaires + + Scénario: Feedback visuel discret après action + Étant donné que j'appuie sur "Suivant" + Quand le contenu change + Alors l'interface CarPlay/Android Auto affiche le nouveau titre + Et aucune popup ne bloque la vue + Et le changement est fluide et immédiat + + Scénario: Like automatique renforcé après écoute ≥80% + Étant donné que j'écoute un contenu de 5 minutes tagué "Automobile" + Quand j'écoute pendant 4 minutes 30 secondes (90%) + Alors un like automatique renforcé (+2 points) est enregistré + Et un badge discret "♥ Ajouté à vos favoris" s'affiche 2 secondes + Et aucune action manuelle n'est requise + + Scénario: Like automatique standard après écoute 30-79% + Étant donné que j'écoute un contenu de 5 minutes tagué "Voyage" + Quand j'écoute pendant 2 minutes (40%) + Et que j'appuie sur "Suivant" + Alors un like automatique standard (+1 point) est enregistré + Et un badge discret s'affiche brièvement + Et je peux continuer à conduire sans interruption + + Scénario: Signal négatif après skip rapide <10s + Étant donné que j'écoute un contenu tagué "Politique" + Quand j'appuie sur "Suivant" après seulement 5 secondes + Alors un signal négatif (-0.5 point) est enregistré + Et la jauge "Politique" diminue légèrement + Et aucun message n'est affiché (transparence) + + Scénario: Pas de like si écoute <30% + Étant donné que j'écoute un contenu de 10 minutes + Quand j'écoute pendant 2 minutes (20%) + Et que j'appuie sur "Suivant" + Alors aucun like n'est enregistré + Et les jauges ne changent pas + Et le système considère l'écoute comme neutre + + Scénario: Badge de feedback visuel disparaît après 2 secondes + Étant donné que je reçois un like automatique + Quand le badge "♥ Ajouté à vos favoris" apparaît + Alors il reste visible 2 secondes en bas de l'écran + Et il disparaît automatiquement sans action + Et il ne bloque pas la vue de la route + + Scénario: Tracking du temps d'écoute précis côté client + Étant donné que je démarre la lecture d'un contenu + Quand le player audio iOS/Android enregistre le temps + Alors le startTime est enregistré à la milliseconde + Quand j'arrête la lecture (Suivant, Pause, ou fin) + Alors la durée exacte écoutée est calculée + Et le pourcentage (durée / durée_totale * 100) est envoyé à l'API + + Scénario: API reçoit les événements d'écoute pour calcul + Étant donné que j'écoute un contenu de 5 minutes à 80% + Quand l'événement est envoyé à l'API + Alors le backend reçoit: + """ + { + "content_id": "abc123", + "duration": 240, + "content_total": 300, + "percentage": 80.0, + "action": "completed" + } + """ + Et le backend calcule le like automatique (+2 points) + Et les jauges sont mises à jour immédiatement (Redis + PostgreSQL) + + Scénario: Actions différentes selon arrêt du contenu + Étant donné que j'écoute un contenu + Quand j'appuie sur "Suivant" + Alors l'action envoyée est "skipped" + Quand le contenu se termine naturellement + Alors l'action envoyée est "completed" + Quand j'appuie sur "Pause" + Alors l'action envoyée est "paused" + Et le backend traite chaque action différemment + + Scénario: Calcul immédiat côté backend sans délai + Étant donné que l'API reçoit un événement d'écoute + Quand le backend traite l'événement + Alors les jauges sont mises à jour immédiatement (< 100ms) + Et les nouvelles recommandations utilisent les valeurs actualisées + Et il n'y a aucun batch différé + + Scénario: Compatibilité iOS avec AVPlayer + Étant donné que l'app iOS utilise AVPlayer + Quand les commandes physiques sont interceptées + Alors les événements MPRemoteCommandCenter sont capturés: + | Commande | Événement iOS | + | Suivant | nextTrackCommand | + | Précédent | previousTrackCommand | + | Play/Pause | playCommand / pauseCommand | + Et le tracking du temps utilise CMTime + + Scénario: Compatibilité Android avec MediaSession + Étant donné que l'app Android utilise MediaPlayer + Quand les commandes physiques sont interceptées + Alors les événements MediaSession sont capturés: + | Commande | Action Android | + | Suivant | ACTION_SKIP_TO_NEXT | + | Précédent | ACTION_SKIP_TO_PREVIOUS | + | Play/Pause | ACTION_PLAY / ACTION_PAUSE | + Et le tracking du temps utilise SystemClock.elapsedRealtime() + + Scénario: Sécurité maximale - pas de distraction + Étant donné que je conduis à 80 km/h + Quand j'utilise RoadWave avec les commandes au volant + Alors je n'ai jamais besoin de regarder mon téléphone + Et je n'ai jamais besoin de toucher l'écran CarPlay/Android Auto + Et toutes les actions sont accessibles via boutons physiques + Et les likes sont enregistrés automatiquement + + Plan du Scénario: Calcul du like automatique selon pourcentage + Étant donné que j'écoute un contenu tagué "Sport" + Quand j'écoute pendant % + Alors le like automatique est + Et l'impact sur la jauge est + + Exemples: + | pourcentage | type | points | + | 10 | aucun | 0 | + | 25 | aucun | 0 | + | 29 | aucun | 0 | + | 30 | standard | +1 | + | 50 | standard | +1 | + | 79 | standard | +1 | + | 80 | renforcé | +2 | + | 90 | renforcé | +2 | + | 100 | renforcé | +2 | + + Plan du Scénario: Signal négatif uniquement si skip très rapide + Étant donné que j'écoute un contenu + Quand je skip après secondes + Alors le signal est + Et l'impact est + + Exemples: + | secondes | type | points | + | 3 | négatif | -0.5 | + | 5 | négatif | -0.5 | + | 9 | négatif | -0.5 | + | 10 | neutre | 0 | + | 15 | neutre | 0 | + | 30 | neutre | 0 | diff --git a/features/navigation/file-attente-suivant.feature b/features/navigation/file-attente-suivant.feature new file mode 100644 index 0000000..c83f3ef --- /dev/null +++ b/features/navigation/file-attente-suivant.feature @@ -0,0 +1,188 @@ +# language: fr +Fonctionnalité: File d'attente et commande "Suivant" + En tant qu'auditeur en déplacement + Je veux que l'application pré-calcule intelligemment les prochains contenus + Afin d'avoir une navigation fluide sans latence + + Contexte: + Étant donné que l'API RoadWave est disponible + Et qu'un utilisateur est connecté + Et que la géolocalisation est activée + + Scénario: Pré-calcul initial de 5 contenus en cache + Étant donné que je viens de démarrer l'application + Et que je suis situé à Paris (48.8566, 2.3522) + Et que je suis en mode voiture (vitesse ≥ 5 km/h) + Quand l'application initialise la lecture + Alors une file d'attente de 5 contenus est pré-calculée + Et la file est stockée en cache Redis avec la clé "user:{user_id}:queue" + Et les métadonnées incluent ma position, le timestamp de calcul et le mode + Et le cache a un TTL de 15 minutes + + Scénario: Commande "Suivant" sans latence + Étant donné qu'une file d'attente de 5 contenus est en cache + Et que j'écoute actuellement le contenu "A" + Quand j'appuie sur le bouton "Suivant" + Alors le contenu suivant démarre immédiatement (< 100ms) + Et le contenu est retiré de la file d'attente + Et il reste 4 contenus dans la file + + Scénario: Recalcul automatique après déplacement >10km + Étant donné que la file a été calculée à Paris (48.8566, 2.3522) + Et que j'ai 5 contenus en cache + Quand je me déplace à Versailles (48.8049, 2.1204) soit 12km + Alors la file d'attente est invalidée automatiquement + Et une nouvelle file de 5 contenus est recalculée + Et elle est basée sur ma nouvelle position + + Scénario: Recalcul automatique toutes les 10 minutes + Étant donné qu'une file a été calculée il y a 10 minutes + Et que ma position n'a pas changé + Quand le timer de rafraîchissement expire + Alors une nouvelle file de 5 contenus est recalculée + Et les anciens contenus non écoutés sont remplacés + Et les nouveaux contenus publiés depuis sont inclus + + Scénario: Recalcul quand il reste moins de 3 contenus + Étant donné qu'il reste 3 contenus dans ma file d'attente + Quand j'appuie sur "Suivant" + Alors il reste 2 contenus + Et un recalcul asynchrone est déclenché en arrière-plan + Et 3 nouveaux contenus sont ajoutés à la file + Et la file contient maintenant 5 contenus + + Scénario: Insertion prioritaire d'un contenu géolocalisé en mode voiture + Étant donné que j'ai une file de 5 contenus pré-calculée + Et que je suis en mode voiture + Et que je me déplace à 50 km/h vers un point avec contenu géolocalisé + Quand je suis à 98m du point (ETA = 7 secondes) + Alors une notification est envoyée (icône + compteur 7→1 + son) + Et je dois appuyer sur "Suivant" dans les 7 secondes pour valider + Quand j'appuie sur "Suivant" + Alors un décompte de 5 secondes démarre + Et après 5 secondes, le contenu géolocalisé s'insère et démarre + Et il remplace le contenu actuel dans la lecture + + Scénario: Contenu géolocalisé ignoré est perdu (cooldown activé) + Étant donné qu'une notification géolocalisée est affichée (compteur 7→1) + Quand je ne clique pas sur "Suivant" pendant les 7 secondes + Alors la notification disparaît + Et le contenu géolocalisé est perdu (pas d'insertion dans la file) + Et un cooldown de 10 minutes est activé + Et aucune nouvelle notification géolocalisée ne sera envoyée pendant 10 minutes + + Scénario: Validation d'une notification géolocalisée + Étant donné qu'une notification géolocalisée est affichée (compteur à 5) + Et que j'écoute un podcast + Quand j'appuie sur "Suivant" + Alors le compteur bascule à "5" (décompte final) + Et le podcast actuel continue de jouer + Et après 5 secondes, le contenu géolocalisé démarre + Et le podcast est mis en pause et sauvegardé dans l'historique + + Scénario: Invalidation immédiate après modification des préférences + Étant donné que j'ai une file de 5 contenus en cache + Et que ma vitesse GPS est de 5 km/h (piéton) + Quand je modifie mes curseurs de préférences (géo/découverte/politique) + Alors la file d'attente est invalidée immédiatement + Et une nouvelle file est recalculée avec les nouvelles préférences + Et les anciens contenus en cache sont supprimés + + Scénario: Blocage modification préférences en conduite (>10 km/h) + Étant donné que ma vitesse GPS est de 50 km/h (en voiture) + Quand j'essaie d'accéder aux réglages de préférences + Alors l'interface affiche "Paramètres verrouillés en conduite" + Et je ne peux pas modifier les curseurs géo/découverte/politique + Et un message "Arrêtez-vous pour modifier vos préférences" s'affiche + + Scénario: Invalidation lors du démarrage d'un live suivi + Étant donné que je suis abonné au créateur "RadioVoyage" + Et que j'ai une file de 5 contenus en cache + Et que je suis dans la zone géographique du créateur + Quand le créateur "RadioVoyage" démarre une radio live + Alors je reçois une notification push + Et le contenu live s'insère en tête de la file d'attente + Et la file d'attente est recalculée + + Scénario: Métadonnées de cache Redis + Étant donné qu'une file d'attente est calculée + Quand elle est stockée dans Redis + Alors la clé est "user:{user_id}:queue" + Et les métadonnées incluent: + | champ | valeur | + | last_lat | 48.8566 | + | last_lon | 2.3522 | + | computed_at | 2026-01-21T10:30:00Z | + | mode | voiture | + Et le TTL est de 15 minutes (900 secondes) + + Scénario: Contenu géolocalisé remplace le contenu actuel (pas d'insertion en file) + Étant donné que j'écoute le contenu C2 de ma file [C1, C2, C3, C4, C5] + Et qu'une notification géolocalisée "Tour Eiffel" est déclenchée + Quand je valide la notification + Et que le décompte de 5s se termine + Alors le contenu "Tour Eiffel" remplace C2 et démarre + Et C2 est sauvegardé dans l'historique de navigation + Et la file reste [C3, C4, C5] (pas de contenu retiré) + Et quand "Tour Eiffel" se termine, C3 démarre + + Scénario: Invalidation après déplacement exactement 10km + Étant donné que la file a été calculée à une position donnée + Quand je me déplace d'exactement 10.0 km + Alors la file d'attente n'est PAS invalidée (seuil strict >10km) + Et les contenus en cache restent valides + Quand je me déplace de 10.1 km supplémentaires (total 10.1km) + Alors la file d'attente est invalidée + Et une nouvelle file est calculée + + Scénario: Rafraîchissement exactement après 10 minutes + Étant donné qu'une file a été calculée à 10:00:00 + Quand l'heure actuelle est 10:10:00 + Alors le timer de rafraîchissement expire + Et une nouvelle file de 5 contenus est recalculée + Et le timestamp "computed_at" est mis à jour + + Scénario: Recalcul asynchrone non-bloquant + Étant donné qu'il reste 2 contenus dans la file + Et que j'appuie sur "Suivant" + Quand le recalcul asynchrone démarre + Alors la lecture du contenu actuel n'est pas interrompue + Et le recalcul se fait en arrière-plan + Et les nouveaux contenus sont ajoutés dès disponibles (< 500ms) + Et l'utilisateur ne perçoit aucune latence + + Scénario: Notification basée sur ETA (pas distance fixe) + Étant donné qu'un contenu géolocalisé existe à un point GPS + Et que je roule à 130 km/h + Quand je suis à 252m du point (ETA = 7 secondes) + Alors une notification est envoyée + Quand je suis à 300m du point (ETA = 8 secondes) + Alors aucune notification n'est envoyée (ETA >7s) + + Plan du Scénario: Différentes distances de déplacement et invalidation + Étant donné qu'une file a été calculée à une position donnée + Quand je me déplace de km + Alors la file est + + Exemples: + | distance | action | + | 5 | conservée | + | 9.9 | conservée | + | 10.0 | conservée | + | 10.1 | invalidée et recalculée | + | 15 | invalidée et recalculée | + | 50 | invalidée et recalculée | + + Scénario: Quota de 6 contenus géolocalisés par heure + Étant donné que j'ai validé 6 notifications géolocalisées dans la dernière heure + Quand un 7ème contenu géolocalisé est détecté (ETA 7s) + Alors aucune notification n'est envoyée + Et le quota horaire est respecté + + Scénario: Mode piéton - pas de notification avec compteur 7s + Étant donné que je suis en mode piéton (vitesse <5 km/h) + Et qu'un audio-guide géolocalisé existe à 150m + Quand je passe dans le rayon de 200m + Alors une notification push système est envoyée + Et aucun compteur 7s n'est affiché + Et je peux ouvrir l'app en tapant sur la notification diff --git a/features/navigation/lecture-enchainement.feature b/features/navigation/lecture-enchainement.feature new file mode 100644 index 0000000..8e38e6a --- /dev/null +++ b/features/navigation/lecture-enchainement.feature @@ -0,0 +1,255 @@ +# language: fr +Fonctionnalité: Lecture en boucle et enchaînement automatique + En tant qu'auditeur + Je veux que les contenus s'enchaînent automatiquement avec un délai paramétrable + Afin d'avoir une expérience fluide sans interruption + + Contexte: + Étant donné que l'API RoadWave est disponible + Et qu'un utilisateur est connecté + + Scénario: Passage automatique après 2 secondes (mode standard) + Étant donné que j'écoute un contenu "A" en mode standard + Quand la lecture se termine naturellement + Alors un timer de 2 secondes démarre + Et un overlay s'affiche: "Contenu suivant dans 2s..." + Et une barre de décompte visuelle s'affiche + Quand le timer atteint 0 + Alors le contenu "B" démarre automatiquement + Et l'overlay disparaît + + Scénario: Passage automatique après 1 seconde (mode Kids) + Étant donné que je suis en mode Kids + Et que j'écoute un contenu pour enfants + Quand la lecture se termine + Alors un timer de 1 seconde démarre + Et le message "Contenu suivant dans 1s..." s'affiche + Quand le timer expire + Alors le contenu suivant démarre automatiquement + Car l'attention des enfants est plus courte + + Scénario: Passage immédiat après une radio live (0 seconde) + Étant donné que j'écoute une radio live + Quand le créateur arrête la diffusion + Alors le passage au contenu suivant est immédiat (0s de délai) + Et aucun overlay de décompte n'est affiché + Et la transition est fluide + + Scénario: Annulation du passage automatique + Étant donné qu'un contenu se termine + Et que le timer de 2 secondes démarre + Quand je clique sur "Rester sur ce contenu" pendant le décompte + Alors le timer est annulé + Et le contenu actuel reste en pause à la fin + Et le contenu suivant n'est pas lancé + + Scénario: Insertion de publicité pendant le délai de transition + Étant donné que j'ai écouté 4 contenus sans publicité + Et que le 5ème contenu se termine + Quand le délai de 2 secondes démarre + Alors une publicité s'insère dans la file d'attente + Et le message devient "Publicité (15s)" + Et la publicité démarre après les 2 secondes + Et elle ne coupe jamais un contenu en cours + + Scénario: Fréquence de publicité paramétrable admin + Étant donné que la fréquence pub est configurée à "1/5 contenus" + Quand j'écoute 10 contenus + Alors 2 publicités sont insérées (après les contenus 5 et 10) + Étant donné que l'admin change la fréquence à "1/3 contenus" + Quand j'écoute 9 contenus + Alors 3 publicités sont insérées (après les contenus 3, 6 et 9) + + Scénario: Publicité skippable après 5 secondes par défaut + Étant donné qu'une publicité de 30 secondes démarre + Et que le délai minimal de visionnage est configuré à 5 secondes + Quand j'écoute pendant 3 secondes + Alors le bouton "Passer" n'est pas encore visible + Quand j'atteins 5 secondes d'écoute + Alors le bouton "Passer" apparaît + Et je peux cliquer pour passer au contenu suivant + + Scénario: Délai minimal de publicité paramétrable admin + Étant donné qu'une publicité démarre + Et que l'admin a configuré le délai à 10 secondes + Quand j'écoute pendant 9 secondes + Alors le bouton "Passer" n'est pas visible + Quand j'atteins 10 secondes + Alors le bouton "Passer" apparaît + Et je peux skipper la publicité + + Scénario: Like et abonnement autorisés sur une publicité + Étant donné qu'une publicité est en lecture + Quand je clique sur le bouton cœur (véhicule arrêté) + Alors la publicité reçoit un like (+2% jauges tags pub) + Quand je clique sur "S'abonner" au créateur de la pub + Alors je suis abonné (+5% jauges tags créateur) + Et le créateur de pub bénéficie de l'engagement + + Scénario: Métriques d'engagement publicité trackées + Étant donné qu'une publicité de 30s est diffusée à 100 auditeurs + Quand 40 auditeurs écoutent entièrement (30s) + Et que 50 auditeurs skippent après 10s + Et que 10 auditeurs skippent avant 5s + Alors les métriques sont: + | Métrique | Valeur | + | Taux d'écoute complète | 40% | + | Taux de skip après seuil | 50% | + | Taux de skip immédiat | 10% | + | Durée moyenne d'écoute | 18s | + + Scénario: Message "Aucun contenu disponible" si file vide + Étant donné que la file d'attente est vide + Et qu'aucun contenu n'est disponible dans ma zone + Quand le contenu actuel se termine + Alors un message s'affiche: "Aucun contenu disponible dans cette zone" + Et une proposition apparaît: "Élargir la zone de recherche ?" + Et un bouton "Élargir" est disponible + Et la lecture se met en pause automatiquement + + Scénario: Élargissement automatique de la zone de recherche + Étant donné que le message "Aucun contenu disponible" s'affiche + Quand je clique sur "Élargir la zone" + Alors l'algorithme relance une recherche avec rayon +50km + Et une notification "Recherche élargie à 50km" s'affiche + Et la file d'attente est recalculée + Et la lecture reprend automatiquement + + Scénario: Refus d'élargissement laisse en pause + Étant donné que le message "Aucun contenu disponible" s'affiche + Quand je clique sur "Annuler" + Alors la lecture reste en pause + Et l'interface affiche "En attente de contenu" + Et je peux manuellement naviguer ou chercher du contenu + + Scénario: Retry avec backoff exponentiel en cas d'échec réseau + Étant donné que le contenu suivant échoue au chargement + Quand la première tentative échoue + Alors le système retente après 1 seconde (backoff 1s) + Quand la 2ème tentative échoue + Alors le système retente après 2 secondes (backoff 2s) + Quand la 3ème tentative échoue + Alors le système retente après 4 secondes (backoff 4s) + Et après 3 échecs totaux, le système bascule en mode offline + + Scénario: Basculement mode offline après 3 échecs réseau + Étant donné que j'ai eu 3 échecs de chargement consécutifs + Quand le 3ème échec se produit + Alors un message "Connexion instable, basculement mode offline" s'affiche + Et la lecture continue avec les contenus téléchargés uniquement + Et les contenus en ligne sont temporairement désactivés + Quand la connexion revient + Alors le mode en ligne est automatiquement rétabli + + Scénario: Overlay de décompte avec barre visuelle + Étant donné qu'un contenu se termine + Quand le timer de 2 secondes démarre + Alors un overlay semi-transparent s'affiche en bas de l'écran + Et le texte "Contenu suivant dans 2s..." est visible + Et une barre de progression décroît de 100% à 0% en 2 secondes + Et la couleur de la barre passe de vert à orange + Et l'overlay disparaît automatiquement après le décompte + + Scénario: Bouton "Rester sur ce contenu" pendant décompte + Étant donné que le décompte de 2 secondes est actif + Quand l'overlay s'affiche + Alors un bouton "Rester sur ce contenu" est visible + Et il est cliquable pendant les 2 secondes + Quand je clique dessus + Alors le timer est annulé immédiatement + Et l'overlay disparaît + Et le contenu actuel reste affiché en pause + + Scénario: Pas d'interruption d'un contenu en cours + Étant donné que j'écoute un contenu de 10 minutes + Et que je suis à 5 minutes de lecture + Quand une publicité devrait s'insérer (fréquence 1/5) + Alors la publicité n'interrompt jamais le contenu en cours + Et elle attend la fin du contenu actuel + Et elle s'insère pendant le délai de transition (2s) + + Scénario: Publicités uniquement pour utilisateurs gratuits + Étant donné que je suis un utilisateur gratuit + Quand j'écoute 5 contenus + Alors une publicité est insérée après le 5ème contenu + Étant donné que je passe en compte Premium + Quand j'écoute 100 contenus + Alors aucune publicité n'est insérée + Et l'enchaînement est direct (2s de transition seulement) + + Scénario: Message clair pour l'utilisateur lors de la publicité + Étant donné qu'une publicité va démarrer + Quand le délai de transition démarre + Alors le message affiché est: "Publicité (15s)" + Et la durée totale de la pub est indiquée + Et l'utilisateur sait qu'il s'agit d'une pub + Et la transparence est maximale + + Scénario: Transition fluide entre contenus sans coupure + Étant donné qu'un contenu se termine + Et que le suivant est pré-chargé en cache + Quand le timer de 2s expire + Alors la transition audio utilise un crossfade de 0.3s + Et il n'y a aucun blanc ou coupure + Et l'expérience est fluide + + Scénario: Gestion des erreurs de chargement avec retry + Étant donné que le contenu suivant échoue au chargement + Quand la 1ère tentative échoue + Alors une notification "Chargement..." s'affiche + Et le système retente automatiquement + Quand la 2ème tentative réussit + Alors la lecture démarre normalement + Et aucune action utilisateur n'est requise + + Scénario: Mode offline après échecs multiples + Étant donné que j'ai 50 contenus téléchargés en mode offline + Et que j'ai eu 3 échecs réseau consécutifs + Quand le mode offline s'active + Alors seuls les contenus téléchargés sont disponibles + Et un badge "Mode offline" s'affiche en haut de l'écran + Et la lecture continue sans interruption + + Scénario: Compteur de contenus avant prochaine publicité + Étant donné que la fréquence pub est 1/5 contenus + Et que j'ai écouté 3 contenus depuis la dernière pub + Quand je consulte l'interface + Alors un indicateur discret affiche "2 contenus avant pub" + Et l'utilisateur sait quand attendre la prochaine publicité + + Plan du Scénario: Délai de transition selon mode + Étant donné que je suis en mode + Quand un contenu se termine + Alors le délai de transition est secondes + Et le message affiché est + + Exemples: + | mode | delai | message | + | Standard | 2 | "Contenu suivant dans 2s..." | + | Kids | 1 | "Contenu suivant dans 1s..." | + | Live | 0 | (aucun message) | + + Plan du Scénario: Fréquence d'insertion des publicités + Étant donné que la fréquence pub est configurée à + Quand j'écoute contenus + Alors publicités sont insérées + + Exemples: + | frequence | contenus | pubs | + | 1/3 | 6 | 2 | + | 1/3 | 9 | 3 | + | 1/5 | 10 | 2 | + | 1/5 | 15 | 3 | + | 1/7 | 14 | 2 | + | 1/7 | 21 | 3 | + + Plan du Scénario: Backoff exponentiel retry + Étant donné que le chargement échoue + Quand je suis à la tentative + Alors le délai de retry est secondes + + Exemples: + | tentative | delai | + | 1 | 1 | + | 2 | 2 | + | 3 | 4 | diff --git a/features/partage/partage-contenu.feature b/features/partage/partage-contenu.feature new file mode 100644 index 0000000..de9938e --- /dev/null +++ b/features/partage/partage-contenu.feature @@ -0,0 +1,206 @@ +# language: fr + +Fonctionnalité: Partage de contenu + En tant qu'utilisateur de RoadWave + Je veux pouvoir partager du contenu audio + Afin de faire découvrir l'application à d'autres personnes + + Contexte: + Étant donné que l'application RoadWave est démarrée + Et que l'utilisateur "jean@example.com" est connecté + + # 15.1.1 - Bouton "Partager" + + Scénario: Bouton partager disponible dans le player en lecture + Étant donné que le contenu "Balade à Paris" est en cours de lecture + Quand l'utilisateur consulte les contrôles du player + Alors le bouton "Partager" ⬆️ est visible + + Scénario: Bouton partager disponible sur la page profil créateur + Étant donné que l'utilisateur consulte le profil de "@paris_stories" + Quand l'utilisateur consulte un contenu dans la liste + Alors le bouton "Partager" est disponible pour chaque contenu + + Scénario: Bouton partager dans la liste de recherche + Étant donné que l'utilisateur effectue une recherche "voyage paris" + Quand l'utilisateur ouvre le menu contextuel d'un résultat + Alors l'option "Partager" est disponible + + Scénario: Bouton partager dans l'historique personnel + Étant donné que l'utilisateur consulte son historique d'écoute + Quand l'utilisateur sélectionne un contenu de l'historique + Alors le bouton "Partager" est accessible + + Plan du Scénario: Menu de partage avec options multiples + Étant donné que le contenu "" est disponible + Quand l'utilisateur clique sur le bouton "Partager" + Alors le menu natif OS s'ouvre + Et les options suivantes sont disponibles: + | option | + | Copier le lien | + | WhatsApp | + | Email | + | SMS | + | Plus... | + + Exemples: + | contenu | + | Balade à Paris | + | Secrets de Montmartre | + + # 15.1.2 - Comportement du lien partagé + + Scénario: Génération du lien de partage + Étant donné un contenu avec l'ID "content_12345" + Quand l'utilisateur copie le lien de partage + Alors le lien généré est "https://roadwave.fr/share/c/content_12345" + + Scénario: Ouverture du lien partagé avec l'application installée (Deep link) + Étant donné que l'application RoadWave est installée sur l'appareil + Et qu'un lien "https://roadwave.fr/share/c/content_12345" est partagé + Quand l'utilisateur clique sur le lien + Alors l'application RoadWave s'ouvre automatiquement + Et le contenu "content_12345" commence à jouer + + Scénario: Ouverture du lien partagé sans l'application installée (Web player) + Étant donné que l'application RoadWave n'est pas installée + Et qu'un lien "https://roadwave.fr/share/c/content_12345" est partagé + Quand l'utilisateur clique sur le lien + Alors une page web responsive s'affiche + Et le web player HTML5 est visible + Et les boutons de téléchargement App Store et Google Play sont affichés + + Scénario: Contenu de la page web de partage + Étant donné un contenu public avec les métadonnées suivantes: + | champ | valeur | + | titre | Balade à Paris | + | créateur | @paris_stories | + | durée | 12 min | + | écoutes | 2300 | + | localisation | Paris 5e | + | type_geo | Ancré | + | tags | Voyage, Histoire | + Quand la page de partage est affichée + Alors la page contient: + | élément | + | Cover image 16:9 | + | Titre "Balade à Paris" | + | "@paris_stories" | + | "12 min · 🎧 2.3K" | + | "📍 Paris 5e · Ancré" | + | "🏷️ #Voyage #Histoire" | + | Description | + | Player HTML5 | + | Bouton App Store | + | Bouton Google Play | + + Scénario: Métadonnées Open Graph pour partage social + Étant donné un contenu "Balade à Paris" par "@paris_stories" + Quand la page de partage est générée + Alors les métadonnées Open Graph incluent: + | propriété | valeur | + | og:title | Balade à Paris - RoadWave | + | og:description | Écoutez ce contenu par @paris_stories | + | og:type | music.song | + | og:site_name | RoadWave | + | twitter:card | player | + Et l'aperçu s'affiche correctement sur WhatsApp + Et l'aperçu s'affiche correctement sur Facebook + Et l'aperçu s'affiche correctement sur Twitter + + Plan du Scénario: Deep linking par plateforme + Étant donné que l'application RoadWave est installée sur + Et qu'un lien de partage est ouvert + Quand le système détecte l'application + Alors l'application s'ouvre via + + Exemples: + | plateforme | mécanisme | + | iOS | Universal Links | + | Android | App Links | + + Scénario: Fallback URL scheme pour deep linking + Étant donné que les App Links ne fonctionnent pas + Quand le système tente d'ouvrir le contenu + Alors l'URL scheme "roadwave://content/content_12345" est utilisé + + # 15.1.3 - Contenus Premium partagés + + Scénario: Badge Premium visible sur le lien partagé + Étant donné un contenu Premium "Visite VIP Louvre" + Quand l'utilisateur non-premium clique sur le lien partagé + Alors la page web affiche le badge "👑 Contenu Premium" + + Scénario: Preview 30 secondes d'un contenu Premium partagé + Étant donné un contenu Premium "Visite VIP Louvre" de 15 minutes + Et qu'un utilisateur non-premium ouvre le lien partagé + Quand le player démarre automatiquement + Alors l'audio joue pendant 30 secondes exactement + Et un fade out de 2 secondes est appliqué + Et un overlay "Contenu réservé Premium" s'affiche après 32 secondes + + Scénario: Contenu de l'overlay paywall Premium + Étant donné qu'un contenu Premium a atteint la limite de 30 secondes + Quand l'overlay paywall s'affiche + Alors le texte suivant est visible: + """ + 👑 Contenu réservé Premium + + Profitez de ce contenu complet + et de milliers d'autres + sans publicité + + [Passer Premium - 4.99€/mois] + [Télécharger l'app] + """ + + Scénario: Actions disponibles sur l'overlay Premium + Étant donné que l'overlay paywall Premium est affiché + Quand l'utilisateur consulte les options + Alors les actions suivantes sont disponibles: + | action | comportement | + | Passer Premium | Redirection vers paiement Mangopay web | + | Télécharger l'app | Redirection vers App Store/Google Play | + | Rejouer les 30 premières sec | Relecture illimitée du preview | + + Scénario: Relecture illimitée du preview Premium + Étant donné un contenu Premium partagé + Et que l'utilisateur a écouté les 30 premières secondes + Quand l'utilisateur clique sur "Rejouer" + Alors les 30 premières secondes sont rejouées + Et cette action est possible de manière illimitée + + Scénario: Tracking des partages Premium + Étant donné un créateur "@guide_louvre" avec un contenu Premium + Quand son contenu est partagé + Alors les métriques suivantes sont enregistrées: + | métrique | valeur | + | Partages Premium | +1 | + | Ouvertures lien | compteur | + | Conversions Premium | si souscription | + + Scénario: Rémunération créateur sur conversion Premium via partage + Étant donné un contenu Premium partagé par "@guide_louvre" + Quand un utilisateur s'abonne via le lien partagé + Alors le créateur reçoit 70% des revenus de cet abonnement + Et la conversion est trackée dans son dashboard + + # Cas d'erreur + + Scénario: Partage d'un contenu supprimé + Étant donné qu'un lien de partage "https://roadwave.fr/share/c/deleted_content" est ouvert + Et que le contenu n'existe plus + Quand la page web se charge + Alors un message "Ce contenu n'est plus disponible" s'affiche + Et les boutons de téléchargement de l'app sont affichés + + Scénario: Partage d'un contenu en attente de modération + Étant donné un contenu en cours de validation modération + Quand un lien de partage est ouvert + Alors le message "Ce contenu est en cours de validation" s'affiche + + Scénario: Génération du lien hors connexion + Étant donné que l'utilisateur n'a pas de connexion réseau + Quand l'utilisateur tente de partager un contenu + Alors le lien est copié dans le presse-papiers + Et un message "Lien copié (nécessite connexion pour ouvrir)" s'affiche diff --git a/features/premium/avantages-premium.feature b/features/premium/avantages-premium.feature new file mode 100644 index 0000000..bc89471 --- /dev/null +++ b/features/premium/avantages-premium.feature @@ -0,0 +1,362 @@ +# language: fr +Fonctionnalité: Avantages Premium + En tant qu'abonné Premium + Je veux bénéficier d'avantages exclusifs + Afin de profiter d'une expérience audio améliorée sans publicité + + Contexte: + Étant donné que je suis connecté à l'application RoadWave + + # ===== PUBLICITÉS ===== + + Scénario: Utilisateur gratuit voit 1 publicité tous les 5 contenus + Étant donné que je suis un utilisateur gratuit + Quand j'écoute ma file de contenus + Alors je vois une publicité tous les 5 contenus + Et la publicité dure 30 secondes en moyenne + Et je ne peux pas la skip + + Scénario: Utilisateur Premium ne voit aucune publicité + Étant donné que je suis un utilisateur Premium + Quand j'écoute mes contenus + Alors aucune publicité n'est diffusée + Et je passe directement d'un contenu à l'autre + Et l'expérience d'écoute est fluide et ininterrompue + + Scénario: Badge "0 publicité" sur page Premium + Étant donné que je consulte la page des avantages Premium + Quand je lis la liste des avantages + Alors je vois en premier: + """ + 🚫 0 publicité + Profitez d'une écoute sans interruption + """ + Et c'est l'argument principal mis en avant + + # ===== CONTENUS EXCLUSIFS ===== + + Scénario: Utilisateur gratuit voit contenus Premium bloqués + Étant donné que je suis un utilisateur gratuit + Quand je consulte les contenus d'un créateur + Alors je vois les contenus marqués Premium avec badge 👑 + Mais je ne peux pas les lire (overlay bloquant) + + Scénario: Utilisateur Premium accède à tous les contenus exclusifs + Étant donné que je suis un utilisateur Premium + Quand je consulte les contenus d'un créateur + Alors tous les contenus Premium sont accessibles + Et je peux les lire sans restriction + Et j'ai accès à 100% du catalogue (gratuit + Premium) + + Scénario: Nombre de contenus Premium disponibles + Étant donné que je suis Premium + Quand je consulte les statistiques + Alors je vois combien de contenus Premium sont disponibles sur la plateforme + Et par exemple: "8,547 contenus Premium exclusifs disponibles" + Et cela justifie la valeur de l'abonnement + + # ===== QUALITÉ AUDIO ===== + + Scénario: Utilisateur gratuit écoute en 48 kbps Opus + Étant donné que je suis un utilisateur gratuit + Quand je lance un contenu + Alors l'audio est streamé en 48 kbps Opus + Et cela consomme environ 20 MB/heure + Et la qualité est très correcte pour de la voix + + Scénario: Utilisateur Premium écoute en 64 kbps Opus + Étant donné que je suis un utilisateur Premium + Quand je lance un contenu + Alors l'audio est streamé en 64 kbps Opus + Et cela consomme environ 30 MB/heure + Et la qualité est excellente (détails audio supérieurs) + + Scénario: Comparaison qualité 48 kbps vs 64 kbps + Étant donné que je consulte la page Premium + Quand je lis la section qualité audio + Alors je vois l'explication: + """ + 📻 Qualité audio supérieure + + Gratuit: 48 kbps Opus (~20 MB/h) + Premium: 64 kbps Opus (~30 MB/h) + + Profitez d'une qualité audio exceptionnelle avec plus de détails + et une meilleure restitution des voix et ambiances. + """ + + Scénario: Justification 48 kbps suffisant pour gratuit + Étant donné que le contenu RoadWave est principalement de la voix + Quand la qualité est fixée à 48 kbps pour gratuit + Alors c'est largement suffisant pour comprendre clairement + Et équivalent à la qualité radio FM + Et les utilisateurs gratuits ne sont pas frustrés + + Scénario: Justification 64 kbps avantage tangible Premium + Étant donné que les audiophiles et créateurs audio sont exigeants + Quand la qualité Premium est à 64 kbps + Alors la différence est perceptible à l'oreille + Et les ambiances, musiques de fond, nuances de voix sont mieux rendues + Et cela justifie l'abonnement Premium + + Scénario: Switch automatique qualité selon abonnement + Étant donné que je suis gratuit et j'écoute en 48 kbps + Quand je souscris à Premium + Alors dès le contenu suivant, je passe automatiquement en 64 kbps + Et je peux entendre la différence de qualité immédiatement + + Scénario: Consommation data Premium vs Gratuit + Étant donné que je roule 1 heure par jour + Quand je calcule la consommation mensuelle + Alors en gratuit: 20 MB/h × 1h × 22 jours = 440 MB/mois + Et en Premium: 30 MB/h × 1h × 22 jours = 660 MB/mois + Et la différence est de 220 MB/mois (acceptable pour 4G/5G illimitée) + + # ===== MODE OFFLINE ===== + + Scénario: Utilisateur gratuit limité à 50 contenus téléchargés + Étant donné que je suis un utilisateur gratuit + Quand j'accède au mode offline + Alors je peux télécharger jusqu'à 50 contenus maximum + Et si j'essaie de télécharger un 51ème, je vois: + """ + Limite atteinte (50 contenus max en gratuit). + Passez Premium pour des téléchargements illimités. + """ + + Scénario: Utilisateur Premium téléchargements illimités + Étant donné que je suis un utilisateur Premium + Quand j'accède au mode offline + Alors je peux télécharger autant de contenus que je veux + Et la seule limite est l'espace de stockage de mon device + Et par exemple 500 contenus × 10 MB = 5 GB + + Scénario: Justification limite 50 contenus gratuit + Étant donné que 50 contenus de 10 minutes = ~8 heures d'écoute + Quand un utilisateur gratuit prépare un road trip + Alors 8 heures couvrent largement une journée de trajet + Et cela permet un usage offline raisonnable sans abuser + + Scénario: Justification illimité Premium pour longs road trips + Étant donné qu'un road trip de plusieurs jours nécessite 20-50h de contenu + Quand un utilisateur Premium télécharge 200 contenus + Alors il peut partir serein sans connexion internet pendant 1 semaine + Et cela justifie pleinement l'abonnement Premium + + Scénario: Affichage compteur téléchargements gratuit + Étant donné que je suis gratuit et j'ai téléchargé 37 contenus + Quand j'accède à la page Téléchargements + Alors je vois: + """ + 📥 Téléchargements offline + + 37 / 50 contenus téléchargés + + Passez Premium pour des téléchargements illimités + [Découvrir Premium] + ``` + + Scénario: Pas de compteur pour Premium + Étant donné que je suis Premium et j'ai téléchargé 187 contenus + Quand j'accède à la page Téléchargements + Alors je vois simplement: + """ + 📥 Téléchargements offline + + 187 contenus téléchargés (illimité) + Espace utilisé: 1.8 GB + ``` + Et aucune limite n'est affichée + + # ===== HISTORIQUE ÉCOUTE ===== + + Scénario: Utilisateur gratuit historique limité à 100 derniers + Étant donné que je suis un utilisateur gratuit + Quand j'accède à mon historique d'écoute + Alors je vois les 100 derniers contenus écoutés + Et les contenus plus anciens ne sont pas affichés + Et je vois un message "Historique limité à 100 contenus. Passez Premium pour un historique illimité." + + Scénario: Utilisateur Premium historique illimité + Étant donné que je suis un utilisateur Premium + Quand j'accède à mon historique d'écoute + Alors je vois tous les contenus que j'ai écoutés depuis mon inscription + Et je peux scroller jusqu'au premier contenu jamais écouté + Et l'historique est complet et permanent + + Scénario: Recherche dans historique Premium + Étant donné que je suis Premium et j'ai 2 000 contenus dans mon historique + Quand je recherche "Tesla" dans mon historique + Alors tous les contenus écoutés contenant "Tesla" sont affichés + Et je peux retrouver facilement un contenu écouté il y a 6 mois + + Scénario: Justification limite 100 gratuit suffisante + Étant donné que 100 contenus de 10 min = ~16 heures d'écoute + Quand un utilisateur gratuit écoute 1h/jour + Alors l'historique couvre les 16 derniers jours + Et cela suffit pour retrouver un contenu récent + + Scénario: Justification illimité Premium pour power users + Étant donné qu'un power user écoute 3h/jour depuis 2 ans + Quand il veut retrouver un contenu spécifique écouté il y a 1 an + Alors l'historique illimité Premium lui permet de retrouver ce contenu + Et cela apporte une vraie valeur ajoutée + + Scénario: Export historique complet (Premium uniquement) + Étant donné que je suis Premium + Quand je demande l'export de mes données + Alors l'historique complet est inclus dans l'export: + ```json + { + "listen_history": [ + { + "content_title": "Mon épisode préféré", + "creator_name": "JeanDupont", + "listened_at": "2025-06-15T14:30:00Z", + "completion_rate": 0.95 + }, + ... + ], + "total_listens": 2847 + } + ``` + + # ===== TABLEAU COMPARATIF ===== + + Scénario: Affichage tableau comparatif Gratuit vs Premium + Étant donné que je consulte la page Premium + Quand je vois le tableau comparatif + Alors il affiche: + ``` + ┌─────────────────────────┬──────────────┬──────────────┐ + │ Avantage │ Gratuit │ Premium │ + ├─────────────────────────┼──────────────┼──────────────┤ + │ Publicités │ 1/5 contenus │ 0 (aucune) │ + │ Contenus exclusifs │ ❌ Bloqués │ ✅ Accès │ + │ Qualité audio │ 48 kbps Opus │ 64 kbps Opus │ + │ Mode offline │ 50 max │ Illimité │ + │ Historique écoute │ 100 derniers │ Illimité │ + │ Prix │ Gratuit │ 4.99€/mois │ + └─────────────────────────┴──────────────┴──────────────┘ + ``` + + # ===== JUSTIFICATIONS GÉNÉRALES ===== + + Scénario: Justification 0 pub = argument principal + Étant donné qu'une publicité de 30s tous les 5 contenus = 6 min/h de pub + Quand un utilisateur écoute 1h/jour + Alors il subit 180 min de pub/mois (3 heures !) + Et payer 4.99€ pour éviter 3h de pub/mois est très rentable + Et c'est l'argument de conversion n°1 + + Scénario: Justification qualité audio avantage tangible + Étant donné que la différence 48 kbps → 64 kbps est audible + Quand un audiophile compare les deux + Alors il entend clairement la différence sur un bon système audio voiture + Et cela justifie l'abonnement pour les exigeants + + Scénario: Justification offline illimité pour road trips + Étant donné qu'un road trip de 2 semaines nécessite 50-100h de contenu + Quand un utilisateur Premium télécharge 300 contenus avant de partir + Alors il peut partir en zone sans réseau sereinement + Et cela apporte une vraie valeur pratique + + Scénario: Justification pas d'over-engineering + Étant donné que RoadWave se concentre sur l'essentiel + Quand les avantages Premium sont définis + Alors il n'y a pas de: + | fonctionnalité superflue | raison exclusion | + | Badges cosmétiques | Pas de valeur réelle | + | Avatar Premium exclusif | Inutile pour audio | + | Fonctionnalités sociales avancées | Pas prioritaire MVP | + | Early access nouveaux contenus | Complexité > bénéfice | + Et cela réduit la complexité et le coût de développement + + # ===== CONVERSION ET INCITATION ===== + + Scénario: CTA Premium après 5ème publicité + Étant donné que je suis gratuit et je viens d'entendre ma 5ème pub + Quand la publicité se termine + Alors je vois un message: + """ + 😫 Marre des pubs ? + + Passez Premium pour seulement 4.99€/mois : + • 0 publicité + • Qualité audio supérieure + • Téléchargements illimités + + [Essayer Premium] [Plus tard] + ``` + + Scénario: CTA Premium quand limite 50 téléchargements atteinte + Étant donné que je suis gratuit et j'ai atteint 50 téléchargements + Quand j'essaie de télécharger un 51ème contenu + Alors je vois une popup: + """ + 📥 Limite atteinte + + Vous avez atteint la limite de 50 téléchargements. + + Avec Premium (4.99€/mois), téléchargez autant de contenus que vous voulez + pour vos longs road trips ! + + [Passer Premium] [Gérer mes téléchargements] + ``` + + Scénario: CTA Premium quand contenu exclusif bloqué + Étant donné que je suis gratuit et je clique sur un contenu Premium + Quand l'overlay bloquant apparaît + Alors je vois: + """ + 👑 Contenu Premium + + Ce contenu est réservé aux abonnés Premium. + + Débloquez 8,547 contenus Premium exclusifs pour 4.99€/mois ! + + [Passer Premium] [Découvrir d'autres contenus] + ``` + + Scénario: Statistiques conversion - Quel avantage convertit le mieux ? + Étant donné qu'un admin consulte les statistiques de conversion + Quand il analyse les sources de conversion + Alors il voit: + | source de conversion | % conversions | + | CTA après 5ème pub | 42% | + | CTA contenu Premium bloqué | 28% | + | CTA limite 50 téléchargements | 18% | + | Page Premium directe | 12% | + Et cela aide à optimiser le placement des CTA + + Scénario: A/B test message CTA + Étant donné que RoadWave veut optimiser les conversions + Quand un A/B test est lancé sur le CTA après pub + Alors groupe A voit "Marre des pubs ?" (focus négatif) + Et groupe B voit "Profitez de 0 publicité" (focus positif) + Et le taux de conversion est mesuré + Et le message le plus performant est déployé + + Scénario: Notification Premium après 30 jours d'utilisation gratuite + Étant donné que je suis utilisateur gratuit depuis 30 jours + Et que j'écoute régulièrement (15h cumulées) + Quand le 30ème jour arrive + Alors je reçois une notification: + """ + 🎉 Vous avez écouté 15h sur RoadWave ! + + Profitez encore plus avec Premium : + • 0 publicité + • Qualité supérieure + • Téléchargements illimités + + Offre découverte : -20% sur le premier mois (3.99€) + [Découvrir Premium] + ``` + + Scénario: Trial gratuit refusé mais onboarding amélioré + Étant donné qu'il n'y a pas de trial gratuit + Quand un nouvel utilisateur s'inscrit + Alors un onboarding explique clairement les avantages Premium + Et il peut comparer gratuit vs Premium dès le premier lancement + Et cela l'aide à décider rapidement s'il veut payer diff --git a/features/premium/gestion-abonnement.feature b/features/premium/gestion-abonnement.feature new file mode 100644 index 0000000..2f09257 --- /dev/null +++ b/features/premium/gestion-abonnement.feature @@ -0,0 +1,457 @@ +# language: fr +Fonctionnalité: Gestion abonnement Premium + En tant qu'utilisateur + Je veux gérer facilement mon abonnement Premium + Afin de souscrire, renouveler ou annuler en toute transparence + + Contexte: + Étant donné que je suis connecté à l'application RoadWave + + # ===== SOUSCRIPTION ===== + + Scénario: Souscription via Web (desktop/mobile) avec Mangopay + Étant donné que je consulte la page Premium sur le site web + Quand je clique sur "S'abonner - Mensuel 4.99€" + Alors je suis redirigé vers le formulaire de paiement Mangopay + Et je saisis mes informations de carte bancaire + Et le paiement de 4.99€ est prélevé immédiatement + Et la commission Mangopay est de 1.8% + 0.18€ = 0.27€ + Et RoadWave reçoit 4.72€ net + + Scénario: Calcul commission Mangopay + Étant donné qu'un utilisateur paie 4.99€ via Mangopay + Quand la commission est calculée + Alors la commission est : 4.99€ × 1.8% + 0.18€ = 0.09€ + 0.18€ = 0.27€ + Et RoadWave reçoit : 4.99€ - 0.27€ = 4.72€ + Et la commission représente 5.4% du prix + + Scénario: Souscription via iOS App avec Apple IAP + Étant donné que j'utilise l'app iOS + Quand je clique sur "S'abonner - Mensuel 5.99€" + Alors je suis redirigé vers l'interface Apple In-App Purchase + Et le prix affiché est 5.99€ (majoré de 20%) + Et le paiement est effectué via mon compte Apple + Et Apple prend 30% de commission = 1.80€ + Et RoadWave reçoit 4.19€ net + + Scénario: Souscription via Android App avec Google Play Billing + Étant donné que j'utilise l'app Android + Quand je clique sur "S'abonner - Mensuel 5.99€" + Alors je suis redirigé vers l'interface Google Play Billing + Et le prix affiché est 5.99€ (majoré de 20%) + Et le paiement est effectué via mon compte Google + Et Google prend 30% de commission = 1.80€ + Et RoadWave reçoit 4.19€ net + + Scénario: Majoration 20% sur mobile pour compenser commission 30% + Étant donné que Apple/Google prennent 30% de commission + Quand RoadWave fixe le prix mobile + Alors le prix web est 4.99€ (commission Mangopay 5.4%) + Et le prix mobile est 5.99€ (commission Apple/Google 30%) + Et la majoration est de 1€ (+20%) + Et cela compense partiellement la commission excessive + + Scénario: Email incitation souscription web moins chère + Étant donné que je consulte Premium depuis l'app mobile + Quand je vois le prix 5.99€ + Alors je vois aussi un message: + """ + 💡 Astuce : Abonnez-vous sur roadwave.com pour seulement 4.99€/mois + Économisez 12€/an en évitant les frais Apple/Google ! + """ + Et un lien vers le site web est fourni + + Scénario: Calcul économie souscription web vs mobile + Étant donné que le prix web est 4.99€/mois + Et que le prix mobile est 5.99€/mois + Quand je calcule l'économie annuelle + Alors web : 4.99€ × 12 = 59.88€/an + Et mobile : 5.99€ × 12 = 71.88€/an + Et économie : 12€/an (soit 20% d'économie) + + Scénario: Activation immédiate après paiement réussi + Étant donné que je viens de payer mon abonnement Premium + Quand le paiement est confirmé + Alors mon statut passe immédiatement à "Premium" + Et je peux accéder aux avantages Premium dès maintenant + Et je reçois un email de confirmation + + Scénario: Email confirmation souscription + Étant donné que j'ai souscrit à Premium + Quand la souscription est confirmée + Alors je reçois un email: + """ + 🎉 Bienvenue Premium ! + + Votre abonnement Premium est actif. + + Formule: Mensuel 4.99€/mois + Prochain renouvellement: 15 juillet 2025 + + Vos avantages: + • 0 publicité + • Contenus exclusifs + • Qualité audio 64 kbps + • Téléchargements illimités + • Historique illimité + + Profitez pleinement de RoadWave ! + + Gérer mon abonnement: [Lien] + """ + + # ===== RENOUVELLEMENT AUTOMATIQUE ===== + + Scénario: Email rappel 7 jours avant renouvellement + Étant donné que mon abonnement mensuel se renouvelle le 15 juillet + Quand le 8 juillet arrive (7 jours avant) + Alors je reçois un email de rappel: + """ + 📅 Votre abonnement Premium se renouvelle dans 7 jours + + Formule: Mensuel 4.99€/mois + Date de renouvellement: 15 juillet 2025 + Montant: 4.99€ + + Votre carte bancaire sera débitée automatiquement. + + Vous souhaitez annuler ? [Gérer mon abonnement] + """ + + Scénario: Renouvellement automatique réussi + Étant donné que mon abonnement mensuel arrive à échéance le 15 juillet + Quand le 15 juillet arrive + Alors Mangopay/Apple/Google prélève automatiquement 4.99€ (ou 5.99€) + Et mon abonnement est renouvelé pour 1 mois supplémentaire + Et je reçois un email de confirmation + + Scénario: Email confirmation renouvellement + Étant donné que mon abonnement vient d'être renouvelé + Quand le paiement est confirmé + Alors je reçois un email: + """ + ✅ Abonnement Premium renouvelé + + Votre abonnement a été renouvelé avec succès. + + Montant débité: 4.99€ + Prochaine échéance: 15 août 2025 + + Merci de continuer à soutenir RoadWave et ses créateurs ! + + Gérer mon abonnement: [Lien] + """ + + Scénario: Échec paiement renouvellement - Tentative 1 + Étant donné que mon abonnement doit se renouveler le 15 juillet + Mais que ma carte bancaire est expirée ou sans fonds + Quand le prélèvement échoue + Alors je reçois un email: + """ + ⚠️ Échec renouvellement abonnement Premium + + Le paiement de votre abonnement a échoué. + Raison: Carte bancaire expirée + + Nous allons réessayer automatiquement dans 3 jours. + Veuillez mettre à jour vos informations de paiement: [Lien] + + Votre accès Premium reste actif jusqu'au 22 juillet (7 jours). + """ + + Scénario: Retry automatique paiement après 3 jours + Étant donné que le paiement a échoué le 15 juillet + Quand le 18 juillet arrive (J+3) + Alors Mangopay/Apple/Google tente automatiquement un nouveau prélèvement + Et si le paiement réussit, l'abonnement est renouvelé normalement + Et si le paiement échoue encore, un 2ème retry est programmé + + Scénario: Retry automatique paiement après 7 jours + Étant donné que 2 tentatives ont échoué (15 juillet et 18 juillet) + Quand le 22 juillet arrive (J+7) + Alors une 3ème et dernière tentative est effectuée + Et si le paiement réussit, l'abonnement est sauvé + Et si le paiement échoue, l'abonnement est annulé automatiquement + + Scénario: Annulation automatique après 3 échecs paiement + Étant donné que les 3 tentatives de renouvellement ont échoué (J+0, J+3, J+7) + Quand la 3ème tentative échoue + Alors mon abonnement Premium est annulé automatiquement + Et mon statut repasse à "Gratuit" + Et je perds accès aux avantages Premium + Et je reçois un email d'annulation + + Scénario: Email annulation automatique pour impayé + Étant donné que mon abonnement a été annulé pour échec paiement + Quand l'annulation devient effective + Alors je reçois un email: + """ + ❌ Abonnement Premium annulé + + Votre abonnement a été annulé suite à 3 échecs de paiement. + + Vous repassez en mode gratuit et perdez l'accès à: + • Contenus Premium exclusifs + • Qualité audio supérieure + • Téléchargements illimités + + Pour réactiver Premium, mettez à jour vos informations de paiement: [Lien] + """ + + # ===== ANNULATION ===== + + Scénario: Annulation self-service dans Settings + Étant donné que je veux annuler mon abonnement + Quand j'accède à "Paramètres > Abonnement" + Alors je vois un bouton "Annuler l'abonnement" + Et je peux annuler en 2 clics sans contacter le support + + Scénario: Confirmation avant annulation + Étant donné que je clique sur "Annuler l'abonnement" + Quand une popup de confirmation apparaît + Alors je vois: + """ + 😢 Vous allez annuler votre abonnement Premium + + Vous perdrez l'accès à: + • 0 publicité + • Contenus Premium exclusifs + • Qualité audio supérieure + • Téléchargements illimités + + Accès maintenu jusqu'au: 15 juillet 2025 (fin période payée) + Pas de remboursement prorata. + + [Confirmer l'annulation] [Rester Premium] + ``` + + Scénario: Accès Premium maintenu jusqu'à fin période payée + Étant donné que j'ai annulé mon abonnement le 1er juillet + Et que mon abonnement mensuel était valable jusqu'au 15 juillet + Quand l'annulation est confirmée + Alors je garde l'accès Premium jusqu'au 15 juillet + Et à partir du 16 juillet, je repasse en gratuit + Et je ne suis pas remboursé pour les 14 jours restants + + Scénario: Justification pas de remboursement prorata + Étant donné que l'industrie (Spotify, Netflix, YouTube) ne rembourse pas prorata + Quand RoadWave applique la même règle + Alors c'est le standard accepté par les utilisateurs + Et cela simplifie la gestion comptable + Et évite les abus (souscription puis annulation immédiate pour remboursement) + + Scénario: Email confirmation annulation + Étant donné que j'ai annulé mon abonnement + Quand l'annulation est enregistrée + Alors je reçois un email: + """ + ✅ Annulation confirmée + + Votre abonnement Premium a été annulé. + + Accès Premium maintenu jusqu'au: 15 juillet 2025 + Après cette date, vous repasserez en mode gratuit. + + Pas de remboursement pour la période restante (standard industrie). + + Vous pouvez vous réabonner à tout moment ! + + Nous espérons vous revoir bientôt. + Réabonner: [Lien] + """ + + Scénario: Pas de renouvellement après annulation + Étant donné que j'ai annulé mon abonnement le 1er juillet + Quand le 15 juillet arrive (date de renouvellement prévue) + Alors aucun prélèvement n'est effectué + Et mon statut passe automatiquement à "Gratuit" + Et je ne reçois pas d'email de renouvellement + + # ===== RÉABONNEMENT ===== + + Scénario: Réabonnement possible immédiatement + Étant donné que j'ai annulé mon abonnement il y a 5 jours + Quand j'accède à la page Premium + Alors je peux me réabonner immédiatement + Et le processus de paiement est le même que la première fois + + Scénario: Pas de nouvelle période d'essai au réabonnement + Étant donné que j'ai annulé mon abonnement il y a 3 mois + Quand je me réabonne + Alors je paie immédiatement 4.99€ (pas d'essai gratuit) + Car RoadWave ne propose jamais d'essai gratuit (ni première fois ni réabonnement) + + Scénario: Offre win-back pour utilisateurs ayant annulé + Étant donné que j'ai annulé mon abonnement il y a 1 mois + Quand je reçois un email de win-back + Alors je vois une offre spéciale: + """ + 🎁 On vous a manqué ? + + Revenez en Premium avec une offre exclusive: + -30% sur les 3 premiers mois (3.49€/mois au lieu de 4.99€) + + Offre valable jusqu'au 31 juillet. + + [Réactiver Premium] + ``` + + # ===== ARCHITECTURE DONNÉES ===== + + Scénario: Table subscriptions en base PostgreSQL + Étant donné qu'un utilisateur souscrit à Premium + Quand les données sont enregistrées + Alors la table subscriptions contient: + ```sql + CREATE TABLE subscriptions ( + id UUID PRIMARY KEY, + user_id UUID NOT NULL REFERENCES users(id) UNIQUE, + mangopay_recurring_payin_id VARCHAR(255), -- Null si IAP + mangopay_user_id VARCHAR(255), -- Null si IAP + apple_transaction_id VARCHAR(255), -- Null si Mangopay + google_purchase_token VARCHAR(255), -- Null si Mangopay + status VARCHAR(50) NOT NULL, -- 'active', 'cancelled', 'expired', 'past_due' + plan VARCHAR(50) NOT NULL, -- 'monthly', 'yearly' + current_period_start TIMESTAMP NOT NULL, + current_period_end TIMESTAMP NOT NULL, + cancelled_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT NOW() + ); + ``` + + Scénario: Statuts possibles dans subscription.status + Étant donné qu'un abonnement peut avoir différents statuts + Quand le statut est stocké en base + Alors les valeurs possibles sont: + | statut | description | + | active | Abonnement actif et payé | + | cancelled | Annulé par utilisateur (accès jusqu'à fin période) | + | expired | Période terminée, pas renouvelé | + | past_due | Échec paiement, en retry automatique | + + Scénario: Cache Redis pour vérification Premium temps réel + Étant donné qu'un utilisateur lance un contenu + Quand l'app vérifie s'il est Premium + Alors une clé Redis est consultée: + ``` + Key: premium:{user_id} + Value: true/false + TTL: 1 heure + ``` + Et si la clé n'existe pas, elle est recalculée depuis PostgreSQL + Et cela garantit des performances <10ms + + Scénario: Refresh cache Redis via webhooks + Étant donné qu'un paiement est confirmé par Mangopay/Apple/Google + Quand un webhook est reçu par RoadWave + Alors le cache Redis premium:{user_id} est mis à jour immédiatement + Et l'utilisateur voit son statut Premium activé sans délai + + Scénario: Webhooks Mangopay - PAYIN_NORMAL_SUCCEEDED + Étant donné qu'un paiement Mangopay réussit + Quand Mangopay envoie le webhook PAYIN_NORMAL_SUCCEEDED + Alors RoadWave met à jour subscriptions.status = 'active' + Et met à jour current_period_end = NOW() + 1 mois + Et refresh le cache Redis premium:{user_id} = true + + Scénario: Webhooks Mangopay - PAYIN_NORMAL_FAILED + Étant donné qu'un paiement Mangopay échoue + Quand Mangopay envoie le webhook PAYIN_NORMAL_FAILED + Alors RoadWave met à jour subscriptions.status = 'past_due' + Et programme un retry automatique dans 3 jours + Et envoie un email à l'utilisateur + + Scénario: Webhooks Apple - App Store Server Notifications + Étant donné qu'un paiement Apple IAP change de statut + Quand Apple envoie une notification serveur + Alors RoadWave parse la notification (JSON) + Et met à jour la subscription en conséquence + Et refresh le cache Redis + + Scénario: Webhooks Google - Real-time Developer Notifications + Étant donné qu'un paiement Google Play change de statut + Quand Google envoie une notification temps réel + Alors RoadWave parse la notification (JSON) + Et met à jour la subscription en conséquence + Et refresh le cache Redis + + # ===== STATISTIQUES ET MONITORING ===== + + Scénario: Dashboard admin - Métriques abonnements + Étant donné qu'un admin consulte les métriques Premium + Quand il accède au dashboard + Alors il voit: + | métrique | valeur | + | Abonnés actifs | 12,547 | + | Nouveaux abonnements ce mois | 1,234 | + | Annulations ce mois | 287 (2.3%) | + | Churn rate mensuel | 2.3% | + | MRR (Revenus mensuels récurrents) | 58,890€ | + | Taux conversion gratuit → Premium | 8.5% | + + Scénario: Calcul churn rate mensuel + Étant donné que 287 utilisateurs ont annulé ce mois + Et qu'il y avait 12,547 abonnés au début du mois + Quand le churn rate est calculé + Alors churn = 287 / 12,547 = 2.3% + Et un churn <5% est considéré comme excellent + Et RoadWave surveille cette métrique de près + + Scénario: Alerte si churn rate >5% + Étant donné que le churn rate mensuel dépasse 5% + Quand le système détecte cette anomalie + Alors une alerte est envoyée à l'équipe: + """ + ⚠️ Churn rate anormal: 6.2% + + Nombre annulations ce mois: 778 + Causes principales: + - Prix jugé trop élevé: 42% + - Utilisation faible: 28% + - Concurrent moins cher: 18% + - Autre: 12% + + Action recommandée: Enquête satisfaction + offres win-back + """ + + Scénario: Enquête satisfaction à l'annulation + Étant donné que je viens d'annuler mon abonnement + Quand l'annulation est confirmée + Alors je vois un questionnaire rapide: + """ + Pourquoi annulez-vous Premium ? + + ☐ Prix trop élevé + ☐ Je n'utilise pas assez RoadWave + ☐ Pas assez de contenus exclusifs + ☐ Problèmes techniques + ☐ J'ai trouvé une alternative moins chère + ☐ Autre: [Texte libre] + + [Envoyer] [Ignorer] + ``` + Et les réponses aident à améliorer l'offre Premium + + Scénario: Répartition canaux souscription + Étant donné qu'un admin analyse les canaux de souscription + Quand il consulte les statistiques + Alors il voit: + | canal | abonnés | % total | revenus/mois | + | Web (Mangopay) | 8,234 | 65.6% | 41,088€ | + | iOS (Apple) | 2,845 | 22.7% | 17,042€ | + | Android (Google)| 1,468 | 11.7% | 8,793€ | + Et cela aide à orienter les efforts marketing (inciter web = moins de commission) + + Scénario: Performance vérification Premium <10ms + Étant donné que 100 000 utilisateurs consultent des contenus simultanément + Quand chaque requête vérifie le statut Premium via Redis + Alors le temps de réponse moyen est <10ms + Et Redis gère facilement 100 000 requêtes/seconde + Et l'expérience utilisateur est fluide + + Scénario: Backup données abonnements + Étant donné que les données d'abonnements sont critiques + Quand un backup est effectué + Alors PostgreSQL est répliqué en temps réel sur un replica + Et un snapshot quotidien est stocké sur S3 + Et en cas de crash, les données peuvent être restaurées <5 minutes diff --git a/features/premium/multi-devices-detection.feature b/features/premium/multi-devices-detection.feature new file mode 100644 index 0000000..d5de462 --- /dev/null +++ b/features/premium/multi-devices-detection.feature @@ -0,0 +1,279 @@ +# language: fr +Fonctionnalité: Multi-devices et détection simultanée + En tant qu'abonné Premium + Je veux utiliser mon compte sur plusieurs appareils + Mais limité à 1 seul stream actif à la fois pour éviter le partage abusif + + Contexte: + Étant donné que je suis un utilisateur Premium actif + Et que mon compte est valide + + # ===== LIMITE 1 STREAM ACTIF ===== + + Scénario: 1 seul stream actif autorisé par compte + Étant donné que je n'écoute rien actuellement + Quand je lance un contenu sur mon iPhone + Alors le stream démarre normalement + Et Redis enregistre: active_streams:{user_id} = {device_id: "iPhone", started_at: timestamp} + + Scénario: Détection connexion simultanée - Arrêt premier device + Étant donné que j'écoute un contenu sur mon iPhone + Quand je lance un contenu sur mon iPad + Alors le système détecte une session active sur iPhone + Et la lecture sur iPhone est arrêtée immédiatement (WebSocket close) + Et je vois sur iPhone: "Lecture interrompue : votre compte est utilisé sur un autre appareil" + Et la lecture démarre sur iPad normalement + + Scénario: Message explicite sur device interrompu + Étant donné que ma lecture sur iPhone vient d'être interrompue + Quand je regarde l'écran de mon iPhone + Alors je vois une overlay avec le message: + """ + 🔴 Lecture interrompue + + Votre compte Premium est utilisé sur un autre appareil. + + Un seul stream actif est autorisé à la fois pour protéger + les revenus des créateurs et éviter le partage de compte. + """ + Et un bouton "Reprendre ici" est disponible + + Scénario: Reprendre lecture sur device interrompu + Étant donné que ma lecture sur iPhone a été interrompue + Et que je veux reprendre sur iPhone + Quand je clique sur "Reprendre ici" + Alors la lecture démarre sur iPhone + Et l'iPad est à son tour interrompu avec le même message + Et le "ping-pong" entre devices est possible (mais pénible) + + # ===== IMPLÉMENTATION TECHNIQUE REDIS ===== + + Scénario: Enregistrement session active dans Redis + Étant donné que je lance un contenu sur mon iPhone + Quand la lecture démarre + Alors une entrée Redis est créée: + ``` + Key: active_streams:{user_id} + Value: { + "device_id": "iPhone-ABC123", + "started_at": "2025-06-15T14:30:00Z", + "content_id": "xyz789" + } + TTL: 300 secondes (5 minutes) + ``` + + Scénario: Heartbeat toutes les 30 secondes pour maintenir session + Étant donné que j'écoute un contenu sur mon iPhone + Quand 30 secondes s'écoulent + Alors l'app envoie un heartbeat au serveur + Et le serveur refresh le TTL Redis à 300 secondes + Et la session reste active + + Scénario: Session considérée morte après 5 minutes sans heartbeat + Étant donné que j'écoute un contenu sur mon iPhone + Mais que l'app crash ou que le réseau coupe + Quand 5 minutes s'écoulent sans heartbeat + Alors l'entrée Redis expire automatiquement (TTL atteint) + Et je peux relancer sur n'importe quel device sans conflit + + Scénario: Vérification session avant démarrage lecture + Étant donné que je veux lancer un contenu sur mon iPad + Quand j'appuie sur Play + Alors le serveur vérifie Redis: active_streams:{user_id} + Et si une session existe sur un autre device, elle est tuée + Et la nouvelle session iPad est enregistrée dans Redis + + Scénario: Gestion multi-utilisateurs simultanés + Étant donné que 100 000 utilisateurs Premium écoutent simultanément + Quand Redis stocke 100 000 entrées active_streams + Alors chaque entrée a un TTL de 5 minutes + Et Redis gère facilement cette charge (~10 MB de RAM) + Et les vérifications sont quasi-instantanées (O(1)) + + # ===== EXCEPTIONS ET CAS PARTICULIERS ===== + + Scénario: Contenus téléchargés (offline) ne comptent pas comme stream + Étant donné que j'ai téléchargé 20 contenus en mode offline + Quand j'écoute un contenu téléchargé sur mon iPhone sans réseau + Alors aucune session active n'est enregistrée dans Redis + Et je peux écouter offline pendant qu'un autre device stream online + Car le contenu offline ne consomme pas de bande passante serveur + + Scénario: Transition rapide device <10s tolérée + Étant donné que j'écoute dans ma voiture sur mon iPhone + Et que j'arrive chez moi + Quand je lance la lecture sur mon iPad dans les 10 secondes + Alors la transition est considérée comme un changement de device légitime + Et aucun message d'erreur n'est affiché sur iPhone + Et la lecture reprend exactement où j'étais sur iPad + + Scénario: Détection transition rapide via timestamps + Étant donné que la session iPhone a started_at = 14:30:00 + Quand je lance sur iPad à 14:30:05 (5 secondes après) + Alors le serveur détecte: diff = 5s < 10s + Et applique une "graceful transition" (pas de message d'erreur iPhone) + Et Redis met à jour: active_streams:{user_id} = {device_id: "iPad", ...} + + Scénario: Plusieurs devices disponibles mais 1 seul actif + Étant donné que je possède: + | device | status | + | iPhone | Installé | + | iPad | Installé | + | MacBook (web) | Connecté | + | Android (conjoint)| Installé | + Quand je lance un stream sur n'importe quel device + Alors seulement 1 peut être actif à la fois + Et les autres devices sont en "standby" + + # ===== JUSTIFICATIONS ===== + + Scénario: Justification anti-partage compte + Étant donné qu'un utilisateur Premium partage son compte avec un ami + Quand les 2 personnes essaient d'écouter simultanément + Alors la lecture est constamment interrompue sur l'un ou l'autre + Et l'expérience devient inutilisable + Et cela décourage fortement le partage de compte + + Scénario: Justification protection revenus créateurs + Étant donné que 1 abonnement Premium = 4.99€/mois + Quand 70% sont reversés aux créateurs (3.49€) + Alors les créateurs sont rémunérés pour 1 personne + Et si 2 personnes utilisent le même compte simultanément, c'est injuste + Et la limite 1 stream protège l'équité du système + + Scénario: Justification UX claire + Étant donné qu'un stream est interrompu sur un device + Quand l'utilisateur voit le message explicite + Alors il comprend immédiatement pourquoi (autre device actif) + Et il peut choisir de reprendre sur le device actuel ou l'autre + Et il n'y a pas de confusion ou frustration + + Scénario: Comparaison avec Spotify (limite 1 stream) + Étant donné que Spotify Premium limite aussi à 1 stream actif + Quand RoadWave applique la même règle + Alors les utilisateurs connaissent déjà ce comportement + Et cela paraît normal et accepté par l'industrie + + Scénario: Comparaison avec Netflix (plusieurs streams selon formule) + Étant donné que Netflix permet 1-4 streams selon la formule + Quand RoadWave limite à 1 stream pour tous + Alors c'est plus strict que Netflix + Mais Netflix cible le foyer familial (TV partagée) + Alors que RoadWave cible l'individu conducteur (usage personnel) + + # ===== MONITORING ET DÉTECTION ABUS ===== + + Scénario: Détection pattern suspect - Changements devices fréquents + Étant donné qu'un utilisateur change de device 50 fois en 1 heure + Quand le système détecte ce pattern anormal + Alors une alerte est générée pour l'équipe modération + Et le compte peut être marqué pour surveillance + Et si abus confirmé, suspension possible + + Scénario: Logs des changements de device + Étant donné que je change de device plusieurs fois par jour + Quand les changements sont loggés + Alors chaque événement est enregistré: + | timestamp | from_device | to_device | content_id | + | 2025-06-15 08:30:00 | null | iPhone | abc123 | + | 2025-06-15 09:15:00 | iPhone | iPad | def456 | + | 2025-06-15 18:30:00 | iPad | iPhone | ghi789 | + Et ces logs aident à détecter les partages de compte + + Scénario: Métriques admin - Changements devices par utilisateur + Étant donné qu'un admin consulte les métriques de streaming + Quand il accède au dashboard + Alors il voit: + | métrique | valeur | + | Utilisateurs Premium actifs | 12,547 | + | Changements de device/jour (médiane) | 2 | + | Utilisateurs >10 changements/jour | 47 (0.4%) | + | Comptes suspects (>20 changements/j) | 3 | + + Scénario: Email d'avertissement si changements excessifs + Étant donné que je change de device 30 fois par jour pendant 3 jours + Quand le système détecte ce pattern + Alors je reçois un email d'avertissement: + """ + ⚠️ Activité inhabituelle détectée sur votre compte + + Nous avons détecté un nombre anormalement élevé de changements de device (30/jour). + + Rappel: Le partage de compte Premium est interdit selon nos CGU. + Un seul stream actif est autorisé à la fois. + + Si cette activité continue, votre compte pourra être suspendu. + """ + + Scénario: Suspension compte après avertissement ignoré + Étant donné que j'ai reçu un email d'avertissement il y a 7 jours + Mais que je continue à changer de device 30 fois par jour + Quand l'équipe modération examine le compte + Alors mon compte Premium peut être suspendu pour partage abusif + Et je reçois un email de suspension avec justification + + # ===== SUPPORT UTILISATEUR ===== + + Scénario: FAQ - Pourquoi ma lecture s'arrête quand j'utilise un autre device ? + Étant donné que je consulte la FAQ Premium + Quand je cherche "lecture interrompue" + Alors je trouve la réponse: + """ + Q: Pourquoi ma lecture s'arrête quand j'utilise un autre appareil ? + + R: Votre abonnement Premium autorise 1 seul stream actif à la fois. + Si vous lancez la lecture sur un autre appareil, le premier est automatiquement arrêté. + + Pourquoi cette limite ? + - Protéger les revenus des créateurs (1 abonnement = 1 personne) + - Éviter le partage de compte abusif + + Vous pouvez utiliser autant d'appareils que vous voulez, mais un seul peut lire à la fois. + """ + + Scénario: Support - Utilisateur pense être piraté + Étant donné qu'un utilisateur voit constamment "Lecture interrompue" + Et qu'il pense que son compte est piraté + Quand il contacte le support + Alors le support vérifie les logs de changements de device + Et peut identifier les devices (iPhone, iPad perso vs iPhone inconnu) + Et conseille de changer le mot de passe si device inconnu détecté + + Scénario: Changement mot de passe déconnecte tous les devices + Étant donné que je pense que mon compte est compromis + Quand je change mon mot de passe + Alors tous mes devices sont déconnectés immédiatement + Et les sessions actives dans Redis sont supprimées + Et je dois me reconnecter sur chaque device + Et cela sécurise mon compte + + # ===== TESTS TECHNIQUES ===== + + Scénario: Test charge - 100 000 vérifications/seconde + Étant donné que 100 000 utilisateurs Premium lancent des contenus + Quand chaque lancement vérifie Redis (GET active_streams:{user_id}) + Alors Redis peut gérer facilement 100 000 requêtes/seconde + Et le temps de réponse moyen est <1ms + Et aucun ralentissement n'est constaté + + Scénario: Test failover Redis + Étant donné que le serveur Redis principal tombe en panne + Quand le failover automatique vers le replica Redis s'active + Alors les sessions actives peuvent être perdues temporairement (max 5 min) + Mais les utilisateurs peuvent relancer immédiatement + Et l'impact est minimal (pas de perte de données critiques) + + Scénario: Test concurrence - Lancement simultané 2 devices + Étant donné que je lance exactement au même instant sur iPhone et iPad + Quand les 2 requêtes arrivent en parallèle au serveur + Alors Redis utilise un lock (SETNX) pour atomicité + Et 1 seul device gagne (par exemple iPhone) + Et l'autre device (iPad) reçoit immédiatement une erreur + Et l'utilisateur peut retry sur iPad si souhaité + + Scénario: Nettoyage automatique sessions expirées + Étant donné que 1000 sessions Redis ont expiré (TTL atteint) + Quand Redis supprime automatiquement ces entrées + Alors la mémoire est libérée + Et les nouveaux streams peuvent démarrer sans conflit + Et aucune intervention manuelle n'est nécessaire diff --git a/features/premium/offre-tarification.feature b/features/premium/offre-tarification.feature new file mode 100644 index 0000000..aef37d6 --- /dev/null +++ b/features/premium/offre-tarification.feature @@ -0,0 +1,254 @@ +# language: fr +Fonctionnalité: Offre et tarification Premium + En tant qu'utilisateur + Je veux pouvoir souscrire à un abonnement Premium + Afin de profiter d'une expérience sans publicité avec des avantages exclusifs + + Contexte: + Étant donné que l'API RoadWave est disponible + Et que je suis connecté en tant qu'utilisateur + + # ===== FORMULES DISPONIBLES ===== + + Scénario: Formule mensuelle à 4.99€/mois + Étant donné que je consulte les offres Premium + Quand je vois la formule mensuelle + Alors le prix affiché est 4.99€/mois + Et il n'y a aucune réduction + Et le prix effectif par mois est 4.99€ + + Scénario: Formule annuelle à 49.99€/an (2 mois offerts) + Étant donné que je consulte les offres Premium + Quand je vois la formule annuelle + Alors le prix affiché est 49.99€/an + Et l'économie affichée est "2 mois offerts" + Et le prix effectif par mois est 4.16€ + Et je vois le badge "Meilleure offre" + + Scénario: Calcul économie formule annuelle + Étant donné que la formule mensuelle coûte 4.99€/mois + Quand je calcule le coût annuel en mensuel + Alors 12 mois × 4.99€ = 59.88€/an + Et la formule annuelle coûte 49.99€ + Et l'économie est de 9.89€ (≈ 2 mois gratuits) + Et la réduction est de 16.5% + + Scénario: Pas d'essai gratuit disponible + Étant donné que je consulte les offres Premium + Quand je recherche une option "Essai gratuit" + Alors aucune option d'essai gratuit n'est proposée + Et je dois payer dès le premier jour pour accéder au Premium + + Scénario: Justification absence essai gratuit - Anti-abus vacances + Étant donné que RoadWave ne propose pas d'essai gratuit + Quand un utilisateur envisage un road trip de 14 jours + Alors il ne peut pas s'abonner pour l'essai gratuit puis annuler + Et cela évite les inscriptions opportunistes + Et protège les revenus des créateurs + + Scénario: Justification absence essai gratuit - Protection revenus créateurs + Étant donné qu'un utilisateur Premium écoute des contenus + Quand il génère des écoutes dès le jour 1 + Alors les créateurs sont rémunérés immédiatement (70% de 4.99€) + Et il n'y a pas de "période gratuite" sans rémunération créateurs + + Scénario: Justification absence essai gratuit - Simplicité + Étant donné que RoadWave gère les abonnements + Quand il n'y a pas d'essai gratuit + Alors pas de gestion complexe de période trial + Et pas de workflow de conversion trial → payant + Et cela réduit la complexité technique + + Scénario: Justification absence essai gratuit - Engagement + Étant donné qu'un utilisateur paie dès le début + Quand il souscrit à Premium + Alors il est plus engagé qu'un utilisateur en essai gratuit + Et le taux de churn est généralement plus faible + Et la lifetime value (LTV) est plus élevée + + Scénario: Pas de partage familial au MVP + Étant donné que je consulte les offres Premium + Quand je recherche une option "Famille" ou "Partage" + Alors aucune option de partage familial n'est disponible + Et seuls les abonnements individuels sont proposés + + Scénario: Justification absence partage familial - Complexité technique + Étant donné que le partage familial nécessite: + | fonctionnalité | complexité | + | Gestion invitations | Moyenne | + | Validation liens famille | Moyenne | + | Limite devices par membre | Élevée | + | Dashboard admin famille | Élevée | + Quand RoadWave évalue le ROI + Alors le coût dev/support est trop élevé pour le MVP + Et la fonctionnalité est reportée post-MVP + + Scénario: Justification absence partage familial - Risque abus + Étant donné qu'une offre famille permet 5-6 membres + Quand il n'y a pas de vérification stricte de lien familial + Alors des "familles" de 6 inconnus pourraient se former + Et cela réduirait fortement les revenus (6 personnes pour 1 abonnement) + + Scénario: Justification absence partage familial - Cible individuelle + Étant donné que RoadWave cible principalement les conducteurs + Quand chaque conducteur utilise l'app individuellement en voiture + Alors le besoin de partage familial est limité + Et la plupart des utilisateurs sont des individus (pas des familles) + + Scénario: Post-MVP - Offre Famille à 9.99€/mois pour 5 comptes + Étant donné que RoadWave envisage une offre Famille post-MVP + Quand la fonctionnalité est spécifiée + Alors le prix serait 9.99€/mois pour 5 comptes + Et cela représente 2€/mois/personne + Mais cette offre n'est pas disponible au MVP + + Scénario: Comparaison tarif - Spotify à 10.99€/mois + Étant donné que Spotify Premium coûte 10.99€/mois + Quand RoadWave fixe son prix à 4.99€/mois + Alors RoadWave est 54.5% moins cher que Spotify + Et cela positionne RoadWave comme très accessible + + Scénario: Comparaison tarif - YouTube Premium à 11.99€/mois + Étant donné que YouTube Premium coûte 11.99€/mois + Quand RoadWave fixe son prix à 4.99€/mois + Alors RoadWave est 58.4% moins cher que YouTube Premium + Et cela est un argument commercial fort + + Scénario: Comparaison tarif - Apple Music à 10.99€/mois + Étant donné qu'Apple Music coûte 10.99€/mois + Quand RoadWave fixe son prix à 4.99€/mois + Alors RoadWave est 54.5% moins cher qu'Apple Music + Et cela attire les utilisateurs sensibles au prix + + Scénario: Justification tarif bas - Cible conducteurs quotidiens + Étant donné que RoadWave cible les trajets quotidiens domicile-travail + Quand le prix est fixé à 4.99€/mois + Alors c'est un budget raisonnable pour un conducteur + Et équivalent à ~1-2 cafés/mois + Et psychologiquement acceptable pour un usage quotidien + + Scénario: Justification formule annuelle - Engagement long terme + Étant donné que la formule annuelle offre 2 mois gratuits + Quand un utilisateur souscrit pour 1 an + Alors il s'engage sur le long terme + Et RoadWave sécurise 49.99€ de revenus immédiatement + Et le cash flow est amélioré + + Scénario: Justification formule annuelle - Réduction churn + Étant donné qu'un utilisateur paie 49.99€ pour l'année + Quand il envisage d'arrêter après 3 mois + Alors il a déjà payé pour 12 mois + Et il continuera probablement à utiliser l'app + Et le taux de churn est réduit significativement + + Scénario: Affichage comparatif des deux formules + Étant donné que je consulte la page Premium + Quand je vois les deux formules côte à côte + Alors je vois: + ``` + ┌─────────────────────────────────┐ ┌─────────────────────────────────┐ + │ MENSUEL │ │ ANNUEL ⭐ Meilleure offre │ + │ │ │ │ + │ 4.99€/mois │ │ 49.99€/an │ + │ │ │ soit 4.16€/mois │ + │ Engagement 1 mois │ │ │ + │ │ │ 💰 2 mois offerts ! │ + │ │ │ Économie: 9.89€/an │ + │ │ │ │ + │ [S'abonner] │ │ [S'abonner] │ + └─────────────────────────────────┘ └─────────────────────────────────┘ + ``` + + Scénario: Mise en avant formule annuelle + Étant donné que je consulte la page Premium + Quand je vois les deux formules + Alors la formule annuelle a un badge "Meilleure offre" ⭐ + Et elle est visuellement mise en avant (bordure colorée, taille plus grande) + Et l'économie de 2 mois est affichée en gros + Et cela incite à choisir la formule annuelle + + Scénario: Lien "Pourquoi pas d'essai gratuit ?" en FAQ + Étant donné que je consulte la page Premium + Quand je clique sur "FAQ" + Alors je vois une question "Pourquoi pas d'essai gratuit ?" + Et la réponse explique: + """ + RoadWave ne propose pas d'essai gratuit pour 3 raisons: + + 1. Protection des créateurs: Vos écoutes rémunèrent les créateurs dès le premier jour. + 2. Engagement: Un abonnement payant dès le début garantit une meilleure expérience. + 3. Anti-abus: Éviter les inscriptions opportunistes (essai avant vacances puis annulation). + + Le prix de 4.99€/mois reste très accessible (moitié prix de Spotify/YouTube Premium). + """ + + Scénario: A/B test formule annuelle (post-MVP) + Étant donné que RoadWave veut optimiser la conversion annuelle + Quand un A/B test est lancé + Alors groupe A voit "2 mois offerts" (économie en durée) + Et groupe B voit "Économisez 9.89€" (économie en argent) + Et les taux de souscription sont mesurés + Et le message le plus performant est déployé + + Scénario: Promo temporaire exceptionnelle (Black Friday, etc.) + Étant donné que c'est le Black Friday + Quand une promo temporaire est activée + Alors la formule annuelle peut passer à 39.99€/an (au lieu de 49.99€) + Et l'économie affichée est "4 mois offerts !" + Et la promo dure 3 jours uniquement + Et cela génère un pic de souscriptions + + Scénario: Code promo partenariat influenceur + Étant donné qu'un influenceur promeut RoadWave + Quand il partage un code promo "INFLUENCEUR20" + Alors les utilisateurs obtiennent -20% sur le premier mois (3.99€ au lieu de 4.99€) + Et le code est valable 1 mois + Et les conversions sont trackées par code promo + + Scénario: Statistiques admin - Répartition formules + Étant donné qu'un admin consulte les métriques d'abonnements + Quand il accède au dashboard + Alors il voit: + | métrique | valeur | + | Abonnés Premium total | 12,547 | + | Abonnés mensuels | 7,234 (58%)| + | Abonnés annuels | 5,313 (42%)| + | Revenus mensuels récurrents | 58,890€ | + Et ces données aident à piloter la stratégie tarifaire + + Scénario: Calcul revenus mensuels récurrents (MRR) + Étant donné que RoadWave a: + | formule | nombre abonnés | prix | + | Mensuel | 7,234 | 4.99€/mois| + | Annuel | 5,313 | 49.99€/an | + Quand le MRR est calculé + Alors MRR mensuel = 7,234 × 4.99€ = 36,098€ + Et MRR annuel ramené au mois = 5,313 × 49.99€ / 12 = 22,139€ + Et MRR total = 58,237€/mois + + Scénario: Projection revenus annuels (ARR) + Étant donné que le MRR est de 58,237€ + Quand l'ARR est calculé + Alors ARR = 58,237€ × 12 = 698,844€/an + Et cela aide à évaluer la valorisation de l'entreprise + + Scénario: Affichage prix TTC (TVA incluse) + Étant donné que RoadWave est une plateforme française + Quand les prix sont affichés + Alors tous les prix sont TTC (TVA 20% incluse) + Et le prix 4.99€ inclut déjà la TVA + Et cela respecte la réglementation française + + Scénario: Performance page Premium avec cache + Étant donné que la page Premium est consultée fréquemment + Quand un utilisateur charge la page + Alors les prix et avantages sont servis depuis un cache CDN + Et le temps de chargement est <200ms + Et cela garantit une expérience fluide + + Scénario: Localisation prix selon pays (post-MVP) + Étant donné que RoadWave se lance à l'international post-MVP + Quand un utilisateur se connecte depuis l'Allemagne + Alors les prix peuvent être ajustés (ex: 4.99€ en France, 4.49€ en Pologne) + Et cela respecte le pouvoir d'achat local + Mais cette fonctionnalité n'est pas au MVP (France uniquement) diff --git a/features/profil/profil-createur.feature b/features/profil/profil-createur.feature new file mode 100644 index 0000000..10edf3f --- /dev/null +++ b/features/profil/profil-createur.feature @@ -0,0 +1,293 @@ +# language: fr + +Fonctionnalité: Profil créateur + En tant qu'utilisateur de RoadWave + Je veux consulter les profils des créateurs + Afin de découvrir leur contenu et décider de m'abonner + + Contexte: + Étant donné que l'application RoadWave est démarrée + + # 15.2.1 - Structure de la page profil + + Scénario: URL du profil créateur + Étant donné un créateur avec le pseudo "paris_stories" + Quand l'utilisateur accède au profil + Alors l'URL est "https://roadwave.fr/@paris_stories" + + Scénario: Informations principales du profil + Étant donné un créateur "@paris_stories" avec les informations suivantes: + | champ | valeur | + | photo | avatar_120x120.jpg | + | pseudo | paris_stories | + | badge_vérifié | true | + | bio | Histoires et anecdotes de Paris | + | abonnés | 1200 | + | contenus | 42 | + | durée_totale | 18h | + | écoutes_totales | 54000 | + Quand le profil est affiché + Alors les éléments suivants sont visibles: + | élément | valeur affichée | + | Photo profil | 120×120 px | + | @pseudo | @paris_stories | + | Badge vérifié | ✓ | + | Bio | Histoires et... | + | Nombre abonnés | 1.2K abonnés | + | Nombre contenus | 42 contenus | + | Durée totale | 18h de contenu créé | + | Écoutes totales | 54K écoutes totales | + + Plan du Scénario: Arrondi des statistiques publiques + Étant donné un créateur avec + Quand le profil est affiché + Alors la valeur affichée est "" + + Exemples: + | métrique | valeur_exacte | valeur_affichée | + | abonnés | 342 | 342 | + | abonnés | 1200 | 1.2K | + | abonnés | 54000 | 54K | + | abonnés | 1200000 | 1.2M | + | écoutes | 842 | 842 | + | écoutes | 5400 | 5.4K | + | écoutes | 142000 | 142K | + | écoutes | 2100000 | 2.1M | + | durée (heures) | 18 | 18h | + | durée (heures) | 142 | 142h | + + Scénario: Bio avec markdown basique + Étant donné un créateur avec la bio suivante en markdown: + """ + **Histoires de Paris** - Découvrez la capitale + *Nouveau contenu chaque semaine* + https://paris-stories.fr + """ + Quand le profil est affiché + Alors le texte en gras "Histoires de Paris" est formaté + Et le texte en italique "Nouveau contenu chaque semaine" est formaté + Et le lien "https://paris-stories.fr" est cliquable + + Scénario: Limitation de la bio à 300 caractères + Étant donné un créateur qui entre une bio de 350 caractères + Quand la bio est sauvegardée + Alors seuls les 300 premiers caractères sont conservés + Et un message "Maximum 300 caractères" s'affiche + + Scénario: Boutons d'action principaux + Étant donné que l'utilisateur consulte un profil créateur + Quand la page est chargée + Alors les boutons suivants sont visibles: + | bouton | action | + | S'abonner | Abonnement au créateur | + | Partager profil | Menu de partage | + | ••• | Menu contextuel | + + Scénario: Menu contextuel du profil [•••] + Étant donné que l'utilisateur clique sur le bouton [•••] + Quand le menu s'ouvre + Alors les options suivantes sont disponibles: + | option | description | + | Partager profil | Partager le lien du profil | + | Signaler profil | Signaler spam ou usurpation d'identité | + | Bloquer créateur | Masquer tous les contenus du créateur | + + Scénario: Liste des contenus du créateur + Étant donné un créateur avec 3 contenus publiés + Quand le profil est affiché + Alors chaque contenu affiche: + | élément | exemple | + | Cover image | Image 16:9 | + | Titre | Balade à Paris | + | Durée et écoutes | 12 min · 🎧 2.3K | + | Localisation | 📍 Paris | + | Bouton lecture | ▶️ | + + Plan du Scénario: Options de tri des contenus + Étant donné un créateur avec 10 contenus publiés + Quand l'utilisateur sélectionne le tri "" + Alors les contenus sont triés par + + Exemples: + | option_tri | critère | + | Plus récents | Date publication DESC (défaut) | + | Plus populaires | Écoutes × facteur temporel (90 jours) | + | Plus anciens | Date publication ASC | + + Scénario: Filtrage des contenus par tag + Étant donné un créateur avec des contenus taggés "Voyage", "Histoire", "Gastronomie" + Quand l'utilisateur filtre par tags "Voyage, Histoire" + Alors seuls les contenus avec ces tags sont affichés + Et le nombre de résultats est indiqué "12 contenus" + + Scénario: Recherche locale dans le profil + Étant donné que l'utilisateur consulte le profil de "@paris_stories" + Et que le créateur a publié 50 contenus + Quand l'utilisateur entre "Montmartre" dans la barre de recherche + Alors la recherche s'effectue sur les titres et descriptions + Et seuls les contenus correspondants sont affichés + Et le placeholder indique "Rechercher dans les contenus de @paris_stories" + + Scénario: Chargement paginé des contenus + Étant donné un créateur avec 100 contenus publiés + Quand le profil est affiché + Alors 20 contenus sont chargés initialement + Et un bouton "Charger plus" est visible en bas de page + Quand l'utilisateur clique sur "Charger plus" + Alors 20 contenus supplémentaires sont chargés + + # 15.2.2 - Statistiques publiques + + Scénario: Informations publiques visibles par tous + Étant donné que l'utilisateur consulte un profil créateur + Alors les informations suivantes sont publiques: + | information | visible | + | Photo et pseudo | ✅ | + | Badge vérifié | ✅ | + | Bio | ✅ | + | Nombre abonnés | ✅ | + | Nombre contenus | ✅ | + | Durée totale créée | ✅ | + | Écoutes totales | ✅ | + + Scénario: Informations privées non visibles + Étant donné que l'utilisateur consulte un profil créateur + Alors les informations suivantes sont privées: + | information | visible | + | Liste des abonnés | ❌ | + | Revenus | ❌ | + | Localisation précise | ❌ | + | Email | ❌ | + + Scénario: Dashboard créateur avec métriques privées + Étant donné que le créateur "@paris_stories" consulte son propre dashboard + Quand la page statistiques est affichée + Alors les métriques suivantes sont accessibles: + | métrique | type | + | Taux complétion moyen | 78% | + | Évolution abonnés | Graphique | + | Écoutes par contenu | Tableau | + | Revenus | Dashboard | + | Taux conversion Premium | Pourcentage | + | Démographie (âge/zone) | Agrégée | + + Scénario: Graphique d'évolution des abonnés + Étant donné que le créateur consulte son dashboard + Quand il sélectionne la période "30 jours" + Alors un graphique d'évolution des abonnés est affiché + Et les périodes disponibles sont: + | période | + | 30j | + | 90j | + | 1 an | + + Scénario: Tableau détaillé des écoutes par contenu + Étant donné un créateur avec 10 contenus publiés + Quand il consulte le tableau des performances + Alors chaque contenu affiche: + | métrique | exemple | + | Titre | Balade | + | Écoutes totales | 2300 | + | Écoutes complètes >80% | 1840 | + | Taux complétion | 80% | + | Likes | 420 | + | Partages | 56 | + + # 15.2.3 - Badge vérifié + + Scénario: Affichage du badge vérifié + Étant donné un créateur vérifié "@paris_stories" + Quand son profil est affiché + Alors le badge bleu "✓" est accolé au pseudo + Et un tooltip "Compte vérifié" s'affiche au survol + + Scénario: Badge vérifié visible partout + Étant donné un créateur vérifié "@paris_stories" + Alors le badge "✓" est affiché dans: + | emplacement | + | Page profil | + | Player en lecture | + | Résultats de recherche| + | Notifications | + + Plan du Scénario: Attribution automatique du badge selon critères + Étant donné un créateur avec + Quand les conditions sont validées + Alors le badge vérifié est attribué + + Exemples: + | critère | automatique | + | KYC Mangopay validé | Oui | + | ≥10K abonnés + compte >6 mois | Oui | + | Célébrité / Média officiel | Manuel | + + Scénario: Attribution automatique via KYC + Étant donné un créateur qui complète son KYC Mangopay + Quand les documents sont validés + Alors le badge vérifié est attribué automatiquement + Et une notification "Votre compte est maintenant vérifié ✓" est envoyée + + Scénario: Attribution automatique à 10K abonnés + Étant donné un créateur avec 9999 abonnés et un compte de 7 mois + Quand il atteint 10000 abonnés + Alors le badge vérifié est attribué automatiquement + Et une notification de félicitations est envoyée + + Scénario: Demande manuelle de vérification (célébrité) + Étant donné un créateur reconnu publiquement + Quand il soumet le formulaire de demande de vérification + Alors une requête est créée pour l'équipe RoadWave + Et l'équipe vérifie l'identité sous 48-72h + Et le badge est attribué si validation réussie + + Scénario: Retrait du badge en cas de suspension + Étant donné un créateur vérifié avec le badge "✓" + Quand sa monétisation est suspendue + Alors le badge vérifié est retiré temporairement + Et le badge est restauré après levée de la suspension + + Scénario: Retrait définitif du badge pour strikes multiples + Étant donné un créateur vérifié avec 3 strikes actifs + Quand un 4ème strike est appliqué (ban) + Alors le badge vérifié est retiré définitivement + Et le compte est banni + + Scénario: Retrait du badge pour usurpation d'identité + Étant donné un créateur vérifié qui usurpe l'identité d'une célébrité + Quand la fraude est détectée + Alors le badge est retiré immédiatement + Et le compte est banni + Et une enquête est ouverte + + # Cas d'erreur et limites + + Scénario: Profil créateur supprimé + Étant donné qu'un utilisateur tente d'accéder à "@deleted_user" + Quand la page est chargée + Alors un message "Ce profil n'existe pas ou a été supprimé" s'affiche + + Scénario: Blocage d'un créateur + Étant donné que l'utilisateur bloque le créateur "@spam_account" + Quand l'utilisateur consulte son flux de recommandations + Alors aucun contenu de "@spam_account" n'est affiché + Et le créateur n'apparaît plus dans les recherches + + Scénario: Déblocage d'un créateur + Étant donné que l'utilisateur a bloqué "@paris_stories" + Quand il accède à ses paramètres "Comptes bloqués" + Et qu'il débloque "@paris_stories" + Alors les contenus du créateur réapparaissent dans les recommandations + + Scénario: Signalement d'un profil pour spam + Étant donné que l'utilisateur signale le profil "@spam_account" + Quand il sélectionne la raison "Spam" + Alors le signalement est envoyé à la modération + Et un message de confirmation s'affiche + Et le profil reste visible jusqu'à décision de modération + + Scénario: Signalement pour usurpation d'identité + Étant donné que l'utilisateur signale le profil "@fake_celebrity" + Quand il sélectionne "Usurpation d'identité" + Et qu'il fournit une preuve + Alors le signalement est priorisé (priorité HAUTE) + Et l'équipe modération traite sous 24h diff --git a/features/publicites/campagnes-publicitaires.feature b/features/publicites/campagnes-publicitaires.feature new file mode 100644 index 0000000..a9aeabe --- /dev/null +++ b/features/publicites/campagnes-publicitaires.feature @@ -0,0 +1,254 @@ +# language: fr +Fonctionnalité: Création de campagnes publicitaires + En tant que publicitaire + Je veux créer des campagnes avec ciblage précis et maîtrise du budget + Afin d'optimiser mes investissements publicitaires + + Contexte: + Étant donné que l'API RoadWave est disponible + Et qu'un compte publicitaire est créé et vérifié + + Scénario: Création d'une campagne publicitaire complète + Étant donné que je suis connecté en tant que publicitaire + Quand je crée une nouvelle campagne avec les paramètres: + | Paramètre | Valeur | + | Budget total | 300€ | + | Date début | 2026-02-01 | + | Date fin | 2026-02-14 | + | Zone géographique | Département du Var | + | Plages horaires | 7h-9h, 17h-19h | + | Tags ciblés | Automobile, Voyage | + | Tranche d'âge | 18+ | + Alors la campagne est créée avec succès + Et le budget quotidien calculé est de 21.43€/jour + Et les diffusions estimées sont de ~430 écoutes complètes + Et un statut "En attente de validation" est assigné + + Scénario: Budget minimum 50€ requis + Étant donné que je crée une nouvelle campagne + Quand je définis un budget de 40€ + Alors une erreur s'affiche: "Budget minimum requis: 50€" + Et la campagne n'est pas créée + + Scénario: Budget de 50€ exactement accepté + Étant donné que je crée une nouvelle campagne + Quand je définis un budget de 50€ + Alors la campagne est créée avec succès + Et aucune erreur n'est affichée + + Scénario: Calcul automatique du budget quotidien + Étant donné une campagne avec: + | Budget total | 300€ | + | Durée | 14 j | + Quand le système calcule le budget quotidien + Alors le budget/jour est de 21.43€ + Et le nombre estimé de diffusions/jour est de 430 (à 0.05€/écoute) + + Scénario: Ciblage géographique point GPS précis + Étant donné que je crée une campagne + Quand je sélectionne "Point GPS" avec coordonnées (43.1234, 5.9234) + Et que je définis un rayon de 5km + Alors la campagne cible uniquement les utilisateurs dans ce rayon + Et la zone est représentée par un cercle sur la carte + + Scénario: Ciblage géographique ville + Étant donné que je crée une campagne + Quand je sélectionne "Ville" et choisis "Marseille" + Alors la campagne cible tous les utilisateurs dans la commune de Marseille + Et les limites administratives sont affichées sur la carte + + Scénario: Ciblage géographique département + Étant donné que je crée une campagne + Quand je sélectionne "Département" et choisis "Var (83)" + Alors la campagne cible tout le département du Var + Et une estimation de population cible est affichée + + Scénario: Ciblage géographique région + Étant donné que je crée une campagne + Quand je sélectionne "Région" et choisis "Provence-Alpes-Côte d'Azur" + Alors la campagne cible toute la région PACA + Et l'estimation de population cible est mise à jour + + Scénario: Ciblage géographique national + Étant donné que je crée une campagne + Quand je sélectionne "National" + Alors la campagne cible tous les utilisateurs en France + Et aucune limite géographique n'est appliquée + + Scénario: Ciblage horaire plages multiples + Étant donné que je crée une campagne + Quand je définis les plages horaires: + | Plage | + | 7h-9h | + | 12h-14h | + | 17h-19h | + Alors la publicité est diffusée uniquement pendant ces plages + Et elle n'est jamais diffusée en dehors (ex: 10h, 15h, 20h) + + Scénario: Ciblage horaire toute la journée + Étant donné que je crée une campagne + Quand je ne définis aucune plage horaire spécifique + Alors la publicité est diffusée 24h/24 + Et aucune restriction horaire n'est appliquée + + Scénario: Ciblage par centres d'intérêt + Étant donné que je crée une campagne pour un garage automobile + Quand je sélectionne les tags: + | Tag | + | Automobile | + | Mécanique | + | Sport | + Alors la publicité est prioritaire pour les utilisateurs avec jauges élevées sur ces tags + Et elle peut quand même être diffusée à d'autres utilisateurs (ciblage non exclusif) + + Scénario: Classification d'âge obligatoire + Étant donné que je crée une campagne + Quand j'essaie de valider sans sélectionner une tranche d'âge + Alors une erreur s'affiche: "Classification d'âge obligatoire" + Et les options proposées sont: + | Option | + | Tout public | + | 13+ | + | 16+ | + | 18+ | + + Scénario: Upload audio publicitaire formats acceptés + Étant donné que je crée une campagne + Quand j'upload un fichier audio format MP3 + Alors le fichier est accepté + Quand j'upload un fichier audio format AAC (.aac ou .m4a) + Alors le fichier est accepté + Quand j'upload un fichier audio format WAV + Alors une erreur s'affiche: "Format non supporté. Utilisez MP3 ou AAC" + + Scénario: Durée audio publicitaire validée + Étant donné que je crée une campagne + Quand j'upload un audio de 8 secondes + Alors une erreur s'affiche: "Durée minimale: 10 secondes" + Quand j'upload un audio de 65 secondes + Alors une erreur s'affiche: "Durée maximale: 60 secondes" + Quand j'upload un audio de 30 secondes + Alors le fichier est accepté + + Scénario: Prépaiement obligatoire via Mangopay + Étant donné que j'ai configuré une campagne à 300€ + Quand j'arrive à l'étape de paiement + Alors je dois payer les 300€ avant validation + Et le paiement est traité via Mangopay + Et seule la carte bancaire est acceptée + + Scénario: Recharge automatique optionnelle + Étant donné que j'ai une campagne active + Quand je configure la recharge automatique à 10% du budget + Alors si le budget restant passe sous 30€ (10% de 300€) + Et que la campagne recharge automatiquement 100€ + Et ma carte bancaire est débitée de 100€ + Et le budget total passe à 130€ + + Scénario: Désactivation recharge automatique + Étant donné que j'ai activé la recharge automatique + Quand je désactive cette option + Alors aucune recharge ne se produit automatiquement + Et la campagne s'arrête quand le budget atteint 0€ + + Scénario: Étalement budget sur période longue + Étant donné une campagne avec: + | Budget total | 1000€ | + | Durée | 30 j | + Quand le système calcule l'étalement + Alors le budget/jour est de 33.33€ + Et si le budget se consomme plus vite (ex: 50€/jour) + Alors une alerte "Budget épuisé dans 10 jours" est envoyée + + Scénario: Estimation population cible selon zone + Étant donné que je sélectionne la zone "Marseille" + Quand le système calcule la population cible + Alors l'estimation affichée est "~15 000 utilisateurs potentiels" + Et un message "Estimation basée sur utilisateurs actifs dans la zone" s'affiche + + Scénario: Campagne avec date de début différée + Étant donné que je crée une campagne + Quand je définis la date de début au 2026-03-01 (dans 1 mois) + Alors la campagne a le statut "Programmée" + Et elle démarre automatiquement le 2026-03-01 à 00h00 + Et le budget n'est pas consommé avant cette date + + Scénario: Interface self-service accessible + Étant donné que je suis un publicitaire + Quand j'accède à l'interface publicitaire + Alors je peux créer une campagne sans contact commercial RoadWave + Et toutes les options sont configurables en autonomie + Et un tutoriel guidé est disponible (première utilisation) + + Scénario: Aperçu zone ciblée sur carte interactive + Étant donné que je configure une zone géographique + Quand je sélectionne "Département du Var" + Alors une carte Leaflet affiche les limites du département en surbrillance + Et un compteur "~50 000 utilisateurs actifs" est affiché + Et je peux zoomer/dézoomer pour visualiser la zone + + Scénario: Tags multiples pour ciblage affiné + Étant donné que je crée une campagne pour un restaurant + Quand je sélectionne les tags: + | Tag | + | Gastronomie | + | Tourisme | + | Famille | + Alors la publicité est prioritaire pour utilisateurs intéressés par ces 3 thèmes + Et le score de ciblage combine les 3 jauges d'intérêt + + Scénario: Validation des dates de campagne + Étant donné que je crée une campagne + Quand je définis une date de début postérieure à la date de fin + Alors une erreur s'affiche: "Date de fin doit être après date de début" + Et la campagne n'est pas créée + + Scénario: Durée minimale de campagne + Étant donné que je crée une campagne + Quand je définis une durée de moins de 24 heures + Alors une erreur s'affiche: "Durée minimale: 1 jour" + Et je dois ajuster les dates + + Scénario: Durée maximale de campagne + Étant donné que je crée une campagne + Quand je définis une durée de plus de 90 jours + Alors une erreur s'affiche: "Durée maximale: 90 jours" + Et je dois ajuster les dates ou créer plusieurs campagnes + + Plan du Scénario: Calcul budget quotidien selon durée + Étant donné une campagne avec un budget de € + Quand la durée est de jours + Alors le budget quotidien est de €/jour + + Exemples: + | budget | duree | budget_jour | + | 100 | 10 | 10.00 | + | 300 | 14 | 21.43 | + | 500 | 30 | 16.67 | + | 1000 | 60 | 16.67 | + + Plan du Scénario: Estimation diffusions selon budget + Étant donné un budget quotidien de € + Quand le coût par écoute complète est 0.05€ + Alors le nombre estimé de diffusions/jour est + + Exemples: + | budget_jour | diffusions | + | 10.00 | 200 | + | 21.43 | 429 | + | 50.00 | 1000 | + | 100.00 | 2000 | + + Plan du Scénario: Formats audio acceptés/rejetés + Étant donné que j'upload un fichier + Quand le format est + Alors le résultat est + + Exemples: + | fichier | format | resultat | + | pub.mp3 | MP3 | accepté | + | pub.aac | AAC | accepté | + | pub.m4a | AAC | accepté | + | pub.wav | WAV | rejeté | + | pub.ogg | OGG | rejeté | + | pub.flac | FLAC | rejeté | diff --git a/features/publicites/caracteristiques-pub.feature b/features/publicites/caracteristiques-pub.feature new file mode 100644 index 0000000..03fe96f --- /dev/null +++ b/features/publicites/caracteristiques-pub.feature @@ -0,0 +1,288 @@ +# language: fr +Fonctionnalité: Caractéristiques et facturation des publicités + En tant que système RoadWave + Je veux appliquer des règles précises de durée, skippabilité et facturation + Afin d'équilibrer expérience utilisateur et rentabilité publicitaire + + Contexte: + Étant donné que l'API RoadWave est disponible + Et qu'un utilisateur gratuit écoute du contenu + + Scénario: Durée minimale 10 secondes + Étant donné qu'un publicitaire uploade une publicité de 8 secondes + Quand le système valide la durée + Alors une erreur s'affiche: "Durée minimale: 10 secondes" + Et l'upload est rejeté + + Scénario: Durée maximale 60 secondes + Étant donné qu'un publicitaire uploade une publicité de 65 secondes + Quand le système valide la durée + Alors une erreur s'affiche: "Durée maximale: 60 secondes" + Et l'upload est rejeté + + Scénario: Durée recommandée 15-30 secondes + Étant donné qu'un publicitaire crée une campagne + Quand il voit les recommandations + Alors un message s'affiche: + """ + 💡 Durée recommandée: 15-30 secondes + + Les publicités de cette durée ont: + - Taux d'écoute complète: 45% (vs 35% pour 60s) + - Meilleur engagement utilisateur + - Coût par écoute optimisé + """ + + Scénario: Publicité de 10 secondes acceptée + Étant donné qu'un publicitaire uploade une publicité de 10 secondes + Quand le système valide la durée + Alors le fichier est accepté + Et aucune erreur n'est affichée + + Scénario: Publicité de 60 secondes acceptée + Étant donné qu'un publicitaire uploade une publicité de 60 secondes + Quand le système valide la durée + Alors le fichier est accepté + Et un avertissement s'affiche: "⚠️ Durée longue: taux de skip potentiellement élevé" + + Scénario: Délai minimum skippable 5 secondes par défaut + Étant donné qu'une publicité de 30 secondes démarre + Et que le délai minimal est configuré à 5 secondes + Quand j'écoute pendant 3 secondes + Alors le bouton "Passer" n'est pas visible + Et je dois attendre 2 secondes supplémentaires + Quand j'atteins 5 secondes d'écoute + Alors le bouton "Passer" apparaît + Et je peux cliquer pour passer au contenu suivant + + Scénario: Délai minimum paramétrable admin (3 secondes) + Étant donné que l'admin configure le délai à 3 secondes + Et qu'une publicité démarre + Quand j'écoute pendant 3 secondes + Alors le bouton "Passer" apparaît immédiatement + Et je peux skipper + + Scénario: Délai minimum paramétrable admin (10 secondes) + Étant donné que l'admin configure le délai à 10 secondes + Et qu'une publicité démarre + Quand j'écoute pendant 9 secondes + Alors le bouton "Passer" n'est toujours pas visible + Quand j'atteins 10 secondes + Alors le bouton "Passer" apparaît + + Scénario: Facturation écoute complète (>80%) - 0.05€ + Étant donné qu'une publicité de 30 secondes est diffusée + Quand j'écoute pendant 25 secondes (83%) + Alors l'écoute est considérée comme "complète" + Et le publicitaire est facturé 0.05€ + Et le compteur "écoutes complètes" s'incrémente + + Scénario: Facturation écoute complète exactement 80% + Étant donné qu'une publicité de 30 secondes est diffusée + Quand j'écoute pendant exactement 24 secondes (80%) + Alors l'écoute est considérée comme "complète" + Et le publicitaire est facturé 0.05€ + + Scénario: Facturation skip après délai minimal - 0.02€ + Étant donné qu'une publicité de 30 secondes est diffusée + Et que le délai minimal est 5 secondes + Quand j'écoute pendant 10 secondes (33%) + Et que je clique sur "Passer" + Alors l'écoute est considérée comme "partielle" + Et le publicitaire est facturé 0.02€ + Car il y a eu une exposition partielle + + Scénario: Facturation skip immédiat (<5s) - 0€ + Étant donné qu'une publicité de 30 secondes est diffusée + Et que le délai minimal est 5 secondes + Quand j'écoute pendant 3 secondes + Et que je clique sur "Suivant" (pas de bouton skip encore) + Alors l'écoute est considérée comme "non engagée" + Et le publicitaire n'est PAS facturé (0€) + Car l'exposition est trop courte + + Scénario: Comptabilisation écoute complète à 79% + Étant donné qu'une publicité de 30 secondes est diffusée + Quand j'écoute pendant 23 secondes (77%) + Alors l'écoute est considérée comme "partielle" (pas complète) + Et le publicitaire est facturé 0.02€ + Car le seuil 80% n'est pas atteint + + Scénario: Comptabilisation écoute complète à 100% + Étant donné qu'une publicité de 30 secondes est diffusée + Quand j'écoute les 30 secondes complètes (100%) + Alors l'écoute est considérée comme "complète" + Et le publicitaire est facturé 0.05€ + + Scénario: Budget consommé selon mix écoutes + Étant donné qu'une campagne à 300€ a généré: + | Type écoute | Nombre | Coût unitaire | Total | + | Complète (>80%) | 4000 | 0.05€ | 200€ | + | Partielle (5-80%) | 2000 | 0.02€ | 40€ | + | Skip immédiat | 1000 | 0€ | 0€ | + Quand je calcule le budget consommé + Alors le total est 240€ + Et il reste 60€ de budget disponible + + Scénario: Affichage compteur secondes restantes + Étant donné qu'une publicité de 30s démarre + Et que le délai minimal est 5s + Quand j'écoute pendant 2 secondes + Alors un compteur s'affiche: "Passer dans 3s..." + Quand j'atteins 5 secondes + Alors le compteur disparaît + Et le bouton "Passer la publicité" s'affiche + + Scénario: Progress bar publicité visible + Étant donné qu'une publicité de 30s est en lecture + Quand 10 secondes se sont écoulées + Alors la progress bar affiche 33% (10/30) + Et l'indicateur temporel affiche "0:10 / 0:30" + Et l'utilisateur visualise la progression + + Scénario: Message "Publicité" clairement affiché + Étant donné qu'une publicité démarre + Quand l'audio commence + Alors un badge "Publicité" est affiché en haut de l'écran + Et la durée totale est indiquée: "Publicité (30s)" + Et la transparence est maximale (utilisateur sait que c'est une pub) + + Scénario: Transition fluide après publicité + Étant donné qu'une publicité de 30s se termine + Quand la lecture atteint 30 secondes + Alors le délai de transition de 2s démarre + Et le contenu normal suivant est annoncé + Et l'enchaînement est naturel (même UX que entre contenus) + + Scénario: Like autorisé sur publicité + Étant donné qu'une publicité est en lecture + Et que le véhicule est à l'arrêt + Quand je clique sur le bouton cœur + Alors un like explicite (+2%) est enregistré + Et mes jauges d'intérêt sont mises à jour selon les tags de la pub + Et le publicitaire voit un compteur "Likes" incrémenté + + Scénario: Abonnement autorisé sur publicité + Étant donné qu'une publicité est diffusée par un créateur + Et que le véhicule est à l'arrêt + Quand je clique sur "S'abonner" + Alors l'abonnement est enregistré (+5% jauges) + Et le publicitaire bénéficie de l'engagement fort + Et cela compte comme une conversion majeure + + Scénario: Bouton skip visible et accessible + Étant donné qu'une publicité a dépassé le délai minimal + Quand le bouton "Passer" s'affiche + Alors il est positionné en bas à droite de l'écran + Et il a une taille de clic confortable (44×44px minimum iOS) + Et il est clairement visible (contraste élevé) + + Scénario: Analytics tracking précis par type + Étant donné qu'une publicité est diffusée + Quand un événement se produit + Alors il est tracké en temps réel: + | Événement | Données enregistrées | + | Impression | timestamp, user_id, pub_id, zone_geo | + | Écoute complète | durée_ecoutee, pourcentage, coût (0.05€) | + | Skip après délai | durée_ecoutee, pourcentage, coût (0.02€) | + | Skip immédiat | durée_ecoutee, pourcentage, coût (0€) | + | Like | timestamp, tags impactés | + | Abonnement | timestamp, creator_id | + + Scénario: Recommandation sweet spot 15-30s + Étant donné les statistiques RoadWave globales: + | Durée pub | Taux complétion moyen | + | 10s | 65% | + | 15s | 55% | + | 30s | 45% | + | 45s | 30% | + | 60s | 20% | + Quand un publicitaire consulte les recommandations + Alors le sweet spot affiché est "15-30 secondes" + Et l'explication est "Meilleur compromis engagement/message" + + Scénario: Optimisation durée selon taux de skip campagne + Étant donné qu'une campagne de 60s a un taux de skip de 85% + Quand le publicitaire consulte les recommandations + Alors le système suggère: + """ + ⚠️ Taux de skip élevé (85%) + + Recommandation: Réduire la durée à 30s maximum + Impact estimé: Taux de skip attendu 55% (-30%) + """ + + Scénario: Coût effectif moyen (CEM) calculé + Étant donné une campagne avec: + | Type écoute | Nombre | Coût unitaire | Total | + | Complète | 2000 | 0.05€ | 100€ | + | Partielle | 3000 | 0.02€ | 60€ | + | Skip immédiat | 1000 | 0€ | 0€ | + Quand je calcule le coût effectif moyen + Alors CEM = 160€ / 6000 impressions = 0.027€/impression + Et cette métrique aide à comparer avec CPM industrie + + Scénario: Publicité non skippable interdite + Étant donné qu'un publicitaire demande "Publicité non skippable" + Quand il configure sa campagne + Alors cette option n'existe pas + Et toutes les publicités sont obligatoirement skippables après 5s minimum + Car l'expérience utilisateur est prioritaire + + Scénario: Délai minimal jamais <3 secondes + Étant donné qu'un admin essaie de configurer le délai à 2 secondes + Quand il valide le paramètre + Alors une erreur s'affiche: "Délai minimal: 3 secondes minimum" + Car un délai trop court dégrade trop l'expérience + + Scénario: Délai minimal jamais >10 secondes + Étant donné qu'un admin essaie de configurer le délai à 15 secondes + Quand il valide le paramètre + Alors une erreur s'affiche: "Délai maximal: 10 secondes maximum" + Car un délai trop long réduit les revenus publicitaires + + Plan du Scénario: Facturation selon durée écoutée + Étant donné qu'une publicité de 30s est diffusée + Quand j'écoute pendant s (%) + Alors le type d'écoute est + Et le coût facturé est € + + Exemples: + | duree | pourcentage | type | cout | + | 3 | 10 | skip immédiat| 0 | + | 5 | 17 | partielle | 0.02 | + | 10 | 33 | partielle | 0.02 | + | 20 | 67 | partielle | 0.02 | + | 24 | 80 | complète | 0.05 | + | 27 | 90 | complète | 0.05 | + | 30 | 100 | complète | 0.05 | + + Plan du Scénario: Budget consommé selon distribution écoutes + Étant donné écoutes complètes à 0.05€ + Et écoutes partielles à 0.02€ + Et skips immédiats à 0€ + Quand je calcule le budget total consommé + Alors le résultat est € + + Exemples: + | completes | partielles | skips | budget_total | + | 1000 | 500 | 100 | 60 | + | 2000 | 1000 | 500 | 120 | + | 5000 | 2000 | 1000 | 290 | + | 0 | 1000 | 0 | 20 | + | 1000 | 0 | 0 | 50 | + + Plan du Scénario: Apparition bouton skip selon délai configuré + Étant donné que le délai minimal est configuré à s + Quand j'écoute pendant s + Alors le bouton "Passer" est + + Exemples: + | delai | temps_ecoute | visible | + | 5 | 3 | non visible | + | 5 | 5 | visible | + | 5 | 10 | visible | + | 10 | 8 | non visible | + | 10 | 10 | visible | + | 3 | 2 | non visible | + | 3 | 3 | visible | diff --git a/features/publicites/gestion-budget-pub.feature b/features/publicites/gestion-budget-pub.feature new file mode 100644 index 0000000..f702d27 --- /dev/null +++ b/features/publicites/gestion-budget-pub.feature @@ -0,0 +1,361 @@ +# language: fr +Fonctionnalité: Gestion du budget et alertes publicitaires + En tant que publicitaire + Je veux suivre en temps réel mon budget et recevoir des alertes + Afin de maîtriser mes dépenses et optimiser mes campagnes + + Contexte: + Étant donné que l'API RoadWave est disponible + Et qu'un compte publicitaire est connecté + Et qu'une campagne active est en cours + + Scénario: Dashboard budget temps réel + Étant donné que ma campagne a un budget de 300€ + Et que j'ai consommé 220€ + Quand je consulte le dashboard budget + Alors je vois: + | Métrique | Valeur | + | Budget total | 300€ | + | Budget consommé | 220€ | + | Budget restant | 80€ | + | Pourcentage | 73% consommé | + + Scénario: Jauge visuelle budget consommé + Étant donné que j'ai consommé 220€ sur 300€ + Quand je consulte le dashboard + Alors une jauge de progression affiche 73% + Et la couleur est orange (seuil 50-80%) + Et un indicateur "80€ restants" est affiché clairement + + Scénario: Couleur jauge selon seuil + Étant donné un budget de 300€ + Quand j'ai consommé 150€ (50%) + Alors la jauge est verte + Quand j'ai consommé 240€ (80%) + Alors la jauge est orange + Quand j'ai consommé 285€ (95%) + Alors la jauge est rouge + Et un message "Budget presque épuisé" s'affiche + + Scénario: Projection épuisement budget + Étant donné que j'ai consommé 220€ en 10 jours + Et qu'il reste 4 jours de campagne + Quand le système calcule la projection + Alors la consommation quotidienne moyenne est 22€/jour + Et la projection affiche "Budget épuisé dans 3.6 jours" + Et un avertissement "Campagne s'arrêtera avant la fin prévue" s'affiche + + Scénario: Projection avec budget suffisant + Étant donné que j'ai consommé 100€ en 10 jours + Et qu'il reste 4 jours de campagne + Et que le budget total est 300€ + Quand le système calcule la projection + Alors la consommation quotidienne moyenne est 10€/jour + Et la projection affiche "Budget suffisant pour toute la campagne" + Et le budget restant estimé à la fin est 160€ + + Scénario: Alerte 80% budget consommé + Étant donné que mon budget est de 300€ + Quand je consomme 240€ (80%) + Alors je reçois immédiatement un email: + """ + ⚠️ Alerte Budget - 80% consommé + + Campagne: [Nom] + Budget consommé: 240€ / 300€ + Budget restant: 60€ + + À ce rythme, votre budget sera épuisé dans X jours. + Recommandation: Envisagez une recharge pour maintenir la diffusion. + """ + Et une notification push est envoyée + Et une notification in-app s'affiche + + Scénario: Alerte 90% budget consommé + Étant donné que mon budget est de 300€ + Quand je consomme 270€ (90%) + Alors je reçois immédiatement un email: + """ + 🚨 Alerte Budget - 90% consommé + + Campagne: [Nom] + Budget consommé: 270€ / 300€ + Budget restant: 30€ + + ATTENTION: Votre campagne s'arrêtera sous peu. + Actions possibles: + - Recharger le budget + - Laisser la campagne se terminer + """ + + Scénario: Alerte budget épuisé (100%) + Étant donné que mon budget est de 300€ + Quand je consomme les 300€ (100%) + Alors je reçois immédiatement un email: + """ + ✅ Campagne terminée - Budget épuisé + + Campagne: [Nom] + Budget consommé: 300€ + Durée effective: 12 jours (sur 14 prévus) + + Métriques finales: + - Impressions: 6000 + - Écoutes complètes: 4000 (67%) + - Coût par écoute: 0.05€ + + Voir le rapport complet: [Lien] + Créer une nouvelle campagne: [Lien] + """ + Et la campagne est automatiquement mise en pause + Et plus aucune diffusion ne se produit + + Scénario: Pause manuelle de campagne + Étant donné que ma campagne est active + Et qu'il reste 150€ de budget + Quand je clique sur "Mettre en pause" + Alors le statut passe à "En pause" + Et les diffusions s'arrêtent immédiatement + Et le budget de 150€ est conservé + Et je peux réactiver la campagne plus tard + + Scénario: Reprise campagne pausée + Étant donné que ma campagne est en pause + Et qu'il reste 150€ de budget + Quand je clique sur "Reprendre la campagne" + Alors le statut passe à "Active" + Et les diffusions reprennent immédiatement + Et le budget restant de 150€ continue de se consommer + + Scénario: Prolongation campagne avec recharge + Étant donné que ma campagne se termine dans 2 jours + Et qu'il reste 20€ de budget + Quand je clique sur "Prolonger la campagne" + Et que j'ajoute 200€ supplémentaires + Alors le budget total passe à 220€ + Et la date de fin peut être prolongée de 10 jours + Et un nouveau paiement Mangopay de 200€ est traité + + Scénario: Recharge automatique activée + Étant donné que j'ai configuré la recharge automatique + Et que le seuil est fixé à 10% (30€ sur budget 300€) + Et que le montant de recharge est 100€ + Quand le budget restant passe sous 30€ + Alors une recharge automatique de 100€ est déclenchée + Et ma carte bancaire est débitée via Mangopay + Et le budget total passe à budget_restant + 100€ + Et je reçois un email de confirmation + + Scénario: Échec recharge automatique (carte expirée) + Étant donné que la recharge automatique est activée + Et que ma carte bancaire a expiré + Quand le budget passe sous le seuil de 10% + Alors la recharge automatique échoue + Et je reçois un email urgent: + """ + ❌ Échec recharge automatique + + Votre carte bancaire a été refusée. + Raison probable: carte expirée ou fonds insuffisants + + Action requise: Mettez à jour vos informations de paiement + Campagne en pause si budget épuisé: [Lien] + """ + Et la campagne continue jusqu'à épuisement du budget restant + + Scénario: Modification ciblage si budget <50% consommé + Étant donné que j'ai consommé 120€ sur 300€ (40%) + Quand j'essaie de modifier le ciblage géographique + Alors la modification est autorisée + Et le ciblage est mis à jour immédiatement + Et les nouvelles diffusions utilisent le nouveau ciblage + + Scénario: Blocage modification ciblage si budget >50% consommé + Étant donné que j'ai consommé 180€ sur 300€ (60%) + Quand j'essaie de modifier le ciblage géographique + Alors une erreur s'affiche: + """ + Modification bloquée - Budget >50% consommé + + Les modifications de ciblage ne sont autorisées que si moins de 50% du budget est consommé. + Raison: Cohérence des métriques et analytics. + + Options: + - Créer une nouvelle campagne avec le nouveau ciblage + - Laisser cette campagne se terminer + """ + + Scénario: Modification audio nécessite nouvelle validation + Étant donné que ma campagne est active + Quand je veux modifier le fichier audio + Alors un message s'affiche: + """ + Modification audio = Nouvelle validation + + Changer l'audio nécessite une nouvelle validation manuelle (24-48h). + Votre campagne sera mise en pause pendant la validation. + + Recommandation: Créer une nouvelle campagne pour tester un nouveau créatif. + """ + + Scénario: Modification plages horaires autorisée + Étant donné que ma campagne cible 7h-9h et 17h-19h + Quand je modifie pour cibler 12h-14h aussi + Alors la modification est appliquée immédiatement + Et les diffusions suivantes incluent la nouvelle plage + Et aucune re-validation n'est nécessaire + + Scénario: Historique consommation budget jour par jour + Étant donné que ma campagne a duré 10 jours + Quand je consulte l'historique + Alors je vois un graphique avec: + | Jour | Consommation | Cumulé | + | 1 | 22€ | 22€ | + | 2 | 25€ | 47€ | + | 3 | 20€ | 67€ | + | ... | ... | ... | + | 10 | 18€ | 220€ | + Et je peux identifier les pics de consommation + + Scénario: Notification fin de campagne programmée + Étant donné que ma campagne se termine le 14/02 + Quand la date de fin est atteinte + Alors je reçois un email: + """ + ✅ Campagne terminée - Date de fin atteinte + + Campagne: [Nom] + Durée: 14 jours (comme prévu) + Budget consommé: 280€ / 300€ + Budget non utilisé: 20€ + + Métriques finales: + - Impressions: 5600 + - Écoutes complètes: 3800 (68%) + - ROI: [Calcul si applicable] + + Le budget non utilisé (20€) sera remboursé sous 5-7 jours. + """ + + Scénario: Remboursement budget non utilisé + Étant donné que ma campagne avait 300€ de budget + Et qu'elle s'est terminée avec 280€ consommés + Quand la campagne se termine (date ou épuisement) + Alors un remboursement de 20€ est initié via Mangopay + Et le délai est de 5-7 jours ouvrés + Et je reçois une notification de confirmation + + Scénario: Aucun remboursement si budget entièrement consommé + Étant donné que ma campagne avait 300€ de budget + Et qu'elle s'est terminée avec 300€ consommés + Quand la campagne se termine + Alors aucun remboursement n'est initié + Et le message final indique "Budget entièrement utilisé" + + Scénario: Statistiques comparatives budget vs objectif + Étant donné que j'avais défini un objectif de 5000 impressions + Et que mon budget était 300€ + Quand je consulte les statistiques finales + Alors je vois: + | Métrique | Objectif | Réalisé | Écart | + | Impressions | 5000 | 6000 | +20% | + | Budget | 300€ | 280€ | -7% | + | Coût/impression | 0.06€ | 0.047€ | -22% | + Et une analyse "✅ Objectifs dépassés avec budget optimisé" + + Scénario: Export rapport financier détaillé + Étant donné que je veux analyser mes dépenses + Quand je clique sur "Exporter rapport financier" + Alors je télécharge un CSV avec: + | Colonne | + | Date/Heure | + | Type écoute | + | Coût unitaire | + | Zone géographique | + | Utilisateur (anonyme)| + | Durée écoutée | + Et je peux l'importer dans Excel pour analyses + + Scénario: Tableau de bord multi-campagnes + Étant donné que j'ai 3 campagnes actives + Quand je consulte la vue d'ensemble + Alors je vois un tableau récapitulatif: + | Campagne | Budget | Consommé | % | Jours restants | Projection | + | A | 300€ | 220€ | 73| 4j | Suffisant | + | B | 500€ | 480€ | 96| 10j | Épuisé 2j | + | C | 200€ | 50€ | 25| 20j | Suffisant | + Et un badge alerte rouge sur la campagne B + + Scénario: Alerte consolidée multi-campagnes + Étant donné que j'ai 5 campagnes actives + Et que 2 campagnes ont >80% budget consommé + Quand je reçois les notifications + Alors un email consolidé unique est envoyé: + """ + ⚠️ Alerte Budget - 2 campagnes nécessitent attention + + Campagne A: 85% consommé, épuisement dans 2j + Campagne C: 92% consommé, épuisement dans 1j + + Actions recommandées: [Liens] + """ + Et je ne reçois pas 2 emails séparés (évite spam) + + Scénario: Configuration seuils alertes personnalisés + Étant donné que je configure mes préférences d'alerte + Quand je définis les seuils: + | Seuil | Valeur | + | Alerte 1 | 70% | + | Alerte 2 | 85% | + | Alerte 3 | 95% | + Alors je reçois des alertes à 70%, 85% et 95% + Et non aux seuils par défaut 80%, 90%, 100% + + Scénario: Désactivation alertes email + Étant donné que je préfère uniquement les notifications in-app + Quand je désactive les alertes email dans mes préférences + Alors je ne reçois plus d'emails d'alerte budget + Mais les notifications in-app continuent + Et les alertes critiques (échec paiement) sont toujours envoyées par email + + Plan du Scénario: Couleur jauge selon pourcentage consommé + Étant donné un budget de 300€ + Quand j'ai consommé € (%) + Alors la couleur de la jauge est + + Exemples: + | montant | pourcentage | couleur | + | 100 | 33 | verte | + | 150 | 50 | verte | + | 180 | 60 | orange | + | 240 | 80 | orange | + | 270 | 90 | rouge | + | 285 | 95 | rouge | + | 300 | 100 | rouge | + + Plan du Scénario: Projection épuisement selon consommation + Étant donné un budget de 300€ + Et une consommation actuelle de € + Et une durée écoulée de jours + Quand je calcule la consommation quotidienne moyenne + Alors elle est de €/jour + Et le budget sera épuisé dans jours + + Exemples: + | consomme | jours_ecoules | conso_jour | jours_restants | + | 100 | 5 | 20 | 10 | + | 200 | 10 | 20 | 5 | + | 150 | 10 | 15 | 10 | + | 270 | 12 | 22.5 | 1.3 | + + Plan du Scénario: Alertes envoyées selon seuils + Étant donné un budget de 500€ + Quand je consomme € (%) + Alors je reçois une alerte + + Exemples: + | montant | pourcentage | niveau | + | 350 | 70 | aucune | + | 400 | 80 | alerte 80% | + | 450 | 90 | alerte 90% | + | 500 | 100 | budget épuisé| diff --git a/features/publicites/insertion-frequence-pub.feature b/features/publicites/insertion-frequence-pub.feature new file mode 100644 index 0000000..725e91d --- /dev/null +++ b/features/publicites/insertion-frequence-pub.feature @@ -0,0 +1,271 @@ +# language: fr +Fonctionnalité: Insertion et fréquence des publicités + En tant que système RoadWave + Je veux insérer les publicités de manière équilibrée et non intrusive + Afin de préserver l'expérience utilisateur tout en monétisant + + Contexte: + Étant donné que l'API RoadWave est disponible + Et qu'un utilisateur gratuit est connecté + + Scénario: Fréquence par défaut 1 pub / 5 contenus + Étant donné que la fréquence par défaut est configurée à 1/5 + Et que je suis un utilisateur gratuit + Quand j'écoute 5 contenus + Alors 1 publicité est insérée après le 5ème contenu + Quand j'écoute 10 contenus + Alors 2 publicités sont insérées (après les contenus 5 et 10) + + Scénario: Aucune publicité pour utilisateurs Premium + Étant donné que je suis un utilisateur Premium + Quand j'écoute 100 contenus + Alors aucune publicité n'est insérée + Et je bénéficie d'une expérience sans interruption publicitaire + + Scénario: Fréquence paramétrable par admin (1/3) + Étant donné que l'admin configure la fréquence à 1/3 + Et que je suis un utilisateur gratuit + Quand j'écoute 6 contenus + Alors 2 publicités sont insérées (après contenus 3 et 6) + + Scénario: Fréquence paramétrable par admin (1/10) + Étant donné que l'admin configure la fréquence à 1/10 + Et que je suis un utilisateur gratuit + Quand j'écoute 20 contenus + Alors 2 publicités sont insérées (après contenus 10 et 20) + + Scénario: Jamais d'interruption d'un contenu en cours + Étant donné que j'écoute un contenu de 10 minutes + Et que je suis à 5 minutes de lecture + Et qu'une publicité devrait être insérée selon la fréquence + Quand le système vérifie l'insertion + Alors la publicité attend la fin du contenu actuel + Et elle s'insère pendant le délai de transition (2s) + Et le contenu n'est jamais interrompu + + Scénario: Insertion entre deux contenus uniquement + Étant donné que le contenu "A" se termine + Et que le délai de transition de 2s démarre + Quand le système détecte qu'une publicité doit être insérée + Alors le message "Publicité (30s)" s'affiche + Et la publicité démarre après les 2 secondes + Et l'enchaînement est naturel et fluide + + Scénario: Rotation limite 3 fois/jour par utilisateur + Étant donné qu'un utilisateur a entendu la publicité "A" 3 fois aujourd'hui + Quand le système sélectionne une nouvelle publicité à diffuser + Alors la publicité "A" n'est plus éligible pour cet utilisateur aujourd'hui + Et une autre publicité "B" est sélectionnée + Et cela évite la saturation publicitaire + + Scénario: Compteur de diffusions par pub et par utilisateur + Étant donné qu'un utilisateur écoute la pub "RestaurantX" + Quand la diffusion se termine + Alors un compteur Redis "pub:RestaurantX:user:123:count" s'incrémente + Et le TTL est de 24h (reset à minuit) + Quand le compteur atteint 3 + Alors la pub "RestaurantX" est exclue des prochaines sélections aujourd'hui + + Scénario: Limite max 6 pubs/heure par utilisateur + Étant donné qu'un utilisateur a entendu 6 publicités dans la dernière heure + Quand le système devrait insérer une 7ème pub + Alors l'insertion est reportée à l'heure suivante + Et un compteur horaire Redis "pub:user:123:hourly" est vérifié + Et cela évite le spam publicitaire + + Scénario: Ciblage géographique prioritaire - Point GPS + Étant donné qu'une publicité cible un point GPS à 2km de ma position + Et qu'une autre publicité cible ma ville entière + Quand le système sélectionne une publicité + Alors la publicité point GPS est priorisée (score géo plus élevé) + Et le ciblage précis est favorisé + + Scénario: Ciblage géographique prioritaire - Hiérarchie + Étant donné que 4 publicités sont éligibles: + | Publicité | Zone | Distance | + | A | Point GPS | 1km | + | B | Ville | 0km | + | C | Département | 0km | + | D | National | N/A | + Quand le système sélectionne selon priorité géographique + Alors l'ordre de priorité est: A > B > C > D + Et la publicité A (Point GPS, la plus précise) est diffusée + + Scénario: Ciblage centres d'intérêt secondaire + Étant donné que 2 publicités ciblent ma zone géographique: + | Publicité | Tags | Mes jauges | + | A | Automobile | 80% | + | B | Voyage | 40% | + Quand le système applique le score centres d'intérêt + Alors la publicité A est favorisée (meilleur match jauges) + Et le ciblage thématique affine la sélection + + Scénario: Ciblage horaire strict + Étant donné qu'une campagne cible uniquement 7h-9h + Et qu'il est 10h30 + Quand le système sélectionne une publicité + Alors cette campagne n'est PAS éligible + Et seules les campagnes "toute la journée" ou avec plage horaire actuelle sont considérées + + Scénario: Ciblage horaire pendant plage active + Étant donné qu'une campagne cible 7h-9h et 17h-19h + Et qu'il est 8h15 + Quand le système sélectionne une publicité + Alors cette campagne est éligible + Et elle peut être diffusée + + Scénario: Normalisation volume audio -14 LUFS + Étant donné qu'une publicité est uploadée avec volume trop élevé (-6 LUFS) + Quand le système encode l'audio via FFmpeg + Alors le volume est normalisé automatiquement à -14 LUFS + Et le publicitaire reçoit une notification "Volume audio ajusté pour conformité" + Et cela évite l'effet "pub trop forte" frustrant + + Scénario: Validation volume audio lors encodage + Étant donné qu'une publicité est soumise + Quand FFmpeg encode le fichier + Alors une commande loudnorm est appliquée: + ``` + ffmpeg -i input.mp3 -filter:a loudnorm=I=-14:LRA=11:TP=-1 output.mp3 + ``` + Et le fichier final respecte le standard broadcast -14 LUFS + + Scénario: Sélection aléatoire si critères équivalents + Étant donné que 3 publicités ont le même score géo + Et qu'elles ont toutes des jauges centres d'intérêt équivalentes + Et qu'aucune n'a été diffusée 3 fois aujourd'hui + Quand le système sélectionne une publicité + Alors une sélection aléatoire équitable est faite + Et chaque campagne a 33% de chances d'être diffusée + + Scénario: Exclusion publicités avec budget épuisé + Étant donné qu'une campagne "A" a épuisé son budget + Et qu'une campagne "B" a encore du budget disponible + Quand le système sélectionne une publicité + Alors seule la campagne "B" est éligible + Et la campagne "A" est automatiquement exclue + + Scénario: Exclusion publicités hors dates de campagne + Étant donné qu'une campagne "A" est programmée du 01/02 au 14/02 + Et que nous sommes le 20/01 + Quand le système sélectionne une publicité + Alors la campagne "A" n'est pas éligible + Et seules les campagnes actives aujourd'hui sont considérées + + Scénario: Publicité visible uniquement dans zone géographique + Étant donné qu'une publicité cible "Marseille uniquement" + Et que je suis à Lyon + Quand le système sélectionne une publicité + Alors cette publicité n'est jamais éligible pour moi + Et je ne la verrai jamais tant que je reste à Lyon + + Scénario: Tracking compteur horaire avec TTL + Étant donné qu'un utilisateur entend une pub à 10h05 + Quand le compteur horaire est incrémenté + Alors la clé Redis "pub:user:123:hourly:2026012110" est créée + Et le TTL est de 1 heure (expire à 11h05) + Et le système compte les pubs dans la fenêtre glissante d'1h + + Scénario: Reset compteur quotidien à minuit + Étant donné qu'un utilisateur a entendu la pub "A" 3 fois le 20/01 + Quand minuit passe et on est le 21/01 + Alors le compteur "pub:A:user:123:count" est expiré (TTL 24h) + Et l'utilisateur peut à nouveau entendre la pub "A" jusqu'à 3 fois + + Scénario: Aucune pub si aucune campagne éligible + Étant donné qu'aucune campagne n'a de budget disponible + Ou que toutes les campagnes ont déjà été diffusées 3 fois aujourd'hui + Quand le système devrait insérer une publicité + Alors aucune pub n'est insérée + Et l'enchaînement de contenus continue normalement + Et le prochain contenu démarre directement + + Scénario: Priorisation campagnes avec budget important restant + Étant donné que 2 campagnes sont éligibles: + | Campagne | Budget restant | Jours restants | + | A | 500€ | 2j | + | B | 50€ | 10j | + Quand le système applique la priorisation budgétaire + Alors la campagne A est légèrement favorisée (urgence dépense) + Et cela aide à épuiser les budgets avant fin de campagne + + Scénario: Log des sélections pour analytics + Étant donné qu'une publicité "RestaurantX" est sélectionnée + Quand elle est diffusée à l'utilisateur "123" + Alors un événement est loggé en base: + | Champ | Valeur | + | pub_id | RestaurantX | + | user_id | 123 | + | timestamp | 2026-01-21 10:30 | + | zone_geo | Marseille | + | score_geo | 0.85 | + | score_interet | 0.70 | + Et cela permet l'analytics publicitaire + + Scénario: Détection changement statut utilisateur (gratuit → premium) + Étant donné que je suis un utilisateur gratuit + Et que j'entends des publicités + Quand je souscris à Premium + Alors le système détecte le changement de statut immédiatement + Et plus aucune publicité n'est insérée dès le prochain contenu + Et mon expérience devient sans pub instantanément + + Scénario: Interface admin pour ajuster fréquence globale + Étant donné que je suis admin RoadWave + Quand j'accède aux paramètres publicitaires + Alors je peux ajuster le curseur de fréquence: + | Option | Fréquence | + | 1/3 | Haute (agressif) | + | 1/5 | Standard (défaut)| + | 1/7 | Modérée | + | 1/10 | Faible | + Et le changement s'applique en temps réel à tous les utilisateurs + + Scénario: A/B testing fréquence sur cohortes utilisateurs + Étant donné que l'admin active un test A/B + Quand 50% des utilisateurs ont fréquence 1/5 + Et 50% des utilisateurs ont fréquence 1/7 + Alors les métriques sont trackées séparément: + | Cohorte | Fréquence | Taux désabonnement | Revenus/user | + | A | 1/5 | 2.5% | 0.50€ | + | B | 1/7 | 1.8% | 0.40€ | + Et l'admin peut identifier la fréquence optimale + + Plan du Scénario: Insertion publicité selon fréquence + Étant donné que la fréquence est + Quand j'écoute contenus + Alors publicités sont insérées + + Exemples: + | frequence | contenus | pubs | + | 1/3 | 9 | 3 | + | 1/5 | 10 | 2 | + | 1/5 | 25 | 5 | + | 1/7 | 14 | 2 | + | 1/10 | 30 | 3 | + + Plan du Scénario: Priorité géographique selon type zone + Étant donné qu'une publicité cible + Quand le système calcule le score géographique + Alors la priorité est + + Exemples: + | type_zone | score | + | Point GPS | 1.0 | + | Ville | 0.8 | + | Département | 0.6 | + | Région | 0.4 | + | National | 0.2 | + + Plan du Scénario: Exclusion publicité selon compteur quotidien + Étant donné qu'une publicité a été entendue fois aujourd'hui + Quand le système vérifie l'éligibilité + Alors la publicité est + + Exemples: + | fois | eligible | + | 0 | éligible | + | 1 | éligible | + | 2 | éligible | + | 3 | non éligible | + | 4 | non éligible | diff --git a/features/publicites/metriques-engagement-pub.feature b/features/publicites/metriques-engagement-pub.feature new file mode 100644 index 0000000..4e0b2a1 --- /dev/null +++ b/features/publicites/metriques-engagement-pub.feature @@ -0,0 +1,299 @@ +# language: fr +Fonctionnalité: Métriques d'engagement et dashboard publicitaire + En tant que publicitaire + Je veux consulter des métriques détaillées en temps réel + Afin d'optimiser mes campagnes et mesurer leur ROI + + Contexte: + Étant donné que l'API RoadWave est disponible + Et qu'un compte publicitaire est connecté + Et qu'une campagne active est en cours + + Scénario: Dashboard temps réel avec métriques essentielles + Étant donné que ma campagne a généré 1000 diffusions + Quand je consulte le dashboard + Alors je vois les métriques suivantes mises à jour en temps réel: + | Métrique | Valeur | + | Impressions | 1000 | + | Écoutes complètes (>80%)| 400 | + | Taux d'écoute complète | 40% | + | Taux de skip | 60% | + | Durée moyenne d'écoute | 18s | + | Likes | 25 | + | Abonnements | 5 | + | Coût par écoute | 0.05€ | + + Scénario: Calcul impressions totales + Étant donné que ma publicité a été diffusée 2500 fois + Quand je consulte le dashboard + Alors le compteur "Impressions" affiche 2500 + Et il s'incrémente en temps réel à chaque nouvelle diffusion + + Scénario: Calcul écoutes complètes (>80%) + Étant donné que ma publicité de 30s a été: + | Durée écoutée | Nombre | + | 25s (83%) | 300 | + | 20s (67%) | 200 | + | 10s (33%) | 150 | + | 5s (17%) | 50 | + Quand je consulte les écoutes complètes + Alors le compteur affiche 300 (uniquement ≥80%) + Et le taux d'écoute complète est de 43% (300/700) + + Scénario: Calcul taux de skip + Étant donné 1000 diffusions totales + Et 400 écoutes complètes + Quand je consulte le taux de skip + Alors il affiche 60% ((1000-400)/1000) + Et il est calculé comme (total - complètes) / total + + Scénario: Durée moyenne d'écoute calculée + Étant donné que ma publicité de 30s a été écoutée: + | Durée | Nombre d'utilisateurs | + | 30s | 400 | + | 20s | 300 | + | 10s | 200 | + | 5s | 100 | + Quand je consulte la durée moyenne + Alors le calcul est: (30×400 + 20×300 + 10×200 + 5×100) / 1000 + Et le résultat affiché est 21s + + Scénario: Métriques de likes sur publicité + Étant donné que 50 utilisateurs ont liké ma publicité + Quand je consulte le dashboard + Alors le compteur "Likes" affiche 50 + Et un taux de like de 5% est calculé (50/1000 impressions) + Et cela indique une forte appréciation du contenu + + Scénario: Métriques d'abonnements générés + Étant donné que 10 utilisateurs se sont abonnés après avoir entendu ma pub + Quand je consulte le dashboard + Alors le compteur "Abonnements" affiche 10 + Et un taux de conversion de 1% est calculé (10/1000) + Et cela représente un engagement très fort + + Scénario: Calcul coût par écoute (CPE) + Étant donné que j'ai dépensé 200€ + Et obtenu 4000 écoutes complètes + Quand je consulte le coût par écoute + Alors le CPE affiché est 0.05€ (200/4000) + Et il correspond au tarif standard RoadWave + + Scénario: Répartition géographique avec heatmap + Étant donné que ma campagne cible le département du Var + Et que j'ai 1000 diffusions réparties: + | Zone | Diffusions | Pourcentage | + | Toulon | 400 | 40% | + | Hyères | 250 | 25% | + | Fréjus | 200 | 20% | + | Autres | 150 | 15% | + Quand je consulte la heatmap géographique + Alors une carte Leaflet affiche les zones avec intensité proportionnelle + Et Toulon apparaît en rouge foncé (forte concentration) + Et les autres villes en dégradé orange/jaune + + Scénario: Répartition horaire avec graphique + Étant donné que ma campagne cible les plages 7h-9h et 17h-19h + Et que j'ai 1000 diffusions: + | Plage horaire | Diffusions | + | 7h-8h | 300 | + | 8h-9h | 250 | + | 17h-18h | 280 | + | 18h-19h | 170 | + Quand je consulte le graphique horaire + Alors un histogramme Chart.js affiche les 4 barres + Et je peux identifier que 7h-8h est le pic d'écoute + Et optimiser mes futures campagnes sur cette plage + + Scénario: Taux de complétion par tranche d'âge + Étant donné que ma campagne est Tout Public + Et que j'ai des écoutes sur différentes tranches: + | Tranche d'âge | Écoutes complètes | Total diffusions | Taux | + | 18-24 ans | 120 | 400 | 30% | + | 25-34 ans | 200 | 400 | 50% | + | 35-44 ans | 80 | 200 | 40% | + Quand je consulte l'analyse par âge + Alors je vois que les 25-34 ans ont le meilleur taux (50%) + Et je peux cibler cette tranche pour mes prochaines campagnes + + Scénario: Comparatif de campagnes A/B testing + Étant donné que j'ai 2 campagnes actives: + | Campagne | Budget | Écoutes complètes | Taux | CPE | + | A | 300€ | 4000 | 40% | 0.075€| + | B | 300€ | 6000 | 60% | 0.05€ | + Quand je consulte le comparatif + Alors je vois que la campagne B performe mieux + Et le tableau recommande "Campagne B: +50% écoutes, -33% CPE" + Et je peux allouer plus de budget à la campagne B + + Scénario: Export données CSV pour analyse externe + Étant donné que je veux analyser mes données dans Excel + Quand je clique sur "Exporter CSV" + Alors je télécharge un fichier avec les colonnes: + | Colonne | + | Date | + | Heure | + | Zone géographique | + | Tranche d'âge | + | Durée écoute | + | Skip (Oui/Non) | + | Like (Oui/Non) | + | Abonnement (Oui/Non) | + Et je peux faire des analyses personnalisées + + Scénario: Export graphiques interactifs + Étant donné que je consulte le dashboard + Quand je clique sur un graphique Chart.js + Alors je peux zoomer/filtrer interactivement + Et je peux exporter le graphique en PNG + Et l'image est en haute résolution pour présentations + + Scénario: Rapport PDF automatique fin de campagne + Étant donné que ma campagne de 14 jours se termine + Quand la date de fin est atteinte + Alors un rapport PDF est généré automatiquement + Et il contient: + | Section | + | Résumé exécutif | + | Métriques clés | + | Graphiques de performance | + | Heatmap géographique | + | Répartition horaire | + | Analyse tranches d'âge | + | Recommandations optimisation | + Et je reçois un email avec le PDF en pièce jointe + + Scénario: Métriques temps réel rafraîchies automatiquement + Étant donné que je consulte le dashboard à 10h00 + Quand une nouvelle diffusion se produit à 10h01 + Alors les métriques sont rafraîchies automatiquement (polling 30s) + Et je vois les nouveaux chiffres sans recharger la page + Et un badge "Mis à jour il y a 15s" s'affiche + + Scénario: Alertes performance personnalisées + Étant donné que je configure une alerte "Taux de skip >70%" + Et que ma campagne atteint 72% de skip + Quand le seuil est dépassé + Alors je reçois un email d'alerte: + """ + Alerte Performance - Taux de skip élevé + + Votre campagne a un taux de skip de 72%. + Recommandations: + - Réduire la durée de la publicité + - Améliorer l'accroche des 5 premières secondes + - Vérifier le ciblage (audience pertinente?) + """ + + Scénario: Benchmark vs moyennes RoadWave + Étant donné que ma campagne a 45% d'écoutes complètes + Quand je consulte le benchmark + Alors je vois "Votre taux: 45% | Moyenne RoadWave: 40%" + Et un badge "📊 Performance: +12% vs moyenne" s'affiche + Et je sais que ma campagne performe au-dessus de la moyenne + + Scénario: Coût total consommé vs budget + Étant donné que j'ai un budget de 300€ + Et que j'ai consommé 220€ + Quand je consulte le dashboard + Alors je vois une jauge "Budget consommé: 73%" (220/300) + Et le montant restant "80€ restants" + Et une projection "Épuisé dans 3 jours à ce rythme" + + Scénario: Répartition coûts par type d'écoute + Étant donné que j'ai dépensé 200€ avec: + | Type d'écoute | Nombre | Coût unitaire | Total | + | Écoute complète | 3000 | 0.05€ | 150€ | + | Skip après 5s | 2000 | 0.02€ | 40€ | + | Skip immédiat | 500 | 0€ | 0€ | + Quand je consulte la répartition + Alors un graphique camembert affiche: + | Segment | Pourcentage | + | Écoutes complètes | 75% (150€) | + | Skips partiels | 20% (40€) | + | Skips immédiats | 5% (0€) | + + Scénario: Évolution performance dans le temps + Étant donné une campagne de 30 jours + Quand je consulte le graphique d'évolution + Alors je vois une courbe Chart.js avec: + | Axe | Donnée | + | X | Jours (1-30) | + | Y | Taux d'écoute complète (%) | + Et je peux identifier les tendances (amélioration/dégradation) + Et les jours avec pics d'engagement + + Scénario: Métriques avancées - Taux de réécoute + Étant donné qu'un utilisateur a entendu ma pub 3 fois + Et qu'il l'a écoutée complètement les 3 fois + Quand je consulte les métriques avancées + Alors le "Taux de réécoute" affiche 100% + Et cela indique que le contenu n'est pas perçu comme spam + Et les utilisateurs tolèrent bien la répétition + + Scénario: Recommandations automatiques d'optimisation + Étant donné que ma campagne a un taux de skip de 75% + Et que la durée moyenne d'écoute est de 8s sur 30s + Quand je consulte les recommandations + Alors le système suggère: + """ + 🔍 Analyse de votre campagne + + Problème identifié: Perte d'attention après 8 secondes + + Recommandations: + 1. Placer votre message clé dans les 5 premières secondes + 2. Réduire la durée totale à 15-20 secondes + 3. Ajouter un appel à l'action plus tôt + 4. Tester un nouveau créatif avec accroche différente + """ + + Scénario: Suivi multi-campagnes avec vue consolidée + Étant donné que j'ai 3 campagnes actives simultanément + Quand je consulte la vue consolidée + Alors je vois un tableau récapitulatif: + | Campagne | Budget | Dépensé | Diffusions | Taux complète | CPE | + | A | 300€ | 220€ | 4000 | 40% | 0.05€ | + | B | 500€ | 150€ | 3000 | 60% | 0.05€ | + | C | 200€ | 180€ | 3600 | 35% | 0.05€ | + Et je peux comparer les performances d'un coup d'œil + + Plan du Scénario: Calcul taux d'écoute complète + Étant donné diffusions totales + Et écoutes complètes (≥80%) + Quand je calcule le taux + Alors le résultat est % + + Exemples: + | total | completes | taux | + | 1000 | 400 | 40 | + | 2000 | 1200 | 60 | + | 500 | 100 | 20 | + | 1000 | 850 | 85 | + + Plan du Scénario: Calcul coût par écoute (CPE) + Étant donné un budget dépensé de € + Et écoutes complètes + Quand je calcule le CPE + Alors le résultat est € + + Exemples: + | depense | ecoutes | cpe | + | 100 | 2000 | 0.05 | + | 300 | 6000 | 0.05 | + | 50 | 1000 | 0.05 | + | 500 | 10000 | 0.05 | + + Plan du Scénario: Classification performance vs benchmark + Étant donné un taux d'écoute complète de % + Et une moyenne RoadWave de 40% + Quand je compare à la moyenne + Alors la performance est + + Exemples: + | taux | classification | + | 60 | Excellente (+50%) | + | 50 | Bonne (+25%) | + | 40 | Moyenne | + | 30 | Faible (-25%) | + | 20 | Très faible (-50%)| diff --git a/features/publicites/validation-moderation-pub.feature b/features/publicites/validation-moderation-pub.feature new file mode 100644 index 0000000..75a34a8 --- /dev/null +++ b/features/publicites/validation-moderation-pub.feature @@ -0,0 +1,261 @@ +# language: fr +Fonctionnalité: Validation et modération des publicités + En tant que modérateur RoadWave + Je veux valider manuellement toutes les publicités avant diffusion + Afin de garantir la qualité et la légalité des contenus publicitaires + + Contexte: + Étant donné que l'API RoadWave est disponible + Et qu'un modérateur RoadWave est connecté + + Scénario: Validation manuelle obligatoire avant diffusion + Étant donné qu'un publicitaire a créé une campagne + Et que le paiement de 300€ a été effectué + Quand la campagne est soumise + Alors elle passe en statut "En attente de validation" + Et elle est ajoutée à la file d'attente des modérateurs + Et la diffusion ne démarre PAS avant validation manuelle + Et le publicitaire reçoit un email "Votre campagne est en cours de validation (24-48h)" + + Scénario: Délai de validation 24-48h ouvrées + Étant donné qu'une campagne est soumise le lundi 10h + Quand le modérateur la valide le mardi 15h + Alors le délai est de 29h (dans les 48h ouvrées) + Et le publicitaire reçoit une notification "Votre campagne est approuvée" + + Scénario: Validation dépassant 48h avec notification + Étant donné qu'une campagne est soumise le lundi 10h + Quand 48h ouvrées se sont écoulées + Et que la campagne n'est toujours pas validée + Alors le publicitaire reçoit un email automatique: + """ + Validation en cours - Délai prolongé + + Votre campagne nécessite une analyse approfondie. + Nous vous contacterons sous 24h supplémentaires. + """ + Et un modérateur senior est assigné automatiquement + + Scénario: Acceptation de campagne publicitaire + Étant donné qu'une campagne est en attente de validation + Et que l'audio respecte toutes les règles + Quand le modérateur clique sur "Approuver" + Alors le statut passe à "Approuvée" + Et la campagne démarre à la date programmée + Et le publicitaire reçoit un email de confirmation + Et le budget commence à être consommé dès le début + + Scénario: Refus de campagne avec motif détaillé + Étant donné qu'une campagne contient du contenu alcool + Quand le modérateur clique sur "Refuser" + Et qu'il sélectionne le motif "Contenu interdit: Alcool" + Et qu'il ajoute le commentaire "La publicité pour l'alcool est interdite en France" + Alors le statut passe à "Refusée" + Et le publicitaire reçoit un email détaillé avec: + | Champ | Valeur | + | Motif | Contenu interdit: Alcool | + | Commentaire | La publicité pour l'alcool est interdite en France | + | Action requise | Modifier votre contenu et soumettre à nouveau | + Et un remboursement automatique de 300€ est déclenché + + Scénario: Remboursement automatique après refus + Étant donné qu'une campagne à 500€ est refusée + Quand le statut passe à "Refusée" + Alors un remboursement Mangopay de 500€ est initié automatiquement + Et le délai de remboursement est de 5-7 jours ouvrés + Et le publicitaire reçoit un email "Remboursement en cours" + + Scénario: Contenus interdits - Alcool + Étant donné qu'une publicité mentionne "Whisky premium 40°" + Quand le modérateur écoute l'audio + Alors il doit refuser la campagne + Et sélectionner le motif "Contenu interdit: Alcool" + Car la publicité pour l'alcool est interdite en France + + Scénario: Contenus interdits - Tabac + Étant donné qu'une publicité mentionne "Cigarettes électroniques" + Quand le modérateur écoute l'audio + Alors il doit refuser la campagne + Et sélectionner le motif "Contenu interdit: Tabac/Vape" + Car la publicité pour le tabac et dérivés est interdite + + Scénario: Contenus interdits - Jeux d'argent + Étant donné qu'une publicité mentionne "Gagnez 10 000€ - Paris sportifs" + Quand le modérateur écoute l'audio + Alors il doit refuser la campagne + Et sélectionner le motif "Contenu interdit: Jeux d'argent" + Car la publicité pour les jeux d'argent est soumise à régulation stricte + + Scénario: Contenus interdits - Politique pendant campagne électorale + Étant donné qu'une publicité politique est soumise + Et que nous sommes en période de campagne électorale officielle + Quand le modérateur écoute l'audio + Alors il doit refuser la campagne + Et sélectionner le motif "Contenu interdit: Publicité politique (période électorale)" + + Scénario: Contenus interdits - Contenu sexuel + Étant donné qu'une publicité contient des propos sexuellement explicites + Quand le modérateur écoute l'audio + Alors il doit refuser la campagne + Et sélectionner le motif "Contenu interdit: Contenu sexuel" + + Scénario: Contenus interdits - Violence + Étant donné qu'une publicité contient des descriptions violentes + Quand le modérateur écoute l'audio + Alors il doit refuser la campagne + Et sélectionner le motif "Contenu interdit: Violence" + + Scénario: Contenu légal autorisé - Commerce local + Étant donné qu'une publicité pour un restaurant local dit "Découvrez notre menu du jour" + Quand le modérateur écoute l'audio + Alors il doit approuver la campagne + Car le contenu est légal et conforme + + Scénario: Contenu légal autorisé - Service professionnel + Étant donné qu'une publicité pour un garage dit "Révision complète à partir de 99€" + Quand le modérateur écoute l'audio + Alors il doit approuver la campagne + Car le contenu est légal et informatif + + Scénario: Critères de validation - Qualité audio + Étant donné qu'une publicité a une qualité audio très basse (bruits, saturation) + Quand le modérateur écoute l'audio + Alors il peut refuser avec le motif "Qualité audio insuffisante" + Et recommander "Veuillez soumettre un fichier audio de meilleure qualité" + + Scénario: Critères de validation - Classification d'âge correcte + Étant donné qu'une publicité contient du langage familier + Et qu'elle est classée "Tout public" + Quand le modérateur écoute l'audio + Alors il peut refuser avec le motif "Classification d'âge incorrecte" + Et recommander "Reclasser en 13+ minimum" + + Scénario: Critères de validation - Respect réglementation française + Étant donné qu'une publicité fait des promesses mensongères "Perdez 10kg en 1 semaine" + Quand le modérateur écoute l'audio + Alors il doit refuser avec le motif "Non-conformité réglementaire: Publicité mensongère" + Car cela viole la réglementation sur la publicité trompeuse + + Scénario: File d'attente modération priorisée + Étant donné que 10 campagnes sont en attente de validation + Et que la campagne A a été soumise il y a 40h + Et que la campagne B a été soumise il y a 2h + Quand le modérateur consulte sa file + Alors la campagne A apparaît en premier (priorité temporelle) + Et un badge "Urgente - >40h" est affiché + + Scénario: Dashboard modération - Vue d'ensemble + Étant donné que je suis modérateur + Quand j'accède au dashboard modération publicités + Alors je vois: + | Métrique | Exemple valeur | + | Campagnes en attente | 5 | + | Délai moyen de validation | 28h | + | Campagnes validées aujourd'hui | 12 | + | Campagnes refusées aujourd'hui | 3 | + | Taux d'acceptation | 80% | + + Scénario: Transcription automatique pour aide modération + Étant donné qu'une publicité audio est soumise + Quand le système traite l'audio + Alors une transcription automatique est générée via Whisper + Et elle est affichée au modérateur pour faciliter la revue + Et elle permet une recherche par mots-clés (alcool, tabac, etc.) + + Scénario: Détection automatique mots-clés interdits + Étant donné qu'une publicité audio est soumise + Quand la transcription contient "whisky" ou "vodka" + Alors un flag automatique "⚠️ Alcool détecté" est ajouté + Et la campagne est priorisée pour validation manuelle rapide + Et le modérateur est alerté du contenu potentiellement interdit + + Scénario: Historique modération publicitaire + Étant donné qu'un publicitaire a eu 2 campagnes refusées + Quand il soumet une 3ème campagne + Alors le modérateur voit l'historique: + | Date | Statut | Motif | + | 2026-01-15 | Refusée | Contenu interdit: Alcool | + | 2026-01-20 | Refusée | Qualité audio faible | + Et il peut en tenir compte dans sa décision + + Scénario: Appel possible après refus + Étant donné que ma campagne a été refusée pour "Classification incorrecte" + Quand je conteste la décision via le formulaire d'appel + Alors un modérateur senior revoit la campagne + Et il peut approuver si la classification est en fait correcte + Et le délai de réponse est de 48-72h + + Scénario: Notification temps réel pour modérateurs + Étant donné que je suis modérateur connecté + Quand une nouvelle campagne est soumise + Alors je reçois une notification in-app + Et le compteur "Campagnes en attente" s'incrémente en temps réel + Et je peux cliquer pour consulter immédiatement + + Scénario: Statistiques conformité par catégorie + Étant donné que je suis admin modération + Quand je consulte les statistiques mensuelles + Alors je vois les motifs de refus: + | Motif | Nombre | Pourcentage | + | Alcool | 15 | 30% | + | Qualité audio | 12 | 24% | + | Classification erronée | 10 | 20% | + | Publicité mensongère | 8 | 16% | + | Autres | 5 | 10% | + + Scénario: Export rapport modération + Étant donné que je suis modérateur senior + Quand j'exporte le rapport mensuel + Alors je reçois un fichier CSV avec: + | Colonne | + | Campagne ID | + | Publicitaire | + | Date soumission | + | Date décision | + | Statut | + | Motif (si refus) | + | Modérateur | + Et je peux l'analyser dans Excel + + Scénario: Validation partielle avec demande modification + Étant donné qu'une campagne a un contenu acceptable + Mais que la classification d'âge est incorrecte + Quand le modérateur clique sur "Demander modification" + Alors le publicitaire reçoit un email: + """ + Modification requise + + Votre campagne nécessite un ajustement: + - Reclasser de "Tout public" à "13+" + + Veuillez modifier et soumettre à nouveau. + Budget conservé, pas de remboursement. + """ + Et le statut devient "Modification requise" + Et le publicitaire peut modifier sans repayer + + Plan du Scénario: Contenus interdits automatiquement détectés + Étant donné qu'une publicité contient le mot + Quand la transcription automatique est analysée + Alors un flag est ajouté + Et le motif de refus suggéré est + + Exemples: + | mot_cle | flag | motif | + | whisky | ⚠️ Alcool | Contenu interdit: Alcool | + | vodka | ⚠️ Alcool | Contenu interdit: Alcool | + | cigarette | ⚠️ Tabac | Contenu interdit: Tabac | + | casino | ⚠️ Jeux argent | Contenu interdit: Jeux | + | paris sportifs | ⚠️ Jeux argent | Contenu interdit: Jeux | + + Plan du Scénario: Délais de validation selon soumission + Étant donné qu'une campagne est soumise à + Quand elle est validée heures plus tard + Alors le statut est + + Exemples: + | jour | heure | delai | conformite | + | Lundi | 10h | 24 | Dans les délais (24h) | + | Lundi | 10h | 48 | Dans les délais (48h) | + | Lundi | 10h | 50 | Hors délais (>48h) | + | Vendredi| 16h | 72 | Dans les délais (we) | diff --git a/features/radio-live/architecture-technique-live.feature b/features/radio-live/architecture-technique-live.feature new file mode 100644 index 0000000..b1b8fb2 --- /dev/null +++ b/features/radio-live/architecture-technique-live.feature @@ -0,0 +1,197 @@ +# language: fr +Fonctionnalité: Architecture technique radio live + En tant que système + Je veux gérer efficacement les flux audio en temps réel + Afin d'assurer une diffusion stable et scalable des lives + + Contexte: + Étant donné que l'infrastructure RoadWave est opérationnelle + Et que les serveurs Go avec Pion WebRTC sont actifs + + Scénario: Ingestion WebRTC du flux créateur + Étant donné qu'un créateur démarre un live depuis son application mobile + Quand le flux audio WebRTC (Opus 48 kbps) arrive sur le serveur + Alors le serveur Go avec Pion WebRTC accepte la connexion + Et le flux est traité en temps réel + + Scénario: Conversion temps réel Opus vers segments HLS + Étant donné qu'un flux WebRTC Opus est reçu par le serveur + Quand le serveur traite le flux + Alors FFmpeg convertit en segments HLS (.ts) + Et un fichier manifest .m3u8 est généré et mis à jour régulièrement + Et les segments ont une durée de 2 secondes chacun + + Scénario: Distribution via NGINX Cache + Étant donné que les segments HLS sont générés + Quand un auditeur demande à rejoindre le live + Alors le manifest .m3u8 est servi via NGINX Cache (OVH) + Et les segments .ts sont cachés sur le cache NGINX + Et la distribution est globale avec latence minimale + + Scénario: Lecture HLS native sur mobile iOS + Étant donné qu'un auditeur iOS rejoint un live + Quand l'application charge le flux HLS + Alors le player natif AVPlayer gère la lecture + Et le buffer de 15 secondes est appliqué automatiquement + Et la qualité s'adapte selon la connexion + + Scénario: Lecture HLS native sur mobile Android + Étant donné qu'un auditeur Android rejoint un live + Quand l'application charge le flux HLS + Alors le player natif ExoPlayer gère la lecture + Et le buffer de 15 secondes est configuré + Et la qualité s'adapte selon la connexion + + Scénario: Enregistrement parallèle du flux pour replay + Étant donné qu'un live est en cours + Alors un processus parallèle enregistre le flux Opus raw + Et l'enregistrement est stocké temporairement sur le serveur + Et l'enregistrement est indépendant de la diffusion HLS + + Scénario: Traitement post-live asynchrone + Étant donné qu'un live vient de se terminer + Quand le processus post-live démarre + Alors un job asynchrone est créé dans la queue Redis + Et un worker Go récupère le job + Et le worker exécute FFmpeg pour les conversions + + Scénario: Conversion Opus raw vers MP3 256 kbps + Étant donné qu'un worker traite un job post-live + Quand la conversion démarre + Alors FFmpeg convertit Opus raw en MP3 256 kbps + Et la normalisation audio à -14 LUFS est appliquée + Et les silences prolongés (>3 secondes) sont détectés et nettoyés + + Scénario: Génération segments HLS pour le replay + Étant donné que le MP3 256 kbps est généré + Quand le worker crée les segments HLS + Alors des segments .ts de 10 secondes sont créés + Et un manifest .m3u8 est généré + Et les segments sont uploadés vers OVH Object Storage + + Scénario: Publication automatique du replay + Étant donné que tous les segments HLS sont uploadés + Quand le worker finalise le job + Alors une entrée de contenu "replay" est créée en base PostgreSQL + Et le titre est "[REPLAY] [Titre live original]" + Et le type géographique est "Géo-neutre" + Et le replay est immédiatement disponible pour les auditeurs + + Scénario: Suppression automatique fichier Opus raw après 7 jours + Étant donné qu'un replay est publié depuis 7 jours + Quand le job de nettoyage quotidien s'exécute + Alors le fichier Opus raw est supprimé du stockage + Et seul le MP3 256 kbps et les segments HLS sont conservés + Et l'espace de stockage est libéré + + Scénario: Scalabilité horizontale des workers de conversion + Étant donné que 50 lives se terminent simultanément + Quand les jobs post-live sont créés + Alors les workers Go disponibles traitent les jobs en parallèle + Et si tous les workers sont occupés, les jobs attendent en queue Redis + Et de nouveaux workers peuvent être lancés automatiquement (Kubernetes) + + Scénario: Limitation du nombre de lives simultanés (MVP) + Étant donné que l'infrastructure MVP est configurée pour 100 lives simultanés + Et que 100 lives sont actuellement en cours + Quand un nouveau créateur essaie de démarrer un live + Alors la demande est refusée avec le code erreur 503 + Et le message "Capacité maximale atteinte. Veuillez réessayer dans quelques minutes" est retourné + Et la demande peut être mise en queue prioritaire si créateur Premium + + Scénario: Monitoring des ressources serveur en temps réel + Étant donné que plusieurs lives sont en cours + Alors le système monitore en temps réel: + | métrique | seuil alerte | + | CPU utilisation | >80% | + | Mémoire utilisation | >85% | + | Bande passante upload | >80% capacité| + | Nombre connexions WebRTC | >90 | + | Latence moyenne CDN | >200ms | + Et si un seuil est dépassé, une alerte est envoyée à l'équipe technique + + Scénario: Calcul du coût de bande passante CDN + Étant donné qu'un live a 100 auditeurs simultanés + Et que la qualité est 48 kbps Opus + Quand le live dure 1 heure + Alors la bande passante totale est d'environ 2.16 GB + Et le coût estimé infrastructure est d'environ 0.02€ + Et ces métriques sont enregistrées pour facturation créateur si nécessaire + + Scénario: Cache NGINX des segments HLS + Étant donné qu'un live est diffusé via NGINX Cache + Quand un segment .ts est généré + Alors le segment est uploadé vers OVH Object Storage origin + Et NGINX Cache met en cache le segment + Et les auditeurs suivants récupèrent le segment depuis le cache + Et la charge sur le serveur origin est réduite de ~90% + + Scénario: Gestion de la latence WebRTC créateur + Étant donné qu'un créateur diffuse avec une connexion 4G + Quand la latence réseau augmente ponctuellement + Alors le buffer côté serveur absorbe les fluctuations + Et la qualité peut être réduite temporairement (48 kbps → 32 kbps) + Et un warning est affiché au créateur si la connexion est trop instable + + Scénario: Détection automatique de la musique protégée (post-MVP) + Étant donné qu'un live contient de la musique en arrière-plan + Quand le système d'audio fingerprint analyse le flux + Alors une empreinte audio est calculée toutes les 30 secondes + Et l'empreinte est comparée à une base de données de contenus protégés + Et si une correspondance est trouvée, un warning est envoyé au créateur + Et si le créateur ne corrige pas sous 30 secondes, le live peut être arrêté + + Scénario: Stockage des métadonnées de live en PostgreSQL + Étant donné qu'un créateur démarre un live + Alors les métadonnées suivantes sont enregistrées: + | champ | exemple valeur | + | live_id | uuid v4 | + | creator_id | uuid créateur | + | title | "Mon super live" | + | started_at | timestamp UTC | + | zone_geo | "Île-de-France" | + | tags | ["Actualité", "Tech"] | + | classification_age | "Tout public" | + Et ces données sont indexées pour recherche et analytics + + Scénario: Cache Redis pour compteurs temps réel + Étant donné qu'un live est en cours + Alors Redis stocke les compteurs temps réel: + | clé Redis | valeur exemple | + | live:[live_id]:listeners | 247 | + | live:[live_id]:likes | 89 | + | live:[live_id]:reports | 0 | + Et ces compteurs sont mis à jour toutes les 2 secondes + Et les compteurs sont persistés en PostgreSQL toutes les 60 secondes + + Scénario: Heartbeat auditeurs pour compteur précis + Étant donné qu'un auditeur écoute un live + Alors l'application envoie un heartbeat toutes les 10 secondes + Et le heartbeat met à jour le timestamp dans Redis + Et si aucun heartbeat n'est reçu pendant 30 secondes, l'auditeur est retiré du compteur + + Scénario: Gestion des pannes serveur pendant un live + Étant donné qu'un live est en cours sur serveur A + Quand le serveur A tombe en panne + Alors Kubernetes redémarre automatiquement un pod + Mais le live en cours est perdu (pas de failover temps réel en MVP) + Et le créateur voit le message "Connexion perdue. Veuillez redémarrer le live" + Et les auditeurs voient "Le live est terminé suite à un problème technique" + + Scénario: Backup automatique des enregistrements live + Étant donné qu'un live est enregistré en Opus raw + Quand l'enregistrement dépasse 10 minutes + Alors un backup incrémental est créé toutes les 10 minutes + Et le backup est stocké sur un stockage secondaire (S3-compatible) + Et en cas de crash serveur, le live peut être récupéré jusqu'au dernier backup + + Scénario: Logs et audit trail des lives + Étant donné qu'un live démarre, se déroule et se termine + Alors tous les événements sont loggés: + | événement | détails enregistrés | + | Démarrage live | timestamp, creator_id, zone_geo | + | Auditeur rejoint | timestamp, user_id, position GPS | + | Auditeur quitte | timestamp, user_id, durée écoute | + | Signalement | timestamp, user_id, catégorie | + | Fin live | timestamp, durée totale, stats finales | + Et ces logs sont conservés 90 jours pour analytics et conformité RGPD diff --git a/features/radio-live/arret-live.feature b/features/radio-live/arret-live.feature new file mode 100644 index 0000000..ae8e84a --- /dev/null +++ b/features/radio-live/arret-live.feature @@ -0,0 +1,159 @@ +# language: fr +Fonctionnalité: Arrêt du live + En tant que créateur + Je veux arrêter ma diffusion en direct de manière contrôlée + Afin de terminer proprement mon live et générer un replay automatiquement + + Contexte: + Étant donné que l'API RoadWave est disponible + Et que je suis connecté en tant que créateur + Et que je diffuse actuellement un live + + Scénario: Arrêt manuel avec compte à rebours 5 secondes + Quand j'appuie sur le bouton "Arrêter live" + Alors un compte à rebours de 5 secondes démarre + Et je vois le message "Ce live se termine dans 5... 4... 3... 2... 1" + Et un bouton "Annuler" est affiché pendant le décompte + Et l'audio du compte à rebours est diffusé aux auditeurs + + Scénario: Annulation du compte à rebours + Étant donné que j'ai appuyé sur "Arrêter live" + Et que le compte à rebours affiche "3 secondes" + Quand j'appuie sur "Annuler" + Alors le compte à rebours s'arrête + Et le live continue normalement + Et aucune notification n'est envoyée aux auditeurs + + Scénario: Arrêt effectif après compte à rebours + Étant donné que le compte à rebours est à 0 + Alors le live s'arrête + Et la diffusion aux auditeurs se termine + Et le message "Live terminé" s'affiche + Et le processus de traitement post-live démarre automatiquement + + Scénario: Déconnexion créateur courte (moins de 60 secondes) + Étant donné que je diffuse un live + Quand ma connexion est perdue pendant 30 secondes + Alors les auditeurs voient le message "Connexion créateur perdue, reconnexion en cours..." + Et le live continue de bufferer + Et quand ma connexion revient, le live reprend normalement + + Scénario: Déconnexion créateur longue (60 secondes ou plus) + Étant donné que je diffuse un live + Quand ma connexion est perdue pendant 60 secondes + Alors le live s'arrête automatiquement + Et les auditeurs voient le message "Le live est terminé suite à une coupure de connexion" + Et le processus de traitement post-live démarre + + Scénario: Enregistrement automatique pendant le live + Étant donné que je diffuse un live + Alors mon flux audio est enregistré en continu + Et le format d'enregistrement est Opus raw + Et l'enregistrement est stocké temporairement sur le serveur + + Scénario: Génération automatique du replay après arrêt + Étant donné que mon live vient de se terminer + Et que l'option "Publier replay automatiquement" est activée (par défaut) + Quand le traitement post-live démarre + Alors un job asynchrone est créé + Et le job effectue les opérations suivantes: + | opération | détail | + | Conversion format | Opus raw → MP3 256 kbps | + | Génération segments HLS | Segments .ts pour streaming | + | Normalisation volume | -14 LUFS | + | Détection silences prolongés | Nettoyage automatique | + + Scénario: Publication du replay + Étant donné que le traitement post-live est terminé + Alors le replay est publié automatiquement sous 5 à 10 minutes + Et le titre est "[REPLAY] [Titre live original]" + Et la zone de diffusion est la même que le live + Et les tags sont identiques au live + Et la classification d'âge est identique + Et le type géographique est "Géo-neutre" (contenu pérenne) + + Scénario: Notification de disponibilité du replay aux auditeurs + Étant donné que le replay de mon live est publié + Quand un auditeur qui a écouté le live se reconnecte + Alors il voit une notification in-app "Le replay de [Titre] est disponible" + + Scénario: Option désactivation publication automatique replay + Étant donné que je configure un nouveau live + Quand je désactive l'option "Publier replay automatiquement" + Et que je démarre puis arrête le live + Alors le live est enregistré + Mais le replay n'est pas publié automatiquement + Et je peux décider manuellement de le publier plus tard + + Scénario: Suppression manuelle du replay après publication + Étant donné que mon live a généré un replay publié + Quand j'accède à mes contenus + Alors je vois le replay dans ma liste + Et je peux le supprimer comme n'importe quel contenu + Quand je supprime le replay + Alors le fichier source Opus raw est supprimé immédiatement + + Scénario: Conservation fichier source Opus raw + Étant donné que mon live est terminé + Et que le replay est publié + Alors le fichier Opus raw est conservé pendant 7 jours + Et après 7 jours, le fichier raw est supprimé automatiquement + Et seul le MP3 256 kbps est conservé + + Scénario: Modification du replay interdite + Étant donné que mon live a généré un replay publié + Quand j'essaie de modifier l'audio du replay + Alors l'action est refusée + Et je vois le message "Les replays ne peuvent pas être modifiés pour garantir l'intégrité de l'enregistrement" + Et je peux uniquement modifier les métadonnées (titre, description) + + Scénario: Statistiques du live disponibles après arrêt + Étant donné que mon live est terminé + Quand j'accède aux statistiques + Alors je vois: + | métrique | exemple valeur | + | Durée totale | 1h 23min | + | Nombre d'auditeurs max | 247 | + | Nombre d'auditeurs moyen | 183 | + | Nombre de likes | 89 | + | Nombre d'abonnements | 12 | + | Signalements reçus | 0 | + + Scénario: Live terminé avec signalements en cours + Étant donné que mon live a reçu 3 signalements pendant la diffusion + Quand le live se termine + Alors le replay n'est pas publié automatiquement + Et le contenu est en attente de modération + Et je vois le message "Votre replay sera publié après vérification suite aux signalements reçus" + Et un modérateur doit valider ou refuser le replay sous 24h + + Scénario: Arrêt forcé par un modérateur + Étant donné que je diffuse un live + Et qu'un modérateur détecte du contenu interdit + Quand le modérateur clique sur "Arrêter le live immédiatement" + Alors le live s'arrête sans compte à rebours + Et je vois le message "Votre live a été interrompu par la modération" + Et je reçois une notification détaillant la raison + Et le replay n'est pas publié + Et le fichier source est conservé 30 jours pour appel + + Scénario: Métriques de bande passante pendant le live + Étant donné que je diffuse un live + Et que 100 auditeurs écoutent simultanément + Alors la bande passante consommée est d'environ 4.8 Mbps via NGINX Cache + Et le coût estimé infrastructure est d'environ 0.02€ par heure de diffusion + Et je peux voir ces métriques en temps réel dans l'interface créateur + + Scénario: Live sans auditeurs pendant 5 minutes + Étant donné que je diffuse un live + Et qu'aucun auditeur n'écoute depuis 5 minutes + Alors je vois un message d'information "Aucun auditeur actuellement connecté" + Mais le live continue normalement + Et je peux choisir de continuer ou d'arrêter + + Scénario: Qualité audio du replay supérieure au live + Étant donné que mon live était diffusé en Opus 48 kbps + Quand le replay est généré + Alors le replay est encodé en MP3 256 kbps + Et la qualité audio du replay est supérieure au live + Et la taille du fichier est optimisée pour le stockage long terme diff --git a/features/radio-live/comportement-auditeur.feature b/features/radio-live/comportement-auditeur.feature new file mode 100644 index 0000000..6d180f1 --- /dev/null +++ b/features/radio-live/comportement-auditeur.feature @@ -0,0 +1,209 @@ +# language: fr +Fonctionnalité: Comportement auditeur pendant un live + En tant qu'auditeur + Je veux écouter des lives de manière stable + Afin de profiter du contenu en temps réel sans coupures + + Contexte: + Étant donné que l'API RoadWave est disponible + Et que je suis connecté en tant qu'auditeur + Et qu'un créateur diffuse actuellement un live + + Scénario: Rejoindre un live avec buffer de synchronisation 15 secondes + Quand je clique sur "Rejoindre le live" + Alors la connexion au flux HLS s'établit + Et je commence à écouter avec un décalage de 15 secondes par rapport au créateur + Et le buffer de 15 secondes garantit une lecture stable + + Scénario: Justification du buffer 15 secondes + Étant donné les alternatives de buffer possibles: + | buffer | stabilité 3G | stabilité 4G | décalage perceptible | décision | + | 5s | Faible | Moyenne | Non | ❌ | + | 10s | Moyenne | Bonne | Non | ❌ | + | 15s | Bonne | Excellente | Léger acceptable | ✅ | + | 20s+ | Excellente | Excellente | Oui | ❌ | + Alors le buffer optimal est 15 secondes + + Scénario: Lecture stable sur réseau 3G + Étant donné que je suis sur réseau 3G + Et que j'écoute un live + Quand des micro-coupures réseau surviennent + Alors le buffer de 15 secondes absorbe les coupures + Et la lecture continue sans interruption perceptible + + Scénario: Lecture stable sur réseau 4G + Étant donné que je suis sur réseau 4G + Et que j'écoute un live + Alors la lecture est fluide + Et le buffer de 15 secondes prévient les coupures lors de changement de cellule + + Scénario: Continuation du live en sortant de la zone géographique + Étant donné que j'écoute un live régional "Île-de-France" + Et que je suis situé en Île-de-France + Quand je me déplace et sors du département + Alors le live continue de jouer normalement + Et je peux écouter jusqu'à la fin naturelle du live + Et après la fin du live, l'algorithme propose du contenu correspondant à ma nouvelle position + + Scénario: Abonné dans la zone reçoit notification push + Étant donné que je suis abonné au créateur "JeanDupont" + Et que je suis situé en Île-de-France + Quand "JeanDupont" démarre un live en Île-de-France + Alors je reçois une notification push "🔴 JeanDupont est en direct : [Titre du live]" + Et quand je tape sur la notification, l'app s'ouvre et le live démarre immédiatement + + Scénario: Abonné hors zone ne reçoit pas de notification + Étant donné que je suis abonné au créateur "JeanDupont" + Et que je suis situé à Lyon + Quand "JeanDupont" démarre un live en Île-de-France + Alors je ne reçois pas de notification push + Et cela évite la frustration de ne pas pouvoir écouter un live hors zone + + Scénario: Découverte d'un live via l'algorithme de recommandation + Étant donné que je suis dans la zone géographique du live + Et que je navigue dans l'app avec "Suivant" + Quand l'algorithme propose un live en cours + Alors je vois l'indicateur "🔴 EN DIRECT" + Et je peux choisir de le rejoindre ou de passer au suivant + + Scénario: Reconnexion rapide après coupure réseau (moins de 90 secondes) + Étant donné que j'écoute un live + Quand je perds ma connexion réseau pendant 45 secondes + Et que je retrouve ma connexion + Alors je reprends le live au moment actuel (pas au buffer ancien) + Et le saut temporel est transparent (pas de message d'erreur) + Et je ne rate que quelques secondes de contenu + + Scénario: Reconnexion longue après coupure réseau (90 secondes ou plus) + Étant donné que j'écoute un live + Quand je perds ma connexion réseau pendant 90 secondes + Et que je retrouve ma connexion + Alors je vois le message "Live en cours perdu, passage au contenu suivant" + Et l'algorithme propose automatiquement le contenu suivant + Et je peux manuellement revenir au live s'il est toujours en cours + + Scénario: Interactions disponibles pendant le live - Like + Étant donné que j'écoute un live + Et que mon véhicule est à l'arrêt + Quand je clique sur le bouton "❤️ Like" + Alors le like est enregistré immédiatement + Et le compteur de likes visible par le créateur s'incrémente + Et ma jauge d'intérêt pour les tags du live augmente de +2% + + Scénario: Interactions disponibles pendant le live - Abonnement + Étant donné que j'écoute un live + Et que je ne suis pas encore abonné au créateur + Quand je clique sur le bouton "S'abonner" + Alors je m'abonne au créateur + Et ma jauge d'intérêt pour tous les tags du créateur augmente de +5% + Et je recevrai des notifications pour ses prochains lives + + Scénario: Interactions disponibles pendant le live - Skip + Étant donné que j'écoute un live + Quand j'appuie sur "Suivant" (ou commande au volant) + Alors je quitte le live immédiatement + Et l'algorithme propose le contenu suivant + Et si j'ai écouté moins de 10 secondes, ma jauge d'intérêt diminue de -0.5% + + Scénario: Commande Précédent désactivée pendant un live + Étant donné que j'écoute un live + Quand j'appuie sur "Précédent" (ou commande au volant) + Alors rien ne se passe + Et un message d'information s'affiche brièvement "Précédent non disponible sur les lives" + + Scénario: Chat en direct désactivé (décision définitive) + Étant donné que j'écoute un live + Alors aucune interface de chat n'est disponible + Et je ne peux pas envoyer de messages au créateur + Et je ne peux pas voir de messages d'autres auditeurs + Et cette fonctionnalité ne sera jamais implémentée + + Scénario: Réactions emoji désactivées (décision définitive) + Étant donné que j'écoute un live + Alors aucune réaction emoji n'est disponible + Et je ne peux pas envoyer d'emoji en temps réel + Et cette fonctionnalité ne sera jamais implémentée + + Scénario: Message d'information sur l'absence de chat + Étant donné que j'écoute mon premier live + Quand j'accède à l'interface du live + Alors je vois un bandeau informatif "💬 Les discussions ne sont pas disponibles sur RoadWave pour garantir votre sécurité en voiture et éviter le harcèlement." + Et ce bandeau n'apparaît qu'une seule fois (première expérience) + + Scénario: Signalement d'un live en cours + Étant donné que j'écoute un live + Et que le contenu me semble inapproprié + Quand je clique sur le bouton "Signaler" + Alors je vois les catégories de signalement: + | catégorie | + | Haine et violence | + | Contenu sexuel | + | Illégalité | + | Droits d'auteur | + | Désinformation dangereuse | + | Harcèlement | + | Autre | + Et quand je sélectionne une catégorie + Alors le signalement est envoyé en priorité selon la catégorie + Et un modérateur peut écouter le live en temps réel si besoin + + Scénario: Statistiques visibles par les auditeurs pendant le live + Étant donné que j'écoute un live + Quand je consulte les informations du live + Alors je vois: + | information | exemple valeur | + | Nombre d'auditeurs | 247 personnes | + | Durée du live | 1h 23min | + | Nom du créateur | @JeanDupont | + | Zone de diffusion | Île-de-France | + | Tags | Actualité, Société | + Mais je ne vois pas les likes ou autres métriques détaillées + + Scénario: Compteur d'auditeurs arrondi pour préserver la vie privée + Étant donné que j'écoute un live avec exactement 247 auditeurs + Quand je consulte le nombre d'auditeurs + Alors je vois "~250 auditeurs" (arrondi à la dizaine supérieure) + + Scénario: Qualité audio adaptative pendant le live + Étant donné que j'écoute un live + Quand ma connexion passe de 4G à 3G + Alors la qualité audio s'adapte automatiquement + Et je passe de 48 kbps à 24 kbps Opus + Et la transition est transparente sans coupure + + Scénario: Consommation de données pendant un live + Étant donné que j'écoute un live en qualité standard 48 kbps + Et que j'écoute pendant 1 heure + Alors j'ai consommé environ 21.6 MB de données mobiles + Et cette consommation est affichée dans les paramètres de l'app + + Scénario: Lecture du replay après la fin du live + Étant donné que j'écoute un live depuis 30 minutes + Quand le créateur arrête le live + Alors je vois le message "Le live est terminé. Le replay sera disponible dans quelques minutes" + Et le contenu suivant est automatiquement proposé après 2 secondes + + Scénario: Notification de disponibilité du replay + Étant donné que j'ai écouté un live jusqu'à la fin + Et que le replay est publié 8 minutes plus tard + Quand je rouvre l'application + Alors je vois une notification in-app "Le replay de [Titre] est maintenant disponible" + Et je peux cliquer pour l'écouter immédiatement + + Scénario: Aucune publicité pendant un live pour utilisateurs gratuits + Étant donné que je suis un utilisateur gratuit + Et que j'écoute un live + Alors aucune publicité n'est insérée pendant le live + Et la publicité apparaît seulement entre le live et le contenu suivant + + Scénario: Détection de contexte voiture pendant un live + Étant donné que j'écoute un live + Et que ma vitesse est supérieure à 10 km/h + Alors l'interface tactile est désactivée pour la sécurité + Et seules les commandes au volant sont actives (Play/Pause/Suivant) + + Scénario: Détection de contexte piéton pendant un live + Étant donné que j'écoute un live + Et que ma vitesse est inférieure à 5 km/h + Alors l'interface tactile complète est disponible + Et je peux liker, m'abonner, signaler via l'écran tactile diff --git a/features/radio-live/demarrage-live.feature b/features/radio-live/demarrage-live.feature new file mode 100644 index 0000000..8c19d03 --- /dev/null +++ b/features/radio-live/demarrage-live.feature @@ -0,0 +1,151 @@ +# language: fr +Fonctionnalité: Démarrage d'un live + En tant que créateur + Je veux démarrer une diffusion en direct + Afin de partager du contenu audio en temps réel avec mes auditeurs + + Contexte: + Étant donné que l'API RoadWave est disponible + Et que je suis connecté en tant que créateur vérifié + Et que j'ai les permissions de diffusion live + + Scénario: Vérifications pré-live réussies + Étant donné que ma connexion upload est supérieure à 1 Mbps + Et que j'ai autorisé l'accès au microphone + Et que j'ai défini une zone de diffusion "Île-de-France" + Quand je lance les vérifications pré-live + Alors toutes les vérifications sont validées + Et je peux démarrer le live + + Scénario: Échec pré-live avec connexion insuffisante + Étant donné que ma connexion upload est de 0.5 Mbps + Quand je lance les vérifications pré-live + Alors je vois un warning "Connexion insuffisante pour garantir une diffusion stable (minimum 1 Mbps)" + Et je peux choisir de continuer quand même ou d'annuler + + Scénario: Échec pré-live sans autorisation microphone + Étant donné que je n'ai pas autorisé l'accès au microphone + Quand j'essaie de démarrer un live + Alors je vois le message "Accès au microphone requis pour démarrer un live" + Et je suis redirigé vers les paramètres système + + Scénario: Échec pré-live sans zone de diffusion définie + Étant donné que je n'ai pas défini de zone de diffusion + Quand j'essaie de démarrer un live + Alors je vois le message "Veuillez définir une zone de diffusion avant de démarrer" + Et je suis redirigé vers le formulaire de configuration du live + + Scénario: Démarrage live avec buffer 15 secondes + Étant donné que toutes les vérifications pré-live sont validées + Quand j'appuie sur "Démarrer live" + Alors je vois le message "Live démarre dans 15s... Testez votre micro" + Et un compte à rebours de 15 secondes s'affiche + Et mon flux audio est enregistré pendant ces 15 secondes + Et le live n'est pas encore visible publiquement + + Scénario: Live devient public après buffer initial + Étant donné que j'ai démarré un live + Et que le buffer de 15 secondes s'est écoulé + Alors le live devient public + Et les auditeurs peuvent le rejoindre + Et les abonnés dans la zone reçoivent une notification push + + Scénario: Notification push aux abonnés dans la zone géographique + Étant donné que j'ai 1000 abonnés au total + Et que 300 abonnés sont situés en Île-de-France + Et que 700 abonnés sont situés hors Île-de-France + Quand mon live en Île-de-France devient public + Alors 300 abonnés reçoivent une notification push "🔴 [Mon pseudo] est en direct : [Titre live]" + Et 700 abonnés ne reçoivent pas de notification + + Scénario: Configuration métadonnées obligatoires pour un live + Quand je configure un nouveau live + Alors je dois renseigner: + | champ | format | validation | + | Titre | 5-100 caractères | Obligatoire | + | Tags | 1-3 centres intérêt| Sélection liste prédéfinie | + | Classification âge | Enum | Tout public / 13+ / 16+ / 18+ | + | Zone diffusion | Geo | Ville / Département / Région / National | + + Scénario: Validation échouée avec titre trop court + Quand j'essaie de créer un live avec le titre "Live" + Alors la validation échoue + Et je vois le message "Le titre doit contenir entre 5 et 100 caractères" + + Scénario: Validation échouée sans tags + Étant donné que j'ai rempli tous les champs sauf les tags + Quand j'essaie de démarrer le live + Alors la validation échoue + Et je vois le message "Veuillez sélectionner entre 1 et 3 centres d'intérêt" + + Scénario: Limite de durée 8 heures + Étant donné que mon live dure depuis 7 heures et 30 minutes + Alors je vois un warning "Votre live se terminera dans 30 min" + Et le message est affiché de manière non intrusive + + Scénario: Arrêt automatique à 8 heures + Étant donné que mon live dure depuis 8 heures + Alors le live s'arrête automatiquement + Et je vois le message "Durée maximale atteinte (8 heures). Vous pouvez redémarrer un nouveau live si nécessaire" + Et le processus de traitement post-live démarre + + Scénario: Diffusion contenu interdit - Concert en direct + Étant donné que je diffuse un concert en direct depuis une salle + Et qu'un auditeur signale le contenu pour "Violation droits d'auteur" + Quand un modérateur écoute le live + Et qu'il confirme la violation + Alors le live est arrêté immédiatement + Et je reçois un Strike 2 (suspension 7 jours) + Et je vois le message "Votre live a été interrompu pour violation des droits d'auteur" + Et le replay n'est pas publié + + Scénario: Diffusion contenu interdit - Événement sportif payant + Étant donné que je diffuse un match de football avec droits TV + Et que le contenu est détecté par l'IA audio fingerprint + Quand la détection est confirmée + Alors le live est arrêté immédiatement + Et je reçois un Strike 2 (suspension 7 jours) + + Scénario: Diffusion contenu violent + Étant donné que je diffuse du contenu violent (agression physique) + Et que 5 auditeurs signalent le contenu + Quand un modérateur vérifie en temps réel + Et confirme la violence + Alors le live est coupé immédiatement + Et mon compte est banni définitivement + Et les autorités sont notifiées + + Scénario: Détection musique protégée en arrière-plan + Étant donné que mon live contient de la musique protégée en fond + Quand l'IA audio fingerprint détecte la violation après 2 minutes + Alors je reçois un avertissement en direct "Musique protégée détectée. Veuillez couper le son ou risquez un arrêt du live" + Et j'ai 30 secondes pour corriger + Et si je ne corrige pas, le live est arrêté avec Strike 1 + + Scénario: Signalement pendant un live + Étant donné que je diffuse un live + Et qu'un auditeur clique sur "Signaler" + Quand l'auditeur sélectionne la catégorie "Harcèlement" + Alors le signalement est envoyé en priorité HAUTE + Et un modérateur peut écouter le live en temps réel + Et le live continue pendant l'écoute de vérification + + Scénario: Dépassement nombre de lives simultanés autorisés (limite plateforme) + Étant donné que la plateforme héberge actuellement 2000 lives simultanés + Et que c'est la limite de l'infrastructure actuelle + Quand j'essaie de démarrer un nouveau live + Alors je vois le message "Capacité maximale atteinte. Veuillez réessayer dans quelques minutes" + Et ma demande est mise en file d'attente prioritaire si je suis créateur Premium + + Scénario: Premier live d'un nouveau créateur + Étant donné que je n'ai jamais diffusé de live auparavant + Et que j'ai moins de 3 contenus validés + Quand j'essaie de démarrer mon premier live + Alors je vois le message "Les lives sont disponibles après validation de vos 3 premiers contenus" + Et le bouton "Démarrer live" est désactivé + + Scénario: Créateur avec score de confiance faible + Étant donné que j'ai 2 strikes actifs + Quand j'essaie de démarrer un live + Alors je vois le message "Fonctionnalité live temporairement indisponible suite à vos sanctions" + Et je dois attendre la fin de ma suspension diff --git a/features/recherche/recherche.feature b/features/recherche/recherche.feature new file mode 100644 index 0000000..597961f --- /dev/null +++ b/features/recherche/recherche.feature @@ -0,0 +1,472 @@ +# language: fr + +Fonctionnalité: Recherche de contenu + En tant qu'utilisateur de RoadWave + Je veux rechercher des contenus audio par mots-clés, localisation et filtres + Afin de trouver facilement le contenu qui m'intéresse + + Contexte: + Étant donné que l'application RoadWave est démarrée + Et que l'utilisateur "jean@example.com" est connecté + + # 15.3.1 - Recherche par mot-clé + + Scénario: Recherche full-text basique + Étant donné que la base contient les contenus suivants: + | titre | description | créateur | + | Balade à Paris | Visite du quartier Latin | @paris_stories | + | Secrets de Montmartre | Histoire de la butte | @explore_paris | + | Voyage en Normandie | Découverte des plages | @voyages_fr | + Quand l'utilisateur recherche "paris" + Alors 2 résultats sont retournés + Et les résultats incluent "Balade à Paris" + Et les résultats incluent "Secrets de Montmartre" + + Scénario: Recherche avec stemming français + Étant donné un contenu avec le titre "Voyage en Bretagne" + Quand l'utilisateur recherche "voyages" + Alors le contenu "Voyage en Bretagne" est trouvé + Et le stemming a transformé "voyages" en racine "voyag" + + Plan du Scénario: Stemming français sur différentes formes + Étant donné un contenu avec le mot "" + Quand l'utilisateur recherche "" + Alors le contenu est trouvé grâce au stemming français + + Exemples: + | mot_original | recherche | + | voyage | voyages | + | voyager | voyage | + | balades | balade | + | historique | histoire | + + Scénario: Recherche avec accents ignorés + Étant donné un contenu avec le titre "Découverte de l'Élysée" + Quand l'utilisateur recherche "decouverte elysee" + Alors le contenu est trouvé + Et les accents sont normalisés automatiquement + + Scénario: Champs indexés avec pondération + Étant donné les contenus suivants: + | titre | description | créateur | tags | + | Voyage Paris | Balade sympa | @user1 | Tourisme | + | Balade Lyon | Voyage en ville | @paris_guide | Voyage | + Quand l'utilisateur recherche "paris" + Alors "Voyage Paris" est en première position + Parce que le titre a un poids × 3 + Et "@paris_guide" apparaît en second + Parce que le créateur a un poids × 2 + + Scénario: Ranking par pertinence et popularité + Étant donné les contenus suivants: + | titre | écoutes | rang_texte | + | Balade Paris | 50000 | 0.8 | + | Paris la nuit | 1000 | 0.9 | + Quand l'utilisateur recherche "paris" + Alors le score final combine rang_texte × (1 + log(écoutes + 1)) + Et "Balade Paris" est mieux classé grâce à sa popularité + + Scénario: Autocomplete pendant la frappe + Étant donné que l'utilisateur commence à taper "par" + Quand 3 caractères sont saisis + Alors des suggestions apparaissent: + | suggestion | + | paris | + | parc naturel | + | parvis notre-dame | + Et le top 5 des suggestions est affiché + + Scénario: Historique des 10 dernières recherches + Étant donné que l'utilisateur a effectué les recherches suivantes: + | recherche | date | + | voyage paris | 2026-01-20 | + | audio-guide louvre | 2026-01-19 | + | podcast automobile | 2026-01-18 | + Quand l'utilisateur ouvre la barre de recherche + Alors les 10 dernières recherches sont affichées + Et elles sont triées par date décroissante + + Scénario: Correction automatique si aucun résultat + Étant donné que l'utilisateur recherche "ballade paris" (faute d'orthographe) + Et qu'aucun résultat n'est trouvé + Quand la page de résultats s'affiche + Alors une suggestion "Essayez plutôt : balade paris" est affichée + + Scénario: Recherches populaires suggérées + Étant donné qu'aucun résultat n'est trouvé pour une recherche + Quand la page s'affiche + Alors des suggestions populaires sont affichées: + | suggestion | + | balade paris | + | audio-guide louvre | + | visite montmartre | + + # 15.3.2 - Recherche géographique + + Scénario: Saisie d'un lieu avec autocomplete + Étant donné que l'utilisateur ouvre le filtre "Lieu" + Quand il tape "Louv" + Alors Nominatim retourne des suggestions: + | suggestion | type | + | Musée du Louvre, Paris | monument | + | Louvres, Val-d'Oise | commune | + + Scénario: Sélection d'un lieu et définition du rayon + Étant donné que l'utilisateur sélectionne "Paris, France" + Et que les coordonnées sont (48.8566, 2.3522) + Quand il définit un rayon de 50 km + Alors la recherche PostGIS utilise ST_DWithin avec 50000 mètres + + Plan du Scénario: Recherche géographique avec différents rayons + Étant donné un contenu à 30 km de Paris + Quand l'utilisateur recherche autour de Paris avec un rayon de + Alors le contenu est + + Exemples: + | rayon | résultat | + | 20 km | non trouvé | + | 50 km | trouvé | + | 100 km | trouvé | + + Scénario: Utilisation de "Autour de moi" (GPS actuel) + Étant donné que l'utilisateur active le GPS + Et que sa position est (48.8566, 2.3522) + Quand il sélectionne "Autour de moi" + Alors la recherche utilise ses coordonnées GPS actuelles + Et un rayon par défaut de 10 km est appliqué + + Scénario: Curseur de rayon avec limites + Étant donné que l'utilisateur ouvre le curseur de rayon + Quand il ajuste le curseur + Alors les valeurs disponibles vont de 5 km à 500 km + Et la valeur s'affiche en temps réel "50 km" + + Scénario: Affichage de la distance dans les résultats + Étant donné une recherche géographique autour de Paris + Et un contenu à 2.3 km de distance + Quand les résultats sont affichés + Alors la distance "À 2.3 km" est indiquée pour chaque résultat + + Plan du Scénario: Tri par proximité géographique + Étant donné des contenus à différentes distances de Paris: + | contenu | distance | + | Louvre Guide | 0.5 km | + | Tour Eiffel | 2.0 km | + | Versailles | 20 km | + Quand l'utilisateur trie par "Proximité" + Alors les résultats sont affichés dans l'ordre: + | position | contenu | + | 1 | Louvre Guide | + | 2 | Tour Eiffel | + | 3 | Versailles | + + Scénario: Géocodage avec Nominatim (MVP) + Étant donné que l'application est en phase MVP + Quand une requête de géocodage est effectuée + Alors l'API publique Nominatim est utilisée + Et le rate limit de 1 req/s est respecté + + Scénario: Géocodage avec fallback Mapbox + Étant donné que Nominatim ne retourne aucun résultat + Quand l'application tente un fallback + Alors l'API Mapbox Geocoding est utilisée + Et le coût de 0.50€ / 1000 requêtes est appliqué + + # 15.3.3 - Filtres avancés + + Scénario: Ouverture du panneau de filtres + Étant donné que l'utilisateur est sur la page de recherche + Quand il clique sur "Filtres" + Alors un panneau latéral s'ouvre + Et 7 catégories de filtres sont affichées: + | catégorie | + | Type de contenu | + | Durée | + | Classification âge | + | Géo-pertinence | + | Tags | + | Date de publication | + | Abonnement | + + Scénario: Filtre par type de contenu (multi-sélection) + Étant donné que l'utilisateur ouvre les filtres + Quand il sélectionne: + | type | + | Contenu court | + | Audio-guide | + Alors seuls ces types de contenus sont recherchés + Et les podcasts et radios live sont exclus + + Plan du Scénario: Filtre par durée + Étant donné un contenu de minutes + Quand l'utilisateur filtre par "" + Alors le contenu est + + Exemples: + | durée | tranche | résultat | + | 3 | <5 min | trouvé | + | 3 | 5-15 min | non trouvé | + | 10 | 5-15 min | trouvé | + | 20 | 15-30 min | trouvé | + | 45 | >30 min | trouvé | + + Scénario: Filtre par classification âge + Étant donné des contenus avec différentes classifications: + | contenu | classification | + | Conte enfants | Tout public | + | Podcast news | 13+ | + | Débat politique | 16+ | + Quand l'utilisateur filtre "Tout public" + Alors seul "Conte enfants" est affiché + + Scénario: Filtre par géo-pertinence + Étant donné des contenus avec différents types géo: + | contenu | type_geo | + | Guide Louvre | Ancré | + | Podcast Paris | Contextuel | + | News nationales | Neutre | + Quand l'utilisateur filtre "Ancré, Contextuel" + Alors "Guide Louvre" et "Podcast Paris" sont affichés + Et "News nationales" est exclu + + Scénario: Filtre par tags (multi-sélection) + Étant donné des contenus taggés: + | contenu | tags | + | Voyage en Italie | Voyage, Gastronomie | + | Histoire de Rome | Voyage, Histoire | + | Économie italienne | Économie | + Quand l'utilisateur sélectionne les tags "Voyage, Histoire" + Alors "Histoire de Rome" est en priorité (2 tags correspondants) + Et "Voyage en Italie" est affiché (1 tag correspondant) + Et "Économie italienne" est exclu + + Plan du Scénario: Filtre par date de publication + Étant donné un contenu publié il y a + Quand l'utilisateur filtre par "" + Alors le contenu est + + Exemples: + | délai | période | résultat | + | 12 heures | Dernières 24h | trouvé | + | 3 jours | Cette semaine | trouvé | + | 15 jours | Ce mois | trouvé | + | 8 mois | Cette année | trouvé | + | 2 ans | Toutes dates | trouvé | + | 2 ans | Cette année | non trouvé | + + Scénario: Filtre par type d'abonnement + Étant donné des contenus gratuits et Premium: + | contenu | type | + | Balade Paris | Gratuit | + | Visite VIP Louvre | Premium | + Quand l'utilisateur filtre "Premium uniquement 👑" + Alors seul "Visite VIP Louvre" est affiché + + Scénario: Combinaison de filtres multiples (AND logic) + Étant donné que l'utilisateur applique les filtres: + | filtre | valeur | + | Type | Audio-guide | + | Durée | 5-15 min | + | Tags | Voyage | + | Classification | Tout public | + Quand la recherche est lancée + Alors seuls les contenus respectant TOUS les critères sont affichés + + Scénario: Réinitialisation des filtres + Étant donné que l'utilisateur a appliqué 5 filtres différents + Quand il clique sur "Réinitialiser" + Alors tous les filtres sont désactivés + Et la recherche affiche tous les résultats + + Scénario: Sauvegarde d'une recherche + Étant donné que l'utilisateur a appliqué plusieurs filtres + Quand il clique sur "💾 Sauvegarder cette recherche" + Et qu'il entre le nom "Podcasts voyage Paris" + Alors la recherche est sauvegardée + Et elle apparaît dans l'onglet "Recherches sauvegardées" + + Scénario: Limite de 5 recherches sauvegardées + Étant donné que l'utilisateur a déjà 5 recherches sauvegardées + Quand il tente de sauvegarder une 6ème recherche + Alors un message d'erreur s'affiche + Et il doit supprimer une recherche existante avant d'en ajouter une nouvelle + + Scénario: Notifications pour recherches sauvegardées + Étant donné une recherche sauvegardée "Podcasts voyage Paris" + Et que l'utilisateur a activé les notifications + Quand 3 nouveaux contenus correspondants sont publiés + Alors une notification "3 nouveaux contenus dans 'Podcasts voyage Paris'" est envoyée + + Plan du Scénario: Options de tri des résultats + Étant donné une recherche avec plusieurs résultats + Quand l'utilisateur sélectionne le tri "