Endpoint sécurisé, création de produit, price dynamique, session checkout, webhooks.
Dans ce chapitre, on va voir comment exposer un endpoint REST dans NestJS pour créer une session de paiement Stripe Checkout. L’objectif : permettre à un utilisateur de faire un don à un autre utilisateur via Stripe, tout en stockant la donation dans Prisma et en transférant automatiquement les fonds sur le compte connecté du bénéficiaire.
Avant de démarrer, assure-toi d’avoir :
STRIPE_SECRET_KEY
configurée en variable d’environnement.StripeService
et un controller StripeController
.User
et Donation
.Pense à valider côté frontend que l’utilisateur courant a un stripeAccountId
avant de proposer le bouton “Faire un don”.
On commence par exposer un POST /stripe/donations
dans StripeController. Ce controller va recevoir les deux IDs (receivingUserId
et givingUserId
) et renvoyer l’URL de la session Stripe.
1import { Controller, Post, Body, UseGuards, Req } from '@nestjs/common';2import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';3import { StripeService } from './stripe.service';45@Controller('stripe')6export class StripeController {7constructor(private readonly stripeService: StripeService) {}89@UseGuards(JwtAuthGuard) // ^? Sécurisation via JWT10@Post('donations')11async createDonation(12@Body()13payload: {14receivingUserId: string;15},16@Req() req: any,17) {18const givingUserId = req.user.userId;19return this.stripeService.createDonation({20receivingUserId: payload.receivingUserId,21givingUserId,22});23}24}
// @callout: On récupère givingUserId
depuis le token JWT fourni par le guard.
Le cœur de la logique se trouve dans StripeService.createDonation()
. Voici l’extrait clé :
1async createDonation({2receivingUserId,3givingUserId,4}: {5receivingUserId: string;6givingUserId: string;7}): Promise<{8error: boolean;9message: string;10sessionUrl: string | null;11}> {12try {13if (receivingUserId === givingUserId) {14throw new Error('Vous ne pouvez pas vous faire de dons à vous-même');15}1617// Récupérer les infos Stripe des deux utilisateurs18const [receivingUser, givingUser] = await Promise.all([19this.prisma.user.findUniqueOrThrow({20where: { id: receivingUserId },21select: { id: true, firstName: true, stripeProductId: true, stripeAccountId: true },22}),23this.prisma.user.findUniqueOrThrow({24where: { id: givingUserId },25select: { id: true, stripeAccountId: true },26}),27]);2829if (!receivingUser.stripeAccountId || !givingUser.stripeAccountId) {30throw new Error("Les comptes Stripe doivent être configurés pour les deux utilisateurs");31}
1// Créer le product Stripe si pas encore présent2let { stripeProductId } = receivingUser;3if (!stripeProductId) {4const product = await this.stripe.products.create({5name: `Soutenez ${receivingUser.firstName}`,6});7await this.prisma.user.update({8where: { id: receivingUser.id },9data: { stripeProductId: product.id },10});11stripeProductId = product.id;12}1314// Créer un prix personnalisable15const price = await this.stripe.prices.create({16currency: 'EUR',17custom_unit_amount: { enabled: true },18product: stripeProductId,19});2021▶ // Créer une donation en base pour tracker la session22const createdDonation = await this.prisma.donation.create({23data: {24stripePriceId: price.id,25stripeProductId,26receivingUser: { connect: { id: givingUser.id } },27givingUser: { connect: { id: receivingUser.id } },28},29});3031▶ // Générer la session Checkout32const session = await this.stripe.checkout.sessions.create({33mode: 'payment',34line_items: [{ price: price.id, quantity: 1 }],35payment_intent_data: {36application_fee_amount: 0,37metadata: { donationId: createdDonation.id },38transfer_data: { destination: receivingUser.stripeAccountId },39},40success_url: 'http://localhost:3000',41cancel_url: 'http://localhost:3000',42});4344return {45sessionUrl: session.url,46error: false,47message: 'Session Stripe créée avec succès',48};49} catch (error) {50return { error: true, message: error.message, sessionUrl: null };51}52}
// @callout: custom_unit_amount
permet à l’utilisateur de saisir le montant.
Validation
Vérifier que l’utilisateur ne se fait pas de don à lui-même.
Chargement des comptes Stripe
On récupère stripeAccountId
et stripeProductId
via Prisma.
Création du product
Si stripeProductId
est vide, on crée un nouveau produit dans Stripe et on l’enregistre en base.
Création d’un prix
Chaque don est lié à un Price
Stripe avec custom_unit_amount.enabled
.
Enregistrement de la donation
On crée un enregistrement Donation
dans la base pour pouvoir suivre l’avancée.
Génération de la session Checkout
payment_intent_data.transfer_data.destination
pour router les fonds.metadata.donationId
pour retrouver la donation après webhook.Utilise curl
ou Postman pour déclencher la création de session :
1curl -X POST http://localhost:3000/stripe/donations \2-H "Authorization: Bearer $JWT_TOKEN" \3-H "Content-Type: application/json" \4-d '{"receivingUserId":"usr_123abc"}'
Réponse attendue :
1{2"error": false,3"message": "Session Stripe créée avec succès",4"sessionUrl": "https://checkout.stripe.com/pay/cs_test_…"5}
Ensuite, tu rediriges l’utilisateur vers sessionUrl
dans ton UI Remix.
custom_unit_amount
offre la flexibilité du montant saisi par l’utilisateur.transfer_data.destination
permet de verser les dons directement sur le compte Stripe connecté du bénéficiaire.Ne renvoie jamais ta clé secrète Stripe au frontend. Utilise toujours un endpoint backend.
/donate/$receivingUserId
, consomme l’API et crée un bouton “Payer” qui redirige vers sessionUrl
.400
si error: true
, plutôt que 200
.