Pasarela de pagos: SDK del proveedor filtrado al dominio y 27 cobros duplicados
Una aerolínea de bajo costo integró hace 18 meses la pasarela de pagos de un proveedor externo. El contrato cambió tres veces: renombraron campos, agregaron el estado PENDING_REVIEW y convirtieron los códigos de error de numéricos a cadenas. El código de integración está disperso en 23 clases y el paquete de dominio importa directamente tipos del SDK del proveedor (PaymentGatewayClient, TxResponse). Cuando apareció PENDING_REVIEW, 14 clases fallaron en producción y el turno de soporte tardó 5 horas en diagnosticar. Además, el proveedor a veces responde timeout después de haber cobrado; el sistema reintenta 3 veces de forma automática y el mes pasado generó 27 cobros duplicados, con 12 reembolsos manuales.
1. Diseñe la frontera entre el dominio y el proveedor. ¿Qué tipos deben existir en el dominio y qué responsabilidades tiene el adaptador?
Read the worked answer
Problema de fondo. El dominio importa TxResponse y PaymentGatewayClient: eso es dependencia de implementación en la capa de negocio. Cualquier cambio del proveedor obliga a recompilar y reentender el dominio, y explica que 14 clases fallaran a la vez.
Puerto (definido por el dominio, no por el proveedor).
PasarelaPagoscon operaciones del negocio:cobrar(orden, dinero, claveIdempotencia),consultarEstado(referencia),reembolsar(referencia, dinero).- Tipos propios:
EstadoPagocomo enumeración cerrada (PENDIENTE,EN_REVISION,APROBADO,RECHAZADO,DESCONOCIDO),ResultadoCobro,MotivoRechazoclasificado por el negocio (fondos, fraude, tarjeta inválida, desconocido). Nada de códigos crudos del proveedor en el dominio.
Adaptador. Traduce en ambos sentidos: mapea EstadoPago.EN_REVISION ↔ PENDING_REVIEW, convierte cadenas de error a MotivoRechazo, aplica timeouts, reintentos y autenticación, y registra la carga cruda para auditoría. Es la única capa que conoce el SDK.
Regla de dependencia. El paquete del adaptador depende del dominio; el dominio no depende de nada del proveedor. Una prueba de arquitectura debe fallar si aparece un import del SDK fuera del adaptador.
Beneficio medible. El próximo cambio de contrato se absorbe en una clase (o dos) y con una prueba de contrato que se actualiza en un solo lugar, en lugar de una cacería en 23 clases.
2. ¿Cómo rediseñaría el reintento para eliminar los cobros duplicados? Sea específico sobre idempotencia, estados y compensación.
Read the worked answer
Causa raíz. Un timeout no significa «no cobrado»: significa «resultado desconocido». Reintentar a ciegas convierte una respuesta ambigua en un segundo cargo. Tres reintentos sobre una operación no idempotente multiplican el daño.
Diseño.
- Clave de idempotencia generada por el cliente (UUID por intento de pago, persistido antes de la primera llamada y reutilizado en todos los reintentos). Si el proveedor la soporta, él deduplica; si no, el sistema la usa para reconciliar.
- Máquina de estados explícita del pago:
INICIADO → ENVIADO → APROBADO | RECHAZADO | EN_REVISION | INDETERMINADO. La transiciónENVIADO → INDETERMINADOse dispara por timeout, nunca por error del proveedor. - Reintento con verificación previa: ante
INDETERMINADO, la acción no es cobrar otra vez sinoconsultarEstado(referencia)con espera exponencial acotada. Solo se reenvía el cobro si la consulta confirma que no existe la transacción y la ventana de ambigüedad expiró. - Conciliación: job periódico que cruza pagos
INDETERMINADOyEN_REVISIONcontra el reporte del proveedor. - Compensación: si se detecta un duplicado confirmado, reembolso automático ligado al mismo identificador de orden, idempotente también, y evento de dominio
PagoDuplicadoCompensadopara auditoría.
Regla operativa. Ningún reintento automático sobre operaciones de dinero sin clave de idempotencia y sin consulta de estado previa.
3. ¿Cómo probaría esta integración sin depender del proveedor real y sin volver a llevarse sorpresas cuando cambie el contrato?
Read the worked answer
Tres niveles, con propósitos distintos.
- Dominio con doble de prueba. Un
PasarelaPagosFalsaen memoria implementa el puerto y permite probar todos los caminos, incluidosEN_REVISION,INDETERMINADOy reembolsos, sin red ni latencia. Aquí viven las pruebas de la máquina de estados y de la lógica de reintento. - Adaptador con pruebas de contrato. Se graban respuestas reales del proveedor (anonimizadas) y se convierten en fixtures versionados: JSON de éxito, de rechazo por fondos, de
PENDING_REVIEW, de timeout y de error con código en cadena. Las pruebas del adaptador verifican el mapeo SDK → dominio. Estas pruebas son las que detectan un cambio de contrato. - Pruebas de integración en sandbox, ejecutadas en el pipeline nocturno y no en cada compilación, para validar credenciales y comportamiento real sin acoplar la velocidad del equipo al proveedor.
Anticipar el próximo cambio. Diseñar el adaptador para tolerar campos nuevos (ignorar desconocidos) y estados no reconocidos: un estado desconocido no debe lanzar excepción en el dominio sino mapear a DESCONOCIDO y activar conciliación. Ese único detalle habría evitado 14 clases fallando y 5 horas de diagnóstico.
Monitoreo. Alerta cuando aparezca un código de error o estado no mapeado, con la carga cruda registrada para analizarlo el mismo día.