Contexto.
Fue el proyecto final del ciclo, hecho en equipo con Ismael Achamrouk y Danna Guevara. Después lo retomé por mi cuenta para modernizarlo: despliegue, CI, el pago con Stripe, un mapa de butacas accesible y un diseño nuevo.
La confirmación de la compra, con su bloqueo pesimista, sigue como la dejó el equipo. Quería un proyecto donde la concurrencia fuera de verdad, no un listado de películas.
El problema.
Entre que alguien elige una butaca y termina de pagar pasan minutos. Si otra persona la elige en ese tiempo, las dos podrían comprarla.
Lo que decidí, y por qué.
01
Retener la butaca al elegirla
- qué
- Al seleccionar una butaca se crea un bloqueo temporal de 8 minutos ligado a esa persona. El resto la ve ocupada al momento gracias a un sondeo por AJAX.
02
Confirmar con bloqueo pesimista
- qué
- La compra se confirma dentro de una transacción con SELECT … FOR UPDATE (lockForUpdate) y hasta 3 intentos. Dos compras simultáneas se ordenan y la segunda falla de forma limpia.
03
Precio según el tipo de entrada
- qué
- El precio parte del precio base de la sesión y aplica un factor por tipo: adulto, reducida, familiar y mayor de 65. Se calcula en el servidor, en céntimos enteros: el navegador no envía importes.
04
Bloqueos que valen igual para invitados y para cuentas
- qué
- Una butaca bloqueada por un invitado aparecía libre para quien tenía sesión iniciada, y esa persona podía quitársela. Ahora el bloqueo se respeta para las dos.
- por qué
- Un bloqueo que solo vale para parte de las personas no protege la butaca.
05
Que confirme Stripe, no el navegador
- qué
- El pago es Stripe Checkout en modo de pruebas. La reserva no la confirma el navegador al volver: la confirma un webhook firmado. Cada evento se procesa una sola vez, aunque llegue repetido.
- por qué
- Si la reserva dependiera de la vuelta del navegador, cerrar la pestaña dejaría un cobro sin reserva.
06
Butacas retenidas mientras se paga
- qué
- Mientras alguien paga, sus butacas siguen bloqueadas: el bloqueo se amplía hasta que caduca el pago. Si el pago caduca, falla o se cancela, las butacas se liberan al momento.
- por qué
- Sin ampliar el bloqueo, otra persona podría quedarse la butaca durante el pago. Sin liberarla, un pago abandonado dejaría la butaca ocupada.
07
Reembolso si otra compra gana
- qué
- Si otra compra se queda la butaca un instante antes, el pago se reembolsa solo.
- por qué
- La butaca ya está vendida. Devolver el dinero sin que nadie lo pida evita un cobro sin entrada.
08
Un mapa de butacas que se usa con el teclado
- qué
- El mapa es una rejilla que se maneja con flechas, Inicio, Fin y Espacio. Cada butaca dice su estado con forma, icono y texto, no solo con color. Un lector de pantalla anuncia qué butaca eliges y cuántas te faltan, y la cuenta atrás del bloqueo se anuncia a los 5, 2 y 1 minutos y a los 30 segundos.
- por qué
- Si el mapa solo funcionara con ratón o con color, parte de las personas no podría elegir butaca.


Resultado.
- 76 pruebas con PHPUnit (eran 38), ninguna saltada, incluidas las de condiciones de carrera, caducidad de los bloqueos y webhook con firma inválida o evento repetido.
- Dos pruebas de navegador en cada PR: una con axe recorre 13 pantallas a 390 y 1440 px y otra maneja el mapa de butacas con el teclado. Lighthouse móvil da 100 en accesibilidad en portada, cartelera y ficha.
- Entradas con código QR firmado. Sin claves de Stripe, la demo usa un pago simulado.
Qué aprendí.
- Un pago tiene más estados que «pagado» y «no pagado»: abandonado, caducado, repetido y llegado tarde. Los dos fallos que salieron en la revisión final estaban en esos bordes.
- Un webhook puede llegar dos veces. Si no es idempotente, la segunda vez tiene un coste.
- Soltar una butaca un solo instante basta para que otra persona se la quede.
