Aller au contenu

Ruby

La gem wajub, ses ressources et son intégration dans une application Rails.

La gem wajub constitue la partie serveur d'une intégration Wajub. Elle conserve votre clé secrète, crée des paiements, lit leur statut réel et vérifie les signatures des webhooks.

Version
1.3.0
Runtime
Ruby 3.1+

Couvre

  • Paiements
  • Facturation
  • Transferts
  • Sync
  • Shield
  • Taxes

Installation

Gemfile
gem 'wajub', '~> 1.3'

Vous pouvez aussi l'installer sans Gemfile.

Directement depuis RubyGems
gem install wajub

Créer le client

Le constructeur accepte des arguments nommés et utilise les variables d'environnement en repli. Dans le cas courant, aucun argument n'est donc nécessaire.

config/initializers/wajub.rb
require 'wajub'

# Reads WAJUB_API_KEY and WAJUB_WEBHOOK_SECRET when you pass nothing.
WAJUB = Wajub::Client.new

# Or pass them yourself.
WAJUB = Wajub::Client.new(
  api_key: ENV.fetch('WAJUB_API_KEY'),
  webhook_secret: ENV.fetch('WAJUB_WEBHOOK_SECRET')
)
Argument nomméRôle
api_keyVotre clé sk. ou sk_test.. Utilise WAJUB_API_KEY en repli
webhook_secretLe secret whsec_. Utilise WAJUB_WEBHOOK_SECRET en repli
transportVotre propre objet qui répond à call, pour les tests ou une stack personnalisée

Une clé vide déclenche ArgumentError: Wajub: api_key is required lors de la construction. Une mauvaise configuration échoue donc au démarrage.

Un seul client partagé constitue le bon choix

Construisez le client une fois dans un initializer et référencez la constante. Vous pouvez le partager entre les threads Puma.

Le premier appel

Créer un paiement
payment = WAJUB.payments.create(
  'amount' => 25_000,
  'currency' => 'XAF',
  'email' => 'amina@example.com',
  'description' => 'Order 4172',
  'reference' => "order-#{order.id}",
  'callback' => checkout_complete_url
)

redirect_to payment.authorization_url, allow_other_host: true

Les paramètres sont transmis dans un hash avec les noms de champs exacts de l'API, et les clés symboles fonctionnent aussi. Le résultat est un ApiObject. Chaque champ se lit donc comme une méthode.

Lire le résultat
payment.id                   # trx_test_8kQ2mW9vB4nL6hR1cY3d
payment.status               # pending
payment.authorization_url    # https://pay.wajub.com/test_xxxxx
payment['authorization_url'] # the same thing
payment.to_h                 # the whole hash, for logging

Les montants utilisent l'unité principale

25_000 avec XAF représente vingt-cinq mille francs. Une devise décimale accepte un nombre décimal : 'amount' => 12.50 avec GHS.

Toutes les ressources du client

PropriétéMéthodes
client.globalping, channels, countries, currencies
client.paymentscreate, retrieve, list, cancel, process, process_split, list_refunds
client.customerscreate, retrieve, update, delete, list, block, unblock, activate, deactivate, list_tax_ids, create_tax_id, delete_tax_id
client.refundscreate, retrieve, list
client.transferscreate, retrieve, list
client.beneficiariescreate, retrieve, update, delete, list
client.linkscreate, retrieve, update, delete, list
client.invoicescreate, retrieve, update, delete, list, send, mark_paid, cancel
client.accountscreate, retrieve, update, delete, list, regenerate_token
client.webhook_endpointscreate, retrieve, update, delete, list, rotate_secret
client.balanceretrieve
client.eventslist, retrieve, resend
client.disputeslist, retrieve, submit_evidence, accept, close, send_message
client.identityresolve, validate
client.taxget_settings, update_settings, rates, calculate, reports, list_codes, retrieve_code, list_registrations, create_registration, retrieve_registration, update_registration, delete_registration, jurisdictions, thresholds, threshold_alerts
client.shieldget_settings, update_settings, stats, list_blocklist, add_to_blocklist, remove_from_blocklist
client.webhooksconstruct_event
  • La vérification de signature de webhooks s'exécute en local, sans appel HTTP. Toutes les autres ressources appellent l'API REST.
  • links, invoices, tax et shield ne fonctionnent qu'en live. Une clé sandbox reçoit 403 This feature is only available in live mode. sur chacune de leurs méthodes.
  • refunds et transfers ne proposent que la création, la lecture et la liste.

Parcourir une liste par pages

Une page ou toutes les pages
page = WAJUB.payments.list('status' => 'succeeded', 'per_page' => 50)

page.data.each do |payment|
  puts "#{payment['id']} #{payment['amount']}"
end

page = page.next_page if page.has_more

# Or let it walk every page for you.
WAJUB.payments.list.auto_paging_each do |payment|
  Reconcile.call(payment)
end

page.data contient des hash simples, pas des ApiObject. Les lignes utilisent donc l'accès par crochets. La méthode est next_page, pas get_next_page, et auto_paging_each accepte le bloc.

Idempotence et nouvelles tentatives

Les requêtes en échec sont retentées automatiquement, et une nouvelle tentative ne crée jamais rien en double. Si la même commande peut être soumise à nouveau, par la nouvelle tentative d'un job par exemple, passez votre propre clé.

Un numéro de commande constitue la meilleure clé
WAJUB.payments.create(
  params,
  Wajub::RequestOptions.new(idempotency_key: "order-#{order.id}")
)

Agir pour un compte connecté

Un appel pour un vendeur
WAJUB.payments.create(
  { 'amount' => 25_000, 'currency' => 'XAF', 'email' => buyer.email },
  Wajub::RequestOptions.new(sync: seller.wajub_account_id)
)

sync reçoit l'identifiant du compte connecté. La configuration est présentée dans Sync.

Webhooks

construct_event vérifie la signature et renvoie l'événement analysé. Il a besoin du corps exactement tel qu'il est arrivé.

Un contrôleur Rails
class WajubWebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token

  def create
    event = WAJUB.webhooks.construct_event(
      request.raw_post,
      request.headers['X-Wajub-Signature'],
      request.headers['X-Wajub-Timestamp']
    )

    HandleWajubEventJob.perform_later(event) if event['event'] == 'payment.succeeded'

    head :ok
  rescue Wajub::WebhookSignatureVerificationError
    head :bad_request
  end
end

Une route de webhook Rails demande deux précautions supplémentaires. Ignorez la vérification CSRF, car Wajub ne transmet aucun jeton de session, et répondez rapidement. Accusez réception avec head :ok, puis laissez un job effectuer le travail. La livraison est retentée si votre endpoint met trop de temps à répondre.

Le nom de l'événement se trouve dans event, pas dans type, et le résultat est un hash simple. La tolérance est de 300 secondes par défaut, et construct_event accepte un quatrième argument positionnel. Consultez Vérification de signature.

Erreurs

Intercepter d'abord l'erreur précise
begin
  WAJUB.payments.create(params)
rescue Wajub::InvalidRequestError => e
  # e.errors vaut {"amount" => "The amount must be at least 25."}, une chaîne par champ
  render json: { fields: e.errors }, status: :unprocessable_entity
rescue Wajub::RateLimitError => e
  response.set_header('Retry-After', (e.retry_after || 5).to_s)
  head :service_unavailable
rescue Wajub::WajubError => e
  Rails.logger.error("wajub failed code=#{e.code} status=#{e.http_status}")
  raise
end
ClasseCas de déclenchement
Wajub::AuthenticationError401
Wajub::PermissionError403
Wajub::NotFoundError404
Wajub::InvalidRequestError400 et 422
Wajub::RateLimitError429, avec retry_after en secondes
Wajub::WajubErrorTout autre statut. C'est aussi la classe parente de toutes les erreurs ci-dessus
Wajub::ApiConnectionErrorAucune réponse, à cause du réseau ou d'un délai d'expiration
Wajub::WebhookSignatureVerificationErrorUn webhook dont la signature n'a pas été vérifiée

Toutes exposent code, http_status, errors et raw. Elles descendent toutes de StandardError. Un simple rescue les intercepte donc.

Un nom propre à Ruby

L'accesseur des endpoints de webhook est WAJUB.webhook_endpoints, en snake_case comme le reste de la gem, tandis que le chemin d'API est /webhooks.

Tester sans appeler l'API

L'argument nommé transport accepte tout objet qui répond à call. Vous pouvez ainsi utiliser toute la gem hors ligne dans un test.

Un transport simulé
class FakeTransport
  def initialize(status:, body:)
    @status = status
    @body = body
  end

  def call(method:, path:, headers:, body:, query:)
    { status: @status, body: @body, headers: {}, request: { method: method, path: path } }
  end
end

client = Wajub::Client.new(
  api_key: 'sk_test.fake',
  transport: FakeTransport.new(
    status: 201,
    body: {
      'authorization_url' => 'https://pay.wajub.com/test_xxxxx',
      'authorization_token' => 'test_xxxxx',
      'transaction' => { 'id' => 'trx_test', 'status' => 'pending' }
    }
  )
)

expect(client.payments.create('amount' => 25_000).authorization_token).to eq('test_xxxxx')

Débiter sans page hébergée

Requête push Mobile Money
payment = WAJUB.payments.create(
  'amount' => 25_000, 'currency' => 'XAF', 'phone' => '+237670000000'
)

WAJUB.payments.process(
  payment.id,
  'channel' => 'cm.mtn', 'phone' => '+237670000000'
)

Un canal suit le format country.operator. Ainsi, cm.mtn désigne MTN Mobile Money au Cameroun. La liste complète se trouve dans Mobile Money. Le payeur doit toujours approuver l'opération sur son téléphone. Le résultat arrive donc par webhook.

Effectuer un payout

Un transfert vers un numéro de téléphone
transfer = WAJUB.transfers.create(
  {
    'amount' => 100_000,
    'currency' => 'XAF',
    'beneficiary' => {
      'name' => 'Amina Diallo',
      'channel' => 'cm.mtn',
      'phone' => '+237670000000'
    },
    'reference' => 'payout-892'
  },
  Wajub::RequestOptions.new(idempotency_key: 'payout-892')
)

beneficiary accepte aussi l'identifiant ben_… d'un bénéficiaire enregistré. Cette forme est préférable dès que vous payez plusieurs fois la même personne.

Développement local

Deux terminaux
wajub listen --forward-to localhost:3000/wajub_webhooks
wajub trigger payment.succeeded

Pour en savoir plus, consultez la CLI.

Que pensez-vous de ce contenu ?