Feature #1148
openFeature #1147: Tiers réels — 2 pools (maintenance/évolutions) + flux/SLA + page /offres + jauge à 2 indicateurs
Backend — Modèle tiers à 2 pools (maintenance/évolutions illimitables) + reseed + API + ajustement/alertes par pool
0%
issue:1147 — spec dans l'épopée + description
Description
## Objectif
Faire évoluer le modèle billing de #1133 d'un pool unique de crédits vers **deux pools** (maintenance + évolutions), chacun étant un entier OU illimité (NULL). Racine de l'épopée #1147 — le frontend (#offres + jauge) en dépend.
## Portée (api/, hexagonal, TDD)
- **Domaine `Tier`** : remplacer `monthly_credits` (single) par `maintenance_credits: int | None` (None = illimité) et `evolution_credits: int | None`, ajouter `streams: int`. Garder `monthly_price_eur`. Le `flux`/`sla` textuels actuels peuvent rester ou être enrichis, mais les vraies valeurs structurées (flux=streams, etc.) priment. **Valeurs réelles** (voir #1147) : Démarrage 100/10/1flux ; Maintenance ∞/20/1 ; Évolutions ∞/∞/2 ; Pilotage ∞/∞/4.
- **`CreditBalance`** : suivre `maintenance_remaining: int | None` et `evolution_remaining: int | None` par client (None = illimité, jamais « bas », jamais décrémenté). `usage_ratio`/`is_low` deviennent par-pool (un pool illimité n'est jamais bas).
- **Migration Alembic** (nouvelle révision) : ALTER `tiers` (colonnes maintenance_credits/evolution_credits/streams, nullable) + ALTER `credit_balances` (maintenance_remaining/evolution_remaining nullable) ; **reseed** des 4 tiers avec les vraies valeurs ; migrer les `credit_balances` existants vers le nouveau schéma (recalcul depuis la définition de tier = allocation pleine, pools illimités → NULL). Gérer le down-revision proprement (chaîne sur la dernière migration billing actuelle).
- **Application** : `ChangeTier` réinitialise les deux pools selon la nouvelle formule (upgrade = pleine allocation, illimité → NULL). `AdjustCredits` doit cibler **un pool** (`pool: "maintenance" | "evolution"`) ; ajuster un pool illimité → 422 (rien à ajuster). Audit : enregistrer le pool concerné. `LowCredits` n'est émis que pour un pool **fini** passant sous 20 % (jamais pour un pool illimité).
- **Présentation/API** : mettre à jour les schémas de sortie : `TierOut { code, name, monthly_price_eur, maintenance_credits|null, evolution_credits|null, streams, ... }`, `CreditBalanceOut { maintenance_remaining|null, evolution_remaining|null, maintenance_unlimited, evolution_unlimited, ... + ratios/is_low par pool }`. `GET /billing/clients` (admin) expose les deux pools + verdict par pool. L'ajustement admin prend le `pool`. **Documenter le nouveau contrat dans le rapport** (le frontend #offres en dépend).
## Acceptance criteria
- [ ] Les 4 tiers ont maintenance/evolution (entier ou NULL=illimité) + streams, reseedés via migration ; valeurs = matrice #1147.
- [ ] `CreditBalanceOut` expose les deux pools, avec « illimité » distinct de 0, ratios/is_low par pool, pool illimité jamais « bas ».
- [ ] `ChangeTier` règle les deux pools (upgrade = plein, illimité = NULL) ; même tier → 409.
- [ ] `AdjustCredits` cible un pool ; pool illimité → 422 ; audit enregistre le pool ; non-admin → 403.
- [ ] `LowCredits` n'est émis que pour un pool fini franchissant <20 % (une fois par franchissement).
- [ ] Migration up/down propre, idempotente, credit_balances existants migrés sans perte ; `alembic upgrade head` OK.
- [ ] pytest verts (TDD), `ruff check .`, `mypy src` propres. Ne pas casser les events #1142 (PasswordChanged/TierChanged/CreditsAdjusted/LowCredits) — adapter CreditsAdjusted/LowCredits au pool.