Dans le cas de paiement, il est utile de mettre en place un webhook sur nos services afin d’être tenu au courant quand un paiement réussi, échoué, etc …
Le webhook doit enregistrer les paramètres reçu pour être traités dans un job en asynchrone et retourner obligatoirement une 200 (pas besoin d’attendre que le job soit traité)
<aside> ✅ Pré-requis
<aside> ⌛ Temps indicatif : 3h
</aside>
POST /webhooks/{nom_du_service}
/webhooks/{nom_du_service}
Exemple : webhooks/revenuecat
source : ‣
Raison :
Le controller doit toujours retourner une 200 afin d’éviter le moindre timeout et expliquer au service qui utilise notre webhook que nous avons bien reçu la demande
Le test s’assure que le job est bien lancé afin de traiter la demande en asynchrone (toujours dans cette optique de répondre le plus rapidement possible)
# config/routes.rb
namespace :webhooks do
resource :revenuecat, only: :create
end
# app/controllers/webhooks/revenuecat_controller.rb
module Webhooks
class RevenuecatController < ActionController::API
def create
Webhooks::RevenuecatJob.perform_later(
revenuecat_params[:api_version],
revenuecat_params[:event]
)
head 200
end
def revenuecat_params
params.permit(:api_version, event: {})
end
end
end
webhooks
source : ‣
# app/jobs/webhooks/revenuecat_job.rb
class Webhooks::RevenuecatJob < ApplicationJob
queue_as :webhooks
def perform(api_version, event)
Webhook::Revenuecat.call(
api_version: api_version,
event: event
)
end
end
source : https://github.com/Captive-Studio/vera-serveur/blob/main/app/models/webhook.rb
# app/models/webhooks/revenuecat.rb
# Liste des types de RevenueCat :
# <https://www.revenuecat.com/docs/event-types-and-fields>
module Webhooks
class Revenuecat < ApplicationRecord
self.table_name = "webhooks_revenuecat"
validates :event_id, uniqueness: true
PREMIUM_VALIDE = /INITIAL_PURCHASE|RENEWAL|UNCANCELLATION|NON_RENEWING_PURCHASE/
PREMIUM_INVALIDE = /EXPIRATION/
PREMIUM_TRANSFER = /TRANSFER/
def self.call(api_version:, event:)
event_id = event[:id]
event_type = event[:type]
ActiveRecord::Base.transaction do
create(event_id: event_id, api_version: api_version, event: event)
case event_type
when PREMIUM_VALIDE
recupere_utilisateur(event)&.update(premium: true)
when PREMIUM_INVALIDE
recupere_utilisateur(event)&.update(premium: false)
when PREMIUM_TRANSFER
Utilisateur.where(id: event[:transferred_from]).update_all(premium: false)
Utilisateur.where(id: event[:transferred_to]).update_all(premium: true)
end
end
end
def self.recupere_utilisateur(event)
utilisateur_id = event[:app_user_id]
Utilisateur.find_by id: utilisateur_id
end
end
end
Pour une question de sécurité. On ne souhaite pas que quelqu’un nous dise que le paiement a été effectué alors que c’est faux.
# spec/requests/webhooks/revenuecat_spec.rb
describe "Webhook RevenueCat", swagger_doc: "webhooks/swagger.yaml" do
path "/webhooks/revenuecat" do
post "soumet un évènement lié aux paiements (<https://www.revenuecat.com/docs/event-types-and-fields>)" do
security [bearer: []]
...
response "401", "Le token fourni est invalide" do
let(:Authorization) { "" } # on utilise celui défini dans le Rails credentials
before(:each) do |example|
submit_request(example.metadata)
end
it do
expect(response).to have_http_status(:unauthorized)
end
end
end
end
end
# config/initializers/rswag_ui.rb
Rswag::Ui.configure do |c|
...
c.openapi_endpoint "/api-docs/webhooks/swagger.yaml", "Webhooks Docs"
end
# spec/swagger_helper.rb
# frozen_string_literal: true
require "rails_helper"
RSpec.configure do |config|
...
config.openapi_specs = {
...
"webhooks/swagger.yaml" => {
openapi: "3.0.1",
info: {
title: "Webhooks",
version: "v1",
},
paths: {},
servers: [
{
url: "<http://127.0.0.1:3000>",
description: "Local avec rails s",
},
{
url: "<http://127.0.0.1:5000>",
description: "Local avec foreman start",
},
{
url: "<https://staging.vera.captive.dev>",
description: "Staging",
},
{
url: "<https://app.vera-app.fr>",
description: "Production",
},
],
components: {
securitySchemes: {
bearer: {
description: "Bearer token définis par la variable d'environnement 'REVENUECAT_AUTHORIZATION_HEADER''",
type: :apiKey,
name: "Authorization",
in: :header,
},
},
},
},
}
end
# app/controllers/webhooks/revenuecat_controller.rb
module Webhooks
class RevenuecatController < ActionController::API
before_action :authenticate_api_token!
...
private
REVENUECAT_AUTHORIZATION_HEADER = Rails.application.credentials.revenuecat&.authorization_header
def authenticate_api_token!
return if token_from_header.present? &&
token_from_header == REVENUECAT_AUTHORIZATION_HEADER
head :unauthorized
end
def token_from_header
request.headers.fetch("Authorization", "").split(" ").last
end
end
end