Back to 0fee
0fee

Le moteur de routage intelligent des paiements

Comment 0fee.dev route les paiements à travers 117 méthodes et 53+ fournisseurs grâce à une sélection par priorité avec repli automatique. Par Juste A. Gnimavo et Claude.

Thales & Claude | March 30, 2026 6 min 0fee
EN/ FR/ ES
routingpaymentsafricamobile-moneyarchitecture

Quand un marchand envoie une demande de paiement à 0fee.dev, quelque chose doit décider quel fournisseur la traite. Pas « Stripe ou PayPal » -- c'est un simple choix binaire. La vraie question est : pour un paiement Orange Money en Côte d'Ivoire, le système doit-il utiliser PaiementPro (priorité 1), PawaPay (priorité 2) ou Hub2 (priorité 3) ? Et si PaiementPro est en panne, doit-il automatiquement basculer vers PawaPay ? C'est le moteur de routage.

Le problème central

0fee.dev couvre 53+ fournisseurs dans 200+ pays. Pour de nombreuses méthodes de paiement, plusieurs fournisseurs peuvent traiter la même transaction. En Afrique francophone, un paiement Orange Money en Côte d'Ivoire peut être traité par PaiementPro, PawaPay, Hub2 ou BUI. Chacun a des frais différents, des historiques de fiabilité différents et des lacunes de couverture différentes.

Le moteur de routage doit :

  1. Découvrir quelles méthodes de paiement sont disponibles pour un pays donné.
  2. Sélectionner le meilleur fournisseur pour une méthode de paiement donnée.
  3. Basculer vers le fournisseur suivant si le premier échoue.
  4. Respecter les fournisseurs configurés et les identifiants du marchand.

Découverte des méthodes de paiement par pays

La première étape de tout paiement est de découvrir les méthodes de paiement disponibles. Cela est piloté par la table payin_methods :

sqlCREATE TABLE payin_methods (
    id TEXT PRIMARY KEY,
    code TEXT UNIQUE NOT NULL,     -- ex. "PAYIN_ORANGE_CI"
    name TEXT NOT NULL,            -- ex. "Orange Money Ivory Coast"
    country_code TEXT NOT NULL,    -- ex. "CI"
    currency TEXT NOT NULL,        -- ex. "XOF"
    type TEXT NOT NULL,            -- ex. "mobile_money"
    operator TEXT,                 -- ex. "Orange"
    is_active INTEGER DEFAULT 1,
    min_amount INTEGER,
    max_amount INTEGER,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP
);

Pour la Côte d'Ivoire (CI), cela renvoie :

CodeNomTypeOpérateurDevise
PAYIN_ORANGE_CIOrange Money Ivory Coastmobile_moneyOrangeXOF
PAYIN_MTN_CIMTN Mobile Money Ivory Coastmobile_moneyMTNXOF
PAYIN_WAVE_CIWave Ivory Coastmobile_moneyWaveXOF
PAYIN_MOOV_CIMoov Money Ivory Coastmobile_moneyMoovXOF
PAYIN_CARD_GLOBALCard Paymentcard-Multiple

Sélection de fournisseur par priorité

Une fois la méthode de paiement choisie, le moteur de routage détermine quel fournisseur la traite. Le système de priorité à trois niveaux fonctionne ainsi :

PrioritéSignificationExemple
1Fournisseur principal -- utilisé en premierPaiementPro pour PAYIN_ORANGE_CI
2Fournisseur secondaire -- utilisé si la priorité 1 échouePawaPay pour PAYIN_ORANGE_CI
3Fournisseur tertiaire -- dernier recoursHub2 pour PAYIN_ORANGE_CI

La colonne provider_method_code est essentielle. Chaque fournisseur utilise ses propres codes internes pour la même méthode de paiement. PaiementPro appelle Orange Money en Côte d'Ivoire « OMCIV2 ». PawaPay l'appelle « ORANGE_CIV ». Hub2 l'appelle simplement « Orange » avec un paramètre pays. La table de routage mappe le code unifié (PAYIN_ORANGE_CI) vers le code interne de chaque fournisseur.

L'algorithme de routage

La fonction de routage principale interroge la table provider_routing, ordonne par priorité et renvoie le meilleur fournisseur disponible :

pythonasync def get_provider_for_unified_method(
    payment_method: str,
    app_id: str,
    environment: str = "production",
    skip_providers: list[str] = None
) -> dict | None:
    skip_providers = skip_providers or []

    with get_db() as conn:
        routes = conn.execute(
            """
            SELECT pr.provider_id, pr.provider_method_code, pr.priority
            FROM provider_routing pr
            WHERE pr.payin_method_code = ?
              AND pr.environment = ?
              AND pr.is_active = 1
              AND pr.provider_id NOT IN ({})
            ORDER BY pr.priority ASC
            """.format(",".join("?" * len(skip_providers))),
            (payment_method, environment, *skip_providers)
        ).fetchall()

        if not routes:
            return None

        for route in routes:
            has_creds = conn.execute(
                """
                SELECT 1 FROM provider_credentials
                WHERE app_id = ? AND provider_id = ?
                  AND environment = ? AND is_active = 1
                """,
                (app_id, route["provider_id"], environment)
            ).fetchone()

            if has_creds:
                return {
                    "provider_id": route["provider_id"],
                    "provider_method_code": route["provider_method_code"],
                    "priority": route["priority"],
                }

        return None

Logique de repli

Quand un paiement échoue au niveau du fournisseur, le système peut réessayer avec le fournisseur suivant dans la chaîne :

pythonasync def initiate_payment_with_fallback(
    payment_method: str,
    payment_data: dict,
    app_id: str,
    environment: str,
    max_attempts: int = 3
) -> tuple[InitPaymentResult, str]:
    skip_providers = []

    for attempt in range(max_attempts):
        route = await get_provider_for_unified_method(
            payment_method, app_id, environment,
            skip_providers=skip_providers
        )

        if not route:
            break

        provider_id = route["provider_id"]
        credentials = await get_decrypted_credentials(
            app_id, provider_id, environment
        )
        provider = provider_registry.get_instance(
            provider_id, credentials, app_id
        )

        payment_data["provider_method_code"] = route["provider_method_code"]
        result = await provider.initiate_payment(payment_data)

        if result.status != "failed":
            return result, provider_id

        skip_providers.append(provider_id)

    return InitPaymentResult(
        provider_ref="",
        status="failed",
        instructions="Aucun fournisseur disponible n'a pu traiter ce paiement"
    ), ""

Considérez le repli en pratique pour un paiement PAYIN_ORANGE_CI :

  1. Tentative 1 : Essayer PaiementPro (priorité 1). PaiementPro renvoie une erreur de timeout.
  2. Tentative 2 : Ignorer PaiementPro, essayer PawaPay (priorité 2). PawaPay initie le paiement avec succès.
  3. Le marchand et le client ne savent jamais que PaiementPro a été tenté en premier.

La table de routage : 117 méthodes de paiement

La table de routage complète mappe 117 méthodes de paiement dans 30+ pays vers leurs configurations de fournisseurs. Le pattern est cohérent : pour l'Afrique de l'Ouest francophone (zone UEMOA), PaiementPro est généralement en priorité 1 car il offre les meilleurs tarifs pour le mobile money local. PawaPay couvre le plus large éventail de pays comme option secondaire. Hub2 sert de repli tertiaire.

Le moteur de routage comme levier commercial

Au-delà du routage technique, le système de priorité est un outil commercial. Si 0fee.dev négocie un meilleur tarif avec un nouveau fournisseur, ajuster la table de routage change quel fournisseur gère le trafic -- sans modification de code, sans intervention du marchand et sans temps d'arrêt. Une seule instruction UPDATE déplace le volume de paiements d'un fournisseur à un autre.

C'est la puissance d'un moteur de routage dans un orchestrateur de paiement. Il transforme la sélection de fournisseur d'une décision codée en dur en un processus configurable et piloté par les données, optimisable en continu.


Cet article fait partie de la série « Comment nous avons construit 0fee.dev ». 0fee.dev est un orchestrateur de paiement couvrant 53+ fournisseurs dans 200+ pays, construit par Juste A. GNIMAVO et Claude depuis Abidjan sans aucun ingénieur humain. Suivez la série pour l'histoire complète de la construction.

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles