Project

General

Profile

Actions

Feature #1153

open
RA

Consommation de crédits au dépôt de ticket (coût = score de complexité LLM, par pool)

Feature #1153: Consommation de crédits au dépôt de ticket (coût = score de complexité LLM, par pool)

Added by Redmine Admin about 1 month ago. Updated about 1 month ago.

Status:
QA
Priority:
Normal
Assignee:
-
Start date:
06/23/2026
Due date:
% Done:

0%

Estimated time:
spec_ref:

issue:self — spec dans la description (décisions verrouillées + seams)

build_status:
build_number:
ci_run_url:
scan_status:
scan_report_url:
deploy_status:
preprod_url:
deployed_at:
branch:
feat/1153-credit-consumption
pr_url:
https://github.com/omdev-tech/PipeLiner-Client/pull/83
security_key:
severity:
paused:

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.

RA Updated by Redmine Admin about 1 month ago Actions #1

  • Status changed from Submitted to Spec

RA Updated by Redmine Admin about 1 month ago Actions #2

  • Status changed from Spec to In development

RA Updated by Redmine Admin about 1 month ago Actions #3

  • Status changed from In development to QA
  • branch set to feat/1153-credit-consumption
  • pr_url set to https://github.com/omdev-tech/PipeLiner-Client/pull/83

Implémenté et poussé en PR vers `dev` : https://github.com/omdev-tech/PipeLiner-Client/pull/83

Branche : `feat/1153-credit-consumption` (basée sur `origin/dev`, dépend du modèle 2 pools #1147).

**Backend (api/, hexagonal, TDD)**
- Coût = `clamp(draft.complexity.score, 0, 10)`, tel quel. Mapping pool : `feature` → évolution, `bug` → maintenance.
- Nouveau use-case `ChargeTicketCredits` qui compose le modèle billing #1147 : pré-vérifie le solde, puis réutilise `AdjustCredits` (plancher 0, audit, events). Aucune réimplémentation.
- `InsufficientCreditsError` levée **avant** la création de l'issue Redmine (aucun ticket créé) → HTTP **409** avec detail structuré `{message, pool, cost, remaining}`.
- Ordre : pré-check → création issue → débit (committé). Toute défaillance résiduelle du débit après dépôt est **logguée**, jamais 500/rollback.
- Traçabilité au ticket : nouvelle action d'audit `ticket_filed` + colonne nullable `reason` (`ticket:<id>`), exposée dans l'historique. Migration **0015** ajoute `billing_audit.reason`.
- Pool illimité → dépôt sans débit ; coût 0 → dépôt sans débit ; jamais négatif.

**Frontend (src/, TDD, next-intl)**
- `TicketDraftPanel` affiche le coût + le pool ciblé + le solde restant, mis à jour quand on change bug/feature ou le score. Pool fini insuffisant → bouton de dépôt **désactivé** + bandeau « Crédits insuffisants » (coût vs solde) ; pool illimité → « illimité », jamais bloqué. La réponse 409 backend est aussi gérée.
- Nouveau hook `useCredits` ; `useCreateTicket` invalide les queries crédits + profil au succès (rafraîchit la jauge profil). `ApiError.detailData` porte désormais les details structurés.
- Clés i18n ajoutées dans `en.ts` ET `fr.ts`, placeholders ICU identiques.

**Gates verts** — API : pytest (2× sur une même base, 664 ok) / ruff / mypy. FE : vitest (677 ok) / tsc / eslint / build.

Passage en QA.

Actions

Also available in: PDF Atom