<!-- https://docs.portafolio-renzoramos.com/booqi/ -->
# Booqi · Documentación · Renzo Ramos

> Booqi, de Renzo Ramos: reserva de entradas para eventos con microservicios Spring Boot, MySQL y React. Documentación completa de la app y su API.

Fuente: https://docs.portafolio-renzoramos.com/booqi/
Autor: Renzo Ramos (https://portafolio-renzoramos.com/)

# Booqi, de punta a punta

Documentación de Booqi, un proyecto de [Renzo Ramos](https://portafolio-renzoramos.com/).

Todo lo que hace la aplicación y cómo lo hace: cada pantalla para quien compra y para quien administra, los cuatro microservicios y su API, el recorrido de una compra entre servicios, la base de datos, el frontend, la infraestructura, las pruebas y las limitaciones conocidas.

- Java 17
- Spring Boot 4.0.3
- React 19
- MySQL 8.0
- Licencia MIT

## Qué es Booqi

Booqi es un sistema de gestión y reserva de entradas para eventos construido con microservicios. Permite explorar eventos, reservarlos, pagarlos con un pago simulado y descargar las entradas en PDF con su código QR. Es un trabajo de fin de grado de código abierto.

- Para quien compra Catálogo con filtros, detalle del evento, carrito, pago, entradas en PDF, historial de reservas y perfil.
- Para quien administra Gestión de usuarios y eventos, listado de ventas con filtros y un informe de ventas en PDF.
- Cuatro microservicios Usuarios, eventos, reservas y pagos en Spring Boot, cada uno con su propio esquema de MySQL.
- Un solo comando Todo el stack (seis contenedores) se levanta con Docker Compose o Podman.

### Enlaces

- Código: [github.com/RenzoRamosDEV/Booqui-TFG](https://github.com/RenzoRamosDEV/Booqui-TFG)
- Demo sin backend: [renzoramosdev.github.io/Booqui-TFG](https://renzoramosdev.github.io/Booqui-TFG/)
- Repositorio original del grupo: [gitlab.com/booqui](https://gitlab.com/booqui)

## Probar la demo

La [demo publicada](https://renzoramosdev.github.io/Booqui-TFG/) es la misma aplicación React, pero sin servidor: un backend simulado en el navegador responde a todas las llamadas a la API con 250 ms de latencia. Funciona igual que la real: errores con el mismo formato, aprobación del pago por el último dígito de la tarjeta, control de stock y administración completa.

| Rol | Email | Contraseña |
| --- | --- | --- |
| Cliente | `cliente@booqi.demo` | `demo1234` |
| Administrador | `admin@booqi.demo` | `demo1234` |

- La pantalla de inicio de sesión de la demo tiene los botones «Entrar como cliente demo» y «Entrar como admin demo».
- Incluye 10 eventos en ciudades de España con fechas relativas al momento en que abres la demo, y algunas ventas previas para el panel de administración.
- Lo que hagas se guarda en `sessionStorage`: sobrevive a una recarga y desaparece al cerrar la pestaña.
- Una etiqueta fija «Demo · datos simulados» recuerda que nada es real.
- Usa rutas con `#` porque GitHub Pages no reenvía las rutas de una SPA a `index.html`.

Truco para probar pagos

Una tarjeta cuyo último dígito es par se aprueba; si es impar, se rechaza. PayPal siempre se aprueba.

## Ejecutar en local

Solo necesitas git, Docker o Podman con Compose v2, y `make`.

```
git clone https://github.com/RenzoRamosDEV/Booqui-TFG.git
cd Booqui-TFG
cp infra/.env.example infra/.env   # cambia las contraseñas de ejemplo
make up                            # construye y levanta el stack
```

La primera vez construye las imágenes; el stack tarda unos 2–3 minutos en estar sano (unos 195 s medidos en frío). Después abre [http://localhost](http://localhost).

| Componente | Dirección |
| --- | --- |
| Frontend | `http://localhost` |
| user-service | `http://localhost:8080` |
| event-service | `http://localhost:8081` |
| booking-service | `http://localhost:8082` |
| payment-service | `http://localhost:8083` |
| MySQL | `localhost:3307` |

Cada servicio publica su documentación en `/swagger-ui.html`, el contrato en `/v3/api-docs` y su estado en `/actuator/health`.

### Usuarios de prueba

| Rol | Email | Contraseña |
| --- | --- | --- |
| ADMIN | `mel@booqi.com` | `admin_mel` |
| CUSTOMER | `andres.g@email.com` | `cust_pass_1` |

Podman rootless o puerto 80 ocupado

Si tu sistema no deja publicar el puerto 80 sin privilegios, pon `BOOQI_FRONTEND_PORT=8088` en `infra/.env` y abre `http://localhost:8088`.

Para pararlo: `make down`. Para borrarlo todo, también los datos: `make clean`.

## Navegación y estructura

La barra superior muestra enlaces distintos según quién esté usando la app:

| Quién | Enlaces |
| --- | --- |
| Cualquiera | Inicio, Eventos, Contacto |
| Con sesión | Lo anterior + Mis reservas y Perfil |
| Administrador | Lo anterior + Administración |

### Rutas

| Ruta | Pantalla | Acceso |
| --- | --- | --- |
| `/` | Inicio | Libre |
| `/events` | Catálogo de eventos | Libre |
| `/events/:id` | Detalle de un evento | Libre |
| `/cart` | Carrito | Libre |
| `/checkout` | Pago | Sesión |
| `/my-bookings` | Mis reservas | Sesión |
| `/profile` | Mi perfil | Sesión |
| `/admin` | Administración | ADMIN |
| `/login`, `/register` | Acceso y registro | Libre |
| `/contact` | Contacto | Libre |
| `*` | Página no encontrada | Libre |

- Cada página se descarga al abrirla por primera vez y entra con una transición suave.
- Sin conexión aparece una pantalla «Sin conexión» con un botón «Reintentar».
- Si una página falla, aparece «Algo ha salido mal» con un botón para recargar.
- Hay un enlace «Saltar al contenido» para teclado y lectores de pantalla.

## Inicio y catálogo

![Catálogo de eventos con filtros por texto, categoría, ciudad, fechas y precio máximo](https://docs.portafolio-renzoramos.com/booqi/assets/screenshots/eventos-light-1280.webp)
*Catálogo de eventos con sus filtros.*

### Inicio

- Un hero con el titular «Tus eventos favoritos, sin colas» y un buscador de artista, evento u organizador.
- «Explora por categoría», con accesos directos a cada categoría.
- **3 destacados:** los próximos eventos que aún tienen entradas, por fecha.
- **6 próximos:** los siguientes eventos futuros.
- Si no hay eventos futuros: «Todavía no hay eventos próximos».

### Catálogo

El catálogo se descarga una sola vez y se filtra y pagina en el navegador. Todos los filtros se guardan en la URL, así que un listado filtrado se puede compartir o recargar.

| Filtro | Parámetro | Cómo filtra |
| --- | --- | --- |
| Buscar | `q` | Texto en título, descripción u organizador, sin tildes ni mayúsculas |
| Categoría | `category` | Música, Arte, Deporte, Gastronomía, Tecnología, Educación u Otros |
| Ciudad | `city` | Texto contenido en la ubicación |
| Desde / Hasta | `from`, `to` | Días del calendario |
| Precio máximo | `maxPrice` | Euros |
| Página | `page` | 12 eventos por página, ordenados por fecha |

Los filtros se combinan entre sí. La paginación muestra como mucho 7 botones, con puntos suspensivos en medio.

## Detalle de un evento

![Detalle de un evento en tema oscuro con precio, selector de cantidad y botón para añadir al carrito](https://docs.portafolio-renzoramos.com/booqi/assets/screenshots/evento-dark-1280.webp)
*Detalle de un evento en tema oscuro.*

- Muestra categoría, inicio, fin, organizador y lugar; el lugar abre Google Maps en otra pestaña.
- El precio por entrada, o «Gratis» si vale 0.
- Un panel «Comprar entradas» con selector de cantidad y el aviso «Máximo 10 entradas por evento».
- Si el evento no existe: «Evento no encontrado».

| Entradas disponibles | Etiqueta |
| --- | --- |
| 0 | Agotado |
| 1 | Última entrada |
| 2 a 10 | Últimas N entradas |
| Más de 10 | N entradas disponibles |

Al añadir al carrito sin haber iniciado sesión, la app te lleva al inicio de sesión y después te devuelve al evento. Con sesión, aparece el aviso «Añadido al carrito».

## Carrito y pago

### Carrito

- Una línea por evento. La cantidad va de 1 al mínimo entre las entradas disponibles y 10.
- Los importes se calculan en céntimos y se muestran en euros, sin comisiones.
- El carrito vive en memoria: se vacía al recargar la página o al cerrar sesión.
- Vacío: «Tu carrito está vacío».

### Pago

El pago tiene tres pasos: **Revisión**, **Pago** y **Confirmación**.

| Método | Datos que pide |
| --- | --- |
| Tarjeta de crédito | Titular, número, caducidad y CVV |
| Tarjeta de débito | Titular, número, caducidad y CVV |
| PayPal | Nada: no envía tarjeta |

Validación de la tarjeta antes de enviar:

- Titular obligatorio.
- Número de 13 a 19 dígitos (los espacios se ignoran).
- Caducidad en formato `MM/AA` y no vencida.
- CVV de 3 o 4 dígitos.

Al pagar, la app procesa cada línea del carrito **por orden y de una en una**: crea la reserva y, si sale bien, la paga. Un fallo en una línea no detiene las demás. Solo una línea aprobada cuenta como pagada: las aprobadas salen del carrito y las rechazadas se quedan con un aviso, para reintentarlas.

- Si todo se aprueba: «¡Pago completado!», con un check animado y confeti.
- Si no: «Resultado del pago», con el resultado de cada línea, su código de reserva, el QR y la descarga de entradas.
- No se envía ningún correo: las entradas se descargan desde la app.

## Entradas en PDF con QR

- Se generan en el propio navegador, sin pasar por el servidor.
- Una página por entrada, numerada `n / N`, en A5 apaisado, con la banda de color de la marca, el logo y el QR.
- El QR codifica el número de la reserva.
- Las reservas canceladas no generan entradas.
- El archivo se llama `booqi--.pdf`.
- Las librerías de PDF y QR solo se descargan cuando hacen falta, para no pesar en el resto de la app.

## Mis reservas

- Agrupa tus reservas por evento, del más reciente al más antiguo.
- Cada grupo muestra el total de entradas y el importe, sin contar las canceladas.
- Un panel lateral muestra el detalle de cada reserva.
- Se pueden descargar las entradas de nuevo en cualquier momento.

| Insignia del grupo | Cuándo |
| --- | --- |
| Confirmada | Todas sus reservas están confirmadas |
| Cancelada | Todas sus reservas están canceladas |
| Pendiente | Cualquier otro caso |

### Cancelar

Un diálogo pide confirmación: «¿Cancelar la reserva?». La cancelación no se puede deshacer y **no devuelve el importe pagado**; tampoco devuelve las entradas al stock del evento.

## Cuenta, perfil y contacto

### Registro

- Nombre y apellidos: de 2 a 50 caracteres.
- Fecha de nacimiento en el pasado.
- Teléfono opcional, de 9 a 15 dígitos con `+` opcional.
- Email válido y único.
- Contraseña de al menos 8 caracteres, repetida para confirmar.
- Toda cuenta nueva es de cliente. Los errores del servidor aparecen bajo su campo.

### Inicio de sesión

- Email y contraseña. Si fallan: «Email o contraseña incorrectos» (no se dice cuál de los dos).
- Después vuelve a la página de la que venías; si no venías de ninguna, un administrador va a Administración y un cliente al inicio.

### Mi perfil

- Edita nombre, apellidos, fecha de nacimiento, teléfono y email.
- Contraseña nueva opcional (al menos 8 caracteres, repetida).
- Solo se envían los campos cambiados; sin cambios, avisa «No hay cambios que guardar».
- Si el email ya pertenece a otra cuenta: «Este email ya está registrado».

### Contacto

Un formulario de nombre, email, asunto y mensaje (hasta 2000 caracteres). Es una simulación, marcada como «Demo»: muestra «¡Mensaje recibido!» pero no envía nada, porque el backend no tiene servicio de contacto.

## Panel de administración

![Panel de administración con métricas y la tabla de usuarios](https://docs.portafolio-renzoramos.com/booqi/assets/screenshots/admin-light-1280.webp)
*Panel de administración: métricas y usuarios.*

Solo para administradores. Su código ni siquiera se descarga hasta que la app comprueba que tienes ese rol.

### Métricas

Cuatro tarjetas: número de administradores, número de clientes, precio medio de los eventos y total de ventas aprobadas.

### Usuarios

- Listado de todos los usuarios con su rol.
- Borrar cualquier usuario excepto a ti mismo, con confirmación.
- No se crean ni editan usuarios desde aquí.

### Eventos

Crear, editar y borrar eventos. El formulario valida:

| Campo | Regla |
| --- | --- |
| Título | 3 a 200 caracteres |
| Descripción | Hasta 2000 caracteres |
| Entradas | Número entero, 0 o más |
| Precio | 0 o más |
| Ubicación | 3 a 200 caracteres |
| Inicio | En el futuro (solo al crear) |
| Fin | No anterior al inicio |
| Organizador | 3 a 100 caracteres |
| Categoría | Una de la lista |
| Email de contacto | Válido y único entre eventos |
| Teléfono | 9 a 15 dígitos |
| Imagen | URL opcional |

Al editar solo se envían los cambios. Si hay un conflicto (por ejemplo, un email repetido), el error aparece en el propio diálogo, que no se cierra.

### Ventas

- Cruza los pagos con sus reservas para mostrar fecha, cliente, evento, entradas, importe, método y estados.
- Filtros por estado del pago (todos, aprobados, rechazados, pendientes) y por evento.
- Totales aprobado, rechazado y pendiente.

### Informe de ventas en PDF

- De un evento o de la selección filtrada, titulado «Informe de ventas · …».
- Columnas: Fecha, Cliente, Evento, Entradas, Importe, Método, Pago y Reserva, con los totales al final.
- Pie con la fecha de generación y quién lo exportó.
- Se puede previsualizar antes de descargar. Archivo: `booqi-informe--.pdf`.

## Arquitectura

Seis contenedores: un MySQL, cuatro microservicios Spring Boot y un nginx que sirve el frontend y hace de proxy de la API.

- El navegador siempre habla con un único origen: nginx reenvía `/api/v1/users`, `/events`, `/bookings` y `/payments` a su servicio. En desarrollo, Vite hace lo mismo.
- **Una base de datos por servicio:** cada servicio es dueño de su esquema y nadie más lo toca.
- Los servicios se llaman entre sí por HTTP: reservas consulta a usuarios y eventos; pagos descuenta stock en eventos y confirma o cancela en reservas.
- Todos siguen la misma estructura de capas: `controller` → `service` → `repository`, con DTO como records y MapStruct para convertir entidades.
- Monorepo: `services/`, `frontend/`, `infra/` y `docs/`, con un `Makefile` en la raíz.

### Común a los cuatro servicios

- Java 17 y Spring Boot 4.0.3, paquete `com.booqi.`.
- Hibernate solo valida el esquema (`ddl-auto: validate`); nunca lo modifica.
- Errores en formato estándar RFC 9457 mediante un manejador global.
- Actuator con `health` e `info`, y OpenAPI con Swagger UI.
- CORS configurable con `booqi.cors.allowed-origins`.

## Stack tecnológico

Todas las herramientas del proyecto, agrupadas por capa. Las versiones son las del repositorio.

### Backend

- Java 17
- Spring Boot 4.0.3
- Spring Web (MVC)
- Spring Data JPA · Hibernate
- Spring Validation
- Spring Boot Actuator
- Spring Security Crypto (BCrypt)
- MapStruct 1.6.3
- Lombok
- springdoc-openapi 3.0.3
- Swagger UI
- MySQL Connector/J
- H2 tests
- Maven 3.9.12
- RFC 9457 Problem Details

### Frontend

- React 19.3.1
- TypeScript 6.0.3
- Vite 8.3.5
- Tailwind CSS 4.3.3
- React Router 7.18.4
- TanStack Query 5.104.5
- Axios 1.20.0
- React Hook Form 7.91.0
- Zod 4.6.7
- Zustand 5.0.16
- Motion 14.6.0
- Lucide 1.61.0
- pdf-lib 1.17.1
- qrcode 1.5.4
- Inter · Space Grotesk

### Infraestructura

- Docker Compose v2
- Podman (rootless)
- nginx alpine (proxy /api/v1)
- MySQL 8.0
- eclipse-temurin 17 (JRE alpine)
- maven:3.9 (build)
- node:22-alpine
- Makefile
- GitHub Pages (demo)

### Pruebas y calidad

- JUnit 5
- Mockito
- @WebMvcTest · @DataJpaTest
- PIT 1.17.0
- Checkstyle 12.3.1
- Spotless
- google-java-format 1.35.0
- Vitest 5.0
- Testing Library (dom · react · user-event · jest-dom)
- jsdom 30
- Playwright 1.64
- Stryker 10.1.1
- ESLint 10
- typescript-eslint · react-hooks · react-refresh
- Prettier 3.9.10
- node --test

### CI y método

- GitHub Actions (backend · frontend · compose · Pages)
- OpenSpec (19 capacidades)
- ADRs (MADR)
- Conventional Commits
- TDD

## Microservicios y API

Toda la API cuelga de `/api/v1`. Crear devuelve `201` con la cabecera `Location`. Los listados aceptan filtros por query string, pero solo uno a la vez (en usuarios, nombre y apellido sí se combinan); los filtros vacíos se ignoran.

### user-service · puerto 8080

| Método | Ruta | Qué hace |
| --- | --- | --- |
| POST | `/users` | Crea un usuario (CUSTOMER si no se indica rol) |
| GET | `/users` | Lista; filtros `contactEmail`, `contactPhone`, `role`, `firstName`, `lastName` |
| GET | `/users/{id}` | Obtiene un usuario |
| PUT | `/users/{id}` | Actualización parcial; si cambia la contraseña, la vuelve a cifrar |
| DELETE | `/users/{id}` | Borra |
| POST | `/users/login` | Inicia sesión con `contactEmail` y `password` |

El filtro por rol no distingue mayúsculas (un rol desconocido devuelve una lista vacía) y los de nombre tampoco.

### event-service · puerto 8081

| Método | Ruta | Qué hace |
| --- | --- | --- |
| POST | `/events` | Crea un evento (409 si el email ya existe; 422 si el fin es anterior al inicio) |
| GET | `/events` | Lista; filtros `title`, `location`, `organizer` (contiene), `category` (igual) o `contactEmail` (exacto) |
| GET | `/events/{id}` | Obtiene un evento |
| PUT | `/events/{id}` | Actualización parcial, con las mismas comprobaciones de fechas y email |
| DELETE | `/events/{id}` | Borra |
| PATCH | `/events/{id}/stock?quantity=n` | Descuenta stock; 422 si la cantidad no es positiva o no hay bastantes entradas |

### booking-service · puerto 8082

| Método | Ruta | Qué hace |
| --- | --- | --- |
| POST | `/bookings` | Crea una reserva pendiente con `userId`, `eventId` y `ticketQuantity` (1 o más) |
| GET | `/bookings` | Lista; filtros `userId`, `eventId` o `status` |
| GET | `/bookings/{id}` | Obtiene una reserva |
| PATCH | `/bookings/{id}/confirm` | La confirma (si ya lo estaba, la devuelve igual) |
| PATCH | `/bookings/{id}/cancel` | La cancela (si ya lo estaba, la devuelve igual) |

Al crear una reserva consulta el usuario y el evento y guarda una copia de sus datos (nombre, email, título, descripción, fecha y lugar), para que la reserva conserve cómo eran en ese momento. El precio total es el precio del evento por la cantidad. Las llamadas a otros servicios tienen 2 s de tiempo de conexión y 5 s de lectura, sin reintentos.

### payment-service · puerto 8083

| Método | Ruta | Qué hace |
| --- | --- | --- |
| POST | `/payments` | Procesa un pago simulado; responde 201 aunque se rechace |
| GET | `/payments` | Lista; filtros `userId`, `eventId`, `bookingId` o `status` |
| GET | `/payments/{id}` | Obtiene un pago |

Un pago lleva `bookingId`, `userId`, `eventId`, `ticketQuantity`, `totalPrice`, el método (`CREDIT_CARD`, `DEBIT_CARD`, `PAYPAL` o `BANK_TRANSFER`, este último sin uso en la interfaz) y, opcionalmente, tarjeta y CVV.

### Errores al llamar a otro servicio

| Respuesta del otro servicio | Lo que devuelve el que llama |
| --- | --- |
| 5xx | 503 «X service is unavailable» |
| 404 | 404 con el mismo mensaje |
| Otro 4xx | 422 con el mismo mensaje |
| Sin respuesta o tiempo agotado | 503 |

## Flujo de una compra

Lo que pasa, servicio a servicio, por cada línea del carrito:

1. Reserva El frontend llama a `POST /bookings`. booking-service consulta a user-service y a event-service, guarda la reserva como **PENDING** y responde 201.
2. Pago El frontend llama a `POST /payments` con el número de reserva y el total. payment-service guarda el pago como **PENDING** y simula medio segundo de procesamiento.
3. Decisión simulada Se aprueba si no hay tarjeta (PayPal) o si el **último dígito de la tarjeta es par**; si es impar, se rechaza.
4. Aprobado payment-service descuenta el stock en event-service y confirma la reserva en booking-service. El pago queda **APPROVED** con un identificador de transacción y su fecha.
5. Rechazado o con error El pago queda **REJECTED** con el motivo («Payment rejected by simulation», falta de stock, un servicio caído…) y la reserva se cancela.
6. Resultado El servicio responde siempre 201 con el pago. El frontend quita del carrito las líneas aprobadas y recarga eventos y reservas.

| Estado | Reserva | Pago |
| --- | --- | --- |
| Inicial | PENDING | PENDING |
| Aprobado | CONFIRMED | APPROVED |
| Rechazado | CANCELLED | REJECTED |
| Cancelada por el usuario | CANCELLED | Sin cambios |

## Errores y validación

Todos los errores siguen el estándar RFC 9457 (`application/problem+json`) con `type`, `title`, `status`, `detail` e `instance` (la ruta pedida). Los errores de validación añaden una lista `errors` con el campo y el mensaje.

```
{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "Validation failed",
  "instance": "/api/v1/users",
  "errors": [{ "field": "contactEmail", "message": "…" }]
}
```

| Código | Cuándo |
| --- | --- |
| 400 | Validación, varios filtros a la vez o un parámetro de tipo incorrecto |
| 401 | Email o contraseña incorrectos al iniciar sesión |
| 404 | El recurso no existe |
| 409 | Conflicto, como un email repetido |
| 422 | Regla de negocio: fechas incoherentes, stock insuficiente… |
| 503 | Otro servicio no responde |
| 500 | Error inesperado |

Los mensajes de validación están en español; los de negocio, en inglés. En el frontend, cada error se convierte en un mensaje para el usuario: primero el del servidor y, si no hay, uno por defecto en español según el código (por ejemplo, para 503: «El servicio no está disponible…»).

## Base de datos

Un único MySQL 8.0 con cuatro esquemas, uno por servicio: `user`, `event`, `booking` y `payment`. La aplicación usa un usuario de base de datos con permisos solo sobre esos cuatro esquemas. No hay migraciones: el esquema se crea con un script al inicializar el contenedor.

- **users**: Nombre, apellidos, fecha de nacimiento, teléfono, email (único), contraseña cifrada con BCrypt, rol (ADMIN o CUSTOMER) y fechas de creación y actualización.
- **events**: Título, descripción, entradas disponibles, ubicación, inicio, fin, organizador, precio, categoría, email (único), teléfono e imagen.
- **bookings**: Usuario y evento (con la copia de sus datos), cantidad, precio base, precio total y estado.
- **payments**: Reserva, usuario, evento, cantidad, total, método, estado, identificador de transacción, mensaje de error y fecha de pago.

### Datos iniciales

- **17 usuarios:** 2 administradores y 15 clientes.
- **36 eventos** en Madrid y alrededores, de música, deporte, gastronomía, cultura, salud, educación, tecnología y arte.
- Sin reservas ni pagos: se crean al usarla.

## Sesión y roles

- Las contraseñas se guardan cifradas con BCrypt (coste 10). Los registros nunca imprimen contraseñas, tarjetas ni CVV: se muestran como `***`.
- El inicio de sesión devuelve los datos del usuario sin la contraseña. No hay tokens ni cookies.
- El frontend guarda la sesión en el navegador, la valida al cargar (si está corrupta la descarta) y no caduca.
- Hay dos roles: **CUSTOMER** y **ADMIN**.
- Las rutas protegidas te mandan a iniciar sesión y te devuelven después. Si no tienes el rol necesario, ves «Página no encontrada» y el contenido protegido ni se carga.
- Las redirecciones tras iniciar sesión solo aceptan rutas internas, nunca otra web.

Ojo: es un proyecto académico

Los permisos se aplican en la interfaz, pero el backend no comprueba quién llama. Está documentado en las [limitaciones conocidas](https://docs.portafolio-renzoramos.com/booqi/#limitaciones); los tokens JWT quedan como trabajo futuro.

## Frontend por dentro

### Estructura

- **`src/app`**: Router, proveedores, layout, guards y páginas genéricas.
- **`src/features`**: Una carpeta por dominio (admin, auth, bookings, cart, checkout, contact, events, tickets), cada una con `api`, `components`, `hooks` y `pages`.
- **`src/components/ui`**: Componentes base: botones, campos, tarjetas, insignias, esqueletos, pestañas, paginación, diálogos, paneles laterales y avisos.
- **`src/lib`**: Cliente HTTP, errores, dinero, tema, animaciones, paginación y contraste.

Las dependencias van en un solo sentido: `app` → `features` → `components` → `lib` → `types`.

### Datos y estado

- Un único cliente Axios con rutas relativas; toda respuesta se valida con Zod antes de usarse.
- TanStack Query para leer de la API: reintenta una vez si no hay respuesta o el servidor falla, nunca reintenta escrituras y no recarga al volver a la pestaña.
- Zustand para la sesión (guardada en el navegador), el carrito (en memoria) y el tema.

### Tema, accesibilidad y rendimiento

- Tema claro y oscuro con tokens de color; un test comprueba que todos los textos cumplen un contraste WCAG de al menos 4,5:1 en ambos temas.
- Foco visible, diálogos y paneles que atrapan el foco y lo devuelven al cerrar, pestañas navegables con flechas y avisos anunciados a los lectores de pantalla.
- Con «movimiento reducido» activado en el sistema, las animaciones se reducen a fundidos cortos.
- Fuentes Inter y Space Grotesk servidas desde la propia app, sin Google Fonts.
- Cada página y las librerías de PDF y QR se cargan por separado; un script hace fallar el build si algún fragmento supera 500 kB.
- La interfaz está solo en español.

### Modo demo

`npm run build:demo` genera la demo en `dist-demo/`: cambia el adaptador de Axios por un backend en memoria (el mismo que usan las pruebas de Playwright) y las rutas por rutas con `#`. El build normal no se ve afectado; una prueba lo comprueba.

## Infraestructura

| Contenedor | Imagen | Puerto en tu equipo |
| --- | --- | --- |
| booqi-mysql | `mysql:8.0` | 3307 |
| booqi-user-service | `booqi/user-service` | 8080 (con el override) |
| booqi-event-service | `booqi/event-service` | 8081 (con el override) |
| booqi-booking-service | `booqi/booking-service` | 8082 (con el override) |
| booqi-payment-service | `booqi/payment-service` | 8083 (con el override) |
| booqi-frontend | `booqi/frontend` | 80 o `BOOQI_FRONTEND_PORT` |

- Arrancan en orden y cada uno espera a que el anterior esté sano: MySQL → usuarios y eventos → reservas → pagos → frontend.
- Cada contenedor tiene su comprobación de salud y se reinicia solo si se cae.
- Los servicios se construyen con Maven sobre Temurin 17 y se ejecutan con un JRE 17 mínimo como usuario sin privilegios. El frontend se construye con Node 22 y se sirve con nginx.
- El fichero `docker-compose.override.yml` publica los puertos 8080–8083 y activa el perfil de desarrollo; el `Makefile` siempre lo incluye.
- Los datos de MySQL se guardan en el volumen `booqi-mysql-data`.

### Variables de entorno

| Variable | Para qué | Ejemplo |
| --- | --- | --- |
| `MYSQL_ROOT_PASSWORD` | Contraseña de root de MySQL | `change-me-root` |
| `BOOQI_DB_USER` | Usuario de la aplicación | `booqi` |
| `BOOQI_DB_PASSWORD` | Su contraseña (sin comillas simples) | `change-me-app` |
| `BOOQI_CORS_ALLOWED_ORIGINS` | Orígenes permitidos | `http://localhost,…:5173` |
| `BOOQI_FRONTEND_PORT` | Puerto del frontend | `80` |
| `BOOQI_TAG` | Etiqueta de las imágenes | `latest` |

### Perfiles de Spring

- **por defecto**: MySQL en `localhost:3307`, para ejecutar un servicio fuera de Docker.
- **`docker`**: Usa los nombres de los contenedores.
- **`dev`**: Muestra el SQL y sube el nivel de log de la app.
- **`test`**: Base H2 en memoria que imita MySQL.

### nginx

Sirve la app y reenvía cada prefijo de la API a su servicio conservando el host con su puerto, para que CORS funcione también fuera del puerto 80. Cualquier otra ruta devuelve `index.html`, de modo que se puede recargar cualquier página de la app. Swagger y actuator no pasan por el proxy: se abren en el puerto de cada servicio.

## Pruebas y calidad

### Backend

- Unas **456 pruebas**: de controladores, de repositorios con H2, de servicios con Mockito, y de clientes HTTP, configuración, errores y mappers.
- Pruebas de mutación con **PIT 1.17.0** sobre controladores, servicios, errores, configuración y clientes, con un objetivo de más del 95 %.
- Formato con **google-java-format 1.35.0** (Spotless) y estilo con **Checkstyle 12.3.1**: líneas de hasta 120 caracteres, sin imports con `*` ni sin usar, y sin `System.out` ni `printStackTrace`.

### Frontend

- Más de **1000 pruebas** con Vitest y Testing Library.
- **Playwright**: 30 pruebas de extremo a extremo con la API simulada, 31 contra el stack real y pruebas de la demo tal como se publica en GitHub Pages.
- Pruebas de mutación con **Stryker 10.1.1**, con un mínimo del 95 %; la última ejecución dio un 99,77 %.
- ESLint y Prettier.

## CI y publicación

Cada push y pull request ejecuta en GitHub Actions:

- **backend**: Los cuatro servicios en paralelo con Java 17: compilar, probar, formato y estilo (`./mvnw verify`).
- **frontend**: Con Node 22: lint, formato, build, build de la demo y pruebas.
- **compose**: Comprueba que la configuración de Docker Compose es válida.

Cada cambio en `main` publica en GitHub Pages la demo, en la raíz de [renzoramosdev.github.io/Booqui-TFG](https://renzoramosdev.github.io/Booqui-TFG/). La antigua dirección `/demo/` redirige a ella.

## Desarrollo

| Comando | Qué hace |
| --- | --- |
| `make up` | Construye y levanta el stack |
| `make down` | Lo para |
| `make clean` | Lo para y borra los datos |
| `make logs` / `make ps` | Logs y estado de los contenedores |
| `make build` | Empaqueta los servicios y construye el frontend |
| `make test` | Pruebas de todos los servicios |
| `make lint` / `make format` | Comprueba o aplica el formato y el estilo |

### Por partes

- Un servicio: `cd services/-service && ./mvnw spring-boot:run` (JDK 17).
- Frontend: `cd frontend && npm ci && npm run dev`; Vite reenvía la API a los puertos 8080–8083.
- Pruebas: `npm test`, `npm run test:e2e` y `npm run test:e2e:real` (contra el stack levantado).
- Mutación en un servicio: `./mvnw test-compile org.pitest:pitest-maven:mutationCoverage`.
- Demo publicada: tras `npm run build:demo`, `npm run serve:pages` la sirve como en GitHub Pages en `http://localhost:4190/Booqui-TFG/`, y `npm run test:pages` la prueba.

Los cambios se gestionan con OpenSpec: hay 19 capacidades especificadas en `openspec/specs`.

## Decisiones de arquitectura

- 0001 · Monorepo Carpetas `services`, `frontend`, `infra` y `docs`. Sin librería común: el poco código compartido se duplica y se compara.
- 0002 · API y errores Rutas `/api/v1/`, filtros excluyentes, acciones con PATCH y errores RFC 9457.
- 0003 · Capas planas Controller, service y repository con records y MapStruct, en lugar de una arquitectura hexagonal con modelos triplicados.
- 0004 · BCrypt y login Contraseñas con BCrypt sin toda la cadena de Spring Security, y un 401 genérico al fallar. JWT queda para el futuro.
- 0005 · Proxy nginx y Vite El navegador siempre llama al mismo origen; nginx y Vite reenvían cada prefijo a su servicio.

## Limitaciones conocidas

Booqi es un proyecto académico. Estas son sus limitaciones, documentadas en el repositorio:

- **Seguridad:** el backend no verifica quién hace cada llamada, así que cualquiera podría usar la API directamente (por ejemplo, cambiar un rol o confirmar una reserva).
- **Pagos:** repetir un pago descuenta el stock dos veces; el total no se contrasta con la reserva; y si la confirmación falla después de descontar stock, este no se devuelve.
- **Stock:** no hay bloqueo, así que dos compras simultáneas podrían vender la misma entrada; crear una reserva no comprueba el stock.
- **Cancelaciones:** no devuelven el dinero ni las entradas, y los estados de la reserva no están protegidos contra transiciones incoherentes.
- **Categorías:** algunos eventos de los datos iniciales usan categorías («Deportes», «Cultura», «Salud») que no están en la lista del filtro.
- **No existen:** recuperación de contraseña, envío de correos, servicio de contacto ni paginación en el servidor.

## Problemas frecuentes

- **No abre en el puerto 80**: Pasa con Podman rootless. Pon `BOOQI_FRONTEND_PORT=8088` en `infra/.env` y abre `http://localhost:8088`.
- **Cambié el esquema y no arranca**: El script de la base de datos solo se ejecuta la primera vez. Haz `make clean` y luego `make up`.
- **Tildes raras en los datos**: Si ves textos como «GastronomÃ­a», la base se cargó con una codificación distinta de UTF-8; recrea el volumen con `make clean`.
- **Swagger no abre desde el frontend**: No pasa por el proxy: ábrelo en el puerto de cada servicio, por ejemplo `http://localhost:8081/swagger-ui.html`.
- **Podman avisa del HEALTHCHECK**: Podman ignora el de los Dockerfile, pero Compose define los suyos, así que no afecta.

## Créditos y licencia

Trabajo de fin de grado de Renzo Iván Ramos de los Ríos ([RenzoRamosDEV](https://github.com/RenzoRamosDEV)) y Melanie Gabriela Cárdenas Hidalgo ([MelGabi](https://github.com/MelGabi)), publicado con licencia MIT.

## Otros proyectos de Renzo Ramos

Toda la documentación está en la [Biblioteca de Renzo Ramos](https://docs.portafolio-renzoramos.com/biblioteca/); más sobre el autor en su [portafolio](https://portafolio-renzoramos.com/), [GitHub](https://github.com/RenzoRamosDEV) y [LinkedIn](https://www.linkedin.com/in/renzoinv04/).

- [Documentación de AgentBoard](https://docs.portafolio-renzoramos.com/agentboard/) App de escritorio con el coste y la actividad de tus agentes de código (Claude Code, Codex, Copilot, Cursor, Gemini y OpenCode), en local y con servidor MCP.
- [Documentación de Pokédex Retro](https://docs.portafolio-renzoramos.com/pokedex-retro/) La Pokédex de PokéAPI con estética Game Boy, un chat con Claude u OpenAI y un servidor MCP. Sin frameworks ni build.
- [Documentación de Brainy](https://docs.portafolio-renzoramos.com/brainy/) Web gratuita y en español para aprender jugando: explicaciones visuales, quizzes, taller de ortografía, puntos, ranking y PokeBrainy.
