Pratique

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.uuidorders.user_uuid | credits.user_uuid | tasks.user_uuid | apikeys.user_uuid | affiliates.user_uuid
  • orders.order_nocredits.order_no
  • credits.trans_notasks.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)

  1. orders : order_no passe à paid avec les détails du paiement.
  2. credits : insertion d’une ligne positive { trans_type: "order_pay", credits: <order.credits>, order_no }.
  3. affiliates : récompense optionnelle liée à cette commande.

Consommation par une tâche (Texte→Vidéo)

  1. credits : ligne négative { trans_type: "task_text_to_video", credits: -N } (on récupère trans_no).
  2. tasks : ligne { credits_used: N, credits_trans_no: <ce trans_no> } pour tracer qui a payé quoi.

Mini ping (1 crédit)

  1. 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 — retourne balance, 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 ligne tasks 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

  1. Lancer l'app : pnpm dev (Postgres requis via DATABASE_URL).

  2. Créer un compte : /fr/signup, puis connectez-vous.

  3. 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é.

  4. 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>"
  5. Simuler une dépense : appelez /api/ping avec un message pour débiter CreditsAmount.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 liez order_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 par expired_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.