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. Elle n'a aucune dépendance à une autre gem et utilise seulement net/http de la bibliothèque standard.

Version
1.1.1
Runtime
Ruby 3.1+, bibliothèque standard uniquement

Couvre

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

Installation

Gemfile
gem 'wajub', '~> 1.1'

Vous pouvez aussi l'installer sans Gemfile, car aucune dépendance transitive n'est à résoudre.

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
idempotency_key_prefixPréfixe de la clé générée. Valeur par défaut : 'wajub'
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

Le client conserve une seule connexion protégée par un mutex. Vous pouvez donc le partager entre les threads Puma. Construisez-le une fois dans un initializer et référencez la constante. Créer un client par requête ouvre une nouvelle connexion TLS à chaque fois.

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. Les clés symboles fonctionnent aussi, car elles sont normalisées avant l'envoi. 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/tok_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

PropertyMéthodes
client.globalping, channels, countries, currencies
client.paymentscreate, initialize_payment, 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.listenconfig, auth
client.webhooksconstruct_event
  • initialize is the Ruby constructor, so the alias of create is named initialize_payment.
  • webhooks signature verification runs locally, no HTTP call. All other resources call the merchant REST API.
  • links, invoices, tax and shield are live only. A sandbox key gets 403 This feature is only available in live mode. on every one of their methods.
  • refunds and transfers are create, retrieve and list only. The shared CRUD base also exposes update and delete on them, but the API serves no such route.

Parcourir une liste par pages

Une page ou toutes les pages
page = WAJUB.payments.list('status' => 'success', '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

Chaque requête POST et PUT contient une Idempotency-Key générée. Cette clé sécurise les nouvelles tentatives automatiques.

Un numéro de commande constitue la meilleure clé
WAJUB.payments.create(
  params,
  Wajub::RequestOptions.new(idempotency_key: "order-#{order.id}")
)
ÉlémentValeur
Statuts concernés429, 500, 502, 503, 504 et toute erreur réseau
Tentatives3 au total, l'appel initial et deux nouvelles tentatives
Attente progressiveExponentielle avec une part d'aléa (jitter), en respectant Retry-After pour une erreur 429
Délai d'expiration30 secondes pour l'ouverture et 30 secondes pour la lecture

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 devient l'en-tête X-Sync. 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 is {"amount" => ["The amount must be at least 25."]}
  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.

Deux noms propres à Ruby

Le constructeur a pris le bon nom
# Everywhere else this alias of create is named initialize.
# In Ruby that name belongs to the constructor, so it is:
WAJUB.payments.initialize_payment(params)

L'autre est l'accesseur des endpoints de webhook. Le client expose WAJUB.webhook_endpoints, en snake_case comme le reste de la gem, tandis que le chemin d'API reste /webhook-endpoints.

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/tok_test',
      'authorization_token' => 'tok_test',
      'transaction' => { 'id' => 'trx_test', 'status' => 'pending' }
    }
  )
)

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

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 Moyens de paiement et canaux. 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 ?