Paiements

Wrapper Stripe, sessions Checkout, PaymentIntents, clients, remboursements et endpoint de webhook.

core/payments enveloppe Stripe : sessions de checkout, intents, clients, remboursements, et l'endpoint de webhook servi par le core.

Import

import {
  isEnabled, getPublishableKey, getStripe,
  createCheckoutSession, createPaymentIntent,
  getCheckoutSession, getPaymentIntent,
  registerStripeEventHandler, getSubscribedStripeEvents,
  verifyWebhookSignature,
  createCustomer, getOrCreateCustomer, listPaymentMethods,
  createRefund,
} from '@lrvo/core/payments';

@lrvo/core/stripe expose le wrapper Stripe seul, sans le registre d'événements : registerStripeEventHandler ne sort que par @lrvo/core/payments. Module serveur : la clé secrète ne doit jamais atteindre le bundle client.

Variables d'environnement

VariableObligatoireRôle
STRIPE_SECRET_KEYouiClé secrète, côté serveur.
STRIPE_PUBLISHABLE_KEYouiClé publique, transmise au client.
STRIPE_WEBHOOK_SECRETpour les webhooksSecret de signature.
LRVO_CURRENCYnonDevise par défaut des PaymentIntents. Défaut : cad.

API

isEnabled()

Retourne true si STRIPE_SECRET_KEY et STRIPE_PUBLISHABLE_KEY sont définies. Sert à conditionner l'affichage des fonctionnalités de paiement plutôt qu'à laisser un appel échouer.

getPublishableKey()

Retourne la clé publique, ou null. À passer au client pour initialiser Stripe.js.

getStripe()

Retourne le client Stripe sous-jacent, instancié une seule fois sur l'API 2023-10-16. Lève si STRIPE_SECRET_KEY est absente.

Sert aux appels que ce module n'enveloppe pas : abonnements, factures, transferts. Passer par lui plutôt que d'instancier un second client, qui vivrait sur sa propre version d'API.

createCheckoutSession(options)

Crée une session Stripe Checkout et retourne la session.

const session = await createCheckoutSession({
  lineItems: [{ price: 'price_xxx', quantity: 1 }],
  successUrl: 'https://example.com/success',
  cancelUrl: 'https://example.com/cancel',
  customerEmail: '[email protected]',
  mode: 'payment',
});
// rediriger vers session.url
ParamètreTypeRôle
lineItemsobject[]Lignes de commande Stripe.
successUrlstringRetour après paiement réussi.
cancelUrlstringRetour après annulation.
customerEmailstringPré-remplit le formulaire. Optionnel.
clientReferenceIdstringIdentifiant interne pour rapprochement. Optionnel.
metadataobjectMétadonnées Stripe. Optionnel.
modestring'payment', 'subscription' ou 'setup'. Défaut : 'payment'.

createPaymentIntent(options)

const intent = await createPaymentIntent({
  amount: 4999,          // en centimes
  currency: 'cad',
  metadata: { orderId: '123' },
});
ParamètreTypeRôle
amountnumberMontant en centimes.
currencystringCode ISO. Défaut : LRVO_CURRENCY, sinon cad.
metadataobjectOptionnel.
automaticPaymentMethodsobjectDéfaut : { enabled: true }.

getCheckoutSession(sessionId) et getPaymentIntent(paymentIntentId)

Relecture par identifiant. getCheckoutSession s'utilise dans la route successUrl pour confirmer le paiement côté serveur : ne jamais considérer un retour navigateur comme preuve de paiement.

registerStripeEventHandler(eventType, handler)

Abonne une fonction à un type d'événement Stripe. C'est la seule chose à faire pour traiter un webhook : l'endpoint est déjà servi par le core.

import { registerStripeEventHandler } from '@lrvo/core/payments';

registerStripeEventHandler('checkout.session.completed', async (event) => {
  const session = event.data.object;
  await fulfillOrder({
    userId:    session.client_reference_id,
    sessionId: session.id,
    amount:    session.amount_total,
  });
});

Le handler reçoit l'événement Stripe déjà vérifié. Plusieurs abonnés peuvent partager un même type : ils sont appelés dans l'ordre d'enregistrement.

Retour du handlerRéponse à StripeConséquence
Retour normal200L'événement est acquitté, Stripe passe au suivant.
Exception500Stripe réessaie, avec des délais croissants, pendant trois jours.

Un échec définitif se termine donc par un retour normal, jamais par une exception : journaliser et rendre la main, sinon Stripe rejoue l'événement pendant des jours pour rien.

L'idempotence est à la charge de l'abonné. Stripe livre au moins une fois, et le même événement peut arriver deux fois. Enregistrer event.id dans une table avec une contrainte d'unicité, ou poser une contrainte sur la commande elle-même.

getSubscribedStripeEvents()

Retourne les types d'événements ayant au moins un abonné. Sert à savoir quoi cocher dans le tableau de bord Stripe.

verifyWebhookSignature(payload, signature)

Vérifie la signature d'un webhook et retourne l'événement. Lève si la signature est invalide ou si STRIPE_WEBHOOK_SECRET est absent.

Le core l'appelle déjà pour sa route de webhook, donc un abonné n'en a pas besoin. Elle reste exportée pour un endpoint Stripe monté à part, hors de LRVO.

Le payload doit être le corps brut de la requête. Un corps déjà parsé en JSON ne produit pas la même signature.

createCustomer(options) et getOrCreateCustomer(email, defaultData)

const customer = await getOrCreateCustomer('[email protected]', {
  name: 'Jean Dupont',
  metadata: { userId: '42' },
});

getOrCreateCustomer utilise une clé d'idempotence dérivée de l'adresse, ce qui limite les doublons en cas d'appels concurrents.

listPaymentMethods(customerId, type?)

Retourne les méthodes de paiement du client. type vaut 'card' par défaut.

createRefund(options)

const refund = await createRefund({
  paymentIntentId: 'pi_xxx',
  amount: 1000,                      // partiel ; total si omis
  reason: 'requested_by_customer',
});
ParamètreTypeRôle
paymentIntentIdstringPaymentIntent à rembourser.
amountnumberMontant en centimes. Remboursement total si omis.
reasonstringduplicate, fraudulent ou requested_by_customer. Optionnel.

Route servie

RouteAuthRôle
POST /payments/stripe/webhookpublicReçoit les webhooks Stripe, vérifie la signature, distribue aux abonnés.

L'URL complète est donc https://exemple.ca/lrvo/api/payments/stripe/webhook. C'est elle qu'on déclare dans le tableau de bord Stripe. Le projet hôte n'a aucun fichier à créer : la route fait partie du core, comme /health.

La route porte skipCsrf: true et skipRateLimit: true (voir core/api) : Stripe n'envoie ni Origin ni Referer, et une rafale d'événements ne doit pas déclencher la limitation par IP. C'est la vérification de signature qui authentifie l'appel, et rien n'est distribué avant qu'elle passe.

SituationRéponse
STRIPE_SECRET_KEY ou STRIPE_WEBHOOK_SECRET absente404
En-tête stripe-signature absent400
Signature invalide400
Aucun abonné pour ce type d'événement200
Un abonné lève500, donc retry Stripe

Notes

Un endpoint Stripe par mode. Le mode test et le mode production tiennent chacun leur liste de webhooks, avec leur propre secret de signature. Un secret de test posé en production fait échouer toutes les vérifications, donc tout arrive en 400.

stripe listen génère encore un autre secret, le sien, à utiliser en développement. Voir Configuration.

Les abonnés s'enregistrent au boot. Un module le fait dans son register(), une application dans son instrumentation.js. Un abonnement posé à l'intérieur d'un handler de requête n'existerait que dans le processus qui a servi cette requête.

Sur cette page