Ayman, inicio

Casos / Una butaca, una sola compra

Una butaca, una sola compra.

Un sistema de reservas de cine donde dos personas que eligen la misma butaca a la vez no pueden acabar pagándola las dos.

rol
Proyecto final de ciclo en equipo, ampliado por mi cuenta
año
2025
stack
PHP 8.4Laravel 12MySQLStripeJavaScriptPHPUnit
enlaces
Código ↗Demo ↗
Portada de CineFlow con la película en cartelera y el botón para ver sesiones
// contexto

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.

// problema

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.

// decisiones

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.
Mapa de butacas con estados por forma y texto y la cuenta atrás del bloqueo
Mapa de butacas con estados por forma y texto y la cuenta atrás del bloqueo
Cómo se compra: pago con Stripe en modo de prueba y entrada con código QR
Cómo se compra: pago con Stripe en modo de prueba y entrada con código QR
// resultado

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í

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.