← VOLVER

Estudio de caso

Plataforma transaccional de reservas y pagos

Pagos • Webhooks • Concurrencia

Imágenes

Galería no disponible.

Resumen

Backend para Patagonia Dreams — operadora de turismo con +180k pasajeros/año y 7.000+ reseñas cinco estrellas en Google. Construí y lideré la plataforma desde cero en 5+ meses de desarrollo activo. Cubre e-commerce B2C y niveles de partner B2B (oro/platino/diamante con precios diferenciados y pago diferido), reservas y pagos transaccionales (Mercado Pago, Stripe), sync bidireccional con un panel externo de actividades (disponibilidad y precios actualizados cada 15 min), un bot de ventas por WhatsApp con disponibilidad y contenido estructurado, integración de reseñas de Google Business, feed de Google Merchant Center, recuperación de carritos abandonados por WhatsApp y un programa de auditoría de seguridad completo (nomenclatura H/M/L, 9+ CVEs parcheados). El invariante central: una reserva solo está 'pagada' cuando el webhook lo confirma — nunca basado en el estado del cliente. Los webhooks se validan con HMAC y se procesan de forma idempotente por event_id. La disponibilidad se bloquea de forma pesimista (SELECT FOR UPDATE) para serializar reservas concurrentes. El precio siempre se recalcula server-side — los montos enviados por el cliente nunca se confían. Auth dual: AWS Cognito (JWKS) para clientes finales, SimpleJWT para el backoffice. Stack: Python 3.12, Django 5.2 LTS, DRF, PostgreSQL, Redis, Docker, GitHub Actions CI/CD; todos los secrets via AWS Secrets Manager.

Arquitectura y diseño

Transactional Flow Overview

Internal System
Internal System

System Invariants

  • ·A payment intent cannot transition from failed to succeeded.
  • ·Reservation "paid" is set only after a verified webhook; frontend and redirect cannot set it.
  • ·Webhook events are processed idempotently by provider event_id.
  • ·Availability for a slot is updated under pessimistic lock (SELECT FOR UPDATE); no optimistic commit.
  • ·Idempotency keys are scoped per client and stored; duplicate key returns original response.
  • ·Payment and reservation state changes for a webhook occur in a single database transaction.

Architecture Decision Records

  • ADR-01Webhooks como única fuente de verdad del estado de pago — el redirect del cliente no puede setear 'pagado'
  • ADR-02Bloqueo pesimista (SELECT FOR UPDATE) en el slot de disponibilidad — las reservas concurrentes se serializan, no compiten
  • ADR-03Claves de idempotencia en creación de reservas; deduplicación por event_id en todos los webhooks entrantes
  • ADR-04Validación HMAC en cada payload de webhook antes de procesarlo
  • ADR-05AWS Cognito como único punto de entrada de identidad; ID token verificado con JWKS antes de confiar en datos del usuario
  • ADR-06Toda la config crítica via AWS Secrets Manager — sin secrets en código ni repo

Escala y restricciones

Volumen de requests
Operadora con +180k pasajeros/año. Reservas en plataforma online + bursts de webhooks hasta ~50/min en pico.
Concurrencia
Lock pesimista en la fila de disponibilidad por slot; único escritor para el estado de pago. Sin locking cruzado entre slots.
Dependencias externas
Mercado Pago, Stripe (pagos); Panel externo de actividades (disponibilidad, tarifas y sync bidireccional de reservas); AWS Cognito, SES, Secrets Manager; Google (OAuth, My Business, Merchant Center); Meta; WhatsApp (WATI). Los webhooks son asíncronos; el estado de pago solo llega por webhook.
Modos de fallo
Timeout o demora del proveedor → la reserva queda pendiente hasta el webhook o reconciliación manual. Webhook duplicado → idempotente por event_id. Cognito/Panel caídos → auth degradada o sync de catálogo interrumpida.
Consistencia de datos
Una transacción DB para reserva + pago en el webhook. Reserva 'pagada' solo tras webhook; el frontend no puede setear pagado. Sync Cognito ↔ Django via get_or_create y verificación de ID token.

Qué se rechazó explícitamente

  • Frontend o redirect callback como fuente de 'pagado'

    Los redirects y el estado del cliente son poco confiables; los reintentos del proveedor y múltiples pestañas permitirían doble aplicación o actualizaciones perdidas.

  • Bloqueo optimista en disponibilidad

    La tasa de conflictos en slots muy demandados generaría muchos reintentos y mala UX; el lock pesimista dio comportamiento predecible al load observado.

  • Microservicios por dominio (pagos, reservas, catálogo)

    El costo operacional y de consistencia (transacciones distribuidas, consistencia eventual) no se justifica al scale actual; se eligió monolito modular con fronteras claras.

  • Export CSV para operaciones

    Riesgo de inyección de fórmulas Excel/CSV; reemplazado por respuesta JSON con datos controlados.

  • Secrets o URLs sensibles en código o repo

    Toda la config crítica (API keys, credenciales de proveedores, URLs internas, etc.) via env desde AWS Secrets Manager.