Módulo 0 · Puesta a punto

Lección 00b — Modelado inicial de TicketFlow

Del negocio a las invariantes y de ahí al esquema: constraints que impiden vender un asiento dos veces.

Publicada
En esta lección
  1. Ejercicio 1 — Implementa el modelo v1
  2. Ejercicio 2 — Demuestra la invariante I1 en la consola
  3. Ejercicio 3 — Consulta de disponibilidad
  4. Ejercicio 4 — Máquina de estado en el modelo
  5. Ejercicio 5 — Diseña lo que la v1 dejó fuera (discusión)
  6. Ejercicio 6 — Reflexión senior (minutos 5)
  7. Entrega

La única forma de aprender a modelar es modelando. Implementa el modelo v1 y luego responde los retos. No mires solutions.md hasta entregar.

Preparación:

bash
cd ~/dev/ticketflow && source .venv/bin/activate

Ejercicio 1 — Implementa el modelo v1

Crea en events/models.py las clases de la lección: TimeStampedModel, User, Event, Seat, Reservation, ReservationItem y Payment (con sus TextChoices).

Registra la app en settings.py (INSTALLED_APPS), y declara en settings.py:

python
AUTH_USER_MODEL = "events.User"

Hazlo antes de la primera migración. Cambiar AUTH_USER_MODEL a mitad de proyecto es una de las migraciones más dolorosas de Django; por eso lo decidimos ahora (Lección 11 adelantada: las decisiones de esquema se toman antes de que existan datos).

Aplica:

bash
python manage.py makemigrations
python manage.py migrate

Entrega: el listado de modelos que te imprime python manage.py makemigrations --dry-run -v2 (o simplemente confirma que migró sin errores y pega el resumen).

Ejercicio 2 — Demuestra la invariante I1 en la consola

Abre python manage.py shell y intenta romper la invariante: dos reservas activas con el mismo asiento.

python
from events.models import *

u1 = User.objects.create(email="ana@example.com")
u2 = User.objects.create(email="beto@example.com")
ev = Event.objects.create(title="Concierto", venue="Sala X", starts_at="2027-03-01T21:00:00Z", state=EventState.PUBLISHED, organizer=u1)
s1 = Seat.objects.create(event=ev, row="A", number=1)
s2 = Seat.objects.create(event=ev, row="A", number=2)

r1 = Reservation.objects.create(user=u1, event=ev, status=ReservationState.PENDING_PAYMENT, expires_at="2026-10-01T12:00:00Z")
ReservationItem.objects.create(reservation=r1, seat=s1, price_at_purchase="50.00")

r2 = Reservation.objects.create(user=u2, event=ev, status=ReservationState.PENDING_PAYMENT, expires_at="2026-10-01T12:00:00Z")
ReservationItem.objects.create(reservation=r2, seat=s1, price_at_purchase="50.00")   # ← ¿debe explotar?
  1. ¿Qué excepción te lanza la última línea y con qué mensaje?
  2. Borra temporalmente las dos UniqueConstraint condicionales del modelo, rehaz migración y repite el experimento. ¿Se crea la segunda reserva? ¿Qué significa eso para el negocio?
  3. Restaura las constraints. Anota en 2 líneas qué acabas de demostrar (esto es la prueba que contarás en una entrevista).

Ejercicio 3 — Consulta de disponibilidad

Escribe (en shell, o en events/services.py si te atreves) una función available_seats(event_id) que devuelva los asientos del evento sin reserva activa ni confirmada.

  1. Escríbela sin usar SQL crudo (pista: subconsulta o exclude sobre relación inversa).
  2. Imprime el número de queries que ejecuta con django.db.connection.queries o django-debug-toolbar... o simplemente con len(connection.queries) tras llamarla dos veces.
  3. ¿Ejecuta 1 query o N? (Guarda tu respuesta: la retomaremos en la Lección 09 con EXPLAIN.)

Ejercicio 4 — Máquina de estado en el modelo

Añade a Reservation los métodos con transiciones legales:

python
def confirm(self, when):
    ...
def cancel(self, when):
    ...

Reglas: solo se confirma una reserva PENDING_PAYMENT con expires_at no vencido y un pago SUCCEEDED; solo se cancela si no está ya cancelada (cancelar algo cancelado debe ser idempotente: no falla, no cambia nada).

  1. Implementa ambos métodos lanzando ValueError (o una excepción de dominio propia) en transiciones ilegales.
  2. En shell: crea una reserva vencida (expires_at en el pasado) e intenta confirm(). ¿Qué ocurre?
  3. Ejecuta cancel() dos veces. ¿La segunda es un no-op silencioso o lanza? Justifica tu elección.

Ejercicio 5 — Diseña lo que la v1 dejó fuera (discusión)

Responde razonando trade-offs (2-3 frases por pregunta, no hace falta código):

  1. El organizador quiere "fila VIP A completa" como producto aparte con precio distinto. ¿Qué cambiarías en el modelo? ¿Merece la pena para v1?
  2. TicketFlow quiere permitir "hasta 4 asientos por usuario por evento". ¿Dónde vives esa regla: constraint de BD, validación de serializer, o método del modelo? ¿Por qué?
  3. Un competidor vende "entradas generales sin asiento asignado" (solo aforo). ¿Reutilizarías Seat o modelarías otro tipo de ticket? ¿Qué rompe cada opción?

Ejercicio 6 — Reflexión senior (minutos 5)

Imagina que en 6 meses TicketFlow tiene 500 eventos y 2M de asientos. Con el modelo v1:

  1. ¿Qué consulta te preocupa más? (pista: la que hace todo el mundo cada segundo)
  2. ¿Qué índice o qué tabla sientes que va a doler primero?
  3. ¿Qué harías hoy —barato de hacer ahora, carísimo después— para prepararte?

Entrega

Pega en el chat tus resultados (código, excepciones y respuestas). Te corrijo uno a uno, y con esto cerramos la 00b y la marcamos dominada. Después: Lección 01 — HTTP a fondo si no la hiciste, o Lección 02 — Redes si ya entregaste la 01.