Feature #1169
openRemboursement des crédits à la suppression d'un ticket (Soumis/Spec)
0%
issue:self — spec dans la description (décisions + seams)
Description
## Problème
La consommation de crédits au dépôt (#1153) fonctionne (ex. ticket #1158 → −8 évolutions). Mais **supprimer** un ticket ne **rembourse pas** les crédits débités, alors qu'il le devrait : la suppression n'est possible que tant que le ticket est **Soumis ou Spec** (avant le début du dev — déjà garanti par `ticket_is_mutable`), donc aucun travail n'a été consommé et les crédits doivent revenir.
## Décisions (verrouillées)
- **Déclencheur** : toute suppression réussie d'un ticket (l'endpoint delete est déjà restreint à Soumis/Spec via `ticket_is_mutable` → pas de logique de statut supplémentaire à ajouter ; rembourser à chaque delete réussi).
- **Montant** = le montant **réellement débité** au dépôt, lu depuis l'audit billing (`action=ticket_filed`, `reason="ticket:<issue_id>"`) → `old_value - new_value`, **vers le même pool**. (Robuste face à un score édité ou un flooring.)
- **Pas de débit trouvé** (déposé avant #1153, ou pool illimité au dépôt → aucune ligne `ticket_filed`) → **rien à rembourser**, suppression normale.
- **Anti double-remboursement** : ne rembourser que s'il n'existe pas déjà une ligne de remboursement pour ce ticket.
- **Plafond** : ne pas dépasser l'allocation mensuelle du pool (cap à `monthly_credits` — ex. si le client a changé de formule entre-temps).
- **Pool désormais illimité** → pas de remboursement (rien à créditer).
## Portée backend (api/, hexagonal, TDD)
Seams (origin/dev) :
- `application/ticket_management.py` → `DeleteTicket.__call__` (actuellement : `get_issue` → `ticket_is_mutable` guard → `redmine.delete_issue`). Endpoint : `presentation/api/tickets.py` `delete_ticket()` (~193), `owned: OwnedIssue` + client courant disponibles.
- Remboursement via `application/billing.py` `AdjustCredits` (delta **positif**), nouvelle action d'audit `TICKET_REFUNDED` (ou réutiliser une action avec `reason="ticket:<id>"`), pool = celui du débit. Lookup du débit via `AuditRepo.list_for_client` (filtrer `action=ticket_filed` + `reason="ticket:<id>"`).
- Le `client_id` propriétaire doit être passé au use-case (l'endpoint a le client courant / `owned`).
Comportement :
1. (inchangé) garde `ticket_is_mutable` ; supprimer l'issue Redmine (effet externe irréversible).
2. Chercher le débit `ticket_filed` du ticket pour ce client. Absent → fin (pas de remboursement).
3. Déjà remboursé (ligne refund existante pour ce ticket) → skip.
4. Pool désormais illimité → skip. Sinon créditer `min(debited, monthly_credits - remaining)` via `AdjustCredits(+amount)` ; écrire l'audit `TICKET_REFUNDED` `reason="ticket:<id>"` pool. Plafonner à l'allocation.
5. Best-effort : si le remboursement échoue après la suppression (issue déjà supprimée), rollback de la portion billing + log (ne pas 500 la suppression). Ordonner delete → refund(committé) comme pour le dépôt (#1153).
6. Pas de `LowCredits` sur un remboursement (le solde monte).
## Portée frontend (src/)
- Après une suppression réussie (le hook/mutation de `Supprimer le ticket`), **invalider la query des crédits** (`GET /billing/clients/{id}/credits`) + le profil, pour que la jauge « Crédits restants » se mette à jour immédiatement.
- (Optionnel) afficher un retour « X crédits remboursés » si simple ; sinon le rafraîchissement de la jauge suffit. i18n en/fr si texte ajouté.
## Acceptance criteria
- [ ] Supprimer un ticket Soumis/Spec rembourse au pool d'origine le montant exact débité au dépôt (audit `ticket_filed` / `reason="ticket:<id>"`).
- [ ] Ticket sans débit (avant #1153 / pool illimité au dépôt) → suppression sans remboursement, sans erreur.
- [ ] Pas de double remboursement (idempotent / garde sur audit refund existant).
- [ ] Le remboursement ne dépasse jamais l'allocation mensuelle du pool ; pool désormais illimité → pas de remboursement.
- [ ] Le remboursement écrit une entrée d'historique reliée au ticket ; pas d'alerte crédits bas sur un crédit.
- [ ] La jauge profil se met à jour après suppression.
- [ ] Backend : pytest, ruff, mypy verts. Frontend : vitest, tsc, eslint, build verts.