Especificación funcional de la tienda, la aplicación de la práctica guiada. Es deliberadamente pequeña: los hitos se concentran en la organización y los mecanismos, no en la amplitud del dominio. Esta página describe el estado final (tras el hito 09); cada hito se acerca a él.
Una tienda mínima: los productos se organizan en catálogos; los clientes compran productos de un catálogo, cada pedido reserva las unidades que necesita y su cancelación las libera. Los usuarios se registran, inician sesión y ven sus pedidos; un rol de administración gestiona catálogos y productos.
Los módulos no se conocen internamente: orders consume la API pública de catalog; la identidad del comprador llega en el token, así que orders no necesita nada de users.
| Módulo | Responsabilidad | Desde |
|---|---|---|
| catalog | CRUD de catálogos y productos con listado paginado; reservar y liberar stock | partida (el hito 06 organiza el módulo) |
| orders | crear, consultar y cancelar pedidos | hito 06 |
| users | registro, login con JWT, roles USER y ADMIN | hito 08 |
| Tabla | Campos |
|---|---|
catalogs | id, name (único), description |
products | id, catalog_id (FK), name, description, price, stock |
orders | id, user_id, state, created_at |
order_lines | id, order_id, product_id, quantity, unit_price |
users | id, name, email (único), password_hash, role |
refresh_tokens | id, token_hash, user_id, expires_at, revoked |
Notas:
unit_price guarda el precio capturado al reservar el stock, no el precio actual del producto.refresh_tokens guarda el hash del token, nunca el token en claro.CREATED / CANCELLED.| Módulo | Regla |
|---|---|
| catalog | Todo producto pertenece a un catálogo existente (404 si no existe) |
| catalog | El nombre del catálogo es único (409 si se repite al crear o modificar) |
| catalog | No se puede borrar un catálogo con productos (409) |
| catalog | El precio es mayor que cero y de dos decimales (value object Money) |
| catalog | El stock no es negativo (value object Stock); reservar decrementa, liberar incrementa |
| orders | Un pedido tiene al menos una línea; la cantidad de cada línea es 1 o más |
| orders | Crear un pedido reserva el stock de todas sus líneas; si una falla, no queda nada reservado |
| orders | El total es la suma de cantidad por precio de línea |
| orders | Estado inicial CREATED; única transición: a CANCELLED |
| orders | Cancelar libera el stock de todas sus líneas; cancelar un pedido ya cancelado es un error |
| users | El email es único (error 409 si se repite al registrar) |
| users | La contraseña se guarda como hash; longitud mínima de 8 en el contrato |
| users | El registro crea siempre USER; el rol ADMIN existe solo por semilla |
Provisional hasta el hito 08: la petición de creación de pedidos lleva el comprador en el cuerpo (userId), porque no hay usuarios todavía. En el hito 08 el identificador viene del token y ese campo desaparece del contrato.
Estado final, desde el hito 09:
| Operación | Público | USER | ADMIN |
|---|---|---|---|
Lectura de catálogos y productos (GET) | ✔ | ✔ | ✔ |
Escritura de catálogos y productos (POST, PUT, DELETE) | ✖ | ✖ | ✔ |
| Crear pedido | ✖ | ✔ | ✔ |
| Ver o cancelar pedido | ✖ | el propio | cualquiera |
| Mis pedidos | ✖ | propios | propios |
| Registro, login, refresh, logout | ✔ | ✔ | ✔ |
Spring Security resuelve las filas por rutas; la pertenencia del pedido (propietario o ADMIN) se comprueba en el módulo de pedidos, y esa regla se expresa con autorización por método.
| Endpoint | Aparece | Cambios posteriores |
|---|---|---|
GET /api/products | partida | desaparece en el 04: lo sustituye el listado paginado por catálogo |
GET /api/catalogs | 04 | — |
GET /api/catalogs/{id} | 04 | — |
POST /api/catalogs | 04 | protegido desde el 09 |
PUT /api/catalogs/{id} | 04 | protegido desde el 09 |
DELETE /api/catalogs/{id} | 04 | protegido desde el 09 |
GET /api/catalogs/{id}/products | 04 | paginado (page y size) desde que nace |
GET /api/products/{id} | 04 | — |
POST /api/products | 04 | protegido desde el 09 |
PUT /api/products/{id} | 04 | protegido desde el 09 |
DELETE /api/products/{id} | 04 | protegido desde el 09 |
POST /api/orders | 06 | pierde userId en el 08; protegido desde el 09 |
GET /api/orders/{id} | 06 | protegido desde el 09 |
POST /api/orders/{id}/cancel | 06 | protegido desde el 09 |
GET /api/me/orders | 08 | — |
POST /api/users | 08 | — |
POST /api/auth/login | 08 | — |
POST /api/auth/refresh | 08 | — |
POST /api/auth/logout | 08 | — |
Los errores de la API usan el mismo esquema Problem (RFC 9457) en todas las operaciones: 400 (formato), 404 (recurso inexistente, incluido el catálogo de un producto), 409 (nombre de catálogo repetido, catálogo con productos, stock insuficiente, email repetido, pedido ya cancelado), 401 y 403 (seguridad).
| Tema | Flujo previsto en la tienda |
|---|---|
| 08 — eventos | Aviso de stock bajo al reservar y factura al cancelar: son operaciones auxiliares, fuera del flujo principal; encajan como eventos |
| 09 — logs | Correlación de peticiones con MDC, registro a fichero con rotación y Actuator |
| 10 — Lambda | Informe de ventas del mes (CSV) como función desplegada en local |
| 11 — concurrencia | Análisis del modelo de concurrencia de la tienda; sin cambios de código |
| Decisión | Valor |
|---|---|
| Paquete base | es.cesguiro.shop (cada quien adapta el suyo) |
| Identificadores del código | en inglés |
| Títulos de los errores del contrato | en inglés (Not Found, Invalid request…) |
| Base de datos | MariaDB (imagen mariadb:12); puerto local 3306 |
| Pruebas de persistencia | Testcontainers con la misma imagen de MariaDB (desde el hito 07) |
| Tags del repositorio | partida y hito-04 … hito-09 |
| Versión del contrato | la tabla de la práctica |