Cuando un comerciante envía una solicitud de pago a 0fee.dev, algo tiene que decidir qué proveedor lo maneja. No "Stripe o PayPal" -- esa es una elección binaria simple. La verdadera pregunta es: para un pago de Orange Money en Costa de Marfil, ¿debería el sistema usar PaiementPro (prioridad 1), PawaPay (prioridad 2) o Hub2 (prioridad 3)? Y si PaiementPro está caído, ¿debería recurrir automáticamente a PawaPay? Este es el motor de enrutamiento.
El problema central
0fee.dev cubre más de 53 proveedores en más de 200 países. Para muchos métodos de pago, múltiples proveedores pueden manejar la misma transacción. En el África francófona, un pago de Orange Money en Costa de Marfil puede ser procesado por PaiementPro, PawaPay, Hub2 o BUI. Cada uno tiene diferentes tarifas, diferentes registros de fiabilidad y diferentes brechas de cobertura.
El motor de enrutamiento debe:
- Descubrir qué métodos de pago están disponibles para un país dado.
- Seleccionar el mejor proveedor para un método de pago dado.
- Recurrir al siguiente proveedor si el primero falla.
- Respetar los proveedores y credenciales configurados del comerciante.
Descubrimiento de métodos de pago basado en país
El primer paso en cualquier pago es descubrir qué métodos de pago están disponibles. Esto es impulsado por la tabla payin_methods:
sqlCREATE TABLE payin_methods (
id TEXT PRIMARY KEY,
code TEXT UNIQUE NOT NULL, -- ej., "PAYIN_ORANGE_CI"
name TEXT NOT NULL, -- ej., "Orange Money Ivory Coast"
country_code TEXT NOT NULL, -- ej., "CI"
currency TEXT NOT NULL, -- ej., "XOF"
type TEXT NOT NULL, -- ej., "mobile_money"
operator TEXT, -- ej., "Orange"
is_active INTEGER DEFAULT 1,
min_amount INTEGER,
max_amount INTEGER,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);Cuando comienza una sesión de checkout, el frontend envía el país del cliente. El motor de enrutamiento consulta todos los métodos de pago activos para ese país:
pythonasync def get_payment_methods_for_country(
country_code: str
) -> list[dict]:
"""
Obtener todos los métodos de pago disponibles para un país.
Retorna métodos ordenados por tipo (dinero móvil primero, luego tarjeta).
"""
with get_db() as conn:
methods = conn.execute(
"""
SELECT code, name, type, operator, currency,
min_amount, max_amount
FROM payin_methods
WHERE country_code = ? AND is_active = 1
ORDER BY
CASE type
WHEN 'mobile_money' THEN 1
WHEN 'card' THEN 2
ELSE 3
END,
name
""",
(country_code.upper(),)
).fetchall()
return [dict(m) for m in methods]Para Costa de Marfil (CI), esto retorna:
| Código | Nombre | Tipo | Operador | Moneda |
|---|---|---|---|---|
PAYIN_ORANGE_CI | Orange Money Ivory Coast | mobile_money | Orange | XOF |
PAYIN_MTN_CI | MTN Mobile Money Ivory Coast | mobile_money | MTN | XOF |
PAYIN_WAVE_CI | Wave Ivory Coast | mobile_money | Wave | XOF |
PAYIN_MOOV_CI | Moov Money Ivory Coast | mobile_money | Moov | XOF |
PAYIN_CARD_GLOBAL | Card Payment | card | - | Múltiple |
Selección de proveedor basada en prioridad
Una vez elegido un método de pago, el motor de enrutamiento determina qué proveedor lo maneja. Esta es la tabla provider_routing:
sqlCREATE TABLE provider_routing (
id TEXT PRIMARY KEY,
payin_method_code TEXT NOT NULL, -- ej., "PAYIN_ORANGE_CI"
provider_id TEXT NOT NULL, -- ej., "paiementpro"
provider_method_code TEXT, -- ej., "OMCIV2"
priority INTEGER NOT NULL DEFAULT 1,
environment TEXT DEFAULT 'production',
is_active INTEGER DEFAULT 1,
UNIQUE(payin_method_code, provider_id, environment)
);El sistema de prioridad de tres niveles funciona así:
| Prioridad | Significado | Ejemplo |
|---|---|---|
| 1 | Proveedor principal -- usado primero | PaiementPro para PAYIN_ORANGE_CI |
| 2 | Proveedor secundario -- usado si la prioridad 1 falla | PawaPay para PAYIN_ORANGE_CI |
| 3 | Proveedor terciario -- último recurso | Hub2 para PAYIN_ORANGE_CI |
La columna provider_method_code es esencial. Cada proveedor usa sus propios códigos internos para el mismo método de pago. PaiementPro llama a Orange Money en Costa de Marfil "OMCIV2". PawaPay lo llama "ORANGE_CIV". Hub2 simplemente lo llama "Orange" con un parámetro de país. La tabla de enrutamiento mapea el código unificado (PAYIN_ORANGE_CI) al código interno de cada proveedor.
El algoritmo de enrutamiento
La función central de enrutamiento consulta la tabla provider_routing, ordena por prioridad y devuelve el mejor proveedor disponible:
pythonasync def get_provider_for_unified_method(
payment_method: str,
app_id: str,
environment: str = "production",
skip_providers: list[str] = None
) -> dict | None:
"""
Encontrar el mejor proveedor para un método de pago unificado.
"""
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 NoneLa lógica del algoritmo:
- Consultar todas las entradas de enrutamiento para el método de pago, ordenadas por prioridad (ascendente).
- Excluir cualquier proveedor en
skip_providers(usado para failover después de fallo). - Para cada ruta, verificar si el comerciante tiene credenciales activas para ese proveedor.
- Devolver la primera coincidencia -- el proveedor de mayor prioridad que el comerciante puede usar.
Este diseño maneja una situación común: un comerciante podría no tener credenciales de PaiementPro. Aunque PaiementPro es prioridad 1 para PAYIN_ORANGE_CI, si el comerciante solo tiene credenciales de Hub2, el motor salta PaiementPro y devuelve Hub2.
Lógica de failover
Cuando un pago falla a nivel del proveedor, el sistema puede reintentar con el siguiente proveedor en la cadena:
pythonasync def initiate_payment_with_fallback(
payment_method: str,
payment_data: dict,
app_id: str,
environment: str,
max_attempts: int = 3
) -> tuple[InitPaymentResult, str]:
"""
Intentar iniciar un pago, recurriendo a proveedores de menor
prioridad si el principal falla.
"""
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="No available provider could process this payment"
), ""Considera el failover en la práctica para un pago PAYIN_ORANGE_CI:
- Intento 1: Probar PaiementPro (prioridad 1). PaiementPro devuelve un error de timeout.
- Intento 2: Saltar PaiementPro, probar PawaPay (prioridad 2). PawaPay inicia exitosamente el pago.
- El comerciante y el cliente nunca saben que se intentó PaiementPro primero.
La tabla de enrutamiento: 117 métodos de pago
La tabla de enrutamiento completa mapea 117 métodos de pago en más de 30 países a sus configuraciones de proveedor. El patrón es consistente: para el África Occidental francófona (zona UEMOA), PaiementPro es típicamente prioridad 1 porque ofrece las mejores tarifas para dinero móvil local. PawaPay cubre la gama más amplia de países como opción secundaria. Hub2 sirve como fallback terciario.
Para África Oriental y Austral, PawaPay es típicamente el único proveedor configurado. Para pagos globales con tarjeta, Stripe es la única ruta.
El motor de enrutamiento como palanca de negocio
Más allá del enrutamiento técnico, el sistema de prioridades es una herramienta de negocio. Si 0fee.dev negocia una mejor tarifa con un nuevo proveedor, ajustar la tabla de enrutamiento cambia qué proveedor maneja el tráfico -- sin cambio de código, sin intervención del comerciante y sin tiempo de inactividad. Una sola sentencia UPDATE desplaza el volumen de pagos de un proveedor a otro.
Este es el poder de un motor de enrutamiento en un orquestador de pagos. Transforma la selección de proveedor de una decisión codificada en un proceso configurable e impulsado por datos que puede optimizarse continuamente.
Este artículo es parte de la serie "Cómo construimos 0fee.dev". 0fee.dev es un orquestador de pagos que cubre más de 53 proveedores en más de 200 países, construido por Juste A. GNIMAVO y Claude desde Abiyán sin ningún ingeniero humano. Sigue la serie para conocer la historia completa de construcción.