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 actuelle | Remplacement | Couverture |
|---|---|---|
| Collections and Disbursements | cm.mtn, ci.mtn, gh.mtn, 10 pays | |
| Web Payment, une API par marché | cm.orange, sn.orange, 8 pays | |
| Daraja STK Push | ke.mpesa, tz.mpesa, mz.mpesa | |
| Airtel Africa API | ke.airtel, ug.airtel, 9 pays | |
| Une API par marché | ci.moov, bj.moov, 6 pays | |
| Wave Business API | ci.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 paiement | POST /payments |
| Page de paiement | authorization_url, dans la même réponse |
| Confirmation | Un webhook signé, puis GET /payments/{id} |
| Remboursement | POST /refunds |
| Payout | POST /transfers |
| Sélection de l'opérateur | Champ 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.
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.
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 };
}
}Enregistrez l'identifiant renvoyé, pas seulement votre référence
Tous les appels suivants utilisent payment.id. Votre reference accompagne le paiement et vous
est renvoyée, mais elle ne sert pas à la recherche : GET /payments/{your-reference} répond avec
404. Conservez les deux colonnes sur la commande et indexez l'identifiant généré par Wajub.
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.
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.
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 ailleurs | Chez Wajub |
|---|---|
success, successful, completed | succeeded |
pending, initiated | pending |
ongoing, processing | processing |
| Aucun équivalent | partial, une partie du paiement fractionné est encaissée |
failed, declined | failed |
cancelled | cancelled |
abandoned, timeout | expired |
refunded | refunded |
| Aucun équivalent | partially_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.
Pages associées
- Accepter un paiement, guide completLe parcours cible présenté de bout en bout.
- IdempotenceSécurisez une nouvelle tentative pendant que deux systèmes fonctionnent en parallèle.
- Dépannage des paiementsLes premières erreurs rencontrées par une nouvelle intégration.
- Mise à niveau de l'APIL'autre migration, avec Wajub sur une version plus récente.