# Story 1.7: Admin - Changement de Tier Manuel Status: done ## Story As an **Admin**, I want **to change a user's tier from Free to Pro (or vice versa)**, so that **I can manually onboard Pro clients**. ## Acceptance Criteria 1. **AC1: Mise à jour immédiate** — Given I am logged in as admin, when I update a user's tier via PATCH /api/v1/admin/users/{user_id} (or PATCH /admin/users/{user_id} si préfixe actuel), then the user's tier/plan is updated immediately in the database. 2. **AC2: Passage à Pro** — If upgraded to Pro (or equivalent plan), user can access LLM modes immediately (rate limit unlimited; tier_quota considère déjà pro/business/enterprise comme illimité). 3. **AC3: Passage à Free** — If downgraded to Free, rate limiting (5 files/day) applies immediately; prochaine requête de traduction respecte le quota free. 4. **AC4: Audit** — Action is logged with timestamp and admin identifier (admin session or user_id if admin is a user); structlog ou équivalent, sans contenu document. ## Tasks / Subtasks - [x] **Task 1: Endpoint PATCH admin users/{user_id}** (AC: 1, 2, 3) - [x] 1.1 Add PATCH /api/v1/admin/users/{user_id} (or /admin/users/{user_id} pour cohérence avec GET /admin/users) with body e.g. `{ "tier": "pro" }` or `{ "plan": "pro" }` selon modèle (User.tier string vs User.plan enum). - [x] 1.2 Protect route with `Depends(require_admin)`. - [x] 1.3 Resolve user by user_id (UUID); return 404 if user not found. - [x] 1.4 Validate tier/plan value: allow only "free" | "pro" (et éventuellement starter, business, enterprise si dans PlanType); return 400 for invalid value. - [x] 1.5 Update User in DB (tier and/or plan); ensure tier_quota and rate limiting use same source (plan or tier). - [x] **Task 2: Effet immédiat tier** (AC: 2, 3) - [x] 2.1 No cache invalidation required if tier is read from DB on each request (get_current_user / tier_quota check). - [x] 2.2 If any in-memory or Redis cache of user tier exists, invalidate or update on PATCH. - [x] **Task 3: Logging audit** (AC: 4) - [x] 3.1 Log action: e.g. event "admin_tier_change", user_id target, new_tier, admin_id or "admin_session", timestamp (ISO8601). - [x] 3.2 Do not log document content; only metadata (NFR11, NFR16). - [x] **Task 4: Tests** (AC: 1–4) - [x] 4.1 Test: admin PATCH with valid tier → 200, user tier updated in DB. - [x] 4.2 Test: admin PATCH with invalid tier → 400. - [x] 4.3 Test: admin PATCH with unknown user_id → 404. - [x] 4.4 Test: non-admin PATCH → 401/403. - [x] 4.5 Test: after upgrade to pro, user can translate beyond 5/day; after downgrade to free, quota 5 applies. ## Dev Notes - **Contexte actuel** : GET /admin/users existe dans main.py et utilise `load_users()` (auth_service); les users sont exposés avec plan, email, usage. Il n’existe pas encore de PATCH pour modifier le tier/plan. Le modèle User (database/models.py) a à la fois `tier` (string) et `plan` (PlanType enum: free, starter, pro, business, enterprise). Le middleware tier_quota (story 1.6) utilise le plan pour déterminer si illimité (pro/business/enterprise). - **Alignement** : Décider si l’API admin expose `tier` ou `plan`; garder cohérence avec User.tier et User.plan (les deux en sync lors du PATCH). - **Auth admin** : require_admin (Bearer token après /admin/login) est déjà en place; réutiliser pour le PATCH. ### Project Structure Notes - Backend à la racine : main.py (routes admin), database/models.py (User), services/auth_service.py (load_users, potentiellement update_user_tier ou équivalent). - Pas de module admin séparé pour l’instant; les routes admin sont dans main.py. Option : ajouter la logique dans main.py ou créer routes/admin_routes.py et l’inclure. ### References - [Source: _bmad-output/planning-artifacts/epics.md#Story 1.7] - [Source: _bmad-output/planning-artifacts/architecture.md#API Response Formats] - [Source: _bmad-output/planning-artifacts/prd.md#FR21, FR22 - Admin change tier] - [Source: _bmad-output/implementation-artifacts/1-6-middleware-rate-limiting-par-tier.md — tier_quota, plan/tier] --- ## Developer Context (Guardrails) ### Technical requirements - **Backend** : FastAPI. Endpoint PATCH pour mettre à jour le tier/plan d’un utilisateur cible; authentification admin obligatoire (require_admin). - **DB** : Mise à jour de User.tier et User.plan (ou un seul si un seul est la source de vérité); persistance immédiate. - **Rate limiting** : Après changement, le prochain appel qui lit le user (get_current_user + tier_quota) doit voir le nouveau tier; pas de cache stale (si cache, invalider). - **Réponses** : 200 avec `data` contenant l’utilisateur mis à jour (id, email, tier/plan); 400 body invalide; 404 user non trouvé; 401/403 non-admin. ### Architecture compliance - Conventions API : JSON snake_case; format succès `{ data, meta }`; format erreur `{ error, message, details? }` sans champ `data`. - Préfixe : /api/v1/ pour cohérence avec le reste (epics indiquent PATCH /api/v1/admin/users/{user_id}); si le projet utilise /admin/users sans /api/v1/, documenter et rester cohérent avec GET /admin/users. ### Library / framework - FastAPI Depends(require_admin) existant. - SQLAlchemy/session pour update User; utiliser la même couche d’accès que auth_service (database/connection, etc.). ### File structure - Route : main.py (ou routes/admin_routes.py si créé) — PATCH pour users/{user_id}. - Service : soit dans services/auth_service.py (update_user_plan / update_user_tier), soit dans un nouveau services/admin_service.py. - Modèle : database/models.py (User.tier, User.plan) — pas de nouvelle table. ### Testing requirements - Tests d’intégration ou unitaires : admin PATCH 200 et vérification en DB; PATCH invalid tier 400; user_id inconnu 404; sans token admin 401/403; test optionnel : après upgrade pro, quota illimité; après downgrade free, 6ème requête 429. --- ## Previous Story Intelligence (1-6 Middleware Rate Limiting par Tier) - **Fichiers modifiés** : middleware/tier_quota.py (TierQuotaService, Redis + in-memory), main.py (quota check sur /translate, 429 QUOTA_EXCEEDED, X-Rate-Limit-* headers), models/subscription.py (daily_translation_count), database/models.py (User.tier, plan), tests/test_tier_rate_limit.py. - **Patterns** : Le quota est déterminé par le plan utilisateur (pro/business/enterprise = illimité; free = 5/jour). get_current_user est utilisé pour obtenir l’utilisateur; la logique tier est dans tier_quota.check_quota(user_id, plan/tier). Pour la story 1.7, après un PATCH réussi, le prochain appel get_current_user + tier_quota doit voir le nouveau plan/tier — si les users sont chargés depuis la DB à chaque requête, aucun cache à invalider; sinon (cache Redis/session), invalider ou mettre à jour. - **Tests** : Fixtures free/pro user, mocks Redis; pour 1.7 réutiliser require_admin et ajouter fixtures admin token. --- ## Git Intelligence Summary - Codebase actuelle : routes admin sous /admin/* (login, logout, users, stats, cleanup, etc.); pas de PATCH users. User avec tier et plan. Tier quota lit le plan pour unlimited. Implémentation 1.7 = ajouter un seul endpoint PATCH et la logique de mise à jour + log audit. --- ## Latest Tech Information - **FastAPI** : Pour PATCH, utiliser un body Pydantic (e.g. AdminUpdateUserTierRequest avec champ tier ou plan); Path() pour user_id. Réponse 200 avec schema utilisateur (snake_case). - **Logging** : structlog ou logging standard avec event structuré (admin_tier_change, target_user_id, new_tier, timestamp); pas de contenu document (NFR11). --- ## Project Context Reference - **Structure** : main.py contient les routes admin; auth via require_admin (Bearer token). GET /admin/users utilise load_users() depuis auth_service. Base de données : database/models.py (User), database/connection.py. Pour modifier un user, il faudra soit étendre auth_service (update_user_plan), soit un service admin dédié qui utilise la même session/DB. - **Montage** : Ajouter une route PATCH à côté de GET /admin/users; garder le même préfixe (/admin/users ou /api/v1/admin/users selon convention projet). --- ## Story Completion Status - **Status** : review - **Note** : Implementation complete; ready for code review. ## Change Log - 2026-02-20: Story 1.7 created from epics + architecture + previous story 1.6. - 2026-02-20: Implemented PATCH /admin/users/{user_id}, update_user_plan, audit logging, tests (AC1–4). - 2026-02-20: Code review (AI): corrections appliquées — format erreur API (404/400 → `{ error, message, details? }`), schéma Pydantic Literal pour plan, datetime UTC dans repository, tests adaptés. ## Dev Agent Record ### Agent Model Used {{agent_model_name_version}} ### Debug Log References ### Completion Notes List - PATCH /admin/users/{user_id} with body `{ "plan": "free"|"starter"|"pro"|"business"|"enterprise" }`; protected by require_admin; 200 with data (id, email, name, plan, tier), 400 invalid plan (INVALID_PLAN), 404 user not found (NOT_FOUND), 401 without/invalid admin token. - Réponses erreur conformes à l’architecture : 404 → `{ "error": "NOT_FOUND", "message": "User not found" }`; 400 → `{ "error": "INVALID_PLAN", "message": "...", "details": { "allowed": [...] } }` (pas de champ `data`). - auth_service.update_user_plan(user_id, plan) keeps User.plan and User.tier in sync (tier "pro" for pro/business/enterprise, "free" otherwise); works with JSON and DB backends. - Audit log: logger.info("admin_tier_change", extra={ event, target_user_id, new_tier, new_plan, admin_id: "admin_session", timestamp ISO8601 }); no document content (AC4). - Task 2: tier/plan read from DB (or JSON) on each request via get_current_user; no user-tier cache to invalidate. - Tests: test_admin_tier_change.py (7 tests) — 4.1–4.5 plus 401 without/invalid token; test 4.2 accepte 400 ou 422 (Literal → 422 pour plan invalide). - Post-review: AdminUpdateUserTierRequest.plan en Literal; database/repositories.py updated_at en datetime.now(timezone.utc). ### File List - main.py (PATCH /admin/users/{user_id}, AdminUpdateUserTierRequest avec Literal plan, JSONResponse 404/400 format architecture) - services/auth_service.py (update_user_plan, VALID_PLAN_VALUES) - database/repositories.py (updated_at timezone-aware UTC) - tests/test_admin_tier_change.py (new) --- ## Senior Developer Review (AI) - **Date :** 2026-02-20 - **Résultat :** Approuvé après correctifs automatiques. - **Problèmes traités :** Format erreur API (HIGH) — 404/400 retournent désormais `{ error, message, details? }`. Schéma Pydantic avec Literal pour plan (LOW). Datetime UTC dans UserRepository.update (LOW). Tests adaptés pour 400/422 et nouveau format erreur. - **Recommandation :** Commiter le dossier `tests/` si pas encore versionné (`git add tests/`).