Aller au contenu

Migrer depuis un prestataire

Cartographiez l'ancienne intégration, exécutez les deux en parallèle, puis basculez.

La méthode reste la même, que vous quittiez un agrégateur, l'API directe d'un opérateur ou un code écrit il y a quatre ans. L'ordre compte davantage que la technique : cartographiez d'abord, exécutez ensuite les deux systèmes, puis basculez.

Ce que vous remplacez

Toutes les intégrations directes avec des opérateurs sont remplacées par le même appel. Vous n'avez plus à maintenir un client, un ensemble d'identifiants et un format de webhook par opérateur.

Intégration directe actuelleRemplacementCouverture
MTN MoMoMTN MoMoCollections and Disbursementscm.mtn, ci.mtn, gh.mtn, 10 pays
Orange MoneyOrange MoneyWeb Payment, une API par marchécm.orange, sn.orange, 8 pays
M-PesaM-PesaDaraja STK Pushke.mpesa, tz.mpesa, mz.mpesa
Airtel MoneyAirtel MoneyAirtel Africa APIke.airtel, ug.airtel, 9 pays
Moov MoneyMoov MoneyUne API par marchéci.moov, bj.moov, 6 pays
WaveWaveWave Business APIci.wave, sn.wave

Couverture répertorie tous les pays et canaux. Consultez-la avant toute autre étape. L'absence d'un corridor indispensable modifie votre plan de migration.

Phase 1 : cartographier l'existant

Intégration actuelleÉquivalent Wajub
Initialiser un paiementPOST /payments
Page de paiementauthorization_url, dans la même réponse
ConfirmationUn webhook signé, puis GET /payments/{id}
RemboursementPOST /refunds
PayoutPOST /transfers
Sélection de l'opérateurChamp channel pendant le traitement

Créez ce tableau pour votre propre code avant d'en écrire. Les lignes que vous ne pouvez pas remplir constituent la véritable migration. Elles se trouvent généralement dans le parcours de confirmation.

Phase 2 : placer les deux derrière une interface

Deux prestataires peuvent fonctionner en parallèle uniquement si le reste de votre code ne les distingue pas. Utilisez une seule interface et un adaptateur pour chacun.

L'interface implémentée par les deux prestataires
export class PaymentGateway {
  async initialize(order) { throw new Error('not implemented'); }
  async verify(id) { throw new Error('not implemented'); }
  async refund(paymentId, amount) { throw new Error('not implemented'); }
}

Pour Wajub, utilisez le SDK officiel plutôt qu'un wrapper fetch écrit à la main. Il gère les nouvelles tentatives, les clés d'idempotence et les erreurs typées que vous devriez sinon écrire deux fois.

L'adaptateur Wajub
import Wajub from '@wajub/node';
import { PaymentGateway } from './payment-gateway.js';

const wajub = new Wajub({
  secretKey: process.env.WAJUB_SECRET_KEY,
  webhookSecret: process.env.WAJUB_WEBHOOK_SECRET,
});

export class WajubGateway extends PaymentGateway {
  async initialize(order) {
    const payment = await wajub.payments.create(
      {
        amount: order.amount,
        currency: order.currency,
        customer: { email: order.email, phone: order.phone },
        reference: `order-${order.id}`,
        callback: `${process.env.BASE_URL}/payment/return`,
      },
      { idempotencyKey: `order-${order.id}` },
    );

    return { id: payment.id, redirectUrl: payment.authorization_url };
  }

  async verify(id) {
    const payment = await wajub.payments.retrieve(id);
    return { status: payment.status, amount: payment.amount };
  }

  async refund(paymentId, amount) {
    const refund = await wajub.refunds.create({
      payment: paymentId,
      amount,
      reason: 'requested_by_customer',
    });

    return { id: refund.id, status: refund.status };
  }
}

Phase 3 : exécuter les deux systèmes

Conservez l'ancien prestataire comme référence pendant que Wajub traite le même trafic dans la sandbox. Il ne s'agit pas d'encaisser deux fois, mais de comparer deux réponses à la même question.

Dupliquer dans la sandbox sans modifier le parcours live
export async function initialize(order) {
  const legacy = await legacyGateway.initialize(order);

  if (process.env.WAJUB_MIRROR === 'true') {
    wajubSandboxGateway
      .initialize(order)
      .then((mirrored) => logComparison(order.id, legacy, mirrored))
      .catch((err) => logger.warn({ err, orderId: order.id }, 'mirror failed'));
  }

  return legacy;
}

L'appel dupliqué n'est volontairement pas attendu et son échec n'est pas bloquant. Une duplication capable de casser le checkout est pire que son absence.

Phase 4 : déplacer le trafic

Répartissez le trafic à partir d'une valeur stable afin qu'un client qui recharge la page ne change pas de prestataire pendant sa commande.

Un réglage progressif, pas un interrupteur
import { createHash } from 'node:crypto';

function bucketOf(orderId) {
  const digest = createHash('sha256').update(String(orderId)).digest('hex');
  return parseInt(digest.slice(0, 8), 16) % 100;
}

export function gatewayFor(orderId) {
  const percent = Number(process.env.WAJUB_ROLLOUT_PERCENT ?? 0);
  return bucketOf(orderId) < percent ? wajubGateway : legacyGateway;
}

Augmentez le pourcentage tant que le taux de réussite, le délai de confirmation et le comportement des remboursements restent stables. Toute variation justifie une pause, pas une accélération.

Quatre différences qui cassent les intégrations

Les montants utilisent les unités principales

amount: 25000 signifie 25 000 XAF. Les prestataires qui utilisent des kobo, des pesewas ou des centimes ont habitué votre code à multiplier par cent. Cette habitude transforme une commande de 250 XAF en commande de 25 000 XAF. Les limites par devise figurent dans Plafonds et quotas.

Les statuts ne correspondent pas un à un

Wajub utilise neuf statuts de paiement. Deux d'entre eux n'ont aucun équivalent chez la plupart des prestataires.

Statut ailleursChez Wajub
success, successful, completedsucceeded
pending, initiatedpending
ongoing, processingprocessing
Aucun équivalentpartial, une partie du paiement fractionné est encaissée
failed, declinedfailed
cancelledcancelled
abandoned, timeoutexpired
refundedrefunded
Aucun équivalentpartially_refunded

Traitez partial et processing comme des statuts non définitifs plutôt que comme des échecs. Vous éviterez d'annuler des commandes qui allaient réussir.

Le navigateur ne confirme pas le paiement

Si votre ancien prestataire vous permettait de marquer une commande comme payée depuis l'URL de retour, ce raccourci ne fonctionne plus après la migration. La redirection indique uniquement que le payeur est revenu. Seul le webhook signé ou un appel GET /payments/{id} depuis votre serveur peut confirmer le paiement d'une commande.

Les signatures des webhooks sont obligatoires

Chaque livraison contient un horodatage et une signature HMAC-SHA256. Leur vérification constitue la base du modèle de sécurité. Vérifiez la signature sur le corps brut avant de l'analyser. Vérification de signature fournit le code pour chaque langage.

Effectuez le rapprochement avant la bascule

Exécutez les deux systèmes pendant au moins 72 heures de trafic réel. Comparez les montants, les statuts et les frais dans API Logs. Un écart découvert à cette étape coûte une requête. Le même écart découvert après la bascule exige un rapprochement.

Après la bascule

Les intégrations conservées pour la redondance ne sont plus utiles. Orchestration des paiements route un même appel entre plusieurs prestataires. Une panne d'opérateur déclenche alors un repli au lieu de perdre la transaction. Configurez ce comportement dans Cascade et repli.

Que pensez-vous de ce contenu ?