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.
wajub
Stable · GARubyGems
- Version
- 1.1.1
- Runtime
- Ruby 3.1+, bibliothèque standard uniquement
Couvre
- Paiements
- Facturation
- Transferts
- Sync
- Shield
- Taxes
Installation
gem 'wajub', '~> 1.1'Vous pouvez aussi l'installer sans Gemfile, car aucune dépendance transitive n'est à résoudre.
gem install wajubCré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.
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_key | Votre clé sk. ou sk_test.. Utilise WAJUB_API_KEY en repli |
webhook_secret | Le secret whsec_. Utilise WAJUB_WEBHOOK_SECRET en repli |
idempotency_key_prefix | Préfixe de la clé générée. Valeur par défaut : 'wajub' |
transport | Votre 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
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: trueLes 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.
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 loggingUn champ inconnu déclenche une erreur au lieu de renvoyer nil
ApiObject répond uniquement aux clés réellement envoyées par l'API. payment.settlement_batch_id
déclenche NoMethodError lorsque ce champ est absent. Ce comportement Ruby est utile dans un test,
mais surprenant en production. Utilisez payment['settlement_batch_id'] pour tout champ facultatif,
car la notation avec crochets renvoie nil.
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
| Property | Méthodes |
|---|---|
| client.global | ping, channels, countries, currencies |
| client.payments | create, initialize_payment, retrieve, list, cancel, process, process_split, list_refunds |
| client.customers | create, retrieve, update, delete, list, block, unblock, activate, deactivate, list_tax_ids, create_tax_id, delete_tax_id |
| client.refunds | create, retrieve, list |
| client.transfers | create, retrieve, list |
| client.beneficiaries | create, retrieve, update, delete, list |
| client.links | create, retrieve, update, delete, list |
| client.invoices | create, retrieve, update, delete, list, send, mark_paid, cancel |
| client.accounts | create, retrieve, update, delete, list, regenerate_token |
| client.webhook_endpoints | create, retrieve, update, delete, list, rotate_secret |
| client.balance | retrieve |
| client.events | list, retrieve, resend |
| client.disputes | list, retrieve, submit_evidence, accept, close, send_message |
| client.identity | resolve, validate |
| client.tax | get_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.shield | get_settings, update_settings, stats, list_blocklist, add_to_blocklist, remove_from_blocklist |
| client.listen | config, auth |
| client.webhooks | construct_event |
initializeis the Ruby constructor, so the alias ofcreateis namedinitialize_payment.webhookssignature verification runs locally, no HTTP call. All other resources call the merchant REST API.links,invoices,taxandshieldare live only. A sandbox key gets403 This feature is only available in live mode.on every one of their methods.refundsandtransfersare create, retrieve and list only. The shared CRUD base also exposesupdateanddeleteon them, but the API serves no such route.
Parcourir une liste par 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)
endpage.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.
WAJUB.payments.create(
params,
Wajub::RequestOptions.new(idempotency_key: "order-#{order.id}")
)| Élément | Valeur |
|---|---|
| Statuts concernés | 429, 500, 502, 503, 504 et toute erreur réseau |
| Tentatives | 3 au total, l'appel initial et deux nouvelles tentatives |
| Attente progressive | Exponentielle avec une part d'aléa (jitter), en respectant Retry-After pour une erreur 429 |
| Délai d'expiration | 30 secondes pour l'ouverture et 30 secondes pour la lecture |
Créez le paiement dans un job, pas dans le contrôleur
Une API lente bloque un thread Puma pendant 30 secondes au maximum. Trois appels de ce type peuvent épuiser un petit pool. Placez l'appel dans un ActiveJob et donnez-lui la même clé d'idempotence. Une nouvelle tentative du job retrouvera ainsi le même paiement au lieu d'en créer un second.
Agir pour un compte connecté
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é.
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
endraw_post, jamais params
La signature couvre {timestamp}.{raw body}. Rails a déjà analysé le corps dans params lorsque
votre action s'exécute. Le réencoder modifie l'ordre des clés et les espaces. Le hash ne correspond
alors plus. request.raw_post renvoie les octets tels qu'ils ont été livrés.
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
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| Classe | Cas de déclenchement |
|---|---|
Wajub::AuthenticationError | 401 |
Wajub::PermissionError | 403 |
Wajub::NotFoundError | 404 |
Wajub::InvalidRequestError | 400 et 422 |
Wajub::RateLimitError | 429, avec retry_after en secondes |
Wajub::WajubError | Tout autre statut. C'est aussi la classe parente de toutes les erreurs ci-dessus |
Wajub::ApiConnectionError | Aucune réponse, à cause du réseau ou d'un délai d'expiration |
Wajub::WebhookSignatureVerificationError | Un 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
# 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.
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
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
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
wajub listen --forward-to localhost:3000/wajub_webhooks
wajub trigger payment.succeededPour en savoir plus, consultez la CLI.
Pages associées
- Démarrage rapide des SDKsLe même premier appel dans cinq langages, présentés côte à côte.
- WebhooksTous les événements et leurs garanties de livraison.
- Conventions de nommage des SDKsPourquoi cette gem conserve le snake_case contrairement à Go et Java.
- IdempotenceCe que protège une clé et pendant combien de temps.
- Gestion des erreursLes échecs à retenter et ceux à présenter à l'utilisateur.
- Vue d'ensemble de la référence APILes endpoints utilisés par chaque méthode ci-dessus.