🎯 Intention


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

✅ Points clés


<aside> ⌛ Temps indicatif : 3h

</aside>

Etape

Visuel / Exemple


✍️ Faire le point d’api (Webhook)

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

💼 Faire le job webhook

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

🟢 Écrire la classe responsable de la logique du webhook

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

🔒 Sécuriser le webhook

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