Comptes, commandes et crédits
Comprendre comment les utilisateurs, les commandes et le grand livre des crédits fonctionnent ensemble dans le template Sushi SaaS. Découvrez la formule du solde, la gestion de l’expiration et les APIs/services pour accorder, consommer et consulter des crédits.
Pourquoi c'est important
Ce template fournit l'authentification, le suivi des commandes compatible Stripe et un système de crédits. Comprendre la relation entre ces briques vous permet d'ajouter vos propres offres ou d'échanger le fournisseur de paiement.
Les acteurs (tables clés)
users
— Source de vérité du profil.uuid
(clé partagée), email, locale, rôle, etc. Renseigné par Better Auth.accounts
&sessions
— Internes Better Auth pour l’authentification. Utile pour l’audit; hors calcul de solde.orders
— Chaque tentative/résultat de paiement.order_no
,status
,amount
, colonnes d’abonnement (sub_*
),credits
,expired_at
.credits
— Grand livre append‑only. Positif = ajout; négatif = consommation. Colonnes:trans_no
,user_uuid
,trans_type
,credits
,order_no
,expired_at
.tasks
— Tâches d’usage (ex.: texte→vidéo).credits_used
,credits_trans_no
relient la tâche à la ligne de consommation.- Caméos:
apikeys
,affiliates
sont aussi rattachés àuser_uuid
.
Liens (sans clés étrangères, mais stables):
users.uuid
→orders.user_uuid
|credits.user_uuid
|tasks.user_uuid
|apikeys.user_uuid
|affiliates.user_uuid
orders.order_no
→credits.order_no
credits.trans_no
→tasks.credits_trans_no
Chaque table est horodatée pour reconstituer l’historique et alimenter l’analytics.
L’analogie : Portefeuille + Reçus
- Imaginez
credits
comme un portefeuille transparent. Chaque dépôt ou dépense est un reçu glissé dedans. - Dépôts (lignes positives) ajoutent du cash. Dépenses (négatives) en retirent. Les cartes‑cadeaux expirées ? Les reçus restent, mais ne comptent plus pour payer.
- Le solde = somme des positifs non expirés − somme des négatifs.
C’est pour cela que le grand livre est append‑only: on n’édite pas l’ancien, on ajoute de nouveaux reçus pour garder une histoire explicable.
Une action → plusieurs lignes (exemples)
Paiement validé (Stripe)
orders
:order_no
passe àpaid
avec les détails du paiement.credits
: insertion d’une ligne positive{ trans_type: "order_pay", credits: <order.credits>, order_no }
.affiliates
: récompense optionnelle liée à cette commande.
Consommation par une tâche (Texte→Vidéo)
credits
: ligne négative{ trans_type: "task_text_to_video", credits: -N }
(on récupèretrans_no
).tasks
: ligne{ credits_used: N, credits_trans_no: <ce trans_no> }
pour tracer qui a payé quoi.
Mini ping (1 crédit)
credits
: ligne négative{ trans_type: "ping", credits: -1 }
.
Expiration
- Toute ligne positive dont
expired_at
est passé n’entre plus dans le solde, mais reste au journal pour l’audit.
Services & API utiles
src/services/credit.ts#getUserCreditSummary
— retournebalance
,granted
,consumed
,expired
,expiringSoon[]
,ledger[]
(tronqué).src/services/credit.ts#getUserCredits
— état léger pour le gating :{ left_credits, is_pro, is_recharged }
.src/services/credit.ts#decreaseCredits
— ajoute une ligne négative; lève une erreur si solde insuffisant.src/services/credit.ts#increaseCredits
— ajoute une ligne positive; utilisé par Stripe et les attributions.src/services/stripe.ts#handleCheckoutSession
— transforme une session payée en mise à jour d’orders
+ ligne de crédit.src/services/tasks.ts#createTextToVideoTask
— décrémente les crédits, puis insère une lignetasks
qui référence cette dépense.
Endpoints HTTP
POST /api/account/credits
— résumé; accepte{ includeLedger, ledgerLimit, includeExpiring }
.POST /api/account/profile
— profil + résumé;{ includeCreditLedger: false }
pour alléger.POST /api/ping
— démo de consommation (1 crédit par défaut).
Toutes répondent via respJson
: { code, message, data }
.
Tester en local
-
Lancer l'app :
pnpm dev
(Postgres requis viaDATABASE_URL
). -
Créer un compte :
/fr/signup
, puis connectez-vous. -
Ajouter des crédits manuellement (dev uniquement) :
insert into credits (trans_no, user_uuid, trans_type, credits, created_at) values ('dev-grant-1', '<uuid-utilisateur>', 'manual_adjustment', 100, now());
Exécutez la requête avec
pnpm drizzle-kit studio
ou votre client SQL préféré. -
Consulter le solde :
curl -X POST http://localhost:3000/api/account/credits \ -H "Content-Type: application/json" \ -H "Cookie: <copiez les cookies d'authentification de votre navigateur>"
-
Simuler une dépense : appelez
/api/ping
avec un message pour débiterCreditsAmount.PingCost
(1 crédit par défaut), puis relancez la requête de solde.curl -X POST http://localhost:3000/api/ping \ -H "Content-Type: application/json" \ -H "Cookie: <copiez les cookies d'authentification de votre navigateur>" \ -d '{\"message\":\"bonjour\"}'
Personnaliser
Ajuster la fenêtre d'expiration
Modifiez EXPIRING_WINDOW_DAYS
dans src/services/credit.ts
pour changer le seuil "expire bientôt". Pour décrémenter automatiquement, planifiez un job qui ajoute une ligne négative quand expired_at
est dépassé.
Bonus d'accueil
Appelez insertCredit
(src/models/credit.ts
) après insertUser
pour créditer les nouveaux comptes. Gardez trans_no
unique afin d'éviter les doublons.
Autre processeur de paiement
- Utilisez
insertOrder
avec la payload de votre fournisseur. - Ajoutez la ligne correspondante dans
credits
et liezorder_no
pour tracer les remboursements. - Exposez les métadonnées de subscription (
sub_*
) dans votre UI si vous vendez des plans récurrents.
Plus de contexte
Ajoutez des colonnes (ex. meta
JSON) dans orders
/credits
pour stocker des flags ou des ID d'expériences. Les migrations Drizzle actualiseront le snapshot.
Pièges & Conseils
- Le solde se calcule par sommes; on ne “consomme” pas une ligne positive précise. Les entrées négatives référencent une source probable via
order_no
pour la traçabilité, sans muter l’historique. getUserValidCredits
ordonne parexpired_at
pour étiqueter une source proche lors de la consommation.- Besoin d’un strict FIFO ou d’une consommation par lot ? Étendez le modèle, mais conservez le journal append‑only pour l’audit.
- Gardez
users.uuid
stable : c’est la colle entre orders, credits, tasks, keys, affiliates.
Prochaines étapes
- Construire un tableau de bord en réutilisant les totaux fournis par
getUserCreditSummary
. - Utiliser
/api/ping
comme point de départ pour d'autres fonctionnalités qui consomment des crédits. - Écrire des tests unitaires pour vérifier le calcul du solde, l'expiration et les alertes.
- Besoin d’autres langues ? Je peux synchroniser cette page.
Mise en place de la base de données
Préparer Postgres et Drizzle ORM — l’épine dorsale pour l’auth, la facturation, le stockage, les tâches et les tables métier.
Tâches à Crédits — Texte en Vidéo
Ajoutez une monétisation à l’usage avec une table générique de tâches, un registre de crédits et un générateur texte‑vers‑vidéo modulable. Découvrez le schéma, les APIs, les constantes et une UI minimale pour expédier des fonctions IA.