diff --git a/app/Domains/Attachable/documentacion/README.md b/app/Domains/Attachable/documentacion/README.md new file mode 100644 index 0000000..fec8cbd --- /dev/null +++ b/app/Domains/Attachable/documentacion/README.md @@ -0,0 +1,29 @@ +# Dominio Attachable + +## Propósito + +Centraliza el almacenamiento y la metadata de archivos adjuntos. Acepta archivos subidos o contenido Base64, los persiste en S3 y registra su tipo, MIME, extensión, tamaño, nombre original y clave única. + +## Componentes principales + +- `Models/Attachment.php`: representa un adjunto y genera URL temporales de acceso. +- `Services/AttachmentService.php`: almacena, copia y elimina archivos, compensando en S3 si falla la escritura en base de datos. +- `Enums/AttachmentType.php`: clasifica imágenes, videos, PDF, audio, documentos y otros archivos. +- `Exceptions/AttachmentStorageException.php`: expresa fallos propios del almacenamiento. + +## Flujo principal + +1. El consumidor entrega un `UploadedFile` o una cadena Base64 y un directorio. +2. El servicio valida el contenido, detecta MIME/extensión y genera una clave UUID. +3. El archivo se guarda en el disco `s3`. +4. Se crea el registro `Attachment`; ante error se elimina el objeto que había sido subido. + +## API y dependencias + +No expone rutas HTTP propias. Lo consumen otros dominios, especialmente `Catalog` y `Tenant`. Depende de Laravel Storage, Symfony Mime y del modelo `Attachment`. + +## Consideraciones + +- El directorio no puede quedar vacío después de normalizarlo. +- La eliminación se considera fallida si S3 no confirma el borrado. +- Las URL generadas son temporales; el vencimiento predeterminado es de 10 minutos. diff --git a/app/Domains/Auth/documentacion/README.md b/app/Domains/Auth/documentacion/README.md new file mode 100644 index 0000000..40ea937 --- /dev/null +++ b/app/Domains/Auth/documentacion/README.md @@ -0,0 +1,36 @@ +# Dominio Auth + +## Propósito + +Gestiona identidad y acceso de usuarios de la tienda y del panel administrativo: registro, inicio y cierre de sesión, perfil, autenticación con Google y recuperación de contraseña. + +## Modelo y servicios + +- `User`: usuario autenticable, asociado a tenant, rol, intentos de acceso y categorías habilitadas para escaneo. +- `LoginAttempt` y `ResetPasswordAttempt`: trazabilidad de accesos y recuperación de contraseña. +- `PasswordLoginService`: autentica tienda y AdminApp, incluyendo bloqueo por intentos. +- `RegisterUserService` y `ProfileService`: alta y edición del usuario. +- `ResetPasswordAttemptService`: crea, valida y consume códigos de recuperación. +- `GoogleAuthService`: redirección, callback e intercambio de código para Google OAuth. +- `AdminAppContextService`: carga el contexto requerido por un usuario administrativo. + +## Endpoints públicos + +- `POST /register`, `POST /login` y `POST /logout`. +- `GET /me` y `PUT /me`, protegidos por `auth:sanctum`. +- Creación, validación y aplicación de intentos de recuperación bajo `/password`. +- `POST /auth/google/exchange` para canjear el código de autenticación. +- `POST /v1/adminapp/login` y consulta del usuario administrativo dentro del grupo autenticado de AdminApp. + +## Validación y respuestas + +Los `FormRequest` validan cada operación. `UserResource` y `AdminAppMeResource` definen las representaciones de salida. Los endpoints sensibles aplican `auth:sanctum` y límites de frecuencia. + +## Dependencias y eventos + +Se relaciona con `Tenant` y `Authorization`; el registro y la recuperación disparan flujos atendidos por `Notification`. El carrito invitado puede integrarse al usuario autenticado mediante el dominio `Cart`. + +## Consideraciones + +- La resolución del tenant forma parte de la autenticación y no debe omitirse. +- Los cambios en reglas de login deben conservar los límites de intentos y el manejo de `AccountLockedException`. diff --git a/app/Domains/Authorization/documentacion/README.md b/app/Domains/Authorization/documentacion/README.md new file mode 100644 index 0000000..9a3845c --- /dev/null +++ b/app/Domains/Authorization/documentacion/README.md @@ -0,0 +1,26 @@ +# Dominio Authorization + +## Propósito + +Define el esquema de roles y permisos usado para autorizar funcionalidades de la aplicación. + +## Componentes principales + +- `Enums/RoleCode.php`: códigos de roles conocidos por el sistema. +- `Models/Role.php`: rol con relaciones hacia permisos, usuarios y menús. +- `Models/Permission.php`: permiso asignable a uno o más roles. +- `Models/RolePermission.php`: entidad de asociación entre rol y permiso. + +## API + +No expone controladores ni rutas propias. Su información se consume desde autenticación, menús, políticas y middleware de autorización. + +## Relaciones relevantes + +- `Role` tiene muchos usuarios del dominio `Auth`. +- Roles y permisos mantienen una relación muchos-a-muchos. +- Los roles determinan los menús disponibles mediante el dominio `Menu`. + +## Consideraciones + +Los códigos definidos en `RoleCode` funcionan como contrato entre datos persistidos y lógica de aplicación. Al agregar un rol o permiso se deben revisar seeds, asociaciones y consumidores. diff --git a/app/Domains/Bootstrap/documentacion/README.md b/app/Domains/Bootstrap/documentacion/README.md new file mode 100644 index 0000000..2d9c6a4 --- /dev/null +++ b/app/Domains/Bootstrap/documentacion/README.md @@ -0,0 +1,28 @@ +# Dominio Bootstrap + +## Propósito + +Entrega la configuración inicial que necesitan la tienda y el panel administrativo antes de renderizar su interfaz. + +## Flujos + +- `TenantBootstrapService` resuelve un tenant desde el dominio solicitado y carga su información pública. +- `AdminAppBootstrapService` prepara el contexto inicial del panel administrativo para el tenant autenticado. +- Los controladores invocables transforman el resultado mediante `TenantResource` o `AdminAppBootstrapResource`. + +## Endpoints + +- `GET /tenants/bootstrap/{dominio}`: bootstrap público de la tienda. +- Endpoint de bootstrap bajo `/v1/adminapp`, protegido por `auth:sanctum` y `adminapp.tenant`. + +## Validación + +`TenantBootstrapRequest` valida el dominio recibido. `AdminAppBootstrapRequest` reutiliza ese contrato para el panel. + +## Dependencias + +Depende principalmente de `Tenant` para resolver y cargar la tienda, y de los dominios que aportan datos al contexto administrativo. + +## Consideraciones + +Este dominio es un agregador de lectura. Debe mantenerse liviano y delegar la obtención de cada dato al dominio propietario. diff --git a/app/Domains/Cart/documentacion/README.md b/app/Domains/Cart/documentacion/README.md new file mode 100644 index 0000000..f80a34d --- /dev/null +++ b/app/Domains/Cart/documentacion/README.md @@ -0,0 +1,32 @@ +# Dominio Cart + +## Propósito + +Gestiona el carrito activo de un tenant tanto para visitantes como para usuarios autenticados. + +## Modelo + +- `Cart`: pertenece a un tenant y opcionalmente a un usuario; calcula el total y permite agregar, actualizar o quitar ítems. +- `CartItem`: referencia un `CatalogItem` y, opcionalmente, una `Variant`; expone la selección efectiva. + +## Servicios + +- `CartService`: obtiene el carrito, modifica ítems y administra la cookie del token invitado. +- `GuestCartMergeService`: incorpora el carrito invitado al usuario cuando este se autentica. + +## Endpoints + +Bajo `/tenants/{tenant:codigo}`: + +- `GET /cart`. +- `POST /cart/items`. +- `PATCH /cart/items/{cartItem}`. +- `DELETE /cart/items/{cartItem}`. + +## Contratos + +`AddCartItemRequest` y `UpdateCartItemQuantityRequest` validan selección y cantidad. `CartResource` y `CartItemResource` estabilizan la respuesta pública. + +## Dependencias y reglas + +Depende de `Catalog` para productos y variantes, de `Tenant` para aislar datos y de `Auth` cuando existe usuario. Toda operación debe comprobar que carrito e ítem pertenecen al tenant actual. diff --git a/app/Domains/Catalog/documentacion/README.md b/app/Domains/Catalog/documentacion/README.md new file mode 100644 index 0000000..9ecf3aa --- /dev/null +++ b/app/Domains/Catalog/documentacion/README.md @@ -0,0 +1,33 @@ +# Dominio Catalog + +## Propósito + +Modela y publica la oferta comercial del tenant: productos, variantes, categorías, marcas, atributos, inventario, bundles y grupos destacados. + +## Modelo + +- `CatalogItem` es la raíz del producto y se relaciona con tenant, categoría, marca, inventario, variantes, atributos, adjuntos y grupos destacados. +- `Variant`, `ItemAttribute`, `Attribute`, `AttributeOption` y `VariantDefinition` describen opciones comercializables. +- `Inventory` administra stock disponible, reservado y comprado. +- `Category` soporta jerarquía y categorías globales o propias del tenant. +- `FeaturedGroup` y `FeaturedItem` organizan secciones destacadas. +- `BundleComponent` representa los componentes de un paquete. + +## Servicios + +- `CatalogService`: alta, búsqueda, detalle, listado por categoría y eliminación. +- `CatalogInventoryService`: consulta, reserva, libera y confirma inventario. +- `FeaturedGroupService`: pagina los ítems destacados para la tienda. +- `OnTicketFeaturedGroupService`: administra grupos destacados del panel para sitios de tickets. + +## Endpoints de tienda + +Bajo `/tenants/{tenant:codigo}` se publican catálogo, búsqueda, categoría, detalle, alta de ítems y paginación de grupos destacados. + +## Endpoints administrativos + +Bajo `/v1/adminapp/tenant/featured-groups`, con `auth:sanctum` y `adminapp.tenant`, se listan, crean y actualizan grupos destacados. + +## Dependencias y reglas + +Usa `Attachable` para imágenes/archivos, `Tenant` para aislamiento y `Ticket`/`Event` para vigencia y fechas. `Cart` y `Purchase` consumen sus precios, variantes e inventario. Los cambios de stock deben pasar por `CatalogInventoryService` para conservar reservas y disponibilidad. diff --git a/app/Domains/Event/documentacion/README.md b/app/Domains/Event/documentacion/README.md new file mode 100644 index 0000000..6959a0b --- /dev/null +++ b/app/Domains/Event/documentacion/README.md @@ -0,0 +1,28 @@ +# Dominio Event + +## Propósito + +Administra la configuración temporal de un tenant orientado a eventos y sus fechas disponibles. + +## Componentes + +- `Models/EventDate.php`: fecha del evento con inicio, fin, tenant y variantes asociadas. +- `Services/EventService.php`: obtiene y actualiza la configuración de evento del tenant. +- `Controllers/AdminApp/EventController.php`: consulta y modificación desde AdminApp. +- `UpdateEventRequest`: valida datos y reglas cruzadas de fechas. +- `EventResource`: serializa la configuración de salida. + +## Endpoints + +Bajo `/v1/adminapp/tenant/event`, protegidos por `auth:sanctum` y `adminapp.tenant`: + +- `GET`: obtiene la configuración. +- `PUT`: actualiza la configuración. + +## Dependencias + +Depende de `Tenant`. Las fechas se vinculan con variantes de `Catalog`, que a su vez pueden generar tickets. + +## Consideraciones + +El archivo `routes/api.php` no publica operaciones adicionales. Al modificar fechas debe mantenerse la validación de orden y coherencia temporal de `UpdateEventRequest`. diff --git a/app/Domains/Forms/documentacion/README.md b/app/Domains/Forms/documentacion/README.md new file mode 100644 index 0000000..826f1af --- /dev/null +++ b/app/Domains/Forms/documentacion/README.md @@ -0,0 +1,25 @@ +# Dominio Forms + +## Propósito + +Provee catálogos y opciones auxiliares para construir formularios del panel administrativo. Es un dominio de lectura que compone datos pertenecientes a otros dominios. + +## Formularios disponibles + +- `EventFormService`: devuelve redes sociales disponibles y las URL configuradas para el tenant. +- `SaleFormService`: expone los estados admitidos para compras con sus etiquetas de presentación. +- `StaffFormService`: lista categorías raíz que pueden asignarse al personal del tenant. + +Cada servicio tiene un controlador invocable y un `JsonResource` específico. `SocialMediaOptionResource` representa las opciones de redes sociales. + +## Endpoints + +Bajo `/v1/adminapp/forms`, con `auth:sanctum` y `adminapp.tenant`: + +- `GET /event`. +- `GET /sale`. +- `GET /staff`. + +## Dependencias + +Compone datos de `Tenant`, `Purchase` y `Catalog`. No debe duplicar reglas de negocio: las listas y estados canónicos siguen perteneciendo a sus dominios de origen. diff --git a/app/Domains/Integration/documentacion/README.md b/app/Domains/Integration/documentacion/README.md new file mode 100644 index 0000000..0975bb3 --- /dev/null +++ b/app/Domains/Integration/documentacion/README.md @@ -0,0 +1,29 @@ +# Dominio Integration + +## Propósito + +Gestiona integraciones externas disponibles y su configuración por tenant. Incluye correo y pagos mediante Telepagos. + +## Modelo y seguridad + +- `Integration`: definición global de una integración. +- `TenantIntegration`: configuración y credenciales de una integración para un tenant. +- `EncryptedIntegrationData`: cast que protege los datos sensibles persistidos. +- `TenantIntegrationService`: consulta y configura integraciones del tenant. + +## Servicios externos + +- `BaseIntegrationService`: base para resolver configuración, URL y cliente del tenant. +- `MailService`: envío de correo usando la integración configurada. +- `TelepagosIntegrationService`: autenticación, caché de token, generación de QR y consulta de cobros. +- `TelepagosWebhookService`: procesa notificaciones recibidas desde Telepagos. + +## Endpoints + +- CRUD global bajo `/integrations`. +- Consulta y configuración por tenant bajo `/{tenant_code}/integrations`. +- `POST /webhooks/telepagos/{tenant_codigo}` para notificaciones del proveedor. + +## Dependencias y reglas + +Se integra con `Tenant` y con el checkout de `Purchase`. `Notification` utiliza `MailService`. Las credenciales no deben exponerse en respuestas ni logs; los webhooks deben validar su contrato antes de alterar una compra. diff --git a/app/Domains/Logging/documentacion/README.md b/app/Domains/Logging/documentacion/README.md new file mode 100644 index 0000000..31c028d --- /dev/null +++ b/app/Domains/Logging/documentacion/README.md @@ -0,0 +1,25 @@ +# Dominio Logging + +## Propósito + +Registra cambios relevantes de valores en modelos de negocio, indicando tenant, atributo, valor anterior/nuevo, fecha y actor. + +## Componentes + +- `Models/ValueChange.php`: entrada persistida del historial, relacionada polimórficamente con el objeto modificado. +- `Models/Concerns/LogsValueChanges.php`: trait reutilizable que escucha actualizaciones del modelo. +- `Enums/ValueChangeActorType.php`: distingue cambios realizados por usuario o por el sistema. + +## Uso + +Un modelo consumidor debe: + +1. Usar el trait `LogsValueChanges`. +2. Declarar la propiedad `loggedAttributes` con los atributos auditables. +3. Implementar `valueChangeTenantCode()`. + +El trait solo registra atributos configurados que efectivamente cambiaron. Si existe un usuario autenticado lo asocia al cambio; en caso contrario marca al sistema como actor. + +## API y dependencias + +No expone rutas HTTP. `Purchase` lo utiliza para auditar cambios de estado y `Sale` consulta esas modificaciones para reportes. diff --git a/app/Domains/MailTest/documentacion/README.md b/app/Domains/MailTest/documentacion/README.md new file mode 100644 index 0000000..ac07f63 --- /dev/null +++ b/app/Domains/MailTest/documentacion/README.md @@ -0,0 +1,24 @@ +# Dominio MailTest + +## Propósito + +Ofrece una operación técnica para verificar la configuración de correo de un tenant sin ejecutar un flujo funcional real. + +## Componentes + +- `MailTestController`: endpoint invocable de envío. +- `SendTestMailRequest`: valida destinatario y contenido requerido. +- `MailTestService`: coordina el envío de prueba. +- `TestMail`: mailable utilizado para construir el mensaje. + +## Endpoint + +- `POST /{tenant_code}/mail-test/send`. + +## Dependencias + +Usa la configuración de correo del dominio `Integration` y resuelve el tenant indicado. + +## Consideraciones + +Es una herramienta de diagnóstico. Debe restringirse o deshabilitarse en entornos donde no corresponda exponer envíos de prueba, y nunca debe registrar credenciales. diff --git a/app/Domains/Menu/documentacion/README.md b/app/Domains/Menu/documentacion/README.md new file mode 100644 index 0000000..a15c15f --- /dev/null +++ b/app/Domains/Menu/documentacion/README.md @@ -0,0 +1,24 @@ +# Dominio Menu + +## Propósito + +Define menús disponibles y permite configurar su contenido para cada tenant y rol. + +## Modelo + +- `Menu`: definición global de una entrada de menú y su tipo de contenido. +- `TenantMenu`: configuración específica por tenant, incluyendo contenido estático cuando corresponde. +- `MenuRole`: asociación entre menú y rol autorizado. + +## Servicios + +`TenantMenuService::configure()` crea o actualiza atómicamente la configuración de un menú para un tenant. Solo conserva `static_content` cuando el menú fue definido como contenido estático. + +## Endpoints + +- Recurso REST `/menues` mediante `MenuController`. +- `POST /{tenant_code}/menues/{menu_code}` para configurar un menú del tenant. + +## Dependencias y reglas + +Depende de `Tenant` y `Authorization`. Los códigos de menú y tenant forman la identidad lógica de la configuración; el contenido enviado debe respetar el tipo definido por `Menu`. diff --git a/app/Domains/Notification/documentacion/README.md b/app/Domains/Notification/documentacion/README.md new file mode 100644 index 0000000..6d7dd16 --- /dev/null +++ b/app/Domains/Notification/documentacion/README.md @@ -0,0 +1,26 @@ +# Dominio Notification + +## Propósito + +Orquesta notificaciones de negocio por correo a partir de eventos de otros dominios. + +## Eventos atendidos + +- `UserRegistered`: dispara el correo de bienvenida. +- `PasswordResetRequested`: envía el código de recuperación si el intento sigue pendiente. +- `PurchasePaid`: envía la confirmación de pago. +- `TicketsAvailable`: informa y entrega la disponibilidad de tickets. + +## Componentes + +Los listeners `SendWelcomeEmail`, `SendPasswordResetEmail`, `SendPurchasePaidEmail` y `SendTicketsAvailableEmail` delegan en `NotificationMailService`. Este servicio carga el contexto necesario, renderiza las vistas y envía mediante `Integration/MailService`. + +## API y dependencias + +No expone rutas HTTP. Consume datos de `Auth`, `Tenant`, `Purchase` y `Ticket`, y delega la entrega al dominio `Integration`. + +## Consideraciones + +- Los listeners reciben identificadores y vuelven a cargar los modelos, evitando transportar entidades obsoletas. +- La recuperación no se envía si el intento dejó de estar pendiente. +- Los handlers deben permanecer idempotentes o tolerantes a reintentos de cola. diff --git a/app/Domains/Purchase/documentacion/README.md b/app/Domains/Purchase/documentacion/README.md new file mode 100644 index 0000000..35d53d0 --- /dev/null +++ b/app/Domains/Purchase/documentacion/README.md @@ -0,0 +1,33 @@ +# Dominio Purchase + +## Propósito + +Implementa el ciclo de compra y checkout: crea una compra desde el carrito, toma una instantánea de sus ítems, reserva inventario, permite ediciones, inicia el pago y confirma, cancela o vence la operación. + +## Modelo + +- `Purchase`: raíz de la compra; estados `created`, `pending_payment`, `paid`, `cancelled`, `rejected` y `expired`. +- `PurchaseItem`: snapshot del producto o variante, cantidad, precio y total al comprar. +- `TelepagosQr` y `TelepagosPayment`: datos del QR e intentos/resultados del proveedor. +- `PurchasePaid`: evento emitido una sola vez al pasar a pagada bajo bloqueo transaccional. + +## Servicios de checkout + +`CheckoutService` es la fachada estable. Delega en: + +- `StartCheckoutService`: inicia la compra desde el carrito. +- `EditCheckoutService`: modifica cliente o cantidades antes del cierre. +- `CompleteCheckoutService`: completa, envía a revisión o confirma el pago. +- `ReleaseCheckoutService`: cancela, vence y procesa vencimientos pendientes. +- `SourceCartService`: sincroniza, restaura o finaliza el carrito fuente. +- `CatalogSelectionResolver` y `PurchaseItemSnapshotFactory`: resuelven selecciones y generan snapshots. + +`UserPurchaseLimitService` controla límites de compra y `CheckoutService` conserva el punto de entrada para controladores e integraciones. + +## Endpoints + +Bajo `/tenants/{tenant:codigo}/compras`, con `auth:sanctum`: listado, inicio, detalle, edición de ítems, datos del cliente, intención de pago, finalización, revisión y cancelación. + +## Dependencias y reglas + +Depende de `Cart`, `Catalog`, `Tenant`, `Auth` e `Integration`; emite eventos consumidos por `Ticket` y `Notification`. Los cambios de estado e inventario deben ser transaccionales y usar los servicios del checkout, no actualizaciones directas del modelo. diff --git a/app/Domains/Sale/documentacion/README.md b/app/Domains/Sale/documentacion/README.md new file mode 100644 index 0000000..3f6c49b --- /dev/null +++ b/app/Domains/Sale/documentacion/README.md @@ -0,0 +1,28 @@ +# Dominio Sale + +## Propósito + +Provee consultas administrativas y exportaciones de ventas confirmadas, además del historial de modificaciones auditadas. + +## Componentes + +- `AdminAppSaleService`: pagina ventas, calcula totales y obtiene colecciones para exportación; también consulta modificaciones. +- `AdminAppSalePdfService`: genera descargas PDF de ventas y de cambios. +- `AdminAppSaleIndexRequest`: valida filtros del listado y la exportación. +- `SaleResource` y `SaleModificationResource`: representan ventas e historial para AdminApp. +- `SaleController`: entrada HTTP del panel. + +## Endpoints + +Bajo `/v1/adminapp/tenant`, protegidos por `auth:sanctum` y `adminapp.tenant`: + +- `GET /sales` y `GET /sales/pdf`. +- `GET /sales/modifications` y `GET /sales/modifications/pdf`. + +## Dependencias + +Consume compras de `Purchase`, datos del tenant y entradas de `Logging`. No es dueño del estado de una compra; cualquier mutación debe ejecutarse en `Purchase`. + +## Consideraciones + +La consulta paginada y la colección de exportación deben aplicar los mismos filtros para evitar diferencias entre pantalla y PDF. diff --git a/app/Domains/Shared/documentacion/README.md b/app/Domains/Shared/documentacion/README.md new file mode 100644 index 0000000..546bea3 --- /dev/null +++ b/app/Domains/Shared/documentacion/README.md @@ -0,0 +1,18 @@ +# Dominio Shared + +## Propósito + +Contiene contratos técnicos reutilizables que no pertenecen a un único dominio funcional. + +## Componentes + +- `Enums/FieldType.php`: tipos de campos dinámicos y helpers para determinar si admiten opciones estáticas o dinámicas. +- `Rules/ImageOrBase64Rule.php`: regla de validación para aceptar una imagen subida o codificada en Base64. + +## API + +No posee modelos persistentes, controladores ni rutas. Sus elementos se importan desde requests y servicios de otros dominios. + +## Criterio de pertenencia + +Solo deben incorporarse aquí conceptos verdaderamente transversales. Una regla o enum con significado de negocio específico debe permanecer en su dominio propietario. diff --git a/app/Domains/Staff/documentacion/README.md b/app/Domains/Staff/documentacion/README.md new file mode 100644 index 0000000..f72d4a6 --- /dev/null +++ b/app/Domains/Staff/documentacion/README.md @@ -0,0 +1,20 @@ +# Dominio Staff + +## Propósito + +Administra usuarios de personal de un tenant y las categorías que tienen habilitadas para operar o escanear. + +## Componentes + +- `AdminAppStaffController`: listado, alta, modificación y baja. +- `StaffService`: aplica el alcance por tenant, busca personal y sincroniza sus datos/asignaciones. +- `StoreStaffRequest` y `UpdateStaffRequest`: validan cada operación. +- `StaffResource`: representación de salida para AdminApp. + +## Endpoints + +Recurso REST `/v1/adminapp/tenant/staff`, excepto detalle individual, protegido por `auth:sanctum` y `adminapp.tenant`. + +## Dependencias y reglas + +Usa `Auth/User` como entidad de personal, `Authorization` para su rol, `Catalog/Category` para asignaciones y `Tenant` para aislamiento. Toda búsqueda, edición o borrado debe comprobar que el usuario pertenece al tenant autenticado. diff --git a/app/Domains/StorageTest/documentacion/README.md b/app/Domains/StorageTest/documentacion/README.md new file mode 100644 index 0000000..09d7138 --- /dev/null +++ b/app/Domains/StorageTest/documentacion/README.md @@ -0,0 +1,23 @@ +# Dominio StorageTest + +## Propósito + +Expone operaciones técnicas para comprobar la escritura en S3 y la generación de URL temporales. + +## Componentes + +- `S3TestController`: recibe solicitudes de carga y URL temporal. +- `S3TestService`: almacena un archivo de prueba y genera el enlace firmado. +- `StoreS3TestFileRequest`: valida la carga. +- `GenerateS3TemporaryUrlRequest`: valida ruta y tiempo de expiración. + +## Endpoints + +Bajo `/storage-test/s3`: + +- `POST /upload`. +- `GET /temporary-url`. + +## Consideraciones + +Es infraestructura de diagnóstico, no una API funcional de archivos. Debe restringirse por entorno o autorización. Para adjuntos de negocio se debe usar el dominio `Attachable`. diff --git a/app/Domains/Tenant/documentacion/README.md b/app/Domains/Tenant/documentacion/README.md new file mode 100644 index 0000000..aca3c97 --- /dev/null +++ b/app/Domains/Tenant/documentacion/README.md @@ -0,0 +1,30 @@ +# Dominio Tenant + +## Propósito + +Es la raíz del modelo multi-tenant. Gestiona organizaciones/sitios, tipos de web, redes sociales, logos y extras configurables por sitio. + +## Modelo + +- `Tenant`: entidad principal, resuelta en rutas por `codigo`; relaciona catálogo, fechas, redes, menús y configuración visual. +- `WebsiteType`: plantilla o tipo de sitio disponible. +- `WebsiteTypeExtra`: definición de un extra y su configuración admitida. +- `WebsiteExtra`: valor resuelto y estado del extra para un tenant. +- `SocialMedia`: catálogo de redes sociales asociables. + +## Servicios + +- `TenantService`: crea y actualiza tenants, incluyendo sus recursos asociados. +- `TenantInformationService`: carga un tenant y las relaciones requeridas por cada contexto. +- `WebsiteTypeService`: crea o actualiza tipos de sitio. +- `WebsiteExtraService`: construye reglas dinámicas, crea, actualiza y habilita/deshabilita extras. +- `TenantDomainNormalizer`: normaliza dominios antes de resolver el tenant. + +## Endpoints + +- Recurso REST público/administrativo `/tenants`. +- Bajo `/v1/adminapp/tenant/website-extras`, con autenticación y contexto de tenant: consulta general, detalle, actualización y activación/desactivación. + +## Dependencias y reglas + +Usa `Attachable` para logos y archivos. Es referenciado por casi todos los dominios para aislamiento. El `codigo` es clave de ruta y clave foránea heredada; no debe sustituirse por `id` sin una migración integral. diff --git a/app/Domains/Ticket/Services/TicketGeneratorService.php b/app/Domains/Ticket/Services/TicketGeneratorService.php index 3a7b5d5..5a9a9b9 100644 --- a/app/Domains/Ticket/Services/TicketGeneratorService.php +++ b/app/Domains/Ticket/Services/TicketGeneratorService.php @@ -5,6 +5,7 @@ namespace App\Domains\Ticket\Services; use App\Domains\Auth\Models\User; use App\Domains\Catalog\Models\CatalogItem; use App\Domains\Catalog\Models\Variant; +use App\Domains\Ticket\Enums\ValidityTimeType; use App\Domains\Ticket\Exceptions\TicketGenerationException; use App\Domains\Ticket\Models\Ticket; use App\Domains\Ticket\Models\ValidityTime; @@ -34,14 +35,21 @@ class TicketGeneratorService $quantity, $sourceVariantId, ); + $fixedValidityTimes = []; return $targets->map(function (array $target) use ( + &$fixedValidityTimes, $sourcePurchaseId, $user, ): Ticket { $item = $target['catalog_item']; $variant = $target['variant']; $validityTime = $this->resolveValidityTime($item, $variant); + $validityTime = $this->materializeEventDateValidityTime( + $variant, + $validityTime, + $fixedValidityTimes, + ); return Ticket::query()->create([ 'tenant_code' => $item->tenant_code, @@ -168,4 +176,56 @@ class TicketGeneratorService return $validityTimes->first(); } + + /** + * Convert a recurring time window into the fixed date and time purchased by + * the customer. Equal tickets generated together share the same snapshot. + * + * @param array $fixedValidityTimes + */ + private function materializeEventDateValidityTime( + ?Variant $variant, + ?ValidityTime $validityTime, + array &$fixedValidityTimes, + ): ?ValidityTime { + if ( + $variant === null + || $validityTime === null + || $validityTime->type !== ValidityTimeType::TimeWindow + ) { + return $validityTime; + } + + $variant->loadMissing('eventDate'); + $eventDate = $variant->eventDate; + + if ($eventDate === null) { + return $validityTime; + } + + $cacheKey = $eventDate->getKey().':'.$validityTime->getKey(); + + if (isset($fixedValidityTimes[$cacheKey])) { + return $fixedValidityTimes[$cacheKey]; + } + + $startsAt = $validityTime->startsAt($eventDate->date); + $expiresAt = $validityTime->expiresAt($eventDate->date); + + if ( + $startsAt !== null + && $expiresAt !== null + && $expiresAt->lessThanOrEqualTo($startsAt) + ) { + $expiresAt = $expiresAt->addDay(); + } + + return $fixedValidityTimes[$cacheKey] = ValidityTime::query()->create([ + 'type' => ValidityTimeType::FixedWindow, + 'start_time' => null, + 'end_time' => null, + 'fixed_starts_at' => $startsAt, + 'fixed_expires_at' => $expiresAt, + ]); + } } diff --git a/app/Domains/Ticket/documentacion/README.md b/app/Domains/Ticket/documentacion/README.md new file mode 100644 index 0000000..60b1e27 --- /dev/null +++ b/app/Domains/Ticket/documentacion/README.md @@ -0,0 +1,33 @@ +# Dominio Ticket + +## Propósito + +Genera, valida, consulta y exporta entradas asociadas a compras pagadas de productos o variantes ticketables. + +## Modelo + +- `Ticket`: pertenece a tenant y usuario, conserva referencias a compra, producto, variante, validez y usuario escáner. +- `ValidityTime`: define ventanas absolutas o relativas de vigencia para productos, opciones y tickets. +- `ValidityTimeType`: enum de estrategias de vigencia. + +El modelo calcula si un ticket está vigente, vencido o usado, y resuelve sus fechas efectivas de inicio y fin. + +## Flujo de generación + +1. `Purchase` emite `PurchasePaid` al confirmarse el pago. +2. `GenerateTicketsForPaidPurchase` atiende el evento. +3. `TicketGeneratorService` crea los tickets requeridos según ítems, cantidades y vigencia. +4. El flujo puede emitir disponibilidad para que `Notification` informe al comprador. + +## Endpoints + +Bajo `/tenants/{tenant:codigo}`, protegidos por `auth:sanctum`: + +- `GET /tickets`. +- `POST /tickets/pdf`. + +`TicketPdfService` genera la descarga y `TicketResource`/`ValidityTimeResource` definen las respuestas. + +## Dependencias y reglas + +Depende de `Purchase`, `Catalog`, `Tenant` y `Auth`. La generación debe ser idempotente ante reintentos del evento. `TicketNotAvailableException` y `TicketGenerationException` separan indisponibilidad de errores de generación. diff --git a/tests/Feature/Ticket/TicketGeneratorServiceTest.php b/tests/Feature/Ticket/TicketGeneratorServiceTest.php index 76aafab..4deb0c1 100644 --- a/tests/Feature/Ticket/TicketGeneratorServiceTest.php +++ b/tests/Feature/Ticket/TicketGeneratorServiceTest.php @@ -6,12 +6,17 @@ use App\Domains\Attachable\Enums\AttachmentType; use App\Domains\Attachable\Models\Attachment; use App\Domains\Auth\Models\User; use App\Domains\Catalog\Enums\CatalogItemType; +use App\Domains\Catalog\Models\Attribute; use App\Domains\Catalog\Models\CatalogItem; use App\Domains\Catalog\Models\Inventory; +use App\Domains\Event\Models\EventDate; use App\Domains\Notification\Events\TicketsAvailable; use App\Domains\Purchase\Models\Purchase; +use App\Domains\Shared\Enums\FieldType; use App\Domains\Tenant\Models\Tenant; +use App\Domains\Ticket\Enums\ValidityTimeType; use App\Domains\Ticket\Exceptions\TicketGenerationException; +use App\Domains\Ticket\Models\ValidityTime; use App\Domains\Ticket\Services\TicketGeneratorService; use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -180,6 +185,56 @@ class TicketGeneratorServiceTest extends TestCase ]); } + public function test_event_date_and_time_window_are_snapshotted_as_a_fixed_window(): void + { + $item = $this->createTicketableItem('scheduled-meal'); + $eventDate = EventDate::query()->create([ + 'tenant_code' => $this->tenant->codigo, + 'date' => '2026-08-20', + 'time_start' => '09:00:00', + 'time_end' => '23:59:59', + ]); + $timeWindow = ValidityTime::query()->create([ + 'type' => ValidityTimeType::TimeWindow, + 'start_time' => '22:00:00', + 'end_time' => '02:00:00', + ]); + $schedule = Attribute::query()->create([ + 'tenant_codigo' => $this->tenant->codigo, + 'codigo' => 'schedule', + 'nombre' => 'Schedule', + 'type' => FieldType::Select, + ]); + $schedule->options()->create([ + 'value' => 'night', + 'label' => '22:00 - 02:00', + 'validity_time_id' => $timeWindow->id, + ]); + $itemSchedule = $item->itemAttributes()->create([ + 'attribute_id' => $schedule->id, + ]); + $inventory = Inventory::query()->create(); + $variant = $item->variants()->create([ + 'event_date_id' => $eventDate->id, + 'inventory_id' => $inventory->id, + ]); + $variant->definitions()->create([ + 'item_attribute_id' => $itemSchedule->id, + 'value' => 'night', + ]); + + $tickets = $this->service->generate($item, $this->user, 2, $variant->id); + + $this->assertCount(2, $tickets); + $this->assertSame(1, $tickets->pluck('validity_time_id')->unique()->count()); + $this->assertNotSame($timeWindow->id, $tickets->first()->validity_time_id); + + $fixedWindow = $tickets->first()->validityTime; + $this->assertSame(ValidityTimeType::FixedWindow, $fixedWindow->type); + $this->assertSame('2026-08-20 22:00:00', $fixedWindow->fixed_starts_at->format('Y-m-d H:i:s')); + $this->assertSame('2026-08-21 02:00:00', $fixedWindow->fixed_expires_at->format('Y-m-d H:i:s')); + } + public function test_marking_a_purchase_as_paid_ignores_items_without_tickets(): void { Event::fake([TicketsAvailable::class]);