Aller au contenu

PHP

Le package wajub/wajub-php, ses ressources et son intégration dans une application Laravel.

wajub/wajub-php constitue la partie serveur d'une intégration Wajub. Il conserve votre clé secrète, crée des paiements, lit leur statut réel et vérifie les signatures des webhooks. Il repose sur Guzzle, respecte PSR-4 et ne suppose aucun framework.

wajub/wajub-php

Stable · GA

Packagist

Version
2.0.0
Runtime
PHP 8.4+, Guzzle 8.2+, ext-json

Couvre

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

Installer

Un package et deux dépendances
composer require wajub/wajub-php

Créer le client

Le constructeur accepte un tableau. api_key n'a aucune valeur de repli dans l'environnement, lisez donc vous-même la variable. Seul webhook_secret retombe sur WAJUB_WEBHOOK_SECRET quand il est omis.

PHP simple
<?php

use Wajub\Wajub;

$wajub = new Wajub([
    'api_key' => getenv('WAJUB_API_KEY'),
    'webhook_secret' => getenv('WAJUB_WEBHOOK_SECRET'),
]);
CléFonction
api_keyVotre clé sk. ou sk_test.. Obligatoire, une valeur vide génère une erreur
webhook_secretSecret whsec_, uniquement nécessaire pour webhooks->constructEvent(). WAJUB_WEBHOOK_SECRET par défaut
idempotency_key_prefixPréfixe de la clé générée. 'wajub' par défaut
http_clientVotre propre GuzzleHttp\Client, pour un proxy ou un handler simulé dans les tests
max_network_retriesNouvelles tentatives sur les échecs temporaires. 2 par défaut
timeoutSecondes transmises au client Guzzle par défaut. 30 par défaut

Une clé manquante génère InvalidArgumentException: Wajub: api_key is required lors de la construction. Une mauvaise configuration échoue donc au démarrage plutôt qu'au premier paiement.

Dans une application Laravel

Associez-le une fois comme singleton, puis injectez-le. Un seul pool de connexions Guzzle est ainsi conservé pendant tout le processus.

<?php
// app/Providers/AppServiceProvider.php

use Wajub\Wajub;

public function register(): void
{
    $this->app->singleton(Wajub::class, fn () => new Wajub([
        'api_key' => config('services.wajub.secret'),
        'webhook_secret' => config('services.wajub.webhook_secret'),
    ]));
}

Dans Laravel, ajoutez les deux clés à config/services.php au lieu d'appeler env() hors d'un fichier de configuration. Sinon, php artisan config:cache vous donnera une clé nulle en production.

config/services.php
'wajub' => [
    'secret' => env('WAJUB_API_KEY'),
    'webhook_secret' => env('WAJUB_WEBHOOK_SECRET'),
],

Premier appel

Créer un paiement
$payment = $wajub->payments->create([
    'amount' => 25000,
    'currency' => 'XAF',
    'email' => 'amina@example.com',
    'description' => 'Order 4172',
    'reference' => 'order-4172',
    'callback' => route('checkout.complete'),
]);

return redirect()->away($payment->authorization_url);

Les paramètres sont transmis dans un tableau associatif avec les noms de champs exacts de l'API. La réponse est un ApiObject. Chaque champ est donc une propriété et aucune donnée n'est perdue.

Lire le résultat
$payment->id;                   // trx_test_8kQ2mW9vB4nL6hR1cY3d
$payment->status;               // pending
$payment->authorization_url;    // https://pay.wajub.com/tok_xxxxx
$payment->authorization_token;  // tok_xxxxx

// A field added by the API after this release is still readable.
$payment->settlement_batch_id;

Les montants utilisent l'unité principale

25000 en XAF représente vingt-cinq mille francs. Une devise décimale utilise une valeur décimale : 'amount' => 12.50 en GHS.

Toutes les ressources du client

PropertyMéthodes
$wajub->globalping, channels, countries, currencies
$wajub->paymentscreate, initialize, retrieve, list, cancel, process, processSplit, listRefunds
$wajub->customerscreate, retrieve, update, delete, list, block, unblock, activate, deactivate, listTaxIds, createTaxId, deleteTaxId
$wajub->refundscreate, retrieve, list
$wajub->transferscreate, retrieve, list
$wajub->beneficiariescreate, retrieve, update, delete, list
$wajub->linkscreate, retrieve, update, delete, list
$wajub->invoicescreate, retrieve, update, delete, list, send, markPaid, cancel
$wajub->accountscreate, retrieve, update, delete, list, regenerateToken
$wajub->webhookEndpointscreate, retrieve, update, delete, list, rotateSecret
$wajub->balanceretrieve
$wajub->eventslist, retrieve, resend
$wajub->disputeslist, retrieve, submitEvidence, accept, close, sendMessage
$wajub->identityresolve, validate
$wajub->taxgetSettings, updateSettings, rates, calculate, reports, listCodes, retrieveCode, listRegistrations, createRegistration, retrieveRegistration, updateRegistration, deleteRegistration, jurisdictions, thresholds, thresholdAlerts
$wajub->shieldgetSettings, updateSettings, stats, listBlocklist, addToBlocklist, removeFromBlocklist
$wajub->listenconfig, auth
$wajub->webhooksconstructEvent
  • 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 paginée

list() renvoie un PagedResult qui implémente Iterator. Un foreach sur l'objet parcourt donc la page actuelle, tandis que autoPagingIterator() parcourt toutes les pages.

Une page ou toutes les pages
$page = $wajub->payments->list(['status' => 'success', 'per_page' => 50]);

foreach ($page->data as $payment) {
    echo $payment['id'].' '.$payment['amount'];
}

if ($page->hasMore) {
    $page = $page->getNextPage();
}

// Or let it fetch the following pages for you.
foreach ($wajub->payments->list()->autoPagingIterator() as $payment) {
    $this->reconcile($payment);
}

$page->data contient de simples tableaux, pas des ApiObject. Les lignes utilisent donc l'accès par tableau. $page->meta contient total, per_page, current_page et last_page lorsque l'endpoint les renvoie.

Idempotence et nouvelles tentatives

Chaque POST et PUT contient une valeur Idempotency-Key générée, qui sécurise les nouvelles tentatives automatiques. Transmettez votre propre clé lorsqu'une valeur naturelle existe.

Un numéro de commande constitue la meilleure clé
use Wajub\RequestOptions;

$wajub->payments->create(
    $params,
    new RequestOptions(idempotencyKey: "order-{$order->id}"),
);
ÉlémentValeur
Statuts réessayés429, 500, 502, 503, 504 et tout échec réseau
Tentatives3 au total, un appel initial et deux nouvelles tentatives
Attente progressiveExponentielle avec part d'aléa, en respectant Retry-After quand il est présent, jamais plus de 10 secondes
Délai d'expiration30 secondes par requête

Agir pour un compte connecté

Un appel pour un vendeur
use Wajub\RequestOptions;

$wajub->payments->create(
    ['amount' => 25000, 'currency' => 'XAF', 'email' => $buyer->email],
    new RequestOptions(sync: $seller->wajub_account_id),
);

sync devient l'en-tête X-Sync. La configuration figure dans Sync.

Webhooks

constructEvent vérifie la signature et renvoie l'événement analysé. Il nécessite le corps exact reçu.

Un contrôleur Laravel
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Wajub\Exception\WebhookSignatureVerificationError;
use Wajub\Wajub;

class WajubWebhookController extends Controller
{
    public function __invoke(Request $request, Wajub $wajub)
    {
        try {
            $event = $wajub->webhooks->constructEvent(
                $request->getContent(),
                $request->header('X-Wajub-Signature', ''),
                $request->header('X-Wajub-Timestamp', ''),
            );
        } catch (WebhookSignatureVerificationError) {
            return response()->noContent(400);
        }

        if ($event['event'] === 'payment.succeeded') {
            HandleWajubEvent::dispatch($event);
        }

        return response()->noContent(200);
    }
}

Une route de webhook Laravel nécessite deux autres précautions. Excluez-la de la protection CSRF, car Wajub ne possède aucun jeton de session, et répondez rapidement. Accusez réception avec 200, puis confiez le travail à une tâche en file d'attente. La livraison est réessayée si votre endpoint répond trop lentement.

bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->validateCsrfTokens(except: ['webhooks/wajub']);
})

Le nom de l'événement se trouve dans event, pas dans type. La tolérance est de 300 secondes par défaut. constructEvent accepte un quatrième argument si le décalage de vos horloges est supérieur. Consultez Vérification de signature.

Erreurs

Intercepter d'abord l'erreur précise
use Wajub\Exception\InvalidRequestException;
use Wajub\Exception\RateLimitException;
use Wajub\Exception\WajubError;

try {
    $wajub->payments->create($params);
} catch (InvalidRequestException $e) {
    // $e->errors vaut ['amount' => 'The amount must be at least 25.']
    return response()->json(['fields' => $e->errors], 422);
} catch (RateLimitException $e) {
    return response()->noContent(503)
        ->header('Retry-After', (string) ($e->retryAfter ?? 5));
} catch (WajubError $e) {
    Log::error('wajub failed', ['code' => $e->errorCode, 'status' => $e->httpStatus]);
    throw $e;
}
Classe dans Wajub\ExceptionCause
AuthenticationException401
PermissionException403
NotFoundException404
InvalidRequestException400 et 422
RateLimitException429, avec retryAfter en secondes
WajubErrorTout autre statut et classe parente de toutes les erreurs ci-dessus
ApiConnectionExceptionAucune réponse, problème réseau ou délai d'expiration
WebhookSignatureVerificationErrorÉchec de vérification d'un webhook

Tester sans appeler l'API

L'option http_client accepte tout client Guzzle. Un handler simulé permet donc d'utiliser tout le SDK hors ligne.

Un test PHPUnit sans réseau
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use Wajub\Wajub;

$mock = new MockHandler([
    new Response(201, [], json_encode([
        'code' => 201,
        'authorization_url' => 'https://pay.wajub.com/tok_test',
        'authorization_token' => 'tok_test',
        'transaction' => ['id' => 'trx_test', 'status' => 'pending', 'amount' => 25000],
    ])),
]);

$wajub = new Wajub([
    'api_key' => 'sk_test.fake',
    'http_client' => new Client(['handler' => HandlerStack::create($mock)]),
]);

$payment = $wajub->payments->create(['amount' => 25000, 'currency' => 'XAF']);

$this->assertSame('tok_test', $payment->authorization_token);

Débiter sans la page hébergée

Requête push Mobile Money
$payment = $wajub->payments->create([
    'amount' => 25000,
    'currency' => 'XAF',
    'phone' => '+237670000000',
]);

$wajub->payments->process($payment->id, [
    'channel' => 'cm.mtn',
    'phone' => '+237670000000',
]);

Un canal suit le format country.operator. cm.mtn désigne donc MTN Mobile Money au Cameroun. La liste complète figure dans Moyens de paiement et canaux. Le payeur doit encore approuver sur son téléphone. Le résultat arrive donc dans le webhook.

Effectuer un payout

Un transfert vers un numéro de téléphone
use Wajub\RequestOptions;

$transfer = $wajub->transfers->create(
    [
        'amount' => 100000,
        'currency' => 'XAF',
        'beneficiary' => [
            'name' => 'Amina Diallo',
            'channel' => 'cm.mtn',
            'phone' => '+237670000000',
        ],
        'reference' => 'payout-892',
    ],
    new RequestOptions(idempotencyKey: 'payout-892'),
);

beneficiary accepte aussi l'identifiant ben_… d'un bénéficiaire enregistré. Préférez cette forme dès que vous payez deux fois la même personne.

Développement local

Deux terminaux
wajub listen --forward-to localhost:8000/webhooks/wajub
wajub trigger payment.succeeded

Consultez la CLI pour plus de détails.

Que pensez-vous de ce contenu ?