====== 02 - Especificación de la tienda ====== Especificación funcional de la tienda, la aplicación de la [[proyecto:start|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 [[proyecto:09-spring-security|hito 09]]); cada hito se acerca a él. ===== 1. Dominio ===== 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. @startuml !theme cerulean skinparam backgroundColor transparent skinparam shadowing false skinparam ArrowThickness 1 skinparam classAttributeIconSize 0 skinparam class { BackgroundColor #FEFEFE BorderColor #2FA4E7 ArrowColor #2FA4E7 FontColor #343A40 } skinparam classAttributeFontColor #343A40 skinparam stereotypeCBackgroundColor #2FA4E7 skinparam stereotypeCBorderColor #1B7FB8 skinparam package { BackgroundColor transparent BorderColor #2FA4E7 FontColor #343A40 } skinparam package { BackgroundColor transparent BorderColor #2FA4E7 FontColor #343A40 } allowmixing package "shop" #E8F5E9 { class ShopApplication } package "catalog" #C8E6C9 { component "REST: catálogos y productos" as restCatalog component "dominio y persistencia propios" as catalogInternals interface "CatalogApi" as api } package "orders" #BBDEFB { component "REST: pedidos" as restOrders component "PlaceOrder / CancelOrder" as orderUseCases component "dominio y persistencia propios" as orderInternals } package "users" #FFE0B2 { component "REST: registro y login" as restUsers component "tokens y persistencia propios" as userInternals } ShopApplication --> catalog ShopApplication --> orders ShopApplication --> users orders --> api : reserva y libera stock\n(colaboración síncrona) users ..> orders : la identidad del comprador\nviaja en los claims del token @enduml 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 [[proyecto:06-modulos-modulith|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 | ===== 2. Datos ===== ^ 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: * Todo producto pertenece a un catálogo; el borrado de un catálogo con productos se rechaza. * ''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. * El estado del pedido se guarda como cadena: ''CREATED'' / ''CANCELLED''. ===== 3. Reglas de negocio ===== ^ 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 [[proyecto:08-login-jwt|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. ===== 4. Roles y autorización ===== Estado final, desde el [[proyecto:09-spring-security|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. ===== 5. Endpoints y su evolución ===== ^ 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). ===== 6. Ganchos para los temas finales ===== ^ 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 | ===== 7. Convenciones fijadas ===== ^ 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 [[proyecto:start|la práctica]] |