Crédits & facturation
Modèle hybride : on déduit toujours, on alerte sur seuil, on bloque seulement si tu actives le soft-limit. Pas de hard cut-off par défaut — un client compromis ne tombe jamais brutalement sans alerte préalable.
Coût par opération
| Opération | Coût (crédits) | Endpoint |
|---|---|---|
| Ouverture de session | 1 | POST /v1/sessions |
| Attachement d'artifact | 1 | POST /v1/sessions/{id}/artifacts |
| Génération bundle ASiC-E | 5 | POST /v1/proofs |
Les actions internes (validate OTP, record action, close) sont gratuites — elles font partie d'une session déjà payée. Idem refus (POST /refuse).
Champs organisation
| Champ | Type | Default | Description |
|---|---|---|---|
| credit_balance | integer | 0 | Solde courant. Peut devenir négatif si soft_limit_enforced=false. |
| credit_alert_threshold | integer 0-100 | 80 | Pourcentage de conso à partir duquel une alerte est levée (V1 = observation manuelle dans usage_event, V2 = webhook). |
| soft_limit_enforced | boolean | false | Si true, toute opération qui ferait passer le solde < 0 est refusée avec HTTP 402. |
Cycle de vie d'une déduction
POST /v1/sessions (organisation.credit_balance = 100)
└─ OpenSessionUseCase
└─ DeductCreditsUseCase.execute(organisation, 'session_open', 1)
├─ if (softLimit && balance - 1 < 0) → throw InsufficientCreditsException
├─ organisation.deductCredits(1)
├─ organisationRepo.save(organisation) → balance = 99
└─ usageRepo.recordEvent( credit_deduction, qty=1,
{ reason: 'session_open', balance_after: 99, alert_threshold: 80 })
Réponse HTTP :
201 Created (succès)
402 Payment Required (si soft-limit ON et insuffisant) :
{ "error": "INSUFFICIENT_CREDITS", "message": "Organisation X has insufficient credits...",
"balance": 0, "required": 1 }
Recharger une organisation
Via l'admin UI (V1) :
http://proof.test/admin/organisations/{id} → bloc "Crédits"
→ champ amount + bouton "Topup"
→ POST /admin/organisations/{id}/credits/topup (CSRF)
Via console (script ops) :
php bin/console doctrine:dbal:sql \ "UPDATE organisation SET credit_balance = credit_balance + 1000 WHERE id = '01900000-...'"
V2 (en backlog) : webhook Stripe → topup automatique après paiement réussi. Pour V1, c'est saisie manuelle (modèle compatible avec une facturation post-payée si tu préfères ne jamais activer le soft-limit).
Observer la consommation
Tous les événements de déduction sont enregistrés dans la table usage_event avec leur metadata complète. Pour le reporting :
-- Conso du mois courant par type d'opération
SELECT
metadata->>'reason' AS operation,
COUNT(*) AS occurrences,
SUM(quantity) AS total_credits
FROM usage_event
WHERE organisation_id = '01900000-...'
AND event_type = 'credit_deduction'
AND occurred_at >= date_trunc('month', now())
GROUP BY 1
ORDER BY 3 DESC;
Le dashboard admin /admin/usage consolide ces données par organisation + période.
soft_limit_enforced à false sur tes organisations payants (postpayé), et active-le uniquement sur les comptes d'essai / freemium pour cap au solde initial.