Aller au contenu

Java

L'artefact wajub-java de com.wajub, ses services et ses exceptions non vérifiées.

wajub-java 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.

com.wajub:wajub-java

Stable · GA

Maven Central

Version
1.1.1
Runtime
Java 17+, OkHttp 4.12, Jackson 2.18

Couvre

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

Installation

<dependency>
  <groupId>com.wajub</groupId>
  <artifactId>wajub-java</artifactId>
  <version>1.2.1</version>
</dependency>

Il nécessite Java 17 ou une version plus récente, et installe OkHttp 4.12.0 et jackson-databind 2.18.2 avec lui. Si votre application fixe déjà leur version, les règles habituelles de gestion des dépendances s'appliquent.

Créer le client

Wajub.create est le point d'entrée. Il utilise les variables d'environnement en repli. La forme courte suffit donc dans la plupart des applications.

Deux façons de le construire
import com.wajub.Wajub;

// Reads WAJUB_API_KEY and WAJUB_WEBHOOK_SECRET when the field is blank.
Wajub wajub = Wajub.create(Wajub.Config.builder().build());

// Or spell everything out.
Wajub wajub = Wajub.create(
    Wajub.Config.builder()
        .apiKey(System.getenv("WAJUB_API_KEY"))
        .webhookSecret(System.getenv("WAJUB_WEBHOOK_SECRET"))
        .build());

// Or pass just the key.
Wajub wajub = Wajub.create(System.getenv("WAJUB_API_KEY"));
Méthode du builderRôle
apiKeyVotre clé sk. ou sk_test.. Utilise WAJUB_API_KEY en repli
webhookSecretLe secret whsec_. Utilise WAJUB_WEBHOOK_SECRET en repli
httpClientVotre propre OkHttpClient, pour un intercepteur, un proxy ou le traçage

Une clé vide déclenche IllegalArgumentException: wajub: api key is required lors de la construction. Une mauvaise configuration échoue donc au démarrage, pas lors du premier paiement.

Sous forme de bean Spring
@Configuration
public class WajubConfiguration {

    @Bean
    Wajub wajub(@Value("${wajub.api-key}") String apiKey,
                @Value("${wajub.webhook-secret}") String webhookSecret) {
        return Wajub.create(
            Wajub.Config.builder()
                .apiKey(apiKey)
                .webhookSecret(webhookSecret)
                .build());
    }
}

Le client est sûr pour les accès concurrents. Un singleton pour toute l'application est donc le bon choix.

Le premier appel

Créer un paiement
import com.wajub.Payment;

Payment payment = wajub.payments().create(
    Map.of(
        "amount", 25000,
        "currency", "XAF",
        "email", "amina@example.com",
        "description", "Order 4172",
        "reference", "order-4172",
        "callback", "https://shop.example.com/complete"),
    null);

return "redirect:" + payment.getAuthorizationUrl();

Les ressources sont des méthodes, pas des champs. Utilisez donc wajub.payments() et non wajub.payments. Les paramètres sont transmis dans un Map<String, Object> avec les noms de champs exacts de l'API.

Tout ce qui dépasse les six champs décodés
String reference = (String) payment.getRaw().get("reference");
Number amount = (Number) payment.getRaw().get("amount");

Les montants utilisent l'unité principale

25000 avec XAF représente vingt-cinq mille francs, pas deux cent cinquante.

Tous les services du client

ServiceMéthodes
client.global()ping, channels, countries, currencies
client.payments()create, retrieve, list, cancel, process, processSplit, listRefunds
client.customers()create, retrieve, update, delete, list, block, unblock, activate, deactivate, listTaxIds, createTaxId, deleteTaxId
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, markPaid, cancel
client.accounts()create, retrieve, update, delete, list, regenerateToken
client.webhookEndpoints()create, retrieve, update, delete, list, rotateSecret
client.balance()retrieve
client.events()list, retrieve, resend
client.disputes()list, retrieve, submitEvidence, accept, close, sendMessage
client.identity()resolve, validate
client.tax()getSettings, updateSettings, rates, calculate, reports, listCodes, retrieveCode, listRegistrations, createRegistration, retrieveRegistration, updateRegistration, deleteRegistration, jurisdictions, thresholds, thresholdAlerts
client.shield()getSettings, updateSettings, stats, listBlocklist, addToBlocklist, removeFromBlocklist
client.webhooks()constructEvent
  • webhooks / ConstructEvent s'exécute en local, sans appel HTTP. Tous les autres services 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
PagedResult page = wajub.payments().list(Map.of("status", "succeeded", "per_page", 50));

for (Map<String, Object> payment : page.getData()) {
    log.info("{} {}", payment.get("id"), payment.get("amount"));
}

if (page.hasMore()) {
    page = page.getNextPage();
}

// Or collect every page at once.
List<Map<String, Object>> all = page.autoPaging();

Les lignes sont des Map<String, Object>, pas des objets typés. autoPaging() simplifie le parcours, mais consomme plus de mémoire, car il conserve toutes les lignes avant de les renvoyer.

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, passez votre propre clé.

Enchaîner les RequestOptions
Payment payment = wajub.payments().create(
    params,
    new RequestOptions().setIdempotencyKey("order-" + orderId));

Pour définir vos propres délais d'expiration, fournissez votre propre OkHttpClient au builder.

Votre propre client OkHttp
OkHttpClient http = new OkHttpClient.Builder()
    .connectTimeout(Duration.ofSeconds(10))
    .readTimeout(Duration.ofSeconds(60))
    .addInterceptor(new TracingInterceptor())
    .build();

Wajub wajub = Wajub.create(Wajub.Config.builder().httpClient(http).build());

Agir pour un compte connecté

Un appel pour un vendeur
wajub.payments().create(
    params,
    new RequestOptions().setSync(seller.getWajubAccountId()));

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

Webhooks

constructEvent vérifie la signature et renvoie l'événement analysé. Il accepte le corps sous forme d'octets, exactement tel qu'il a été livré.

Un contrôleur Spring Boot
@PostMapping(value = "/webhooks/wajub", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> wajubWebhook(
        @RequestBody byte[] body,
        @RequestHeader("X-Wajub-Signature") String signature,
        @RequestHeader("X-Wajub-Timestamp") String timestamp) {

    Map<String, Object> event;
    try {
        event = wajub.webhooks().constructEvent(body, signature, timestamp, null);
    } catch (WebhookSignatureVerificationException e) {
        return ResponseEntity.badRequest().build();
    }

    if ("payment.succeeded".equals(event.get("event"))) {
        fulfilmentQueue.submit(event);
    }

    return ResponseEntity.ok().build();
}

Une tolérance null applique la fenêtre par défaut de 300 secondes, elle ne supprime pas la vérification. Il est volontairement impossible de désactiver la protection contre les rejeux.

Le nom de l'événement se trouve dans event, pas dans type, et le résultat est un Map<String, Object>. Consultez Vérification de signature.

Erreurs

Intercepter d'abord l'erreur précise
import com.wajub.exception.*;

try {
    wajub.payments().create(params, null);
} catch (InvalidRequestException e) {
    // e.getErrors() is {"amount": "The amount must be at least 25."}
    return ResponseEntity.unprocessableEntity().body(e.getErrors());
} catch (RateLimitException e) {
    return ResponseEntity.status(503)
        .header("Retry-After", String.valueOf(e.getRetryAfter()))
        .build();
} catch (WajubException e) {
    log.error("wajub failed code={} status={}", e.getCode(), e.getHttpStatus());
    throw e;
}
Classe dans com.wajub.exceptionCas de déclenchement
AuthenticationException401
PermissionException403
NotFoundException404
InvalidRequestException400 et 422
RateLimitException429, avec getRetryAfter() en secondes
WajubExceptionTout autre statut. C'est aussi la classe parente de toutes les exceptions ci-dessus
ApiConnectionExceptionAucune réponse, à cause du réseau ou d'un délai d'expiration
WebhookSignatureVerificationExceptionUn webhook dont la signature n'a pas été vérifiée

Toutes exposent getMessage(), getCode(), getHttpStatus(), getErrors() et getRaw().

getErrors() renvoie un Map<String, String>, avec le premier message de chaque champ.

Tester sans appeler l'API

Fournissez au builder un OkHttpClient dont l'intercepteur répond avant le départ de l'appel, et tout le SDK fonctionne hors ligne.

Un test JUnit sans réseau
Interceptor stub = chain -> new Response.Builder()
    .request(chain.request())
    .protocol(Protocol.HTTP_1_1)
    .code(201)
    .message("Created")
    .body(ResponseBody.create("""
        {"code":201,
         "authorization_url":"https://pay.wajub.com/test_xxxxx",
         "authorization_token":"test_xxxxx",
         "transaction":{"id":"trx_test","status":"pending","amount":25000}}
        """, MediaType.get("application/json")))
    .build();

Wajub wajub = Wajub.create(
    Wajub.Config.builder()
        .apiKey("sk_test.fake")
        .httpClient(new OkHttpClient.Builder().addInterceptor(stub).build())
        .build());

Payment payment = wajub.payments().create(Map.of("amount", 25000, "currency", "XAF"), null);

assertEquals("test_xxxxx", payment.getAuthorizationToken());

Débiter sans page hébergée

Requête push Mobile Money
Payment payment = wajub.payments().create(
    Map.of("amount", 25000, "currency", "XAF", "phone", "+237670000000"),
    null);

wajub.payments().process(
    payment.getId(),
    Map.of("channel", "cm.mtn", "phone", "+237670000000"),
    null);

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
Map<String, Object> transfer = wajub.transfers().create(
    Map.of(
        "amount", 100000,
        "currency", "XAF",
        "beneficiary", Map.of(
            "name", "Amina Diallo",
            "channel", "cm.mtn",
            "phone", "+237670000000"),
        "reference", "payout-892"),
    new RequestOptions().setIdempotencyKey("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:8080/webhooks/wajub
wajub trigger payment.succeeded

Pour en savoir plus, consultez la CLI.

Que pensez-vous de ce contenu ?