Facturation (admin + user)
Pages billing user et admin — refunds, comp days, vue MRR/ARR/churn, factures Stripe.
Deux pages distinctes :
- User —
/account/settings → Facturation: son plan, ses factures, son code coupon actif - Admin —
/admin/billing: KPI revenu + liste des subs avec actions par row
User-side
API GET /api/billing/me agrège en un payload :
subscription(status / period end / canceledAt) depuis la DBplan(name / prix / yearly?) en matchantstripePriceIdcouponactif (code / kind / appliedUntil)invoices(6 derniers via Stripe API : number, amount, status, pdfUrl)
UI affiche tout. Boutons :
- Stripe Portal (
POST /api/billing/portal) — modifier carte - Changer de plan →
/account/upgrade
Si pas de sub : "Plan gratuit" + CTA upgrade.
Résiliation
Quand l'utilisateur clique sur le plan gratuit depuis sa page billing, un
dialog de confirmation s'affiche (pas de redirection Stripe). Il conserve
son accès jusqu'à currentPeriodEnd.
POST /api/billing/cancel- Appelle
stripe.subscriptions.update({ cancel_at_period_end: true }) - Met à jour
canceledAten DB immédiatement (sans attendre le webhook) - Audit
subscription.canceled
L'UI bascule le bouton sur "Résiliation en cours…" (désactivé) dès
que subscription.canceledAt est renseigné.
Réactivation
Si l'abonnement est en cours de résiliation (canceledAt renseigné mais
subscription encore active), le bouton du plan actuel devient
"Réactiver l'abonnement" (vert).
POST /api/billing/reactivate- Appelle
stripe.subscriptions.update({ cancel_at_period_end: false }) - Vide
canceledAten DB immédiatement - Audit
subscription.reactivated
Le webhook customer.subscription.updated confirme les deux opérations
(idempotent).
Admin-side
/admin/billing :
- KPI cards : MRR, ARR, abonnés payants, churn 30j
- Liste paginée des
Subscriptionavec user inclus - Filtres : recherche email + status
- Actions par row (menu) :
| Action | Effet |
|---|---|
| Ouvrir dans Stripe | lien direct vers le customer Stripe |
| Rembourser le dernier paiement | refund Stripe API + audit |
| Offrir N jours d'accès | étend stripeCurrentPeriodEnd (DB only) |
| Annuler à la fin de période | cancel_at_period_end Stripe |
API : POST /api/admin/billing/[subscriptionId] avec
{ action: "refund_last" | "comp_days" | "cancel_at_period_end" }.
Comp days vs Stripe
L'action comp_days est volontairement DB only — on étend la date de fin de période sans toucher à Stripe. Si l'user a un abonnement actif, Stripe re-charge à la prochaine date Stripe (qui peut être avant). Pour un comp "propre", utilise plutôt :
- Un coupon
FREE_MONTHSStripe (auto-créé via coupons) - Ou
LIFETIMEsi c'est un partenariat permanent
Refunds
refund_last cherche la dernière invoice en status paid du customer
et call stripe.refunds.create({ payment_intent, reason }). Reasons
acceptés : duplicate, fraudulent, requested_by_customer.
Tracé dans AuditLog (billing.refund avec metadata.refundId +
amount).
Packs de crédits
Les credit packs sont des achats one-shot (hors abonnement). Chaque pack est lié à un Stripe Product + Price créé automatiquement.
Sync Stripe automatique
| Opération admin | Effet Stripe |
|---|---|
| Créer un pack | Crée un Product + Price → stocke stripePriceId |
| Modifier prix ou devise | Archive l'ancien Price, crée un nouveau |
| Modifier le nom | Met à jour le Product.name |
Désactiver (active: false) | Archive le Price Stripe |
| Réactiver | Crée un nouveau Price actif |
| Supprimer | Archive Price + Product, puis supprime en DB |
La sync est best-effort : si Stripe est indisponible lors de la
création, le pack est créé en DB sans stripePriceId. Au checkout,
si stripePriceId est absent, un price_data inline est utilisé comme
fallback.
POST /api/admin/credit-packs → crée pack + Stripe Product/Price
PATCH /api/admin/credit-packs/[id] → sync Stripe sur changements
DELETE /api/admin/credit-packs/[id] → archive Stripe + supprime DBCheckout
POST /api/billing/credit-pack-checkout { packId }Génère une session Stripe Checkout one-time. Le webhook
checkout.session.completed (metadata type: "credit_pack") crédite
totalCredits = credits + bonusCredits sur le compte user et log un
audit credits.purchased.
Allez plus loin
- Setup Stripe — créer ses clés API, restricted key, Customer Portal
- Plans — config des plans Stripe
- Coupons — codes promo (PERCENT / FREE_MONTHS / LIFETIME)
- Webhooks Stripe — sync DB sur events Stripe