Descargo de responsabilidad: Este artículo es material educativo de integración de sistemas financieros. Las APIs de pasarelas de pago cambian frecuentemente; consultar siempre la documentación oficial vigente de cada proveedor antes de implementar. El manejo incorrecto de credenciales o flujos de pago puede resultar en pérdidas financieras directas. El uso de este contenido es bajo exclusiva responsabilidad del desarrollador u operador.
1. El Problema del Vendor Lock-in en Pagos LATAM
Integrar una sola pasarela de pagos parece eficiente al inicio, pero en LATAM se convierte rápidamente en riesgo operativo. Cada país tiene preferencias locales distintas: México domina con OXXO y SPEI, Colombia con PSE y Nequi, Perú con Yape y BCP, Brasil con PIX. Ninguna pasarela cubre todos los métodos con igual calidad, precio y tasa de aprobación.
Más crítico aún: cuando tu única pasarela sufre una caída regional (evento común en LATAM), tu plataforma deja de facturar completamente. La solución arquitectónica es la abstracción multi-gateway: una capa intermedia que normaliza las diferencias entre proveedores y permite cambiar de gateway sin modificar lógica de negocio, habilitando failover automático y optimización dinámica de rutas de pago.
2. Patrón Adapter: Normalización de Interfaces Heterogéneas
Cada pasarela tiene su propia API, formatos de respuesta, códigos de error y mecanismos de autenticación. El patrón Adapter encapsula estas diferencias detrás de una interfaz común:
<?php
declare(strict_types=1);
namespace App\Payments\Contracts;
/**
* Interfaz unificada para todas las pasarelas de pago
* Cualquier adapter debe implementar este contrato
*/
interface PaymentGatewayInterface
{
/**
* Procesa cargo único
* @return PaymentResult Objeto normalizado independiente del proveedor
*/
public function charge(PaymentRequest $request): PaymentResult;
/**
* Reembolso total o parcial
*/
public function refund(string $transactionId, int $amountCents): RefundResult;
/**
* Consulta estado de transacción
*/
public function getTransactionStatus(string $transactionId): TransactionStatus;
/**
* Verifica disponibilidad del gateway (health check)
*/
public function isAvailable(): bool;
}
/**
* DTO normalizado de resultado de pago
* Independiente del formato nativo de cada pasarela
*/
readonly class PaymentResult
{
public function __construct(
public string $transactionId,
public PaymentStatus $status,
public int $amountCents,
public string $currency,
public string $gatewayName,
public ?string $gatewayRawResponse = null,
public ?string $errorCode = null,
public ?string $errorMessage = null
) {}
public function isSuccess(): bool
{
return $this->status === PaymentStatus::APPROVED;
}
}
enum PaymentStatus: string
{
case APPROVED = 'approved';
case DECLINED = 'declined';
case PENDING = 'pending'; // Esperando confirmación asíncrona
case ERROR = 'error'; // Fallo técnico, no rechazo bancario
case CANCELLED = 'cancelled';
}
| Pasarela | Código Éxito Nativo | Código Rechazo Nativo | Normalizado a |
|---|---|---|---|
| Stripe | succeeded | card_declined / insufficient_funds | APPROVED / DECLINED |
| Mercado Pago | approved | rejected / cc_rejected_bad_filled_security_code | APPROVED / DECLINED |
| Kushki | APPROVED | DECLINED / INSUFFICIENT_FUNDS | APPROVED / DECLINED |
| dLocal | PAID | REJECTED / EXPIRED | APPROVED / DECLINED |
3. Implementación de Adapters Específicos
Cada adapter traduce la interfaz unificada al lenguaje nativo de la pasarela. Ejemplo simplificado para Stripe:
<?php
declare(strict_types=1);
namespace App\Payments\Adapters;
use App\Payments\Contracts\{PaymentGatewayInterface, PaymentRequest, PaymentResult, PaymentStatus};
class StripeAdapter implements PaymentGatewayInterface
{
private string $apiKey;
private string $endpoint = 'https://api.stripe.com/v1';
public function __construct(string $apiKey)
{
$this->apiKey = $apiKey;
}
public function charge(PaymentRequest $request): PaymentResult
{
$payload = http_build_query([
'amount' => $request->amountCents,
'currency' => strtolower($request->currency),
'payment_method' => $request->paymentMethodToken,
'confirm' => 'true',
'idempotency_key' => $request->idempotencyKey,
'metadata[merchant_id]' => $request->merchantId,
]);
$response = $this->post('/payment_intents', $payload);
$data = json_decode($response['body'], true);
// Traducción de estados nativos a enum unificado
$statusMap = [
'succeeded' => PaymentStatus::APPROVED,
'requires_action' => PaymentStatus::PENDING,
'canceled' => PaymentStatus::CANCELLED,
];
$status = $statusMap[$data['status']] ?? PaymentStatus::ERROR;
$errorCode = null;
$errorMessage = null;
if ($status === PaymentStatus::DECLINED || $status === PaymentStatus::ERROR) {
$errorCode = $data['last_payment_error']['code'] ?? 'unknown';
$errorMessage = $data['last_payment_error']['message'] ?? 'Unknown error';
}
return new PaymentResult(
transactionId: $data['id'] ?? '',
status: $status,
amountCents: $data['amount'] ?? $request->amountCents,
currency: strtoupper($data['currency'] ?? $request->currency),
gatewayName: 'stripe',
gatewayRawResponse: $response['body'],
errorCode: $errorCode,
errorMessage: $errorMessage
);
}
public function isAvailable(): bool
{
// Health check ligero: verificar conectividad sin procesar pago
try {
$response = $this->get('/account');
return $response['http_code'] === 200;
} catch (\Throwable) {
return false;
}
}
// ... refund() y getTransactionStatus() omitidos por brevedad
private function post(string $path, string $payload): array
{
$ch = curl_init($this->endpoint . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $this->apiKey,
'Content-Type: application/x-www-form-urlencoded'
]
]);
$body = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return ['body' => $body, 'http_code' => $httpCode];
}
}
4. Router Inteligente: Selección Dinámica de Gateway
Tener múltiples adapters es inútil sin un router que decida cuál usar en tiempo real. La selección puede basarse en múltiples factores:
- Geografía del cliente: PIX → dLocal/Ebanx en Brasil; OXXO → Conekta/Stripe en México; PSE → PayU/Mercado Pago en Colombia.
- Método de pago solicitado: Tarjeta → Stripe/Kushki; Transferencia bancaria → gateway local; Wallet → pasarela con mejor integración.
- Disponibilidad en tiempo real: Si health check de gateway primario falla, router redirige automáticamente al secundario.
- Costo por transacción: Para métodos donde múltiples gateways compiten, elegir el de menor fee dinámicamente.
- Tasa de aprobación histórica: ML simple basado en últimos 1000 intentos por bin/card_type/gateway. Elegir el con mayor approval rate.
<?php
declare(strict_types=1);
namespace App\Payments;
use App\Payments\Contracts\{PaymentGatewayInterface, PaymentRequest, PaymentResult};
class SmartPaymentRouter
{
/** @var array<string, PaymentGatewayInterface> */
private array $gateways;
private array $routingRules;
public function __construct(array $gateways, array $routingRules)
{
$this->gateways = $gateways;
$this->routingRules = $routingRules;
}
public function processPayment(PaymentRequest $request): PaymentResult
{
// Obtener lista priorizada de gateways según reglas
$candidates = $this->resolveCandidates($request);
foreach ($candidates as $gatewayName) {
$gateway = $this->gateways[$gatewayName] ?? null;
if (!$gateway || !$gateway->isAvailable()) {
continue; // Saltar gateway no disponible
}
try {
$result = $gateway->charge($request);
// Si fue aprobado o rechazado legítimo (no error técnico), retornar
if ($result->status !== \App\Payments\Contracts\PaymentStatus::ERROR) {
return $result;
}
// Error técnico: intentar siguiente gateway (failover)
error_log(sprintf(
'[PAYMENT_ROUTER] Gateway %s failed with ERROR for merchant %s, trying next candidate',
$gatewayName,
$request->merchantId
));
} catch (\Throwable $e) {
error_log(sprintf(
'[PAYMENT_ROUTER] Exception in %s: %s',
$gatewayName,
$e->getMessage()
));
// Continuar con siguiente candidato
}
}
// Todos los gateways fallaron
return new PaymentResult(
transactionId: '',
status: \App\Payments\Contracts\PaymentStatus::ERROR,
amountCents: $request->amountCents,
currency: $request->currency,
gatewayName: 'none',
errorCode: 'ALL_GATEWAYS_FAILED',
errorMessage: 'No payment gateway available'
);
}
private function resolveCandidates(PaymentRequest $request): array
{
// Lógica de routing basada en país, método, monto, etc.
// Retorna array ordenado por prioridad: ['stripe', 'kushki', 'mercadopago']
// Implementación específica según reglas de negocio
return $this->routingRules[$request->country][$request->paymentMethod]
?? ['stripe']; // Default fallback
}
}
5. Gestión Segura de Credenciales Multi-Gateway
Cada gateway requiere sus propias credenciales (API keys, certificados, webhooks secrets). Almacenarlas en variables de entorno planas o código fuente es inaceptable en producción financiera:
- Vault/Infisical: Secrets cifrados con rotación automática. Aplicación fetch por identidad de servicio, no por credencial estática.
- Encriptación por gateway: Cada conjunto de credenciales cifrado con clave independiente. Compromiso de una no expone las demás.
- Auditoría de acceso: Log inmutable de quién/qué accedió a cada credencial y cuándo. Requisito PCI-DSS y regulaciones LATAM.
- Separación por entorno: Keys de test/sandbox nunca coexisten con producción en mismo almacén. Namespaces o vaults separados obligatorios.
6. Testing de Integraciones Sin Dinero Real
Probar integraciones de pago en staging sin gastar dinero real ni arriesgar datos de producción requiere estrategia deliberada:
- Sandbox oficiales: Todas las pasarelas LATAM ofrecen entornos sandbox con tarjetas de prueba y simuladores de escenarios (aprobado, rechazado, timeout, 3DS requerido).
- Mock adapters para tests unitarios: Implementar
MockPaymentGatewayque retorna resultados configurables sin llamar APIs reales. Tests rápidos y determinísticos. - Contract testing: Validar que tus adapters cumplen el contrato de la interfaz unificada incluso cuando la API del proveedor cambia. Detectar breaking changes antes de deploy.
- Simulador de red: Inyectar latencia, timeouts y errores de red en tests de integración para validar comportamiento del router y circuit breakers sin depender de sandbox externo.
7. Conclusión y Recursos Oficiales
La abstracción multi-gateway transforma la dependencia de un solo proveedor en ventaja competitiva operativa. El patrón Adapter + Smart Router permite failover automático, optimización de costos y expansión geográfica sin reescrituras masivas. En LATAM, donde la fragmentación de métodos de pago es la norma, esta arquitectura no es lujo: es requisito de supervivencia comercial.
Documentación oficial de pasarelas LATAM:
- Stripe Payments Documentation: API reference y guías de integración.
- Mercado Pago Developers: API docs para LATAM.
- Kushki Documentation: Integración para Chile, Colombia, Ecuador, Perú.
- dLocal Developer Docs: Cobertura 30+ mercados emergentes.
Nota técnica: Este artículo utiliza marcado Schema.org Article coexistente con el nodo FinancialService corporativo. Los metadatos semánticos se indexan automáticamente vía The SEO Framework.