Feature #1153
openConsommation de crédits au dépôt de ticket (coût = score de complexité LLM, par pool)
0%
issue:self — spec dans la description (décisions verrouillées + seams)
Description
## Objectif
Quand un client **dépose un ticket** (bouton « créer le ticket » → ticket créé), déduire son **coût en crédits** du pool correspondant. Le coût = le **score de complexité estimé par le LLM** (0–10) déjà produit au moment de la rédaction du draft (champ `complexity.score`). Pas de déduction au simple remplissage : c'est l'acte de **filer le ticket** qui débite.
Dépend du modèle billing 2 pools (épopée #1147, sur `dev`).
## Décisions produit (verrouillées)
- **Coût** = `draft.complexity.score` (entier 0–10, le « point » de crédit estimé par le LLM). Utilisé tel quel, sans multiplicateur.
- **Mapping pool** : `feature` → pool **évolutions** ; `bug` → pool **maintenance**. (Le flux de création conversationnel ne gère que bug/feature ; secops = tracker séparé, hors scope ici.)
- **Crédits insuffisants** → **bloquer le dépôt** (le ticket n'est PAS créé) si le pool **fini** ne couvre pas le coût. Message clair « crédits insuffisants » (coût demandé + solde restant).
- **Pool illimité** (tier Maintenance/Évolutions/Pilotage) → toujours autorisé, **aucune déduction**.
- Plancher à 0 (jamais négatif).
## Portée backend (api/, hexagonal, TDD)
Seams identifiés (origin/dev) :
- Use-case de dépôt : `application/create_ticket_from_draft.py` → `CreateTicketFromDraft.__call__` (l'issue Redmine est créée ~ligne 147-161). Endpoint : `presentation/api/conversations.py` `create_ticket()` (~502-550), `client: Client = Depends(current_client)` déjà présent → passer `client_id=client.id`.
- Déduction : `application/billing.py` `AdjustCredits` (pool + delta, plancher 0, audit, events). Réutiliser, ne pas réimplémenter.
- Pools : `domain/billing/entities.py` `CreditPool` (MAINTENANCE/EVOLUTION).
Comportement :
1. Calculer `cost = clamp(draft.complexity.score, 0, 10)` ; `pool = EVOLUTION si issue_type==feature sinon MAINTENANCE`.
2. **Pré-vérifier** le solde : si le pool est **fini** et `remaining < cost` → lever une erreur métier `InsufficientCreditsError` AVANT de créer l'issue Redmine (aucun ticket créé). Mapper en HTTP (422 ou 409) avec un detail structuré `{ message, pool, cost, remaining }`.
3. Si le pool est **illimité** → créer le ticket, **pas** de déduction.
4. Sinon : créer l'issue Redmine, puis déduire `cost` du pool via `AdjustCredits(delta=-cost)`. La déduction doit être **traçable au ticket** (référencer l'issue id dans l'audit — ex. action `ticket_filed` ou un champ raison/ref ; au minimum l'audit billing doit permettre de relier la déduction au ticket déposé). `cost == 0` → pas de débit (skip), ticket créé normalement.
5. La déduction se fait dans la même session/commit que possible ; l'issue Redmine est l'effet externe irréversible — ordonner pré-check → create issue → deduct (committé) ; logguer toute défaillance résiduelle de déduction (ne pas laisser un état incohérent silencieux).
6. Injecter les dépendances billing dans `CreateTicketFromDraft` (ou un petit use-case `ChargeTicketCredits`) ; garder la couche domaine pure.
## Portée frontend (src/, TDD, next-intl)
- `src/components/organisms/TicketDraftPanel.tsx` + `src/features/chat/useCreateTicket.ts` : afficher le **coût** du ticket (= score, ex. « Coût : 2 crédits — pool Évolutions ») et le **solde restant** du pool ciblé ; le coût/pool se met à jour si le client change la classification bug/feature. Si le pool fini ne couvre pas le coût → **désactiver** le bouton de dépôt + bandeau « Crédits insuffisants » (coût vs solde). Pool illimité → afficher « illimité », pas de blocage.
- Gérer aussi la réponse d'erreur backend (422/409 crédits insuffisants) au cas où l'état change entre l'affichage et le dépôt.
- Après dépôt réussi : invalider la query du solde (`GET /billing/clients/{id}/credits`) pour rafraîchir la jauge profil.
- i18n `en.ts` ET `fr.ts` (coût, pool, « crédits insuffisants », « illimité »), placeholders ICU identiques.
## Acceptance criteria
- [ ] Déposer un ticket `feature` débite `complexity.score` crédits du pool **évolutions** ; `bug` du pool **maintenance**.
- [ ] Le coût affiché = le score de complexité du draft ; suit le changement de classification.
- [ ] Pool **fini** insuffisant → dépôt **bloqué** (aucun ticket créé), message « crédits insuffisants » (coût + solde) côté API et UI.
- [ ] Pool **illimité** → ticket déposé, aucune déduction.
- [ ] `cost == 0` → ticket déposé, aucun débit.
- [ ] La déduction écrit une entrée d'historique billing reliable au ticket déposé ; alerte crédits bas (<20 %) émise si franchissement sur un pool fini.
- [ ] Le solde profil se rafraîchit après dépôt.
- [ ] Backend : pytest verts, ruff, mypy. Frontend : vitest, tsc, eslint, build verts. i18n en/fr cohérents.