Connecter une application Rails avec Stripe
Intéressé pour créer un API avec Ruby on Rails?
Jette un coup d'œil à mon livre: API on Rails 6. Tu peux télécharger une version gratuite au format PDF sur Github. Si tu aimes mon travail tu peux acheter un version payante sur Leanpub.
🇫🇷 Bonjour
Pour mon projet iSignif j’ai voulu implémenter la fonctionnalité d’ accès restreint au site uniquement si l’utilisateur bénéficie d’un compte premium. Le but final est que l’utilisateur doive souscrire un compte premium afin d’accéder à certaines pages.
Afin d’implémenter cette fonctionnalité, nous avons commencé par analyser le besoin. Le comportement attendu est le suivant:
- le client bénéficie d’un mois de découverte de notre outil dès son inscription
- une fois son solde de jours premium épuisé, il reçoit un mail lui indiquant qu’il va falloir racheter des jours
- l’utilisateur met à jour son solde en effectuant un paiement ponctuel qui lui rajoute un mois à son solde premium où il souscrit un abonnement qui effectuera un paiement automatique au début de chaque mois
Afin d’implémenter cela, j’ai rapidement fait le tour des solutions de paiement existantes (PayPal, BNP, etc..). Il s’est avéré que Stripe était le meilleur compromis.
Stripe est une société américaine qui a pour but de simplifier les paiements en ligne. Créée en 2010, Stripe pèse maintenant plus de 10 milliards!
J’ai choisi Stripe car ses avantages sont:
- le client peut payer sans avoir un compte ouvert chez Stripe
- les tarifs sont assez “raisonnables” (1,4% + 0,25€ par transaction pour les cartes européennes)
- la facilité de la mise en place car, en plus de proposer une belle API, Stripe propose des librairies pour les langages les plus utilisés (PHP, Pyhton, Ruby, Java et même Go)
- une excellente documentation
De plus, Stripe va bien plus loin qu’une simple solution de paiement puisqu’il propose tout un écosystème pour gérer des clients, des factures, des produits, etc…
Dans cet article je vais donc retracer le développement de la fonctionnalité en essayant d’être le plus générique possible. Je précise aussi avant de commencer que ce n’est pas un article sponsorisé et je n’ai pas reçu d’argent de la part de Stripe (j’aurais bien voulu…).
TLDR: Stripe est très simple à mettre en place et nous permet vraiment de complètement déléguer la gestion des paiement. Cela permet de se focaliser sur son business et c’est quelque chose d’inestimable pour un projet qui débute.
Table of contents
Implémentation du mode premium
Dans cette première partie, je vais vous parler de l’implémentation de la fonctionnalité premium. Ici on va juste coder le comportement attendu sans toucher à Stripe pour le moment (patience…).
Modification du modèle User
Nous allons donc commencer mettre en place un système de restriction de certaines pages aux utilisateurs premium.
L’idée est d’ajouter un attribut premium_until
de type DateTime
qui contiendra la date de validité du compte premium. On ajoute donc cette colonne pour la table des users
.
rails g migration add_premium_until_to_users premium_until:date
Cette commande va générer la migration suivante:
# db/migrate/20190116132207_addpremiumuntil_to_users.rb
class AddPremiumUntilToUsers < ActiveRecord::Migration[5.2]
def change
add_column :users, :premium_until, :date
end
end
Vu que nous sommes généreux, nous allons aussi créer une migration supplémentaire afin d’ offrir un mois à tous les utilisateurs existants:
rails g migration offer_one_monthspremiumto_users
On boucle juste sur tous les utilisateurs existants
# db/migrate/20190116132207_add_premium_until_to_users.rb
# ...
def up
premium_until_offer = DateTime.now + 1.month
User.all.each { |user| user.update! premium_until: premium_until_offer }
end
Et voilĂ .
Création de la logique premium
Nous avons maintenant une belle colonne premium_until
qui contient la date de validité du compte premium. Nous voulons créer une méthode User#increment_premium
qui sera appelée à chaque fois qu’un paiement est réceptionné. Celle-ci va simplement ajouter un mois à l’attribut premium_until
.
Créons les tests unitaires qui définissent le comportement attendu de cette fonction. Cette méthode est très importante donc nous allons couvrir tous les cas possibles:
- lorsque l’utilisateur possède déjà un solde de jours
# test/models/user_test.rb
test 'should offer one month premium to user' do
user = User.create!(
premium_until: (Date.today + 5.days)
# ...
)
assert_equal (Date.today + 1.month + 5.days), user.premium_until
end
- lorsque l’utilisateur ne possède pas encore de solde de jours
# test/models/user_test.rb
test 'should set correct premium_until for never premium user' do
user = User.new
user.increment_premium
assert_equal (Date.today + 1.month), user.premium_until
end
- vérifie que l’on ajoute un mois à partir d’aujourd’hui pour un utilisateur qui vient de réactiver son compte après une inactivité
# test/models/user_test.rb
test 'should set correct premium_until for past-premium user' do
user = User.new(premium_until: (Date.today - 1.year))
user.increment_premium
assert_equal (Date.today + 1.month), user.premium_until
end
Je pense que ces tests suffisent à couvrir tous les cas possibles. Nous avons écrit beaucoup de tests mais l’implémentation est très rapide:
# app/models/user.rb
class User < ApplicationRecord
before_create :increment_premium
def increment_premium
if premium_until.nil? || (premium_until < Date.today)
self.premium_until = Date.today
end
self.premium_until += 1.month
end
# ...
end
Et voilà . les tests passent désormais:
bin/rails test test/models/user_test.rb
# Running:
....
Restrictions actions
L’implémentation de la restriction est vraiment facile mais commençons par écrire les tests unitaires. On va donc créer deux fixtures (Les fixtures sont des données insérées dans la base de données afin de tester l’application) une représentant un utilisateur premium et une autre un utilisateur expiré.
# test/fixtures/users.yml
premium_advocate:
premium_until: <%= DateTime.now + 1.month %>
# ...
expired_advocate:
premium_until: <%= DateTime.now - 1.month %>
# ...
Maintenant, imaginons une page autorisée seulement aux utilisateurs premium. Le test est assez facile:
- On connecte un utilisateur
- On accède à la page
- on vérifie que la réponse est 200 (= success) si l’utilisateur est premium et sinon on vérifie que l’utilisateur est redirigé vers la page de paiement Stripe
Voici donc l’implémentation des tests:
# test/controllers/acts_controller_test.rb
class ActsControllerTest < ActionDispatch::IntegrationTest
test 'should forbid get index for non-premium user' do
login users(:expired_advocate)
get acts_url
assert_response root_path
end
test 'should get index for premium user' do
login users(:premium_advocate)
get acts_url
assert_response :success
end
# ...
end
A ce moment, si vous lancez les test, vous obtiendrez une belle erreur de ce genre:
ActsControllerTest#test_should_forbid_get_index_for_non-premium_user
Expected response to be a <3XX: redirect>, but was a <200: OK>
L’implémentation pour le faire passer est assez facile. Il suffit de créer une méthode qui va vérifier que l’attribut premium_until
de l’utilisateur connecté est supérieur à DateTime.now
. Ensuite il suffit d’appeler cette méthode en utilisant un hook dans le contrôleur. Voici le code
# app/controllers/acts_controller.rb
class ActsController < ApplicationController
before_action :redirect_if_not_premium, only: %i[index]
# ...
private
def redirect_if_not_premium
redirect_to root_path if current_user.nil? or current_user.premium_until < DateTime.now
end
end
La méthode
current_user
me permet de récupérer l’utilisateur connecté à l’application. Pour l’implémenter, je vous recommande d’utiliser Authlogic
Et voilà . Le test devrait désormais passer! La création de la logique pour le mode utilisateur est maintenant terminée. Passons (enfin) à Stripe!
Paiement ponctuel
Nous avons donc mis en place la logique pour restreindre certaines pages aux utilisateurs premium. Nous avons aussi créé la méthode qui ajoutera un mois de compte premium à un utilisateur. Il ne reste plus qu’à appeler cette méthode lorsqu’un paiement est effectué.
Tout d’abord, pour utiliser Stripe, il faut se créer un compte qui vous permettra d’obtenir une clé d’API. Une fois ceci fait, l’intégration à votre application Rails est très facile car Stripe propose une gemme! Bien sûr, nous allons l’utiliser ici.
Commençons donc par ajouter cette gemme à notre projet:
bundle add stripe
Dans cette première version nous allons simplement mettre en place un paiement ponctuel Stripe et appeler User#increment_premium
si tout se passe bien. Dans le jargon de Stripe, un simple paiement est une charge.
On va donc créer un contrôleur charges
qui va contenir deux actions:
new
qui va simplement proposer un formulaire pour payercreate
qui recevra la réponse de Stripe
Générons donc tout ça avec la commande rails generate
:
rails generate controller charges new create
Il ne nous reste qu’à modifier un peu le code généré par Rails. Tout d’abord on modifie les routes:
# config/routes.rb
Rails.application.routes.draw do
# ...
resources :charges, only: %i[new create]
# ...
end
On implémente ensuite les actions dans le contrôleur:
# app/controllers/charges_controller.rb
class ChargesController < ApplicationController
# display Stripe form to make a new payment
def new; end
# & check all data from Sripe
def create
# Amount in cents
@amount = 500
# get customer from POST params
customer = Stripe::Customer.create(
email: params[:stripeEmail],
source: params[:stripeToken]
)
begin
charge = Stripe::Charge.create(
customer: customer.id,
amount: @amount,
description: 'Rails Stripe customer',
currency: 'eur'
)
current_user.increment_premium!
rescue Stripe::CardError => e
flash[:error] = e.message
redirect_to new_charge_path
end
end
end
Ca fait beacoup de code . Découpons un peu la méthode create
:
Stripe::Customer.create
va enregistrer l’utilisateur chez Stripe. Elle va s’occuper de faire toutes les vérifications pour nous (validité de carte, informations transmises, etc…)Stripe::Charge.create
va créer la charge en la liant au customer que nous venons de créer- Nous appelons la méthode
increment_premium
pour ajouter du crédit à l’utilisateur
Rien de très compliqué. Maintenant, on modifie un peu les vues et on génère un formulaire:
<!-- app/views/charges/new.html.erb -->
<h1>Souscrire Ă un abonnement mensuel</h1>
<p>Actuellement, votre compte premium est disponible jusqu'au <%= current_user.premium_until.strftime('%d/%m/%Y') %></p>
<%= form_tag charges_path do %>
<script src="https://checkout.stripe.com/checkout.js" class="stripe-button"
data-key="<%= Rails.application.secrets.stripe[:publishable_key] %>"
data-description="A month's subscription"
data-amount="500"
data-locale="auto"></script>
<% end %>
<!-- app/views/charges/create.html.erb -->
<h1>Votre paiement a été accepté</h1>
<p>Le paiement a été effectué. Votre compte premium à été étendu au <%= current_user.premium_until.strftime('%d/%m/%Y') %></p>
On termine par la configuration. Il suffit de récupérer les clés API via l’interface Stripe et des les ajouter dans le fichier secrets.yml
# config/secrets.yml
development:
stripe:
publishable_key: pk_test_azerty
secret_key: sk_test_clef_a_ne_pas_commiter
test: &development
production:
stripe:
publishable_key: pk_live_azerty
secret_key: sk_live_clef_a_ne_pas_commiter
Évidemment, il faut renseigner votre propre clé ici
Et maintenant de créer la configuration nécessaire dans un initializer spécifique à Stripe:
# config/initializers/stripe.rb
Rails.configuration.stripe = {
publishable_key: Rails.application.secrets.stripe[:publishable_key],
secret_key: Rails.application.secrets.stripe[:secret_key]
}
Stripe.api_key = Rails.application.secrets.stripe[:secret_key]
Une fois la première version mise en place, il suffit de tester que tout se passe bien.
Au risque de vous décevoir, je n’ai rien inventé et j’ai quasiment tout pompé sur le guide de Stripe.
Pour tester, on lance le serveur Rails et on se connecte sur http://localhost:3000/charges/new. Un bouton vous emmènera sur le formulaire de Stripe:
J’ai volontairement utilisé le numéro de carte 4242 4242 4242 4242
qui est une carte de test. Certaines cartes vous permettent de simuler des erreurs. La liste complète des cartes de test est disponible ici
Une fois le formulaire envoyé, vous êtes redirigé vers la page charges#create
qui vous confirme votre achat. Vous pouvez retrouver le paiement sur Stripe dans la section payments:
Sauvegarde du cutomer token
Nous allons effectuer une petite modification à l’implémentation proposée par Stripe. Nous voulons sauvegarder le customer créé par Stripe afin de le réutiliser s’il paie une nouvelle fois. On va donc ajouter une colonne users.stripe_token
.
rails g migration add_stripe_token_to_users stripe_token:string
Nous allons créer un concern qui va s’occuper de récupérer ou créer un customer Stripe:
Si vous n’êtes pas à l’aise avec les concerns, j’en parle dans un précédent article.
# app/controllers/concerns/stripe_concern.rb
module StripeConcern
extend ActiveSupport::Concern
protected
# Try to retreive Stripe customer and create if not already registered
# @params [User]
# @return [Stripe::Customer]
def create_or_retrieve_customer(user)
customer = retrieve_stripe_customer(user)
if customer.nil?
customer = Stripe::Customer.create email: params[:stripeEmail], source: params[:stripeToken]
user.update! stripe_token: customer.id
end
customer
end
private
# @params [User]
# @return [Stripe::Customer|Nil]
def retrieve_stripe_customer(user)
return nil if user.stripe_token.nil?
begin
customer = Stripe::Customer.retrieve user.stripe_token
return customer
rescue Stripe::InvalidRequestError
# if stripe token is invalid, remove it!
user.update! stripe_token: nil
return nil
end
end
end
Il suffit maintenant de modifier un peu notre contrôleur pour l’utiliser.
# app/controllers/charges_controller.rb
class ChargesController < ApplicationController
include StripeConcern
# ...
def create
# Amount in cents
@amount = 500
customer = create_or_retrieve_customer(current_user)
begin
charge = Stripe::Charge.create(
customer: customer.id,
amount: @amount,
description: 'Rails Stripe customer',
currency: 'eur'
)
current_user.increment_premium!
rescue Stripe::CardError => e
flash[:error] = e.message
redirect_to new_charge_path
end
end
end
Et voilà ! Le fonctionnement est identique mais désormais nous récupérons le client s’il existe déjà . Nous pouvons aussi nous passer du bouton Stripe si l’utilisateur a déjà un token:
<!-- app/views/charges/new.html.erb -->
<h1>Souscrire Ă un abonnement mensuel</h1>
<p>Actuellement, votre compte premium est disponible jusqu'au <%= current_user.premium_until.strftime('%d/%m/%Y') %></p>
<%= form_tag charges_path do %>
<% if current_user.stripe_token %>
<%= submit_tag 'Payer avec votre compte Stripe' %>
<% else %>
<script src="https://checkout.stripe.com/checkout.js" class="stripe-button"
data-key="<%= Rails.application.secrets.stripe[:publishable_key] %>"
data-description="A month's subscription"
data-amount="500"
data-locale="auto"></script>
<% end %>
<% end %>
Abonnement
Ne lâchez pas, nous avons presque terminé. Une des dernière fonctionnalité à créer est de proposer un abonnement. L’utilisateur pourra ainsi souscrire un abonnement qui enclenchera un paiement automatique au début du mois. Dans le jargon de Stripe, cela s’appelle une subscriptions.
Chaque plan est joint à un produit qui représente (…) le service offert aux clients. Les produits peuvent avoir plus d’un plan, reflétant les variations de prix et de durée - comme les prix mensuels et annuels à des taux différents. Il existe deux types de produits: les biens et les services. (…) qui sont destinés aux abonnements.
Création du plan
Créons donc notre premier produit la en utilisant la gemme Stripe. Voici un exemple avec la console Rails (Vous pouvez faire la même chose en utilisant l’interface d’administration).
2.6.0 :001 > product= Stripe::Product.create name: 'Abonnement compte premium', type: 'service'
=> #<Stripe::Product:0x3fe4f20a1420 id=prod_EMb13PJreiAcF2> JSON: {
2.6.0 :002 > plan = Stripe::Plan.create amount: 5000, interval: 'month', product: product.id, currency: 'eur', id: 'premium-monthly'
)
=> #<Stripe::Plan:0x2ab3e0b46d24 id=premium-monthly> JSON: {
Là encore je n’ai rien inventé, tout est Dans la documentation de Stripe
Nous obtenons donc un belle instance Ruby correspondant à un Plan. Nous allons juste noter l’ id
et le noter dans le fichier secret.yml
:
# config/secrets.yml
development:
stripe:
premium_plan_id: premium-monthly
# ...
Création de la logique
Comme nous avons créé un contrôleur charges
, nous allons en créer un nouveau nommé subscriptions
avec deux méthodes:
new
qui va simplement proposer un formulaire pour payercreate
qui recevra la réponse de Stripe
Utilisons une fois de plus la commande rails generate
rails generate controller subscriptions new create
Cette méthode n’est pas parfaite, il faut modifier un peu les routes
# config/routes.rb
Rails.application.routes.draw do
resources :subscriptions, only: %w[new create]
# ...
end
L’implémentation du SubscriptionsController
est quasiment identique au ChargesController
(c’est pour cela que nous avons utilisé un concern un peu plus haut afin d’éviter de dupliquer le code). Nous devons juste appeler la méthode Stripe::Charge.create
class SubscriptionsController < ApplicationController
include StripeConcern
before_action :check_login
before_action :only_advocates
def new; end
def create
customer = create_or_retrieve_customer(current_user)
begin
Stripe::Subscription.create(customer: customer.id, items: [{ plan: Rails.application.secrets.stripe[:premium_plan_id] }])
rescue Stripe::CardError => e
flash[:error] = e.message
redirect_to new_subscription_path
end
end
end
Et les vues:
<!-- app/views/subscriptions/new.html.erb -->
<h1>Souscrire Ă un abonnement mensuel</h1>
<p>Abonner vous afin de recharger votre compte automatiquement tous les mois</p>
<%= form_tag subscriptions_path do %>
<% if current_user.stripe_token %>
<p class="text-center">
<%= submit_tag t('premium.pay'), class: 'btn btn-primary btn-lg' %>
</p>
<% else %>
<script src="https://checkout.stripe.com/checkout.js" class="stripe-button"
data-key="<%= Rails.application.secrets.stripe[:publishable_key] %>"
data-image="<%= image_url 'favicon.png' %>"
data-name="iSignif SAS"
data-description="Compte premium pendant un mois"
data-email="<%= current_user.email %>"
data-locale="auto"></script>
<% end %>
<% end %>
<!-- app/views/subscriptions/create.html.erb -->
<h1><%= @title %></h1>
<p>Le prélèvement automatique est maitenant configuré</p>
Et voilà . Nous pouvons désormais souscrire un abonnement.
Mise en place du Webhook
Nous avons mis en place un paiement mensuel mais nous voulons être notifié des paiements effectués au début du mois. Dans notre cas, le workflow type est le suivant:
- l’utilisateur effectue une demande d’abonnement
- Stripe crée un abonnement pour cette utilisateur
- lorsque l’abonnement est renouvelé (c-à -d. lorsque Stripe facture le client et qu’il est facturé de nouveau).
Stripe envoie une requête pour signaler que le paiement a été effectué par le biais du hook. Les Webhook sont simplement des routes que nous mettons à disposition pour recevoir les requêtes de la part de Stripe. Une fois la route créée, nous devons communiquer l’URL à Stripe via l’interface d’administration de Stripe (cela se fait très facilement).
Notez que j’ai choisi de ne recevoir que le signal
invoice.payment_succeeded
qui est envoyé lorsqu’une facture est payée. Encore une fois je n’invente rien, tout est dans la documentation de Stripe
Générons une route avec Rails.
rails g controller hooks stripe --no-assets --no-helper
Nous allons juste supprimer la vue que Rails vient de nous créer et passer la route accessible avec le verbe POST
:
rm -r app/views/hooks
# config/routes.rb
Rails.application.routes.draw do
post 'hooks/stripe'
# ...
end
Il suffit maintenant d’ajouter une méthode dans le contrôleur qui recevra la requête de Stripe.Comme d’habitude, commençons par les tests.
Test fonctionnels
C’est toujours compliqué de tester l’intégration d’un API donc j’ai simplement choisi de simuler une requête de la part de Stripe et de vérifier si notre contrôleur ajoute du crédit à l’utilisateur.
pour cela, j’ai simplement copié/collé les paramètres envoyés par Stripe via leur interface de test des webhooks.
Une fois la requête copiée, je l’ai transformée en Hash
Ruby en ne gardant que les paramètres qui m’intéressent.
# test/controllers/hooks_controller_test.rb
# ...
class HooksControllerTest < ActionDispatch::IntegrationTest
# Stripe webook params copied from <https://dashboard.stripe.com/test/webhooks>
STRIPE_INVOICE_SUCCEEDED_PARAMS = {
id: 'invoice.payment_00000000000000',
type: 'invoice.payment_succeeded',
# ...
data: {
object: {
customer: 'cus_00000000000000',
# ...
}
},
# ...
}.freeze
# ...
end
Maintenant il suffit d’envoyer une requête POST
et de vérifier que notre utilisateur est incrémenté.
# test/controllers/hooks_controller_test.rb
# ...
class HooksControllerTest < ActionDispatch::IntegrationTest
# ...
setup do
@user = users(:one_advocate)
end
test 'Stripe hook should add premium days to the given user' do
old = user.premium_until
post hooks_stripe_url, params: STRIPE_INVOICE_SUCCEEDED_PARAMS
assert_response :success
user.reload
assert_operator old, :<=, user.premium_until
end
end
Et voilà . L’implémentation est très simple:
# app/controllers/hooks_controller.rb
class HooksController < ApplicationController
protect_from_forgery except: [:stripe]
def stripe
if is_payment_succeeded?
advocate = retrieve_user
advocate.increment_premium! unless advocate.nil?
end
head :ok, content_type: 'text/html'
end
private
def retrieve_user
customer_token = params.dig(:data, :object, :customer)
return nil if customer_token.nil?
User.where(stripe_token: customer_token).first
end
def is_payment_succeeded?
params[:type] == 'invoice.payment_succeeded'
end
end
Attention à bien désactiver le
protect_from_forgery
qui va bloquer les requêtes provenant de l’extérieur.
Et voilà , notre paiement récurrent est désormais en place!
Test en développement
Il suffit d’utiliser un service comme Ngrok ou Serveo (je vous recommande ce dernier qui est plus facile à utiliser) afin d’exposer votre application à l’extérieur et ensuite de tester votre webhook via l’interface de test des webhooks de Stripe. Je ne vais pas vous le montrer ici car nous avons déjà mis en place un test qui le simule pour nous.
Conclusion
Je vous ai donc démontré via cet article qu’il était très facile de mettre en place un système de paiement récurrent avec Stripe. La documentation quasi parfaite et leur gemme nous simplifient vraiment la tâche.
Mais les fonctionnalités de Stripe ne s’arrêtent pas la. Stripe nous permet aussi de mettre en place un système de facturation (avec la génération de belles factures PDF), de remboursement ou encore de gestion de litiges.
Je pense que pour la création de votre application il est beaucoup plus intelligent de déléguer toutes les tâches de paiement à Stripe et de se concentrer sur son business.
Voir les autres articles liés
Participez au développement de votre navigateur preéferé
Participez au développement de votre navigateur preéferé