refactor(ticket): implement fixed validity time generation for event date and time windows in TicketGeneratorService

This commit is contained in:
2026-08-07 10:43:05 -03:00
parent 717ee5d194
commit 91af233941
22 changed files with 665 additions and 0 deletions

View File

@@ -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.

View File

@@ -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`.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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`.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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`.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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`.

View File

@@ -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.

View File

@@ -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<string, ValidityTime> $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,
]);
}
}

View File

@@ -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.

View File

@@ -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]);