Cómo aplicar SRP en un procesador de pagos con Stripe

Clase 4 de 27Curso de Patrones de Diseño y SOLID en Python

Resumen

Aplicar el principio de responsabilidad única (SRP) en Python con Stripe no solo ordena el código: también hace el flujo de cobro más claro, testeable y mantenible. Aquí verás cómo se pasó de un PaymentProcessor monolítico a un diseño con validadores, servicio orquestador y componentes dedicados para notificación y registro, usando data class, type hints y manejo de excepciones.

¿Qué problema resuelve el principio de responsabilidad única en este procesador de pagos?

Separar responsabilidades evita que una clase haga de todo: validar, cobrar, notificar y loguear. El punto de partida fue un procesador rudimentario que incumplía SOLID. Se reorganizó con clases específicas: CustomerValidation, PaymentDataValidator, Notifier, TransactionLogger y un StripePaymentProcessor, coordinadas por PaymentService.

  • Importaciones y entorno al inicio del archivo: organización estándar de la industria.
  • PaymentProcessor como data class: instanciación simple sin definir init manual.
  • processTransaction con type hints: retorna una instancia de Stripe Charge.
  • Validaciones con excepciones: ValueError en lugar de objetos vacíos.
  • Corrección de flujo: si no hay email ni teléfono, igual se devuelve el charge exitoso.

¿Cómo estaba el código antes y qué ajustes iniciales se hicieron?

  • Importaciones y variables de entorno (por ejemplo, stripe_api_key) movidas al inicio.
  • PaymentProcessor convertido en data class para simplificar instanciación.
  • Firma de processTransaction conservada, ahora con retorno tipado a Charge.
  • Validaciones reemplazadas por raise ValueError con mensajes claros.
  • Bug corregido: ausencia de email y teléfono ya no corta el flujo; se devuelve el cargo igualmente.

¿Cuáles responsabilidades se identifican y por qué separarlas?

  • Validación de customer: nombre y contact info correctos.
  • Validación de pago: existencia de source en payment_data.
  • Procesamiento del pago: interacción con la librería de Stripe.
  • Notificación: por email o SMS, comportamiento moqueado.
  • Registro en logs: persistir contexto del cargo.

Separarlas permite que cada clase cambie por razones únicas, cumpliendo SRP.

¿Qué clases nuevas encapsulan cada responsabilidad?

  • CustomerValidation con validate(customer_data).
  • PaymentDataValidator con validate(payment_data).
  • Notifier con sendConfirmation(customer_data) para email o teléfono.
  • TransactionLogger con log(customer_data, payment_data, charge).
  • StripePaymentProcessor (renombrado para ser semántico) con processTransaction(...).
  • PaymentService como orquestador.

Ejemplo de estructura mínima:

from dataclasses import dataclass

class CustomerValidation:
    def validate(self, customer_data: dict) -> None:
        if not customer_data.get("name"):
            raise ValueError("missing name")
        if not customer_data.get("contact_info"):
            raise ValueError("missing contact info")

class PaymentDataValidator:
    def validate(self, payment_data: dict) -> None:
        if "source" not in payment_data:
            raise ValueError("missing source")

class Notifier:
    def sendConfirmation(self, customer_data: dict) -> None:
        if customer_data.get("email"):
            print("enviar email moqueado.")
        elif customer_data.get("phone"):
            print("enviar sms moqueado.")

class TransactionLogger:
    def log(self, customer_data: dict, payment_data: dict, charge):
        print(f"pagó {payment_data['amount']} la persona {customer_data['name']}")

¿Cómo coordina PaymentService el flujo con validadores, processor y notificaciones?

PaymentService concentra el flujo: valida datos, procesa con StripePaymentProcessor, notifica con Notifier y registra con TransactionLogger. Devuelve el Charge de Stripe y propaga stripe_error si algo falla. Se usa try/except para manejar ValueError ya emitidos por los validadores.

@dataclass
class PaymentService:
    customer_validator: CustomerValidation
    payment_validator: PaymentDataValidator
    stripe_payment_processor
    notifier: Notifier
    transaction_logger: TransactionLogger

    def processTransaction(self, customer_data: dict, payment_data: dict) -> "Charge":
        try:
            self.customer_validator.validate(customer_data)
        except ValueError as e:
            raise e

        try:
            self.payment_validator.validate(payment_data)
        except ValueError as e:
            raise e

        try:
            charge = self.stripe_payment_processor.processTransaction(customer_data, payment_data)
        except stripe_error as e:
            raise e

        self.notifier.sendConfirmation(customer_data)
        self.transaction_logger.log(customer_data, payment_data, charge)
        return charge

¿Qué cambio clave evita errores en las validaciones?

En lugar de if esperando booleanos, los validadores ya hacen raise ValueError. Por eso, en PaymentService se usa try/except y se re-lanza la misma excepción. Esto elimina falsos positivos de validación y simplifica el control de flujo.

try:
    customer_validator.validate(customer_data)
except ValueError as e:
    raise e

¿Qué se corrige en notificaciones y retorno del cargo?

  • Si no hay email ni phone, el flujo continúa y se retorna el charge exitoso.
  • La notificación es moqueada: impresión por consola según disponibilidad de contacto.
  • El logger persiste contexto: monto (p. ej., 500 o 700) y nombre (p. ej., John Doe).

¿Cómo se maneja un fallo de Stripe con tokens de prueba?

  • Se reproducen fallos con token de riesgo como “radar block”: la tarjeta es declinada por defecto.
  • El StripePaymentProcessor levanta la excepción capturada; PaymentService la propaga.
  • En el código cliente, se puede envolver la llamada en try/except y reportar: “falló el procesamiento: {error}”.
try:
    charge = payment_service.processTransaction(customer_data, payment_data)
except Exception as e:
    print(f"falló el procesamiento: {e}")

¿Qué habilidades y conceptos aplicas al refactor bajo SOLID?

Refactorizar con SRP exige precisión y criterio. Aquí se aplican prácticas de alto impacto para código de pagos en producción.

  • SRP (principio de responsabilidad única): cada clase cambia por una razón única.
  • Orquestación con PaymentService: coordina validación, cobro, notificación y logging.
  • Renombramiento semántico: StripePaymentProcessor refleja su dominio real.
  • Python data class: instanciación clara, sin __init__ explícito.
  • Python type hints: processTransaction -> Charge mejora legibilidad y contratos.
  • Manejo de excepciones: ValueError en validaciones; propagación de stripe_error.
  • Mocks funcionales: envío de email/SMS como impresión controlada.
  • Corrección de flujo: retorno del charge aunque no haya medios de contacto.
  • Depuración con debugger y breakpoints: verificación de estados intermedios.
  • Pruebas de error con tokens de Stripe: “radar block” simula tarjeta declinada.

¿Tú cómo separarías aún más las responsabilidades o qué cambios harías para reforzar SRP en este flujo de pagos? Comparte tus ideas y mejoras en los comentarios.