diff --git a/AGENTS.md b/AGENTS.md index 075fcaa..75c5e02 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,18 +8,22 @@ ## Architecture -This project uses a domain-oriented structure under `app/Domains`. +This project uses a domain-oriented structure grouped by business area under +`app/Domains`. Cross-cutting modules live under `app/Shared`. -Each domain must live in: +Business domains must live in: ```text -app/Domains// +app/Domains/// ``` +The current areas are `Core`, `Commerce`, and `Ticketing`. Cross-cutting +modules live in `app/Shared//`. + Supported folders inside each domain: ```text -app/Domains// +app/Domains/// ├── Models/ ├── Controllers/ ├── Requests/ @@ -34,22 +38,28 @@ app/Domains// - Root API routes live in `routes/api.php`. - `routes/api.php` should act as an aggregator only. -- Each domain defines its own routes in `app/Domains//routes/api.php`. +- Each business domain defines its own routes in + `app/Domains///routes/api.php`. +- Shared modules that expose routes define them in + `app/Shared//routes/api.php`. ## Namespaces Use this namespace pattern: ```php -App\Domains\\Models -App\Domains\\Controllers -App\Domains\\Requests -App\Domains\\Resources -App\Domains\\Services -App\Domains\\Policies -App\Domains\\Exceptions +App\Domains\\\Models +App\Domains\\\Controllers +App\Domains\\\Requests +App\Domains\\\Resources +App\Domains\\\Services +App\Domains\\\Policies +App\Domains\\\Exceptions ``` +For cross-cutting modules, use `App\Shared\\...`. Reusable enums +and rules live directly under `App\Shared`. + ## Responsibilities - `Models/`: Eloquent models and model relationships. @@ -80,13 +90,19 @@ App\Domains\\Exceptions - Namespace, folder, and class names must stay aligned. - Route files should be named `api.php`. -## Current Domains +## Current Areas and Modules -- `Tenant` -- `Product` +- `Core`: `Administrator`, `Auth`, `Authorization`, `Bootstrap`, `Client`, + `Menu`, `Staff`, `Tenant`. +- `Commerce`: `Cart`, `Catalog`, `Purchase`, `Sale`. +- `Ticketing`: `Desfile`, `Event`, `FiestaFutbolInfantil`, `Ticket`. +- `Shared`: `Attachable`, `Forms`, `Integration`, `Logging`, `MailTest`, + `Notification`, `StorageTest`, plus shared enums and rules. ## Notes For Future Changes - New features should be added inside the corresponding domain first. -- If a concern does not belong to an existing domain, create a new domain under `app/Domains`. +- Add new business features to the appropriate area under `app/Domains`. +- Keep generic reusable code in `app/Shared`; do not use it as a catch-all for + business features. - Avoid placing new business code in `app/Http/Controllers` or `app/Models`. diff --git a/ShopIt_API_Postman_Collection.json b/ShopIt_API_Postman_Collection.json index c855602..1a5b968 100644 --- a/ShopIt_API_Postman_Collection.json +++ b/ShopIt_API_Postman_Collection.json @@ -2,7 +2,7 @@ "info": { "_postman_id": "76fd6fd2-53b9-4d92-8e02-1ddcc6207fa2", "name": "ShopIt API — Complete", - "description": "Colección canónica generada desde las rutas reales de Laravel. Incluye 137 operaciones HTTP, presets de configuración para cada integración, ejemplos de payload, filtros, archivos y tokens separados para Storefront, Admin App y Scanner.\n\nUso rápido:\n1. Ajustá `base_url` y las credenciales.\n2. Ejecutá el Login de la aplicación correspondiente; el token se guarda automáticamente.\n3. Ajustá los IDs y códigos de las variables de colección.\n\nRegeneración: `php postman/generate-shopit-collection.php`.", + "description": "Colección canónica generada desde las rutas reales de Laravel. Incluye 136 operaciones HTTP, presets de configuración para cada integración, ejemplos de payload, filtros, archivos y tokens separados para Storefront, Admin App y Scanner.\n\nUso rápido:\n1. Ajustá `base_url` y las credenciales.\n2. Ejecutá el Login de la aplicación correspondiente; el token se guarda automáticamente.\n3. Ajustá los IDs y códigos de las variables de colección.\n\nRegeneración: `php postman/generate-shopit-collection.php`.", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "item": [ @@ -678,57 +678,6 @@ }, "response": [] }, - { - "name": "Create Catalog", - "request": { - "method": "POST", - "header": [ - { - "key": "Accept", - "value": "application/json", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "description": "Ruta Laravel: `POST /api/tenants/{tenant:codigo}/catalog-items`\n\nControlador: `App\\Domains\\Catalog\\Controllers\\CatalogController@store`", - "url": { - "raw": "{{base_url}}/api/tenants/{{tenant_code}}/catalog-items?q=demo&page=1", - "host": [ - "{{base_url}}" - ], - "path": [ - "api", - "tenants", - "{{tenant_code}}", - "catalog-items" - ], - "query": [ - { - "key": "q", - "value": "demo" - }, - { - "key": "page", - "value": "1" - } - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"type\": \"standard\",\n \"category_id\": {{category_id}},\n \"slug\": \"producto-demo\",\n \"nombre\": \"Producto Demo\",\n \"descripcion\": \"Producto creado desde Postman\",\n \"precio\": 10000,\n \"inventory_policy\": \"tracked\",\n \"inventory_subject\": \"product\",\n \"max_units_per_user\": 5,\n \"has_tickets\": false,\n \"real_stock\": 100,\n \"images\": [\n \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\"\n ]\n}", - "options": { - "raw": { - "language": "json" - } - } - } - }, - "response": [] - }, { "name": "Get Catalog", "request": { @@ -1907,7 +1856,7 @@ "description": "Consulta, configura o desvincula cualquier integración usando `{{integration_code}}`.", "item": [ { - "name": "List Website Type Integration", + "name": "List Admin Website Type Integration", "request": { "method": "GET", "header": [ @@ -1917,16 +1866,16 @@ "type": "text" } ], - "description": "Ruta Laravel: `GET /api/website-types/{websiteType:codigo}/integrations`\n\nControlador: `App\\Domains\\Integration\\Controllers\\WebsiteTypeIntegrationController@index`\n\nRequiere autenticación Sanctum.", + "description": "Ruta Laravel: `GET /api/admin-website-types/{adminWebsiteType:codigo}/integrations`\n\nControlador: `App\\Domains\\Integration\\Controllers\\AdminWebsiteTypeIntegrationController@index`\n\nRequiere autenticación Sanctum.", "url": { - "raw": "{{base_url}}/api/website-types/{{website_type_code}}/integrations", + "raw": "{{base_url}}/api/admin-website-types/{{admin_website_type_code}}/integrations", "host": [ "{{base_url}}" ], "path": [ "api", - "website-types", - "{{website_type_code}}", + "admin-website-types", + "{{admin_website_type_code}}", "integrations" ] }, @@ -1944,7 +1893,7 @@ "response": [] }, { - "name": "Get Website Type Integration", + "name": "Get Admin Website Type Integration", "request": { "method": "GET", "header": [ @@ -1954,16 +1903,16 @@ "type": "text" } ], - "description": "Ruta Laravel: `GET /api/website-types/{websiteType:codigo}/integrations/{integration_code}`\n\nControlador: `App\\Domains\\Integration\\Controllers\\WebsiteTypeIntegrationController@show`\n\nRequiere autenticación Sanctum.", + "description": "Ruta Laravel: `GET /api/admin-website-types/{adminWebsiteType:codigo}/integrations/{integration_code}`\n\nControlador: `App\\Domains\\Integration\\Controllers\\AdminWebsiteTypeIntegrationController@show`\n\nRequiere autenticación Sanctum.", "url": { - "raw": "{{base_url}}/api/website-types/{{website_type_code}}/integrations/{{integration_code}}", + "raw": "{{base_url}}/api/admin-website-types/{{admin_website_type_code}}/integrations/{{integration_code}}", "host": [ "{{base_url}}" ], "path": [ "api", - "website-types", - "{{website_type_code}}", + "admin-website-types", + "{{admin_website_type_code}}", "integrations", "{{integration_code}}" ] @@ -1997,16 +1946,16 @@ "type": "text" } ], - "description": "Ruta Laravel: `PUT /api/website-types/{websiteType:codigo}/integrations/{integration_code}`\n\nControlador: `App\\Domains\\Integration\\Controllers\\WebsiteTypeIntegrationController@store`\n\nRequiere autenticación Sanctum.", + "description": "Ruta Laravel: `PUT /api/admin-website-types/{adminWebsiteType:codigo}/integrations/{integration_code}`\n\nControlador: `App\\Domains\\Integration\\Controllers\\AdminWebsiteTypeIntegrationController@store`\n\nRequiere autenticación Sanctum.", "url": { - "raw": "{{base_url}}/api/website-types/{{website_type_code}}/integrations/{{integration_code}}", + "raw": "{{base_url}}/api/admin-website-types/{{admin_website_type_code}}/integrations/{{integration_code}}", "host": [ "{{base_url}}" ], "path": [ "api", - "website-types", - "{{website_type_code}}", + "admin-website-types", + "{{admin_website_type_code}}", "integrations", "{{integration_code}}" ] @@ -2034,7 +1983,7 @@ "response": [] }, { - "name": "Delete Website Type Integration", + "name": "Delete Admin Website Type Integration", "request": { "method": "DELETE", "header": [ @@ -2044,16 +1993,16 @@ "type": "text" } ], - "description": "Ruta Laravel: `DELETE /api/website-types/{websiteType:codigo}/integrations/{integration_code}`\n\nControlador: `App\\Domains\\Integration\\Controllers\\WebsiteTypeIntegrationController@destroy`\n\nRequiere autenticación Sanctum.", + "description": "Ruta Laravel: `DELETE /api/admin-website-types/{adminWebsiteType:codigo}/integrations/{integration_code}`\n\nControlador: `App\\Domains\\Integration\\Controllers\\AdminWebsiteTypeIntegrationController@destroy`\n\nRequiere autenticación Sanctum.", "url": { - "raw": "{{base_url}}/api/website-types/{{website_type_code}}/integrations/{{integration_code}}", + "raw": "{{base_url}}/api/admin-website-types/{{admin_website_type_code}}/integrations/{{integration_code}}", "host": [ "{{base_url}}" ], "path": [ "api", - "website-types", - "{{website_type_code}}", + "admin-website-types", + "{{admin_website_type_code}}", "integrations", "{{integration_code}}" ] @@ -2093,16 +2042,16 @@ "type": "text" } ], - "description": "Ruta Laravel: `PUT /api/website-types/{websiteType:codigo}/integrations/{integration_code}`\n\nControlador: `App\\Domains\\Integration\\Controllers\\WebsiteTypeIntegrationController@store`\n\nRequiere autenticación Sanctum.\n\nPreset: `email`. Configura el transporte SMTP usado para el envío de correos.", + "description": "Ruta Laravel: `PUT /api/admin-website-types/{adminWebsiteType:codigo}/integrations/{integration_code}`\n\nControlador: `App\\Domains\\Integration\\Controllers\\AdminWebsiteTypeIntegrationController@store`\n\nRequiere autenticación Sanctum.\n\nPreset: `email`. Configura el transporte SMTP usado para el envío de correos.", "url": { - "raw": "{{base_url}}/api/website-types/{{website_type_code}}/integrations/email", + "raw": "{{base_url}}/api/admin-website-types/{{admin_website_type_code}}/integrations/email", "host": [ "{{base_url}}" ], "path": [ "api", - "website-types", - "{{website_type_code}}", + "admin-website-types", + "{{admin_website_type_code}}", "integrations", "email" ] @@ -2151,16 +2100,16 @@ "type": "text" } ], - "description": "Ruta Laravel: `PUT /api/website-types/{websiteType:codigo}/integrations/{integration_code}`\n\nControlador: `App\\Domains\\Integration\\Controllers\\WebsiteTypeIntegrationController@store`\n\nRequiere autenticación Sanctum.\n\nPreset: `telepagos`. Configura las credenciales productivas de Telepagos.", + "description": "Ruta Laravel: `PUT /api/admin-website-types/{adminWebsiteType:codigo}/integrations/{integration_code}`\n\nControlador: `App\\Domains\\Integration\\Controllers\\AdminWebsiteTypeIntegrationController@store`\n\nRequiere autenticación Sanctum.\n\nPreset: `telepagos`. Configura las credenciales productivas de Telepagos.", "url": { - "raw": "{{base_url}}/api/website-types/{{website_type_code}}/integrations/telepagos", + "raw": "{{base_url}}/api/admin-website-types/{{admin_website_type_code}}/integrations/telepagos", "host": [ "{{base_url}}" ], "path": [ "api", - "website-types", - "{{website_type_code}}", + "admin-website-types", + "{{admin_website_type_code}}", "integrations", "telepagos" ] @@ -2209,16 +2158,16 @@ "type": "text" } ], - "description": "Ruta Laravel: `PUT /api/website-types/{websiteType:codigo}/integrations/{integration_code}`\n\nControlador: `App\\Domains\\Integration\\Controllers\\WebsiteTypeIntegrationController@store`\n\nRequiere autenticación Sanctum.\n\nPreset: `telepagos_homo`. Configura las credenciales del entorno de homologación de Telepagos.", + "description": "Ruta Laravel: `PUT /api/admin-website-types/{adminWebsiteType:codigo}/integrations/{integration_code}`\n\nControlador: `App\\Domains\\Integration\\Controllers\\AdminWebsiteTypeIntegrationController@store`\n\nRequiere autenticación Sanctum.\n\nPreset: `telepagos_homo`. Configura las credenciales del entorno de homologación de Telepagos.", "url": { - "raw": "{{base_url}}/api/website-types/{{website_type_code}}/integrations/telepagos_homo", + "raw": "{{base_url}}/api/admin-website-types/{{admin_website_type_code}}/integrations/telepagos_homo", "host": [ "{{base_url}}" ], "path": [ "api", - "website-types", - "{{website_type_code}}", + "admin-website-types", + "{{admin_website_type_code}}", "integrations", "telepagos_homo" ] @@ -2521,45 +2470,6 @@ }, "response": [] }, - { - "name": "Create Tenant", - "request": { - "method": "POST", - "header": [ - { - "key": "Accept", - "value": "application/json", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "description": "Ruta Laravel: `POST /api/tenants`\n\nControlador: `App\\Domains\\Tenant\\Controllers\\TenantController@store`", - "url": { - "raw": "{{base_url}}/api/tenants", - "host": [ - "{{base_url}}" - ], - "path": [ - "api", - "tenants" - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"client_id\": {{client_id}},\n \"codigo\": \"{{tenant_code}}\",\n \"nombre\": \"Tenant Demo\",\n \"dominio\": \"{{tenant_domain}}\",\n \"site_title\": \"ShopIt Demo\",\n \"primary_color\": \"#111827\",\n \"secondary_color\": \"#2563EB\",\n \"danger_color\": \"#DC2626\",\n \"success_color\": \"#16A34A\",\n \"header_bg_color\": \"#FFFFFF\",\n \"footer_bg_color\": \"#111827\",\n \"header_logo\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\",\n \"footer_logo\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\",\n \"search_product_layout\": \"column_with_image\",\n \"search_group_layout\": \"paginated\",\n \"search_items_per_page\": 12,\n \"display_categories\": true,\n \"display_seach_bar\": true,\n \"display_cart\": true,\n \"cart_editing_policy\": \"full\",\n \"website_type_code\": \"shopit\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - }, - "response": [] - }, { "name": "Get Tenant", "request": { @@ -2586,86 +2496,6 @@ }, "response": [] }, - { - "name": "Update Tenant (PUT)", - "request": { - "method": "PUT", - "header": [ - { - "key": "Accept", - "value": "application/json", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "description": "Ruta Laravel: `PUT /api/tenants/{tenant}`\n\nControlador: `App\\Domains\\Tenant\\Controllers\\TenantController@update`", - "url": { - "raw": "{{base_url}}/api/tenants/{{tenant_id}}", - "host": [ - "{{base_url}}" - ], - "path": [ - "api", - "tenants", - "{{tenant_id}}" - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"nombre\": \"Tenant Demo Actualizado\",\n \"site_title\": \"ShopIt Demo\",\n \"primary_color\": \"#111827\",\n \"cart_editing_policy\": \"full\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - }, - "response": [] - }, - { - "name": "Update Tenant (PATCH)", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Accept", - "value": "application/json", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "description": "Ruta Laravel: `PATCH /api/tenants/{tenant}`\n\nControlador: `App\\Domains\\Tenant\\Controllers\\TenantController@update`", - "url": { - "raw": "{{base_url}}/api/tenants/{{tenant_id}}", - "host": [ - "{{base_url}}" - ], - "path": [ - "api", - "tenants", - "{{tenant_id}}" - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"nombre\": \"Tenant Demo Actualizado\",\n \"site_title\": \"ShopIt Demo\",\n \"primary_color\": \"#111827\",\n \"cart_editing_policy\": \"full\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - }, - "response": [] - }, { "name": "Delete Tenant", "request": { @@ -6268,7 +6098,7 @@ "type": "string" }, { - "key": "website_type_code", + "key": "storefront_website_type_code", "value": "shopit", "type": "string" }, @@ -6381,6 +6211,10 @@ "key": "s3_path", "value": "postman/test-file.png", "type": "string" + }, + { + "key": "admin_website_type_code", + "value": "shopit" } ] } diff --git a/app/Domains/Catalog/Requests/StoreCatalogItemRequest.php b/app/Domains/Catalog/Requests/StoreCatalogItemRequest.php deleted file mode 100644 index 4827d96..0000000 --- a/app/Domains/Catalog/Requests/StoreCatalogItemRequest.php +++ /dev/null @@ -1,145 +0,0 @@ - */ - public function rules(): array - { - $tenantCode = $this->route('tenant')?->codigo; - $type = $this->input('type', CatalogItemType::Standard->value); - $isBundle = $type === CatalogItemType::Bundle->value; - - return [ - 'tenant_code' => ['prohibited'], - 'type' => ['sometimes', Rule::enum(CatalogItemType::class)], - 'category_id' => [ - 'sometimes', - 'nullable', - 'integer', - Rule::exists('categorias', 'id')->where( - fn ($query) => $query->where('tenant_code', $tenantCode) - ), - ], - 'brand_id' => [ - 'sometimes', - 'nullable', - 'integer', - Rule::exists('brands', 'id')->where( - fn ($query) => $query->where('tenant_codigo', $tenantCode) - ), - ], - 'slug' => [ - 'required', - 'string', - 'max:255', - Rule::unique('catalog_items', 'slug')->where( - fn ($query) => $query->where('tenant_code', $tenantCode) - ), - ], - 'nombre' => ['required', 'string', 'max:255'], - 'group_order' => ['sometimes', 'integer', 'min:0'], - 'descripcion' => ['sometimes', 'nullable', 'string'], - 'precio' => ['required', 'numeric', 'min:0'], - 'inventory_policy' => [Rule::prohibitedIf($isBundle), 'sometimes', Rule::enum(InventoryPolicy::class)], - 'inventory_subject' => ['sometimes', Rule::enum(InventorySubject::class)], - 'max_units_per_user' => ['sometimes', 'nullable', 'integer', 'min:1'], - 'has_tickets' => [Rule::prohibitedIf($isBundle), 'sometimes', 'boolean'], - 'real_stock' => [Rule::prohibitedIf($isBundle), 'sometimes', 'integer', 'min:0'], - 'inventory_id' => ['prohibited'], - 'reserved_stock' => ['prohibited'], - 'sold_units' => ['prohibited'], - 'attribute_codes' => [Rule::prohibitedIf($isBundle), 'sometimes', 'array'], - 'attribute_codes.*' => [ - 'required', - 'string', - 'distinct', - Rule::exists('attribute', 'codigo')->where( - fn ($query) => $query->where('tenant_codigo', $tenantCode) - ), - ], - 'multi_select_attribute_codes' => [Rule::prohibitedIf($isBundle), 'sometimes', 'array'], - 'multi_select_attribute_codes.*' => [ - 'required', - 'string', - 'distinct', - Rule::exists('attribute', 'codigo')->where( - fn ($query) => $query->where('tenant_codigo', $tenantCode) - ), - ], - 'hidden_attribute_codes' => [Rule::prohibitedIf($isBundle), 'sometimes', 'array'], - 'hidden_attribute_codes.*' => [ - 'required', - 'string', - 'distinct', - Rule::exists('attribute', 'codigo')->where( - fn ($query) => $query->where('tenant_codigo', $tenantCode) - ), - ], - 'images' => ['sometimes', 'array'], - 'images.*' => ['required', new ImageOrBase64Rule], - 'variants' => [Rule::prohibitedIf($isBundle), 'sometimes', 'array'], - 'variants.*.real_stock' => ['sometimes', 'integer', 'min:0'], - 'variants.*.descripcion' => ['sometimes', 'nullable', 'string'], - 'variants.*.precio' => ['sometimes', 'nullable', 'numeric', 'min:0', 'max:99999999.99'], - 'variants.*.event_date_id' => [ - 'sometimes', - 'nullable', - 'integer', - Rule::exists('event_dates', 'id')->where( - fn ($query) => $query->where('tenant_code', $tenantCode) - ), - ], - 'variants.*.event_date_ids' => ['sometimes', 'array', 'min:1'], - 'variants.*.event_date_ids.*' => [ - 'required', - 'integer', - 'distinct', - Rule::exists('event_dates', 'id')->where( - fn ($query) => $query->where('tenant_code', $tenantCode) - ), - ], - 'variants.*.inventory_id' => ['prohibited'], - 'variants.*.reserved_stock' => ['prohibited'], - 'variants.*.sold_units' => ['prohibited'], - 'variants.*.values' => ['sometimes', 'array'], - 'variants.*.values.*' => ['nullable'], - 'variants.*.values.*.*' => ['required', 'string'], - 'variants.*.images' => ['sometimes', 'array'], - 'variants.*.images.*' => ['required', new ImageOrBase64Rule], - 'components' => [ - Rule::requiredIf($isBundle), - Rule::prohibitedIf(! $isBundle), - 'array', - 'min:1', - ], - 'components.*.catalog_item_id' => [ - 'required', - 'integer', - Rule::exists('catalog_items', 'id')->where( - fn ($query) => $query->where('tenant_code', $tenantCode) - ), - ], - 'components.*.variant_id' => [ - 'sometimes', - 'nullable', - 'integer', - Rule::exists('variantes', 'id'), - ], - 'components.*.quantity' => ['required', 'integer', 'min:1'], - ]; - } -} diff --git a/app/Domains/Catalog/Resources/CatalogItemResource.php b/app/Domains/Catalog/Resources/CatalogItemResource.php deleted file mode 100644 index 0012c76..0000000 --- a/app/Domains/Catalog/Resources/CatalogItemResource.php +++ /dev/null @@ -1,50 +0,0 @@ - */ - public function toArray(Request $request): array - { - return [ - 'id' => $this->id, - 'type' => $this->type->value, - 'category_id' => $this->category_id, - 'brand_id' => $this->brand_id, - 'slug' => $this->slug, - 'nombre' => $this->nombre, - 'descripcion' => $this->descripcion, - 'precio' => $this->precio, - 'inventory_policy' => $this->inventory_policy?->value, - 'inventory_subject' => $this->inventory_subject->value, - 'max_units_per_user' => $this->max_units_per_user, - 'has_tickets' => $this->has_tickets, - 'real_stock' => $this->whenLoaded('inventory', fn () => $this->inventory?->real_stock), - 'images' => $this->whenLoaded('attachments', fn () => $this->attachments - ->map(fn ($attachment) => $attachment->getTemporaryUrl(1440)) - ->values()), - 'variants' => $this->whenLoaded('variants', fn () => $this->variants - ->map(fn ($variant) => [ - 'id' => $variant->id, - 'event_date_id' => $variant->event_date_id, - 'event_date' => $variant->eventDate?->date?->format('Y-m-d'), - 'event_date_ids' => $variant->selectedEventDates()->pluck('id')->values(), - 'event_dates' => $variant->selectedEventDates()->map(fn ($eventDate): string => $eventDate->date->format('Y-m-d'))->values(), - 'descripcion' => $variant->getDescription(), - 'precio' => number_format($variant->getPrice(), 2, '.', ''), - 'real_stock' => $variant->inventory?->real_stock, - 'values' => $variant->selectorOptions($this->itemAttributes), - 'images' => $variant->attachments - ->map(fn ($attachment) => $attachment->getTemporaryUrl(1440)) - ->values(), - ]) - ->values()), - ]; - } -} diff --git a/app/Domains/Cart/Controllers/CartController.php b/app/Domains/Commerce/Cart/Controllers/CartController.php similarity index 87% rename from app/Domains/Cart/Controllers/CartController.php rename to app/Domains/Commerce/Cart/Controllers/CartController.php index 368a77c..d576ce6 100644 --- a/app/Domains/Cart/Controllers/CartController.php +++ b/app/Domains/Commerce/Cart/Controllers/CartController.php @@ -1,13 +1,13 @@ whereKey($catalogItemId) - ->where('tenant_code', $this->tenant_codigo); + ->forTenantCatalog($this->tenant()->firstOrFail()); if ($lockForUpdate) { $catalogItemQuery->lockForUpdate(); diff --git a/app/Domains/Cart/Models/CartItem.php b/app/Domains/Commerce/Cart/Models/CartItem.php similarity index 89% rename from app/Domains/Cart/Models/CartItem.php rename to app/Domains/Commerce/Cart/Models/CartItem.php index 3841533..96db6f3 100644 --- a/app/Domains/Cart/Models/CartItem.php +++ b/app/Domains/Commerce/Cart/Models/CartItem.php @@ -1,9 +1,9 @@ user()->tenant()->firstOrFail(); - abort_unless($tenant->website_type_code === 'onticket', 404); + abort_unless(in_array($tenant->storefront_website_type_code, ['onticket', 'onticket_multi_event'], true), 404); return $tenant; } diff --git a/app/Domains/Catalog/Controllers/CatalogController.php b/app/Domains/Commerce/Catalog/Controllers/CatalogController.php similarity index 75% rename from app/Domains/Catalog/Controllers/CatalogController.php rename to app/Domains/Commerce/Catalog/Controllers/CatalogController.php index e949d00..0dce199 100644 --- a/app/Domains/Catalog/Controllers/CatalogController.php +++ b/app/Domains/Commerce/Catalog/Controllers/CatalogController.php @@ -1,27 +1,25 @@ json($featuredGroups->map( fn (FeaturedGroup $featuredGroup): array => (new CatalogFeaturedGroupResource( $featuredGroup, - $featuredGroupService->itemsResponse($featuredGroup, 1, $this->userId($request)), + $featuredGroupService->itemsResponse($featuredGroup, $tenant, 1, $this->userId($request)), ))->resolve() )); } @@ -102,6 +100,7 @@ class CatalogController extends Controller return response()->json($featuredGroupService->itemsResponse( $featuredGroup, + $tenant, $page, $this->userId($request), )); @@ -114,7 +113,8 @@ class CatalogController extends Controller CatalogService $catalogService, CatalogItemAllowanceService $allowances, ): CatalogItemDetailResource { - abort_unless($catalogItem->tenant_code === $tenant->codigo, 404); + abort_unless($catalogItem->tenant_code === $tenant->codigo + && ($tenant->active_event_id === null || $catalogItem->event_id === $tenant->active_event_id), 404); $variantId = $request->validated('variant_id'); @@ -134,7 +134,8 @@ class CatalogController extends Controller VariantSelectionService $variantSelectionService, CartService $cartService, ): CatalogVariantOptionsResource { - abort_unless($catalogItem->tenant_code === $tenant->codigo, 404); + abort_unless($catalogItem->tenant_code === $tenant->codigo + && ($tenant->active_event_id === null || $catalogItem->event_id === $tenant->active_event_id), 404); $includedVariantId = null; $cartItemId = $request->validated('cart_item_id'); @@ -163,21 +164,6 @@ class CatalogController extends Controller ); } - public function store( - StoreCatalogItemRequest $request, - Tenant $tenant, - CatalogService $catalogService, - ): JsonResponse { - $catalogItem = $catalogService->create([ - ...$request->validated(), - 'tenant_code' => $tenant->codigo, - ]); - - return CatalogItemResource::make($catalogItem) - ->response() - ->setStatusCode(201); - } - private function userId(Request $request): ?int { $userId = $request->user()?->getAuthIdentifier() ?? Auth::guard('sanctum')->id(); diff --git a/app/Domains/Catalog/Enums/CatalogItemType.php b/app/Domains/Commerce/Catalog/Enums/CatalogItemType.php similarity index 84% rename from app/Domains/Catalog/Enums/CatalogItemType.php rename to app/Domains/Commerce/Catalog/Enums/CatalogItemType.php index b9d45b6..cafa547 100644 --- a/app/Domains/Catalog/Enums/CatalogItemType.php +++ b/app/Domains/Commerce/Catalog/Enums/CatalogItemType.php @@ -1,6 +1,6 @@ 0, ]; + protected static function booted(): void + { + static::creating(function (CatalogItem $item): void { + if ($item->event_id !== null || $item->tenant_code === null) { + return; + } + + $item->event_id = Tenant::query() + ->where('codigo', $item->tenant_code) + ->value('active_event_id'); + }); + } + protected function casts(): array { return [ + 'event_id' => 'integer', 'category_id' => 'integer', 'brand_id' => 'integer', 'inventory_id' => 'integer', 'type' => CatalogItemType::class, 'group_order' => 'integer', 'precio' => 'decimal:2', + 'sales_end_at' => 'datetime', 'inventory_policy' => InventoryPolicy::class, 'inventory_subject' => InventorySubject::class, 'max_units_per_user' => 'integer', @@ -73,6 +90,21 @@ class CatalogItem extends Model return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo'); } + /** @return BelongsTo */ + public function event(): BelongsTo + { + return $this->belongsTo(Event::class); + } + + /** @param Builder $query */ + public function scopeForTenantCatalog(Builder $query, Tenant $tenant): Builder + { + return $query + ->where('catalog_items.tenant_code', $tenant->codigo) + ->when($tenant->active_event_id !== null, fn (Builder $query): Builder => $query + ->where('catalog_items.event_id', $tenant->active_event_id)); + } + /** @return BelongsTo */ public function category(): BelongsTo { @@ -161,6 +193,10 @@ class CatalogItem extends Model public function isAvailable(): bool { + if (! $this->isSaleOpen()) { + return false; + } + if ($this->type === CatalogItemType::Bundle) { $availableStock = $this->availableStock(); @@ -178,6 +214,9 @@ class CatalogItem extends Model public function scopeWhereAvailable(Builder $query): Builder { return $query->where(function (Builder $query): void { + $query->whereNull('catalog_items.sales_end_at') + ->orWhere('catalog_items.sales_end_at', '>', now()); + })->where(function (Builder $query): void { $query ->where(function (Builder $unlimitedQuery): void { $unlimitedQuery @@ -217,10 +256,16 @@ class CatalogItem extends Model }); } + public function isSaleOpen(): bool + { + return $this->sales_end_at === null || now()->lt($this->sales_end_at); + } + /** @return Collection */ public function visibleVariants(?int $includedVariantId = null): Collection { return $this->variants + ->each(fn (Variant $variant) => $variant->setRelation('catalogItem', $this)) ->filter(fn (Variant $variant): bool => $variant->hasOnlyActiveEventDates() && (($includedVariantId !== null && $variant->id === $includedVariantId) || ($variant->isSellable() && ( diff --git a/app/Domains/Catalog/Models/Category.php b/app/Domains/Commerce/Catalog/Models/Category.php similarity index 93% rename from app/Domains/Catalog/Models/Category.php rename to app/Domains/Commerce/Catalog/Models/Category.php index c0ed114..7f2d1d2 100644 --- a/app/Domains/Catalog/Models/Category.php +++ b/app/Domains/Commerce/Catalog/Models/Category.php @@ -1,9 +1,9 @@ sales_disabled_at === null && $this->replaced_by_variant_id === null + && $this->catalogItem->isSaleOpen() && $this->hasOnlyActiveEventDates(); } diff --git a/app/Domains/Catalog/Models/VariantDefinition.php b/app/Domains/Commerce/Catalog/Models/VariantDefinition.php similarity index 93% rename from app/Domains/Catalog/Models/VariantDefinition.php rename to app/Domains/Commerce/Catalog/Models/VariantDefinition.php index 2c4fe38..cbfd104 100644 --- a/app/Domains/Catalog/Models/VariantDefinition.php +++ b/app/Domains/Commerce/Catalog/Models/VariantDefinition.php @@ -1,6 +1,6 @@ $option->validity_time_id, 'validity_time' => $option->validityTime === null ? null - : ValidityTimeResource::make($option->validityTime), + : ValidityTimeResource::make($option->validityTime, $this->tenant->timezone), 'metadata' => $option->metadata, ]) ->values(); diff --git a/app/Domains/Catalog/Resources/CatalogSearchItemResource.php b/app/Domains/Commerce/Catalog/Resources/CatalogSearchItemResource.php similarity index 90% rename from app/Domains/Catalog/Resources/CatalogSearchItemResource.php rename to app/Domains/Commerce/Catalog/Resources/CatalogSearchItemResource.php index 86e13b0..e02e6a5 100644 --- a/app/Domains/Catalog/Resources/CatalogSearchItemResource.php +++ b/app/Domains/Commerce/Catalog/Resources/CatalogSearchItemResource.php @@ -1,11 +1,11 @@ where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->whereAvailable() ->where(function (Builder $query) use ($containsPattern): void { $query @@ -287,7 +287,7 @@ class CatalogService int $page, ): LengthAwarePaginator { return CatalogItem::query() - ->where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->where('category_id', $category->id) ->whereAvailable() ->with([ @@ -369,6 +369,8 @@ class CatalogService $componentItem = CatalogItem::query() ->whereKey($catalogItemId) ->where('tenant_code', $bundle->tenant_code) + ->when($bundle->event_id !== null, fn (Builder $query): Builder => $query + ->where('event_id', $bundle->event_id)) ->first(); if ($componentItem === null) { diff --git a/app/Domains/Catalog/Services/ExpireStockReservationsService.php b/app/Domains/Commerce/Catalog/Services/ExpireStockReservationsService.php similarity index 95% rename from app/Domains/Catalog/Services/ExpireStockReservationsService.php rename to app/Domains/Commerce/Catalog/Services/ExpireStockReservationsService.php index 58bf5a8..565626b 100644 --- a/app/Domains/Catalog/Services/ExpireStockReservationsService.php +++ b/app/Domains/Commerce/Catalog/Services/ExpireStockReservationsService.php @@ -1,11 +1,11 @@ group_layout !== GroupLayout::Paginated) { - $query = $this->itemsQuery($featuredGroup); + $query = $this->itemsQuery($featuredGroup, $tenant); if ($featuredGroup->group_layout === GroupLayout::Single) { $query->limit(1); @@ -35,7 +36,7 @@ class FeaturedGroupService return CatalogFeaturedItemResource::collection($items)->resolve(); } - $paginator = $this->paginateItems($featuredGroup, $page); + $paginator = $this->paginateItems($featuredGroup, $tenant, $page); $this->attachGroup($paginator->getCollection(), $featuredGroup); $this->allowances->attach($paginator->getCollection(), $userId); @@ -45,10 +46,10 @@ class FeaturedGroupService } /** @return Builder */ - private function itemsQuery(FeaturedGroup $featuredGroup): Builder + private function itemsQuery(FeaturedGroup $featuredGroup, Tenant $tenant): Builder { $query = CatalogItem::query() - ->where('catalog_items.tenant_code', $featuredGroup->tenant_code) + ->forTenantCatalog($tenant) ->whereAvailable() ->where(function (Builder $query): void { $query @@ -90,9 +91,10 @@ class FeaturedGroupService private function paginateItems( FeaturedGroup $featuredGroup, + Tenant $tenant, int $page, ): LengthAwarePaginator { - $paginator = $this->itemsQuery($featuredGroup)->paginate( + $paginator = $this->itemsQuery($featuredGroup, $tenant)->paginate( perPage: self::ITEMS_PER_PAGE, pageName: 'page', page: $page, diff --git a/app/Domains/Catalog/Services/OnTicketFeaturedGroupService.php b/app/Domains/Commerce/Catalog/Services/OnTicketFeaturedGroupService.php similarity index 89% rename from app/Domains/Catalog/Services/OnTicketFeaturedGroupService.php rename to app/Domains/Commerce/Catalog/Services/OnTicketFeaturedGroupService.php index 6b2300a..e5e99e1 100644 --- a/app/Domains/Catalog/Services/OnTicketFeaturedGroupService.php +++ b/app/Domains/Commerce/Catalog/Services/OnTicketFeaturedGroupService.php @@ -1,13 +1,13 @@ group(function (): void { @@ -13,7 +13,6 @@ Route::prefix('tenants/{tenant:codigo}')->group(function (): void { ->name('categories.show'); Route::get('catalog-items/{catalogItem}', [CatalogController::class, 'show']); Route::post('catalog-items/{catalogItem}/variant-options', [CatalogController::class, 'variantOptions']); - Route::post('catalog-items', [CatalogController::class, 'store']); }); require __DIR__.'/adminapp.php'; diff --git a/app/Domains/Purchase/Controllers/PurchaseController.php b/app/Domains/Commerce/Purchase/Controllers/PurchaseController.php similarity index 93% rename from app/Domains/Purchase/Controllers/PurchaseController.php rename to app/Domains/Commerce/Purchase/Controllers/PurchaseController.php index b175a48..f203613 100644 --- a/app/Domains/Purchase/Controllers/PurchaseController.php +++ b/app/Domains/Commerce/Purchase/Controllers/PurchaseController.php @@ -1,17 +1,17 @@ route('tenant'); - $displayImage = ! $tenant instanceof Tenant || $tenant->display_cart_item_images; + $usesEventSummary = $tenant instanceof Tenant + && $tenant->storefrontWebsiteType?->checkout_summary_type + === StorefrontWebsiteType::CHECKOUT_SUMMARY_EVENT; + $displayImage = ! $tenant instanceof Tenant + || (! $usesEventSummary && $tenant->display_cart_item_images); $imageUrl = $displayImage ? $this->imageAttachment?->getTemporaryUrl(1440) @@ -31,7 +36,7 @@ class PurchaseItemResource extends JsonResource 'nombre' => $this->item_nombre, 'descripcion' => $this->descripcion, 'slug' => $this->slug, - 'imagen' => $imageUrl, + 'imagen' => $this->when(! $usesEventSummary, $imageUrl), 'attributes' => $this->variant_attributes ?? [], ], ]; diff --git a/app/Domains/Purchase/Resources/PurchaseResource.php b/app/Domains/Commerce/Purchase/Resources/PurchaseResource.php similarity index 82% rename from app/Domains/Purchase/Resources/PurchaseResource.php rename to app/Domains/Commerce/Purchase/Resources/PurchaseResource.php index fd119fa..2e98a2e 100644 --- a/app/Domains/Purchase/Resources/PurchaseResource.php +++ b/app/Domains/Commerce/Purchase/Resources/PurchaseResource.php @@ -1,10 +1,13 @@ resource->getAttribute('tickets_count') : null; $paymentVerification = $this->resolvePaymentVerification(); + $tenant = $request->route('tenant'); + $usesEventSummary = $tenant instanceof Tenant + && $tenant->storefrontWebsiteType?->checkout_summary_type + === StorefrontWebsiteType::CHECKOUT_SUMMARY_EVENT; + /** @var Event|null $checkoutEvent */ + $checkoutEvent = $usesEventSummary + ? $items + ->map(fn (PurchaseItem $item) => $item->sourceCatalogItem?->event) + ->filter() + ->unique('id') + ->first() + : null; $subtotal = $items->isNotEmpty() ? $items->reduce( @@ -64,6 +79,13 @@ class PurchaseResource extends JsonResource 'email' => $this->email, 'items_source' => $items->isNotEmpty() ? 'purchase' : null, 'items' => PurchaseItemResource::collection($items), + 'event' => $this->when($usesEventSummary, fn () => $checkoutEvent === null + ? null + : [ + 'id' => $checkoutEvent->id, + 'title' => $checkoutEvent->title, + 'image' => $checkoutEvent->attachment?->getTemporaryUrl(1440), + ]), 'tickets_count' => $this->when($ticketsCount !== null, $ticketsCount), 'has_generated_tickets' => $this->when($ticketsCount !== null, $ticketsCount > 0), 'payment_verification' => $this->when($paymentVerification !== null, $paymentVerification), diff --git a/app/Domains/Purchase/Services/Checkout/CatalogSelectionResolver.php b/app/Domains/Commerce/Purchase/Services/Checkout/CatalogSelectionResolver.php similarity index 85% rename from app/Domains/Purchase/Services/Checkout/CatalogSelectionResolver.php rename to app/Domains/Commerce/Purchase/Services/Checkout/CatalogSelectionResolver.php index 15e09d2..24b2484 100644 --- a/app/Domains/Purchase/Services/Checkout/CatalogSelectionResolver.php +++ b/app/Domains/Commerce/Purchase/Services/Checkout/CatalogSelectionResolver.php @@ -1,11 +1,11 @@ whereKey($catalogItemId) - ->where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->lockForUpdate() ->first(); @@ -28,6 +28,12 @@ class CatalogSelectionResolver throw new NotFoundHttpException('Catalog item not found for tenant.'); } + if (! $catalogItem->isSaleOpen()) { + throw ValidationException::withMessages([ + "{$fieldPrefix}.catalog_item_id" => ['La venta de este producto finalizó.'], + ]); + } + if ($catalogItem->isBundle()) { if ($variantId !== null) { throw ValidationException::withMessages([ diff --git a/app/Domains/Purchase/Services/Checkout/CompleteCheckoutService.php b/app/Domains/Commerce/Purchase/Services/Checkout/CompleteCheckoutService.php similarity index 96% rename from app/Domains/Purchase/Services/Checkout/CompleteCheckoutService.php rename to app/Domains/Commerce/Purchase/Services/Checkout/CompleteCheckoutService.php index 61a4062..c30f299 100644 --- a/app/Domains/Purchase/Services/Checkout/CompleteCheckoutService.php +++ b/app/Domains/Commerce/Purchase/Services/Checkout/CompleteCheckoutService.php @@ -1,11 +1,11 @@ loadMissing('tenant.storefrontWebsiteType'); + $usesEventSummary = $purchase->tenant?->storefrontWebsiteType?->checkout_summary_type + === StorefrontWebsiteType::CHECKOUT_SUMMARY_EVENT; + $relations = [ + 'tenant', + 'items.sourceCatalogItem.event.attachment', + 'stockReservation', + ]; + + if (! $usesEventSummary) { + $relations[] = 'items.imageAttachment'; + } + + if ( + $purchase->status === Purchase::STATUS_IN_REVIEW + && $purchase->payment_method === 'transfer' + ) { + $relations[] = 'telepagosPaymentCandidates.payment'; + } + + return $purchase->load($relations); + } +} diff --git a/app/Domains/Purchase/Services/Checkout/ReleaseCheckoutService.php b/app/Domains/Commerce/Purchase/Services/Checkout/ReleaseCheckoutService.php similarity index 95% rename from app/Domains/Purchase/Services/Checkout/ReleaseCheckoutService.php rename to app/Domains/Commerce/Purchase/Services/Checkout/ReleaseCheckoutService.php index e64ef79..e71fbd2 100644 --- a/app/Domains/Purchase/Services/Checkout/ReleaseCheckoutService.php +++ b/app/Domains/Commerce/Purchase/Services/Checkout/ReleaseCheckoutService.php @@ -1,13 +1,13 @@ __('api.purchase.catalog_item_wrong_tenant'), ]); } + + if ($tenant->active_event_id !== null + && $item->catalogItem->event_id !== $tenant->active_event_id) { + throw ValidationException::withMessages([ + 'cart_id' => __('api.purchase.catalog_item_missing'), + ]); + } } } diff --git a/app/Domains/Purchase/Services/CheckoutService.php b/app/Domains/Commerce/Purchase/Services/CheckoutService.php similarity index 82% rename from app/Domains/Purchase/Services/CheckoutService.php rename to app/Domains/Commerce/Purchase/Services/CheckoutService.php index 882e235..b13c7c9 100644 --- a/app/Domains/Purchase/Services/CheckoutService.php +++ b/app/Domains/Commerce/Purchase/Services/CheckoutService.php @@ -1,13 +1,13 @@ middleware('auth:sanctum')->group(function (): void { diff --git a/app/Domains/Sale/Controllers/AdminApp/SaleController.php b/app/Domains/Commerce/Sale/Controllers/AdminApp/SaleController.php similarity index 82% rename from app/Domains/Sale/Controllers/AdminApp/SaleController.php rename to app/Domains/Commerce/Sale/Controllers/AdminApp/SaleController.php index 71fe5f6..bb94f0d 100644 --- a/app/Domains/Sale/Controllers/AdminApp/SaleController.php +++ b/app/Domains/Commerce/Sale/Controllers/AdminApp/SaleController.php @@ -1,18 +1,18 @@ group(function (): void { diff --git a/app/Domains/Auth/routes/api.php b/app/Domains/Core/Auth/routes/api.php similarity index 60% rename from app/Domains/Auth/routes/api.php rename to app/Domains/Core/Auth/routes/api.php index 5b25226..57287f2 100644 --- a/app/Domains/Auth/routes/api.php +++ b/app/Domains/Core/Auth/routes/api.php @@ -1,14 +1,14 @@ group(function (): void { diff --git a/app/Domains/Authorization/Enums/PermissionCode.php b/app/Domains/Core/Authorization/Enums/PermissionCode.php similarity index 63% rename from app/Domains/Authorization/Enums/PermissionCode.php rename to app/Domains/Core/Authorization/Enums/PermissionCode.php index fb520d0..435ff9f 100644 --- a/app/Domains/Authorization/Enums/PermissionCode.php +++ b/app/Domains/Core/Authorization/Enums/PermissionCode.php @@ -1,6 +1,6 @@ */ public function toArray(Request $request): array { - /** @var WebsiteType $websiteType */ + /** @var AdminWebsiteType $websiteType */ $websiteType = $this->resource['website_type']; return [ - 'website_type_code' => $websiteType->codigo, + 'admin_website_type_code' => $websiteType->codigo, 'site_title' => $websiteType->site_title, 'primary_color' => $websiteType->primary_color, 'secondary_color' => $websiteType->secondary_color, diff --git a/app/Domains/Bootstrap/Resources/ScannerBootstrapResource.php b/app/Domains/Core/Bootstrap/Resources/ScannerBootstrapResource.php similarity index 61% rename from app/Domains/Bootstrap/Resources/ScannerBootstrapResource.php rename to app/Domains/Core/Bootstrap/Resources/ScannerBootstrapResource.php index 74b0e16..810274a 100644 --- a/app/Domains/Bootstrap/Resources/ScannerBootstrapResource.php +++ b/app/Domains/Core/Bootstrap/Resources/ScannerBootstrapResource.php @@ -1,5 +1,5 @@ WebsiteType::query() + 'website_type' => AdminWebsiteType::query() ->with(['siteLogo', 'footerLogo', 'favicon']) ->where('dominio', $domain) ->firstOrFail(), diff --git a/app/Domains/Bootstrap/Services/ScannerBootstrapService.php b/app/Domains/Core/Bootstrap/Services/ScannerBootstrapService.php similarity index 56% rename from app/Domains/Bootstrap/Services/ScannerBootstrapService.php rename to app/Domains/Core/Bootstrap/Services/ScannerBootstrapService.php index 4a088a1..ae5276a 100644 --- a/app/Domains/Bootstrap/Services/ScannerBootstrapService.php +++ b/app/Domains/Core/Bootstrap/Services/ScannerBootstrapService.php @@ -1,16 +1,16 @@ WebsiteType::query() + 'website_type' => AdminWebsiteType::query() ->with(['siteLogo', 'footerLogo', 'favicon']) ->where('scanner_domain', $domain) ->firstOrFail(), diff --git a/app/Domains/Bootstrap/Services/TenantBootstrapService.php b/app/Domains/Core/Bootstrap/Services/TenantBootstrapService.php similarity index 73% rename from app/Domains/Bootstrap/Services/TenantBootstrapService.php rename to app/Domains/Core/Bootstrap/Services/TenantBootstrapService.php index b4c6352..9ccb8aa 100644 --- a/app/Domains/Bootstrap/Services/TenantBootstrapService.php +++ b/app/Domains/Core/Bootstrap/Services/TenantBootstrapService.php @@ -1,11 +1,11 @@ $query->where('codigo', RoleCode::User->value) ), - 'categories' => fn ($query) => $query->orderBy('nombre'), + $tenant->storefront_website_type_code === 'onticket_multi_event' + ? 'eventCategories' : 'categories' => fn ($query) => $query->orderBy('nombre'), ] ); } diff --git a/app/Domains/Bootstrap/documentacion/README.md b/app/Domains/Core/Bootstrap/documentacion/README.md similarity index 100% rename from app/Domains/Bootstrap/documentacion/README.md rename to app/Domains/Core/Bootstrap/documentacion/README.md diff --git a/app/Domains/Bootstrap/routes/adminapp.php b/app/Domains/Core/Bootstrap/routes/adminapp.php similarity index 65% rename from app/Domains/Bootstrap/routes/adminapp.php rename to app/Domains/Core/Bootstrap/routes/adminapp.php index 3cf6c66..e5be4bc 100644 --- a/app/Domains/Bootstrap/routes/adminapp.php +++ b/app/Domains/Core/Bootstrap/routes/adminapp.php @@ -1,6 +1,6 @@ tenant()->firstOrFail(); return $this->tenantInformationService->load($tenant, [ - 'websiteType.extras', + 'storefrontWebsiteType.extras', ]); } } diff --git a/app/Domains/Core/Tenant/Controllers/TenantController.php b/app/Domains/Core/Tenant/Controllers/TenantController.php new file mode 100644 index 0000000..d4dc588 --- /dev/null +++ b/app/Domains/Core/Tenant/Controllers/TenantController.php @@ -0,0 +1,42 @@ +latest() + ->paginateFromRequest(); + + $this->tenantInformationService->loadMany($tenants->getCollection()); + + return TenantResource::collection($tenants)->response(); + } + + public function show(Tenant $tenant): TenantResource + { + return TenantResource::make( + $this->tenantInformationService->load($tenant) + ); + } + + public function destroy(Tenant $tenant): Response + { + $tenant->delete(); + + return response()->noContent(); + } +} diff --git a/app/Domains/Tenant/Enums/CartEditingPolicy.php b/app/Domains/Core/Tenant/Enums/CartEditingPolicy.php similarity index 93% rename from app/Domains/Tenant/Enums/CartEditingPolicy.php rename to app/Domains/Core/Tenant/Enums/CartEditingPolicy.php index c90fc05..56fed3c 100644 --- a/app/Domains/Tenant/Enums/CartEditingPolicy.php +++ b/app/Domains/Core/Tenant/Enums/CartEditingPolicy.php @@ -1,6 +1,6 @@ */ + /** @return HasMany */ public function integrations(): HasMany { - return $this->hasMany(WebsiteTypeIntegration::class, 'website_type_code', 'codigo'); + return $this->hasMany(AdminWebsiteTypeIntegration::class, 'admin_website_type_code', 'codigo'); } /** @@ -67,19 +67,11 @@ class WebsiteType extends Model return $this->belongsTo(Attachment::class, 'favicon_id'); } - /** - * @return HasMany - */ - public function extras(): HasMany - { - return $this->hasMany(WebsiteTypeExtra::class, 'website_type_code', 'codigo'); - } - /** * @return HasMany */ public function tenants(): HasMany { - return $this->hasMany(Tenant::class, 'website_type_code', 'codigo'); + return $this->hasMany(Tenant::class, 'admin_website_type_code', 'codigo'); } } diff --git a/app/Domains/Tenant/Models/SocialMedia.php b/app/Domains/Core/Tenant/Models/SocialMedia.php similarity index 60% rename from app/Domains/Tenant/Models/SocialMedia.php rename to app/Domains/Core/Tenant/Models/SocialMedia.php index 616a543..b11568f 100644 --- a/app/Domains/Tenant/Models/SocialMedia.php +++ b/app/Domains/Core/Tenant/Models/SocialMedia.php @@ -1,7 +1,8 @@ withTimestamps() ->orderByPivot('orden'); } + + /** @return BelongsToMany */ + public function events(): BelongsToMany + { + return $this->belongsToMany( + Event::class, + 'event_social_media', + 'social_media_code', + 'event_id', + 'code', + 'id', + )->withPivot(['url', 'orden'])->withTimestamps()->orderByPivot('orden'); + } } diff --git a/app/Domains/Core/Tenant/Models/StorefrontWebsiteType.php b/app/Domains/Core/Tenant/Models/StorefrontWebsiteType.php new file mode 100644 index 0000000..2f31a91 --- /dev/null +++ b/app/Domains/Core/Tenant/Models/StorefrontWebsiteType.php @@ -0,0 +1,36 @@ + self::CHECKOUT_SUMMARY_DEFAULT, + ]; + + /** @return HasMany */ + public function extras(): HasMany + { + return $this->hasMany(StorefrontWebsiteTypeExtra::class, 'storefront_website_type_code', 'codigo'); + } + + /** @return HasMany */ + public function tenants(): HasMany + { + return $this->hasMany(Tenant::class, 'storefront_website_type_code', 'codigo'); + } +} diff --git a/app/Domains/Tenant/Models/WebsiteTypeExtra.php b/app/Domains/Core/Tenant/Models/StorefrontWebsiteTypeExtra.php similarity index 67% rename from app/Domains/Tenant/Models/WebsiteTypeExtra.php rename to app/Domains/Core/Tenant/Models/StorefrontWebsiteTypeExtra.php index 87be8c3..7accc69 100644 --- a/app/Domains/Tenant/Models/WebsiteTypeExtra.php +++ b/app/Domains/Core/Tenant/Models/StorefrontWebsiteTypeExtra.php @@ -1,6 +1,6 @@ @@ -34,11 +34,11 @@ class WebsiteTypeExtra extends Model } /** - * @return BelongsTo + * @return BelongsTo */ - public function websiteType(): BelongsTo + public function storefrontWebsiteType(): BelongsTo { - return $this->belongsTo(WebsiteType::class, 'website_type_code', 'codigo'); + return $this->belongsTo(StorefrontWebsiteType::class, 'storefront_website_type_code', 'codigo'); } /** diff --git a/app/Domains/Tenant/Models/Tenant.php b/app/Domains/Core/Tenant/Models/Tenant.php similarity index 79% rename from app/Domains/Tenant/Models/Tenant.php rename to app/Domains/Core/Tenant/Models/Tenant.php index e119319..c06ecc6 100644 --- a/app/Domains/Tenant/Models/Tenant.php +++ b/app/Domains/Core/Tenant/Models/Tenant.php @@ -1,19 +1,21 @@ self::DEFAULT_TIMEZONE, 'base_path' => '/', 'search_product_layout' => ProductLayout::ColumnWithImage->value, 'search_group_layout' => GroupLayout::Paginated->value, @@ -152,6 +157,7 @@ class Tenant extends Model protected function casts(): array { return [ + 'active_event_id' => 'integer', 'search_product_layout' => ProductLayout::class, 'search_group_layout' => GroupLayout::class, 'search_items_per_page' => 'integer', @@ -210,11 +216,17 @@ class Tenant extends Model } /** - * @return BelongsTo + * @return BelongsTo */ - public function websiteType(): BelongsTo + public function adminWebsiteType(): BelongsTo { - return $this->belongsTo(WebsiteType::class, 'website_type_code', 'codigo'); + return $this->belongsTo(AdminWebsiteType::class, 'admin_website_type_code', 'codigo'); + } + + /** @return BelongsTo */ + public function storefrontWebsiteType(): BelongsTo + { + return $this->belongsTo(StorefrontWebsiteType::class, 'storefront_website_type_code', 'codigo'); } public function catalogItems(): HasMany @@ -222,6 +234,18 @@ class Tenant extends Model return $this->hasMany(CatalogItem::class, 'tenant_code', 'codigo'); } + /** @return HasMany */ + public function events(): HasMany + { + return $this->hasMany(Event::class, 'tenant_code', 'codigo'); + } + + /** @return BelongsTo */ + public function activeEvent(): BelongsTo + { + return $this->belongsTo(Event::class, 'active_event_id'); + } + /** @return HasMany */ public function eventDates(): HasMany { @@ -246,6 +270,12 @@ class Tenant extends Model return $this->hasMany(Category::class, 'tenant_code', 'codigo'); } + /** @return HasMany */ + public function eventCategories(): HasMany + { + return $this->hasMany(EventCategory::class, 'tenant_code', 'codigo'); + } + /** @return HasMany */ public function scanAttempts(): HasMany { diff --git a/app/Domains/Tenant/Models/WebsiteExtra.php b/app/Domains/Core/Tenant/Models/WebsiteExtra.php similarity index 87% rename from app/Domains/Tenant/Models/WebsiteExtra.php rename to app/Domains/Core/Tenant/Models/WebsiteExtra.php index 2698dc2..fd31391 100644 --- a/app/Domains/Tenant/Models/WebsiteExtra.php +++ b/app/Domains/Core/Tenant/Models/WebsiteExtra.php @@ -1,6 +1,6 @@ + * @return BelongsTo */ public function websiteTypeExtra(): BelongsTo { - return $this->belongsTo(WebsiteTypeExtra::class, 'website_type_extra_id'); + return $this->belongsTo(StorefrontWebsiteTypeExtra::class, 'website_type_extra_id'); } public function setResolvedConfig(mixed $config): self diff --git a/app/Domains/Tenant/Policies/.gitkeep b/app/Domains/Core/Tenant/Policies/.gitkeep similarity index 100% rename from app/Domains/Tenant/Policies/.gitkeep rename to app/Domains/Core/Tenant/Policies/.gitkeep diff --git a/app/Domains/Tenant/Requests/AdminApp/UpdateWebsiteExtraRequest.php b/app/Domains/Core/Tenant/Requests/AdminApp/UpdateWebsiteExtraRequest.php similarity index 81% rename from app/Domains/Tenant/Requests/AdminApp/UpdateWebsiteExtraRequest.php rename to app/Domains/Core/Tenant/Requests/AdminApp/UpdateWebsiteExtraRequest.php index e5e19dd..67dc9eb 100644 --- a/app/Domains/Tenant/Requests/AdminApp/UpdateWebsiteExtraRequest.php +++ b/app/Domains/Core/Tenant/Requests/AdminApp/UpdateWebsiteExtraRequest.php @@ -1,8 +1,8 @@ $this->websiteType ? [ - 'codigo' => $this->websiteType->codigo, - 'nombre' => $this->websiteType->nombre, + 'storefront_website_type' => $this->storefrontWebsiteType ? [ + 'codigo' => $this->storefrontWebsiteType->codigo, + 'nombre' => $this->storefrontWebsiteType->nombre, ] : null, - 'definitions' => $this->websiteType?->extras + 'definitions' => $this->storefrontWebsiteType?->extras ->mapWithKeys(fn ($definition) => [ $definition->codigo => [ 'codigo' => $definition->codigo, diff --git a/app/Domains/Tenant/Resources/CartEditingPolicyResource.php b/app/Domains/Core/Tenant/Resources/CartEditingPolicyResource.php similarity index 86% rename from app/Domains/Tenant/Resources/CartEditingPolicyResource.php rename to app/Domains/Core/Tenant/Resources/CartEditingPolicyResource.php index aba3b20..e78e41d 100644 --- a/app/Domains/Tenant/Resources/CartEditingPolicyResource.php +++ b/app/Domains/Core/Tenant/Resources/CartEditingPolicyResource.php @@ -1,8 +1,8 @@ $this->client_id, 'codigo' => $this->codigo, 'nombre' => $this->nombre, + 'timezone' => $this->timezone, 'dominio' => $this->dominio, 'base_path' => $this->base_path, - 'site_title' => $this->site_title - ?? $this->websiteType?->site_title - ?? 'ShopitFront', + 'site_title' => $this->site_title ?? 'ShopitFront', 'asset_url' => config('filesystems.disks.s3.url'), 'address' => $this->address, 'phone' => $this->phone, - 'favicon' => ($this->favicon ?? $this->websiteType?->favicon) - ?->getTemporaryUrl(1440), + 'favicon' => $this->favicon?->getTemporaryUrl(1440), 'primary_color' => $this->primary_color, 'secondary_color' => $this->secondary_color, 'danger_color' => $this->danger_color, 'success_color' => $this->success_color, 'header_bg_color' => $this->header_bg_color, 'footer_bg_color' => $this->footer_bg_color, - 'website_type_code' => $this->website_type_code, - 'event_date_text' => $this->event_date_text, - 'event' => $this->whenLoaded('eventDates', fn () => $this->event_title === null - ? null - : [ - 'title' => $this->event_title, - 'location' => $this->event_location, - 'dates' => $this->eventDates - ->filter(fn (EventDate $eventDate): bool => $eventDate->rescheduled_to_event_date_id === null) - ->map(fn (EventDate $eventDate): array => [ - 'id' => $eventDate->id, - 'date' => $eventDate->date->format('Y-m-d'), - 'time_start' => $eventDate->time_start, - 'time_end' => $eventDate->time_end, - 'info_text' => app(EventDateInfoFormatter::class)->format( - $eventDate, - $eventDateChanges, - ), - 'isCanceled' => $eventDate->suspended_at !== null, - ])->values(), - ]), + 'admin_website_type_code' => $this->admin_website_type_code, + 'storefront_website_type_code' => $this->storefront_website_type_code, + 'header_type' => $this->storefrontWebsiteType?->header_type ?? 'standard', + 'checkout_summary_type' => $this->storefrontWebsiteType?->checkout_summary_type + ?? StorefrontWebsiteType::CHECKOUT_SUMMARY_DEFAULT, + 'active_event_id' => $this->active_event_id, + 'event' => $this->whenLoaded('activeEvent', fn () => $this->activeEvent === null + ? null : $this->formatEvent($this->activeEvent, $eventDateChanges)), 'extras' => $this->whenLoaded( 'websiteExtras', fn () => $this->websiteExtras @@ -101,7 +88,9 @@ class TenantResource extends JsonResource 'ticket_partial_refund_percentage' => $this->ticket_partial_refund_percentage, 'social_media' => $this->whenLoaded( 'socialMedia', - fn () => $this->socialMedia + fn () => ($this->relationLoaded('activeEvent') && $this->activeEvent !== null + ? $this->activeEvent->socialMedia + : $this->socialMedia) ->map(fn ($socialMedia) => [ 'code' => $socialMedia->code, 'icon' => $socialMedia->icon, @@ -114,10 +103,47 @@ class TenantResource extends JsonResource 'menues', fn () => $this->menuTree($this->menues) ), - 'categories' => $this->whenLoaded( - 'categories', - fn () => $this->categoryTree($this->categories) + 'categories' => $this->when( + $this->storefront_website_type_code !== 'onticket_multi_event', + fn () => $this->whenLoaded('categories', fn () => $this->categoryTree($this->categories)) ), + 'event_categories' => $this->when( + $this->storefront_website_type_code === 'onticket_multi_event', + fn () => $this->whenLoaded('eventCategories', fn () => $this->eventCategories + ->map(fn ($category) => [ + 'id' => $category->id, + 'nombre' => $category->nombre, + 'subcategories' => [], + ])->values()) + ), + ]; + } + + private function formatEvent(Event $event, Collection $eventDateChanges): array + { + return [ + 'id' => $event->id, + 'title' => $event->title, + 'subtitle' => $event->subtitle, + 'description' => $event->description, + 'location' => $event->location, + 'date_text' => $event->date_text, + 'dates' => $event->dates + ->filter(fn (EventDate $date): bool => $date->rescheduled_to_event_date_id === null) + ->map(fn (EventDate $date): array => [ + 'id' => $date->id, + 'date' => $date->date->format('Y-m-d'), + 'time_start' => $date->time_start, + 'time_end' => $date->time_end, + 'info_text' => app(EventDateInfoFormatter::class)->format($date, $eventDateChanges), + 'isCanceled' => $date->suspended_at !== null, + ])->values(), + 'social_media' => $event->socialMedia->map(fn ($item): array => [ + 'code' => $item->code, + 'icon' => $item->icon, + 'name' => $item->name, + 'url' => $item->pivot->url, + ])->values(), ]; } diff --git a/app/Domains/Tenant/Services/.gitkeep b/app/Domains/Core/Tenant/Services/.gitkeep similarity index 100% rename from app/Domains/Tenant/Services/.gitkeep rename to app/Domains/Core/Tenant/Services/.gitkeep diff --git a/app/Domains/Tenant/Services/WebsiteTypeService.php b/app/Domains/Core/Tenant/Services/AdminWebsiteTypeService.php similarity index 66% rename from app/Domains/Tenant/Services/WebsiteTypeService.php rename to app/Domains/Core/Tenant/Services/AdminWebsiteTypeService.php index 9c57eba..38fa936 100644 --- a/app/Domains/Tenant/Services/WebsiteTypeService.php +++ b/app/Domains/Core/Tenant/Services/AdminWebsiteTypeService.php @@ -1,14 +1,15 @@ $data */ - public function create(array $data): WebsiteType + public function create(array $data): AdminWebsiteType { - return $this->save(new WebsiteType, $data); + return $this->save(new AdminWebsiteType, $data); } /** @@ -30,10 +31,10 @@ class WebsiteTypeService * @param array $attributes * @param array $values */ - public function updateOrCreate(array $attributes, array $values = []): WebsiteType + public function updateOrCreate(array $attributes, array $values = []): AdminWebsiteType { - /** @var WebsiteType $websiteType */ - $websiteType = WebsiteType::query()->firstOrNew($attributes); + /** @var AdminWebsiteType $websiteType */ + $websiteType = AdminWebsiteType::query()->firstOrNew($attributes); return $this->save($websiteType, [...$attributes, ...$values]); } @@ -41,9 +42,9 @@ class WebsiteTypeService /** * @param array $data */ - private function save(WebsiteType $websiteType, array $data): WebsiteType + private function save(AdminWebsiteType $websiteType, array $data): AdminWebsiteType { - return DB::transaction(function () use ($websiteType, $data): WebsiteType { + return DB::transaction(function () use ($websiteType, $data): AdminWebsiteType { $previousLogos = []; $attachmentFields = [ @@ -68,7 +69,7 @@ class WebsiteTypeService if ($logo) { $attachment = is_string($logo) && Str::isUuid($logo) ? Attachment::query()->where('key', $logo)->first() - : $this->attachmentService->store($logo, 'website-types'); + : $this->attachmentService->store($logo, 'admin-website-types'); } $data[$attachmentField['column']] = $attachment?->id; @@ -82,7 +83,7 @@ class WebsiteTypeService && $previousLogo->id !== $websiteType->site_logo && $previousLogo->id !== $websiteType->footer_logo && $previousLogo->id !== $websiteType->favicon_id - && ! $this->isReferencedByWebsiteType($previousLogo) + && ! $this->isReferencedByBrand($previousLogo) ) { $this->attachmentService->delete($previousLogo); } @@ -92,15 +93,16 @@ class WebsiteTypeService }); } - private function isReferencedByWebsiteType(Attachment $attachment): bool + private function isReferencedByBrand(Attachment $attachment): bool { - return WebsiteType::query() - ->where(function ($query) use ($attachment): void { - $query - ->where('site_logo', $attachment->id) - ->orWhere('footer_logo', $attachment->id) - ->orWhere('favicon_id', $attachment->id); - }) - ->exists(); + return Tenant::query()->where('favicon_id', $attachment->id)->exists() + || AdminWebsiteType::query() + ->where(function ($query) use ($attachment): void { + $query + ->where('site_logo', $attachment->id) + ->orWhere('footer_logo', $attachment->id) + ->orWhere('favicon_id', $attachment->id); + }) + ->exists(); } } diff --git a/app/Domains/Tenant/Services/TenantInformationService.php b/app/Domains/Core/Tenant/Services/TenantInformationService.php similarity index 94% rename from app/Domains/Tenant/Services/TenantInformationService.php rename to app/Domains/Core/Tenant/Services/TenantInformationService.php index 184d266..fcbdcba 100644 --- a/app/Domains/Tenant/Services/TenantInformationService.php +++ b/app/Domains/Core/Tenant/Services/TenantInformationService.php @@ -1,10 +1,10 @@ $data */ + public function create(array $data): Tenant + { + return DB::transaction(function () use ($data): Tenant { + $images = [ + 'header_logo' => $data['header_logo'] ?? null, + 'footer_logo' => $data['footer_logo'] ?? null, + 'favicon' => $data['favicon'] ?? null, + 'header_bg_image' => $data['header_bg_image'] ?? null, + 'footer_bg_image' => $data['footer_bg_image'] ?? null, + ]; + $socialMedia = $data['social_media'] ?? []; + $extras = $data['extras'] ?? []; + + unset( + $data['header_logo'], + $data['footer_logo'], + $data['favicon'], + $data['header_bg_image'], + $data['footer_bg_image'], + $data['social_media'], + $data['extras'], + ); + + foreach ($images as $key => $image) { + $data[$key.'_id'] = $this->storeTenantImage($image); + } + + /** @var Tenant $tenant */ + $tenant = Tenant::query()->create($data); + $this->syncSocialMedia($tenant, $socialMedia); + + foreach ($extras as $code => $config) { + $this->websiteExtraService->updateForTenant($tenant, $code, $config); + } + + return $tenant; + }); + } + + /** @param array $data */ + public function update(Tenant $tenant, array $data): Tenant + { + return DB::transaction(function () use ($tenant, $data): Tenant { + $imageKeys = [ + 'header_logo', + 'footer_logo', + 'favicon', + 'header_bg_image', + 'footer_bg_image', + ]; + $hasSocialMedia = array_key_exists('social_media', $data); + $socialMedia = $data['social_media'] ?? []; + unset($data['social_media']); + + foreach ($imageKeys as $key) { + if (! array_key_exists($key, $data)) { + continue; + } + + $data[$key.'_id'] = $this->storeTenantImage($data[$key]); + unset($data[$key]); + } + + $tenant->update($data); + + if ($hasSocialMedia) { + $this->syncSocialMedia($tenant, $socialMedia); + } + + return $tenant->refresh(); + }); + } + + /** @param array $socialMedia */ + private function syncSocialMedia(Tenant $tenant, array $socialMedia): void + { + $associations = []; + + foreach (array_values($socialMedia) as $index => $item) { + $associations[$item['code']] = [ + 'url' => $item['url'], + 'orden' => $item['orden'] ?? $index, + ]; + } + + $tenant->socialMedia()->sync($associations); + $tenant->unsetRelation('socialMedia'); + } + + private function storeTenantImage(mixed $image): ?int + { + if (! $image) { + return null; + } + + $attachment = is_string($image) && Str::isUuid($image) + ? Attachment::query()->where('key', $image)->first() + : $this->attachmentService->store($image, 'tenants'); + + return $attachment?->id; + } +} diff --git a/app/Domains/Tenant/Services/WebsiteExtraService.php b/app/Domains/Core/Tenant/Services/WebsiteExtraService.php similarity index 63% rename from app/Domains/Tenant/Services/WebsiteExtraService.php rename to app/Domains/Core/Tenant/Services/WebsiteExtraService.php index a86a4aa..196df87 100644 --- a/app/Domains/Tenant/Services/WebsiteExtraService.php +++ b/app/Domains/Core/Tenant/Services/WebsiteExtraService.php @@ -1,17 +1,15 @@ - */ - public function requestRules(?string $websiteTypeCode): array - { - if (! is_string($websiteTypeCode) || $websiteTypeCode === '') { - return [ - 'extras' => ['prohibited'], - ]; - } - - $definitions = $this->definitionsFor($websiteTypeCode); - $allowedCodes = $definitions->pluck('codigo')->all(); - $hasRequiredExtras = $definitions->contains( - fn (WebsiteTypeExtra $definition): bool => $definition->is_required - ); - - $rules = [ - 'extras' => [ - $hasRequiredExtras ? 'required' : 'sometimes', - 'array', - function (string $attribute, mixed $value, \Closure $fail) use ($allowedCodes): void { - if (! is_array($value)) { - return; - } - - $unknownCodes = array_diff(array_keys($value), $allowedCodes); - - if ($unknownCodes !== []) { - $fail( - 'The '.$attribute.' field contains extras not supported by the website type: ' - .implode(', ', $unknownCodes).'.' - ); - } - }, - ], - ]; - - foreach ($definitions as $definition) { - $schemaRules = $definition->config_schema['request_rules'] ?? []; - $rootRules = $this->compileRules($schemaRules['$'] ?? []); - $rootRules = array_values(array_filter( - $rootRules, - fn (mixed $rule): bool => ! in_array($rule, ['required', 'sometimes'], true) - )); - array_unshift($rootRules, $definition->is_required ? 'required' : 'sometimes'); - - $rules["extras.{$definition->codigo}"] = $rootRules; - - foreach ($schemaRules as $path => $pathRules) { - if ($path === '$') { - continue; - } - - $rules[$this->requestAttribute($definition->codigo, $path)] = $this->compileRules($pathRules); - } - } - - return $rules; - } - - /** - * Transform and persist each extra selected for a tenant. - * - * @param array $extras - */ - public function createForTenant(Tenant $tenant, array $extras): void - { - if ($extras === []) { - return; - } - - $definitions = $this->definitionsFor((string) $tenant->website_type_code) - ->keyBy('codigo'); - - foreach ($extras as $name => $config) { - /** @var WebsiteTypeExtra|null $definition */ - $definition = $definitions->get($name); - - if (! $definition) { - throw ValidationException::withMessages([ - 'extras' => ["The extra {$name} is not supported by the selected website type."], - ]); - } - - $requestRoot = "extras.{$definition->codigo}"; - $transformedConfig = $this->applyTransforms( - $tenant, - $definition, - $config, - $requestRoot - ); - $tenant->websiteExtras()->create([ - 'website_type_extra_id' => $definition->id, - 'config' => $transformedConfig, - ]); - } - - $tenant->unsetRelation('websiteExtras'); - } - /** * Build request rules for one extra addressed by its stable code. * @@ -193,27 +88,14 @@ class WebsiteExtraService }); } - public function definitionForTenant(Tenant $tenant, string $extraCode): WebsiteTypeExtra + public function definitionForTenant(Tenant $tenant, string $extraCode): StorefrontWebsiteTypeExtra { - return WebsiteTypeExtra::query() - ->where('website_type_code', $tenant->website_type_code) + return StorefrontWebsiteTypeExtra::query() + ->where('storefront_website_type_code', $tenant->storefront_website_type_code) ->where('codigo', $extraCode) ->firstOrFail(); } - /** - * @return Collection - */ - private function definitionsFor(string $websiteTypeCode): Collection - { - $websiteType = WebsiteType::query() - ->where('codigo', $websiteTypeCode) - ->with('extras') - ->first(); - - return $websiteType?->extras ?? collect(); - } - /** * @param string|array $rules * @return array @@ -232,11 +114,6 @@ class WebsiteExtraService ); } - private function requestAttribute(string $extraCode, string $path): string - { - return $this->configAttribute("extras.{$extraCode}", $path); - } - private function configAttribute(string $root, string $path): string { if ($path === '$') { @@ -252,7 +129,7 @@ class WebsiteExtraService private function applyTransforms( Tenant $tenant, - WebsiteTypeExtra $definition, + StorefrontWebsiteTypeExtra $definition, mixed $config, string $requestRoot ): mixed { @@ -318,7 +195,7 @@ class WebsiteExtraService */ private function transformValue( Tenant $tenant, - WebsiteTypeExtra $definition, + StorefrontWebsiteTypeExtra $definition, string $path, mixed $value, array $transform, diff --git a/app/Domains/Tenant/Support/TenantDomainNormalizer.php b/app/Domains/Core/Tenant/Support/TenantDomainNormalizer.php similarity index 98% rename from app/Domains/Tenant/Support/TenantDomainNormalizer.php rename to app/Domains/Core/Tenant/Support/TenantDomainNormalizer.php index 4e43a2e..3b3256d 100644 --- a/app/Domains/Tenant/Support/TenantDomainNormalizer.php +++ b/app/Domains/Core/Tenant/Support/TenantDomainNormalizer.php @@ -1,6 +1,6 @@ except(['store', 'update']); + +require __DIR__.'/adminapp.php'; diff --git a/app/Domains/Event/documentacion/README.md b/app/Domains/Event/documentacion/README.md deleted file mode 100644 index dd46873..0000000 --- a/app/Domains/Event/documentacion/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# 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. -- `Services/EventDateNoticeService.php`: reclama y agrupa los cambios pendientes de cada usuario. -- `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. - -Para el storefront autenticado: - -- `POST /tenants/{tenant}/event-date-notices/claim`: devuelve hasta un aviso de suspensiones y otro de - reprogramaciones. Cada cambio se muestra como máximo tres veces por usuario. - -## Dependencias - -Depende de `Tenant`. Las fechas se vinculan con variantes de `Catalog`, que a su vez pueden generar tickets. - -## Consideraciones - -Los avisos se construyen dinámicamente después de excluir los cambios que el usuario ya vio -tres veces. Al reclamar los avisos se incrementa una vez cada cambio incluido, aunque varios -cambios aparezcan agrupados en el mismo mensaje. El reclamo bloquea al usuario durante la -transacción para impedir que pestañas concurrentes superen el máximo. diff --git a/app/Domains/Event/routes/api.php b/app/Domains/Event/routes/api.php deleted file mode 100644 index 19a1261..0000000 --- a/app/Domains/Event/routes/api.php +++ /dev/null @@ -1,11 +0,0 @@ -post( - 'tenants/{tenant:codigo}/event-date-notices/claim', - [EventDateNoticeController::class, 'claim'], -); diff --git a/app/Domains/Integration/Controllers/WebsiteTypeIntegrationController.php b/app/Domains/Integration/Controllers/WebsiteTypeIntegrationController.php deleted file mode 100644 index 847ac39..0000000 --- a/app/Domains/Integration/Controllers/WebsiteTypeIntegrationController.php +++ /dev/null @@ -1,43 +0,0 @@ -integrations()->with('integrationInstance')->get()); - } - - public function show(WebsiteType $websiteType, string $integrationCode) - { - return new IntegrationAssociationResource($websiteType->integrations()->with('integrationInstance')->where('integration_code', $integrationCode)->firstOrFail()); - } - - public function store(ConfigureIntegrationRequest $request, WebsiteType $websiteType, string $integrationCode) - { - $integration = Integration::query()->where('integration_code', $integrationCode)->firstOrFail(); - - return new IntegrationAssociationResource($this->service->configure( - $websiteType, - $integration, - $request->validated('integration_data'), - )); - } - - public function destroy(WebsiteType $websiteType, string $integrationCode) - { - $this->service->detach($websiteType, $integrationCode); - - return response()->noContent(); - } -} diff --git a/app/Domains/Integration/routes/api.php b/app/Domains/Integration/routes/api.php deleted file mode 100644 index bad0f08..0000000 --- a/app/Domains/Integration/routes/api.php +++ /dev/null @@ -1,25 +0,0 @@ -group(function (): void { - Route::prefix('website-types/{websiteType:codigo}/integrations')->group(function (): void { - Route::get('/', [WebsiteTypeIntegrationController::class, 'index']); - Route::get('/{integration_code}', [WebsiteTypeIntegrationController::class, 'show']); - Route::put('/{integration_code}', [WebsiteTypeIntegrationController::class, 'store']); - Route::delete('/{integration_code}', [WebsiteTypeIntegrationController::class, 'destroy']); - }); - - Route::group(['prefix' => 'clients/{client}/integrations'], function () { - Route::get('/', [ClientIntegrationController::class, 'index']); - Route::get('/{integration_code}', [ClientIntegrationController::class, 'show']); - Route::put('/{integration_code}', [ClientIntegrationController::class, 'store']); - Route::delete('/{integration_code}', [ClientIntegrationController::class, 'destroy']); - }); -}); - -Route::post('webhooks/telepagos/{client}', [TelepagosWebhookController::class, 'handle']); diff --git a/app/Domains/Purchase/Services/Checkout/PurchaseResponseLoader.php b/app/Domains/Purchase/Services/Checkout/PurchaseResponseLoader.php deleted file mode 100644 index 7f9175d..0000000 --- a/app/Domains/Purchase/Services/Checkout/PurchaseResponseLoader.php +++ /dev/null @@ -1,22 +0,0 @@ -status === Purchase::STATUS_IN_REVIEW - && $purchase->payment_method === 'transfer' - ) { - $relations[] = 'telepagosPaymentCandidates.payment'; - } - - return $purchase->load($relations); - } -} diff --git a/app/Domains/Tenant/Controllers/TenantController.php b/app/Domains/Tenant/Controllers/TenantController.php deleted file mode 100644 index 5b105af..0000000 --- a/app/Domains/Tenant/Controllers/TenantController.php +++ /dev/null @@ -1,64 +0,0 @@ -latest() - ->paginateFromRequest(); - - $this->tenantInformationService->loadMany($tenants->getCollection()); - - return TenantResource::collection($tenants)->response(); - } - - public function store(StoreTenantRequest $request): JsonResponse - { - $tenant = $this->tenantService->create($request->validated()); - - return TenantResource::make( - $this->tenantInformationService->load($tenant) - )->response()->setStatusCode(201); - } - - public function show(Tenant $tenant): TenantResource - { - return TenantResource::make( - $this->tenantInformationService->load($tenant) - ); - } - - public function update(UpdateTenantRequest $request, Tenant $tenant): TenantResource - { - $tenant = $this->tenantService->update($tenant, $request->validated()); - - return TenantResource::make( - $this->tenantInformationService->load($tenant) - ); - } - - public function destroy(Tenant $tenant): Response - { - $tenant->delete(); - - return response()->noContent(); - } -} diff --git a/app/Domains/Tenant/Requests/StoreTenantRequest.php b/app/Domains/Tenant/Requests/StoreTenantRequest.php deleted file mode 100644 index 2dc435b..0000000 --- a/app/Domains/Tenant/Requests/StoreTenantRequest.php +++ /dev/null @@ -1,138 +0,0 @@ -input('dominio'); - $hasExplicitBasePath = $this->has('base_path'); - $rawBasePath = $hasExplicitBasePath - ? $this->input('base_path') - : TenantDomainNormalizer::pathFromDomain($rawDomain); - $normalizedDomain = TenantDomainNormalizer::normalize($rawDomain); - $normalizedBasePath = TenantDomainNormalizer::normalizePath($rawBasePath); - - $this->hasInvalidDomain = TenantDomainNormalizer::hasValue($rawDomain) - && $normalizedDomain === null; - $this->hasInvalidBasePath = ($hasExplicitBasePath && ! is_string($rawBasePath)) - || $normalizedBasePath === null; - - $this->merge([ - 'dominio' => $normalizedDomain, - 'base_path' => $normalizedBasePath, - ]); - } - - /** - * @return array - */ - public function rules(): array - { - $logoRule = ['required', new ImageOrBase64Rule]; - - return array_merge([ - 'client_id' => ['sometimes', 'integer', Rule::exists('clients', 'id')], - 'codigo' => ['required', 'string', 'max:255', Rule::unique('tenants', 'codigo')], - 'nombre' => ['required', 'string', 'max:255'], - 'dominio' => [ - 'bail', - function (string $attribute, mixed $value, Closure $fail): void { - if ($this->hasInvalidDomain) { - $fail("The {$attribute} field must contain a valid domain or URL."); - } - }, - 'required', - 'string', - 'max:255', - Rule::unique('tenants', 'dominio') - ->where('base_path', $this->input('base_path')), - ], - 'base_path' => [ - 'bail', - function (string $attribute, mixed $value, Closure $fail): void { - if ($this->hasInvalidBasePath) { - $fail("The {$attribute} field must contain a valid URL path."); - } - }, - 'required', - 'string', - 'max:255', - Rule::unique('tenants', 'base_path') - ->where('dominio', $this->input('dominio')), - ], - 'site_title' => ['sometimes', 'nullable', 'string', 'max:255'], - 'address' => ['sometimes', 'nullable', 'string', 'max:255'], - 'phone' => ['sometimes', 'nullable', 'string', 'max:255'], - 'primary_color' => ['required', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'], - 'secondary_color' => ['required', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'], - 'danger_color' => ['required', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'], - 'success_color' => ['required', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'], - 'header_bg_color' => ['required', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'], - 'footer_bg_color' => ['required', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'], - 'header_logo' => $logoRule, - 'footer_logo' => $logoRule, - 'favicon' => ['sometimes', 'nullable', new ImageOrBase64Rule], - 'header_bg_image' => ['sometimes', 'nullable', new ImageOrBase64Rule], - 'footer_bg_image' => ['sometimes', 'nullable', new ImageOrBase64Rule], - 'social_media' => ['sometimes', 'array'], - 'social_media.*.code' => [ - 'required', - 'string', - 'distinct', - Rule::exists('social_media', 'code'), - ], - 'social_media.*.url' => ['required', 'url', 'max:2048'], - 'social_media.*.orden' => ['sometimes', 'integer', 'min:0', 'distinct'], - 'search_product_layout' => ['sometimes', Rule::enum(ProductLayout::class)], - 'search_group_layout' => ['sometimes', Rule::enum(GroupLayout::class)], - 'search_items_per_page' => ['sometimes', 'integer', 'min:4', 'max:48'], - 'display_categories' => ['sometimes', 'boolean'], - 'display_seach_bar' => ['sometimes', 'boolean'], - 'display_cart' => ['sometimes', 'boolean'], - 'cart_editing_policy' => ['sometimes', Rule::enum(CartEditingPolicy::class)], - 'checkout_editing_policy' => [ - 'sometimes', - Rule::in([CartEditingPolicy::Disabled->value]), - ], - 'display_cart_item_images' => ['sometimes', 'boolean'], - 'scanner_category_validation_enabled' => ['sometimes', 'boolean'], - 'allow_ticket_refund' => ['sometimes', 'boolean'], - 'allow_ticket_total_refund' => ['sometimes', 'boolean'], - 'allow_ticket_partial_refund' => ['sometimes', 'boolean'], - 'ticket_partial_refund_percentage' => [ - 'sometimes', - 'numeric', - 'decimal:0,2', - 'min:0', - 'max:99.99', - ], - 'website_type_code' => [ - 'required_with:extras', - 'sometimes', - 'string', - Rule::exists('website_type', 'codigo'), - ], - ], app(WebsiteExtraService::class)->requestRules($this->input('website_type_code'))); - } -} diff --git a/app/Domains/Tenant/Requests/UpdateTenantRequest.php b/app/Domains/Tenant/Requests/UpdateTenantRequest.php deleted file mode 100644 index 4acc110..0000000 --- a/app/Domains/Tenant/Requests/UpdateTenantRequest.php +++ /dev/null @@ -1,153 +0,0 @@ -has('dominio')) { - $rawDomain = $this->input('dominio'); - $normalizedDomain = TenantDomainNormalizer::normalize($rawDomain); - $embeddedBasePath = TenantDomainNormalizer::pathFromDomain($rawDomain); - - $this->hasInvalidDomain = TenantDomainNormalizer::hasValue($rawDomain) - && ($normalizedDomain === null || $embeddedBasePath === null); - - $this->merge([ - 'dominio' => $normalizedDomain, - ]); - - if (! $this->has('base_path') && $embeddedBasePath !== null && $embeddedBasePath !== '/') { - $this->merge(['base_path' => $embeddedBasePath]); - } - } - - if ($this->has('base_path')) { - $rawBasePath = $this->input('base_path'); - $normalizedBasePath = TenantDomainNormalizer::normalizePath($rawBasePath); - - $this->hasInvalidBasePath = ! is_string($rawBasePath) - || $normalizedBasePath === null; - - $this->merge(['base_path' => $normalizedBasePath]); - } - } - - /** - * @return array - */ - public function rules(): array - { - /** @var Tenant|null $tenant */ - $tenant = $this->route('tenant'); - $domain = $this->input('dominio', $tenant?->dominio); - $basePath = $this->input('base_path', $tenant?->base_path ?? '/'); - - $logoRule = ['nullable', new ImageOrBase64Rule]; - - return [ - 'client_id' => ['sometimes', 'integer', Rule::exists('clients', 'id')], - 'codigo' => [ - 'nullable', - 'string', - 'max:255', - Rule::unique('tenants', 'codigo')->ignore($tenant?->id), - ], - 'nombre' => ['nullable', 'string', 'max:255'], - 'dominio' => [ - 'bail', - function (string $attribute, mixed $value, Closure $fail): void { - if ($this->hasInvalidDomain) { - $fail("The {$attribute} field must contain a valid domain or URL."); - } - }, - 'nullable', - 'string', - 'max:255', - Rule::unique('tenants', 'dominio') - ->where('base_path', $basePath) - ->ignore($tenant?->id), - ], - 'base_path' => [ - 'bail', - function (string $attribute, mixed $value, Closure $fail): void { - if ($this->hasInvalidBasePath) { - $fail("The {$attribute} field must contain a valid URL path."); - } - }, - 'nullable', - 'string', - 'max:255', - Rule::unique('tenants', 'base_path') - ->where('dominio', $domain) - ->ignore($tenant?->id), - ], - 'site_title' => ['sometimes', 'nullable', 'string', 'max:255'], - 'address' => ['sometimes', 'nullable', 'string', 'max:255'], - 'phone' => ['sometimes', 'nullable', 'string', 'max:255'], - 'primary_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'], - 'secondary_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'], - 'danger_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'], - 'success_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'], - 'header_bg_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'], - 'footer_bg_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'], - 'header_logo' => $logoRule, - 'footer_logo' => $logoRule, - 'favicon' => ['sometimes', 'nullable', new ImageOrBase64Rule], - 'header_bg_image' => ['sometimes', 'nullable', new ImageOrBase64Rule], - 'footer_bg_image' => ['sometimes', 'nullable', new ImageOrBase64Rule], - 'social_media' => ['sometimes', 'array'], - 'social_media.*.code' => [ - 'required', - 'string', - 'distinct', - Rule::exists('social_media', 'code'), - ], - 'social_media.*.url' => ['required', 'url', 'max:2048'], - 'social_media.*.orden' => ['sometimes', 'integer', 'min:0', 'distinct'], - 'search_product_layout' => ['sometimes', Rule::enum(ProductLayout::class)], - 'search_group_layout' => ['sometimes', Rule::enum(GroupLayout::class)], - 'search_items_per_page' => ['sometimes', 'integer', 'min:4', 'max:48'], - 'display_categories' => ['sometimes', 'boolean'], - 'display_seach_bar' => ['sometimes', 'boolean'], - 'display_cart' => ['sometimes', 'boolean'], - 'cart_editing_policy' => ['sometimes', Rule::enum(CartEditingPolicy::class)], - 'checkout_editing_policy' => [ - 'sometimes', - Rule::in([CartEditingPolicy::Disabled->value]), - ], - 'display_cart_item_images' => ['sometimes', 'boolean'], - 'scanner_category_validation_enabled' => ['sometimes', 'boolean'], - 'allow_ticket_refund' => ['sometimes', 'boolean'], - 'allow_ticket_total_refund' => ['sometimes', 'boolean'], - 'allow_ticket_partial_refund' => ['sometimes', 'boolean'], - 'ticket_partial_refund_percentage' => [ - 'sometimes', - 'numeric', - 'decimal:0,2', - 'min:0', - 'max:99.99', - ], - ]; - } -} diff --git a/app/Domains/Tenant/Services/TenantService.php b/app/Domains/Tenant/Services/TenantService.php deleted file mode 100644 index da73cb6..0000000 --- a/app/Domains/Tenant/Services/TenantService.php +++ /dev/null @@ -1,197 +0,0 @@ - $data - */ - public function create(array $data): Tenant - { - return DB::transaction(function () use ($data): Tenant { - $headerLogo = $data['header_logo'] ?? null; - $footerLogo = $data['footer_logo'] ?? null; - $favicon = $data['favicon'] ?? null; - $headerBackgroundImage = $data['header_bg_image'] ?? null; - $footerBackgroundImage = $data['footer_bg_image'] ?? null; - $socialMedia = $data['social_media'] ?? []; - $extras = $data['extras'] ?? []; - - unset( - $data['header_logo'], - $data['footer_logo'], - $data['favicon'], - $data['header_bg_image'], - $data['footer_bg_image'], - $data['social_media'], - $data['extras'], - ); - - $headerAttachmentId = null; - if ($headerLogo) { - $attachment = Str::isUuid($headerLogo) - ? Attachment::query()->where('key', $headerLogo)->first() - : $this->attachmentService->store($headerLogo, 'tenants'); - - if ($attachment) { - $headerAttachmentId = $attachment->id; - } - } - - $footerAttachmentId = null; - if ($footerLogo) { - $attachment = Str::isUuid($footerLogo) - ? Attachment::query()->where('key', $footerLogo)->first() - : $this->attachmentService->store($footerLogo, 'tenants'); - - if ($attachment) { - $footerAttachmentId = $attachment->id; - } - } - - $data['header_logo_id'] = $headerAttachmentId; - $data['footer_logo_id'] = $footerAttachmentId; - $data['favicon_id'] = $this->storeTenantImage($favicon); - $data['header_bg_image_id'] = $this->storeTenantImage($headerBackgroundImage); - $data['footer_bg_image_id'] = $this->storeTenantImage($footerBackgroundImage); - - /** @var Tenant $tenant */ - $tenant = Tenant::query()->create($data); - $this->syncSocialMedia($tenant, $socialMedia); - $this->websiteExtraService->createForTenant($tenant, $extras); - - return $tenant; - }); - } - - /** - * Update an existing tenant and store new logos if uploaded. - * - * @param array $data - */ - public function update(Tenant $tenant, array $data): Tenant - { - return DB::transaction(function () use ($tenant, $data): Tenant { - $hasHeaderLogoKey = array_key_exists('header_logo', $data); - $hasFooterLogoKey = array_key_exists('footer_logo', $data); - $hasFaviconKey = array_key_exists('favicon', $data); - $hasHeaderBackgroundImageKey = array_key_exists('header_bg_image', $data); - $hasFooterBackgroundImageKey = array_key_exists('footer_bg_image', $data); - $hasSocialMediaKey = array_key_exists('social_media', $data); - $headerLogo = $data['header_logo'] ?? null; - $footerLogo = $data['footer_logo'] ?? null; - $favicon = $data['favicon'] ?? null; - $headerBackgroundImage = $data['header_bg_image'] ?? null; - $footerBackgroundImage = $data['footer_bg_image'] ?? null; - $socialMedia = $data['social_media'] ?? []; - - unset( - $data['header_logo'], - $data['footer_logo'], - $data['favicon'], - $data['header_bg_image'], - $data['footer_bg_image'], - $data['social_media'] - ); - - $tenant->fill($data); - - if ($hasHeaderLogoKey) { - if ($headerLogo) { - $attachment = Str::isUuid($headerLogo) - ? Attachment::query()->where('key', $headerLogo)->first() - : $this->attachmentService->store($headerLogo, 'tenants'); - - if ($attachment) { - $tenant->header_logo_id = $attachment->id; - } else { - $tenant->header_logo_id = null; - } - } else { - $tenant->header_logo_id = null; - } - } - - if ($hasFooterLogoKey) { - if ($footerLogo) { - $attachment = Str::isUuid($footerLogo) - ? Attachment::query()->where('key', $footerLogo)->first() - : $this->attachmentService->store($footerLogo, 'tenants'); - - if ($attachment) { - $tenant->footer_logo_id = $attachment->id; - } else { - $tenant->footer_logo_id = null; - } - } else { - $tenant->footer_logo_id = null; - } - } - - if ($hasFaviconKey) { - $tenant->favicon_id = $this->storeTenantImage($favicon); - } - - if ($hasHeaderBackgroundImageKey) { - $tenant->header_bg_image_id = $this->storeTenantImage($headerBackgroundImage); - } - - if ($hasFooterBackgroundImageKey) { - $tenant->footer_bg_image_id = $this->storeTenantImage($footerBackgroundImage); - } - - $tenant->save(); - - if ($hasSocialMediaKey) { - $this->syncSocialMedia($tenant, $socialMedia); - } - - return $tenant; - }); - } - - /** - * @param array $socialMedia - */ - private function syncSocialMedia(Tenant $tenant, array $socialMedia): void - { - $associations = []; - - foreach (array_values($socialMedia) as $index => $item) { - $associations[$item['code']] = [ - 'url' => $item['url'], - 'orden' => $item['orden'] ?? $index, - ]; - } - - $tenant->socialMedia()->sync($associations); - $tenant->unsetRelation('socialMedia'); - } - - private function storeTenantImage(mixed $image): ?int - { - if (! $image) { - return null; - } - - $attachment = is_string($image) && Str::isUuid($image) - ? Attachment::query()->where('key', $image)->first() - : $this->attachmentService->store($image, 'tenants'); - - return $attachment?->id; - } -} diff --git a/app/Domains/Tenant/routes/api.php b/app/Domains/Tenant/routes/api.php deleted file mode 100644 index 1deb043..0000000 --- a/app/Domains/Tenant/routes/api.php +++ /dev/null @@ -1,8 +0,0 @@ -where('tenant_code', $tenant->codigo) - ->where('user_id', $request->user()->getKey()) - ->with([...TicketValidityResolver::RELATIONS, ...TicketPresentationResolver::RELATIONS]) - ->orderByDesc('id') - ->get(); - - return TicketResource::collection($tickets)->response(); - } - - public function downloadPdf(DownloadTicketsPdfRequest $request, Tenant $tenant): Response - { - $ticketIds = $request->validated('ticket_ids'); - $tickets = Ticket::query() - ->where('tenant_code', $tenant->codigo) - ->where('user_id', $request->user()->getKey()) - ->whereIn('id', $ticketIds) - ->with([...TicketValidityResolver::RELATIONS, ...TicketPresentationResolver::RELATIONS]) - ->orderByDesc('id') - ->get(); - - if ($tickets->count() !== count($ticketIds)) { - throw new TicketNotAvailableException(__('api.ticket.not_available')); - } - - return $this->ticketPdfService->download($tenant, $tickets); - } -} diff --git a/app/Domains/Ticket/documentacion/README.md b/app/Domains/Ticket/documentacion/README.md deleted file mode 100644 index d4ced7f..0000000 --- a/app/Domains/Ticket/documentacion/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# 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, y conserva referencias al ítem de compra que lo generó, producto, - variante y usuario escáner. La compra se obtiene a través de su ítem. -- El nombre y la descripción se calculan dinámicamente desde el producto y la variante; los tickets no - persisten una copia de esos textos. -- `ValidityTime`: define ventanas absolutas o relativas de vigencia para fechas de evento y opciones de atributos. -- `ValidityTimeType`: enum de estrategias de vigencia. - -`TicketValidityResolver` deriva la vigencia desde la variante asociada. Las alternativas de un mismo atributo -se combinan con OR y las dimensiones diferentes se combinan con AND. El modelo calcula si un ticket está -vigente, vencido o usado, y resuelve sus fechas efectivas de inicio y fin sin persistir vigencias en el ticket. - -## 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. `Notification` envía la confirmación de compra después de la generación y adjunta los tickets cuando existen. - -## Datos descartables para pruebas de carga - -En ambientes `local`, `testing`, `staging`, `homo` u `homologation`, el comando siguiente crea tickets -válidos, identidades scanner con tokens Sanctum y un dataset JSON importable por Postman: - -```bash -php artisan load-test:tickets:prepare loadtest-evento \ - --tickets=40000 \ - --scanners=100 \ - --owners=1000 \ - --catalog-item=123 \ - --run=evento-001 -``` - -El tenant debe existir y se recomienda que sea exclusivo para carga. Si no se indica `--catalog-item`, -se usa el primer producto estándar del tenant con tickets habilitados. `--variant` -es opcional; al indicarlo, su configuración de vigencia debe estar activa y ser resoluble. Sin variante, -los tickets tienen vigencia irrestricta. - -El archivo se escribe por defecto en `storage/app/private/load-tests/` y contiene tokens secretos, por -lo que no debe versionarse. Para limpiar el tenant después de la ejecución: - -```bash -php artisan tenants:reset-transactions loadtest-evento --dry-run -php artisan tenants:reset-transactions loadtest-evento -``` - -## Endpoints - -Bajo `/tenants/{tenant:codigo}`, protegidos por `auth:sanctum`: - -- `GET /tickets`. -- `POST /tickets/pdf`. - -Bajo `/v1/adminapp/tenant`, protegido por `auth:sanctum`, `adminapp.tenant` y el menú -`adminapp.tickets`: - -- `GET /tickets`, paginado y con búsqueda opcional mediante `q`. La respuesta incluye - `scanned_tickets` y `total_tickets` para el tenant autenticado. - -`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/app/Domains/Desfile/Controllers/EntryController.php b/app/Domains/Ticketing/Desfile/Controllers/EntryController.php similarity index 80% rename from app/Domains/Desfile/Controllers/EntryController.php rename to app/Domains/Ticketing/Desfile/Controllers/EntryController.php index 51e09db..cf5daf9 100644 --- a/app/Domains/Desfile/Controllers/EntryController.php +++ b/app/Domains/Ticketing/Desfile/Controllers/EntryController.php @@ -1,12 +1,12 @@ where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->where('slug', 'entrada'); } diff --git a/app/Domains/Desfile/Services/InvitationPurchaseProvisioner.php b/app/Domains/Ticketing/Desfile/Services/InvitationPurchaseProvisioner.php similarity index 99% rename from app/Domains/Desfile/Services/InvitationPurchaseProvisioner.php rename to app/Domains/Ticketing/Desfile/Services/InvitationPurchaseProvisioner.php index ad16d37..382fe2d 100644 --- a/app/Domains/Desfile/Services/InvitationPurchaseProvisioner.php +++ b/app/Domains/Ticketing/Desfile/Services/InvitationPurchaseProvisioner.php @@ -1,6 +1,6 @@ storefront_website_type_code === 'onticket_multi_event', 404); + + $term = trim((string) $request->validated('q', '')); + return PublicEventResource::collection($events->list( + $tenant, + $term, + (int) $request->validated('page', 1), + $request->validated('category_id', null) === null ? null : (int) $request->validated('category_id'), + )); + } + + public function show(Tenant $tenant, Event $event, PublicEventService $events): PublicEventResource + { + abort_unless($tenant->storefront_website_type_code === 'onticket_multi_event', 404); + + return PublicEventResource::make($events->find($tenant, $event)); + } +} diff --git a/app/Domains/Event/Enums/EventDateChangeType.php b/app/Domains/Ticketing/Event/Enums/EventDateChangeType.php similarity index 72% rename from app/Domains/Event/Enums/EventDateChangeType.php rename to app/Domains/Ticketing/Event/Enums/EventDateChangeType.php index 1dba386..a3ef1fe 100644 --- a/app/Domains/Event/Enums/EventDateChangeType.php +++ b/app/Domains/Ticketing/Event/Enums/EventDateChangeType.php @@ -1,6 +1,6 @@ 'datetime', 'exact_location' => 'array']; + } + + /** @return BelongsTo */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo'); + } + + /** @return BelongsTo */ + public function eventCategory(): BelongsTo + { + return $this->belongsTo(EventCategory::class); + } + + /** @return BelongsTo */ + public function attachment(): BelongsTo + { + return $this->belongsTo(Attachment::class); + } + + /** @return HasMany */ + public function dates(): HasMany + { + return $this->hasMany(EventDate::class)->orderBy('date')->orderBy('time_start'); + } + + /** @return HasMany */ + public function catalogItems(): HasMany + { + return $this->hasMany(CatalogItem::class)->whereAvailable()->orderBy('group_order')->orderBy('id'); + } + + /** @return HasMany */ + public function tickets(): HasMany + { + return $this->hasMany(Ticket::class); + } + + /** @return BelongsToMany */ + public function socialMedia(): BelongsToMany + { + return $this->belongsToMany( + SocialMedia::class, + 'event_social_media', + 'event_id', + 'social_media_code', + 'id', + 'code', + )->withPivot(['url', 'orden'])->withTimestamps()->orderByPivot('orden'); + } +} diff --git a/app/Domains/Ticketing/Event/Models/EventCategory.php b/app/Domains/Ticketing/Event/Models/EventCategory.php new file mode 100644 index 0000000..dbed1e3 --- /dev/null +++ b/app/Domains/Ticketing/Event/Models/EventCategory.php @@ -0,0 +1,25 @@ + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo'); + } + + /** @return HasMany */ + public function events(): HasMany + { + return $this->hasMany(Event::class); + } +} diff --git a/app/Domains/Event/Models/EventDate.php b/app/Domains/Ticketing/Event/Models/EventDate.php similarity index 81% rename from app/Domains/Event/Models/EventDate.php rename to app/Domains/Ticketing/Event/Models/EventDate.php index 02add11..c91a5fa 100644 --- a/app/Domains/Event/Models/EventDate.php +++ b/app/Domains/Ticketing/Event/Models/EventDate.php @@ -1,14 +1,14 @@ $eventDate->syncValidityTime()); - static::created(fn (self $eventDate) => $eventDate->syncTenantDateText()); + static::created(fn (self $eventDate) => $eventDate->syncEventDateText()); static::updated(function (self $eventDate): void { if ($eventDate->wasChanged(['date', 'time_start', 'time_end'])) { $eventDate->syncValidityTime(); } - $eventDate->syncTenantDateText(); + $eventDate->syncEventDateText(); }); static::deleted(function (self $eventDate): void { - $eventDate->syncTenantDateText(); + $eventDate->syncEventDateText(); ValidityTime::query() ->whereKey($eventDate->validity_time_id) ->delete(); @@ -58,6 +59,7 @@ class EventDate extends Model return [ 'date' => 'date:Y-m-d', 'validity_time_id' => 'integer', + 'event_id' => 'integer', 'rescheduled_to_event_date_id' => 'integer', 'suspended_at' => 'datetime', ]; @@ -69,6 +71,12 @@ class EventDate extends Model return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo'); } + /** @return BelongsTo */ + public function event(): BelongsTo + { + return $this->belongsTo(Event::class); + } + /** @return BelongsTo */ public function validityTime(): BelongsTo { @@ -123,12 +131,12 @@ class EventDate extends Model public function startsAt(): CarbonInterface { - return Carbon::parse($this->date->format('Y-m-d').' '.$this->time_start); + return Carbon::parse($this->date->format('Y-m-d').' '.$this->time_start, $this->tenant->timezone); } public function endsAt(): CarbonInterface { - $endsAt = Carbon::parse($this->date->format('Y-m-d').' '.$this->time_end); + $endsAt = Carbon::parse($this->date->format('Y-m-d').' '.$this->time_end, $this->tenant->timezone); return $endsAt->lessThanOrEqualTo($this->startsAt()) ? $endsAt->addDay() @@ -156,17 +164,17 @@ class EventDate extends Model return EventDateStatus::Completed; } - private function syncTenantDateText(): void + private function syncEventDateText(): void { - $tenant = $this->tenant()->first(); + $event = $this->event()->first(); - if (! $tenant) { + if (! $event) { return; } - $tenant->update([ - 'event_date_text' => app(EventDateTextFormatter::class)->format( - $tenant->eventDates() + $event->update([ + 'date_text' => app(EventDateTextFormatter::class)->format( + $event->dates() ->whereNull('rescheduled_to_event_date_id') ->whereNull('suspended_at') ->pluck('date') @@ -176,8 +184,8 @@ class EventDate extends Model private function syncValidityTime(): void { - $startsAt = $this->startsAt(); - $expiresAt = $this->endsAt(); + $startsAt = $this->startsAt()->utc(); + $expiresAt = $this->endsAt()->utc(); $attributes = [ 'type' => ValidityTimeType::FixedWindow, diff --git a/app/Domains/Event/Models/EventDateChange.php b/app/Domains/Ticketing/Event/Models/EventDateChange.php similarity index 90% rename from app/Domains/Event/Models/EventDateChange.php rename to app/Domains/Ticketing/Event/Models/EventDateChange.php index 5fcbfab..1453907 100644 --- a/app/Domains/Event/Models/EventDateChange.php +++ b/app/Domains/Ticketing/Event/Models/EventDateChange.php @@ -1,10 +1,10 @@ ['sometimes', 'string', 'max:120'], + 'page' => ['sometimes', 'integer', 'min:1'], + 'category_id' => ['sometimes', 'integer', 'min:1'], + ]; + } +} diff --git a/app/Domains/Event/Requests/RescheduleEventDateRequest.php b/app/Domains/Ticketing/Event/Requests/RescheduleEventDateRequest.php similarity index 88% rename from app/Domains/Event/Requests/RescheduleEventDateRequest.php rename to app/Domains/Ticketing/Event/Requests/RescheduleEventDateRequest.php index 3573c8a..2bdb9f3 100644 --- a/app/Domains/Event/Requests/RescheduleEventDateRequest.php +++ b/app/Domains/Ticketing/Event/Requests/RescheduleEventDateRequest.php @@ -1,6 +1,6 @@ ['required', 'string', 'max:255'], 'location' => ['required', 'string', 'max:255'], + 'exact_location' => ['sometimes', 'nullable', 'array:latitude,longitude'], + 'exact_location.latitude' => ['required_with:exact_location', 'numeric', 'between:-90,90'], + 'exact_location.longitude' => ['required_with:exact_location', 'numeric', 'between:-180,180'], + 'attachment_id' => ['sometimes', 'nullable', 'integer', Rule::exists('attachments', 'id')->where('type', 'image')], 'social_media' => ['sometimes', 'array'], 'social_media.*' => ['required', 'array:code,url,orden'], 'social_media.*.code' => [ diff --git a/app/Domains/Event/Resources/EventDateChangeResource.php b/app/Domains/Ticketing/Event/Resources/EventDateChangeResource.php similarity index 86% rename from app/Domains/Event/Resources/EventDateChangeResource.php rename to app/Domains/Ticketing/Event/Resources/EventDateChangeResource.php index 025e053..dbd0b56 100644 --- a/app/Domains/Event/Resources/EventDateChangeResource.php +++ b/app/Domains/Ticketing/Event/Resources/EventDateChangeResource.php @@ -1,8 +1,8 @@ */ @@ -17,14 +17,21 @@ class EventResource extends JsonResource return [ 'id' => $this->id, - 'title' => $this->event_title, - 'location' => $this->event_location, - 'allow_ticket_refund' => $this->allow_ticket_refund, - 'allow_ticket_total_refund' => $this->allow_ticket_total_refund, - 'allow_ticket_partial_refund' => $this->allow_ticket_partial_refund, - 'ticket_partial_refund_percentage' => $this->ticket_partial_refund_percentage, + 'slug' => $this->slug, + 'title' => $this->title, + 'subtitle' => $this->subtitle, + 'description' => $this->description, + 'location' => $this->location, + 'exact_location' => $this->exact_location, + 'date_text' => $this->date_text, + 'published_at' => $this->published_at?->toIso8601String(), + 'attachment_id' => $this->attachment_id, + 'allow_ticket_refund' => $this->tenant->allow_ticket_refund, + 'allow_ticket_total_refund' => $this->tenant->allow_ticket_total_refund, + 'allow_ticket_partial_refund' => $this->tenant->allow_ticket_partial_refund, + 'ticket_partial_refund_percentage' => $this->tenant->ticket_partial_refund_percentage, 'dates' => EventDateResource::collection( - app(EventDateGroupingService::class)->group($this->eventDates) + app(EventDateGroupingService::class)->group($this->dates) ), 'social_media' => $this->socialMedia->map(fn ($item): array => [ 'code' => $item->code, diff --git a/app/Domains/Ticketing/Event/Resources/PublicEventResource.php b/app/Domains/Ticketing/Event/Resources/PublicEventResource.php new file mode 100644 index 0000000..b656676 --- /dev/null +++ b/app/Domains/Ticketing/Event/Resources/PublicEventResource.php @@ -0,0 +1,67 @@ + $this->id, + 'slug' => $this->slug, + 'event_category_id' => $this->event_category_id, + 'title' => $this->title, + 'subtitle' => $this->subtitle, + 'description' => $this->description, + 'location' => $this->location, + 'exact_location' => $this->exact_location, + 'date_text' => $this->date_text, + 'start_time' => $this->whenLoaded('dates', fn () => $this->dates->first()?->time_start), + 'starts_at' => $this->whenLoaded('dates', function (): ?string { + $first = $this->dates->first(); + if ($first === null) { + return null; + } + + return $first->startsAt()->toISOString(); + }), + 'dates' => $this->whenLoaded('dates', fn () => $this->dates->map(fn ($date): array => [ + 'id' => $date->id, + 'date' => $date->date->format('Y-m-d'), + 'time_start' => $date->time_start, + ])->values()), + 'social_media' => $this->whenLoaded('socialMedia', fn () => $this->socialMedia->map(fn ($social): array => [ + 'code' => $social->code, + 'url' => $social->pivot->url, + ])->values()), + 'attachment_id' => $this->attachment_id, + 'image' => $this->attachment?->getTemporaryUrl(1440), + 'catalog_items' => $this->whenLoaded('catalogItems', fn () => $this->catalogItems + ->map(fn (CatalogItem $item): array => [ + 'id' => $item->id, + 'name' => $item->nombre, + 'description' => $item->descripcion, + 'price' => $item->precio, + 'image' => $item->attachments->first()?->getTemporaryUrl(1440), + 'requires_selection' => $item->itemAttributes->contains(fn ($attribute): bool => $attribute->show_in_selector && $attribute->attribute?->codigo !== 'event_date'), + 'maximum_quantity' => $item->inventory_policy === InventoryPolicy::Unlimited + ? null + : $item->availableStock(), + 'variants' => $item->visibleVariants()->map(fn (Variant $variant): array => [ + 'id' => $variant->id, + 'price' => number_format($variant->getPrice(), 2, '.', ''), + 'event_date_ids' => $variant->selectedEventDates()->pluck('id')->values(), + 'maximum_quantity' => $item->inventory_policy === InventoryPolicy::Unlimited + ? null + : $variant->inventory?->availableStock(), + ])->values(), + ])->values()), + ]; + } +} diff --git a/app/Domains/Event/Services/AffectedEventDatePurchaseResolver.php b/app/Domains/Ticketing/Event/Services/AffectedEventDatePurchaseResolver.php similarity index 88% rename from app/Domains/Event/Services/AffectedEventDatePurchaseResolver.php rename to app/Domains/Ticketing/Event/Services/AffectedEventDatePurchaseResolver.php index 23b7e04..3840b77 100644 --- a/app/Domains/Event/Services/AffectedEventDatePurchaseResolver.php +++ b/app/Domains/Ticketing/Event/Services/AffectedEventDatePurchaseResolver.php @@ -1,11 +1,11 @@ load(['eventDates.validityTime', 'socialMedia']); + return $tenant->activeEvent->load(['dates.validityTime', 'socialMedia']); } /** @param array $data */ - public function updateForTenant(Tenant $tenant, array $data): Tenant + public function updateForTenant(Tenant $tenant, array $data): Event { - return DB::transaction(function () use ($tenant, $data): Tenant { - $tenant = Tenant::query()->whereKey($tenant->getKey())->lockForUpdate()->firstOrFail(); + return DB::transaction(function () use ($tenant, $data): Event { + $event = $tenant->activeEvent; + $event->update([ + 'title' => $data['title'], + 'location' => $data['location'], + ...array_intersect_key($data, ['attachment_id' => true, 'exact_location' => true]), + ]); $tenant->update([ - 'event_title' => $data['title'], - 'event_location' => $data['location'], ...array_intersect_key($data, array_flip([ 'allow_ticket_refund', 'allow_ticket_total_refund', @@ -55,12 +59,12 @@ class EventService ]); if (array_key_exists('social_media', $data)) { - $this->syncSocialMedia($tenant, $data['social_media']); - } else { - $this->syncLegacyContact($tenant, $data['contact']); + $this->syncSocialMedia($event, $data['social_media']); + } elseif (array_key_exists('contact', $data)) { + $this->syncLegacyContact($event, $data['contact']); } - return $tenant->load(['eventDates.validityTime', 'socialMedia']); + return $event->load(['dates.validityTime', 'socialMedia']); }); } @@ -68,15 +72,16 @@ class EventService public function createDateForTenant(Tenant $tenant, array $data): EventDate { return DB::transaction(function () use ($tenant, $data): EventDate { + $event = $tenant->activeEvent; $attributes = $this->dateAttributes($data); - if ($tenant->eventDates()->where($attributes)->exists()) { + if ($event->dates()->where($attributes)->exists()) { throw ValidationException::withMessages([ 'date' => ['La fecha y el horario ya existen.'], ]); } - return $tenant->eventDates()->create($attributes)->load('validityTime'); + return $event->dates()->create([...$attributes, 'tenant_code' => $tenant->codigo])->load('validityTime'); }); } @@ -102,13 +107,14 @@ class EventService ]); } - $destination = $tenant->eventDates() + $destination = $source->event->dates() ->whereDate('date', $data['date']) ->lockForUpdate() ->first(); if ($destination === null) { - $destination = $tenant->eventDates()->create([ + $destination = $source->event->dates()->create([ + 'tenant_code' => $tenant->codigo, 'date' => $data['date'], 'time_start' => $source->time_start, 'time_end' => $source->time_end, @@ -250,7 +256,7 @@ class EventService $frontier = $affectedDateIds; while ($frontier->isNotEmpty()) { - $predecessors = $tenant->eventDates() + $predecessors = $eventDate->event->dates() ->whereIn('rescheduled_to_event_date_id', $frontier) ->pluck('id') ->diff($affectedDateIds) @@ -313,27 +319,27 @@ class EventService } /** @param array $contact */ - private function syncLegacyContact(Tenant $tenant, array $contact): void + private function syncLegacyContact(Event $event, array $contact): void { foreach (self::CONTACT_CODES as $field => $code) { $url = $contact[$field] ?? null; if ($url === null || $url === '') { - $tenant->socialMedia()->detach($code); + $event->socialMedia()->detach($code); continue; } - $tenant->socialMedia()->syncWithoutDetaching([ + $event->socialMedia()->syncWithoutDetaching([ $code => ['url' => $url], ]); } - $tenant->unsetRelation('socialMedia'); + $event->unsetRelation('socialMedia'); } /** @param array $socialMedia */ - private function syncSocialMedia(Tenant $tenant, array $socialMedia): void + private function syncSocialMedia(Event $event, array $socialMedia): void { $associations = []; @@ -344,7 +350,7 @@ class EventService ]; } - $tenant->socialMedia()->sync($associations); - $tenant->unsetRelation('socialMedia'); + $event->socialMedia()->sync($associations); + $event->unsetRelation('socialMedia'); } } diff --git a/app/Domains/Ticketing/Event/Services/PublicEventService.php b/app/Domains/Ticketing/Event/Services/PublicEventService.php new file mode 100644 index 0000000..39c38eb --- /dev/null +++ b/app/Domains/Ticketing/Event/Services/PublicEventService.php @@ -0,0 +1,51 @@ +events() + ->whereNotNull('published_at') + ->where('published_at', '<=', now()); + + if ($categoryId !== null) { + abort_unless($tenant->eventCategories()->whereKey($categoryId)->exists(), 404); + $query->where('event_category_id', $categoryId); + } + + if ($term !== '') { + $query->where(function ($query) use ($term): void { + $query->where('title', 'like', '%'.$term.'%') + ->orWhere('subtitle', 'like', '%'.$term.'%') + ->orWhere('location', 'like', '%'.$term.'%'); + }); + } + + return $query->with('attachment')->orderByDesc('published_at')->orderByDesc('id')->paginate(15, ['*'], 'page', $page); + } + + public function find(Tenant $tenant, Event $event): Event + { + abort_unless($event->tenant_code === $tenant->codigo + && $event->published_at !== null + && $event->published_at->isPast(), 404); + + return $event->load([ + 'attachment', + 'dates' => fn ($query) => $query->whereNull('rescheduled_to_event_date_id')->whereNull('suspended_at'), + 'socialMedia', + 'catalogItems.attachments', + 'catalogItems.inventory', + 'catalogItems.itemAttributes.attribute', + 'catalogItems.variants.inventory', + 'catalogItems.variants.eventDate', + 'catalogItems.variants.eventDates', + ]); + } +} diff --git a/app/Domains/Ticketing/Event/documentacion/README.md b/app/Domains/Ticketing/Event/documentacion/README.md new file mode 100644 index 0000000..97ac8b4 --- /dev/null +++ b/app/Domains/Ticketing/Event/documentacion/README.md @@ -0,0 +1,55 @@ +# Dominio Event + +## Propósito + +Administra los eventos de un tenant, sus fechas y sus redes sociales. + +## Modelo + +`Tenant` tiene muchos `Event`. Cada evento tiene título, subtítulo, descripción, +ubicación, texto de fechas y `published_at`. Sus fechas se guardan en `event_dates` +mediante `event_id`. El catálogo `social_media` define las plataformas; +`event_social_media` guarda la URL y el orden de cada cuenta del evento. +`exact_location` guarda opcionalmente un objeto JSON con `latitude` (-90 a 90) +y `longitude` (-180 a 180). `location` sigue siendo la dirección legible. + +La migración `2026_09_18_000500_restore_events` crea un evento por cada tenant que +tenía datos de evento o fechas, y migra las fechas y redes correspondientes. +`2026_09_18_000600_associate_active_event_social_media` copia al evento activo las +redes del tenant que falten, sin sobrescribir las URL propias del evento. El seeder +de Fiesta Fútbol Infantil hace lo mismo cuando crea el evento en una base nueva. + +## Storefront + +- `onticket` muestra el evento señalado por `tenants.active_event_id`. +- `onticket_multi_event` permite varios eventos publicados y mantiene + `tenants.active_event_id` en `NULL`. La presentación y gestión de esos eventos + en el frontend quedan pendientes de diseño. + +`active_event_id` selecciona el evento del storefront de evento único; no indica +si está publicado. La asignación de este campo y los cambios de tipo de storefront +se administran directamente en la base de datos. La aplicación no valida ni +automatiza esas operaciones. + +Cuando hay un evento activo, el campo `social_media` del bootstrap usa sus redes, +igual que `event.social_media`. Las actualizaciones de redes de ese tenant también +se guardan en el evento. Sin evento activo se conservan las redes propias del tenant. + +## API + +Los endpoints de AdminApp usan `auth:sanctum` y `adminapp.tenant`: + +- `GET/PUT /v1/adminapp/tenant/event`: acceso al evento seleccionado para el + storefront de evento único. +- `POST /v1/adminapp/tenant/event-dates`: crea una fecha para ese evento. +- `POST /v1/adminapp/tenant/event-dates/{eventDate}/reschedule` y + `POST /v1/adminapp/tenant/event-dates/{eventDate}/suspend`: cambios de fecha. + +El bootstrap del tenant entrega `event` para el evento seleccionado. No entrega +una lista de eventos ni existe todavía una pantalla para gestionarlos. + +## Dependencias + +Las fechas se vinculan con variantes de `Catalog`, que a su vez pueden generar +tickets. Los avisos por suspensión y reprogramación se construyen dinámicamente +después de excluir los cambios que el usuario ya vio tres veces. diff --git a/app/Domains/Event/routes/adminapp.php b/app/Domains/Ticketing/Event/routes/adminapp.php similarity index 89% rename from app/Domains/Event/routes/adminapp.php rename to app/Domains/Ticketing/Event/routes/adminapp.php index 529e0b6..a975e98 100644 --- a/app/Domains/Event/routes/adminapp.php +++ b/app/Domains/Ticketing/Event/routes/adminapp.php @@ -1,6 +1,6 @@ scopeBindings()->group(function (): void { + Route::get('events', [PublicEventController::class, 'index']); + Route::get('events/{event:slug}', [PublicEventController::class, 'show']); +}); + +Route::middleware('auth:sanctum')->post( + 'tenants/{tenant:codigo}/event-date-notices/claim', + [EventDateNoticeController::class, 'claim'], +); diff --git a/app/Domains/FiestaFutbolInfantil/Controllers/AccommodationController.php b/app/Domains/Ticketing/FiestaFutbolInfantil/Controllers/AccommodationController.php similarity index 77% rename from app/Domains/FiestaFutbolInfantil/Controllers/AccommodationController.php rename to app/Domains/Ticketing/FiestaFutbolInfantil/Controllers/AccommodationController.php index 804c320..fb9ca45 100644 --- a/app/Domains/FiestaFutbolInfantil/Controllers/AccommodationController.php +++ b/app/Domains/Ticketing/FiestaFutbolInfantil/Controllers/AccommodationController.php @@ -1,10 +1,10 @@ where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->where('slug', 'alojamiento') ->with([ 'itemAttributes.attribute.options', @@ -126,7 +126,7 @@ class AccommodationService 'nombre' => 'Alojamientos', ]); $accommodation = CatalogItem::withTrashed() - ->where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->where('slug', 'alojamiento') ->lockForUpdate() ->first(); diff --git a/app/Domains/FiestaFutbolInfantil/Services/CategoryVisibilityService.php b/app/Domains/Ticketing/FiestaFutbolInfantil/Services/CategoryVisibilityService.php similarity index 74% rename from app/Domains/FiestaFutbolInfantil/Services/CategoryVisibilityService.php rename to app/Domains/Ticketing/FiestaFutbolInfantil/Services/CategoryVisibilityService.php index d1fb4d5..99198ba 100644 --- a/app/Domains/FiestaFutbolInfantil/Services/CategoryVisibilityService.php +++ b/app/Domains/Ticketing/FiestaFutbolInfantil/Services/CategoryVisibilityService.php @@ -1,10 +1,10 @@ where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->whereHas('category', fn ($query) => $query->where('nombre', 'Entradas')) ->whereHas('variants', fn ($query) => $query ->whereNull('replaced_by_variant_id') @@ -84,7 +84,7 @@ class EntryService { $entry = CatalogItem::query() ->whereKey($entryId) - ->where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->whereHas('category', fn ($query) => $query->where('nombre', 'Entradas')) ->firstOrFail(); @@ -96,7 +96,7 @@ class EntryService { $catalogItem = CatalogItem::query() ->whereKey($entry['id']) - ->where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->whereHas('category', fn ($query) => $query->where('nombre', 'Entradas')) ->lockForUpdate() ->firstOrFail(); diff --git a/app/Domains/FiestaFutbolInfantil/Services/FoodService.php b/app/Domains/Ticketing/FiestaFutbolInfantil/Services/FoodService.php similarity index 95% rename from app/Domains/FiestaFutbolInfantil/Services/FoodService.php rename to app/Domains/Ticketing/FiestaFutbolInfantil/Services/FoodService.php index d9a4064..8848854 100644 --- a/app/Domains/FiestaFutbolInfantil/Services/FoodService.php +++ b/app/Domains/Ticketing/FiestaFutbolInfantil/Services/FoodService.php @@ -1,19 +1,19 @@ where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->where('slug', 'comida') ->with([ 'variants.catalogItem', @@ -119,7 +119,7 @@ class FoodService { return DB::transaction(function () use ($tenant, $variants): CatalogItem { $food = CatalogItem::query() - ->where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->where('slug', 'comida') ->lockForUpdate() ->firstOrFail(); @@ -233,7 +233,7 @@ class FoodService 'nombre' => 'Comidas', ]); $food = CatalogItem::withTrashed() - ->where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->where('slug', 'comida') ->lockForUpdate() ->first(); diff --git a/app/Domains/FiestaFutbolInfantil/Services/MerchandiseService.php b/app/Domains/Ticketing/FiestaFutbolInfantil/Services/MerchandiseService.php similarity index 95% rename from app/Domains/FiestaFutbolInfantil/Services/MerchandiseService.php rename to app/Domains/Ticketing/FiestaFutbolInfantil/Services/MerchandiseService.php index 29111c8..3d3183c 100644 --- a/app/Domains/FiestaFutbolInfantil/Services/MerchandiseService.php +++ b/app/Domains/Ticketing/FiestaFutbolInfantil/Services/MerchandiseService.php @@ -1,17 +1,17 @@ where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->whereHas('category', fn ($query) => $query->where('nombre', 'Merchandising')) ->with([ 'itemAttributes.attribute.options', @@ -164,7 +164,7 @@ class MerchandiseService ): CatalogItem { $item = CatalogItem::query() ->whereKey($itemId) - ->where('tenant_code', $tenant->codigo) + ->forTenantCatalog($tenant) ->where('category_id', $category->id) ->lockForUpdate() ->first(); diff --git a/app/Domains/FiestaFutbolInfantil/routes/api.php b/app/Domains/Ticketing/FiestaFutbolInfantil/routes/api.php similarity index 91% rename from app/Domains/FiestaFutbolInfantil/routes/api.php rename to app/Domains/Ticketing/FiestaFutbolInfantil/routes/api.php index 242cad0..ff248d9 100644 --- a/app/Domains/FiestaFutbolInfantil/routes/api.php +++ b/app/Domains/Ticketing/FiestaFutbolInfantil/routes/api.php @@ -1,10 +1,10 @@ validated('event_id'); + $tickets = $this->userTickets->tickets( + $tenant, + $request->user(), + $eventId === null ? null : (int) $eventId, + ); + + return TicketResource::collection($tickets)->response(); + } + + public function events(Request $request, Tenant $tenant): AnonymousResourceCollection + { + abort_unless($tenant->storefront_website_type_code === 'onticket_multi_event', 404); + + return TicketEventResource::collection( + $this->userTickets->events($tenant, $request->user()) + ); + } + + public function downloadPdf(DownloadTicketsPdfRequest $request, Tenant $tenant): Response + { + $ticketIds = $request->validated('ticket_ids'); + $tickets = Ticket::query() + ->where('tenant_code', $tenant->codigo) + ->where('user_id', $request->user()->getKey()) + ->whereIn('id', $ticketIds) + ->with([...TicketValidityResolver::RELATIONS, ...TicketPresentationResolver::RELATIONS]) + ->orderByDesc('id') + ->get(); + + if ($tickets->count() !== count($ticketIds)) { + throw new TicketNotAvailableException(__('api.ticket.not_available')); + } + + return $this->ticketPdfService->download($tenant, $tickets); + } +} diff --git a/app/Domains/Ticket/Enums/ScanAttemptResult.php b/app/Domains/Ticketing/Ticket/Enums/ScanAttemptResult.php similarity index 89% rename from app/Domains/Ticket/Enums/ScanAttemptResult.php rename to app/Domains/Ticketing/Ticket/Enums/ScanAttemptResult.php index 6516803..f386bd2 100644 --- a/app/Domains/Ticket/Enums/ScanAttemptResult.php +++ b/app/Domains/Ticketing/Ticket/Enums/ScanAttemptResult.php @@ -1,6 +1,6 @@ 'integer', 'source_catalog_item_id' => 'integer', 'source_variant_id' => 'integer', 'source_purchase_item_id' => 'integer', @@ -136,6 +139,12 @@ class Ticket extends Model return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo'); } + /** @return BelongsTo */ + public function event(): BelongsTo + { + return $this->belongsTo(Event::class); + } + public function allow_refund(): bool { return $this->tenant?->allow_refund() ?? false; diff --git a/app/Domains/Ticket/Models/TicketRefund.php b/app/Domains/Ticketing/Ticket/Models/TicketRefund.php similarity index 91% rename from app/Domains/Ticket/Models/TicketRefund.php rename to app/Domains/Ticketing/Ticket/Models/TicketRefund.php index 6af68f6..c6760c2 100644 --- a/app/Domains/Ticket/Models/TicketRefund.php +++ b/app/Domains/Ticketing/Ticket/Models/TicketRefund.php @@ -1,9 +1,9 @@ type === ValidityTimeType::FixedWindow) { return $this->fixed_starts_at; } - return $this->atCurrentDate($this->start_time, $at); + return $this->atCurrentDate($this->start_time, $at, $timezone); } public function expiresAt( ?CarbonInterface $at = null, + string $timezone = 'UTC', ): ?CarbonInterface { if ($this->type === ValidityTimeType::FixedWindow) { return $this->fixed_expires_at; } - return $this->atCurrentDate($this->end_time, $at); + return $this->atCurrentDate($this->end_time, $at, $timezone); } public function isValid( ?CarbonInterface $at = null, + string $timezone = 'UTC', ): bool { $at ??= now(); - $startsAt = $this->startsAt($at); - $expiresAt = $this->expiresAt($at); + $startsAt = $this->startsAt($at, $timezone); + $expiresAt = $this->expiresAt($at, $timezone); + + if ($this->type === ValidityTimeType::TimeWindow + && $startsAt !== null && $expiresAt !== null && $expiresAt->lessThanOrEqualTo($startsAt)) { + if ($at->lessThan($expiresAt)) { + $startsAt = $startsAt->subDay(); + } else { + $expiresAt = $expiresAt->addDay(); + } + } return ($startsAt === null || $startsAt->lessThanOrEqualTo($at)) && ($expiresAt === null || $expiresAt->greaterThan($at)); @@ -79,6 +91,7 @@ class ValidityTime extends Model private function atCurrentDate( ?string $time, ?CarbonInterface $at, + string $timezone, ): ?CarbonInterface { if ($time === null) { return null; @@ -86,8 +99,9 @@ class ValidityTime extends Model $at ??= now(); $localDate = CarbonImmutable::instance($at) + ->setTimezone($timezone) ->format('Y-m-d'); - return CarbonImmutable::parse($localDate.' '.$time); + return CarbonImmutable::parse($localDate.' '.$time, $timezone); } } diff --git a/app/Domains/Ticket/Requests/AdminAppTicketExportRequest.php b/app/Domains/Ticketing/Ticket/Requests/AdminAppTicketExportRequest.php similarity index 77% rename from app/Domains/Ticket/Requests/AdminAppTicketExportRequest.php rename to app/Domains/Ticketing/Ticket/Requests/AdminAppTicketExportRequest.php index a55a60c..30394bf 100644 --- a/app/Domains/Ticket/Requests/AdminAppTicketExportRequest.php +++ b/app/Domains/Ticketing/Ticket/Requests/AdminAppTicketExportRequest.php @@ -1,8 +1,8 @@ */ + public function rules(): array + { + /** @var Tenant $tenant */ + $tenant = $this->route('tenant'); + + return [ + 'event_id' => [ + 'sometimes', + 'integer', + Rule::exists('events', 'id')->where('tenant_code', $tenant->codigo), + ], + ]; + } +} diff --git a/app/Domains/Ticket/Requests/ScanAttemptIndexRequest.php b/app/Domains/Ticketing/Ticket/Requests/ScanAttemptIndexRequest.php similarity index 91% rename from app/Domains/Ticket/Requests/ScanAttemptIndexRequest.php rename to app/Domains/Ticketing/Ticket/Requests/ScanAttemptIndexRequest.php index 4bb06af..3aaab3a 100644 --- a/app/Domains/Ticket/Requests/ScanAttemptIndexRequest.php +++ b/app/Domains/Ticketing/Ticket/Requests/ScanAttemptIndexRequest.php @@ -1,6 +1,6 @@ */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'title' => $this->title, + 'image' => $this->attachment?->getTemporaryUrl(1440), + ]; + } +} diff --git a/app/Domains/Ticket/Resources/TicketResource.php b/app/Domains/Ticketing/Ticket/Resources/TicketResource.php similarity index 91% rename from app/Domains/Ticket/Resources/TicketResource.php rename to app/Domains/Ticketing/Ticket/Resources/TicketResource.php index 16f7531..4b94aee 100644 --- a/app/Domains/Ticket/Resources/TicketResource.php +++ b/app/Domains/Ticketing/Ticket/Resources/TicketResource.php @@ -1,8 +1,8 @@ $this->id, 'tenant_code' => $this->tenant_code, + 'event_id' => $this->event_id, 'ticket' => $this->ticket, 'status' => $this->status, 'status_label' => $this->status_label, diff --git a/app/Domains/Ticket/Resources/ValidityTimeResource.php b/app/Domains/Ticketing/Ticket/Resources/ValidityTimeResource.php similarity index 69% rename from app/Domains/Ticket/Resources/ValidityTimeResource.php rename to app/Domains/Ticketing/Ticket/Resources/ValidityTimeResource.php index 912182c..dd0da57 100644 --- a/app/Domains/Ticket/Resources/ValidityTimeResource.php +++ b/app/Domains/Ticketing/Ticket/Resources/ValidityTimeResource.php @@ -1,15 +1,20 @@ */ public function toArray(Request $request): array { @@ -27,7 +32,7 @@ class ValidityTimeResource extends JsonResource return [ 'id' => $this->id, 'type' => $this->type->value, - 'is_valid' => $this->isValid(), + 'is_valid' => $this->isValid(timezone: $this->timezone), ...array_filter($fields, fn (mixed $value): bool => $value !== null), ]; } diff --git a/app/Domains/Ticket/Services/AdminAppTicketColumnService.php b/app/Domains/Ticketing/Ticket/Services/AdminAppTicketColumnService.php similarity index 97% rename from app/Domains/Ticket/Services/AdminAppTicketColumnService.php rename to app/Domains/Ticketing/Ticket/Services/AdminAppTicketColumnService.php index 173db1b..ba2f8b0 100644 --- a/app/Domains/Ticket/Services/AdminAppTicketColumnService.php +++ b/app/Domains/Ticketing/Ticket/Services/AdminAppTicketColumnService.php @@ -1,8 +1,8 @@ $ids + * @return array> + */ + public function run(array $ids, string $timezone, bool $apply = false, bool $all = false, bool $reverse = false): array + { + if (! in_array($timezone, DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC), true)) { + throw new InvalidArgumentException('Zona horaria inválida.'); + } + if ($all === ($ids !== [])) { + throw new InvalidArgumentException('Indicar --all o --ids, exclusivamente una opción.'); + } + + return DB::transaction(function () use ($ids, $timezone, $apply, $all, $reverse): array { + $query = DB::table('validity_times')->orderBy('id'); + $all ? $query->where('type', 'fixed_window') : $query->whereIn('id', $ids); + if ($apply) { + $query->lockForUpdate(); + } + $rows = $query->get(); + if (! $all && $rows->count() !== count(array_unique($ids))) { + throw new InvalidArgumentException('Uno o más IDs no existen.'); + } + if ($rows->contains(fn ($row): bool => $row->type !== 'fixed_window')) { + throw new InvalidArgumentException('Sólo se admiten IDs de fixed_window.'); + } + $source = $reverse ? 'UTC' : $timezone; + $target = $reverse ? $timezone : 'UTC'; + $result = []; + foreach ($rows as $row) { + $convert = fn (?string $value): ?string => $value === null + ? null + : CarbonImmutable::parse($value, $source)->setTimezone($target)->format('Y-m-d H:i:s'); + $start = $convert($row->fixed_starts_at); + $end = $convert($row->fixed_expires_at); + $result[] = [ + 'id' => $row->id, + 'original_start' => $row->fixed_starts_at, + 'original_end' => $row->fixed_expires_at, + 'result_start' => $start, + 'result_end' => $end, + ]; + if ($apply) { + DB::table('validity_times')->where('id', $row->id)->update([ + 'fixed_starts_at' => $start, + 'fixed_expires_at' => $end, + ]); + } + } + + return $result; + }); + } +} diff --git a/app/Domains/Ticket/Services/LoadTestTicketDatasetService.php b/app/Domains/Ticketing/Ticket/Services/LoadTestTicketDatasetService.php similarity index 95% rename from app/Domains/Ticket/Services/LoadTestTicketDatasetService.php rename to app/Domains/Ticketing/Ticket/Services/LoadTestTicketDatasetService.php index 89bf279..07edadc 100644 --- a/app/Domains/Ticket/Services/LoadTestTicketDatasetService.php +++ b/app/Domains/Ticketing/Ticket/Services/LoadTestTicketDatasetService.php @@ -1,13 +1,13 @@ $validityTimes */ - public function __construct(public Collection $validityTimes) {} + public function __construct(public Collection $validityTimes, public string $timezone = 'UTC') {} /** Comprueba si el instante pertenece a la intersección efectiva del grupo. */ public function isValid(?CarbonInterface $at = null): bool @@ -50,7 +50,17 @@ final readonly class ResolvedValidityGroup $anchor = $this->dateAnchor() ?? $at; return $this->validityTimes - ->map(fn (ValidityTime $validityTime): ?CarbonInterface => $validityTime->startsAt($anchor)) + ->map(function (ValidityTime $validityTime) use ($anchor): ?CarbonInterface { + $start = $validityTime->startsAt($anchor, $this->timezone); + $end = $validityTime->expiresAt($anchor, $this->timezone); + + if ($this->dateAnchor() === null && $start !== null && $end !== null + && $end->lessThanOrEqualTo($start) && $anchor->lessThan($end)) { + return $start->copy()->subDay(); + } + + return $start; + }) ->filter() ->sortByDesc(fn (CarbonInterface $startsAt): int => $startsAt->getTimestamp()) ->first(); @@ -67,11 +77,13 @@ final readonly class ResolvedValidityGroup return $this->validityTimes ->map(function (ValidityTime $validityTime) use ($anchor): ?CarbonInterface { - $startsAt = $validityTime->startsAt($anchor); - $expiresAt = $validityTime->expiresAt($anchor); + $startsAt = $validityTime->startsAt($anchor, $this->timezone); + $expiresAt = $validityTime->expiresAt($anchor, $this->timezone); if ($startsAt !== null && $expiresAt !== null && $expiresAt->lessThanOrEqualTo($startsAt)) { - return $expiresAt->addDay(); + return $this->dateAnchor() === null && $anchor->lessThan($expiresAt) + ? $expiresAt + : $expiresAt->copy()->addDay(); } return $expiresAt; diff --git a/app/Domains/Ticket/Services/ScannerTicketService.php b/app/Domains/Ticketing/Ticket/Services/ScannerTicketService.php similarity index 97% rename from app/Domains/Ticket/Services/ScannerTicketService.php rename to app/Domains/Ticketing/Ticket/Services/ScannerTicketService.php index e696de6..9b3b3b8 100644 --- a/app/Domains/Ticket/Services/ScannerTicketService.php +++ b/app/Domains/Ticketing/Ticket/Services/ScannerTicketService.php @@ -1,11 +1,11 @@ create([ 'tenant_code' => $item->tenant_code, + 'event_id' => $item->event_id, 'ticket' => (string) Str::uuid(), 'source_purchase_item_id' => $sourcePurchaseItemId, 'source_catalog_item_id' => $item->getKey(), diff --git a/app/Domains/Ticket/Services/TicketPdfService.php b/app/Domains/Ticketing/Ticket/Services/TicketPdfService.php similarity index 95% rename from app/Domains/Ticket/Services/TicketPdfService.php rename to app/Domains/Ticketing/Ticket/Services/TicketPdfService.php index 211a4cf..b2fd900 100644 --- a/app/Domains/Ticket/Services/TicketPdfService.php +++ b/app/Domains/Ticketing/Ticket/Services/TicketPdfService.php @@ -1,9 +1,9 @@ loadMissing([ + 'catalogItem.tenant', 'eventDates.validityTime', 'eventDate.validityTime', 'definitions.itemAttribute.attribute.options.validityTime', @@ -149,7 +151,7 @@ class TicketValidityResolver } return new ResolvedTicketValidity( - $groups->map(fn (Collection $times): ResolvedValidityGroup => new ResolvedValidityGroup($times)) + $groups->map(fn (Collection $times): ResolvedValidityGroup => new ResolvedValidityGroup($times, $variant->catalogItem?->tenant?->timezone ?? 'UTC')) ); } } diff --git a/app/Domains/Ticketing/Ticket/Services/UserTicketQueryService.php b/app/Domains/Ticketing/Ticket/Services/UserTicketQueryService.php new file mode 100644 index 0000000..be43822 --- /dev/null +++ b/app/Domains/Ticketing/Ticket/Services/UserTicketQueryService.php @@ -0,0 +1,45 @@ + */ + public function tickets(Tenant $tenant, User $user, ?int $eventId = null): Collection + { + return $this->ticketQuery($tenant, $user) + ->when($eventId !== null, fn (Builder $query): Builder => $query + ->where('event_id', $eventId)) + ->with([...TicketValidityResolver::RELATIONS, ...TicketPresentationResolver::RELATIONS]) + ->orderByDesc('id') + ->get(); + } + + /** @return Collection */ + public function events(Tenant $tenant, User $user): Collection + { + return Event::query() + ->where('tenant_code', $tenant->codigo) + ->whereHas('tickets', fn (Builder $query): Builder => $query + ->where('tenant_code', $tenant->codigo) + ->where('user_id', $user->getKey())) + ->with('attachment') + ->orderByDesc('id') + ->get(); + } + + /** @return Builder */ + private function ticketQuery(Tenant $tenant, User $user): Builder + { + return Ticket::query() + ->where('tenant_code', $tenant->codigo) + ->where('user_id', $user->getKey()); + } +} diff --git a/app/Domains/Ticketing/Ticket/documentacion/README.md b/app/Domains/Ticketing/Ticket/documentacion/README.md new file mode 100644 index 0000000..e460d5c --- /dev/null +++ b/app/Domains/Ticketing/Ticket/documentacion/README.md @@ -0,0 +1,139 @@ +# 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, y conserva referencias al ítem de compra que lo generó, producto, + variante y usuario escáner. La compra se obtiene a través de su ítem. +- El nombre y la descripción se calculan dinámicamente desde el producto y la variante; los tickets no + persisten una copia de esos textos. +- `ValidityTime`: define ventanas absolutas o relativas de vigencia para fechas de evento y opciones de atributos. +- `ValidityTimeType`: enum de estrategias de vigencia. + +`TicketValidityResolver` deriva la vigencia desde la variante asociada. Las alternativas de un mismo atributo +se combinan con OR y las dimensiones diferentes se combinan con AND. El modelo calcula si un ticket está +vigente, vencido o usado, y resuelve sus fechas efectivas de inicio y fin sin persistir vigencias en el ticket. + +## Zona horaria y vigencias + +La zona se configura por tenant mediante `timezone` (identificador IANA, por ejemplo +`America/Argentina/Buenos_Aires`), configurado mediante seeders o base de datos y expuesto en la respuesta de tenants. +Ese es el valor por defecto. La aplicación mantiene su zona global en UTC. + +Las fechas y horas de `EventDate` y los horarios de `time_window` son locales al tenant. +Las ventanas `fixed_window` se guardan en UTC. El resolver pasa la zona del tenant del producto +al grupo de vigencia: antes de combinar fecha y hora, convierte el ancla a esa zona. +Sin fecha de evento, utiliza el día local del instante consultado; contempla ventanas nocturnas. +El inicio es inclusivo y el vencimiento exclusivo. + +Para validar una ventana horaria directamente, pasar la zona explícitamente: +`$validityTime->isValid(now(), $tenant->timezone)`. Un `ValidityTime` aislado no tiene tenant; +los métodos mantienen UTC como valor por defecto para usos sin contexto. + +La migración `2026_09_21_040000_add_timezone_to_tenants.php` asigna la zona inicial y reconstruye +una sola vez las ventanas asociadas a fechas existentes, usando las fechas y horas locales originales. +No modifica ventanas independientes. Los cambios posteriores de zona son responsabilidad del tenant: +reinterpretan los horarios locales, pero no reescriben los instantes UTC guardados. Editar la fecha +u horas de un evento sí vuelve a calcular su ventana. El rollback elimina la columna, sin deshacer +la corrección de los instantes. Ya no se utiliza `EVENT_TIMEZONE`. + +## 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. `Notification` envía la confirmación de compra después de la generación y adjunta los tickets cuando existen. + +## Datos descartables para pruebas de carga + +En ambientes `local`, `testing`, `staging`, `homo` u `homologation`, el comando siguiente crea tickets +válidos, identidades scanner con tokens Sanctum y un dataset JSON importable por Postman: + +```bash +php artisan load-test:tickets:prepare loadtest-evento \ + --tickets=40000 \ + --scanners=100 \ + --owners=1000 \ + --catalog-item=123 \ + --run=evento-001 +``` + +El tenant debe existir y se recomienda que sea exclusivo para carga. Si no se indica `--catalog-item`, +se usa el primer producto estándar del tenant con tickets habilitados. `--variant` +es opcional; al indicarlo, su configuración de vigencia debe estar activa y ser resoluble. Sin variante, +los tickets tienen vigencia irrestricta. + +El archivo se escribe por defecto en `storage/app/private/load-tests/` y contiene tokens secretos, por +lo que no debe versionarse. Para limpiar el tenant después de la ejecución: + +```bash +php artisan tenants:reset-transactions loadtest-evento --dry-run +php artisan tenants:reset-transactions loadtest-evento +``` + +## Endpoints + +Bajo `/tenants/{tenant:codigo}`, protegidos por `auth:sanctum`: + +- `GET /tickets`. +- `POST /tickets/pdf`. + +Bajo `/v1/adminapp/tenant`, protegido por `auth:sanctum`, `adminapp.tenant` y el menú +`adminapp.tickets`: + +- `GET /tickets`, paginado y con búsqueda opcional mediante `q`. La respuesta incluye + `scanned_tickets` y `total_tickets` para el tenant autenticado. + +`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. + +## Conversión de datos históricos a UTC (homo / producción) + +El comando no requiere migraciones adicionales ni crea una tabla de registro. +Para convertir todos los registros de tipo `fixed_window` desde hora argentina: + +```bash +# Vista previa: no escribe en la base +php artisan tickets:convert-validity-times-to-utc --all +# Aplicar a todos los fixed_window +php artisan tickets:convert-validity-times-to-utc --all --apply +``` + +También permite seleccionar IDs específicos (los siguientes son ejemplos): + +```bash +php artisan tickets:convert-validity-times-to-utc --ids=41,42 +php artisan tickets:convert-validity-times-to-utc --ids=41,42 --apply +``` + +`--all` y `--ids` son excluyentes. El origen por defecto es +`America/Argentina/Buenos_Aires`; se puede indicar otra zona IANA con `--source-timezone`. +Cada extremo conserva su fecha original y se convierte por separado, incluso si la ventana abarca +varios días. Los límites nulos y los `time_window` no se modifican. La aplicación es transaccional. + +No hay registro ni detección automática de conversiones anteriores: repetir `--apply` vuelve a +convertir los valores actuales. La migración original `2026_09_21_040000_add_timezone_to_tenants.php` +ya convierte las ventanas asociadas a eventos; no ejecutar este comando sobre esas ventanas si +ya fueron convertidas. `--all` incluye absolutamente todos los `fixed_window`, sin esa distinción. + +### Revertir una conversión + +`--reverse` convierte los valores actuales desde UTC hacia la zona local, con el mismo alcance +`--all` o `--ids`. Sin `--apply` sólo muestra la vista previa. + +```bash +php artisan tickets:convert-validity-times-to-utc --all --reverse +php artisan tickets:convert-validity-times-to-utc --all --reverse --apply +php artisan tickets:convert-validity-times-to-utc --ids=41,42 --reverse --apply +``` + +Con `--reverse`, `--source-timezone` indica la zona local de destino (por defecto +`America/Argentina/Buenos_Aires`). Las fechas de cada extremo y los límites nulos se respetan; +los `time_window` no se modifican. Revierte una conversión si se seleccionan los mismos registros +sin ediciones intermedias. No recupera una copia histórica ni detecta conversiones previas. diff --git a/app/Domains/Ticket/routes/adminapp.php b/app/Domains/Ticketing/Ticket/routes/adminapp.php similarity index 95% rename from app/Domains/Ticket/routes/adminapp.php rename to app/Domains/Ticketing/Ticket/routes/adminapp.php index 66abe50..b785612 100644 --- a/app/Domains/Ticket/routes/adminapp.php +++ b/app/Domains/Ticketing/Ticket/routes/adminapp.php @@ -1,6 +1,6 @@ middleware('auth:sanctum') ->group(function (): void { + Route::get('tickets/events', [TicketController::class, 'events']); Route::get('tickets', [TicketController::class, 'index']); Route::post('tickets/pdf', [TicketController::class, 'downloadPdf']); }); diff --git a/app/Domains/Ticket/routes/scanner.php b/app/Domains/Ticketing/Ticket/routes/scanner.php similarity index 80% rename from app/Domains/Ticket/routes/scanner.php rename to app/Domains/Ticketing/Ticket/routes/scanner.php index a92ccbb..e110a23 100644 --- a/app/Domains/Ticket/routes/scanner.php +++ b/app/Domains/Ticketing/Ticket/routes/scanner.php @@ -1,7 +1,7 @@ temporaryUrl( + return app(TemporaryUrlService::class)->generate( $this->path, - now()->addMinutes($expiresInMinutes) - ); + $expiresInMinutes, + )['temporary_url']; } } diff --git a/app/Domains/Attachable/Models/AttachmentCrop.php b/app/Shared/Attachable/Models/AttachmentCrop.php similarity index 95% rename from app/Domains/Attachable/Models/AttachmentCrop.php rename to app/Shared/Attachable/Models/AttachmentCrop.php index 7487d25..f789a5b 100644 --- a/app/Domains/Attachable/Models/AttachmentCrop.php +++ b/app/Shared/Attachable/Models/AttachmentCrop.php @@ -1,6 +1,6 @@ } */ public function get(Tenant $tenant): array { - $urls = $tenant->socialMedia() - ->pluck('tenant_social_media.url', 'social_media.code'); + $event = $tenant->activeEvent; + $urls = $event?->socialMedia() + ->pluck('event_social_media.url', 'social_media.code') ?? collect(); return [ 'social_media' => SocialMedia::query() diff --git a/app/Domains/Forms/Services/FoodFormService.php b/app/Shared/Forms/Services/FoodFormService.php similarity index 81% rename from app/Domains/Forms/Services/FoodFormService.php rename to app/Shared/Forms/Services/FoodFormService.php index 5ed7636..27945af 100644 --- a/app/Domains/Forms/Services/FoodFormService.php +++ b/app/Shared/Forms/Services/FoodFormService.php @@ -1,12 +1,12 @@ integrations()->with('integrationInstance')->get()); + } + + public function show(AdminWebsiteType $adminWebsiteType, string $integrationCode) + { + return new IntegrationAssociationResource($adminWebsiteType->integrations()->with('integrationInstance')->where('integration_code', $integrationCode)->firstOrFail()); + } + + public function store(ConfigureIntegrationRequest $request, AdminWebsiteType $adminWebsiteType, string $integrationCode) + { + $integration = Integration::query()->where('integration_code', $integrationCode)->firstOrFail(); + + return new IntegrationAssociationResource($this->service->configure( + $adminWebsiteType, + $integration, + $request->validated('integration_data'), + )); + } + + public function destroy(AdminWebsiteType $adminWebsiteType, string $integrationCode) + { + $this->service->detach($adminWebsiteType, $integrationCode); + + return response()->noContent(); + } +} diff --git a/app/Domains/Integration/Controllers/ClientIntegrationController.php b/app/Shared/Integration/Controllers/ClientIntegrationController.php similarity index 86% rename from app/Domains/Integration/Controllers/ClientIntegrationController.php rename to app/Shared/Integration/Controllers/ClientIntegrationController.php index bbc3bee..772c499 100644 --- a/app/Domains/Integration/Controllers/ClientIntegrationController.php +++ b/app/Shared/Integration/Controllers/ClientIntegrationController.php @@ -1,12 +1,12 @@ */ - public function websiteType(): BelongsTo + /** @return BelongsTo */ + public function adminWebsiteType(): BelongsTo { - return $this->belongsTo(WebsiteType::class, 'website_type_code', 'codigo'); + return $this->belongsTo(AdminWebsiteType::class, 'admin_website_type_code', 'codigo'); } /** @return BelongsTo */ diff --git a/app/Domains/Integration/Models/ClientIntegration.php b/app/Shared/Integration/Models/ClientIntegration.php similarity index 90% rename from app/Domains/Integration/Models/ClientIntegration.php rename to app/Shared/Integration/Models/ClientIntegration.php index 999b7ce..d70d672 100644 --- a/app/Domains/Integration/Models/ClientIntegration.php +++ b/app/Shared/Integration/Models/ClientIntegration.php @@ -1,8 +1,8 @@ hasMany(ClientIntegration::class, 'integration_code', 'integration_code'); } - /** @return HasMany */ + /** @return HasMany */ public function websiteTypeIntegrations(): HasMany { - return $this->hasMany(WebsiteTypeIntegration::class, 'integration_code', 'integration_code'); + return $this->hasMany(AdminWebsiteTypeIntegration::class, 'integration_code', 'integration_code'); } } diff --git a/app/Domains/Integration/Models/IntegrationInstance.php b/app/Shared/Integration/Models/IntegrationInstance.php similarity index 82% rename from app/Domains/Integration/Models/IntegrationInstance.php rename to app/Shared/Integration/Models/IntegrationInstance.php index db242eb..4a5a8a1 100644 --- a/app/Domains/Integration/Models/IntegrationInstance.php +++ b/app/Shared/Integration/Models/IntegrationInstance.php @@ -1,8 +1,8 @@ hasMany(ClientIntegration::class); } - /** @return HasMany */ + /** @return HasMany */ public function websiteTypeIntegrations(): HasMany { - return $this->hasMany(WebsiteTypeIntegration::class); + return $this->hasMany(AdminWebsiteTypeIntegration::class); } } diff --git a/app/Domains/Integration/Policies/IntegrationPolicy.php b/app/Shared/Integration/Policies/IntegrationPolicy.php similarity index 53% rename from app/Domains/Integration/Policies/IntegrationPolicy.php rename to app/Shared/Integration/Policies/IntegrationPolicy.php index b4bff7c..ab7b5d1 100644 --- a/app/Domains/Integration/Policies/IntegrationPolicy.php +++ b/app/Shared/Integration/Policies/IntegrationPolicy.php @@ -1,9 +1,9 @@ $this->id, 'client_id' => $this->when(isset($this->client_id), $this->client_id), - 'website_type_code' => $this->when(isset($this->website_type_code), $this->website_type_code), + 'admin_website_type_code' => $this->when(isset($this->admin_website_type_code), $this->admin_website_type_code), 'integration_code' => $this->integration_code, 'integration_instance_id' => $this->integration_instance_id, 'integration_instance' => new IntegrationInstanceResource($this->whenLoaded('integrationInstance')), diff --git a/app/Domains/Integration/Resources/IntegrationInstanceResource.php b/app/Shared/Integration/Resources/IntegrationInstanceResource.php similarity index 91% rename from app/Domains/Integration/Resources/IntegrationInstanceResource.php rename to app/Shared/Integration/Resources/IntegrationInstanceResource.php index a65f359..067515f 100644 --- a/app/Domains/Integration/Resources/IntegrationInstanceResource.php +++ b/app/Shared/Integration/Resources/IntegrationInstanceResource.php @@ -1,6 +1,6 @@ where('integration_code', $this->integrationCode) ->first()?->integrationInstance; - if (! $this->integrationInstance && $this->tenant?->website_type_code) { - $this->integrationInstance = WebsiteTypeIntegration::with('integrationInstance') - ->where('website_type_code', $this->tenant->website_type_code) + if (! $this->integrationInstance && $this->tenant?->admin_website_type_code) { + $this->integrationInstance = AdminWebsiteTypeIntegration::with('integrationInstance') + ->where('admin_website_type_code', $this->tenant->admin_website_type_code) ->where('integration_code', $this->integrationCode) ->first()?->integrationInstance; } diff --git a/app/Domains/Integration/Services/ClientIntegrationService.php b/app/Shared/Integration/Services/ClientIntegrationService.php similarity index 89% rename from app/Domains/Integration/Services/ClientIntegrationService.php rename to app/Shared/Integration/Services/ClientIntegrationService.php index 2f79d8e..80a76b3 100644 --- a/app/Domains/Integration/Services/ClientIntegrationService.php +++ b/app/Shared/Integration/Services/ClientIntegrationService.php @@ -1,10 +1,10 @@ $integration->integration_code, 'name' => $integration->name.' / '.$this->ownerName($owner), @@ -25,9 +25,9 @@ class IntegrationAssociationService }); } - public function associate(Client|WebsiteType $owner, string $code, IntegrationInstance $instance): ClientIntegration|WebsiteTypeIntegration + public function associate(Client|AdminWebsiteType $owner, string $code, IntegrationInstance $instance): ClientIntegration|AdminWebsiteTypeIntegration { - return DB::transaction(function () use ($owner, $code, $instance): ClientIntegration|WebsiteTypeIntegration { + return DB::transaction(function () use ($owner, $code, $instance): ClientIntegration|AdminWebsiteTypeIntegration { $association = $owner->integrations() ->where('integration_code', $code) ->lockForUpdate() @@ -62,7 +62,7 @@ class IntegrationAssociationService }); } - public function detach(Client|WebsiteType $owner, string $code): void + public function detach(Client|AdminWebsiteType $owner, string $code): void { DB::transaction(function () use ($owner, $code): void { $association = $owner->integrations() @@ -91,7 +91,7 @@ class IntegrationAssociationService } } - private function ownerName(Client|WebsiteType $owner): string + private function ownerName(Client|AdminWebsiteType $owner): string { return $owner instanceof Client ? $owner->name : $owner->nombre; } diff --git a/app/Domains/Integration/Services/IntegrationInstanceService.php b/app/Shared/Integration/Services/IntegrationInstanceService.php similarity index 93% rename from app/Domains/Integration/Services/IntegrationInstanceService.php rename to app/Shared/Integration/Services/IntegrationInstanceService.php index 7a86d37..b2176bf 100644 --- a/app/Domains/Integration/Services/IntegrationInstanceService.php +++ b/app/Shared/Integration/Services/IntegrationInstanceService.php @@ -1,8 +1,8 @@ mailer || ! $this->tenant) { @@ -97,7 +97,7 @@ class MailService extends BaseIntegrationService BLADE, [ 'branding' => $branding, - 'headerLogoUrl' => $brand instanceof WebsiteType + 'headerLogoUrl' => $brand instanceof AdminWebsiteType ? $brand->siteLogo?->getTemporaryUrl(1440) : $brand->headerLogo?->getTemporaryUrl(1440), 'footerLogoUrl' => $brand->footerLogo?->getTemporaryUrl(1440), @@ -128,9 +128,9 @@ class MailService extends BaseIntegrationService } /** @return array{name: string, primary_color: string, body_color: string, background_color: string, surface_color: string, header_bg_color: string, footer_bg_color: string} */ - private function brandingFor(Tenant|WebsiteType $brand): array + private function brandingFor(Tenant|AdminWebsiteType $brand): array { - if ($brand instanceof WebsiteType) { + if ($brand instanceof AdminWebsiteType) { $brand->loadMissing(['siteLogo', 'footerLogo']); return [ diff --git a/app/Domains/Integration/Services/TelepagosIntegrationService.php b/app/Shared/Integration/Services/TelepagosIntegrationService.php similarity index 99% rename from app/Domains/Integration/Services/TelepagosIntegrationService.php rename to app/Shared/Integration/Services/TelepagosIntegrationService.php index ef99f7f..83ac810 100644 --- a/app/Domains/Integration/Services/TelepagosIntegrationService.php +++ b/app/Shared/Integration/Services/TelepagosIntegrationService.php @@ -1,6 +1,6 @@ group(function (): void { + Route::prefix('admin-website-types/{adminWebsiteType:codigo}/integrations')->group(function (): void { + Route::get('/', [AdminWebsiteTypeIntegrationController::class, 'index']); + Route::get('/{integration_code}', [AdminWebsiteTypeIntegrationController::class, 'show']); + Route::put('/{integration_code}', [AdminWebsiteTypeIntegrationController::class, 'store']); + Route::delete('/{integration_code}', [AdminWebsiteTypeIntegrationController::class, 'destroy']); + }); + + Route::group(['prefix' => 'clients/{client}/integrations'], function () { + Route::get('/', [ClientIntegrationController::class, 'index']); + Route::get('/{integration_code}', [ClientIntegrationController::class, 'show']); + Route::put('/{integration_code}', [ClientIntegrationController::class, 'store']); + Route::delete('/{integration_code}', [ClientIntegrationController::class, 'destroy']); + }); +}); + +Route::post('webhooks/telepagos/{client}', [TelepagosWebhookController::class, 'handle']); diff --git a/app/Domains/Logging/Enums/ValueChangeActorType.php b/app/Shared/Logging/Enums/ValueChangeActorType.php similarity index 72% rename from app/Domains/Logging/Enums/ValueChangeActorType.php rename to app/Shared/Logging/Enums/ValueChangeActorType.php index 6e3cfcc..784dbe2 100644 --- a/app/Domains/Logging/Enums/ValueChangeActorType.php +++ b/app/Shared/Logging/Enums/ValueChangeActorType.php @@ -1,6 +1,6 @@ $userId, 'tenant_code' => $tenantCode, ]; - $tenant = Tenant::query()->with('websiteType')->where('codigo', $tenantCode)->firstOrFail(); + $tenant = Tenant::query()->where('codigo', $tenantCode)->firstOrFail(); $user = User::query()->findOrFail($userId); $this->sendIdempotently( @@ -41,7 +41,7 @@ class NotificationMailService $context, $user->email, function () use ($user, $tenant, $tenantCode): array { - $brand = $tenant->websiteType ?? $tenant; + $brand = $tenant; $tenantUrl = 'https://'.$tenant->dominio.$tenant->base_path; $this->mailService @@ -54,7 +54,7 @@ class NotificationMailService ); return [ - 'brand_type' => $tenant->websiteType === null ? 'tenant' : 'website_type', + 'brand_type' => 'tenant', ]; }, ); @@ -72,7 +72,7 @@ class NotificationMailService ]; $tenant = Tenant::query() - ->with('websiteType') + ->with('adminWebsiteType') ->where('codigo', $tenantCode) ->firstOrFail(); $attempt = ResetPasswordAttempt::query() @@ -97,8 +97,8 @@ class NotificationMailService $attempt->user->email, function () use ($attempt, $tenant, $tenantCode, $channel): array { $recoveryDomain = match ($channel) { - PasswordResetRequested::CHANNEL_ADMINAPP => $tenant->websiteType?->dominio, - PasswordResetRequested::CHANNEL_SCANNER => $tenant->websiteType?->scanner_domain, + PasswordResetRequested::CHANNEL_ADMINAPP => $tenant->adminWebsiteType?->dominio, + PasswordResetRequested::CHANNEL_SCANNER => $tenant->adminWebsiteType?->scanner_domain, default => $tenant->dominio, }; $recoveryBasePath = $channel === PasswordResetRequested::CHANNEL_STOREFRONT @@ -115,7 +115,9 @@ class NotificationMailService $recoveryUrl = $recoveryDomain === null ? null : 'https://'.$recoveryDomain.$recoveryBasePath.'/recuperar-contrasena/codigo?'.http_build_query($recoveryQuery); - $brand = $tenant->websiteType ?? $tenant; + $brand = $channel === PasswordResetRequested::CHANNEL_STOREFRONT + ? $tenant + : ($tenant->adminWebsiteType ?? $tenant); [$subject, $template] = match ($attempt->reason) { ResetPasswordAttempt::REASON_STAFF_CREATED => [ @@ -291,7 +293,7 @@ class NotificationMailService $newDate, $tickets, ): array { - $brand = $purchase->tenant->websiteType ?? $purchase->tenant; + $brand = $purchase->tenant; $this->mailService ->forTenant($tenantCode) @@ -392,7 +394,7 @@ class NotificationMailService $disabledTickets, $activeTickets, ): array { - $brand = $purchase->tenant->websiteType ?? $purchase->tenant; + $brand = $purchase->tenant; $this->mailService ->forTenant($tenantCode) @@ -419,7 +421,7 @@ class NotificationMailService { return Purchase::query() ->where('tenant_codigo', $tenantCode) - ->with(['tenant.websiteType', 'user']) + ->with(['tenant', 'user']) ->find($purchaseId); } diff --git a/app/Domains/Notification/documentacion/README.md b/app/Shared/Notification/documentacion/README.md similarity index 92% rename from app/Domains/Notification/documentacion/README.md rename to app/Shared/Notification/documentacion/README.md index 683402a..79eeb06 100644 --- a/app/Domains/Notification/documentacion/README.md +++ b/app/Shared/Notification/documentacion/README.md @@ -34,7 +34,7 @@ No expone rutas HTTP. Consume datos de `Auth`, `Tenant`, `Purchase` y `Ticket`, - 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 correos de cuenta (bienvenida y recuperación de contraseña) usan la identidad visual del `WebsiteType` asociado al tenant, con fallback al tenant si no tiene uno configurado. +- La bienvenida y la recuperación del storefront usan la identidad visual del tenant. La recuperación del admin y scanner usa el `AdminWebsiteType`, con fallback al tenant si no tiene uno configurado. - El correo transaccional de compra confirmada usa la identidad visual del tenant y adjunta un único PDF cuando la compra generó tickets. - Los handlers deben permanecer idempotentes o tolerantes a reintentos de cola. - Una entrega queda en estado `processing` mientras un worker posee su claim. Si diff --git a/app/Domains/Shared/Rules/CroppedImageOrBase64Rule.php b/app/Shared/Rules/CroppedImageOrBase64Rule.php similarity index 98% rename from app/Domains/Shared/Rules/CroppedImageOrBase64Rule.php rename to app/Shared/Rules/CroppedImageOrBase64Rule.php index e26f47e..c0aab5f 100644 --- a/app/Domains/Shared/Rules/CroppedImageOrBase64Rule.php +++ b/app/Shared/Rules/CroppedImageOrBase64Rule.php @@ -1,6 +1,6 @@ cacheKey($path, $expiresInMinutes), + now()->addSeconds($cacheTtlInSeconds), + function () use ($path, $expiresInMinutes): array { + $expiresAt = now()->addMinutes($expiresInMinutes); + + return [ + 'temporary_url' => Storage::disk('s3')->temporaryUrl( + $path, + $expiresAt, + [ + 'ResponseCacheControl' => 'private, max-age='.($expiresInMinutes * 60), + ], + ), + 'temporary_url_expires_at' => $expiresAt->toIso8601String(), + ]; + }, + ); + } + + public function forget(string $path, int $expiresInMinutes = 10): void + { + Cache::forget($this->cacheKey($path, $expiresInMinutes)); + } + + protected function cacheKey(string $path, int $expiresInMinutes): string + { + return sprintf( + 's3:temporary-url:%s:%d', + hash('sha256', $path), + $expiresInMinutes, + ); + } +} diff --git a/app/Domains/StorageTest/Controllers/S3TestController.php b/app/Shared/StorageTest/Controllers/S3TestController.php similarity index 85% rename from app/Domains/StorageTest/Controllers/S3TestController.php rename to app/Shared/StorageTest/Controllers/S3TestController.php index c645947..15d7276 100644 --- a/app/Domains/StorageTest/Controllers/S3TestController.php +++ b/app/Shared/StorageTest/Controllers/S3TestController.php @@ -1,11 +1,11 @@ */ @@ -44,8 +49,7 @@ class S3TestService 'mime_type' => $file->getClientMimeType(), 'extension' => $file->extension(), 'size' => $file->getSize(), - 'temporary_url' => $disk->temporaryUrl($path, now()->addMinutes($expiresInMinutes)), - 'temporary_url_expires_at' => now()->addMinutes($expiresInMinutes)->toIso8601String(), + ...$this->temporaryUrlService->generate($path, $expiresInMinutes), ]; } @@ -58,16 +62,10 @@ class S3TestService 'disk' => 's3', 'key' => $path, 'path' => $path, - 'temporary_url' => $this->temporaryUrlForPath($path, $expiresInMinutes), - 'temporary_url_expires_at' => now()->addMinutes($expiresInMinutes)->toIso8601String(), + ...$this->temporaryUrlService->generate($path, $expiresInMinutes), ]; } - protected function temporaryUrlForPath(string $path, int $expiresInMinutes): string - { - return Storage::disk('s3')->temporaryUrl($path, now()->addMinutes($expiresInMinutes)); - } - protected function normalizeDirectory(?string $directory): string { $directory = trim((string) $directory, '/'); diff --git a/app/Domains/StorageTest/documentacion/README.md b/app/Shared/StorageTest/documentacion/README.md similarity index 100% rename from app/Domains/StorageTest/documentacion/README.md rename to app/Shared/StorageTest/documentacion/README.md diff --git a/app/Domains/StorageTest/routes/api.php b/app/Shared/StorageTest/routes/api.php similarity index 81% rename from app/Domains/StorageTest/routes/api.php rename to app/Shared/StorageTest/routes/api.php index ef5ff59..78741e7 100644 --- a/app/Domains/StorageTest/routes/api.php +++ b/app/Shared/StorageTest/routes/api.php @@ -1,6 +1,6 @@ group(function (): void { diff --git a/app/Domains/Shared/documentacion/README.md b/app/Shared/documentacion/README.md similarity index 100% rename from app/Domains/Shared/documentacion/README.md rename to app/Shared/documentacion/README.md diff --git a/bootstrap/app.php b/bootstrap/app.php index 8102d3b..8f4bb7b 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,11 +1,11 @@ orderBy('id')->each(function (object $purchaseItem): void { - if (in_array($purchaseItem->buyable_type, [CatalogItem::class, self::PREVIOUS_CATALOG_ITEM], true)) { + if (in_array($purchaseItem->buyable_type, [CatalogItem::class, self::ORIGINAL_CATALOG_ITEM, self::PREVIOUS_CATALOG_ITEM], true)) { $catalogItem = DB::table('catalog_items') ->where('id', $purchaseItem->buyable_id) ->first(); @@ -48,7 +52,7 @@ return new class extends Migration } } - if (in_array($purchaseItem->buyable_type, [self::LEGACY_PRODUCT_VARIANT, Variant::class, self::PREVIOUS_VARIANT], true)) { + if (in_array($purchaseItem->buyable_type, [self::LEGACY_PRODUCT_VARIANT, Variant::class, self::ORIGINAL_VARIANT, self::PREVIOUS_VARIANT], true)) { $variant = DB::table('variantes') ->select(['id', 'catalog_item_id']) ->where('id', $purchaseItem->buyable_id) diff --git a/database/migrations/2026_07_20_000600_create_catalog_featured_items_tables.php b/database/migrations/2026_07_20_000600_create_catalog_featured_items_tables.php index bb075ae..d725982 100644 --- a/database/migrations/2026_07_20_000600_create_catalog_featured_items_tables.php +++ b/database/migrations/2026_07_20_000600_create_catalog_featured_items_tables.php @@ -1,6 +1,6 @@ where('trackable_type', (new Purchase)->getMorphClass()) + ->whereIn('trackable_type', [ + 'App\\Domains\\Purchase\\Models\\Purchase', + (new Purchase)->getMorphClass(), + ]) ->whereNull('tenant_code') ->orderBy('id') ->chunkById(500, function ($changes): void { diff --git a/database/migrations/2026_08_04_000200_add_source_to_featured_groups_table.php b/database/migrations/2026_08_04_000200_add_source_to_featured_groups_table.php index 309877e..1498884 100644 --- a/database/migrations/2026_08_04_000200_add_source_to_featured_groups_table.php +++ b/database/migrations/2026_08_04_000200_add_source_to_featured_groups_table.php @@ -1,6 +1,6 @@ addTypeForeignKeys(); + + return; + } + + Schema::create('storefront_website_types', function (Blueprint $table): void { + $table->id(); + $table->string('codigo')->unique(); + $table->string('nombre'); + $table->timestamps(); + }); + + DB::table('website_type')->orderBy('id')->chunk(100, function ($types): void { + foreach ($types as $type) { + DB::table('storefront_website_types')->insert([ + 'id' => $type->id, + 'codigo' => $type->codigo, + 'nombre' => $type->nombre, + 'created_at' => $type->created_at, + 'updated_at' => $type->updated_at, + ]); + } + }); + + Schema::table('tenants', function (Blueprint $table): void { + $table->string('storefront_website_type_code')->nullable(); + }); + + DB::table('tenants')->orderBy('id')->chunk(100, function ($tenants): void { + foreach ($tenants as $tenant) { + $type = $tenant->website_type_code + ? DB::table('website_type')->where('codigo', $tenant->website_type_code)->first() + : null; + + DB::table('tenants')->where('id', $tenant->id)->update([ + 'storefront_website_type_code' => $tenant->website_type_code, + 'site_title' => $tenant->site_title ?? $type?->site_title, + 'favicon_id' => $tenant->favicon_id ?? $type?->favicon_id, + ]); + } + }); + + if (DB::getDriverName() !== 'sqlite') { + Schema::table('tenants', fn (Blueprint $table) => $table->dropForeign(['website_type_code'])); + Schema::table('website_type_extras', fn (Blueprint $table) => $table->dropForeign(['website_type_code'])); + Schema::table('website_type_integrations', fn (Blueprint $table) => $table->dropForeign(['website_type_code'])); + } + + Schema::rename('website_type', 'admin_website_types'); + Schema::rename('website_type_extras', 'storefront_website_type_extras'); + Schema::rename('website_type_integrations', 'admin_website_type_integrations'); + + Schema::table('tenants', fn (Blueprint $table) => $table->renameColumn('website_type_code', 'admin_website_type_code')); + Schema::table('storefront_website_type_extras', fn (Blueprint $table) => $table->renameColumn('website_type_code', 'storefront_website_type_code')); + Schema::table('admin_website_type_integrations', fn (Blueprint $table) => $table->renameColumn('website_type_code', 'admin_website_type_code')); + + if (DB::getDriverName() === 'sqlite') { + Schema::disableForeignKeyConstraints(); + Schema::create('storefront_website_type_extras_rebuilt', function (Blueprint $table): void { + $table->id(); + $table->string('storefront_website_type_code'); + $table->foreign('storefront_website_type_code')->references('codigo')->on('storefront_website_types')->cascadeOnUpdate()->cascadeOnDelete(); + $table->string('codigo')->nullable(); + $table->string('nombre'); + $table->text('descripcion'); + $table->boolean('is_required')->default(false); + $table->json('config_schema'); + $table->timestamps(); + $table->unique(['storefront_website_type_code', 'codigo']); + }); + DB::table('storefront_website_type_extras')->orderBy('id')->chunk(100, function ($extras): void { + foreach ($extras as $extra) { + DB::table('storefront_website_type_extras_rebuilt')->insert((array) $extra); + } + }); + Schema::drop('storefront_website_type_extras'); + Schema::rename('storefront_website_type_extras_rebuilt', 'storefront_website_type_extras'); + Schema::enableForeignKeyConstraints(); + } + + $this->addTypeForeignKeys(); + } + + private function addTypeForeignKeys(): void + { + if (DB::getDriverName() !== 'sqlite' && ! $this->hasForeignKey('tenants', 'admin_website_type_code')) { + Schema::table('tenants', function (Blueprint $table): void { + $table->foreign('admin_website_type_code')->references('codigo')->on('admin_website_types')->cascadeOnUpdate()->nullOnDelete(); + }); + } + + if (! $this->hasForeignKey('tenants', 'storefront_website_type_code')) { + Schema::table('tenants', function (Blueprint $table): void { + $table->foreign('storefront_website_type_code')->references('codigo')->on('storefront_website_types')->cascadeOnUpdate()->nullOnDelete(); + }); + } + + if (DB::getDriverName() !== 'sqlite') { + if (! $this->hasForeignKey('storefront_website_type_extras', 'storefront_website_type_code')) { + Schema::table('storefront_website_type_extras', function (Blueprint $table): void { + $table->foreign('storefront_website_type_code', 'swte_storefront_type_fk')->references('codigo')->on('storefront_website_types')->cascadeOnUpdate()->cascadeOnDelete(); + }); + } + + if (! $this->hasForeignKey('admin_website_type_integrations', 'admin_website_type_code')) { + Schema::table('admin_website_type_integrations', function (Blueprint $table): void { + $table->foreign('admin_website_type_code', 'awti_admin_type_fk')->references('codigo')->on('admin_website_types')->cascadeOnDelete(); + }); + } + } + } + + private function hasForeignKey(string $table, string $column): bool + { + foreach (Schema::getForeignKeys($table) as $foreignKey) { + if ($foreignKey['columns'] === [$column]) { + return true; + } + } + + return false; + } + + public function down(): void + { + if (DB::getDriverName() === 'sqlite') { + throw new RuntimeException('SQLite rollback is unsupported for this foreign-key refactor. Recreate the test database instead.'); + } + + Schema::table('tenants', function (Blueprint $table): void { + $table->dropForeign(['admin_website_type_code']); + $table->dropForeign(['storefront_website_type_code']); + }); + Schema::table('storefront_website_type_extras', fn (Blueprint $table) => $table->dropForeign('swte_storefront_type_fk')); + Schema::table('admin_website_type_integrations', fn (Blueprint $table) => $table->dropForeign('awti_admin_type_fk')); + + Schema::table('tenants', fn (Blueprint $table) => $table->renameColumn('admin_website_type_code', 'website_type_code')); + Schema::table('storefront_website_type_extras', fn (Blueprint $table) => $table->renameColumn('storefront_website_type_code', 'website_type_code')); + Schema::table('admin_website_type_integrations', fn (Blueprint $table) => $table->renameColumn('admin_website_type_code', 'website_type_code')); + + Schema::rename('admin_website_types', 'website_type'); + Schema::rename('storefront_website_type_extras', 'website_type_extras'); + Schema::rename('admin_website_type_integrations', 'website_type_integrations'); + + Schema::table('tenants', function (Blueprint $table): void { + $table->foreign('website_type_code')->references('codigo')->on('website_type')->cascadeOnUpdate()->nullOnDelete(); + $table->dropColumn('storefront_website_type_code'); + }); + Schema::table('website_type_extras', fn (Blueprint $table) => $table->foreign('website_type_code')->references('codigo')->on('website_type')->cascadeOnUpdate()->cascadeOnDelete()); + Schema::table('website_type_integrations', fn (Blueprint $table) => $table->foreign('website_type_code')->references('codigo')->on('website_type')->cascadeOnDelete()); + Schema::dropIfExists('storefront_website_types'); + } +}; diff --git a/database/migrations/2026_09_17_000100_add_header_type_to_storefront_website_types.php b/database/migrations/2026_09_17_000100_add_header_type_to_storefront_website_types.php new file mode 100644 index 0000000..afc9e74 --- /dev/null +++ b/database/migrations/2026_09_17_000100_add_header_type_to_storefront_website_types.php @@ -0,0 +1,22 @@ +string('header_type')->default('standard'); + }); + } + + public function down(): void + { + Schema::table('storefront_website_types', function (Blueprint $table): void { + $table->dropColumn('header_type'); + }); + } +}; diff --git a/database/migrations/2026_09_18_000000_add_onticket_immersive_hero_configuration.php b/database/migrations/2026_09_18_000000_add_onticket_immersive_hero_configuration.php new file mode 100644 index 0000000..d6b236e --- /dev/null +++ b/database/migrations/2026_09_18_000000_add_onticket_immersive_hero_configuration.php @@ -0,0 +1,93 @@ +updateOrInsert( + ['codigo' => 'onticket_multi_event'], + [ + 'nombre' => 'OnTicket - múltiples eventos', + 'header_type' => 'immersive', + 'updated_at' => $now, + ] + (DB::table('storefront_website_types')->where('codigo', 'onticket_multi_event')->exists() + ? [] + : ['created_at' => $now]), + ); + + $extraKey = [ + 'storefront_website_type_code' => 'onticket_multi_event', + 'codigo' => 'immersiveHero', + ]; + + DB::table('storefront_website_type_extras')->updateOrInsert( + $extraKey, + [ + 'nombre' => 'Textos del hero inmersivo', + 'descripcion' => 'Frase superior, título y descripción de la portada.', + 'is_required' => false, + 'config_schema' => json_encode([ + 'request_rules' => [ + '$' => 'required|array:eyebrow,title,description', + 'eyebrow' => 'required|string|max:120', + 'title' => 'required|string|max:120', + 'description' => 'required|string|max:255', + ], + 'transforms' => [], + ], JSON_THROW_ON_ERROR), + 'updated_at' => $now, + ] + (DB::table('storefront_website_type_extras')->where($extraKey)->exists() + ? [] + : ['created_at' => $now]), + ); + + $tenant = DB::table('tenants') + ->where('codigo', 'onticket') + ->whereIn('storefront_website_type_code', ['onticket', 'onticket_multi_event']) + ->first(['codigo']); + + if ($tenant === null) { + return; + } + + DB::table('tenants')->where('codigo', $tenant->codigo)->update([ + 'storefront_website_type_code' => 'onticket_multi_event', + 'display_seach_bar' => true, + 'updated_at' => $now, + ]); + + $extraId = DB::table('storefront_website_type_extras') + ->where($extraKey) + ->value('id'); + + if (! DB::table('websites_extras') + ->where('website_code', $tenant->codigo) + ->where('website_type_extra_id', $extraId) + ->exists()) { + DB::table('websites_extras')->insert([ + 'website_code' => $tenant->codigo, + 'website_type_extra_id' => $extraId, + 'config' => json_encode([ + 'eyebrow' => 'Encendé tu', + 'title' => 'experiencia', + 'description' => 'Reservá tu entrada y formá parte', + ], JSON_THROW_ON_ERROR), + 'is_enabled' => true, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + }); + } + + public function down(): void + { + // This data change is intentionally retained to avoid removing tenant edits. + } +}; diff --git a/database/migrations/2026_09_18_000100_add_onticket_immersive_hero_carousel.php b/database/migrations/2026_09_18_000100_add_onticket_immersive_hero_carousel.php new file mode 100644 index 0000000..5ac752d --- /dev/null +++ b/database/migrations/2026_09_18_000100_add_onticket_immersive_hero_carousel.php @@ -0,0 +1,53 @@ + 'onticket_multi_event']; + $definitions = DB::table('storefront_website_type_extras')->where($key); + + if ((clone $definitions)->where('codigo', 'immersiveHeroCarousel')->exists()) { + return; + } + + if ((clone $definitions)->where('codigo', 'heroCarousel')->exists()) { + (clone $definitions)->where('codigo', 'heroCarousel')->update([ + 'codigo' => 'immersiveHeroCarousel', + 'updated_at' => now(), + ]); + + return; + } + + DB::table('storefront_website_type_extras')->insert([ + ...$key, + 'codigo' => 'immersiveHeroCarousel', + 'nombre' => 'Carrusel del hero', + 'descripcion' => 'Lista ordenada de imágenes del hero.', + 'is_required' => false, + 'config_schema' => json_encode([ + 'request_rules' => [ + '$' => 'required|array|max:10', + '$.*' => 'required|image_or_base64|distinct', + ], + 'transforms' => [ + '$.*' => [ + 'handler' => 'attachment', + 'attachment_type' => 'image', + ], + ], + ], JSON_THROW_ON_ERROR), + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + public function down(): void + { + // Preserve configured images and tenant edits. + } +}; diff --git a/database/migrations/2026_09_18_000200_add_onticket_categories.php b/database/migrations/2026_09_18_000200_add_onticket_categories.php new file mode 100644 index 0000000..1f60c2b --- /dev/null +++ b/database/migrations/2026_09_18_000200_add_onticket_categories.php @@ -0,0 +1,35 @@ +where('codigo', 'onticket')->exists()) { + return; + } + + DB::transaction(function (): void { + $now = now(); + + foreach (['Música', 'Teatro', 'Deportes', 'Infantiles', 'Otros'] as $nombre) { + $category = ['tenant_code' => 'onticket', 'nombre' => $nombre]; + + if (! DB::table('categorias')->where($category)->exists()) { + DB::table('categorias')->insert($category + [ + 'is_enabled' => true, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + }); + } + + public function down(): void + { + // Retain categories because they may have been edited or assigned to products. + } +}; diff --git a/database/migrations/2026_09_18_000300_show_onticket_categories.php b/database/migrations/2026_09_18_000300_show_onticket_categories.php new file mode 100644 index 0000000..02c9648 --- /dev/null +++ b/database/migrations/2026_09_18_000300_show_onticket_categories.php @@ -0,0 +1,20 @@ +where('codigo', 'onticket') + ->where('storefront_website_type_code', 'onticket_multi_event') + ->update(['display_categories' => true, 'updated_at' => now()]); + } + + public function down(): void + { + // Retain the tenant setting because an administrator may have changed it. + } +}; diff --git a/database/migrations/2026_09_18_000400_update_stored_morph_types_for_domain_groups.php b/database/migrations/2026_09_18_000400_update_stored_morph_types_for_domain_groups.php new file mode 100644 index 0000000..67d2c6e --- /dev/null +++ b/database/migrations/2026_09_18_000400_update_stored_morph_types_for_domain_groups.php @@ -0,0 +1,49 @@ + [ + 'tokenable_type' => [ + 'App\\Domains\\Auth\\Models\\User' => 'App\\Domains\\Core\\Auth\\Models\\User', + ], + ], + 'value_changes' => [ + 'trackable_type' => [ + 'App\\Domains\\Purchase\\Models\\Purchase' => 'App\\Domains\\Commerce\\Purchase\\Models\\Purchase', + 'App\\Domains\\Ticket\\Models\\Ticket' => 'App\\Domains\\Ticketing\\Ticket\\Models\\Ticket', + ], + ], + ]; + + public function up(): void + { + $this->rewriteTypes(false); + } + + public function down(): void + { + $this->rewriteTypes(true); + } + + private function rewriteTypes(bool $reverse): void + { + foreach (self::TYPE_CHANGES as $table => $columns) { + if (! Schema::hasTable($table)) { + continue; + } + + foreach ($columns as $column => $changes) { + foreach ($changes as $oldType => $newType) { + DB::table($table) + ->where($column, $reverse ? $newType : $oldType) + ->update([$column => $reverse ? $oldType : $newType]); + } + } + } + } +}; diff --git a/database/migrations/2026_09_18_000500_restore_events.php b/database/migrations/2026_09_18_000500_restore_events.php new file mode 100644 index 0000000..69f67f3 --- /dev/null +++ b/database/migrations/2026_09_18_000500_restore_events.php @@ -0,0 +1,124 @@ +id(); + $table->string('tenant_code'); + $table->string('title'); + $table->string('subtitle')->nullable(); + $table->text('description')->nullable(); + $table->string('location')->nullable(); + $table->text('date_text')->nullable(); + $table->timestamp('published_at')->nullable(); + $table->timestamps(); + $table->foreign('tenant_code')->references('codigo')->on('tenants')->cascadeOnUpdate()->cascadeOnDelete(); + $table->index(['tenant_code', 'published_at']); + }); + + Schema::table('event_dates', function (Blueprint $table): void { + $table->foreignId('event_id')->nullable()->after('tenant_code')->constrained('events')->cascadeOnDelete(); + }); + + Schema::create('event_social_media', function (Blueprint $table): void { + $table->id(); + $table->foreignId('event_id')->constrained('events')->cascadeOnDelete(); + $table->string('social_media_code'); + $table->string('url'); + $table->unsignedInteger('orden')->default(0); + $table->timestamps(); + $table->foreign('social_media_code')->references('code')->on('social_media')->cascadeOnDelete(); + $table->unique(['event_id', 'social_media_code']); + }); + + Schema::table('tenants', function (Blueprint $table): void { + $table->foreignId('active_event_id')->nullable()->constrained('events')->nullOnDelete(); + }); + + DB::table('tenants')->orderBy('id')->chunk(100, function ($tenants): void { + foreach ($tenants as $tenant) { + $hasEvent = $tenant->event_title !== null + || DB::table('event_dates')->where('tenant_code', $tenant->codigo)->exists(); + + if (! $hasEvent) { + continue; + } + + $eventId = DB::table('events')->insertGetId([ + 'tenant_code' => $tenant->codigo, + 'title' => $tenant->event_title ?? $tenant->nombre, + 'location' => $tenant->event_location, + 'date_text' => $tenant->event_date_text, + 'published_at' => now(), + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('event_dates')->where('tenant_code', $tenant->codigo)->update(['event_id' => $eventId]); + + DB::table('tenant_social_media') + ->where('tenant_code', $tenant->codigo) + ->orderBy('id') + ->get() + ->each(function ($socialMedia) use ($eventId): void { + DB::table('event_social_media')->insert([ + 'event_id' => $eventId, + 'social_media_code' => $socialMedia->social_media_code, + 'url' => $socialMedia->url, + 'orden' => $socialMedia->orden, + 'created_at' => now(), + 'updated_at' => now(), + ]); + }); + + if ($tenant->storefront_website_type_code === 'onticket') { + DB::table('tenants')->where('id', $tenant->id)->update(['active_event_id' => $eventId]); + } + } + }); + + Schema::table('event_dates', function (Blueprint $table): void { + $table->foreignId('event_id')->nullable(false)->change(); + }); + + Schema::table('tenants', function (Blueprint $table): void { + $table->dropColumn(['event_title', 'event_location', 'event_date_text']); + }); + } + + public function down(): void + { + Schema::table('tenants', function (Blueprint $table): void { + $table->string('event_title')->nullable(); + $table->string('event_location')->nullable(); + $table->text('event_date_text')->nullable(); + }); + + DB::table('tenants')->orderBy('id')->chunk(100, function ($tenants): void { + foreach ($tenants as $tenant) { + $event = DB::table('events')->where('tenant_code', $tenant->codigo) + ->when($tenant->active_event_id !== null, fn ($query) => $query->orderByRaw('id = ? desc', [$tenant->active_event_id])) + ->orderBy('id')->first(); + if ($event !== null) { + DB::table('tenants')->where('id', $tenant->id)->update([ + 'event_title' => $event->title, + 'event_location' => $event->location, + 'event_date_text' => $event->date_text, + ]); + } + } + }); + + Schema::table('tenants', fn (Blueprint $table) => $table->dropConstrainedForeignId('active_event_id')); + Schema::table('event_dates', fn (Blueprint $table) => $table->dropConstrainedForeignId('event_id')); + Schema::dropIfExists('event_social_media'); + Schema::dropIfExists('events'); + } +}; diff --git a/database/migrations/2026_09_18_000600_associate_active_event_social_media.php b/database/migrations/2026_09_18_000600_associate_active_event_social_media.php new file mode 100644 index 0000000..ce24b80 --- /dev/null +++ b/database/migrations/2026_09_18_000600_associate_active_event_social_media.php @@ -0,0 +1,48 @@ +whereNotNull('active_event_id') + ->orderBy('id') + ->chunk(100, function ($tenants): void { + foreach ($tenants as $tenant) { + $eventId = DB::table('events') + ->where('id', $tenant->active_event_id) + ->where('tenant_code', $tenant->codigo) + ->value('id'); + + if ($eventId === null) { + continue; + } + + $rows = DB::table('tenant_social_media') + ->where('tenant_code', $tenant->codigo) + ->get() + ->map(fn ($network): array => [ + 'event_id' => $eventId, + 'social_media_code' => $network->social_media_code, + 'url' => $network->url, + 'orden' => $network->orden, + 'created_at' => now(), + 'updated_at' => now(), + ]) + ->all(); + + if ($rows !== []) { + DB::table('event_social_media')->insertOrIgnore($rows); + } + } + }); + } + + public function down(): void + { + // The backfilled rows may have been edited after migration; deleting them would lose data. + } +}; diff --git a/database/migrations/2026_09_18_000600_create_event_categories_table.php b/database/migrations/2026_09_18_000600_create_event_categories_table.php new file mode 100644 index 0000000..d7f3203 --- /dev/null +++ b/database/migrations/2026_09_18_000600_create_event_categories_table.php @@ -0,0 +1,41 @@ +id(); + $table->string('tenant_code'); + $table->string('nombre'); + $table->timestamps(); + $table->foreign('tenant_code')->references('codigo')->on('tenants')->cascadeOnUpdate()->cascadeOnDelete(); + $table->unique(['tenant_code', 'nombre']); + }); + + Schema::table('events', function (Blueprint $table): void { + $table->foreignId('event_category_id')->nullable()->constrained('event_categories')->nullOnDelete(); + }); + + if (DB::table('tenants')->where('codigo', 'onticket')->exists()) { + $now = now(); + foreach (['Música', 'Teatro', 'Deportes', 'Infantiles', 'Otros'] as $nombre) { + DB::table('event_categories')->insert([ + 'tenant_code' => 'onticket', 'nombre' => $nombre, + 'created_at' => $now, 'updated_at' => $now, + ]); + } + } + } + + public function down(): void + { + Schema::table('events', fn (Blueprint $table) => $table->dropConstrainedForeignId('event_category_id')); + Schema::dropIfExists('event_categories'); + } +}; diff --git a/database/migrations/2026_09_18_000700_add_event_id_to_catalog_items.php b/database/migrations/2026_09_18_000700_add_event_id_to_catalog_items.php new file mode 100644 index 0000000..543bfab --- /dev/null +++ b/database/migrations/2026_09_18_000700_add_event_id_to_catalog_items.php @@ -0,0 +1,26 @@ +foreignId('event_id') + ->nullable() + ->after('tenant_code') + ->constrained('events') + ->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('catalog_items', function (Blueprint $table): void { + $table->dropConstrainedForeignId('event_id'); + }); + } +}; diff --git a/database/migrations/2026_09_18_000800_backfill_catalog_item_events.php b/database/migrations/2026_09_18_000800_backfill_catalog_item_events.php new file mode 100644 index 0000000..52c1a19 --- /dev/null +++ b/database/migrations/2026_09_18_000800_backfill_catalog_item_events.php @@ -0,0 +1,25 @@ +whereIn('codigo', ['desfile_pura_tendencia', 'fiesta_futbol_infantil']) + ->whereNotNull('active_event_id') + ->get(['codigo', 'active_event_id']) + ->each(function (object $tenant): void { + DB::table('catalog_items') + ->where('tenant_code', $tenant->codigo) + ->update(['event_id' => $tenant->active_event_id]); + }); + } + + public function down(): void + { + // The previous event associations cannot be recovered after this backfill. + } +}; diff --git a/database/migrations/2026_09_18_000900_add_attachment_id_to_events.php b/database/migrations/2026_09_18_000900_add_attachment_id_to_events.php new file mode 100644 index 0000000..f53b962 --- /dev/null +++ b/database/migrations/2026_09_18_000900_add_attachment_id_to_events.php @@ -0,0 +1,26 @@ +foreignId('attachment_id')->nullable()->constrained('attachments')->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('events', function (Blueprint $table): void { + if (Schema::getConnection()->getDriverName() !== 'sqlite') { + $table->dropForeign(['attachment_id']); + } + + $table->dropColumn('attachment_id'); + }); + } +}; diff --git a/database/migrations/2026_09_18_000900_add_sales_end_at_to_catalog_items.php b/database/migrations/2026_09_18_000900_add_sales_end_at_to_catalog_items.php new file mode 100644 index 0000000..5550088 --- /dev/null +++ b/database/migrations/2026_09_18_000900_add_sales_end_at_to_catalog_items.php @@ -0,0 +1,22 @@ +timestamp('sales_end_at')->nullable()->after('precio'); + }); + } + + public function down(): void + { + Schema::table('catalog_items', function (Blueprint $table): void { + $table->dropColumn('sales_end_at'); + }); + } +}; diff --git a/database/migrations/2026_09_18_001000_add_exact_location_to_events.php b/database/migrations/2026_09_18_001000_add_exact_location_to_events.php new file mode 100644 index 0000000..ef4ec4b --- /dev/null +++ b/database/migrations/2026_09_18_001000_add_exact_location_to_events.php @@ -0,0 +1,22 @@ +json('exact_location')->nullable()->after('location'); + }); + } + + public function down(): void + { + Schema::table('events', function (Blueprint $table): void { + $table->dropColumn('exact_location'); + }); + } +}; diff --git a/database/migrations/2026_09_21_000000_assign_menus_to_onticket.php b/database/migrations/2026_09_21_000000_assign_menus_to_onticket.php new file mode 100644 index 0000000..8110238 --- /dev/null +++ b/database/migrations/2026_09_21_000000_assign_menus_to_onticket.php @@ -0,0 +1,113 @@ + [ + 'label' => 'Eventos', + 'route' => '/', + ], + 'event.detail' => [ + 'label' => 'Detalle de evento', + 'route' => '/evento/:id', + ], + ]; + + private const EXCLUDED_MENU_CODES = [ + 'product.detail', + 'help', + 'help.faq', + 'help.contact', + 'help.payment-methods', + 'help.shipping', + 'help.terms-and-conditions', + 'adminapp.tickets', + 'adminapp.fiesta-futbol-infantil.entradas', + 'adminapp.fiesta-futbol-infantil.alojamientos', + 'adminapp.fiesta-futbol-infantil.merchandising', + 'adminapp.fiesta-futbol-infantil.comida', + 'adminapp.desfile.entradas', + ]; + + public function up(): void + { + $now = now(); + + DB::transaction(function () use ($now): void { + foreach (self::EVENT_MENUS as $code => $menu) { + DB::table('menues')->updateOrInsert( + ['code' => $code], + [ + 'label' => $menu['label'], + 'parent_menu_code' => null, + 'content_type' => 'dynamic', + 'static_content_schema' => null, + 'route' => $menu['route'], + 'created_at' => $now, + 'updated_at' => $now, + ], + ); + } + + DB::table('roles') + ->whereIn('codigo', ['admin', 'adminapp', 'user']) + ->pluck('codigo') + ->each(function (string $roleCode): void { + foreach (array_keys(self::EVENT_MENUS) as $menuCode) { + DB::table('roles_menues')->updateOrInsert([ + 'rol_codigo' => $roleCode, + 'menu_codigo' => $menuCode, + ]); + } + }); + + if (! DB::table('tenants')->where('codigo', self::TENANT_CODE)->exists()) { + return; + } + + DB::table('menues') + ->whereNotIn('code', self::EXCLUDED_MENU_CODES) + ->pluck('code') + ->each(function (string $menuCode) use ($now): void { + DB::table('tenants_menues')->updateOrInsert( + [ + 'tenant_code' => self::TENANT_CODE, + 'menu_code' => $menuCode, + ], + [ + 'static_content' => null, + 'created_at' => $now, + 'updated_at' => $now, + ], + ); + }); + }); + } + + public function down(): void + { + DB::transaction(function (): void { + $assignedMenuCodes = DB::table('menues') + ->whereNotIn('code', self::EXCLUDED_MENU_CODES) + ->pluck('code'); + + DB::table('tenants_menues') + ->where('tenant_code', self::TENANT_CODE) + ->whereIn('menu_code', $assignedMenuCodes) + ->delete(); + + DB::table('roles_menues') + ->whereIn('menu_codigo', array_keys(self::EVENT_MENUS)) + ->delete(); + + DB::table('menues') + ->whereIn('code', array_keys(self::EVENT_MENUS)) + ->delete(); + }); + } +}; diff --git a/database/migrations/2026_09_21_010000_add_storefront_category_menus.php b/database/migrations/2026_09_21_010000_add_storefront_category_menus.php new file mode 100644 index 0000000..5cfcf37 --- /dev/null +++ b/database/migrations/2026_09_21_010000_add_storefront_category_menus.php @@ -0,0 +1,81 @@ + [ + 'label' => 'Categoría de eventos', + 'route' => '/eventos/categoria/:id', + ], + 'product.category' => [ + 'label' => 'Categoría de productos', + 'route' => '/categoria/:id', + ], + ]; + + public function up(): void + { + $now = now(); + + DB::transaction(function () use ($now): void { + foreach (self::MENUS as $code => $menu) { + DB::table('menues')->updateOrInsert( + ['code' => $code], + [ + 'label' => $menu['label'], + 'parent_menu_code' => null, + 'content_type' => 'dynamic', + 'static_content_schema' => null, + 'route' => $menu['route'], + 'created_at' => $now, + 'updated_at' => $now, + ], + ); + } + + DB::table('roles') + ->whereIn('codigo', ['admin', 'adminapp', 'user']) + ->pluck('codigo') + ->each(function (string $roleCode): void { + foreach (array_keys(self::MENUS) as $menuCode) { + DB::table('roles_menues')->updateOrInsert([ + 'rol_codigo' => $roleCode, + 'menu_codigo' => $menuCode, + ]); + } + }); + + DB::table('tenants') + ->pluck('codigo') + ->each(function (string $tenantCode) use ($now): void { + $menuCode = $tenantCode === 'onticket' ? 'event.category' : 'product.category'; + + DB::table('tenants_menues')->updateOrInsert( + [ + 'tenant_code' => $tenantCode, + 'menu_code' => $menuCode, + ], + [ + 'static_content' => null, + 'created_at' => $now, + 'updated_at' => $now, + ], + ); + }); + }); + } + + public function down(): void + { + DB::transaction(function (): void { + $menuCodes = array_keys(self::MENUS); + + DB::table('tenants_menues')->whereIn('menu_code', $menuCodes)->delete(); + DB::table('roles_menues')->whereIn('menu_codigo', $menuCodes)->delete(); + DB::table('menues')->whereIn('code', $menuCodes)->delete(); + }); + } +}; diff --git a/database/migrations/2026_09_21_020000_group_pyme_rural_tenants.php b/database/migrations/2026_09_21_020000_group_pyme_rural_tenants.php new file mode 100644 index 0000000..d0138f7 --- /dev/null +++ b/database/migrations/2026_09_21_020000_group_pyme_rural_tenants.php @@ -0,0 +1,93 @@ +clientId(self::CLIENT_CODE, self::CLIENT_NAME); + $sourceClientIds = DB::table('tenants') + ->whereIn('codigo', self::TENANT_CODES) + ->pluck('client_id') + ->unique(); + + $this->copyIntegrations($sourceClientIds, $clientId); + + DB::table('tenants') + ->whereIn('codigo', self::TENANT_CODES) + ->update(['client_id' => $clientId]); + }); + } + + public function down(): void + { + DB::transaction(function (): void { + $previousClientId = $this->clientId(self::PREVIOUS_CLIENT_CODE, 'OnTicket'); + $pymeRuralClientId = DB::table('clients') + ->where('code', self::CLIENT_CODE) + ->value('id'); + + if ($pymeRuralClientId !== null) { + $this->copyIntegrations(collect([$pymeRuralClientId]), $previousClientId); + } + + DB::table('tenants') + ->whereIn('codigo', self::TENANT_CODES) + ->update(['client_id' => $previousClientId]); + }); + } + + private function clientId(string $code, string $name): int + { + $clientId = DB::table('clients')->where('code', $code)->value('id'); + + if ($clientId === null) { + return (int) DB::table('clients')->insertGetId([ + 'code' => $code, + 'name' => $name, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + DB::table('clients')->where('id', $clientId)->update([ + 'name' => $name, + 'updated_at' => now(), + ]); + + return (int) $clientId; + } + + private function copyIntegrations(iterable $sourceClientIds, int $targetClientId): void + { + foreach ($sourceClientIds as $sourceClientId) { + DB::table('client_integrations') + ->where('client_id', $sourceClientId) + ->orderBy('id') + ->get() + ->each(function (object $integration) use ($targetClientId): void { + DB::table('client_integrations')->insertOrIgnore([ + 'client_id' => $targetClientId, + 'integration_code' => $integration->integration_code, + 'integration_instance_id' => $integration->integration_instance_id, + 'created_at' => $integration->created_at, + 'updated_at' => $integration->updated_at, + ]); + }); + } + } +}; diff --git a/database/migrations/2026_09_21_030000_add_checkout_summary_type_to_storefront_website_types.php b/database/migrations/2026_09_21_030000_add_checkout_summary_type_to_storefront_website_types.php new file mode 100644 index 0000000..cc7cd72 --- /dev/null +++ b/database/migrations/2026_09_21_030000_add_checkout_summary_type_to_storefront_website_types.php @@ -0,0 +1,27 @@ +string('checkout_summary_type')->default('default')->after('header_type'); + }); + + DB::table('storefront_website_types') + ->where('codigo', 'onticket_multi_event') + ->update(['checkout_summary_type' => 'event']); + } + + public function down(): void + { + Schema::table('storefront_website_types', function (Blueprint $table): void { + $table->dropColumn('checkout_summary_type'); + }); + } +}; diff --git a/database/migrations/2026_09_21_040000_add_timezone_to_tenants.php b/database/migrations/2026_09_21_040000_add_timezone_to_tenants.php new file mode 100644 index 0000000..a046b2b --- /dev/null +++ b/database/migrations/2026_09_21_040000_add_timezone_to_tenants.php @@ -0,0 +1,41 @@ +string('timezone')->default('America/Argentina/Buenos_Aires'); + }); + + // Correct legacy event windows once, using their original local date and hours. + // Subsequent tenant timezone changes do not rewrite stored UTC instants. + DB::table('event_dates')->orderBy('id')->chunkById(200, function ($dates): void { + foreach ($dates as $date) { + $timezone = DB::table('tenants')->where('codigo', $date->tenant_code)->value('timezone'); + $start = CarbonImmutable::parse(substr($date->date, 0, 10).' '.$date->time_start, $timezone); + $end = CarbonImmutable::parse(substr($date->date, 0, 10).' '.$date->time_end, $timezone); + if ($end->lessThanOrEqualTo($start)) { + $end = $end->addDay(); + } + DB::table('validity_times')->where('id', $date->validity_time_id)->update([ + 'fixed_starts_at' => $start->utc()->format('Y-m-d H:i:s'), + 'fixed_expires_at' => $end->utc()->format('Y-m-d H:i:s'), + ]); + } + }); + } + + public function down(): void + { + Schema::table('tenants', function (Blueprint $table): void { + $table->dropColumn('timezone'); + }); + } +}; diff --git a/database/migrations/2026_09_21_050000_provision_onticket_tenant.php b/database/migrations/2026_09_21_050000_provision_onticket_tenant.php new file mode 100644 index 0000000..c47378c --- /dev/null +++ b/database/migrations/2026_09_21_050000_provision_onticket_tenant.php @@ -0,0 +1,64 @@ +environment('testing')) { + Storage::fake('s3'); + } + + app(WebsiteTypeSeeder::class)->run(); + app(OnTicketTenantSeeder::class)->run(); + app(FiestaTradicionArrufoSeeder::class)->run(); + + $now = now(); + + DB::table('menues') + ->whereNotIn('code', self::EXCLUDED_MENU_CODES) + ->pluck('code') + ->each(function (string $menuCode) use ($now): void { + DB::table('tenants_menues')->updateOrInsert( + [ + 'tenant_code' => self::TENANT_CODE, + 'menu_code' => $menuCode, + ], + [ + 'static_content' => null, + 'created_at' => $now, + 'updated_at' => $now, + ], + ); + }); + } + + public function down(): void + { + // OnTicket is reference data. Never remove a possibly active tenant on rollback. + } +}; diff --git a/database/migrations/2026_09_21_060000_repair_onticket_assets_and_event.php b/database/migrations/2026_09_21_060000_repair_onticket_assets_and_event.php new file mode 100644 index 0000000..da85a52 --- /dev/null +++ b/database/migrations/2026_09_21_060000_repair_onticket_assets_and_event.php @@ -0,0 +1,44 @@ +environment('testing')) { + Storage::fake('s3'); + } + + $type = AdminWebsiteType::query() + ->with(['siteLogo', 'footerLogo', 'favicon']) + ->where('codigo', 'onticket') + ->first(); + $typeAssets = $type === null + ? [] + : [$type->siteLogo, $type->footerLogo, $type->favicon]; + + if ( + $type === null + || collect($typeAssets)->contains( + fn ($attachment): bool => $attachment === null + || ! Storage::disk('s3')->exists($attachment->path) + ) + ) { + app(WebsiteTypeSeeder::class)->run(); + } + + app(OnTicketTenantSeeder::class)->run(); + app(FiestaTradicionArrufoSeeder::class)->run(); + } + + public function down(): void + { + // Reference and operational data are intentionally preserved. + } +}; diff --git a/database/migrations/2026_09_22_000000_optimize_onticket_images.php b/database/migrations/2026_09_22_000000_optimize_onticket_images.php new file mode 100644 index 0000000..fe9faeb --- /dev/null +++ b/database/migrations/2026_09_22_000000_optimize_onticket_images.php @@ -0,0 +1,23 @@ +environment('testing')) { + Storage::fake('s3'); + } + + app(OptimizeOnticketImagesSeeder::class)->run(); + } + + public function down(): void + { + // The optimized attachments may already be in active use. Keep this + // data migration irreversible instead of restoring heavier assets. + } +}; diff --git a/database/migrations/2026_09_22_010000_add_slugs_to_events.php b/database/migrations/2026_09_22_010000_add_slugs_to_events.php new file mode 100644 index 0000000..0b0781c --- /dev/null +++ b/database/migrations/2026_09_22_010000_add_slugs_to_events.php @@ -0,0 +1,46 @@ +string('slug')->nullable(); + }); + + $usedSlugsByTenant = []; + + foreach (DB::table('events')->orderBy('tenant_code')->orderBy('id')->get() as $event) { + $baseSlug = Str::slug((string) $event->title); + $baseSlug = $baseSlug !== '' ? substr($baseSlug, 0, 240) : 'evento-'.$event->id; + $slug = $baseSlug; + $suffix = 2; + + while (isset($usedSlugsByTenant[$event->tenant_code][$slug])) { + $slug = $baseSlug.'-'.$suffix; + $suffix++; + } + + DB::table('events')->where('id', $event->id)->update(['slug' => $slug]); + $usedSlugsByTenant[$event->tenant_code][$slug] = true; + } + + Schema::table('events', function (Blueprint $table): void { + $table->unique(['tenant_code', 'slug']); + }); + } + + public function down(): void + { + Schema::table('events', function (Blueprint $table): void { + $table->dropUnique(['tenant_code', 'slug']); + $table->dropColumn('slug'); + }); + } +}; diff --git a/database/migrations/2026_09_22_020000_add_event_id_to_tickets.php b/database/migrations/2026_09_22_020000_add_event_id_to_tickets.php new file mode 100644 index 0000000..351f002 --- /dev/null +++ b/database/migrations/2026_09_22_020000_add_event_id_to_tickets.php @@ -0,0 +1,52 @@ +foreignId('event_id') + ->nullable() + ->after('tenant_code') + ->constrained('events') + ->cascadeOnUpdate() + ->restrictOnDelete(); + $table->index(['tenant_code', 'user_id', 'event_id']); + }); + + DB::table('tickets') + ->join('catalog_items', 'catalog_items.id', '=', 'tickets.source_catalog_item_id') + ->whereNotNull('catalog_items.event_id') + ->select(['tickets.id', 'catalog_items.event_id as source_event_id']) + ->orderBy('tickets.id') + ->chunk(500, function ($tickets): void { + foreach ($tickets as $ticket) { + DB::table('tickets') + ->where('id', $ticket->id) + ->update(['event_id' => $ticket->source_event_id]); + } + }); + } + + public function down(): void + { + if (DB::getDriverName() === 'sqlite') { + Schema::table('tickets', function (Blueprint $table): void { + $table->dropIndex(['tenant_code', 'user_id', 'event_id']); + $table->dropColumn('event_id'); + }); + + return; + } + + Schema::table('tickets', function (Blueprint $table): void { + $table->dropIndex(['tenant_code', 'user_id', 'event_id']); + $table->dropConstrainedForeignId('event_id'); + }); + } +}; diff --git a/database/seeders/AttributeSeeder.php b/database/seeders/AttributeSeeder.php index 001d26a..f19e67b 100644 --- a/database/seeders/AttributeSeeder.php +++ b/database/seeders/AttributeSeeder.php @@ -2,11 +2,11 @@ namespace Database\Seeders; -use App\Domains\Catalog\Models\Attribute; -use App\Domains\Shared\Enums\FieldType; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Ticket\Enums\ValidityTimeType; -use App\Domains\Ticket\Models\ValidityTime; +use App\Domains\Commerce\Catalog\Models\Attribute; +use App\Shared\Enums\FieldType; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Ticketing\Ticket\Enums\ValidityTimeType; +use App\Domains\Ticketing\Ticket\Models\ValidityTime; use Illuminate\Database\Seeder; class AttributeSeeder extends Seeder diff --git a/database/seeders/AuthorizationSeeder.php b/database/seeders/AuthorizationSeeder.php index 3764942..398ec3d 100644 --- a/database/seeders/AuthorizationSeeder.php +++ b/database/seeders/AuthorizationSeeder.php @@ -2,10 +2,10 @@ namespace Database\Seeders; -use App\Domains\Authorization\Enums\PermissionCode; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Authorization\Models\Permission; -use App\Domains\Authorization\Models\Role; +use App\Domains\Core\Authorization\Enums\PermissionCode; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Core\Authorization\Models\Permission; +use App\Domains\Core\Authorization\Models\Role; use Illuminate\Database\Seeder; class AuthorizationSeeder extends Seeder diff --git a/database/seeders/BrandSeeder.php b/database/seeders/BrandSeeder.php index 738a3f1..e2e6d22 100644 --- a/database/seeders/BrandSeeder.php +++ b/database/seeders/BrandSeeder.php @@ -2,8 +2,8 @@ namespace Database\Seeders; -use App\Domains\Catalog\Models\Brand; -use App\Domains\Tenant\Models\Tenant; +use App\Domains\Commerce\Catalog\Models\Brand; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Database\Seeder; class BrandSeeder extends Seeder diff --git a/database/seeders/CategorySeeder.php b/database/seeders/CategorySeeder.php index c6f51ba..9ddf2cc 100644 --- a/database/seeders/CategorySeeder.php +++ b/database/seeders/CategorySeeder.php @@ -2,8 +2,8 @@ namespace Database\Seeders; -use App\Domains\Catalog\Models\Category; -use App\Domains\Tenant\Models\Tenant; +use App\Domains\Commerce\Catalog\Models\Category; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Database\Seeder; class CategorySeeder extends Seeder @@ -13,6 +13,17 @@ class CategorySeeder extends Seeder */ public function run(): void { + $onTicket = Tenant::query()->where('codigo', 'onticket')->first(); + + if ($onTicket instanceof Tenant) { + foreach (['Música', 'Teatro', 'Deportes', 'Infantiles', 'Otros'] as $nombre) { + Category::firstOrCreate([ + 'nombre' => $nombre, + 'tenant_code' => $onTicket->codigo, + ]); + } + } + $tenant = Tenant::query()->where('codigo', 'sonder')->first(); if (! $tenant instanceof Tenant) { diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 5ac3e1a..b5be6dc 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -2,7 +2,7 @@ namespace Database\Seeders; -use App\Domains\Auth\Models\User; +use App\Domains\Core\Auth\Models\User; use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; @@ -33,6 +33,7 @@ class DatabaseSeeder extends Seeder BrandSeeder::class, ProductCatalogFromImagesSeeder::class, FiestaFutbolInfantilProductSeeder::class, + FiestaTradicionArrufoSeeder::class, TelepagosIntegrationSeeder::class, EmailIntegrationSeeder::class, MenuSeeder::class, diff --git a/database/seeders/DesfilePuraTendenciaSeeder.php b/database/seeders/DesfilePuraTendenciaSeeder.php index 73dfa82..9f6b8c9 100644 --- a/database/seeders/DesfilePuraTendenciaSeeder.php +++ b/database/seeders/DesfilePuraTendenciaSeeder.php @@ -2,19 +2,19 @@ namespace Database\Seeders; -use App\Domains\Catalog\Enums\FeaturedGroupSource; -use App\Domains\Catalog\Enums\GroupLayout; -use App\Domains\Catalog\Enums\InventoryPolicy; -use App\Domains\Catalog\Enums\InventorySubject; -use App\Domains\Catalog\Enums\ProductLayout; -use App\Domains\Catalog\Models\Attribute; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\FeaturedGroup; -use App\Domains\Catalog\Services\CatalogService; -use App\Domains\Client\Models\Client; -use App\Domains\Desfile\Services\InvitationPurchaseProvisioner; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Services\TenantService; +use App\Domains\Commerce\Catalog\Enums\FeaturedGroupSource; +use App\Domains\Commerce\Catalog\Enums\GroupLayout; +use App\Domains\Commerce\Catalog\Enums\InventoryPolicy; +use App\Domains\Commerce\Catalog\Enums\InventorySubject; +use App\Domains\Commerce\Catalog\Enums\ProductLayout; +use App\Domains\Commerce\Catalog\Models\Attribute; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\FeaturedGroup; +use App\Domains\Commerce\Catalog\Services\CatalogService; +use App\Domains\Core\Client\Models\Client; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Services\TenantProvisioningService; +use App\Domains\Ticketing\Desfile\Services\InvitationPurchaseProvisioner; use Illuminate\Database\Seeder; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\DB; @@ -22,21 +22,21 @@ use RuntimeException; class DesfilePuraTendenciaSeeder extends Seeder { - private const ONTICKET_CLIENT_CODE = 'onticket'; + private const PYME_RURAL_CLIENT_CODE = 'pyme_rural'; private const TENANT_CODE = 'desfile_pura_tendencia'; public function __construct( - private readonly TenantService $tenantService, + private readonly TenantProvisioningService $tenantService, private readonly CatalogService $catalogService, private readonly InvitationPurchaseProvisioner $invitationPurchaseProvisioner, ) {} public function run(): void { - $client = Client::query()->firstOrCreate( - ['code' => self::ONTICKET_CLIENT_CODE], - ['name' => 'OnTicket'], + $client = Client::query()->updateOrCreate( + ['code' => self::PYME_RURAL_CLIENT_CODE], + ['name' => 'Pyme Rural'], ); $tenant = Tenant::query()->where('codigo', self::TENANT_CODE)->first(); @@ -51,20 +51,26 @@ class DesfilePuraTendenciaSeeder extends Seeder }); } + $activeEventId = DB::table('tenants') + ->where('codigo', self::TENANT_CODE) + ->value('active_event_id'); + if ($activeEventId !== null) { + DB::table('catalog_items') + ->where('tenant_code', self::TENANT_CODE) + ->update(['event_id' => $activeEventId]); + } + $this->invitationPurchaseProvisioner->provision(); } private function createTenant(Client $client): void { - $this->tenantService->create([ + $tenant = $this->tenantService->create([ 'client_id' => $client->id, 'codigo' => self::TENANT_CODE, 'nombre' => 'Desfile Pura Tendencia', 'dominio' => 'desfile-pura-tendencia.localhost', 'site_title' => 'Desfile Pura Tendencia', - 'event_title' => 'Desfile Pura Tendencia', - 'event_location' => 'Salón Centro Recreativo Luz y Fuerza', - 'event_date_text' => '16 de Octubre 2026', 'primary_color' => '#BA69A9', 'secondary_color' => '#A0A0A0', 'danger_color' => '#FF8888', @@ -78,7 +84,8 @@ class DesfilePuraTendenciaSeeder extends Seeder 'checkout_editing_policy' => 'disabled', 'display_cart_item_images' => false, 'scanner_category_validation_enabled' => false, - 'website_type_code' => 'onticket', + 'admin_website_type_code' => 'onticket', + 'storefront_website_type_code' => 'onticket', 'header_logo' => $this->uploadedImage( 'images/tennants/desfile_pura_tendencia/desfile_pura_tendencia_header.png', 'desfile_pura_tendencia_header.png', @@ -106,6 +113,15 @@ class DesfilePuraTendenciaSeeder extends Seeder ], ], ]); + + $event = $tenant->events()->create([ + 'slug' => 'desfile-pura-tendencia', + 'title' => 'Desfile Pura Tendencia', + 'location' => 'Salón Centro Recreativo Luz y Fuerza', + 'date_text' => '16 de Octubre 2026', + 'published_at' => now(), + ]); + $tenant->update(['active_event_id' => $event->id]); } private function createEventDate(): void @@ -123,6 +139,7 @@ class DesfilePuraTendenciaSeeder extends Seeder DB::table('event_dates')->insert([ 'tenant_code' => self::TENANT_CODE, + 'event_id' => DB::table('events')->where('tenant_code', self::TENANT_CODE)->value('id'), 'validity_time_id' => $validityTimeId, 'date' => '2026-10-16', 'time_start' => '20:30:00', diff --git a/database/seeders/EmailIntegrationSeeder.php b/database/seeders/EmailIntegrationSeeder.php index 1eeff2c..450e867 100644 --- a/database/seeders/EmailIntegrationSeeder.php +++ b/database/seeders/EmailIntegrationSeeder.php @@ -2,7 +2,7 @@ namespace Database\Seeders; -use App\Domains\Integration\Models\Integration; +use App\Shared\Integration\Models\Integration; use Illuminate\Database\Seeder; class EmailIntegrationSeeder extends Seeder diff --git a/database/seeders/FiestaFutbolInfantilProductSeeder.php b/database/seeders/FiestaFutbolInfantilProductSeeder.php index e60ca10..d7cbb21 100644 --- a/database/seeders/FiestaFutbolInfantilProductSeeder.php +++ b/database/seeders/FiestaFutbolInfantilProductSeeder.php @@ -2,19 +2,20 @@ namespace Database\Seeders; -use App\Domains\Catalog\Enums\FeaturedGroupSource; -use App\Domains\Catalog\Enums\GroupLayout; -use App\Domains\Catalog\Enums\InventoryPolicy; -use App\Domains\Catalog\Enums\ProductLayout; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Category; -use App\Domains\Catalog\Models\FeaturedGroup; -use App\Domains\Catalog\Services\CatalogService; -use App\Domains\Event\Models\EventDate; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Ticket\Enums\ValidityTimeType; -use App\Domains\Ticket\Models\ValidityTime; +use App\Domains\Commerce\Catalog\Enums\FeaturedGroupSource; +use App\Domains\Commerce\Catalog\Enums\GroupLayout; +use App\Domains\Commerce\Catalog\Enums\InventoryPolicy; +use App\Domains\Commerce\Catalog\Enums\ProductLayout; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Category; +use App\Domains\Commerce\Catalog\Models\FeaturedGroup; +use App\Domains\Commerce\Catalog\Services\CatalogService; +use App\Domains\Ticketing\Event\Models\EventDate; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Ticketing\Ticket\Enums\ValidityTimeType; +use App\Domains\Ticketing\Ticket\Models\ValidityTime; use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\DB; use RuntimeException; class FiestaFutbolInfantilProductSeeder extends Seeder @@ -44,17 +45,37 @@ class FiestaFutbolInfantilProductSeeder extends Seeder 'tenant_code' => $tenant->codigo, ])); - $tenant->update([ - 'event_title' => 'Fiesta Nacional del Fútbol Infantil', - 'event_location' => 'Sunchales, Santa Fe', - ]); + $event = $tenant->events()->firstOrCreate( + ['title' => 'Fiesta Nacional del Fútbol Infantil'], + ['location' => 'Sunchales, Santa Fe', 'published_at' => now()], + ); + $event->update(['location' => 'Sunchales, Santa Fe']); + if ($tenant->storefront_website_type_code === 'onticket' && $tenant->active_event_id === null) { + $tenant->update(['active_event_id' => $event->id]); + } + + $existingCodes = $event->socialMedia()->pluck('social_media.code')->all(); + $missingSocialMedia = $tenant->socialMedia() + ->get() + ->reject(fn ($network): bool => in_array($network->code, $existingCodes, true)) + ->mapWithKeys(fn ($network): array => [ + $network->code => [ + 'url' => $network->pivot->url, + 'orden' => $network->pivot->orden, + ], + ]) + ->all(); + + if ($missingSocialMedia !== []) { + $event->socialMedia()->syncWithoutDetaching($missingSocialMedia); + } $existingValidityTimeIds = $tenant->eventDates()->pluck('validity_time_id'); - $tenant->eventDates()->delete(); + $event->dates()->delete(); ValidityTime::query()->whereKey($existingValidityTimeIds)->delete(); $eventDates = collect(['2026-10-09', '2026-10-10', '2026-10-11', '2026-10-12']) - ->map(function (string $date) use ($tenant): EventDate { + ->map(function (string $date) use ($tenant, $event): EventDate { $validityTime = ValidityTime::query()->create([ 'type' => ValidityTimeType::FixedWindow, 'start_time' => null, @@ -69,9 +90,10 @@ class FiestaFutbolInfantilProductSeeder extends Seeder 'time_start' => '00:00:00', 'time_end' => '23:59:59', 'validity_time_id' => $validityTime->id, + 'tenant_code' => $tenant->codigo, ]); - return $tenant->eventDates()->save($eventDate); + return $event->dates()->save($eventDate); }); $dateIds = $eventDates->pluck('id')->map(fn ($id): int => (int) $id)->values(); @@ -152,6 +174,12 @@ class FiestaFutbolInfantilProductSeeder extends Seeder 'group_name' => 'Productos', 'group_order' => 0, ]); + + if ($tenant->active_event_id !== null) { + DB::table('catalog_items') + ->where('tenant_code', $tenant->codigo) + ->update(['event_id' => $tenant->active_event_id]); + } } private function deleteExistingCatalog(Tenant $tenant): void diff --git a/database/seeders/FiestaTradicionArrufoSeeder.php b/database/seeders/FiestaTradicionArrufoSeeder.php new file mode 100644 index 0000000..79b0dbb --- /dev/null +++ b/database/seeders/FiestaTradicionArrufoSeeder.php @@ -0,0 +1,118 @@ +where('codigo', 'onticket')->first(); + + if ($tenant === null) { + throw new RuntimeException("Tenant 'onticket' no encontrado."); + } + + Attribute::query()->firstOrCreate( + ['tenant_codigo' => $tenant->codigo, 'codigo' => 'event_date'], + ['nombre' => 'Fecha', 'type' => FieldType::EventDate, 'is_required' => true], + ); + + $event = Event::query()->firstOrCreate( + ['tenant_code' => $tenant->codigo, 'title' => self::TITLE], + ['published_at' => now()], + ); + $event->update([ + 'subtitle' => 'Una noche para celebrar nuestras raíces y mantener viva la tradición gaucha.', + 'description' => 'La 26.º Fiesta de la Tradición y 4.º Encuentro de Agrupaciones Gauchas reunirá a agrupaciones, artesanos, pilcheros y público en general para compartir una jornada dedicada a nuestras costumbres y cultura.' + ."\n\n".'Un encuentro para disfrutar de la tradición, la identidad gaucha y el espíritu de camaradería, en el Predio de Doma del Club Unión Deportiva Arrufó.' + ."\n\n".'Organiza: Biblioteca Popular Miguel Ángel Sosa.', + 'location' => 'Predio de Doma del Club Unión Deportiva Arrufó', + ]); + + $eventImage = $event->attachment; + if ($eventImage === null || ! Storage::disk('s3')->exists($eventImage->path)) { + $event->update(['attachment_id' => $this->attachments->store($this->image(), 'events')->id]); + } + + // La hora de cierre es provisoria hasta que la organización la confirme. + $date = $event->dates()->firstOrCreate( + ['date' => '2026-11-14'], + ['tenant_code' => $tenant->codigo, 'time_start' => '19:00', 'time_end' => '23:59'], + ); + + $this->createItem($tenant, $event, $date->id, [ + 'slug' => 'fiesta-tradicion-arrufo-2026-entrada-anticipada', + 'nombre' => 'Entrada anticipada', + 'descripcion' => 'Entrada para personas de 12 años en adelante. Venta anticipada hasta el 13/11/2026 a las 22:00 h.', + 'precio' => 12000, + 'sales_end_at' => Carbon::parse('2026-11-13 22:00:00', 'America/Argentina/Buenos_Aires')->utc(), + 'group_order' => 1, + ]); + + $this->createItem($tenant, $event, $date->id, [ + 'slug' => 'fiesta-tradicion-arrufo-2026-puesto-feria', + 'nombre' => 'Espacio para puesto en la feria', + 'descripcion' => 'Para artesanos, pilcheros y otros puestos de venta. Incluye una entrada para la persona que atiende el puesto. No se permiten puestos de venta de comidas ni bebidas.', + 'precio' => 50000, + 'group_order' => 2, + ]); + } + + /** @param array $data */ + private function createItem(Tenant $tenant, Event $event, int $dateId, array $data): void + { + $existing = CatalogItem::query()->where('tenant_code', $tenant->codigo)->where('slug', $data['slug'])->first(); + if ($existing !== null) { + $existing->update(['sales_end_at' => $data['sales_end_at'] ?? null]); + + return; + } + + $item = $this->catalog->create([ + 'tenant_code' => $tenant->codigo, + 'inventory_policy' => InventoryPolicy::Unlimited->value, + 'has_tickets' => true, + 'attribute_codes' => ['event_date'], + 'hidden_attribute_codes' => ['event_date'], + 'variants' => [['event_date_id' => $dateId]], + 'images' => [$this->image()], + ...$data, + ]); + + $item->forceFill(['event_id' => $event->id])->save(); + } + + private function image(): UploadedFile + { + $path = public_path(self::IMAGE); + + if (! is_file($path)) { + throw new RuntimeException("Image not found at path: {$path}"); + } + + return new UploadedFile($path, basename($path), 'image/avif', null, true); + } +} diff --git a/database/seeders/MenuSeeder.php b/database/seeders/MenuSeeder.php index 3ea8fb3..f698f67 100644 --- a/database/seeders/MenuSeeder.php +++ b/database/seeders/MenuSeeder.php @@ -2,10 +2,10 @@ namespace Database\Seeders; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Authorization\Models\Role; -use App\Domains\Menu\Models\Menu; -use App\Domains\Tenant\Models\Tenant; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Core\Authorization\Models\Role; +use App\Domains\Core\Menu\Models\Menu; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Database\Seeder; class MenuSeeder extends Seeder @@ -17,6 +17,22 @@ class MenuSeeder extends Seeder { $menus = [ ['code' => 'index', 'label' => 'Inicio', 'route' => '/'], + ['code' => 'event.index', 'label' => 'Eventos', 'route' => '/'], + [ + 'code' => 'event.category', + 'label' => 'Categoría de eventos', + 'route' => '/eventos/categoria/:id', + ], + [ + 'code' => 'event.detail', + 'label' => 'Detalle de evento', + 'route' => '/evento/:slug', + ], + [ + 'code' => 'product.category', + 'label' => 'Categoría de productos', + 'route' => '/categoria/:id', + ], [ 'code' => 'product.detail', 'label' => 'Detalle de producto', @@ -291,6 +307,14 @@ class MenuSeeder extends Seeder $desfileMenuCodes = [ 'adminapp.desfile.entradas', ]; + $onTicketMenuCodes = [ + 'event.index', + 'event.category', + 'event.detail', + ]; + $catalogMenuCodes = [ + 'product.category', + ]; $frequentlyAskedQuestions = [ [ 'pregunta' => '¿Hay algún límite de compra?', @@ -358,6 +382,12 @@ class MenuSeeder extends Seeder $menuCodes = array_diff($menuCodes, $desfileMenuCodes); } + if ($tenant->codigo !== 'onticket') { + $menuCodes = array_diff($menuCodes, $onTicketMenuCodes); + } else { + $menuCodes = array_diff($menuCodes, $catalogMenuCodes); + } + // Usar sync para asociar los menues al tenant $tenant->menues()->sync($menuCodes); diff --git a/database/seeders/OnTicketTenantSeeder.php b/database/seeders/OnTicketTenantSeeder.php new file mode 100644 index 0000000..8d2b0d3 --- /dev/null +++ b/database/seeders/OnTicketTenantSeeder.php @@ -0,0 +1,117 @@ +firstOrCreate( + ['code' => 'onticket'], + ['name' => 'OnTicket'], + ); + $type = AdminWebsiteType::query()->where('codigo', 'onticket')->firstOrFail(); + $tenant = Tenant::query()->where('codigo', 'onticket')->first(); + + if ($tenant === null) { + $tenant = $this->tenantProvisioningService->create([ + 'client_id' => $client->id, + 'codigo' => 'onticket', + 'nombre' => $type->nombre, + 'dominio' => $type->dominio, + 'site_title' => $type->site_title, + 'primary_color' => $type->primary_color, + 'secondary_color' => $type->secondary_color, + 'danger_color' => $type->danger_color, + 'success_color' => $type->success_color, + 'header_bg_color' => $type->surface_color, + 'footer_bg_color' => $type->primary_color, + 'display_categories' => true, + 'display_seach_bar' => true, + 'admin_website_type_code' => $type->codigo, + 'storefront_website_type_code' => 'onticket_multi_event', + 'header_logo' => $this->uploadedImage('onticket_logo.png'), + 'footer_logo' => $this->uploadedImage('onticket_footer_logo.png'), + 'favicon' => $this->uploadedImage('onticket_favicon.svg'), + 'header_bg_image' => $this->uploadedImage('onticket_header_background.avif'), + ]); + } else { + $tenant->update([ + 'client_id' => $client->id, + 'storefront_website_type_code' => 'onticket_multi_event', + 'display_categories' => true, + 'display_seach_bar' => true, + ]); + + $missingImages = []; + foreach ([ + 'header_logo' => ['relation' => 'headerLogo', 'filename' => 'onticket_logo.png'], + 'footer_logo' => ['relation' => 'footerLogo', 'filename' => 'onticket_footer_logo.png'], + 'favicon' => ['relation' => 'favicon', 'filename' => 'onticket_favicon.svg'], + 'header_bg_image' => ['relation' => 'headerBackgroundImage', 'filename' => 'onticket_header_background.avif'], + ] as $field => $image) { + $attachment = $tenant->{$image['relation']}; + + if ($attachment === null || ! Storage::disk('s3')->exists($attachment->path)) { + $missingImages[$field] = $this->uploadedImage($image['filename']); + } + } + + if ($missingImages !== []) { + $tenant = $this->tenantProvisioningService->update($tenant, $missingImages); + } + } + + foreach (['Música', 'Teatro', 'Deportes', 'Infantiles', 'Otros'] as $name) { + $tenant->eventCategories()->firstOrCreate(['nombre' => $name]); + } + + if (! $tenant->websiteExtras()->whereHas( + 'websiteTypeExtra', + fn ($query) => $query->where('codigo', 'immersiveHero') + )->exists()) { + $this->websiteExtraService->updateForTenant($tenant, 'immersiveHero', [ + 'eyebrow' => 'Encendé tu', + 'title' => 'experiencia', + 'description' => 'Reservá tu entrada y formá parte', + ]); + } + + $this->carouselSeeder->run(); + } + + private function uploadedImage(string $filename): UploadedFile + { + $path = public_path(self::IMAGE_DIRECTORY."/{$filename}"); + + if (! is_file($path)) { + throw new RuntimeException("Image not found at path: {$path}"); + } + + $mimeType = match (strtolower(pathinfo($filename, PATHINFO_EXTENSION))) { + 'svg' => 'image/svg+xml', + 'avif' => 'image/avif', + default => 'image/png', + }; + + return new UploadedFile($path, $filename, $mimeType, null, true); + } +} diff --git a/database/seeders/OnticketImmersiveHeroCarouselSeeder.php b/database/seeders/OnticketImmersiveHeroCarouselSeeder.php new file mode 100644 index 0000000..888f0c4 --- /dev/null +++ b/database/seeders/OnticketImmersiveHeroCarouselSeeder.php @@ -0,0 +1,61 @@ +where('codigo', 'onticket')->first(); + + if ($tenant?->storefront_website_type_code !== 'onticket_multi_event') { + return; + } + + if ($tenant->websiteExtras()->whereHas( + 'websiteTypeExtra', + fn ($query) => $query->where('codigo', 'immersiveHeroCarousel') + )->exists()) { + return; + } + + $images = array_map(function (string $filename): UploadedFile { + $path = public_path(self::IMAGE_DIRECTORY."/{$filename}"); + + if (! is_file($path)) { + throw new RuntimeException("Image not found at path: {$path}"); + } + + $mimeType = match (strtolower(pathinfo($filename, PATHINFO_EXTENSION))) { + 'jpg', 'jpeg', 'jfif' => 'image/jpeg', + 'avif' => 'image/avif', + default => 'image/png', + }; + + return new UploadedFile($path, $filename, $mimeType, null, true); + }, self::IMAGES); + + $this->websiteExtraService->updateForTenant($tenant, 'immersiveHeroCarousel', $images); + } +} diff --git a/database/seeders/OptimizeOnticketImagesSeeder.php b/database/seeders/OptimizeOnticketImagesSeeder.php new file mode 100644 index 0000000..ccd1cc5 --- /dev/null +++ b/database/seeders/OptimizeOnticketImagesSeeder.php @@ -0,0 +1,155 @@ + 'live-music.avif', + '[Prueba] Festival de Sabores' => 'food-festival.avif', + '[Prueba] Stand Up en el Centro' => 'stand-up.avif', + ]; + + public function run(): void + { + $tenant = Tenant::query()->where('codigo', 'onticket')->first(); + + if ($tenant === null) { + return; + } + + $this->replaceAttachment( + $tenant->headerBackgroundImage, + 'onticket_header_background.avif', + ); + + $carousel = $tenant->websiteExtras() + ->whereHas( + 'websiteTypeExtra', + fn ($query) => $query->where('codigo', 'immersiveHeroCarousel') + ) + ->first(); + $carouselIds = is_array($carousel?->config) ? array_values($carousel->config) : []; + + foreach (self::CAROUSEL_IMAGES as $index => $filename) { + $attachmentId = $carouselIds[$index] ?? null; + + if (is_int($attachmentId)) { + $this->replaceAttachment( + Attachment::query()->find($attachmentId), + 'onticket_tennant_hero_carousel/'.$filename, + ); + } + } + + foreach (self::EVENT_IMAGES as $title => $filename) { + $event = $tenant->events()->where('title', $title)->first(); + + $this->replaceAttachment( + $event?->attachment, + 'test-events/'.$filename, + ); + } + } + + private function replaceAttachment(?Attachment $attachment, string $relativePath): void + { + if ($attachment === null) { + return; + } + + $sourcePath = public_path(self::IMAGE_DIRECTORY.'/'.$relativePath); + + if (! is_file($sourcePath)) { + throw new RuntimeException("Image not found at path: {$sourcePath}"); + } + + $contents = file_get_contents($sourcePath); + + if (! is_string($contents) || $contents === '') { + throw new RuntimeException("Could not read image at path: {$sourcePath}"); + } + + $filename = basename($relativePath); + $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + $mimeType = match ($extension) { + 'avif' => 'image/avif', + 'svg' => 'image/svg+xml', + default => 'image/png', + }; + $directory = trim(str_replace('\\', '/', dirname($attachment->path)), './'); + $storedFilename = $attachment->key.($extension === '' ? '' : '.'.$extension); + $newPath = $directory === '' ? $storedFilename : $directory.'/'.$storedFilename; + $disk = Storage::disk('s3'); + + if ( + $attachment->path === $newPath + && $attachment->filename === $filename + && $attachment->mime_type === $mimeType + && $attachment->size === strlen($contents) + && $disk->exists($newPath) + ) { + return; + } + + $oldPath = $attachment->path; + $oldContents = $disk->exists($oldPath) ? $disk->get($oldPath) : null; + + if (! $disk->put($newPath, $contents)) { + throw new RuntimeException("Could not store optimized image at path: {$newPath}"); + } + + try { + DB::transaction(function () use ( + $attachment, + $extension, + $filename, + $mimeType, + $newPath, + $contents, + ): void { + $attachment->update([ + 'path' => $newPath, + 'filename' => $filename, + 'type' => 'image', + 'mime_type' => $mimeType, + 'extension' => $extension, + 'size' => strlen($contents), + ]); + }); + } catch (Throwable $throwable) { + if ($newPath === $oldPath && is_string($oldContents)) { + $disk->put($oldPath, $oldContents); + } else { + $disk->delete($newPath); + } + + throw $throwable; + } + + if ($oldPath !== $newPath && $disk->exists($oldPath)) { + $disk->delete($oldPath); + } + } +} diff --git a/database/seeders/ProductCatalogFromImagesSeeder.php b/database/seeders/ProductCatalogFromImagesSeeder.php index b0a2925..2416eca 100644 --- a/database/seeders/ProductCatalogFromImagesSeeder.php +++ b/database/seeders/ProductCatalogFromImagesSeeder.php @@ -2,17 +2,17 @@ namespace Database\Seeders; -use App\Domains\Catalog\Enums\FeaturedGroupSource; -use App\Domains\Catalog\Enums\GroupLayout; -use App\Domains\Catalog\Enums\InventoryPolicy; -use App\Domains\Catalog\Enums\ProductLayout; -use App\Domains\Catalog\Models\Attribute; -use App\Domains\Catalog\Models\Brand; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Category; -use App\Domains\Catalog\Models\FeaturedGroup; -use App\Domains\Catalog\Services\CatalogService; -use App\Domains\Tenant\Models\Tenant; +use App\Domains\Commerce\Catalog\Enums\FeaturedGroupSource; +use App\Domains\Commerce\Catalog\Enums\GroupLayout; +use App\Domains\Commerce\Catalog\Enums\InventoryPolicy; +use App\Domains\Commerce\Catalog\Enums\ProductLayout; +use App\Domains\Commerce\Catalog\Models\Attribute; +use App\Domains\Commerce\Catalog\Models\Brand; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Category; +use App\Domains\Commerce\Catalog\Models\FeaturedGroup; +use App\Domains\Commerce\Catalog\Services\CatalogService; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Database\Seeder; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\File; diff --git a/database/seeders/SocialMediaSeeder.php b/database/seeders/SocialMediaSeeder.php index 97e2b40..95ce89f 100644 --- a/database/seeders/SocialMediaSeeder.php +++ b/database/seeders/SocialMediaSeeder.php @@ -2,7 +2,7 @@ namespace Database\Seeders; -use App\Domains\Tenant\Models\SocialMedia; +use App\Domains\Core\Tenant\Models\SocialMedia; use Illuminate\Database\Seeder; class SocialMediaSeeder extends Seeder diff --git a/database/seeders/TelepagosIntegrationSeeder.php b/database/seeders/TelepagosIntegrationSeeder.php index bbaf657..2bc13e6 100644 --- a/database/seeders/TelepagosIntegrationSeeder.php +++ b/database/seeders/TelepagosIntegrationSeeder.php @@ -2,7 +2,7 @@ namespace Database\Seeders; -use App\Domains\Integration\Models\Integration; +use App\Shared\Integration\Models\Integration; use Illuminate\Database\Seeder; class TelepagosIntegrationSeeder extends Seeder diff --git a/database/seeders/TenantSeeder.php b/database/seeders/TenantSeeder.php index 33ecb24..43c4dbe 100644 --- a/database/seeders/TenantSeeder.php +++ b/database/seeders/TenantSeeder.php @@ -2,11 +2,11 @@ namespace Database\Seeders; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Attachable\Services\AttachmentService; -use App\Domains\Client\Models\Client; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Services\TenantService; +use App\Domains\Core\Client\Models\Client; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Services\TenantProvisioningService; +use App\Shared\Attachable\Models\Attachment; +use App\Shared\Attachable\Services\AttachmentService; use Illuminate\Database\Seeder; use Illuminate\Http\UploadedFile; use RuntimeException; @@ -14,7 +14,7 @@ use Throwable; class TenantSeeder extends Seeder { - private const ONTICKET_CLIENT_CODE = 'onticket'; + private const PYME_RURAL_CLIENT_CODE = 'pyme_rural'; private const SONDER_CLIENT_CODE = 'sonder'; @@ -41,7 +41,9 @@ class TenantSeeder extends Seeder ], ]; - public function __construct(protected TenantService $tenantService) {} + public function __construct( + protected TenantProvisioningService $tenantService, + ) {} public function run(): void { @@ -50,11 +52,13 @@ class TenantSeeder extends Seeder ['name' => 'Sonder'], ); - $onTicketClient = Client::query()->firstOrCreate( - ['code' => self::ONTICKET_CLIENT_CODE], - ['name' => 'OnTicket'], + $pymeRuralClient = Client::query()->updateOrCreate( + ['code' => self::PYME_RURAL_CLIENT_CODE], + ['name' => 'Pyme Rural'], ); + $this->call(OnTicketTenantSeeder::class); + $this->deleteTenant('sonder'); Tenant::query() @@ -71,7 +75,7 @@ class TenantSeeder extends Seeder 'danger_color' => '#FF8888', 'success_color' => '#198754', 'header_bg_color' => '#ffffff', - 'footer_bg_color' => '#313131', + 'footer_bg_color' => '#6376F3', 'display_categories' => true, 'display_seach_bar' => true, 'display_cart' => true, @@ -80,7 +84,8 @@ class TenantSeeder extends Seeder 'header_logo' => $this->uploadedImage('images/tennants/sonder/sonder_header.png', 'sonder_header.png'), 'footer_logo' => $this->uploadedImage('images/tennants/sonder/sonder_footer.png', 'sonder_footer.png'), 'social_media' => self::SOCIAL_MEDIA, - 'website_type_code' => 'shopit', + 'admin_website_type_code' => 'shopit', + 'storefront_website_type_code' => 'shopit', 'extras' => [ 'carousel' => [ $this->uploadedImage( @@ -119,7 +124,7 @@ class TenantSeeder extends Seeder ->delete(); $this->tenantService->create([ - 'client_id' => $onTicketClient->id, + 'client_id' => $pymeRuralClient->id, 'codigo' => 'fiesta_futbol_infantil', 'nombre' => 'Fiesta Fútbol Infantil', 'dominio' => $fiestaDomain, @@ -129,7 +134,7 @@ class TenantSeeder extends Seeder 'danger_color' => '#FF8888', 'success_color' => '#198754', 'header_bg_color' => '#ffffff', - 'footer_bg_color' => '#015327', + 'footer_bg_color' => '#00973F', 'display_categories' => false, 'display_seach_bar' => false, 'display_cart' => true, @@ -148,7 +153,8 @@ class TenantSeeder extends Seeder 'futbol_infantil_favicon.png', ), 'social_media' => self::SOCIAL_MEDIA, - 'website_type_code' => 'onticket', + 'admin_website_type_code' => 'onticket', + 'storefront_website_type_code' => 'onticket', 'extras' => [ 'heroConfig' => [ 'title_html' => '

Fiesta Fútbol Infantil

', @@ -279,9 +285,11 @@ class TenantSeeder extends Seeder } $mimeType = match (strtolower(pathinfo($filename, PATHINFO_EXTENSION))) { - 'jpg', 'jpeg' => 'image/jpeg', + 'jpg', 'jpeg', 'jfif' => 'image/jpeg', + 'avif' => 'image/avif', 'gif' => 'image/gif', 'webp' => 'image/webp', + 'svg' => 'image/svg+xml', default => 'image/png', }; diff --git a/database/seeders/TestEventsSeeder.php b/database/seeders/TestEventsSeeder.php new file mode 100644 index 0000000..85fd98f --- /dev/null +++ b/database/seeders/TestEventsSeeder.php @@ -0,0 +1,236 @@ +where('codigo', 'onticket')->firstOrFail(); + + $events = [ + [ + 'title' => '[Prueba] Noche de Música en Vivo', + 'slug' => 'prueba-noche-de-musica-en-vivo', + 'category' => 'Música', + 'subtitle' => 'Una noche para cantar y bailar', + 'description' => 'Evento de prueba para explorar la cartelera de OnTicket.', + 'location' => 'Teatro Broadway, Rosario', + 'image' => 'live-music.avif', + 'weeks_from_now' => [2], + 'time_start' => '21:00:00', + 'time_end' => '23:30:00', + 'catalog_items' => [ + [ + 'slug' => 'prueba-noche-musica-entrada-general', + 'name' => 'Entrada general', + 'description' => 'Acceso general a la noche de música en vivo.', + 'price' => 18000, + 'stock' => 100, + ], + [ + 'slug' => 'prueba-noche-musica-entrada-vip', + 'name' => 'Entrada VIP', + 'description' => 'Acceso preferencial y ubicación VIP.', + 'price' => 32000, + 'stock' => 40, + ], + ], + ], + [ + 'title' => '[Prueba] Festival de Sabores', + 'slug' => 'prueba-festival-de-sabores', + 'category' => 'Otros', + 'subtitle' => 'Gastronomía y música para toda la familia', + 'description' => 'Evento de prueba con dos jornadas disponibles.', + 'location' => 'Parque de España, Rosario', + 'image' => 'food-festival.avif', + 'weeks_from_now' => [3, 3], + 'day_offsets' => [0, 1], + 'time_start' => '12:00:00', + 'time_end' => '20:00:00', + 'catalog_items' => [ + [ + 'slug' => 'prueba-festival-sabores-entrada-general', + 'name' => 'Entrada general', + 'description' => 'Acceso general para la jornada seleccionada.', + 'price' => 12000, + 'stock' => 150, + ], + [ + 'slug' => 'prueba-festival-sabores-pase-premium', + 'name' => 'Pase premium', + 'description' => 'Acceso premium para la jornada seleccionada.', + 'price' => 22000, + 'stock' => 50, + ], + ], + ], + [ + 'title' => '[Prueba] Stand Up en el Centro', + 'slug' => 'prueba-stand-up-en-el-centro', + 'category' => 'Teatro', + 'subtitle' => 'Humor para compartir', + 'description' => 'Evento de prueba para visualizar distintas propuestas.', + 'location' => 'Centro Cultural La Comedia, Rosario', + 'image' => 'stand-up.avif', + 'weeks_from_now' => [4], + 'time_start' => '20:30:00', + 'time_end' => '22:00:00', + 'catalog_items' => [ + [ + 'slug' => 'prueba-stand-up-entrada-general', + 'name' => 'Entrada general', + 'description' => 'Acceso general al show de stand up.', + 'price' => 14000, + 'stock' => 120, + ], + [ + 'slug' => 'prueba-stand-up-primera-fila', + 'name' => 'Primera fila', + 'description' => 'Ubicación preferencial en primera fila.', + 'price' => 26000, + 'stock' => 30, + ], + ], + ], + ]; + + DB::transaction(function () use ($tenant, $events): void { + Attribute::query()->firstOrCreate( + [ + 'tenant_codigo' => $tenant->codigo, + 'codigo' => 'event_date', + ], + [ + 'nombre' => 'Fecha', + 'type' => FieldType::EventDate->value, + 'is_required' => true, + ], + ); + + $entryCategory = Category::query()->firstOrCreate([ + 'tenant_code' => $tenant->codigo, + 'nombre' => 'Entradas', + ]); + + foreach ($events as $definition) { + $event = $tenant->events()->firstOrCreate( + ['title' => $definition['title']], + [ + 'slug' => $definition['slug'], + 'subtitle' => $definition['subtitle'], + 'description' => $definition['description'], + 'location' => $definition['location'], + 'published_at' => now(), + 'event_category_id' => $tenant->eventCategories() + ->where('nombre', $definition['category'])->value('id'), + ], + ); + + if ($event->slug !== $definition['slug']) { + $event->update(['slug' => $definition['slug']]); + } + + if ($event->event_category_id === null) { + $event->update(['event_category_id' => $tenant->eventCategories() + ->where('nombre', $definition['category'])->value('id')]); + } + + if ($event->attachment_id === null) { + $path = public_path(self::IMAGE_DIRECTORY.'/'.$definition['image']); + + if (! is_file($path)) { + throw new RuntimeException("Image not found at path: {$path}"); + } + + $attachment = $this->attachmentService->store( + new UploadedFile($path, $definition['image'], 'image/avif', null, true), + 'tenants/onticket/events', + ); + $event->update(['attachment_id' => $attachment->id]); + } + + if (! $event->dates()->exists()) { + foreach ($definition['weeks_from_now'] as $index => $weeks) { + $date = now()->startOfDay()->addWeeks($weeks) + ->addDays($definition['day_offsets'][$index] ?? 0); + + $event->dates()->create([ + 'tenant_code' => $tenant->codigo, + 'date' => $date->toDateString(), + 'time_start' => $definition['time_start'], + 'time_end' => $definition['time_end'], + ]); + } + } + + $this->seedCatalogItems($tenant, $event, $entryCategory, $definition['catalog_items']); + } + }); + } + + /** @param array> $definitions */ + private function seedCatalogItems( + Tenant $tenant, + Event $event, + Category $category, + array $definitions, + ): void { + $eventDateIds = $event->dates() + ->pluck('id') + ->map(fn ($id): int => (int) $id) + ->values(); + + foreach ($definitions as $order => $definition) { + $catalogItem = CatalogItem::query() + ->where('tenant_code', $tenant->codigo) + ->where('slug', $definition['slug']) + ->first(); + + if ($catalogItem === null) { + $catalogItem = $this->catalogService->create([ + 'tenant_code' => $tenant->codigo, + 'category_id' => $category->id, + 'slug' => $definition['slug'], + 'nombre' => $definition['name'], + 'descripcion' => $definition['description'], + 'precio' => $definition['price'], + 'group_order' => $order, + 'has_tickets' => true, + 'inventory_policy' => InventoryPolicy::Tracked->value, + 'attribute_codes' => ['event_date'], + 'hidden_attribute_codes' => ['event_date'], + 'images' => [$event->attachment->key], + 'variants' => $eventDateIds->map(fn (int $eventDateId): array => [ + 'real_stock' => $definition['stock'], + 'event_date_ids' => [$eventDateId], + ])->all(), + ]); + } + + $catalogItem->forceFill(['event_id' => $event->id])->save(); + } + } +} diff --git a/database/seeders/WebsiteTypeSeeder.php b/database/seeders/WebsiteTypeSeeder.php index 7db0556..230a876 100644 --- a/database/seeders/WebsiteTypeSeeder.php +++ b/database/seeders/WebsiteTypeSeeder.php @@ -2,13 +2,27 @@ namespace Database\Seeders; -use App\Domains\Tenant\Services\WebsiteTypeService; +use App\Domains\Core\Tenant\Models\StorefrontWebsiteType; +use App\Domains\Core\Tenant\Services\AdminWebsiteTypeService; use Illuminate\Database\Seeder; use Illuminate\Http\UploadedFile; use RuntimeException; class WebsiteTypeSeeder extends Seeder { + private const IMAGE_CAROUSEL_SCHEMA = [ + 'request_rules' => [ + '$' => 'required|array|max:10', + '$.*' => 'required|image_or_base64|distinct', + ], + 'transforms' => [ + '$.*' => [ + 'handler' => 'attachment', + 'attachment_type' => 'image', + ], + ], + ]; + private const PRESENTATION = [ 'primary_color' => '#FF7006', 'secondary_color' => '#777777', @@ -22,7 +36,7 @@ class WebsiteTypeSeeder extends Seeder 'border_color' => '#EAEAEA', ]; - public function __construct(private readonly WebsiteTypeService $websiteTypeService) {} + public function __construct(private readonly AdminWebsiteTypeService $websiteTypeService) {} public function run(): void { @@ -40,24 +54,22 @@ class WebsiteTypeSeeder extends Seeder ], ); - $shopIt->extras()->updateOrCreate( + $shopItStorefront = StorefrontWebsiteType::query()->updateOrCreate( + ['codigo' => 'shopit'], + [ + 'nombre' => 'ShopIt', + 'header_type' => 'standard', + 'checkout_summary_type' => StorefrontWebsiteType::CHECKOUT_SUMMARY_DEFAULT, + ], + ); + + $shopItStorefront->extras()->updateOrCreate( ['codigo' => 'carousel'], [ 'nombre' => 'Carrusel principal', 'descripcion' => 'Listado de attachments que se muestran en el carousel principal.', 'is_required' => false, - 'config_schema' => [ - 'request_rules' => [ - '$' => 'required|array|max:10', - '$.*' => 'required|image_or_base64|distinct', - ], - 'transforms' => [ - '$.*' => [ - 'handler' => 'attachment', - 'attachment_type' => 'image', - ], - ], - ], + 'config_schema' => self::IMAGE_CAROUSEL_SCHEMA, ], ); @@ -75,7 +87,53 @@ class WebsiteTypeSeeder extends Seeder ], ); - $onTicket->extras()->updateOrCreate( + $onTicketStorefront = StorefrontWebsiteType::query()->updateOrCreate( + ['codigo' => 'onticket'], + [ + 'nombre' => 'OnTicket - evento único', + 'header_type' => 'standard', + 'checkout_summary_type' => StorefrontWebsiteType::CHECKOUT_SUMMARY_DEFAULT, + ], + ); + + $multiEventStorefront = StorefrontWebsiteType::query()->updateOrCreate( + ['codigo' => 'onticket_multi_event'], + [ + 'nombre' => 'OnTicket - múltiples eventos', + 'header_type' => 'immersive', + 'checkout_summary_type' => StorefrontWebsiteType::CHECKOUT_SUMMARY_EVENT, + ], + ); + + $multiEventStorefront->extras()->updateOrCreate( + ['codigo' => 'immersiveHeroCarousel'], + [ + 'nombre' => 'Carrusel del hero', + 'descripcion' => 'Lista ordenada de imágenes del hero.', + 'is_required' => false, + 'config_schema' => self::IMAGE_CAROUSEL_SCHEMA, + ], + ); + + $multiEventStorefront->extras()->updateOrCreate( + ['codigo' => 'immersiveHero'], + [ + 'nombre' => 'Textos del hero inmersivo', + 'descripcion' => 'Frase superior, título y descripción de la portada.', + 'is_required' => false, + 'config_schema' => [ + 'request_rules' => [ + '$' => 'required|array:eyebrow,title,description', + 'eyebrow' => 'required|string|max:120', + 'title' => 'required|string|max:120', + 'description' => 'required|string|max:255', + ], + 'transforms' => [], + ], + ], + ); + + $onTicketStorefront->extras()->updateOrCreate( ['codigo' => 'heroConfig'], [ 'nombre' => 'Configuración del hero', @@ -100,7 +158,7 @@ class WebsiteTypeSeeder extends Seeder ], ); - $onTicket->extras()->updateOrCreate( + $onTicketStorefront->extras()->updateOrCreate( ['codigo' => 'eventConfig'], [ 'nombre' => 'Información del evento', @@ -127,7 +185,7 @@ class WebsiteTypeSeeder extends Seeder ], ); - $onTicket->extras()->updateOrCreate( + $onTicketStorefront->extras()->updateOrCreate( ['codigo' => 'additionalInfoConfig'], [ 'nombre' => 'Información adicional', diff --git a/postman/generate-shopit-collection.php b/postman/generate-shopit-collection.php index 7393dee..6afedd2 100644 --- a/postman/generate-shopit-collection.php +++ b/postman/generate-shopit-collection.php @@ -120,53 +120,6 @@ function bodyFor(string $method, string $uri): ?array return jsonBody($exact[$key]); } - if ($key === 'POST api/tenants') { - return jsonBody([ - 'client_id' => '{{client_id}}', - 'codigo' => '{{tenant_code}}', - 'nombre' => 'Tenant Demo', - 'dominio' => '{{tenant_domain}}', - 'site_title' => 'ShopIt Demo', - 'primary_color' => '#111827', - 'secondary_color' => '#2563EB', - 'danger_color' => '#DC2626', - 'success_color' => '#16A34A', - 'header_bg_color' => '#FFFFFF', - 'footer_bg_color' => '#111827', - 'header_logo' => TINY_PNG, - 'footer_logo' => TINY_PNG, - 'search_product_layout' => 'column_with_image', - 'search_group_layout' => 'paginated', - 'search_items_per_page' => 12, - 'display_categories' => true, - 'display_seach_bar' => true, - 'display_cart' => true, - 'cart_editing_policy' => 'full', - 'website_type_code' => 'shopit', - ]); - } - - if (in_array($key, ['PUT api/tenants/{tenant}', 'PATCH api/tenants/{tenant}'], true)) { - return jsonBody(['nombre' => 'Tenant Demo Actualizado', 'site_title' => 'ShopIt Demo', 'primary_color' => '#111827', 'cart_editing_policy' => 'full']); - } - - if ($key === 'POST api/tenants/{tenant:codigo}/catalog-items') { - return jsonBody([ - 'type' => 'standard', - 'category_id' => '{{category_id}}', - 'slug' => 'producto-demo', - 'nombre' => 'Producto Demo', - 'descripcion' => 'Producto creado desde Postman', - 'precio' => 10000, - 'inventory_policy' => 'tracked', - 'inventory_subject' => 'product', - 'max_units_per_user' => 5, - 'has_tickets' => false, - 'real_stock' => 100, - 'images' => [TINY_PNG], - ]); - } - if ($key === 'POST api/storage-test/s3/upload') { return formDataBody(['path' => 'postman/test-file.png', 'expires_in_minutes' => '60'], ['file']); } diff --git a/public/images/tennants/onticket/events/fiesta-tradicion-arrufo-2026.avif b/public/images/tennants/onticket/events/fiesta-tradicion-arrufo-2026.avif new file mode 100644 index 0000000..7aae782 Binary files /dev/null and b/public/images/tennants/onticket/events/fiesta-tradicion-arrufo-2026.avif differ diff --git a/public/images/tennants/onticket/onticket_favicon.svg b/public/images/tennants/onticket/onticket_favicon.svg new file mode 100644 index 0000000..30502c6 --- /dev/null +++ b/public/images/tennants/onticket/onticket_favicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/tennants/onticket/onticket_footer_logo.png b/public/images/tennants/onticket/onticket_footer_logo.png new file mode 100644 index 0000000..dd86921 Binary files /dev/null and b/public/images/tennants/onticket/onticket_footer_logo.png differ diff --git a/public/images/tennants/onticket/onticket_header_background.png b/public/images/tennants/onticket/onticket_header_background.png new file mode 100644 index 0000000..d8c168f Binary files /dev/null and b/public/images/tennants/onticket/onticket_header_background.png differ diff --git a/public/images/tennants/onticket/onticket_logo.png b/public/images/tennants/onticket/onticket_logo.png new file mode 100644 index 0000000..12cd594 Binary files /dev/null and b/public/images/tennants/onticket/onticket_logo.png differ diff --git a/public/images/tennants/onticket/onticket_tennant_hero_carousel/onticket_hero_carousel_1.png b/public/images/tennants/onticket/onticket_tennant_hero_carousel/onticket_hero_carousel_1.png new file mode 100644 index 0000000..3156e6e Binary files /dev/null and b/public/images/tennants/onticket/onticket_tennant_hero_carousel/onticket_hero_carousel_1.png differ diff --git a/public/images/tennants/onticket/onticket_tennant_hero_carousel/onticket_hero_carousel_2.png b/public/images/tennants/onticket/onticket_tennant_hero_carousel/onticket_hero_carousel_2.png new file mode 100644 index 0000000..dce4a17 Binary files /dev/null and b/public/images/tennants/onticket/onticket_tennant_hero_carousel/onticket_hero_carousel_2.png differ diff --git a/public/images/tennants/onticket/onticket_tennant_hero_carousel/onticket_hero_carousel_3.avif b/public/images/tennants/onticket/onticket_tennant_hero_carousel/onticket_hero_carousel_3.avif new file mode 100644 index 0000000..94ec3f9 Binary files /dev/null and b/public/images/tennants/onticket/onticket_tennant_hero_carousel/onticket_hero_carousel_3.avif differ diff --git a/public/images/tennants/onticket/onticket_tennant_hero_carousel/onticket_hero_carousel_4.jfif b/public/images/tennants/onticket/onticket_tennant_hero_carousel/onticket_hero_carousel_4.jfif new file mode 100644 index 0000000..5a2c979 Binary files /dev/null and b/public/images/tennants/onticket/onticket_tennant_hero_carousel/onticket_hero_carousel_4.jfif differ diff --git a/public/images/tennants/onticket/onticket_tennant_hero_carousel/onticket_hero_carousel_5.jfif b/public/images/tennants/onticket/onticket_tennant_hero_carousel/onticket_hero_carousel_5.jfif new file mode 100644 index 0000000..5052ff4 Binary files /dev/null and b/public/images/tennants/onticket/onticket_tennant_hero_carousel/onticket_hero_carousel_5.jfif differ diff --git a/public/images/tennants/onticket/onticket_tennant_hero_carousel/onticket_hero_carousel_6.jfif b/public/images/tennants/onticket/onticket_tennant_hero_carousel/onticket_hero_carousel_6.jfif new file mode 100644 index 0000000..9457473 Binary files /dev/null and b/public/images/tennants/onticket/onticket_tennant_hero_carousel/onticket_hero_carousel_6.jfif differ diff --git a/public/images/tennants/onticket/onticket_tennant_hero_carousel/onticket_hero_carousel_7.avif b/public/images/tennants/onticket/onticket_tennant_hero_carousel/onticket_hero_carousel_7.avif new file mode 100644 index 0000000..d2cdf38 Binary files /dev/null and b/public/images/tennants/onticket/onticket_tennant_hero_carousel/onticket_hero_carousel_7.avif differ diff --git a/public/images/tennants/onticket/onticket_tennant_hero_carousel/onticket_hero_carousel_8.avif b/public/images/tennants/onticket/onticket_tennant_hero_carousel/onticket_hero_carousel_8.avif new file mode 100644 index 0000000..3eef8a3 Binary files /dev/null and b/public/images/tennants/onticket/onticket_tennant_hero_carousel/onticket_hero_carousel_8.avif differ diff --git a/public/images/tennants/onticket/test-events/food-festival.png b/public/images/tennants/onticket/test-events/food-festival.png new file mode 100644 index 0000000..a6a31f1 Binary files /dev/null and b/public/images/tennants/onticket/test-events/food-festival.png differ diff --git a/public/images/tennants/onticket/test-events/live-music.png b/public/images/tennants/onticket/test-events/live-music.png new file mode 100644 index 0000000..4eca393 Binary files /dev/null and b/public/images/tennants/onticket/test-events/live-music.png differ diff --git a/public/images/tennants/onticket/test-events/stand-up.png b/public/images/tennants/onticket/test-events/stand-up.png new file mode 100644 index 0000000..e872e87 Binary files /dev/null and b/public/images/tennants/onticket/test-events/stand-up.png differ diff --git a/public/images/tennants/onticket_otpimizado/onticket_favicon.svg b/public/images/tennants/onticket_otpimizado/onticket_favicon.svg new file mode 100644 index 0000000..30502c6 --- /dev/null +++ b/public/images/tennants/onticket_otpimizado/onticket_favicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/tennants/onticket_otpimizado/onticket_footer_logo.png b/public/images/tennants/onticket_otpimizado/onticket_footer_logo.png new file mode 100644 index 0000000..dd86921 Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/onticket_footer_logo.png differ diff --git a/public/images/tennants/onticket_otpimizado/onticket_header_background.avif b/public/images/tennants/onticket_otpimizado/onticket_header_background.avif new file mode 100644 index 0000000..1332b8e Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/onticket_header_background.avif differ diff --git a/public/images/tennants/onticket_otpimizado/onticket_logo.png b/public/images/tennants/onticket_otpimizado/onticket_logo.png new file mode 100644 index 0000000..12cd594 Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/onticket_logo.png differ diff --git a/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_1.avif b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_1.avif new file mode 100644 index 0000000..4b1ad34 Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_1.avif differ diff --git a/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_2.avif b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_2.avif new file mode 100644 index 0000000..6b7809f Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_2.avif differ diff --git a/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_3.avif b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_3.avif new file mode 100644 index 0000000..fcb3f2b Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_3.avif differ diff --git a/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_4.avif b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_4.avif new file mode 100644 index 0000000..cef2f75 Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_4.avif differ diff --git a/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_5.avif b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_5.avif new file mode 100644 index 0000000..6cf82df Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_5.avif differ diff --git a/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_6.avif b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_6.avif new file mode 100644 index 0000000..ddb764f Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_6.avif differ diff --git a/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_7.avif b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_7.avif new file mode 100644 index 0000000..cdb0017 Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_7.avif differ diff --git a/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_8.avif b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_8.avif new file mode 100644 index 0000000..b2bbad9 Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_8.avif differ diff --git a/public/images/tennants/onticket_otpimizado/test-events/food-festival.avif b/public/images/tennants/onticket_otpimizado/test-events/food-festival.avif new file mode 100644 index 0000000..68d9e5a Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/test-events/food-festival.avif differ diff --git a/public/images/tennants/onticket_otpimizado/test-events/live-music.avif b/public/images/tennants/onticket_otpimizado/test-events/live-music.avif new file mode 100644 index 0000000..c7b4246 Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/test-events/live-music.avif differ diff --git a/public/images/tennants/onticket_otpimizado/test-events/stand-up.avif b/public/images/tennants/onticket_otpimizado/test-events/stand-up.avif new file mode 100644 index 0000000..9e78288 Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/test-events/stand-up.avif differ diff --git a/resources/views/pdf/adminapp/sales.blade.php b/resources/views/pdf/adminapp/sales.blade.php index 5f35264..8436a0d 100644 --- a/resources/views/pdf/adminapp/sales.blade.php +++ b/resources/views/pdf/adminapp/sales.blade.php @@ -47,7 +47,7 @@ {{ $sale->created_at?->copy()->timezone($timeZone)->format('d/m/Y H:i') ?? '-' }} {{ $sale->nombre_apellido ?: 'Sin nombre' }} {{ (int) ($sale->quantity ?? 0) }} - {{ \App\Domains\Purchase\Models\Purchase::adminStatusNameFor($sale->status) ?? $sale->status }} + {{ \App\Domains\Commerce\Purchase\Models\Purchase::adminStatusNameFor($sale->status) ?? $sale->status }} ${{ number_format((float) $sale->total, 2, ',', '.') }} {{ (int) ($sale->tickets_count ?? 0) }} diff --git a/routes/api.php b/routes/api.php index 57ebc2b..d5b43b3 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,21 +1,21 @@ purpose('Delete tenant sales, carts, tickets and reservations while preserving users and catalog'); + +Artisan::command( + 'tickets:convert-validity-times-to-utc + {--ids= : IDs separados por coma} + {--all : Seleccionar todos los fixed_window} + {--source-timezone=America/Argentina/Buenos_Aires : Zona local; destino cuando se usa --reverse} + {--reverse : Convertir desde UTC hacia la zona local} + {--apply : Aplicar; sin esta opción sólo muestra una vista previa}', + function (ConvertValidityTimesToUtcService $service): int { + $input = trim((string) $this->option('ids')); + if ($input !== '' && ! preg_match('/^[1-9][0-9]*(\s*,\s*[1-9][0-9]*)*$/', $input)) { + $this->error('--ids debe contener enteros positivos separados por coma.'); + + return self::FAILURE; + } + $ids = $input === '' ? [] : array_values(array_unique(array_map('intval', explode(',', $input)))); + $timezone = (string) $this->option('source-timezone'); + $reverse = (bool) $this->option('reverse'); + $this->info($reverse ? "UTC -> {$timezone}" : "{$timezone} -> UTC"); + $this->warn('Cada ejecución con --apply transforma los valores actuales; no hay registro ni detección de conversiones anteriores.'); + try { + $result = $service->run($ids, $timezone, (bool) $this->option('apply'), (bool) $this->option('all'), $reverse); + } catch (Throwable $exception) { + $this->error($exception->getMessage()); + + return self::FAILURE; + } + $this->table(['ID', 'Inicio original', 'Fin original', 'Inicio resultante', 'Fin resultante'], $result); + $this->info($this->option('apply') ? 'Conversión aplicada.' : 'Vista previa: no se modificaron datos.'); + + return self::SUCCESS; + } +)->purpose('Convertir fixed_window entre hora local y UTC, con vista previa'); diff --git a/routes/web.php b/routes/web.php index 28c53d9..b79b678 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,6 @@ name('auth.google.redirect'); diff --git a/storage/framework/lsp-3d8bcef093e37c90.php b/storage/framework/lsp-3d8bcef093e37c90.php new file mode 100644 index 0000000..f14d849 --- /dev/null +++ b/storage/framework/lsp-3d8bcef093e37c90.php @@ -0,0 +1,130 @@ +hasDefaultValue()) { +return ['default' => $property->getDefaultValue()]; +} + +if ($parameter?->isDefaultValueAvailable()) { +return ['default' => $parameter->getDefaultValue()]; +} + +return []; +} + +public static function formatDefaultValue(mixed $value): mixed +{ +return match (true) { +is_array($value) => 'array(...)', +$value instanceof UnitEnum => get_class($value) . '::' . $value->name, +$value instanceof Closure => 'Closure', +is_object($value) => get_class($value), +is_string($value) => var_export($value, true), +is_null($value) => 'null', +is_bool($value) => $value ? 'true' : 'false', +default => $value, +}; +} +} + +use Pest\Expectation; +use Pest\TestSuite; + +$pest = new class +{ +public function __construct() +{ +if ($this->isInstalled()) { +$this->boot(); +} +} + +public function isInstalled(): bool +{ +return class_exists(TestSuite::class); +} + +protected function boot(): void +{ +require_once base_path('vendor/pestphp/pest/overrides/Runner/TestSuiteLoader.php'); + +TestSuite::getInstance(base_path(), 'tests'); + +if (file_exists($pestFile = base_path('tests/Pest.php'))) { +require_once $pestFile; +} +} + +public function config(): ?array +{ +if (!$this->isInstalled()) { +return null; +} + +return [ +'uses' => $this->uses(), +'expectations' => $this->expectations(), +]; +} + +protected function uses(): array +{ +if (is_null($instance = TestSuite::getInstance())) { +return []; +} + +$reflection = new ReflectionProperty($instance->tests, 'uses'); +$uses = $reflection->getValue($instance->tests); + +return collect($uses)->map(function (array $use, string $path) { +[$classOrTraits] = $use; + +return [ +'path' => LspHelper::relativePath($path), +'classes' => array_values(array_filter($classOrTraits, fn ($c) => class_exists($c))), +'traits' => array_values(array_filter($classOrTraits, fn ($c) => trait_exists($c))), +]; +})->values()->all(); +} + +protected function expectations(): array +{ +$reflection = new ReflectionProperty(Expectation::class, 'extends'); +$extends = $reflection->getValue(); + +return collect($extends)->map(function (Closure $closure, string $name) { +$parameters = collect((new ReflectionFunction($closure))->getParameters()) +->map(function (ReflectionParameter $param) { +$type = $param->hasType() ? $param->getType() . ' ' : ''; + +$default = $param->isOptional() && $param->isDefaultValueAvailable() +? ' = ' . var_export($param->getDefaultValue(), true) +: ''; + +return $type . '$' . $param->getName() . $default; +}) +->join(', '); + +return compact('name', 'parameters'); +})->values()->all(); +} +}; + +echo json_encode($pest->config()); diff --git a/storage/framework/lsp-52251fd8d9839a2d.php b/storage/framework/lsp-52251fd8d9839a2d.php new file mode 100644 index 0000000..f14d849 --- /dev/null +++ b/storage/framework/lsp-52251fd8d9839a2d.php @@ -0,0 +1,130 @@ +hasDefaultValue()) { +return ['default' => $property->getDefaultValue()]; +} + +if ($parameter?->isDefaultValueAvailable()) { +return ['default' => $parameter->getDefaultValue()]; +} + +return []; +} + +public static function formatDefaultValue(mixed $value): mixed +{ +return match (true) { +is_array($value) => 'array(...)', +$value instanceof UnitEnum => get_class($value) . '::' . $value->name, +$value instanceof Closure => 'Closure', +is_object($value) => get_class($value), +is_string($value) => var_export($value, true), +is_null($value) => 'null', +is_bool($value) => $value ? 'true' : 'false', +default => $value, +}; +} +} + +use Pest\Expectation; +use Pest\TestSuite; + +$pest = new class +{ +public function __construct() +{ +if ($this->isInstalled()) { +$this->boot(); +} +} + +public function isInstalled(): bool +{ +return class_exists(TestSuite::class); +} + +protected function boot(): void +{ +require_once base_path('vendor/pestphp/pest/overrides/Runner/TestSuiteLoader.php'); + +TestSuite::getInstance(base_path(), 'tests'); + +if (file_exists($pestFile = base_path('tests/Pest.php'))) { +require_once $pestFile; +} +} + +public function config(): ?array +{ +if (!$this->isInstalled()) { +return null; +} + +return [ +'uses' => $this->uses(), +'expectations' => $this->expectations(), +]; +} + +protected function uses(): array +{ +if (is_null($instance = TestSuite::getInstance())) { +return []; +} + +$reflection = new ReflectionProperty($instance->tests, 'uses'); +$uses = $reflection->getValue($instance->tests); + +return collect($uses)->map(function (array $use, string $path) { +[$classOrTraits] = $use; + +return [ +'path' => LspHelper::relativePath($path), +'classes' => array_values(array_filter($classOrTraits, fn ($c) => class_exists($c))), +'traits' => array_values(array_filter($classOrTraits, fn ($c) => trait_exists($c))), +]; +})->values()->all(); +} + +protected function expectations(): array +{ +$reflection = new ReflectionProperty(Expectation::class, 'extends'); +$extends = $reflection->getValue(); + +return collect($extends)->map(function (Closure $closure, string $name) { +$parameters = collect((new ReflectionFunction($closure))->getParameters()) +->map(function (ReflectionParameter $param) { +$type = $param->hasType() ? $param->getType() . ' ' : ''; + +$default = $param->isOptional() && $param->isDefaultValueAvailable() +? ' = ' . var_export($param->getDefaultValue(), true) +: ''; + +return $type . '$' . $param->getName() . $default; +}) +->join(', '); + +return compact('name', 'parameters'); +})->values()->all(); +} +}; + +echo json_encode($pest->config()); diff --git a/storage/framework/lsp-cd371f347782ba98.php b/storage/framework/lsp-cd371f347782ba98.php new file mode 100644 index 0000000..f14d849 --- /dev/null +++ b/storage/framework/lsp-cd371f347782ba98.php @@ -0,0 +1,130 @@ +hasDefaultValue()) { +return ['default' => $property->getDefaultValue()]; +} + +if ($parameter?->isDefaultValueAvailable()) { +return ['default' => $parameter->getDefaultValue()]; +} + +return []; +} + +public static function formatDefaultValue(mixed $value): mixed +{ +return match (true) { +is_array($value) => 'array(...)', +$value instanceof UnitEnum => get_class($value) . '::' . $value->name, +$value instanceof Closure => 'Closure', +is_object($value) => get_class($value), +is_string($value) => var_export($value, true), +is_null($value) => 'null', +is_bool($value) => $value ? 'true' : 'false', +default => $value, +}; +} +} + +use Pest\Expectation; +use Pest\TestSuite; + +$pest = new class +{ +public function __construct() +{ +if ($this->isInstalled()) { +$this->boot(); +} +} + +public function isInstalled(): bool +{ +return class_exists(TestSuite::class); +} + +protected function boot(): void +{ +require_once base_path('vendor/pestphp/pest/overrides/Runner/TestSuiteLoader.php'); + +TestSuite::getInstance(base_path(), 'tests'); + +if (file_exists($pestFile = base_path('tests/Pest.php'))) { +require_once $pestFile; +} +} + +public function config(): ?array +{ +if (!$this->isInstalled()) { +return null; +} + +return [ +'uses' => $this->uses(), +'expectations' => $this->expectations(), +]; +} + +protected function uses(): array +{ +if (is_null($instance = TestSuite::getInstance())) { +return []; +} + +$reflection = new ReflectionProperty($instance->tests, 'uses'); +$uses = $reflection->getValue($instance->tests); + +return collect($uses)->map(function (array $use, string $path) { +[$classOrTraits] = $use; + +return [ +'path' => LspHelper::relativePath($path), +'classes' => array_values(array_filter($classOrTraits, fn ($c) => class_exists($c))), +'traits' => array_values(array_filter($classOrTraits, fn ($c) => trait_exists($c))), +]; +})->values()->all(); +} + +protected function expectations(): array +{ +$reflection = new ReflectionProperty(Expectation::class, 'extends'); +$extends = $reflection->getValue(); + +return collect($extends)->map(function (Closure $closure, string $name) { +$parameters = collect((new ReflectionFunction($closure))->getParameters()) +->map(function (ReflectionParameter $param) { +$type = $param->hasType() ? $param->getType() . ' ' : ''; + +$default = $param->isOptional() && $param->isDefaultValueAvailable() +? ' = ' . var_export($param->getDefaultValue(), true) +: ''; + +return $type . '$' . $param->getName() . $default; +}) +->join(', '); + +return compact('name', 'parameters'); +})->values()->all(); +} +}; + +echo json_encode($pest->config()); diff --git a/tests/Concerns/CreatesIntegrationInstances.php b/tests/Concerns/CreatesIntegrationInstances.php index 8b37c0d..ee54a2e 100644 --- a/tests/Concerns/CreatesIntegrationInstances.php +++ b/tests/Concerns/CreatesIntegrationInstances.php @@ -2,8 +2,8 @@ namespace Tests\Concerns; -use App\Domains\Integration\Models\ClientIntegration; -use App\Domains\Integration\Models\IntegrationInstance; +use App\Shared\Integration\Models\ClientIntegration; +use App\Shared\Integration\Models\IntegrationInstance; trait CreatesIntegrationInstances { diff --git a/tests/Feature/Administrator/AdministratorControllerTest.php b/tests/Feature/Administrator/AdministratorControllerTest.php index 407592a..2ef0763 100644 --- a/tests/Feature/Administrator/AdministratorControllerTest.php +++ b/tests/Feature/Administrator/AdministratorControllerTest.php @@ -2,14 +2,14 @@ namespace Tests\Feature\Administrator; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\ResetPasswordAttempt; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Notification\Events\PasswordResetRequested; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Models\WebsiteType; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\ResetPasswordAttempt; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Shared\Notification\Events\PasswordResetRequested; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; use Database\Seeders\AuthorizationSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Event; @@ -30,14 +30,14 @@ class AdministratorControllerTest extends TestCase Event::fake([PasswordResetRequested::class]); $this->seed(AuthorizationSeeder::class); - WebsiteType::query()->create(['codigo' => 'onticket', 'nombre' => 'OnTicket']); + AdminWebsiteType::query()->create(['codigo' => 'onticket', 'nombre' => 'OnTicket']); $headerLogo = $this->createAttachment('header.png'); $footerLogo = $this->createAttachment('footer.png'); $this->tenant = Tenant::query()->create([ 'codigo' => 'acme', 'nombre' => 'Acme', 'dominio' => 'acme.test', - 'website_type_code' => 'onticket', + 'admin_website_type_code' => 'onticket', 'primary_color' => '#111111', 'secondary_color' => '#222222', 'danger_color' => '#cc0000', diff --git a/tests/Feature/Attachable/AttachmentTest.php b/tests/Feature/Attachable/AttachmentTest.php index 4cdf184..8a9019d 100644 --- a/tests/Feature/Attachable/AttachmentTest.php +++ b/tests/Feature/Attachable/AttachmentTest.php @@ -2,11 +2,11 @@ namespace Tests\Feature\Attachable; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Exceptions\AttachmentStorageException; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Attachable\Models\AttachmentCrop; -use App\Domains\Attachable\Services\AttachmentService; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Exceptions\AttachmentStorageException; +use App\Shared\Attachable\Models\Attachment; +use App\Shared\Attachable\Models\AttachmentCrop; +use App\Shared\Attachable\Services\AttachmentService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Storage; diff --git a/tests/Feature/Auth/AdminAppLoginControllerTest.php b/tests/Feature/Auth/AdminAppLoginControllerTest.php index a5d6070..97eeb3c 100644 --- a/tests/Feature/Auth/AdminAppLoginControllerTest.php +++ b/tests/Feature/Auth/AdminAppLoginControllerTest.php @@ -2,11 +2,11 @@ namespace Tests\Feature\Auth; -use App\Domains\Auth\Models\LoginAttempt; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Authorization\Models\Role; -use App\Domains\Tenant\Models\Tenant; +use App\Domains\Core\Auth\Models\LoginAttempt; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Core\Authorization\Models\Role; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Hash; use Tests\TestCase; diff --git a/tests/Feature/Auth/AdminAppMeControllerTest.php b/tests/Feature/Auth/AdminAppMeControllerTest.php index 6850b07..7bd0282 100644 --- a/tests/Feature/Auth/AdminAppMeControllerTest.php +++ b/tests/Feature/Auth/AdminAppMeControllerTest.php @@ -2,11 +2,11 @@ namespace Tests\Feature\Auth; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Authorization\Models\Role; -use App\Domains\Menu\Models\Menu; -use App\Domains\Tenant\Models\Tenant; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Core\Authorization\Models\Role; +use App\Domains\Core\Menu\Models\Menu; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; use Laravel\Sanctum\Sanctum; use Tests\TestCase; diff --git a/tests/Feature/Auth/AdminAppResetPasswordControllerTest.php b/tests/Feature/Auth/AdminAppResetPasswordControllerTest.php index 1e301a1..358878e 100644 --- a/tests/Feature/Auth/AdminAppResetPasswordControllerTest.php +++ b/tests/Feature/Auth/AdminAppResetPasswordControllerTest.php @@ -2,12 +2,12 @@ namespace Tests\Feature\Auth; -use App\Domains\Auth\Models\ResetPasswordAttempt; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Authorization\Models\Role; -use App\Domains\Notification\Events\PasswordResetRequested; -use App\Domains\Tenant\Models\Tenant; +use App\Domains\Core\Auth\Models\ResetPasswordAttempt; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Core\Authorization\Models\Role; +use App\Shared\Notification\Events\PasswordResetRequested; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Event; use Tests\TestCase; diff --git a/tests/Feature/Auth/CreateResetPasswordAttemptControllerTest.php b/tests/Feature/Auth/CreateResetPasswordAttemptControllerTest.php index cd7e241..25b0c09 100644 --- a/tests/Feature/Auth/CreateResetPasswordAttemptControllerTest.php +++ b/tests/Feature/Auth/CreateResetPasswordAttemptControllerTest.php @@ -2,12 +2,12 @@ namespace Tests\Feature\Auth; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\ResetPasswordAttempt; -use App\Domains\Auth\Models\User; -use App\Domains\Notification\Events\PasswordResetRequested; -use App\Domains\Tenant\Models\Tenant; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\ResetPasswordAttempt; +use App\Domains\Core\Auth\Models\User; +use App\Shared\Notification\Events\PasswordResetRequested; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Event; use Tests\TestCase; diff --git a/tests/Feature/Auth/EmailUniquenessPerRoleTest.php b/tests/Feature/Auth/EmailUniquenessPerRoleTest.php index a4e9d8d..255fc8b 100644 --- a/tests/Feature/Auth/EmailUniquenessPerRoleTest.php +++ b/tests/Feature/Auth/EmailUniquenessPerRoleTest.php @@ -2,13 +2,13 @@ namespace Tests\Feature\Auth; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\ResetPasswordAttempt; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Notification\Events\PasswordResetRequested; -use App\Domains\Tenant\Models\Tenant; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\ResetPasswordAttempt; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Shared\Notification\Events\PasswordResetRequested; +use App\Domains\Core\Tenant\Models\Tenant; use Database\Seeders\AuthorizationSeeder; use Illuminate\Database\UniqueConstraintViolationException; use Illuminate\Foundation\Testing\RefreshDatabase; diff --git a/tests/Feature/Auth/GoogleTokenExchangeControllerTest.php b/tests/Feature/Auth/GoogleTokenExchangeControllerTest.php index 4d40de0..85d6eb2 100644 --- a/tests/Feature/Auth/GoogleTokenExchangeControllerTest.php +++ b/tests/Feature/Auth/GoogleTokenExchangeControllerTest.php @@ -2,14 +2,14 @@ namespace Tests\Feature\Auth; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\User; -use App\Domains\Cart\Models\Cart; -use App\Domains\Catalog\Enums\InventoryPolicy; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Inventory; -use App\Domains\Tenant\Models\Tenant; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Commerce\Cart\Models\Cart; +use App\Domains\Commerce\Catalog\Enums\InventoryPolicy; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Inventory; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Str; diff --git a/tests/Feature/Auth/LoginControllerTest.php b/tests/Feature/Auth/LoginControllerTest.php index ed7cbd8..4368145 100644 --- a/tests/Feature/Auth/LoginControllerTest.php +++ b/tests/Feature/Auth/LoginControllerTest.php @@ -2,15 +2,15 @@ namespace Tests\Feature\Auth; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\LoginAttempt; -use App\Domains\Auth\Models\User; -use App\Domains\Cart\Models\Cart; -use App\Domains\Catalog\Enums\InventoryPolicy; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Inventory; -use App\Domains\Tenant\Models\Tenant; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\LoginAttempt; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Commerce\Cart\Models\Cart; +use App\Domains\Commerce\Catalog\Enums\InventoryPolicy; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Inventory; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Hash; use Tests\TestCase; diff --git a/tests/Feature/Auth/RegisterControllerTest.php b/tests/Feature/Auth/RegisterControllerTest.php index 8dafa91..2f59d86 100644 --- a/tests/Feature/Auth/RegisterControllerTest.php +++ b/tests/Feature/Auth/RegisterControllerTest.php @@ -2,11 +2,11 @@ namespace Tests\Feature\Auth; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\User; -use App\Domains\Notification\Events\UserRegistered; -use App\Domains\Tenant\Models\Tenant; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\User; +use App\Shared\Notification\Events\UserRegistered; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Event; use Tests\TestCase; diff --git a/tests/Feature/Auth/ResetPasswordAttemptTest.php b/tests/Feature/Auth/ResetPasswordAttemptTest.php index e9ef95f..9156a9f 100644 --- a/tests/Feature/Auth/ResetPasswordAttemptTest.php +++ b/tests/Feature/Auth/ResetPasswordAttemptTest.php @@ -2,8 +2,8 @@ namespace Tests\Feature\Auth; -use App\Domains\Auth\Models\ResetPasswordAttempt; -use App\Domains\Auth\Models\User; +use App\Domains\Core\Auth\Models\ResetPasswordAttempt; +use App\Domains\Core\Auth\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Schema; use Tests\TestCase; diff --git a/tests/Feature/Auth/ResetPasswordControllerTest.php b/tests/Feature/Auth/ResetPasswordControllerTest.php index 57f085f..4fc4ab6 100644 --- a/tests/Feature/Auth/ResetPasswordControllerTest.php +++ b/tests/Feature/Auth/ResetPasswordControllerTest.php @@ -2,8 +2,8 @@ namespace Tests\Feature\Auth; -use App\Domains\Auth\Models\ResetPasswordAttempt; -use App\Domains\Auth\Models\User; +use App\Domains\Core\Auth\Models\ResetPasswordAttempt; +use App\Domains\Core\Auth\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Hash; use Tests\TestCase; diff --git a/tests/Feature/Auth/ScannerLoginControllerTest.php b/tests/Feature/Auth/ScannerLoginControllerTest.php index 6bb37bc..758cdc1 100644 --- a/tests/Feature/Auth/ScannerLoginControllerTest.php +++ b/tests/Feature/Auth/ScannerLoginControllerTest.php @@ -2,16 +2,16 @@ namespace Tests\Feature\Auth; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\LoginAttempt; -use App\Domains\Auth\Models\ResetPasswordAttempt; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\PermissionCode; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Authorization\Models\Permission; -use App\Domains\Authorization\Models\Role; -use App\Domains\Notification\Events\PasswordResetRequested; -use App\Domains\Tenant\Models\Tenant; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\LoginAttempt; +use App\Domains\Core\Auth\Models\ResetPasswordAttempt; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\PermissionCode; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Core\Authorization\Models\Permission; +use App\Domains\Core\Authorization\Models\Role; +use App\Shared\Notification\Events\PasswordResetRequested; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Hash; diff --git a/tests/Feature/Auth/ScannerMeControllerTest.php b/tests/Feature/Auth/ScannerMeControllerTest.php index f5ec62b..576725d 100644 --- a/tests/Feature/Auth/ScannerMeControllerTest.php +++ b/tests/Feature/Auth/ScannerMeControllerTest.php @@ -2,15 +2,15 @@ namespace Tests\Feature\Auth; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\PermissionCode; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Authorization\Models\Permission; -use App\Domains\Authorization\Models\Role; -use App\Domains\Catalog\Models\Category; -use App\Domains\Menu\Models\Menu; -use App\Domains\Tenant\Models\Tenant; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\PermissionCode; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Core\Authorization\Models\Permission; +use App\Domains\Core\Authorization\Models\Role; +use App\Domains\Commerce\Catalog\Models\Category; +use App\Domains\Core\Menu\Models\Menu; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Str; use Laravel\Sanctum\Sanctum; diff --git a/tests/Feature/Auth/ScannerResetPasswordControllerTest.php b/tests/Feature/Auth/ScannerResetPasswordControllerTest.php index a8e80d6..2098201 100644 --- a/tests/Feature/Auth/ScannerResetPasswordControllerTest.php +++ b/tests/Feature/Auth/ScannerResetPasswordControllerTest.php @@ -2,12 +2,12 @@ namespace Tests\Feature\Auth; -use App\Domains\Auth\Models\ResetPasswordAttempt; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Authorization\Models\Role; -use App\Domains\Notification\Events\PasswordResetRequested; -use App\Domains\Tenant\Models\Tenant; +use App\Domains\Core\Auth\Models\ResetPasswordAttempt; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Core\Authorization\Models\Role; +use App\Shared\Notification\Events\PasswordResetRequested; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Hash; diff --git a/tests/Feature/Auth/UserAuthorizationRelationsTest.php b/tests/Feature/Auth/UserAuthorizationRelationsTest.php index 9565d81..d0719b6 100644 --- a/tests/Feature/Auth/UserAuthorizationRelationsTest.php +++ b/tests/Feature/Auth/UserAuthorizationRelationsTest.php @@ -2,8 +2,8 @@ namespace Tests\Feature\Auth; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Schema; use Tests\TestCase; diff --git a/tests/Feature/Auth/ValidateResetPasswordAttemptControllerTest.php b/tests/Feature/Auth/ValidateResetPasswordAttemptControllerTest.php index d6bd514..1f4c6c6 100644 --- a/tests/Feature/Auth/ValidateResetPasswordAttemptControllerTest.php +++ b/tests/Feature/Auth/ValidateResetPasswordAttemptControllerTest.php @@ -2,8 +2,8 @@ namespace Tests\Feature\Auth; -use App\Domains\Auth\Models\ResetPasswordAttempt; -use App\Domains\Auth\Models\User; +use App\Domains\Core\Auth\Models\ResetPasswordAttempt; +use App\Domains\Core\Auth\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; diff --git a/tests/Feature/Authorization/RolePermissionModelTest.php b/tests/Feature/Authorization/RolePermissionModelTest.php index 5bd7d11..64b4348 100644 --- a/tests/Feature/Authorization/RolePermissionModelTest.php +++ b/tests/Feature/Authorization/RolePermissionModelTest.php @@ -2,9 +2,9 @@ namespace Tests\Feature\Authorization; -use App\Domains\Authorization\Models\Permission; -use App\Domains\Authorization\Models\Role; -use App\Domains\Authorization\Models\RolePermission; +use App\Domains\Core\Authorization\Models\Permission; +use App\Domains\Core\Authorization\Models\Role; +use App\Domains\Core\Authorization\Models\RolePermission; use Illuminate\Database\QueryException; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Schema; diff --git a/tests/Feature/Cart/CartControllerTest.php b/tests/Feature/Cart/CartControllerTest.php index 1a5696a..762a08e 100644 --- a/tests/Feature/Cart/CartControllerTest.php +++ b/tests/Feature/Cart/CartControllerTest.php @@ -2,18 +2,18 @@ namespace Tests\Feature\Cart; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\User; -use App\Domains\Cart\Models\Cart; -use App\Domains\Catalog\Enums\InventoryPolicy; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Inventory; -use App\Domains\Catalog\Models\Variant; -use App\Domains\Catalog\Services\CatalogService; -use App\Domains\Purchase\Models\Purchase; -use App\Domains\Purchase\Models\PurchaseItem; -use App\Domains\Tenant\Models\Tenant; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Commerce\Cart\Models\Cart; +use App\Domains\Commerce\Catalog\Enums\InventoryPolicy; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Inventory; +use App\Domains\Commerce\Catalog\Models\Variant; +use App\Domains\Commerce\Catalog\Services\CatalogService; +use App\Domains\Commerce\Purchase\Models\Purchase; +use App\Domains\Commerce\Purchase\Models\PurchaseItem; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Schema; use Tests\TestCase; diff --git a/tests/Feature/Catalog/BundleCatalogItemTest.php b/tests/Feature/Catalog/BundleCatalogItemTest.php index e1e80c5..e0a0283 100644 --- a/tests/Feature/Catalog/BundleCatalogItemTest.php +++ b/tests/Feature/Catalog/BundleCatalogItemTest.php @@ -2,20 +2,20 @@ namespace Tests\Feature\Catalog; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\User; -use App\Domains\Cart\Models\Cart; -use App\Domains\Cart\Resources\CartItemResource; -use App\Domains\Catalog\Enums\CatalogItemType; -use App\Domains\Catalog\Enums\InventoryPolicy; -use App\Domains\Catalog\Models\Attribute; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Services\CatalogInventoryService; -use App\Domains\Catalog\Services\CatalogService; -use App\Domains\Purchase\Services\CheckoutService; -use App\Domains\Shared\Enums\FieldType; -use App\Domains\Tenant\Models\Tenant; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Commerce\Cart\Models\Cart; +use App\Domains\Commerce\Cart\Resources\CartItemResource; +use App\Domains\Commerce\Catalog\Enums\CatalogItemType; +use App\Domains\Commerce\Catalog\Enums\InventoryPolicy; +use App\Domains\Commerce\Catalog\Models\Attribute; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Services\CatalogInventoryService; +use App\Domains\Commerce\Catalog\Services\CatalogService; +use App\Domains\Commerce\Purchase\Services\CheckoutService; +use App\Shared\Enums\FieldType; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Validation\ValidationException; use Tests\TestCase; @@ -247,24 +247,14 @@ class BundleCatalogItemTest extends TestCase ]); } - public function test_catalog_api_creates_and_returns_bundle_details(): void + public function test_catalog_api_returns_bundle_details_created_by_the_service(): void { $component = $this->createStandardItem('api-component', 8); + $bundle = $this->createBundle('api-bundle', [ + ['catalog_item_id' => $component->id, 'quantity' => 2], + ], '75.00'); - $bundleId = $this->postJson("/api/tenants/{$this->tenant->codigo}/catalog-items", [ - 'type' => CatalogItemType::Bundle->value, - 'slug' => 'api-bundle', - 'nombre' => 'API Bundle', - 'precio' => 75, - 'components' => [ - ['catalog_item_id' => $component->id, 'quantity' => 2], - ], - ]) - ->assertCreated() - ->assertJsonPath('data.type', CatalogItemType::Bundle->value) - ->json('data.id'); - - $this->getJson("/api/tenants/{$this->tenant->codigo}/catalog-items/{$bundleId}") + $this->getJson("/api/tenants/{$this->tenant->codigo}/catalog-items/{$bundle->id}") ->assertOk() ->assertJsonPath('data.type', CatalogItemType::Bundle->value) ->assertJsonPath('data.maximum_addable_quantity', 4) @@ -274,7 +264,9 @@ class BundleCatalogItemTest extends TestCase ->assertJsonPath('data.components.0.variant_id', null) ->assertJsonPath('data.components.0.quantity', 2); - $this->postJson("/api/tenants/{$this->tenant->codigo}/catalog-items", [ + $this->expectException(ValidationException::class); + $this->catalogService->create([ + 'tenant_code' => $this->tenant->codigo, 'type' => CatalogItemType::Bundle->value, 'slug' => 'bundle-with-stock', 'nombre' => 'Invalid Bundle', @@ -283,9 +275,7 @@ class BundleCatalogItemTest extends TestCase 'components' => [ ['catalog_item_id' => $component->id, 'quantity' => 1], ], - ]) - ->assertUnprocessable() - ->assertJsonValidationErrors(['real_stock']); + ]); } public function test_bundle_components_are_preserved_when_the_bundle_is_soft_deleted(): void diff --git a/tests/Feature/Catalog/CatalogControllerTest.php b/tests/Feature/Catalog/CatalogControllerTest.php index 1eaa596..56e5530 100644 --- a/tests/Feature/Catalog/CatalogControllerTest.php +++ b/tests/Feature/Catalog/CatalogControllerTest.php @@ -2,18 +2,19 @@ namespace Tests\Feature\Catalog; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\User; -use App\Domains\Cart\Models\Cart; -use App\Domains\Catalog\Enums\FeaturedGroupSource; -use App\Domains\Catalog\Enums\GroupLayout; -use App\Domains\Catalog\Enums\ProductLayout; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Category; -use App\Domains\Catalog\Models\FeaturedGroup; -use App\Domains\Catalog\Models\Inventory; -use App\Domains\Tenant\Models\Tenant; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Commerce\Cart\Models\Cart; +use App\Domains\Commerce\Catalog\Enums\FeaturedGroupSource; +use App\Domains\Commerce\Catalog\Enums\GroupLayout; +use App\Domains\Commerce\Catalog\Enums\ProductLayout; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Category; +use App\Domains\Commerce\Catalog\Models\FeaturedGroup; +use App\Domains\Commerce\Catalog\Models\Inventory; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Ticketing\Event\Models\Event; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Storage; use Tests\TestCase; @@ -22,6 +23,41 @@ class CatalogControllerTest extends TestCase { use RefreshDatabase; + public function test_active_event_limits_catalog_reads_to_its_items(): void + { + $tenant = $this->createTenant('active-event-catalog'); + $group = $this->createGroup($tenant, ProductLayout::Row, 'Events', groupLayout: GroupLayout::Simple); + $group->update(['source_type' => FeaturedGroupSource::All]); + $activeEvent = Event::query()->create(['tenant_code' => $tenant->codigo, 'title' => 'Active']); + $otherEvent = Event::query()->create(['tenant_code' => $tenant->codigo, 'title' => 'Other']); + + $activeItem = $this->createItem($tenant, 'Active item', Inventory::query()->create(['real_stock' => 5])); + $activeItem->event_id = $activeEvent->id; + $activeItem->save(); + + $otherItem = $this->createItem($tenant, 'Other item', Inventory::query()->create(['real_stock' => 5])); + $otherItem->event_id = $otherEvent->id; + $otherItem->save(); + + $tenant->update(['active_event_id' => $activeEvent->id]); + + $this->getJson("/api/tenants/{$tenant->codigo}/catalog") + ->assertOk() + ->assertJsonCount(1, '0.items') + ->assertJsonPath('0.items.0.id', $activeItem->id) + ->assertJsonMissing(['nombre' => 'Other item']); + + $this->getJson("/api/tenants/{$tenant->codigo}/catalog-items?q=item") + ->assertOk() + ->assertJsonCount(1, 'data') + ->assertJsonPath('data.0.id', $activeItem->id); + + $this->getJson("/api/tenants/{$tenant->codigo}/catalog-items/{$otherItem->id}") + ->assertNotFound(); + $this->getJson("/api/tenants/{$tenant->codigo}/catalog-items/{$activeItem->id}") + ->assertOk(); + } + public function test_row_and_column_with_cart_return_item_details_variants_and_maximum_quantity(): void { $tenant = $this->createTenant('catalog-index'); diff --git a/tests/Feature/Catalog/CatalogItemControllerTest.php b/tests/Feature/Catalog/CatalogItemControllerTest.php deleted file mode 100644 index c805fe5..0000000 --- a/tests/Feature/Catalog/CatalogItemControllerTest.php +++ /dev/null @@ -1,131 +0,0 @@ -createTenant(); - $attribute = Attribute::query()->create([ - 'tenant_codigo' => $tenant->codigo, - 'codigo' => 'size', - 'nombre' => 'Size', - 'type' => FieldType::String, - ]); - $image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; - - $response = $this->postJson("/api/tenants/{$tenant->codigo}/catalog-items", [ - 'slug' => 'shirt', - 'nombre' => 'Shirt', - 'group_order' => 7, - 'precio' => 100, - 'max_units_per_user' => 4, - 'attribute_codes' => [$attribute->codigo], - 'images' => [$image, $image], - 'variants' => [ - [ - 'real_stock' => 5, - 'values' => ['size' => 'M'], - 'images' => [$image], - ], - ], - ]); - - $response - ->assertCreated() - ->assertJsonPath('data.nombre', 'Shirt') - ->assertJsonMissingPath('data.group_order') - ->assertJsonPath('data.max_units_per_user', 4) - ->assertJsonCount(2, 'data.images') - ->assertJsonCount(1, 'data.variants') - ->assertJsonCount(1, 'data.variants.0.images'); - - $item = CatalogItem::query()->where('slug', 'shirt')->firstOrFail(); - $variant = $item->variants()->firstOrFail(); - - $this->assertSame([0, 1], $item->attachments()->get()->pluck('pivot.orden')->all()); - $this->assertSame(4, $item->max_units_per_user); - $this->assertSame(7, $item->group_order); - $this->assertSame([0], $variant->attachments()->get()->pluck('pivot.orden')->all()); - $this->assertDatabaseHas('catalog_items_attachments', [ - 'catalog_item_id' => $item->id, - 'variant_id' => $variant->id, - 'orden' => 0, - 'is_enabled' => true, - ]); - } - - public function test_it_validates_images_before_creating_the_catalog_item(): void - { - $tenant = $this->createTenant('validation'); - - $this->postJson("/api/tenants/{$tenant->codigo}/catalog-items", [ - 'slug' => 'invalid-image', - 'nombre' => 'Invalid image', - 'precio' => 100, - 'real_stock' => 1, - 'images' => ['not-an-image'], - ])->assertUnprocessable()->assertJsonValidationErrors('images.0'); - - $this->assertDatabaseMissing('catalog_items', ['slug' => 'invalid-image']); - } - - public function test_it_rejects_a_non_positive_user_purchase_limit(): void - { - $tenant = $this->createTenant('purchase-limit-validation'); - - $this->postJson("/api/tenants/{$tenant->codigo}/catalog-items", [ - 'slug' => 'invalid-purchase-limit', - 'nombre' => 'Invalid purchase limit', - 'precio' => 100, - 'real_stock' => 10, - 'max_units_per_user' => 0, - ])->assertUnprocessable()->assertJsonValidationErrors('max_units_per_user'); - - $this->assertDatabaseMissing('catalog_items', ['slug' => 'invalid-purchase-limit']); - } - - private function createTenant(string $code = 'catalog-controller'): Tenant - { - $headerLogo = $this->createAttachment("{$code}-header"); - $footerLogo = $this->createAttachment("{$code}-footer"); - - return Tenant::query()->create([ - 'codigo' => $code, - 'nombre' => ucfirst($code), - 'dominio' => "{$code}.local", - 'primary_color' => '#000000', - 'secondary_color' => '#000000', - 'danger_color' => '#000000', - 'success_color' => '#000000', - 'header_bg_color' => '#000000', - 'footer_bg_color' => '#000000', - 'header_logo_id' => $headerLogo->id, - 'footer_logo_id' => $footerLogo->id, - ]); - } - - private function createAttachment(string $name): Attachment - { - return Attachment::query()->create([ - 'path' => "test/{$name}.png", - 'filename' => "{$name}.png", - 'type' => AttachmentType::Image, - 'mime_type' => 'image/png', - ]); - } -} diff --git a/tests/Feature/Catalog/CatalogItemDetailControllerTest.php b/tests/Feature/Catalog/CatalogItemDetailControllerTest.php index b466afb..1a8575c 100644 --- a/tests/Feature/Catalog/CatalogItemDetailControllerTest.php +++ b/tests/Feature/Catalog/CatalogItemDetailControllerTest.php @@ -2,18 +2,18 @@ namespace Tests\Feature\Catalog; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\User; -use App\Domains\Cart\Models\Cart; -use App\Domains\Catalog\Enums\InventoryPolicy; -use App\Domains\Catalog\Models\Attribute; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Inventory; -use App\Domains\Catalog\Models\ItemAttribute; -use App\Domains\Catalog\Models\Variant; -use App\Domains\Shared\Enums\FieldType; -use App\Domains\Tenant\Models\Tenant; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Commerce\Cart\Models\Cart; +use App\Domains\Commerce\Catalog\Enums\InventoryPolicy; +use App\Domains\Commerce\Catalog\Models\Attribute; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Inventory; +use App\Domains\Commerce\Catalog\Models\ItemAttribute; +use App\Domains\Commerce\Catalog\Models\Variant; +use App\Shared\Enums\FieldType; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Storage; use Tests\TestCase; diff --git a/tests/Feature/Catalog/CatalogSchemaTest.php b/tests/Feature/Catalog/CatalogSchemaTest.php index 94cb8a2..6e9d1c6 100644 --- a/tests/Feature/Catalog/CatalogSchemaTest.php +++ b/tests/Feature/Catalog/CatalogSchemaTest.php @@ -2,12 +2,12 @@ namespace Tests\Feature\Catalog; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Category; -use App\Domains\Catalog\Models\Inventory; -use App\Domains\Tenant\Models\Tenant; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Category; +use App\Domains\Commerce\Catalog\Models\Inventory; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Schema; use Tests\TestCase; diff --git a/tests/Feature/Catalog/CatalogSearchTest.php b/tests/Feature/Catalog/CatalogSearchTest.php index ba07c2f..446cc5a 100644 --- a/tests/Feature/Catalog/CatalogSearchTest.php +++ b/tests/Feature/Catalog/CatalogSearchTest.php @@ -2,13 +2,13 @@ namespace Tests\Feature\Catalog; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Catalog\Enums\GroupLayout; -use App\Domains\Catalog\Enums\ProductLayout; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Inventory; -use App\Domains\Tenant\Models\Tenant; +use App\Domains\Commerce\Catalog\Enums\GroupLayout; +use App\Domains\Commerce\Catalog\Enums\ProductLayout; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Inventory; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -32,15 +32,11 @@ class CatalogSearchTest extends TestCase { $tenant = $this->createTenant('search-config'); - $this->putJson("/api/tenants/{$tenant->codigo}", [ + $tenant->update([ 'search_product_layout' => ProductLayout::Row->value, 'search_group_layout' => GroupLayout::SimpleVertical->value, 'search_items_per_page' => 24, - ]) - ->assertOk() - ->assertJsonPath('data.search_product_layout', ProductLayout::Row->value) - ->assertJsonPath('data.search_group_layout', GroupLayout::SimpleVertical->value) - ->assertJsonPath('data.search_items_per_page', 24); + ]); $this->getJson('/api/tenants/bootstrap?'.http_build_query([ 'dominio' => $tenant->dominio, diff --git a/tests/Feature/Catalog/CatalogServiceTest.php b/tests/Feature/Catalog/CatalogServiceTest.php index 89a8b50..ae89767 100644 --- a/tests/Feature/Catalog/CatalogServiceTest.php +++ b/tests/Feature/Catalog/CatalogServiceTest.php @@ -2,16 +2,16 @@ namespace Tests\Feature\Catalog; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -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\Catalog\Models\Variant; -use App\Domains\Catalog\Services\CatalogService; -use App\Domains\Shared\Enums\FieldType; -use App\Domains\Tenant\Models\Tenant; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Commerce\Catalog\Enums\CatalogItemType; +use App\Domains\Commerce\Catalog\Models\Attribute; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Inventory; +use App\Domains\Commerce\Catalog\Models\Variant; +use App\Domains\Commerce\Catalog\Services\CatalogService; +use App\Shared\Enums\FieldType; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Validation\ValidationException; use Tests\TestCase; diff --git a/tests/Feature/Catalog/CategoryDetailTest.php b/tests/Feature/Catalog/CategoryDetailTest.php index 0b6ab2a..0eee7db 100644 --- a/tests/Feature/Catalog/CategoryDetailTest.php +++ b/tests/Feature/Catalog/CategoryDetailTest.php @@ -2,12 +2,12 @@ namespace Tests\Feature\Catalog; -use App\Domains\Catalog\Enums\GroupLayout; -use App\Domains\Catalog\Enums\ProductLayout; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Category; -use App\Domains\Catalog\Models\Inventory; -use App\Domains\Tenant\Models\Tenant; +use App\Domains\Commerce\Catalog\Enums\GroupLayout; +use App\Domains\Commerce\Catalog\Enums\ProductLayout; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Category; +use App\Domains\Commerce\Catalog\Models\Inventory; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; diff --git a/tests/Feature/Catalog/OnTicketFeaturedGroupControllerTest.php b/tests/Feature/Catalog/OnTicketFeaturedGroupControllerTest.php index 1c251b6..da4fbb7 100644 --- a/tests/Feature/Catalog/OnTicketFeaturedGroupControllerTest.php +++ b/tests/Feature/Catalog/OnTicketFeaturedGroupControllerTest.php @@ -2,15 +2,16 @@ namespace Tests\Feature\Catalog; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Catalog\Enums\FeaturedGroupSource; -use App\Domains\Catalog\Enums\GroupLayout; -use App\Domains\Catalog\Enums\ProductLayout; -use App\Domains\Catalog\Models\Category; -use App\Domains\Catalog\Models\FeaturedGroup; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Models\WebsiteType; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Commerce\Catalog\Enums\FeaturedGroupSource; +use App\Domains\Commerce\Catalog\Enums\GroupLayout; +use App\Domains\Commerce\Catalog\Enums\ProductLayout; +use App\Domains\Commerce\Catalog\Models\Category; +use App\Domains\Commerce\Catalog\Models\FeaturedGroup; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; +use App\Domains\Core\Tenant\Models\StorefrontWebsiteType; use Database\Seeders\AuthorizationSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Laravel\Sanctum\Sanctum; @@ -25,14 +26,15 @@ class OnTicketFeaturedGroupControllerTest extends TestCase parent::setUp(); $this->seed(AuthorizationSeeder::class); - WebsiteType::query()->create([ + StorefrontWebsiteType::query()->create([ 'codigo' => 'onticket', 'nombre' => 'OnTicket', ]); - WebsiteType::query()->create([ + StorefrontWebsiteType::query()->create([ 'codigo' => 'shopit', 'nombre' => 'Shopit', ]); + AdminWebsiteType::query()->create(['codigo' => 'shopit', 'nombre' => 'Shopit Admin']); } public function test_authentication_is_required(): void @@ -230,7 +232,8 @@ class OnTicketFeaturedGroupControllerTest extends TestCase 'codigo' => $code, 'nombre' => ucfirst($code), 'dominio' => "{$code}.test", - 'website_type_code' => $websiteType, + 'admin_website_type_code' => 'shopit', + 'storefront_website_type_code' => $websiteType, ]); } diff --git a/tests/Feature/Desfile/EntryControllerTest.php b/tests/Feature/Desfile/EntryControllerTest.php index 0ddb972..7484a2c 100644 --- a/tests/Feature/Desfile/EntryControllerTest.php +++ b/tests/Feature/Desfile/EntryControllerTest.php @@ -2,21 +2,21 @@ namespace Tests\Feature\Desfile; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Catalog\Enums\InventoryPolicy; -use App\Domains\Catalog\Enums\InventorySubject; -use App\Domains\Catalog\Models\Attribute; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Inventory; -use App\Domains\Catalog\Models\ItemAttribute; -use App\Domains\Catalog\Models\Variant; -use App\Domains\Menu\Models\Menu; -use App\Domains\Shared\Enums\FieldType; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Models\WebsiteType; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Commerce\Catalog\Enums\InventoryPolicy; +use App\Domains\Commerce\Catalog\Enums\InventorySubject; +use App\Domains\Commerce\Catalog\Models\Attribute; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Inventory; +use App\Domains\Commerce\Catalog\Models\ItemAttribute; +use App\Domains\Commerce\Catalog\Models\Variant; +use App\Domains\Core\Menu\Models\Menu; +use App\Shared\Enums\FieldType; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; use Database\Seeders\AuthorizationSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\UploadedFile; @@ -33,7 +33,7 @@ class EntryControllerTest extends TestCase parent::setUp(); $this->seed(AuthorizationSeeder::class); - WebsiteType::query()->create([ + AdminWebsiteType::query()->create([ 'codigo' => 'onticket', 'nombre' => 'OnTicket', ]); @@ -183,7 +183,7 @@ class EntryControllerTest extends TestCase 'codigo' => $tenantCode, 'nombre' => 'Desfile', 'dominio' => "{$tenantCode}.test", - 'website_type_code' => 'onticket', + 'admin_website_type_code' => 'onticket', ]); $menu = Menu::query()->firstOrCreate( ['code' => 'adminapp.desfile.entradas'], diff --git a/tests/Feature/Event/AdminAppEventControllerTest.php b/tests/Feature/Event/AdminAppEventControllerTest.php index 5c4f79a..8152f62 100644 --- a/tests/Feature/Event/AdminAppEventControllerTest.php +++ b/tests/Feature/Event/AdminAppEventControllerTest.php @@ -2,23 +2,23 @@ namespace Tests\Feature\Event; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Cart\Models\Cart; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Inventory; -use App\Domains\Catalog\Models\StockReservation; -use App\Domains\Catalog\Models\StockReservationLine; -use App\Domains\Catalog\Models\Variant; -use App\Domains\Event\Events\EventDateRescheduled; -use App\Domains\Event\Events\EventDateSuspended; -use App\Domains\Purchase\Services\Checkout\CatalogSelectionResolver; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Models\WebsiteType; -use App\Domains\Ticket\Enums\ValidityTimeType; -use App\Domains\Ticket\Models\Ticket; +use App\Domains\Commerce\Cart\Models\Cart; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Inventory; +use App\Domains\Commerce\Catalog\Models\StockReservation; +use App\Domains\Commerce\Catalog\Models\StockReservationLine; +use App\Domains\Commerce\Catalog\Models\Variant; +use App\Domains\Commerce\Purchase\Services\Checkout\CatalogSelectionResolver; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Ticketing\Event\Events\EventDateRescheduled; +use App\Domains\Ticketing\Event\Events\EventDateSuspended; +use App\Domains\Ticketing\Ticket\Enums\ValidityTimeType; +use App\Domains\Ticketing\Ticket\Models\Ticket; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; use Database\Seeders\AuthorizationSeeder; use Database\Seeders\SocialMediaSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -38,7 +38,7 @@ class AdminAppEventControllerTest extends TestCase parent::setUp(); $this->seed([AuthorizationSeeder::class, SocialMediaSeeder::class]); - WebsiteType::query()->create([ + AdminWebsiteType::query()->create([ 'codigo' => 'onticket', 'nombre' => 'OnTicket', ]); @@ -249,8 +249,8 @@ class AdminAppEventControllerTest extends TestCase $this->assertDatabaseHas('validity_times', [ 'id' => $firstValidityTimeId, 'type' => ValidityTimeType::FixedWindow->value, - 'fixed_starts_at' => '2026-10-01 08:00:00', - 'fixed_expires_at' => '2026-10-01 12:00:00', + 'fixed_starts_at' => '2026-10-01 11:00:00', + 'fixed_expires_at' => '2026-10-01 15:00:00', ]); $this->assertDatabaseHas('event_dates', ['id' => $removedDate->id]); $this->assertSame('1 y 2 de Octubre 2026', $tenant->fresh()->event_date_text); @@ -385,7 +385,7 @@ class AdminAppEventControllerTest extends TestCase ); $this->assertDatabaseHas('validity_times', [ 'id' => $original->validity_time_id, - 'fixed_starts_at' => '2027-10-09 09:00:00', + 'fixed_starts_at' => '2027-10-09 12:00:00', ]); $this->postJson("/api/v1/adminapp/tenant/event-dates/{$destination->id}/reschedule", [ @@ -761,7 +761,7 @@ class AdminAppEventControllerTest extends TestCase 'codigo' => $code, 'nombre' => ucfirst($code), 'dominio' => "{$code}.test", - 'website_type_code' => 'onticket', + 'admin_website_type_code' => 'onticket', 'primary_color' => '#000000', 'secondary_color' => '#000000', 'danger_color' => '#000000', diff --git a/tests/Feature/Event/EventDateNoticeControllerTest.php b/tests/Feature/Event/EventDateNoticeControllerTest.php index 77e3f38..2d9412e 100644 --- a/tests/Feature/Event/EventDateNoticeControllerTest.php +++ b/tests/Feature/Event/EventDateNoticeControllerTest.php @@ -2,11 +2,11 @@ namespace Tests\Feature\Event; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Event\Enums\EventDateChangeType; -use App\Domains\Event\Models\EventDateChange; -use App\Domains\Tenant\Models\Tenant; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Ticketing\Event\Enums\EventDateChangeType; +use App\Domains\Ticketing\Event\Models\EventDateChange; +use App\Domains\Core\Tenant\Models\Tenant; use Database\Seeders\AuthorizationSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Laravel\Sanctum\Sanctum; diff --git a/tests/Feature/Event/PublicEventControllerTest.php b/tests/Feature/Event/PublicEventControllerTest.php new file mode 100644 index 0000000..0511204 --- /dev/null +++ b/tests/Feature/Event/PublicEventControllerTest.php @@ -0,0 +1,88 @@ +seed(WebsiteTypeSeeder::class); + $firstTenant = $this->createTenant('first'); + $secondTenant = $this->createTenant('second'); + + Event::query()->create([ + 'tenant_code' => $firstTenant->codigo, + 'slug' => 'gran-fiesta', + 'title' => 'Fiesta del primer tenant', + 'published_at' => now()->subMinute(), + ]); + Event::query()->create([ + 'tenant_code' => $secondTenant->codigo, + 'slug' => 'gran-fiesta', + 'title' => 'Fiesta del segundo tenant', + 'published_at' => now()->subMinute(), + ]); + + $this->getJson('/api/tenants/second/events/gran-fiesta') + ->assertOk() + ->assertJsonPath('data.slug', 'gran-fiesta') + ->assertJsonPath('data.title', 'Fiesta del segundo tenant'); + } + + public function test_it_does_not_resolve_the_old_numeric_event_url(): void + { + $this->seed(WebsiteTypeSeeder::class); + $tenant = $this->createTenant('acme'); + $event = Event::query()->create([ + 'tenant_code' => $tenant->codigo, + 'slug' => 'gran-fiesta', + 'title' => 'Gran Fiesta', + 'published_at' => now()->subMinute(), + ]); + + $this->getJson("/api/tenants/acme/events/{$event->id}")->assertNotFound(); + } + + private function createTenant(string $code): Tenant + { + $header = $this->createAttachment($code.'-header'); + $footer = $this->createAttachment($code.'-footer'); + + return Tenant::query()->create([ + 'codigo' => $code, + 'nombre' => ucfirst($code), + 'dominio' => $code.'.test', + 'storefront_website_type_code' => 'onticket_multi_event', + 'primary_color' => '#000000', + 'secondary_color' => '#000000', + 'danger_color' => '#000000', + 'success_color' => '#000000', + 'header_bg_color' => '#000000', + 'footer_bg_color' => '#000000', + 'header_logo_id' => $header->id, + 'footer_logo_id' => $footer->id, + ]); + } + + private function createAttachment(string $name): Attachment + { + return Attachment::query()->create([ + 'key' => (string) Str::uuid(), + 'path' => 'test/'.$name.'.png', + 'filename' => $name.'.png', + 'type' => AttachmentType::Image, + 'mime_type' => 'image/png', + ]); + } +} diff --git a/tests/Feature/FiestaFutbolInfantil/AccommodationControllerTest.php b/tests/Feature/FiestaFutbolInfantil/AccommodationControllerTest.php index 9a4e755..d04907f 100644 --- a/tests/Feature/FiestaFutbolInfantil/AccommodationControllerTest.php +++ b/tests/Feature/FiestaFutbolInfantil/AccommodationControllerTest.php @@ -2,17 +2,17 @@ namespace Tests\Feature\FiestaFutbolInfantil; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Catalog\Models\Attribute; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Variant; -use App\Domains\Menu\Models\Menu; -use App\Domains\Shared\Enums\FieldType; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Models\WebsiteType; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Commerce\Catalog\Models\Attribute; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Variant; +use App\Domains\Core\Menu\Models\Menu; +use App\Shared\Enums\FieldType; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; use Database\Seeders\AuthorizationSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Laravel\Sanctum\Sanctum; @@ -27,7 +27,7 @@ class AccommodationControllerTest extends TestCase parent::setUp(); $this->seed(AuthorizationSeeder::class); - WebsiteType::query()->create([ + AdminWebsiteType::query()->create([ 'codigo' => 'onticket', 'nombre' => 'OnTicket', ]); @@ -222,7 +222,7 @@ class AccommodationControllerTest extends TestCase 'footer_bg_color' => '#111111', 'header_logo_id' => $headerLogo->id, 'footer_logo_id' => $footerLogo->id, - 'website_type_code' => 'onticket', + 'admin_website_type_code' => 'onticket', ]); $attribute = Attribute::query()->create([ 'tenant_codigo' => $tenant->codigo, diff --git a/tests/Feature/FiestaFutbolInfantil/EntryControllerTest.php b/tests/Feature/FiestaFutbolInfantil/EntryControllerTest.php index 0a1249b..fb3b59f 100644 --- a/tests/Feature/FiestaFutbolInfantil/EntryControllerTest.php +++ b/tests/Feature/FiestaFutbolInfantil/EntryControllerTest.php @@ -2,17 +2,17 @@ namespace Tests\Feature\FiestaFutbolInfantil; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Catalog\Models\Attribute; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Category; -use App\Domains\Catalog\Models\Inventory; -use App\Domains\Catalog\Models\Variant; -use App\Domains\Menu\Models\Menu; -use App\Domains\Shared\Enums\FieldType; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Models\WebsiteType; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Commerce\Catalog\Models\Attribute; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Category; +use App\Domains\Commerce\Catalog\Models\Inventory; +use App\Domains\Commerce\Catalog\Models\Variant; +use App\Domains\Core\Menu\Models\Menu; +use App\Shared\Enums\FieldType; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; use Database\Seeders\AuthorizationSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Laravel\Sanctum\Sanctum; @@ -27,7 +27,7 @@ class EntryControllerTest extends TestCase parent::setUp(); $this->seed(AuthorizationSeeder::class); - WebsiteType::query()->create([ + AdminWebsiteType::query()->create([ 'codigo' => 'onticket', 'nombre' => 'OnTicket', ]); @@ -420,7 +420,7 @@ class EntryControllerTest extends TestCase 'codigo' => $code, 'nombre' => ucfirst($code), 'dominio' => "{$code}.test", - 'website_type_code' => 'onticket', + 'admin_website_type_code' => 'onticket', ]); } diff --git a/tests/Feature/FiestaFutbolInfantil/FoodControllerTest.php b/tests/Feature/FiestaFutbolInfantil/FoodControllerTest.php index 3bda7f1..bef7436 100644 --- a/tests/Feature/FiestaFutbolInfantil/FoodControllerTest.php +++ b/tests/Feature/FiestaFutbolInfantil/FoodControllerTest.php @@ -2,18 +2,18 @@ namespace Tests\Feature\FiestaFutbolInfantil; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Catalog\Models\Attribute; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Category; -use App\Domains\Catalog\Models\Inventory; -use App\Domains\Catalog\Models\Variant; -use App\Domains\Event\Services\EventService; -use App\Domains\Menu\Models\Menu; -use App\Domains\Shared\Enums\FieldType; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Models\WebsiteType; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Commerce\Catalog\Models\Attribute; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Category; +use App\Domains\Commerce\Catalog\Models\Inventory; +use App\Domains\Commerce\Catalog\Models\Variant; +use App\Domains\Ticketing\Event\Services\EventService; +use App\Domains\Core\Menu\Models\Menu; +use App\Shared\Enums\FieldType; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; use Database\Seeders\AuthorizationSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Carbon; @@ -29,7 +29,7 @@ class FoodControllerTest extends TestCase parent::setUp(); $this->seed(AuthorizationSeeder::class); - WebsiteType::query()->create([ + AdminWebsiteType::query()->create([ 'codigo' => 'onticket', 'nombre' => 'OnTicket', ]); @@ -323,7 +323,7 @@ class FoodControllerTest extends TestCase 'codigo' => 'fiesta_futbol_infantil', 'nombre' => 'Fiesta Fútbol Infantil', 'dominio' => 'fiesta.test', - 'website_type_code' => 'onticket', + 'admin_website_type_code' => 'onticket', ]); $eventDateAttribute = Attribute::query()->create([ 'tenant_codigo' => $tenant->codigo, diff --git a/tests/Feature/FiestaFutbolInfantil/MerchandiseControllerTest.php b/tests/Feature/FiestaFutbolInfantil/MerchandiseControllerTest.php index 7af9f72..09752a4 100644 --- a/tests/Feature/FiestaFutbolInfantil/MerchandiseControllerTest.php +++ b/tests/Feature/FiestaFutbolInfantil/MerchandiseControllerTest.php @@ -2,16 +2,16 @@ namespace Tests\Feature\FiestaFutbolInfantil; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Catalog\Models\Attribute; -use App\Domains\Catalog\Models\Variant; -use App\Domains\Menu\Models\Menu; -use App\Domains\Shared\Enums\FieldType; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Models\WebsiteType; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Commerce\Catalog\Models\Attribute; +use App\Domains\Commerce\Catalog\Models\Variant; +use App\Domains\Core\Menu\Models\Menu; +use App\Shared\Enums\FieldType; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; use Database\Seeders\AuthorizationSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Laravel\Sanctum\Sanctum; @@ -26,7 +26,7 @@ class MerchandiseControllerTest extends TestCase parent::setUp(); $this->seed(AuthorizationSeeder::class); - WebsiteType::query()->create([ + AdminWebsiteType::query()->create([ 'codigo' => 'onticket', 'nombre' => 'OnTicket', ]); @@ -262,7 +262,7 @@ class MerchandiseControllerTest extends TestCase 'footer_bg_color' => '#111111', 'header_logo_id' => $headerLogo->id, 'footer_logo_id' => $footerLogo->id, - 'website_type_code' => 'onticket', + 'admin_website_type_code' => 'onticket', ]); $colorAttribute = Attribute::query()->create([ 'tenant_codigo' => $tenant->codigo, diff --git a/tests/Feature/Forms/AdminAppEntryFormControllerTest.php b/tests/Feature/Forms/AdminAppEntryFormControllerTest.php index 6a0722d..79150d4 100644 --- a/tests/Feature/Forms/AdminAppEntryFormControllerTest.php +++ b/tests/Feature/Forms/AdminAppEntryFormControllerTest.php @@ -2,11 +2,11 @@ namespace Tests\Feature\Forms; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Event\Models\EventDate; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Models\WebsiteType; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Ticketing\Event\Models\EventDate; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; use Database\Seeders\AuthorizationSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Laravel\Sanctum\Sanctum; @@ -21,7 +21,7 @@ class AdminAppEntryFormControllerTest extends TestCase parent::setUp(); $this->seed(AuthorizationSeeder::class); - WebsiteType::query()->create(['codigo' => 'onticket', 'nombre' => 'OnTicket']); + AdminWebsiteType::query()->create(['codigo' => 'onticket', 'nombre' => 'OnTicket']); } public function test_authentication_is_required(): void @@ -62,7 +62,7 @@ class AdminAppEntryFormControllerTest extends TestCase 'codigo' => $code, 'nombre' => ucfirst($code), 'dominio' => "{$code}.test", - 'website_type_code' => 'onticket', + 'admin_website_type_code' => 'onticket', ]); } diff --git a/tests/Feature/Forms/AdminAppEventFormControllerTest.php b/tests/Feature/Forms/AdminAppEventFormControllerTest.php index f482f24..7897afe 100644 --- a/tests/Feature/Forms/AdminAppEventFormControllerTest.php +++ b/tests/Feature/Forms/AdminAppEventFormControllerTest.php @@ -2,10 +2,10 @@ namespace Tests\Feature\Forms; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Models\WebsiteType; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; use Database\Seeders\AuthorizationSeeder; use Database\Seeders\SocialMediaSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -21,7 +21,7 @@ class AdminAppEventFormControllerTest extends TestCase parent::setUp(); $this->seed([AuthorizationSeeder::class, SocialMediaSeeder::class]); - WebsiteType::query()->create([ + AdminWebsiteType::query()->create([ 'codigo' => 'onticket', 'nombre' => 'OnTicket', ]); @@ -38,7 +38,7 @@ class AdminAppEventFormControllerTest extends TestCase 'codigo' => 'acme', 'nombre' => 'Acme', 'dominio' => 'acme.test', - 'website_type_code' => 'onticket', + 'admin_website_type_code' => 'onticket', ]); $tenant->socialMedia()->attach('instagram', [ 'url' => 'https://instagram.com/acme', diff --git a/tests/Feature/Forms/AdminAppFoodFormControllerTest.php b/tests/Feature/Forms/AdminAppFoodFormControllerTest.php index 50f01e1..fede47d 100644 --- a/tests/Feature/Forms/AdminAppFoodFormControllerTest.php +++ b/tests/Feature/Forms/AdminAppFoodFormControllerTest.php @@ -2,12 +2,12 @@ namespace Tests\Feature\Forms; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Catalog\Models\Attribute; -use App\Domains\Shared\Enums\FieldType; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Models\WebsiteType; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Commerce\Catalog\Models\Attribute; +use App\Shared\Enums\FieldType; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; use Database\Seeders\AuthorizationSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Laravel\Sanctum\Sanctum; @@ -21,7 +21,7 @@ class AdminAppFoodFormControllerTest extends TestCase { parent::setUp(); $this->seed(AuthorizationSeeder::class); - WebsiteType::query()->create(['codigo' => 'onticket', 'nombre' => 'OnTicket']); + AdminWebsiteType::query()->create(['codigo' => 'onticket', 'nombre' => 'OnTicket']); } public function test_authentication_is_required(): void @@ -36,7 +36,7 @@ class AdminAppFoodFormControllerTest extends TestCase 'codigo' => 'fiesta', 'nombre' => 'Fiesta', 'dominio' => 'fiesta.test', - 'website_type_code' => 'onticket', + 'admin_website_type_code' => 'onticket', ]); $eventDate = $tenant->eventDates()->create([ 'date' => '2026-10-09', @@ -73,7 +73,7 @@ class AdminAppFoodFormControllerTest extends TestCase 'codigo' => 'fiesta', 'nombre' => 'Fiesta', 'dominio' => 'fiesta.test', - 'website_type_code' => 'onticket', + 'admin_website_type_code' => 'onticket', ]); $available = $tenant->eventDates()->create([ 'date' => '2026-10-09', diff --git a/tests/Feature/Forms/AdminAppMerchandiseFormControllerTest.php b/tests/Feature/Forms/AdminAppMerchandiseFormControllerTest.php index cc37123..846fe64 100644 --- a/tests/Feature/Forms/AdminAppMerchandiseFormControllerTest.php +++ b/tests/Feature/Forms/AdminAppMerchandiseFormControllerTest.php @@ -2,12 +2,12 @@ namespace Tests\Feature\Forms; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Catalog\Models\Attribute; -use App\Domains\Shared\Enums\FieldType; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Models\WebsiteType; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Commerce\Catalog\Models\Attribute; +use App\Shared\Enums\FieldType; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; use Database\Seeders\AuthorizationSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Laravel\Sanctum\Sanctum; @@ -22,7 +22,7 @@ class AdminAppMerchandiseFormControllerTest extends TestCase parent::setUp(); $this->seed(AuthorizationSeeder::class); - WebsiteType::query()->create([ + AdminWebsiteType::query()->create([ 'codigo' => 'onticket', 'nombre' => 'OnTicket', ]); @@ -83,7 +83,7 @@ class AdminAppMerchandiseFormControllerTest extends TestCase 'codigo' => $code, 'nombre' => ucfirst($code), 'dominio' => "{$code}.test", - 'website_type_code' => 'onticket', + 'admin_website_type_code' => 'onticket', ]); } diff --git a/tests/Feature/Forms/AdminAppSaleFormControllerTest.php b/tests/Feature/Forms/AdminAppSaleFormControllerTest.php index 4b90bcb..3b5ec6d 100644 --- a/tests/Feature/Forms/AdminAppSaleFormControllerTest.php +++ b/tests/Feature/Forms/AdminAppSaleFormControllerTest.php @@ -2,13 +2,13 @@ namespace Tests\Feature\Forms; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Purchase\Models\Purchase; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Models\WebsiteType; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Commerce\Purchase\Models\Purchase; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; use Database\Seeders\AuthorizationSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Laravel\Sanctum\Sanctum; @@ -23,7 +23,7 @@ class AdminAppSaleFormControllerTest extends TestCase parent::setUp(); $this->seed(AuthorizationSeeder::class); - WebsiteType::query()->create([ + AdminWebsiteType::query()->create([ 'codigo' => 'onticket', 'nombre' => 'OnTicket', ]); @@ -81,7 +81,7 @@ class AdminAppSaleFormControllerTest extends TestCase 'footer_bg_color' => '#000000', 'header_logo_id' => $headerLogo->id, 'footer_logo_id' => $footerLogo->id, - 'website_type_code' => 'onticket', + 'admin_website_type_code' => 'onticket', ]); } diff --git a/tests/Feature/Forms/AdminAppStaffFormControllerTest.php b/tests/Feature/Forms/AdminAppStaffFormControllerTest.php index 4b2c033..a3b5ea7 100644 --- a/tests/Feature/Forms/AdminAppStaffFormControllerTest.php +++ b/tests/Feature/Forms/AdminAppStaffFormControllerTest.php @@ -2,11 +2,11 @@ namespace Tests\Feature\Forms; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Catalog\Models\Category; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Models\WebsiteType; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Commerce\Catalog\Models\Category; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; use Database\Seeders\AuthorizationSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Laravel\Sanctum\Sanctum; @@ -21,7 +21,7 @@ class AdminAppStaffFormControllerTest extends TestCase parent::setUp(); $this->seed(AuthorizationSeeder::class); - WebsiteType::query()->create([ + AdminWebsiteType::query()->create([ 'codigo' => 'onticket', 'nombre' => 'OnTicket', ]); @@ -78,7 +78,7 @@ class AdminAppStaffFormControllerTest extends TestCase 'codigo' => $code, 'nombre' => ucfirst($code), 'dominio' => "{$code}.test", - 'website_type_code' => 'onticket', + 'admin_website_type_code' => 'onticket', ]); } } diff --git a/tests/Feature/Forms/AdminAppTicketFilterFormControllerTest.php b/tests/Feature/Forms/AdminAppTicketFilterFormControllerTest.php index 3174d77..b6d873b 100644 --- a/tests/Feature/Forms/AdminAppTicketFilterFormControllerTest.php +++ b/tests/Feature/Forms/AdminAppTicketFilterFormControllerTest.php @@ -2,15 +2,15 @@ namespace Tests\Feature\Forms; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Services\CatalogService; -use App\Domains\Menu\Models\Menu; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Ticket\Models\Ticket; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Services\CatalogService; +use App\Domains\Core\Menu\Models\Menu; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Ticketing\Ticket\Models\Ticket; use Database\Seeders\AttributeSeeder; use Database\Seeders\AuthorizationSeeder; use Database\Seeders\FiestaFutbolInfantilProductSeeder; diff --git a/tests/Feature/Forms/AdminAppTicketFormControllerTest.php b/tests/Feature/Forms/AdminAppTicketFormControllerTest.php index 78c97d7..381f5c3 100644 --- a/tests/Feature/Forms/AdminAppTicketFormControllerTest.php +++ b/tests/Feature/Forms/AdminAppTicketFormControllerTest.php @@ -2,11 +2,11 @@ namespace Tests\Feature\Forms; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Tenant\Models\Tenant; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Core\Tenant\Models\Tenant; use Database\Seeders\AttributeSeeder; use Database\Seeders\AuthorizationSeeder; use Database\Seeders\FiestaFutbolInfantilProductSeeder; diff --git a/tests/Feature/Integration/ClientIntegrationControllerTest.php b/tests/Feature/Integration/ClientIntegrationControllerTest.php index ab808e1..ce4dce9 100644 --- a/tests/Feature/Integration/ClientIntegrationControllerTest.php +++ b/tests/Feature/Integration/ClientIntegrationControllerTest.php @@ -2,11 +2,11 @@ namespace Tests\Feature\Integration; -use App\Domains\Auth\Models\User; -use App\Domains\Client\Models\Client; -use App\Domains\Integration\Models\ClientIntegration; -use App\Domains\Integration\Models\Integration; -use App\Domains\Integration\Services\ClientIntegrationService; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Client\Models\Client; +use App\Shared\Integration\Models\ClientIntegration; +use App\Shared\Integration\Models\Integration; +use App\Shared\Integration\Services\ClientIntegrationService; use Illuminate\Foundation\Testing\RefreshDatabase; use Laravel\Sanctum\Sanctum; use Mockery; diff --git a/tests/Feature/Integration/IntegrationInstanceTest.php b/tests/Feature/Integration/IntegrationInstanceTest.php index dbb7026..40ebb57 100644 --- a/tests/Feature/Integration/IntegrationInstanceTest.php +++ b/tests/Feature/Integration/IntegrationInstanceTest.php @@ -2,19 +2,20 @@ namespace Tests\Feature\Integration; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\User; -use App\Domains\Client\Models\Client; -use App\Domains\Integration\Models\Integration; -use App\Domains\Integration\Models\IntegrationInstance; -use App\Domains\Integration\Services\BaseIntegrationService; -use App\Domains\Integration\Services\ClientIntegrationService; -use App\Domains\Integration\Services\IntegrationAssociationService; -use App\Domains\Integration\Services\IntegrationInstanceService; -use App\Domains\Integration\Services\TelepagosIntegrationService; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Models\WebsiteType; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Client\Models\Client; +use App\Shared\Integration\Models\Integration; +use App\Shared\Integration\Models\IntegrationInstance; +use App\Shared\Integration\Services\BaseIntegrationService; +use App\Shared\Integration\Services\ClientIntegrationService; +use App\Shared\Integration\Services\IntegrationAssociationService; +use App\Shared\Integration\Services\IntegrationInstanceService; +use App\Shared\Integration\Services\TelepagosIntegrationService; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; +use App\Domains\Core\Tenant\Models\StorefrontWebsiteType; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; @@ -40,19 +41,19 @@ class IntegrationInstanceTest extends TestCase public function test_management_requires_an_authenticated_global_admin(): void { Client::create(['code' => 'acme', 'name' => 'Acme']); - WebsiteType::create(['codigo' => 'demo', 'nombre' => 'Demo']); + AdminWebsiteType::create(['codigo' => 'demo', 'nombre' => 'Demo']); $this->getJson('/api/clients/acme/integrations')->assertUnauthorized(); Sanctum::actingAs(User::factory()->create()); $this->getJson('/api/clients/acme/integrations')->assertForbidden(); - $this->getJson('/api/website-types/demo/integrations')->assertForbidden(); + $this->getJson('/api/admin-website-types/demo/integrations')->assertForbidden(); } public function test_only_owner_endpoints_can_manage_configuration(): void { $this->admin(); $client = Client::create(['code' => 'acme', 'name' => 'Acme']); - $type = WebsiteType::create(['codigo' => 'demo', 'nombre' => 'Demo']); + $type = AdminWebsiteType::create(['codigo' => 'demo', 'nombre' => 'Demo']); $this->getJson('/api/integrations')->assertNotFound(); $this->getJson('/api/integration-instances')->assertNotFound(); @@ -60,7 +61,7 @@ class IntegrationInstanceTest extends TestCase $this->putJson('/api/clients/acme/integrations/test', ['integration_data' => []]) ->assertUnprocessable()->assertJsonValidationErrors('integration_data.api_key'); - $this->putJson('/api/website-types/demo/integrations/test', ['integration_data' => []]) + $this->putJson('/api/admin-website-types/demo/integrations/test', ['integration_data' => []]) ->assertUnprocessable()->assertJsonValidationErrors('integration_data.api_key'); $this->putJson('/api/clients/acme/integrations/test', [ @@ -69,22 +70,22 @@ class IntegrationInstanceTest extends TestCase $clientInstanceId = $client->integrations()->firstOrFail()->integration_instance_id; self::assertSame('client-secret', IntegrationInstance::findOrFail($clientInstanceId)->integration_data['api_key']); - $this->putJson('/api/website-types/demo/integrations/test', [ + $this->putJson('/api/admin-website-types/demo/integrations/test', [ 'integration_data' => ['api_key' => 'type-secret'], ])->assertCreated() ->assertJsonPath('data.integration_instance.name', 'Test / Demo') ->assertJsonMissingPath('data.integration_instance.integration_data'); $typeInstanceId = $type->integrations()->firstOrFail()->integration_instance_id; - $this->getJson('/api/website-types/demo/integrations/test') + $this->getJson('/api/admin-website-types/demo/integrations/test') ->assertOk()->assertJsonPath('data.integration_instance_id', $typeInstanceId); $this->deleteJson('/api/clients/acme/integrations/test')->assertNoContent(); - $this->deleteJson('/api/website-types/demo/integrations/test')->assertNoContent(); + $this->deleteJson('/api/admin-website-types/demo/integrations/test')->assertNoContent(); } public function test_tenant_prefers_client_then_type_and_client_context_does_not_inherit(): void { - $type = WebsiteType::create(['codigo' => 'demo', 'nombre' => 'Demo']); + $type = AdminWebsiteType::create(['codigo' => 'demo', 'nombre' => 'Demo']); $tenant = $this->tenantForType($type); $associations = new IntegrationAssociationService; $associations->associate($type, 'test', $this->makeInstance('type')); @@ -105,8 +106,10 @@ class IntegrationInstanceTest extends TestCase public function test_required_configuration_can_come_from_the_website_type(): void { $this->integration->update(['requires_configuration' => true]); - $type = WebsiteType::create(['codigo' => 'demo', 'nombre' => 'Demo']); + $type = AdminWebsiteType::create(['codigo' => 'demo', 'nombre' => 'Demo']); $tenant = $this->tenantForType($type); + $storefront = StorefrontWebsiteType::create(['codigo' => 'different-storefront', 'nombre' => 'Storefront']); + $tenant->update(['storefront_website_type_code' => $storefront->codigo]); (new IntegrationAssociationService)->associate($type, 'test', $this->makeInstance('type')); self::assertSame('type', $this->probe()->forTenant($tenant->codigo)->setting()); } @@ -142,7 +145,7 @@ class IntegrationInstanceTest extends TestCase public function test_an_instance_is_deleted_only_after_its_last_association_is_removed(): void { $client = Client::create(['code' => 'acme', 'name' => 'Acme']); - $type = WebsiteType::create(['codigo' => 'demo', 'nombre' => 'Demo']); + $type = AdminWebsiteType::create(['codigo' => 'demo', 'nombre' => 'Demo']); $shared = $this->makeInstance('shared'); $associations = new IntegrationAssociationService; @@ -180,7 +183,7 @@ class IntegrationInstanceTest extends TestCase Http::assertSentCount(2); } - private function tenantForType(WebsiteType $type): Tenant + private function tenantForType(AdminWebsiteType $type): Tenant { $logo = Attachment::create([ 'path' => 'tenants/logo.png', 'filename' => 'logo.png', @@ -190,7 +193,7 @@ class IntegrationInstanceTest extends TestCase return Tenant::create([ 'codigo' => 'acme', 'nombre' => 'Acme', 'dominio' => 'acme.test', - 'website_type_code' => $type->codigo, + 'admin_website_type_code' => $type->codigo, 'primary_color' => '#112233', 'secondary_color' => '#445566', 'danger_color' => '#ff0000', 'success_color' => '#00ff00', 'header_bg_color' => '#112233', 'footer_bg_color' => '#112233', diff --git a/tests/Feature/Integration/IntegrationServiceTest.php b/tests/Feature/Integration/IntegrationServiceTest.php index 847a4e6..dc5d886 100644 --- a/tests/Feature/Integration/IntegrationServiceTest.php +++ b/tests/Feature/Integration/IntegrationServiceTest.php @@ -2,11 +2,11 @@ namespace Tests\Feature\Integration; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Integration\Models\Integration; -use App\Domains\Integration\Services\TelepagosIntegrationService; -use App\Domains\Tenant\Models\Tenant; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Shared\Integration\Models\Integration; +use App\Shared\Integration\Services\TelepagosIntegrationService; +use App\Domains\Core\Tenant\Models\Tenant; use Exception; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\Client\PendingRequest; diff --git a/tests/Feature/Integration/MailServiceTest.php b/tests/Feature/Integration/MailServiceTest.php index 6e7ffeb..425ba52 100644 --- a/tests/Feature/Integration/MailServiceTest.php +++ b/tests/Feature/Integration/MailServiceTest.php @@ -2,15 +2,15 @@ namespace Tests\Feature\Integration; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Integration\Models\Integration; -use App\Domains\Integration\Models\IntegrationInstance; -use App\Domains\Integration\Services\ClientIntegrationService; -use App\Domains\Integration\Services\IntegrationAssociationService; -use App\Domains\Integration\Services\MailService; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Models\WebsiteType; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Shared\Integration\Models\Integration; +use App\Shared\Integration\Models\IntegrationInstance; +use App\Shared\Integration\Services\ClientIntegrationService; +use App\Shared\Integration\Services\IntegrationAssociationService; +use App\Shared\Integration\Services\MailService; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailer; @@ -127,8 +127,8 @@ class MailServiceTest extends TestCase Mail::fake(); $tenant = $this->createTenant(); $this->createEmailIntegration(); - $type = WebsiteType::create(['codigo' => 'mail-brand', 'nombre' => 'Mail Brand']); - $tenant->update(['website_type_code' => $type->codigo]); + $type = AdminWebsiteType::create(['codigo' => 'mail-brand', 'nombre' => 'Mail Brand']); + $tenant->update(['admin_website_type_code' => $type->codigo]); $instance = IntegrationInstance::create([ 'integration_code' => 'email', 'name' => 'Brand SMTP', 'integration_data' => $this->emailData(), ]); diff --git a/tests/Feature/Integration/TelepagosWebhookTest.php b/tests/Feature/Integration/TelepagosWebhookTest.php index 0edfed9..17518dc 100644 --- a/tests/Feature/Integration/TelepagosWebhookTest.php +++ b/tests/Feature/Integration/TelepagosWebhookTest.php @@ -2,20 +2,20 @@ namespace Tests\Feature\Integration; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\User; -use App\Domains\Cart\Models\Cart; -use App\Domains\Catalog\Enums\InventoryPolicy; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Category; -use App\Domains\Catalog\Models\Inventory; -use App\Domains\Catalog\Models\Variant; -use App\Domains\Integration\Models\Integration; -use App\Domains\Purchase\Models\Purchase; -use App\Domains\Purchase\Models\TelepagosPayment; -use App\Domains\Purchase\Services\CheckoutService; -use App\Domains\Tenant\Models\Tenant; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Commerce\Cart\Models\Cart; +use App\Domains\Commerce\Catalog\Enums\InventoryPolicy; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Category; +use App\Domains\Commerce\Catalog\Models\Inventory; +use App\Domains\Commerce\Catalog\Models\Variant; +use App\Shared\Integration\Models\Integration; +use App\Domains\Commerce\Purchase\Models\Purchase; +use App\Domains\Commerce\Purchase\Models\TelepagosPayment; +use App\Domains\Commerce\Purchase\Services\CheckoutService; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; diff --git a/tests/Feature/Logging/LogsValueChangesTest.php b/tests/Feature/Logging/LogsValueChangesTest.php index 11d6d64..b3edd2d 100644 --- a/tests/Feature/Logging/LogsValueChangesTest.php +++ b/tests/Feature/Logging/LogsValueChangesTest.php @@ -2,11 +2,11 @@ namespace Tests\Feature\Logging; -use App\Domains\Logging\Enums\ValueChangeActorType; -use App\Domains\Logging\Models\Concerns\LogsValueChanges; -use App\Domains\Logging\Models\ValueChange; -use App\Domains\Purchase\Models\Purchase; -use App\Domains\Ticket\Models\Ticket; +use App\Shared\Logging\Enums\ValueChangeActorType; +use App\Shared\Logging\Models\Concerns\LogsValueChanges; +use App\Shared\Logging\Models\ValueChange; +use App\Domains\Commerce\Purchase\Models\Purchase; +use App\Domains\Ticketing\Ticket\Models\Ticket; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Schema\Blueprint; diff --git a/tests/Feature/MailTest/MailTestControllerTest.php b/tests/Feature/MailTest/MailTestControllerTest.php index f30d565..dd338ab 100644 --- a/tests/Feature/MailTest/MailTestControllerTest.php +++ b/tests/Feature/MailTest/MailTestControllerTest.php @@ -2,11 +2,11 @@ namespace Tests\Feature\MailTest; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Integration\Models\Integration; -use App\Domains\MailTest\Mailables\TestMail; -use App\Domains\Tenant\Models\Tenant; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Shared\Integration\Models\Integration; +use App\Shared\MailTest\Mailables\TestMail; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Mail\Mailable; use Illuminate\Support\Facades\Mail; diff --git a/tests/Feature/Menu/TenantMenuControllerTest.php b/tests/Feature/Menu/TenantMenuControllerTest.php index a5e811b..a322bfb 100644 --- a/tests/Feature/Menu/TenantMenuControllerTest.php +++ b/tests/Feature/Menu/TenantMenuControllerTest.php @@ -2,10 +2,10 @@ namespace Tests\Feature\Menu; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Menu\Models\Menu; -use App\Domains\Tenant\Models\Tenant; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Menu\Models\Menu; +use App\Domains\Core\Tenant\Models\Tenant; use Database\Seeders\MenuSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; diff --git a/tests/Feature/Migrations/AddCheckoutEditingPolicyToTenantsTest.php b/tests/Feature/Migrations/AddCheckoutEditingPolicyToTenantsTest.php index fcce1c1..502ecee 100644 --- a/tests/Feature/Migrations/AddCheckoutEditingPolicyToTenantsTest.php +++ b/tests/Feature/Migrations/AddCheckoutEditingPolicyToTenantsTest.php @@ -2,7 +2,7 @@ namespace Tests\Feature\Migrations; -use App\Domains\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; diff --git a/tests/Feature/Migrations/AddOnticketImmersiveHeroConfigurationTest.php b/tests/Feature/Migrations/AddOnticketImmersiveHeroConfigurationTest.php new file mode 100644 index 0000000..e6231f0 --- /dev/null +++ b/tests/Feature/Migrations/AddOnticketImmersiveHeroConfigurationTest.php @@ -0,0 +1,96 @@ +id(); + $table->string('codigo')->unique(); + $table->string('nombre'); + $table->string('header_type'); + $table->timestamps(); + }); + Schema::create('storefront_website_type_extras', function (Blueprint $table): void { + $table->id(); + $table->string('storefront_website_type_code'); + $table->string('codigo'); + $table->string('nombre'); + $table->text('descripcion'); + $table->boolean('is_required'); + $table->json('config_schema'); + $table->timestamps(); + $table->unique(['storefront_website_type_code', 'codigo']); + }); + Schema::create('tenants', function (Blueprint $table): void { + $table->id(); + $table->string('codigo')->unique(); + $table->string('storefront_website_type_code'); + $table->boolean('display_seach_bar'); + $table->timestamps(); + }); + Schema::create('websites_extras', function (Blueprint $table): void { + $table->id(); + $table->string('website_code'); + $table->foreignId('website_type_extra_id'); + $table->json('config'); + $table->boolean('is_enabled'); + $table->timestamps(); + $table->unique(['website_code', 'website_type_extra_id']); + }); + + DB::table('storefront_website_types')->insert([ + 'codigo' => 'onticket_multi_event', 'nombre' => 'OnTicket', 'header_type' => 'immersive', + ]); + DB::table('tenants')->insert([ + ['id' => 10, 'codigo' => 'onticket', 'storefront_website_type_code' => 'onticket', 'display_seach_bar' => false], + ['id' => 20, 'codigo' => 'sonder', 'storefront_website_type_code' => 'shopit', 'display_seach_bar' => false], + ]); + DB::table('storefront_website_type_extras')->insert([ + 'id' => 30, 'storefront_website_type_code' => 'onticket_multi_event', 'codigo' => 'heroCarousel', + 'nombre' => 'Carrusel', 'descripcion' => 'Existente', 'is_required' => false, 'config_schema' => '{}', + ]); + DB::table('websites_extras')->insert([ + 'id' => 40, 'website_code' => 'onticket', 'website_type_extra_id' => 30, + 'config' => '[1,2]', 'is_enabled' => true, + ]); + + $migration = require database_path('migrations/2026_09_18_000000_add_onticket_immersive_hero_configuration.php'); + $migration->up(); + + $this->assertSame(10, DB::table('tenants')->where('codigo', 'onticket')->value('id')); + $this->assertSame(20, DB::table('tenants')->where('codigo', 'sonder')->value('id')); + $this->assertSame(0, DB::table('tenants')->where('codigo', 'sonder')->value('display_seach_bar')); + $this->assertSame(1, DB::table('tenants')->where('codigo', 'onticket')->value('display_seach_bar')); + $this->assertSame('onticket_multi_event', DB::table('tenants')->where('codigo', 'onticket')->value('storefront_website_type_code')); + $this->assertSame('[1,2]', DB::table('websites_extras')->where('id', 40)->value('config')); + + $extraId = DB::table('storefront_website_type_extras') + ->where('storefront_website_type_code', 'onticket_multi_event') + ->where('codigo', 'immersiveHero')->value('id'); + $this->assertNotNull($extraId); + $websiteExtra = DB::table('websites_extras') + ->where('website_code', 'onticket')->where('website_type_extra_id', $extraId)->sole(); + $this->assertSame([ + 'eyebrow' => 'Encendé tu', 'title' => 'experiencia', + 'description' => 'Reservá tu entrada y formá parte', + ], json_decode($websiteExtra->config, true)); + + DB::table('websites_extras')->where('id', $websiteExtra->id)->update([ + 'config' => '{"eyebrow":"Personalizado"}', + ]); + $migration->up(); + + $this->assertSame('{"eyebrow":"Personalizado"}', DB::table('websites_extras') + ->where('id', $websiteExtra->id)->value('config')); + $this->assertSame(1, DB::table('websites_extras') + ->where('website_code', 'onticket')->where('website_type_extra_id', $extraId)->count()); + } +} diff --git a/tests/Feature/Migrations/AddSlugsToEventsTest.php b/tests/Feature/Migrations/AddSlugsToEventsTest.php new file mode 100644 index 0000000..00328dc --- /dev/null +++ b/tests/Feature/Migrations/AddSlugsToEventsTest.php @@ -0,0 +1,37 @@ +id(); + $table->string('tenant_code'); + $table->string('title'); + }); + + DB::table('events')->insert([ + ['id' => 1, 'tenant_code' => 'acme', 'title' => 'Gran Fiesta'], + ['id' => 2, 'tenant_code' => 'acme', 'title' => 'Gran Fiesta'], + ['id' => 3, 'tenant_code' => 'other', 'title' => 'Gran Fiesta'], + ['id' => 4, 'tenant_code' => 'acme', 'title' => '---'], + ]); + + $migration = require database_path('migrations/2026_09_22_010000_add_slugs_to_events.php'); + $migration->up(); + + $this->assertSame([ + 1 => 'gran-fiesta', + 2 => 'gran-fiesta-2', + 3 => 'gran-fiesta', + 4 => 'evento-4', + ], DB::table('events')->orderBy('id')->pluck('slug', 'id')->all()); + } +} diff --git a/tests/Feature/Migrations/AddTenantTimezoneTest.php b/tests/Feature/Migrations/AddTenantTimezoneTest.php new file mode 100644 index 0000000..542d39f --- /dev/null +++ b/tests/Feature/Migrations/AddTenantTimezoneTest.php @@ -0,0 +1,48 @@ +id(); + $table->string('codigo'); + }); + Schema::create('event_dates', function (Blueprint $table): void { + $table->id(); + $table->string('tenant_code'); + $table->date('date'); + $table->time('time_start'); + $table->time('time_end'); + $table->integer('validity_time_id'); + }); + Schema::create('validity_times', function (Blueprint $table): void { + $table->id(); + $table->dateTime('fixed_starts_at'); + $table->dateTime('fixed_expires_at'); + }); + DB::table('tenants')->insert(['codigo' => 'football']); + foreach ([1, 2] as $id) { + DB::table('validity_times')->insert([ + 'id' => $id, 'fixed_starts_at' => '2026-09-21 22:00:00', 'fixed_expires_at' => '2026-09-22 02:00:00', + ]); + } + DB::table('event_dates')->insert([ + 'tenant_code' => 'football', 'date' => '2026-09-21', 'time_start' => '22:00:00', 'time_end' => '02:00:00', 'validity_time_id' => 1, + ]); + $migration = require database_path('migrations/2026_09_21_040000_add_timezone_to_tenants.php'); + $migration->up(); + $this->assertDatabaseHas('tenants', ['codigo' => 'football', 'timezone' => 'America/Argentina/Buenos_Aires']); + $this->assertDatabaseHas('validity_times', ['id' => 1, 'fixed_starts_at' => '2026-09-22 01:00:00', 'fixed_expires_at' => '2026-09-22 05:00:00']); + $this->assertDatabaseHas('validity_times', ['id' => 2, 'fixed_starts_at' => '2026-09-21 22:00:00']); + $migration->down(); + $this->assertFalse(Schema::hasColumn('tenants', 'timezone')); + } +} diff --git a/tests/Feature/Migrations/GroupPymeRuralTenantsTest.php b/tests/Feature/Migrations/GroupPymeRuralTenantsTest.php new file mode 100644 index 0000000..23c73c1 --- /dev/null +++ b/tests/Feature/Migrations/GroupPymeRuralTenantsTest.php @@ -0,0 +1,109 @@ +createSchema(); + + $now = now(); + $onTicketClientId = DB::table('clients')->insertGetId([ + 'code' => 'onticket', + 'name' => 'OnTicket', + 'created_at' => $now, + 'updated_at' => $now, + ]); + DB::table('tenants')->insert([ + ['client_id' => $onTicketClientId, 'codigo' => 'onticket'], + ['client_id' => $onTicketClientId, 'codigo' => 'fiesta_futbol_infantil'], + ['client_id' => $onTicketClientId, 'codigo' => 'desfile_pura_tendencia'], + ]); + DB::table('integrations')->insert([ + 'integration_code' => 'email', + 'name' => 'Email', + ]); + $instanceId = DB::table('integration_instances')->insertGetId([ + 'integration_code' => 'email', + 'name' => 'OnTicket email', + 'integration_data' => null, + 'created_at' => $now, + 'updated_at' => $now, + ]); + DB::table('client_integrations')->insert([ + 'client_id' => $onTicketClientId, + 'integration_code' => 'email', + 'integration_instance_id' => $instanceId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + + $migration = require database_path( + 'migrations/2026_09_21_020000_group_pyme_rural_tenants.php' + ); + $migration->up(); + + $pymeRuralClient = DB::table('clients')->where('code', 'pyme_rural')->sole(); + $this->assertSame('Pyme Rural', $pymeRuralClient->name); + $this->assertSame(2, DB::table('tenants') + ->whereIn('codigo', ['fiesta_futbol_infantil', 'desfile_pura_tendencia']) + ->where('client_id', $pymeRuralClient->id) + ->count()); + $this->assertSame($onTicketClientId, DB::table('tenants') + ->where('codigo', 'onticket') + ->value('client_id')); + + $this->assertDatabaseHas('client_integrations', [ + 'client_id' => $pymeRuralClient->id, + 'integration_code' => 'email', + 'integration_instance_id' => $instanceId, + ]); + + $migration->down(); + + $this->assertSame(2, DB::table('tenants') + ->whereIn('codigo', ['fiesta_futbol_infantil', 'desfile_pura_tendencia']) + ->where('client_id', $onTicketClientId) + ->count()); + } + + private function createSchema(): void + { + Schema::create('clients', function (Blueprint $table): void { + $table->id(); + $table->string('code')->unique(); + $table->string('name'); + $table->timestamps(); + }); + Schema::create('tenants', function (Blueprint $table): void { + $table->id(); + $table->foreignId('client_id'); + $table->string('codigo')->unique(); + }); + Schema::create('integrations', function (Blueprint $table): void { + $table->string('integration_code')->unique(); + $table->string('name'); + }); + Schema::create('integration_instances', function (Blueprint $table): void { + $table->id(); + $table->string('integration_code'); + $table->string('name'); + $table->longText('integration_data')->nullable(); + $table->timestamps(); + }); + Schema::create('client_integrations', function (Blueprint $table): void { + $table->id(); + $table->foreignId('client_id'); + $table->string('integration_code'); + $table->foreignId('integration_instance_id'); + $table->timestamps(); + $table->unique(['client_id', 'integration_code']); + }); + } +} diff --git a/tests/Feature/Migrations/RestoreEventsMigrationTest.php b/tests/Feature/Migrations/RestoreEventsMigrationTest.php new file mode 100644 index 0000000..8a4cff8 --- /dev/null +++ b/tests/Feature/Migrations/RestoreEventsMigrationTest.php @@ -0,0 +1,119 @@ +id(); + $table->string('codigo')->unique(); + $table->string('nombre'); + $table->string('storefront_website_type_code'); + $table->string('search_product_layout')->default('column_with_image'); + $table->string('search_group_layout')->default('paginated'); + $table->string('event_title')->nullable(); + $table->string('event_location')->nullable(); + $table->text('event_date_text')->nullable(); + }); + Schema::create('event_dates', function (Blueprint $table): void { + $table->id(); + $table->string('tenant_code'); + $table->date('date'); + $table->time('time_start'); + $table->time('time_end'); + }); + Schema::create('social_media', function (Blueprint $table): void { + $table->id(); + $table->string('code')->unique(); + $table->string('icon'); + $table->string('name'); + }); + Schema::create('tenant_social_media', function (Blueprint $table): void { + $table->id(); + $table->string('tenant_code'); + $table->string('social_media_code'); + $table->string('url'); + $table->unsignedInteger('orden'); + $table->timestamps(); + }); + + DB::table('tenants')->insert([ + ['codigo' => 'one', 'nombre' => 'Uno', 'storefront_website_type_code' => 'onticket', + 'event_title' => 'Evento uno', 'event_location' => 'Rosario', 'event_date_text' => '1 de octubre'], + ['codigo' => 'many', 'nombre' => 'Varios', 'storefront_website_type_code' => 'onticket_multi_event', + 'event_title' => 'Evento varios', 'event_location' => 'Santa Fe', 'event_date_text' => '2 de octubre'], + ]); + DB::table('event_dates')->insert([ + 'tenant_code' => 'one', 'date' => '2026-10-01', 'time_start' => '10:00:00', 'time_end' => '12:00:00', + ]); + DB::table('social_media')->insert(['code' => 'instagram', 'icon' => 'instagram', 'name' => 'Instagram']); + DB::table('tenant_social_media')->insert([ + 'tenant_code' => 'one', 'social_media_code' => 'instagram', + 'url' => 'https://instagram.com/example', 'orden' => 0, + ]); + + $migration = require database_path('migrations/2026_09_18_000500_restore_events.php'); + $migration->up(); + + $one = DB::table('events')->where('tenant_code', 'one')->first(); + $many = DB::table('events')->where('tenant_code', 'many')->first(); + + $this->assertSame('Evento uno', $one->title); + $this->assertSame('Rosario', $one->location); + $this->assertSame('1 de octubre', $one->date_text); + $this->assertSame($one->id, DB::table('tenants')->where('codigo', 'one')->value('active_event_id')); + $this->assertNull(DB::table('tenants')->where('codigo', 'many')->value('active_event_id')); + $this->assertSame($one->id, DB::table('event_dates')->value('event_id')); + $this->assertSame('https://instagram.com/example', DB::table('event_social_media')->value('url')); + $this->assertFalse(Schema::hasColumn('tenants', 'event_title')); + $this->assertNotNull($many); + + $tenant = Tenant::query()->where('codigo', 'one')->firstOrFail(); + $this->assertSame($one->id, $tenant->activeEvent->id); + $this->assertCount(1, $tenant->events); + $this->assertCount(1, $tenant->activeEvent->dates); + $this->assertSame('instagram', $tenant->activeEvent->socialMedia->first()->code); + + DB::table('event_social_media')->where('event_id', $one->id)->update([ + 'url' => 'https://instagram.com/event-only', + ]); + DB::table('social_media')->insert(['code' => 'whatsapp', 'icon' => 'whatsapp', 'name' => 'WhatsApp']); + DB::table('tenant_social_media')->insert([ + ['tenant_code' => 'one', 'social_media_code' => 'whatsapp', + 'url' => 'https://wa.me/123', 'orden' => 1], + ['tenant_code' => 'many', 'social_media_code' => 'whatsapp', + 'url' => 'https://wa.me/456', 'orden' => 1], + ]); + + $socialMigration = require database_path('migrations/2026_09_18_000600_associate_active_event_social_media.php'); + $socialMigration->up(); + $socialMigration->up(); + $this->assertSame(2, DB::table('event_social_media')->where('event_id', $one->id)->count()); + $this->assertSame('https://instagram.com/event-only', DB::table('event_social_media') + ->where('event_id', $one->id)->where('social_media_code', 'instagram')->value('url')); + $this->assertSame('https://wa.me/123', DB::table('event_social_media') + ->where('event_id', $one->id)->where('social_media_code', 'whatsapp')->value('url')); + $this->assertSame(0, DB::table('event_social_media')->where('event_id', $many->id)->count()); + + $tenant->load(['socialMedia', 'activeEvent.socialMedia', 'activeEvent.dates']); + foreach (['headerLogo', 'footerLogo', 'favicon', 'headerBackgroundImage', + 'footerBackgroundImage', 'storefrontWebsiteType'] as $relation) { + $tenant->setRelation($relation, null); + } + $bootstrap = TenantResource::make($tenant)->toArray(request()); + $this->assertSame('https://instagram.com/event-only', $bootstrap['social_media'][0]['url']); + $this->assertSame('https://instagram.com/event-only', $bootstrap['event']['social_media'][0]['url']); + + $migration->down(); + $this->assertSame('Evento uno', DB::table('tenants')->where('codigo', 'one')->value('event_title')); + } +} diff --git a/tests/Feature/Migrations/SetWebsiteTypeFaviconTest.php b/tests/Feature/Migrations/SetWebsiteTypeFaviconTest.php index 674130f..6db875d 100644 --- a/tests/Feature/Migrations/SetWebsiteTypeFaviconTest.php +++ b/tests/Feature/Migrations/SetWebsiteTypeFaviconTest.php @@ -15,29 +15,9 @@ class SetWebsiteTypeFaviconTest extends TestCase { Storage::fake('s3'); - DB::table('website_type')->insert([ - [ - 'codigo' => 'shopit', - 'nombre' => 'ShopIt', - 'created_at' => now(), - 'updated_at' => now(), - ], - [ - 'codigo' => 'onticket', - 'nombre' => 'OnTicket', - 'created_at' => now(), - 'updated_at' => now(), - ], - ]); + $this->seed(\Database\Seeders\WebsiteTypeSeeder::class); - $migration = require database_path( - 'migrations/2026_08_26_000000_set_website_type_favicon.php' - ); - - $migration->up(); - $migration->up(); - - $faviconIds = DB::table('website_type') + $faviconIds = DB::table('admin_website_types') ->whereIn('codigo', ['shopit', 'onticket']) ->pluck('favicon_id'); diff --git a/tests/Feature/Migrations/SplitWebsiteTypesMigrationTest.php b/tests/Feature/Migrations/SplitWebsiteTypesMigrationTest.php new file mode 100644 index 0000000..3f2ff00 --- /dev/null +++ b/tests/Feature/Migrations/SplitWebsiteTypesMigrationTest.php @@ -0,0 +1,95 @@ +id(); + $table->string('codigo')->unique(); + $table->string('nombre'); + $table->string('site_title')->nullable(); + $table->unsignedBigInteger('favicon_id')->nullable(); + $table->timestamps(); + }); + Schema::create('tenants', function (Blueprint $table): void { + $table->id(); + $table->string('website_type_code')->nullable(); + $table->string('site_title')->nullable(); + $table->unsignedBigInteger('favicon_id')->nullable(); + $table->timestamps(); + $table->foreign('website_type_code')->references('codigo')->on('website_type')->nullOnDelete(); + }); + Schema::create('website_type_extras', function (Blueprint $table): void { + $table->id(); + $table->string('website_type_code'); + $table->string('codigo')->nullable(); + $table->string('nombre'); + $table->text('descripcion'); + $table->boolean('is_required')->default(false); + $table->json('config_schema'); + $table->timestamps(); + $table->foreign('website_type_code')->references('codigo')->on('website_type')->cascadeOnDelete(); + }); + Schema::create('websites_extras', function (Blueprint $table): void { + $table->id(); + $table->string('website_code'); + $table->foreignId('website_type_extra_id')->constrained('website_type_extras')->cascadeOnDelete(); + $table->json('config'); + $table->timestamps(); + }); + Schema::create('website_type_integrations', function (Blueprint $table): void { + $table->id(); + $table->string('website_type_code'); + $table->string('integration_code'); + $table->unsignedBigInteger('integration_instance_id'); + $table->timestamps(); + $table->foreign('website_type_code')->references('codigo')->on('website_type')->cascadeOnDelete(); + }); + + DB::table('website_type')->insert(['id' => 10, 'codigo' => 'onticket', 'nombre' => 'OnTicket', 'site_title' => 'Old title', 'favicon_id' => 42]); + DB::table('tenants')->insert([ + ['id' => 1, 'website_type_code' => 'onticket', 'site_title' => null, 'favicon_id' => null], + ['id' => 2, 'website_type_code' => null, 'site_title' => null, 'favicon_id' => null], + ['id' => 3, 'website_type_code' => 'onticket', 'site_title' => 'Custom title', 'favicon_id' => 99], + ]); + DB::table('website_type_extras')->insert(['id' => 20, 'website_type_code' => 'onticket', 'codigo' => 'heroConfig', 'nombre' => 'Hero', 'descripcion' => 'Hero', 'config_schema' => '{}']); + DB::table('websites_extras')->insert(['id' => 30, 'website_code' => 'test', 'website_type_extra_id' => 20, 'config' => '{}']); + DB::table('website_type_integrations')->insert(['id' => 40, 'website_type_code' => 'onticket', 'integration_code' => 'email', 'integration_instance_id' => 50]); + + $migration = require database_path('migrations/2026_09_17_000000_split_website_types.php'); + $migration->up(); + $migration->up(); + + $this->assertDatabaseHas('tenants', ['id' => 1, 'admin_website_type_code' => 'onticket', 'storefront_website_type_code' => 'onticket', 'site_title' => 'Old title', 'favicon_id' => 42]); + $this->assertDatabaseHas('tenants', ['id' => 2, 'admin_website_type_code' => null, 'storefront_website_type_code' => null]); + $this->assertDatabaseHas('tenants', ['id' => 3, 'site_title' => 'Custom title', 'favicon_id' => 99]); + $this->assertDatabaseHas('storefront_website_type_extras', ['id' => 20, 'storefront_website_type_code' => 'onticket']); + $this->assertDatabaseHas('websites_extras', ['id' => 30, 'website_type_extra_id' => 20]); + $this->assertDatabaseHas('admin_website_type_integrations', ['id' => 40, 'admin_website_type_code' => 'onticket']); + + DB::table('storefront_website_types')->insert(['codigo' => 'new-store', 'nombre' => 'New Store']); + DB::table('storefront_website_type_extras')->insert([ + 'storefront_website_type_code' => 'new-store', + 'codigo' => 'banner', + 'nombre' => 'Banner', + 'descripcion' => 'Banner', + 'config_schema' => '{}', + ]); + $this->assertDatabaseHas('storefront_website_type_extras', ['storefront_website_type_code' => 'new-store', 'codigo' => 'banner']); + + DB::table('storefront_website_types')->where('codigo', 'onticket')->delete(); + $this->assertDatabaseMissing('storefront_website_type_extras', ['id' => 20]); + $this->assertDatabaseMissing('websites_extras', ['id' => 30]); + $this->assertDatabaseHas('tenants', ['id' => 1, 'admin_website_type_code' => 'onticket', 'storefront_website_type_code' => null]); + $this->assertDatabaseHas('admin_website_type_integrations', ['id' => 40, 'admin_website_type_code' => 'onticket']); + + } +} diff --git a/tests/Feature/Notification/IdempotentEmailDeliveryServiceTest.php b/tests/Feature/Notification/IdempotentEmailDeliveryServiceTest.php index 5b666a1..714520b 100644 --- a/tests/Feature/Notification/IdempotentEmailDeliveryServiceTest.php +++ b/tests/Feature/Notification/IdempotentEmailDeliveryServiceTest.php @@ -2,8 +2,8 @@ namespace Tests\Feature\Notification; -use App\Domains\Notification\Models\EmailDelivery; -use App\Domains\Notification\Services\IdempotentEmailDeliveryService; +use App\Shared\Notification\Models\EmailDelivery; +use App\Shared\Notification\Services\IdempotentEmailDeliveryService; use Illuminate\Foundation\Testing\RefreshDatabase; use RuntimeException; use Tests\TestCase; diff --git a/tests/Feature/Notification/NotificationMailServiceTest.php b/tests/Feature/Notification/NotificationMailServiceTest.php index 22c1ef0..e1b96a4 100644 --- a/tests/Feature/Notification/NotificationMailServiceTest.php +++ b/tests/Feature/Notification/NotificationMailServiceTest.php @@ -2,18 +2,18 @@ namespace Tests\Feature\Notification; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\ResetPasswordAttempt; -use App\Domains\Auth\Models\User; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Integration\Models\Integration; -use App\Domains\Notification\Events\PasswordResetRequested; -use App\Domains\Notification\Services\NotificationMailService; -use App\Domains\Purchase\Models\Purchase; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Models\WebsiteType; -use App\Domains\Ticket\Models\Ticket; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\ResetPasswordAttempt; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Shared\Integration\Models\Integration; +use App\Shared\Notification\Events\PasswordResetRequested; +use App\Shared\Notification\Services\NotificationMailService; +use App\Domains\Commerce\Purchase\Models\Purchase; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; +use App\Domains\Ticketing\Ticket\Models\Ticket; use Barryvdh\DomPDF\ServiceProvider as DomPdfServiceProvider; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Mail\Mailable; @@ -74,7 +74,7 @@ class NotificationMailServiceTest extends TestCase public function test_it_sends_a_branded_welcome_email(): void { - $this->useWebsiteTypeBranding(); + $this->useAdminWebsiteTypeBranding(); app(NotificationMailService::class)->sendWelcome($this->user->id, $this->tenant->codigo); app(NotificationMailService::class)->sendWelcome($this->user->id, $this->tenant->codigo); @@ -83,18 +83,17 @@ class NotificationMailServiceTest extends TestCase Mail::assertSent(Mailable::class, function (Mailable $mail): bool { $mail->assertTo('ada@example.com'); - $mail->assertHasSubject('Bienvenido a OnTicket'); + $mail->assertHasSubject('Bienvenido a Mail Tenant'); return str_contains($mail->render(), 'Ada Lovelace') - && str_contains($mail->render(), 'OnTicket') - && ! str_contains($mail->render(), 'Mail Tenant') - && str_contains($mail->render(), 'border-top: 4px solid #ff7006'); + && str_contains($mail->render(), 'Mail Tenant') + && str_contains($mail->render(), 'border-top: 4px solid #112233'); }); } public function test_it_sends_a_branded_password_reset_email(): void { - $this->useWebsiteTypeBranding(); + $this->useAdminWebsiteTypeBranding(); $attempt = $this->user->resetPasswordAttempts()->create([ 'codigo' => '0123', ]); @@ -112,15 +111,13 @@ class NotificationMailServiceTest extends TestCase Mail::assertSent(Mailable::class, function (Mailable $mail): bool { $mail->assertTo('ada@example.com'); - $mail->assertHasSubject('Código para recuperar tu contraseña - OnTicket'); + $mail->assertHasSubject('Código para recuperar tu contraseña - Mail Tenant'); $rendered = $mail->render(); return str_contains($rendered, '0123') && str_contains($rendered, 'Ada Lovelace') - && str_contains($rendered, 'OnTicket') - && ! str_contains($rendered, 'Mail Tenant') - && str_contains($rendered, '#ff7006') - && ! str_contains($rendered, 'border: 2px solid #112233'); + && str_contains($rendered, 'Mail Tenant') + && str_contains($rendered, '#112233'); }); } @@ -147,13 +144,13 @@ class NotificationMailServiceTest extends TestCase public function test_it_links_scanner_password_resets_to_the_scanner_domain(): void { - $websiteType = WebsiteType::query()->create([ + $websiteType = AdminWebsiteType::query()->create([ 'codigo' => 'scanner-mail', 'nombre' => 'Scanner Mail', 'dominio' => 'admin.mail.local', 'scanner_domain' => 'scanner.mail.local', ]); - $this->tenant->update(['website_type_code' => $websiteType->codigo]); + $this->tenant->update(['admin_website_type_code' => $websiteType->codigo]); $attempt = $this->user->resetPasswordAttempts()->create([ 'codigo' => '0123', 'reason' => ResetPasswordAttempt::REASON_STAFF_CREATED, @@ -181,11 +178,11 @@ class NotificationMailServiceTest extends TestCase public function test_administrator_creation_has_custom_copy_and_the_existing_code_and_link(): void { - $websiteType = WebsiteType::query()->create([ + $websiteType = AdminWebsiteType::query()->create([ 'codigo' => 'admin-mail', 'nombre' => 'Admin Mail', 'dominio' => 'admin.mail.local', 'scanner_domain' => 'scanner.mail.local', ]); - $this->tenant->update(['website_type_code' => $websiteType->codigo]); + $this->tenant->update(['admin_website_type_code' => $websiteType->codigo]); $attempt = $this->user->resetPasswordAttempts()->create([ 'codigo' => '0456', 'reason' => ResetPasswordAttempt::REASON_ADMINISTRATOR_CREATED, ]); @@ -226,7 +223,7 @@ class NotificationMailServiceTest extends TestCase public function test_it_sends_one_purchase_confirmation_with_generated_tickets_attached(): void { - $this->useWebsiteTypeBranding(); + $this->useAdminWebsiteTypeBranding(); $purchase = Purchase::query()->create([ 'tenant_codigo' => $this->tenant->codigo, 'user_id' => $this->user->id, @@ -443,9 +440,9 @@ class NotificationMailServiceTest extends TestCase ]); } - private function useWebsiteTypeBranding(): void + private function useAdminWebsiteTypeBranding(): void { - $websiteType = WebsiteType::query()->create([ + $websiteType = AdminWebsiteType::query()->create([ 'codigo' => 'onticket', 'nombre' => 'OnTicket', 'dominio' => 'onticket.local', @@ -456,7 +453,7 @@ class NotificationMailServiceTest extends TestCase 'login_header_footer_color' => '#838383', ]); - $this->tenant->update(['website_type_code' => $websiteType->codigo]); - $this->tenant->unsetRelation('websiteType'); + $this->tenant->update(['admin_website_type_code' => $websiteType->codigo]); + $this->tenant->unsetRelation('adminWebsiteType'); } } diff --git a/tests/Feature/Notification/QueuedNotificationListenerTest.php b/tests/Feature/Notification/QueuedNotificationListenerTest.php index f9234ce..d77e6e2 100644 --- a/tests/Feature/Notification/QueuedNotificationListenerTest.php +++ b/tests/Feature/Notification/QueuedNotificationListenerTest.php @@ -2,13 +2,13 @@ namespace Tests\Feature\Notification; -use App\Domains\Event\Events\EventDateRescheduled; -use App\Domains\Event\Events\EventDateSuspended; -use App\Domains\Notification\Listeners\SendEventDateRescheduledEmails; -use App\Domains\Notification\Listeners\SendEventDateSuspendedEmails; -use App\Domains\Notification\Listeners\SendPurchaseConfirmedEmail; -use App\Domains\Notification\Services\NotificationMailService; -use App\Domains\Purchase\Events\PurchasePaid; +use App\Domains\Ticketing\Event\Events\EventDateRescheduled; +use App\Domains\Ticketing\Event\Events\EventDateSuspended; +use App\Shared\Notification\Listeners\SendEventDateRescheduledEmails; +use App\Shared\Notification\Listeners\SendEventDateSuspendedEmails; +use App\Shared\Notification\Listeners\SendPurchaseConfirmedEmail; +use App\Shared\Notification\Services\NotificationMailService; +use App\Domains\Commerce\Purchase\Events\PurchasePaid; use Mockery; use Tests\TestCase; diff --git a/tests/Feature/Purchase/PurchaseCatalogItemTest.php b/tests/Feature/Purchase/PurchaseCatalogItemTest.php index 8d1ac8e..6890c13 100644 --- a/tests/Feature/Purchase/PurchaseCatalogItemTest.php +++ b/tests/Feature/Purchase/PurchaseCatalogItemTest.php @@ -2,14 +2,16 @@ namespace Tests\Feature\Purchase; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\User; -use App\Domains\Cart\Models\Cart; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Inventory; -use App\Domains\Purchase\Services\CheckoutService; -use App\Domains\Tenant\Models\Tenant; +use App\Domains\Commerce\Cart\Models\Cart; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Inventory; +use App\Domains\Commerce\Purchase\Services\CheckoutService; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Tenant\Models\StorefrontWebsiteType; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Ticketing\Event\Models\Event; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Storage; @@ -117,6 +119,70 @@ class PurchaseCatalogItemTest extends TestCase ]); } + public function test_multi_event_checkout_returns_only_the_event_image(): void + { + Storage::fake('s3'); + Storage::disk('s3')->buildTemporaryUrlsUsing( + fn (string $path): string => "https://snapshots.test/{$path}", + ); + + StorefrontWebsiteType::query()->create([ + 'codigo' => 'onticket_multi_event', + 'nombre' => 'OnTicket - múltiples eventos', + 'header_type' => 'immersive', + 'checkout_summary_type' => StorefrontWebsiteType::CHECKOUT_SUMMARY_EVENT, + ]); + $tenant = $this->createTenant(); + $tenant->update(['storefront_website_type_code' => 'onticket_multi_event']); + $user = User::factory()->create(); + + $eventImage = $this->createAttachment('checkout-event'); + $productImage = $this->createAttachment('checkout-product'); + Storage::disk('s3')->put($eventImage->path, 'event-image'); + Storage::disk('s3')->put($productImage->path, 'product-image'); + + $event = Event::query()->create([ + 'tenant_code' => $tenant->codigo, + 'title' => 'Festival de prueba', + 'published_at' => now(), + 'attachment_id' => $eventImage->id, + ]); + $inventory = Inventory::query()->create(['real_stock' => 10]); + $catalogItem = CatalogItem::query()->create([ + 'tenant_code' => $tenant->codigo, + 'inventory_id' => $inventory->id, + 'slug' => 'entrada-general', + 'nombre' => 'Entrada general', + 'precio' => 10000, + ]); + $catalogItem->event_id = $event->id; + $catalogItem->save(); + $catalogItem->attachments()->attach($productImage->id, ['orden' => 0]); + + $cart = Cart::query()->create([ + 'tenant_codigo' => $tenant->codigo, + 'user_id' => $user->id, + 'status' => 'active', + ]); + $cart->addItem($catalogItem->id, null, 1); + + $purchase = app(CheckoutService::class)->startCheckout($tenant, $user->id, [ + 'cart_id' => $cart->id, + 'dni' => '12345678', + 'telefono' => '123456789', + 'nombre_apellido' => 'Test User', + 'email' => 'test@example.com', + ]); + + $this->actingAs($user, 'sanctum') + ->getJson("/api/tenants/{$tenant->codigo}/compras/{$purchase->id}") + ->assertOk() + ->assertJsonMissingPath('data.items.0.item_details.imagen') + ->assertJsonPath('data.event.id', $event->id) + ->assertJsonPath('data.event.title', 'Festival de prueba') + ->assertJsonPath('data.event.image', "https://snapshots.test/{$eventImage->path}"); + } + private function createTenant(): Tenant { $headerLogo = $this->createAttachment('purchase-header'); diff --git a/tests/Feature/Purchase/PurchaseExpiredExceptionResponseTest.php b/tests/Feature/Purchase/PurchaseExpiredExceptionResponseTest.php index 49e4522..705664b 100644 --- a/tests/Feature/Purchase/PurchaseExpiredExceptionResponseTest.php +++ b/tests/Feature/Purchase/PurchaseExpiredExceptionResponseTest.php @@ -2,7 +2,7 @@ namespace Tests\Feature\Purchase; -use App\Domains\Purchase\Exceptions\PurchaseExpiredException; +use App\Domains\Commerce\Purchase\Exceptions\PurchaseExpiredException; use Illuminate\Support\Facades\Route; use Tests\TestCase; diff --git a/tests/Feature/Purchase/StorePurchaseTest.php b/tests/Feature/Purchase/StorePurchaseTest.php index 65ec3f6..f92cb68 100644 --- a/tests/Feature/Purchase/StorePurchaseTest.php +++ b/tests/Feature/Purchase/StorePurchaseTest.php @@ -2,22 +2,22 @@ namespace Tests\Feature\Purchase; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\User; -use App\Domains\Cart\Models\Cart; -use App\Domains\Catalog\Enums\InventoryPolicy; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Category; -use App\Domains\Catalog\Models\Inventory; -use App\Domains\Catalog\Models\StockReservation; -use App\Domains\Catalog\Models\StockReservationLine; -use App\Domains\Catalog\Models\Variant; -use App\Domains\Event\Models\EventDate; -use App\Domains\Purchase\Models\Purchase; -use App\Domains\Purchase\Services\CheckoutService; -use App\Domains\Purchase\Services\UserPurchaseLimitService; -use App\Domains\Tenant\Models\Tenant; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Commerce\Cart\Models\Cart; +use App\Domains\Commerce\Catalog\Enums\InventoryPolicy; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Category; +use App\Domains\Commerce\Catalog\Models\Inventory; +use App\Domains\Commerce\Catalog\Models\StockReservation; +use App\Domains\Commerce\Catalog\Models\StockReservationLine; +use App\Domains\Commerce\Catalog\Models\Variant; +use App\Domains\Ticketing\Event\Models\EventDate; +use App\Domains\Commerce\Purchase\Models\Purchase; +use App\Domains\Commerce\Purchase\Services\CheckoutService; +use App\Domains\Commerce\Purchase\Services\UserPurchaseLimitService; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Queue; use Illuminate\Support\Str; diff --git a/tests/Feature/Sale/AdminAppSaleControllerTest.php b/tests/Feature/Sale/AdminAppSaleControllerTest.php index f995133..5f694a6 100644 --- a/tests/Feature/Sale/AdminAppSaleControllerTest.php +++ b/tests/Feature/Sale/AdminAppSaleControllerTest.php @@ -2,25 +2,25 @@ namespace Tests\Feature\Sale; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Cart\Models\Cart; -use App\Domains\Cart\Models\CartItem; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Inventory; -use App\Domains\Catalog\Models\Variant; -use App\Domains\Logging\Enums\ValueChangeActorType; -use App\Domains\Logging\Models\ValueChange; -use App\Domains\Purchase\Events\PurchasePaid; -use App\Domains\Purchase\Models\Purchase; -use App\Domains\Purchase\Models\PurchaseItem; -use App\Domains\Purchase\Services\CheckoutService; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Models\WebsiteType; -use App\Domains\Ticket\Models\Ticket; -use App\Domains\Ticket\Models\TicketRefund; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Commerce\Cart\Models\Cart; +use App\Domains\Commerce\Cart\Models\CartItem; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Inventory; +use App\Domains\Commerce\Catalog\Models\Variant; +use App\Shared\Logging\Enums\ValueChangeActorType; +use App\Shared\Logging\Models\ValueChange; +use App\Domains\Commerce\Purchase\Events\PurchasePaid; +use App\Domains\Commerce\Purchase\Models\Purchase; +use App\Domains\Commerce\Purchase\Models\PurchaseItem; +use App\Domains\Commerce\Purchase\Services\CheckoutService; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; +use App\Domains\Ticketing\Ticket\Models\Ticket; +use App\Domains\Ticketing\Ticket\Models\TicketRefund; use Database\Seeders\AuthorizationSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Event; @@ -36,7 +36,7 @@ class AdminAppSaleControllerTest extends TestCase parent::setUp(); $this->seed(AuthorizationSeeder::class); - WebsiteType::query()->create(['codigo' => 'onticket', 'nombre' => 'OnTicket']); + AdminWebsiteType::query()->create(['codigo' => 'onticket', 'nombre' => 'OnTicket']); } public function test_sale_modifications_use_sale_filters_and_resulting_status(): void @@ -757,7 +757,7 @@ class AdminAppSaleControllerTest extends TestCase 'footer_bg_color' => '#000000', 'header_logo_id' => $headerLogo->id, 'footer_logo_id' => $footerLogo->id, - 'website_type_code' => 'onticket', + 'admin_website_type_code' => 'onticket', ]); } diff --git a/tests/Feature/Seeders/AuthorizationSeederTest.php b/tests/Feature/Seeders/AuthorizationSeederTest.php index 4fe9976..36a84ee 100644 --- a/tests/Feature/Seeders/AuthorizationSeederTest.php +++ b/tests/Feature/Seeders/AuthorizationSeederTest.php @@ -2,10 +2,10 @@ namespace Tests\Feature\Seeders; -use App\Domains\Authorization\Enums\PermissionCode; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Authorization\Models\Permission; -use App\Domains\Authorization\Models\Role; +use App\Domains\Core\Authorization\Enums\PermissionCode; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Core\Authorization\Models\Permission; +use App\Domains\Core\Authorization\Models\Role; use Database\Seeders\AuthorizationSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; diff --git a/tests/Feature/Seeders/DesfilePuraTendenciaSeederTest.php b/tests/Feature/Seeders/DesfilePuraTendenciaSeederTest.php index df0e9b4..6dc80cb 100644 --- a/tests/Feature/Seeders/DesfilePuraTendenciaSeederTest.php +++ b/tests/Feature/Seeders/DesfilePuraTendenciaSeederTest.php @@ -32,7 +32,7 @@ class DesfilePuraTendenciaSeederTest extends TestCase 'codigo' => 'desfile_pura_tendencia', 'nombre' => 'Desfile Pura Tendencia', 'dominio' => 'desfile-pura-tendencia.localhost', - 'website_type_code' => 'onticket', + 'storefront_website_type_code' => 'onticket', 'event_title' => 'Desfile Pura Tendencia', 'event_location' => 'Salón Centro Recreativo Luz y Fuerza', 'event_date_text' => '16 de Octubre 2026', @@ -55,6 +55,14 @@ class DesfilePuraTendenciaSeederTest extends TestCase 'site_title' => 'Fiesta Fútbol Infantil', ]); + $pymeRuralClientId = DB::table('clients')->where('code', 'pyme_rural')->value('id'); + $this->assertNotNull($pymeRuralClientId); + $this->assertSame('Pyme Rural', DB::table('clients')->where('id', $pymeRuralClientId)->value('name')); + $this->assertSame(2, DB::table('tenants') + ->whereIn('codigo', ['fiesta_futbol_infantil', 'desfile_pura_tendencia']) + ->where('client_id', $pymeRuralClientId) + ->count()); + foreach ([ 'fiesta_futbol_infantil' => 'futbol_infantil_favicon.png', 'desfile_pura_tendencia' => 'pura_tendencia_favicon.png', @@ -82,13 +90,13 @@ class DesfilePuraTendenciaSeederTest extends TestCase $hero = DB::table('websites_extras') ->join( - 'website_type_extras', - 'website_type_extras.id', + 'storefront_website_type_extras', + 'storefront_website_type_extras.id', '=', 'websites_extras.website_type_extra_id', ) ->where('websites_extras.website_code', 'desfile_pura_tendencia') - ->where('website_type_extras.codigo', 'heroConfig') + ->where('storefront_website_type_extras.codigo', 'heroConfig') ->value('websites_extras.config'); $hero = json_decode($hero, true, flags: JSON_THROW_ON_ERROR); diff --git a/tests/Feature/Seeders/FiestaFutbolInfantilProductSeederTest.php b/tests/Feature/Seeders/FiestaFutbolInfantilProductSeederTest.php index 98e5071..7d7a82f 100644 --- a/tests/Feature/Seeders/FiestaFutbolInfantilProductSeederTest.php +++ b/tests/Feature/Seeders/FiestaFutbolInfantilProductSeederTest.php @@ -2,18 +2,18 @@ namespace Tests\Feature\Seeders; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Catalog\Enums\FeaturedGroupSource; -use App\Domains\Catalog\Enums\GroupLayout; -use App\Domains\Catalog\Enums\ProductLayout; -use App\Domains\Catalog\Models\Attribute; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Category; -use App\Domains\Catalog\Models\FeaturedGroup; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Ticket\Enums\ValidityTimeType; -use App\Domains\Ticket\Models\ValidityTime; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Commerce\Catalog\Enums\FeaturedGroupSource; +use App\Domains\Commerce\Catalog\Enums\GroupLayout; +use App\Domains\Commerce\Catalog\Enums\ProductLayout; +use App\Domains\Commerce\Catalog\Models\Attribute; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Category; +use App\Domains\Commerce\Catalog\Models\FeaturedGroup; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Ticketing\Ticket\Enums\ValidityTimeType; +use App\Domains\Ticketing\Ticket\Models\ValidityTime; use Database\Seeders\AttributeSeeder; use Database\Seeders\FiestaFutbolInfantilProductSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; diff --git a/tests/Feature/Seeders/MenuSeederTest.php b/tests/Feature/Seeders/MenuSeederTest.php index d253c8d..10d323c 100644 --- a/tests/Feature/Seeders/MenuSeederTest.php +++ b/tests/Feature/Seeders/MenuSeederTest.php @@ -2,12 +2,12 @@ namespace Tests\Feature\Seeders; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Authorization\Models\Role; -use App\Domains\Menu\Models\Menu; -use App\Domains\Tenant\Models\Tenant; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Core\Authorization\Models\Role; +use App\Domains\Core\Menu\Models\Menu; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; use Database\Seeders\AuthorizationSeeder; use Database\Seeders\MenuSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -183,6 +183,49 @@ class MenuSeederTest extends TestCase ); } + public function test_it_assigns_event_menus_only_to_onticket(): void + { + $onTicket = $this->createTenant('onticket'); + $otherTenant = $this->createTenant('other'); + + $this->seed(AuthorizationSeeder::class); + $this->seed(MenuSeeder::class); + + foreach ([ + 'event.index' => '/', + 'event.category' => '/eventos/categoria/:id', + 'event.detail' => '/evento/:slug', + ] as $code => $route) { + $menu = Menu::query()->where('code', $code)->firstOrFail(); + + $this->assertSame($route, $menu->route); + $this->assertTrue($onTicket->menues()->where('menues.code', $code)->exists()); + $this->assertFalse($otherTenant->menues()->where('menues.code', $code)->exists()); + $this->assertTrue( + Role::query() + ->where('codigo', RoleCode::User->value) + ->firstOrFail() + ->menus() + ->where('menues.code', $code) + ->exists() + ); + } + + $productCategory = Menu::query()->where('code', 'product.category')->firstOrFail(); + + $this->assertSame('/categoria/:id', $productCategory->route); + $this->assertFalse($onTicket->menues()->where('menues.code', 'product.category')->exists()); + $this->assertTrue($otherTenant->menues()->where('menues.code', 'product.category')->exists()); + $this->assertTrue( + Role::query() + ->where('codigo', RoleCode::User->value) + ->firstOrFail() + ->menus() + ->where('menues.code', 'product.category') + ->exists() + ); + } + public function test_it_seeds_the_account_menu_hierarchy(): void { $tenant = $this->createTenant('account_tenant'); diff --git a/tests/Feature/Seeders/ProductCatalogFromImagesSeederTest.php b/tests/Feature/Seeders/ProductCatalogFromImagesSeederTest.php index cf2d47f..2373c6b 100644 --- a/tests/Feature/Seeders/ProductCatalogFromImagesSeederTest.php +++ b/tests/Feature/Seeders/ProductCatalogFromImagesSeederTest.php @@ -2,15 +2,15 @@ namespace Tests\Feature\Seeders; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Catalog\Enums\FeaturedGroupSource; -use App\Domains\Catalog\Enums\GroupLayout; -use App\Domains\Catalog\Enums\ProductLayout; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Category; -use App\Domains\Catalog\Models\FeaturedGroup; -use App\Domains\Tenant\Models\Tenant; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Commerce\Catalog\Enums\FeaturedGroupSource; +use App\Domains\Commerce\Catalog\Enums\GroupLayout; +use App\Domains\Commerce\Catalog\Enums\ProductLayout; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Category; +use App\Domains\Commerce\Catalog\Models\FeaturedGroup; +use App\Domains\Core\Tenant\Models\Tenant; use Database\Seeders\AttributeSeeder; use Database\Seeders\BrandSeeder; use Database\Seeders\CategorySeeder; diff --git a/tests/Feature/Seeders/SocialMediaSeederTest.php b/tests/Feature/Seeders/SocialMediaSeederTest.php index 6bae057..3e771c9 100644 --- a/tests/Feature/Seeders/SocialMediaSeederTest.php +++ b/tests/Feature/Seeders/SocialMediaSeederTest.php @@ -2,7 +2,7 @@ namespace Tests\Feature\Seeders; -use App\Domains\Tenant\Models\SocialMedia; +use App\Domains\Core\Tenant\Models\SocialMedia; use Database\Seeders\SocialMediaSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; diff --git a/tests/Feature/Seeders/TenantSeederTest.php b/tests/Feature/Seeders/TenantSeederTest.php index 8dbd747..8847f80 100644 --- a/tests/Feature/Seeders/TenantSeederTest.php +++ b/tests/Feature/Seeders/TenantSeederTest.php @@ -2,7 +2,9 @@ namespace Tests\Feature\Seeders; -use App\Domains\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Shared\Attachable\Models\Attachment; use Database\Seeders\SocialMediaSeeder; use Database\Seeders\TenantSeeder; use Database\Seeders\WebsiteTypeSeeder; @@ -14,6 +16,71 @@ class TenantSeederTest extends TestCase { use RefreshDatabase; + public function test_it_creates_the_onticket_tenant_with_website_type_branding(): void + { + Storage::fake('s3'); + + $this->seed([ + WebsiteTypeSeeder::class, + SocialMediaSeeder::class, + TenantSeeder::class, + ]); + + $websiteType = AdminWebsiteType::query()->where('codigo', 'onticket')->sole(); + $tenant = Tenant::query()->where('codigo', 'onticket')->sole(); + + $this->assertSame('onticket', $tenant->client->code); + $this->assertSame($websiteType->codigo, $tenant->admin_website_type_code); + $this->assertSame('onticket_multi_event', $tenant->storefront_website_type_code); + $this->assertTrue($tenant->display_seach_bar); + $this->assertSame($websiteType->nombre, $tenant->nombre); + $this->assertSame($websiteType->dominio, $tenant->dominio); + $this->assertSame($websiteType->site_title, $tenant->site_title); + + foreach (['primary_color', 'secondary_color', 'danger_color', 'success_color'] as $color) { + $this->assertSame($websiteType->{$color}, $tenant->{$color}); + } + + $this->assertSame('onticket_logo.png', $tenant->headerLogo->filename); + $this->assertSame('onticket_footer_logo.png', $tenant->footerLogo->filename); + $this->assertSame('onticket_favicon.svg', $tenant->favicon->filename); + $this->assertSame('onticket_header_background.avif', $tenant->headerBackgroundImage->filename); + + foreach ([$tenant->headerLogo, $tenant->footerLogo, $tenant->favicon, $tenant->headerBackgroundImage] as $attachment) { + Storage::disk('s3')->assertExists($attachment->path); + } + + $carousel = $tenant->websiteExtras() + ->whereHas('websiteTypeExtra', fn ($query) => $query->where('codigo', 'immersiveHeroCarousel')) + ->sole(); + $imageIds = $carousel->config; + $this->assertCount(8, $imageIds); + $this->assertSame([ + 'onticket_hero_carousel_1.avif', + 'onticket_hero_carousel_2.avif', + 'onticket_hero_carousel_3.avif', + 'onticket_hero_carousel_4.avif', + 'onticket_hero_carousel_5.avif', + 'onticket_hero_carousel_6.avif', + 'onticket_hero_carousel_7.avif', + 'onticket_hero_carousel_8.avif', + ], array_map(fn (int $id): string => Attachment::query()->findOrFail($id)->filename, $imageIds)); + + foreach ($imageIds as $id) { + Storage::disk('s3')->assertExists(Attachment::query()->findOrFail($id)->path); + } + + $this->seed(TenantSeeder::class); + $this->assertSame($imageIds, $carousel->fresh()->config); + $this->assertSame([ + 'eyebrow' => 'Encendé tu', + 'title' => 'experiencia', + 'description' => 'Reservá tu entrada y formá parte', + ], $tenant->websiteExtras() + ->whereHas('websiteTypeExtra', fn ($query) => $query->where('codigo', 'immersiveHero')) + ->sole()->config); + } + public function test_it_assigns_social_media_to_both_tenants_in_order(): void { Storage::fake('s3'); @@ -60,7 +127,8 @@ class TenantSeederTest extends TestCase ->with('websiteExtras.websiteTypeExtra') ->sole(); - $this->assertSame('shopit', $sonder->website_type_code); + $this->assertSame('shopit', $sonder->storefront_website_type_code); + $this->assertSame('shopit', $sonder->admin_website_type_code); $this->assertSame('quantity_and_remove', $sonder->cart_editing_policy->value); $this->assertSame('disabled', $sonder->checkout_editing_policy->value); @@ -82,7 +150,10 @@ class TenantSeederTest extends TestCase ->with('websiteExtras.websiteTypeExtra') ->sole(); - $this->assertSame('onticket', $fiesta->website_type_code); + $this->assertSame('pyme_rural', $fiesta->client->code); + $this->assertSame('Pyme Rural', $fiesta->client->name); + $this->assertSame('onticket', $fiesta->storefront_website_type_code); + $this->assertSame('onticket', $fiesta->admin_website_type_code); $this->assertSame('full', $fiesta->cart_editing_policy->value); $this->assertSame('disabled', $fiesta->checkout_editing_policy->value); diff --git a/tests/Feature/Seeders/TestEventsSeederTest.php b/tests/Feature/Seeders/TestEventsSeederTest.php new file mode 100644 index 0000000..7bb09e0 --- /dev/null +++ b/tests/Feature/Seeders/TestEventsSeederTest.php @@ -0,0 +1,79 @@ +seed([WebsiteTypeSeeder::class, SocialMediaSeeder::class, TenantSeeder::class]); + + $tenant = Tenant::query()->where('codigo', 'onticket')->sole(); + $this->assertCount(0, $tenant->events); + + $this->seed(TestEventsSeeder::class); + + $events = $tenant->events()->with('dates.validityTime')->get(); + $this->assertCount(3, $events); + $this->assertSame(4, $events->sum(fn ($event): int => $event->dates->count())); + $this->assertSame(6, $events->sum(fn ($event): int => $event->catalogItems()->count())); + $this->assertEqualsCanonicalizing([ + 'prueba-noche-de-musica-en-vivo', + 'prueba-festival-de-sabores', + 'prueba-stand-up-en-el-centro', + ], $events->pluck('slug')->all()); + $this->assertEqualsCanonicalizing( + ['live-music.avif', 'food-festival.avif', 'stand-up.avif'], + $events->map(fn ($event): string => $event->attachment->filename)->all(), + ); + + foreach ($events as $event) { + $this->assertTrue($event->published_at->isPast()); + $this->assertNotNull($event->date_text); + $this->assertNotNull($event->attachment_id); + $this->assertSame('image/avif', $event->attachment->mime_type); + Storage::disk('s3')->assertExists($event->attachment->path); + + $this->assertCount(2, $event->catalogItems); + + foreach ($event->catalogItems as $catalogItem) { + $this->assertTrue($catalogItem->has_tickets); + $this->assertSame('Entradas', $catalogItem->category->nombre); + $this->assertSame($event->id, $catalogItem->event_id); + $this->assertSame($event->dates->count(), $catalogItem->variants->count()); + $this->assertTrue($catalogItem->variants->every( + fn ($variant): bool => $variant->inventory->real_stock > 0 + && $variant->eventDates->count() === 1 + )); + } + + foreach ($event->dates as $date) { + $this->assertTrue($date->startsAt()->isFuture()); + $this->assertNotNull($date->validityTime); + } + } + + $this->seed(TestEventsSeeder::class); + + $this->assertCount(3, $tenant->events()->get()); + $this->assertCount(4, $tenant->eventDates()->get()); + $this->assertCount(6, $tenant->catalogItems()->get()); + $this->assertSame( + $events->pluck('attachment_id')->all(), + $tenant->events()->orderBy('id')->pluck('attachment_id')->all(), + ); + } +} diff --git a/tests/Feature/Seeders/WebsiteTypeSeederTest.php b/tests/Feature/Seeders/WebsiteTypeSeederTest.php index d286cf6..2678ca3 100644 --- a/tests/Feature/Seeders/WebsiteTypeSeederTest.php +++ b/tests/Feature/Seeders/WebsiteTypeSeederTest.php @@ -2,8 +2,9 @@ namespace Tests\Feature\Seeders; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Tenant\Models\WebsiteType; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; +use App\Domains\Core\Tenant\Models\StorefrontWebsiteType; +use App\Shared\Attachable\Models\Attachment; use Database\Seeders\WebsiteTypeSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Storage; @@ -20,7 +21,8 @@ class WebsiteTypeSeederTest extends TestCase $this->seed(WebsiteTypeSeeder::class); $this->seed(WebsiteTypeSeeder::class); - $this->assertSame(2, WebsiteType::query()->count()); + $this->assertSame(2, AdminWebsiteType::query()->count()); + $this->assertSame(3, StorefrontWebsiteType::query()->count()); $this->assertSame(5, Attachment::query()->count()); $expectedPresentation = [ @@ -36,9 +38,8 @@ class WebsiteTypeSeederTest extends TestCase 'border_color' => '#EAEAEA', ]; - $shopIt = WebsiteType::query() + $shopIt = AdminWebsiteType::query() ->where('codigo', 'shopit') - ->with('extras') ->sole(); $this->assertSame('ShopIt', $shopIt->nombre); @@ -51,8 +52,11 @@ class WebsiteTypeSeederTest extends TestCase Storage::disk('s3')->assertExists($shopIt->footerLogo->path); $this->assertSame('onticket_favicon.svg', $shopIt->favicon->filename); Storage::disk('s3')->assertExists($shopIt->favicon->path); - $this->assertSame(['carousel'], $shopIt->extras->pluck('codigo')->all()); - $this->assertSame('Carrusel principal', $shopIt->extras->sole()->nombre); + $shopItStorefront = StorefrontWebsiteType::query()->where('codigo', 'shopit')->with('extras')->sole(); + $this->assertSame('standard', $shopItStorefront->header_type); + $this->assertSame(StorefrontWebsiteType::CHECKOUT_SUMMARY_DEFAULT, $shopItStorefront->checkout_summary_type); + $this->assertSame(['carousel'], $shopItStorefront->extras->pluck('codigo')->all()); + $this->assertSame('Carrusel principal', $shopItStorefront->extras->sole()->nombre); $this->assertSame([ 'request_rules' => [ '$' => 'required|array|max:10', @@ -64,11 +68,10 @@ class WebsiteTypeSeederTest extends TestCase 'attachment_type' => 'image', ], ], - ], $shopIt->extras->sole()->config_schema); + ], $shopItStorefront->extras->sole()->config_schema); - $onTicket = WebsiteType::query() + $onTicket = AdminWebsiteType::query() ->where('codigo', 'onticket') - ->with('extras') ->sole(); $this->assertSame('OnTicket', $onTicket->nombre); @@ -83,12 +86,20 @@ class WebsiteTypeSeederTest extends TestCase $this->assertNotSame($shopIt->footer_logo, $onTicket->footer_logo); $this->assertSame($shopIt->favicon_id, $onTicket->favicon_id); $this->assertSame('onticket_favicon.svg', $onTicket->favicon->filename); + $onTicketStorefront = StorefrontWebsiteType::query()->where('codigo', 'onticket')->with('extras')->sole(); + $this->assertSame('standard', $onTicketStorefront->header_type); + $this->assertSame(StorefrontWebsiteType::CHECKOUT_SUMMARY_DEFAULT, $onTicketStorefront->checkout_summary_type); + $multiEventStorefront = StorefrontWebsiteType::query()->where('codigo', 'onticket_multi_event')->with('extras')->sole(); + $this->assertSame('immersive', $multiEventStorefront->header_type); + $this->assertSame(StorefrontWebsiteType::CHECKOUT_SUMMARY_EVENT, $multiEventStorefront->checkout_summary_type); + $this->assertEqualsCanonicalizing(['immersiveHeroCarousel', 'immersiveHero'], $multiEventStorefront->extras->pluck('codigo')->all()); + $this->assertSame($shopItStorefront->extras->sole()->config_schema, $multiEventStorefront->extras->firstWhere('codigo', 'immersiveHeroCarousel')->config_schema); $this->assertEqualsCanonicalizing( ['heroConfig', 'eventConfig', 'additionalInfoConfig'], - $onTicket->extras->pluck('codigo')->all(), + $onTicketStorefront->extras->pluck('codigo')->all(), ); - $heroSchema = $onTicket->extras->firstWhere('codigo', 'heroConfig')->config_schema; + $heroSchema = $onTicketStorefront->extras->firstWhere('codigo', 'heroConfig')->config_schema; $this->assertSame([ 'request_rules' => [ '$' => 'required|array', @@ -106,7 +117,7 @@ class WebsiteTypeSeederTest extends TestCase ], ], $heroSchema); - $eventSchema = $onTicket->extras->firstWhere('codigo', 'eventConfig')->config_schema; + $eventSchema = $onTicketStorefront->extras->firstWhere('codigo', 'eventConfig')->config_schema; $this->assertSame([ 'request_rules' => [ '$' => 'required|array', @@ -126,7 +137,7 @@ class WebsiteTypeSeederTest extends TestCase 'transforms' => [], ], $eventSchema); - $additionalInfoSchema = $onTicket->extras + $additionalInfoSchema = $onTicketStorefront->extras ->firstWhere('codigo', 'additionalInfoConfig') ->config_schema; $this->assertSame([ diff --git a/tests/Feature/Staff/StaffControllerTest.php b/tests/Feature/Staff/StaffControllerTest.php index b8bfb89..0b0a36c 100644 --- a/tests/Feature/Staff/StaffControllerTest.php +++ b/tests/Feature/Staff/StaffControllerTest.php @@ -2,19 +2,19 @@ namespace Tests\Feature\Staff; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\ResetPasswordAttempt; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Category; -use App\Domains\Notification\Events\PasswordResetRequested; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Models\WebsiteType; -use App\Domains\Ticket\Enums\ScanAttemptResult; -use App\Domains\Ticket\Models\ScanAttempt; -use App\Domains\Ticket\Models\Ticket; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\ResetPasswordAttempt; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Category; +use App\Shared\Notification\Events\PasswordResetRequested; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; +use App\Domains\Ticketing\Ticket\Enums\ScanAttemptResult; +use App\Domains\Ticketing\Ticket\Models\ScanAttempt; +use App\Domains\Ticketing\Ticket\Models\Ticket; use Database\Seeders\AuthorizationSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Event; @@ -36,14 +36,14 @@ class StaffControllerTest extends TestCase Event::fake([PasswordResetRequested::class]); $this->seed(AuthorizationSeeder::class); - WebsiteType::query()->create(['codigo' => 'onticket', 'nombre' => 'OnTicket']); + AdminWebsiteType::query()->create(['codigo' => 'onticket', 'nombre' => 'OnTicket']); $headerLogo = $this->createAttachment('header.png'); $footerLogo = $this->createAttachment('footer.png'); $this->tenant = Tenant::query()->create([ 'codigo' => 'acme', 'nombre' => 'Acme', 'dominio' => 'acme.test', - 'website_type_code' => 'onticket', + 'admin_website_type_code' => 'onticket', 'primary_color' => '#111111', 'secondary_color' => '#222222', 'danger_color' => '#cc0000', @@ -179,7 +179,7 @@ class StaffControllerTest extends TestCase 'codigo' => 'other', 'nombre' => 'Other', 'dominio' => 'other.test', - 'website_type_code' => 'onticket', + 'admin_website_type_code' => 'onticket', 'primary_color' => '#111111', 'secondary_color' => '#222222', 'danger_color' => '#cc0000', diff --git a/tests/Feature/Tenant/AdminAppWebsiteExtraControllerTest.php b/tests/Feature/Tenant/AdminAppWebsiteExtraControllerTest.php index 2cb8b83..508f820 100644 --- a/tests/Feature/Tenant/AdminAppWebsiteExtraControllerTest.php +++ b/tests/Feature/Tenant/AdminAppWebsiteExtraControllerTest.php @@ -2,12 +2,12 @@ namespace Tests\Feature\Tenant; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Attachable\Models\AttachmentCrop; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Models\WebsiteType; +use App\Shared\Attachable\Models\Attachment; +use App\Shared\Attachable\Models\AttachmentCrop; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\StorefrontWebsiteType; use Database\Seeders\AuthorizationSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\UploadedFile; @@ -19,7 +19,7 @@ class AdminAppWebsiteExtraControllerTest extends TestCase { use RefreshDatabase; - private WebsiteType $websiteType; + private StorefrontWebsiteType $websiteType; protected function setUp(): void { @@ -27,7 +27,7 @@ class AdminAppWebsiteExtraControllerTest extends TestCase $this->seed(AuthorizationSeeder::class); - $this->websiteType = WebsiteType::query()->create([ + $this->websiteType = StorefrontWebsiteType::query()->create([ 'codigo' => 'test-store', 'nombre' => 'Test Store', ]); @@ -79,7 +79,7 @@ class AdminAppWebsiteExtraControllerTest extends TestCase $this->getJson('/api/v1/adminapp/tenant/website-extras') ->assertOk() - ->assertJsonPath('data.website_type.codigo', 'test-store') + ->assertJsonPath('data.storefront_website_type.codigo', 'test-store') ->assertJsonPath('data.definitions.contactConfig.codigo', 'contactConfig') ->assertJsonPath('data.definitions.contactConfig.nombre', 'Configuración de contacto') ->assertJsonPath('data.definitions.contactConfig.is_required', false) @@ -407,7 +407,7 @@ class AdminAppWebsiteExtraControllerTest extends TestCase 'codigo' => $code, 'nombre' => ucfirst($code), 'dominio' => "{$code}.test", - 'website_type_code' => $this->websiteType->codigo, + 'storefront_website_type_code' => $this->websiteType->codigo, ]); } diff --git a/tests/Feature/Tenant/WebsiteTypeServiceTest.php b/tests/Feature/Tenant/AdminWebsiteTypeServiceTest.php similarity index 86% rename from tests/Feature/Tenant/WebsiteTypeServiceTest.php rename to tests/Feature/Tenant/AdminWebsiteTypeServiceTest.php index 39cd9a2..3f0dcca 100644 --- a/tests/Feature/Tenant/WebsiteTypeServiceTest.php +++ b/tests/Feature/Tenant/AdminWebsiteTypeServiceTest.php @@ -2,13 +2,13 @@ namespace Tests\Feature\Tenant; -use App\Domains\Tenant\Services\WebsiteTypeService; +use App\Domains\Core\Tenant\Services\AdminWebsiteTypeService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Storage; use Tests\TestCase; -class WebsiteTypeServiceTest extends TestCase +class AdminWebsiteTypeServiceTest extends TestCase { use RefreshDatabase; @@ -16,7 +16,7 @@ class WebsiteTypeServiceTest extends TestCase { Storage::fake('s3'); - $websiteType = app(WebsiteTypeService::class)->create([ + $websiteType = app(AdminWebsiteTypeService::class)->create([ 'codigo' => 'marketplace', 'nombre' => 'Marketplace', 'dominio' => 'marketplace.test', @@ -46,13 +46,13 @@ class WebsiteTypeServiceTest extends TestCase $this->assertSame($siteLogo->id, $websiteType->site_logo); $this->assertSame($footerLogo->id, $websiteType->footer_logo); $this->assertSame($favicon->id, $websiteType->favicon_id); - $this->assertStringStartsWith('website-types/', $siteLogo->path); - $this->assertStringStartsWith('website-types/', $footerLogo->path); - $this->assertStringStartsWith('website-types/', $favicon->path); + $this->assertStringStartsWith('admin-website-types/', $siteLogo->path); + $this->assertStringStartsWith('admin-website-types/', $footerLogo->path); + $this->assertStringStartsWith('admin-website-types/', $favicon->path); Storage::disk('s3')->assertExists($siteLogo->path); Storage::disk('s3')->assertExists($footerLogo->path); Storage::disk('s3')->assertExists($favicon->path); - $this->assertDatabaseHas('website_type', [ + $this->assertDatabaseHas('admin_website_types', [ 'codigo' => 'marketplace', 'dominio' => 'marketplace.test', 'scanner_domain' => 'scanner.marketplace.test', @@ -74,7 +74,7 @@ class WebsiteTypeServiceTest extends TestCase { Storage::fake('s3'); - $service = app(WebsiteTypeService::class); + $service = app(AdminWebsiteTypeService::class); $shopIt = $service->create([ 'codigo' => 'shopit', 'nombre' => 'ShopIt', diff --git a/tests/Feature/Tenant/BootstrapAdminAppControllerTest.php b/tests/Feature/Tenant/BootstrapAdminAppControllerTest.php index c9fdd36..1598fdf 100644 --- a/tests/Feature/Tenant/BootstrapAdminAppControllerTest.php +++ b/tests/Feature/Tenant/BootstrapAdminAppControllerTest.php @@ -2,8 +2,8 @@ namespace Tests\Feature\Tenant; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Tenant\Models\WebsiteType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -16,7 +16,7 @@ class BootstrapAdminAppControllerTest extends TestCase $footerLogo = Attachment::factory()->create(); $favicon = Attachment::factory()->create(); - WebsiteType::query()->create([ + AdminWebsiteType::query()->create([ 'codigo' => 'shopit', 'nombre' => 'ShopIt', 'dominio' => 'admin.shopit.test', @@ -38,7 +38,7 @@ class BootstrapAdminAppControllerTest extends TestCase $this->getJson('/api/v1/adminapp/bootstrap/ADMIN.SHOPIT.TEST') ->assertOk() - ->assertJsonPath('data.website_type_code', 'shopit') + ->assertJsonPath('data.admin_website_type_code', 'shopit') ->assertJsonPath('data.site_title', 'ShopIt Website Type') ->assertJsonPath('data.primary_color', '#112233') ->assertJsonPath('data.warning_color', '#ffaa00') diff --git a/tests/Feature/Tenant/BootstrapScannerControllerTest.php b/tests/Feature/Tenant/BootstrapScannerControllerTest.php index 79ca2c8..399700d 100644 --- a/tests/Feature/Tenant/BootstrapScannerControllerTest.php +++ b/tests/Feature/Tenant/BootstrapScannerControllerTest.php @@ -2,8 +2,8 @@ namespace Tests\Feature\Tenant; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Tenant\Models\WebsiteType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -16,7 +16,7 @@ class BootstrapScannerControllerTest extends TestCase $siteLogo = Attachment::factory()->create(); $favicon = Attachment::factory()->create(); - WebsiteType::query()->create([ + AdminWebsiteType::query()->create([ 'codigo' => 'shopit', 'nombre' => 'ShopIt', 'dominio' => 'admin.shopit.test', @@ -39,7 +39,7 @@ class BootstrapScannerControllerTest extends TestCase $this->getJson('/api/v1/scanner/bootstrap/SCANNER.SHOPIT.TEST') ->assertOk() - ->assertJsonPath('data.website_type_code', 'shopit') + ->assertJsonPath('data.admin_website_type_code', 'shopit') ->assertJsonPath('data.site_title', 'ShopIt Website Type') ->assertJsonPath('data.primary_color', '#112233') ->assertJsonPath('data.site_logo', $siteLogo->getTemporaryUrl(1440)) @@ -53,7 +53,7 @@ class BootstrapScannerControllerTest extends TestCase public function test_it_does_not_match_the_admin_app_domain(): void { - WebsiteType::query()->create([ + AdminWebsiteType::query()->create([ 'codigo' => 'shopit', 'nombre' => 'ShopIt', 'dominio' => 'admin.shopit.test', diff --git a/tests/Feature/Tenant/BootstrapTenantControllerTest.php b/tests/Feature/Tenant/BootstrapTenantControllerTest.php index fe84fa8..2cb2d97 100644 --- a/tests/Feature/Tenant/BootstrapTenantControllerTest.php +++ b/tests/Feature/Tenant/BootstrapTenantControllerTest.php @@ -2,17 +2,18 @@ namespace Tests\Feature\Tenant; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Attachable\Models\AttachmentCrop; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Authorization\Models\Role; -use App\Domains\Catalog\Models\Category; -use App\Domains\Menu\Models\Menu; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Models\WebsiteType; +use App\Domains\Commerce\Catalog\Models\Category; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Core\Authorization\Models\Role; +use App\Domains\Core\Menu\Models\Menu; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; +use App\Domains\Core\Tenant\Models\StorefrontWebsiteType; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Shared\Attachable\Models\AttachmentCrop; +use Database\Seeders\WebsiteTypeSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use Tests\TestCase; @@ -21,6 +22,24 @@ class BootstrapTenantControllerTest extends TestCase { use RefreshDatabase; + public function test_multi_event_bootstrap_returns_event_categories_instead_of_catalog_categories(): void + { + $this->seed(WebsiteTypeSeeder::class); + $tenant = $this->createTenant(); + $tenant->update(['storefront_website_type_code' => 'onticket_multi_event']); + $tenant->categories()->create(['nombre' => 'Producto']); + $eventCategory = $tenant->eventCategories()->create(['nombre' => 'Música']); + + $this->getJson('/api/tenants/bootstrap?dominio=acme.com&path=%2F') + ->assertOk() + ->assertJsonMissingPath('data.categories') + ->assertJsonCount(1, 'data.event_categories') + ->assertJsonPath('data.event_categories.0.id', $eventCategory->id) + ->assertJsonPath('data.event_categories.0.nombre', 'Música') + ->assertJsonPath('data.event_categories.0.subcategories', []) + ->assertJsonPath('data.checkout_summary_type', StorefrontWebsiteType::CHECKOUT_SUMMARY_EVENT); + } + public function test_it_bootstraps_a_tenant_by_domain(): void { $hdrKey = (string) Str::uuid(); @@ -106,6 +125,7 @@ class BootstrapTenantControllerTest extends TestCase ->assertJsonPath('data.checkout_editing_policy.allow_delete', false) ->assertJsonPath('data.checkout_editing_policy.allow_update_quantity', false) ->assertJsonPath('data.checkout_editing_policy.allow_update_variant', false) + ->assertJsonPath('data.checkout_summary_type', StorefrontWebsiteType::CHECKOUT_SUMMARY_DEFAULT) ->assertJsonPath('data.display_cart_item_images', false) ->assertJsonPath('data.header_bg_color', '#ffffff')->assertJsonPath('data.footer_bg_color', '#ffffff'); @@ -217,50 +237,31 @@ class BootstrapTenantControllerTest extends TestCase $this->getJson('/api/tenants/bootstrap/acme.com')->assertNotFound(); } - public function test_it_resolves_browser_branding_from_the_tenant_before_the_website_type(): void + public function test_storefront_branding_comes_from_tenant_even_with_a_different_admin_type(): void { - $typeFavicon = Attachment::query()->create([ - 'key' => (string) Str::uuid(), - 'path' => 'website-types/type-favicon.png', - 'filename' => 'type-favicon.png', - 'type' => AttachmentType::Image, - 'mime_type' => 'image/png', + $admin = AdminWebsiteType::query()->create([ + 'codigo' => 'admin-brand', + 'nombre' => 'Admin Brand', + 'site_title' => 'Admin title', ]); - $tenantFavicon = Attachment::query()->create([ + $favicon = Attachment::query()->create([ 'key' => (string) Str::uuid(), 'path' => 'tenants/tenant-favicon.png', 'filename' => 'tenant-favicon.png', 'type' => AttachmentType::Image, 'mime_type' => 'image/png', ]); - $websiteType = WebsiteType::query()->create([ - 'codigo' => 'store', - 'nombre' => 'Tienda', - 'site_title' => 'Website type title', - 'favicon_id' => $typeFavicon->id, - ]); - $tenant = $this->createTenant([ - 'website_type_code' => $websiteType->codigo, - ]); - - $typeResponse = $this->getJson('/api/tenants/bootstrap?dominio=acme.com&path=%2F'); - - $typeResponse - ->assertOk() - ->assertJsonPath('data.site_title', 'Website type title'); - $this->assertStringContainsString($typeFavicon->key, $typeResponse->json('data.favicon')); - - $tenant->update([ + $this->createTenant([ + 'admin_website_type_code' => $admin->codigo, 'site_title' => 'Tenant title', - 'favicon_id' => $tenantFavicon->id, + 'favicon_id' => $favicon->id, ]); - $tenantResponse = $this->getJson('/api/tenants/bootstrap?dominio=acme.com&path=%2F'); - - $tenantResponse - ->assertOk() - ->assertJsonPath('data.site_title', 'Tenant title'); - $this->assertStringContainsString($tenantFavicon->key, $tenantResponse->json('data.favicon')); + $response = $this->getJson('/api/tenants/bootstrap?dominio=acme.com&path=%2F'); + $response->assertOk() + ->assertJsonPath('data.site_title', 'Tenant title') + ->assertJsonPath('data.admin_website_type_code', 'admin-brand'); + $this->assertStringContainsString($favicon->key, $response->json('data.favicon')); } public function test_it_uses_default_browser_branding_when_none_is_configured(): void @@ -315,11 +316,11 @@ class BootstrapTenantControllerTest extends TestCase public function test_it_returns_only_enabled_website_extras_in_the_bootstrap(): void { $tenant = $this->createTenant(); - $websiteType = WebsiteType::query()->create([ + $websiteType = StorefrontWebsiteType::query()->create([ 'codigo' => 'store', 'nombre' => 'Tienda', ]); - $tenant->update(['website_type_code' => $websiteType->codigo]); + $tenant->update(['storefront_website_type_code' => $websiteType->codigo]); $enabledExtra = $websiteType->extras()->create([ 'codigo' => 'contact', @@ -351,15 +352,56 @@ class BootstrapTenantControllerTest extends TestCase ->assertJsonMissingPath('data.extras.banner'); } + public function test_it_returns_signed_hero_carousel_urls_from_stored_attachment_ids(): void + { + Storage::fake('s3'); + $this->seed(WebsiteTypeSeeder::class); + + $tenant = $this->createTenant(); + $tenant->update(['storefront_website_type_code' => 'onticket_multi_event']); + $definition = StorefrontWebsiteType::query() + ->where('codigo', 'onticket_multi_event') + ->firstOrFail() + ->extras() + ->where('codigo', 'immersiveHeroCarousel') + ->firstOrFail(); + + $images = collect(['first.jpg', 'second.jpg'])->map(fn (string $filename): Attachment => Attachment::query()->create([ + 'key' => (string) Str::uuid(), + 'path' => "tenants/acme/extras/immersiveHeroCarousel/{$filename}", + 'filename' => $filename, + 'type' => AttachmentType::Image, + 'mime_type' => 'image/jpeg', + 'extension' => 'jpg', + ])); + + $extra = $tenant->websiteExtras()->create([ + 'website_type_extra_id' => $definition->id, + 'config' => $images->pluck('id')->all(), + ]); + + $this->assertSame($images->pluck('id')->all(), $extra->fresh()->config); + + $response = $this->getJson('/api/tenants/bootstrap?dominio=acme.com&path=%2F')->assertOk(); + $response->assertJsonCount(2, 'data.extras.immersiveHeroCarousel'); + + foreach ($images->values() as $index => $image) { + $response->assertJsonPath("data.extras.immersiveHeroCarousel.{$index}", function (string $url) use ($image): bool { + return str_contains($url, $image->path) + && (str_contains($url, 'Expires=') || str_contains($url, 'X-Amz-Expires=')); + }); + } + } + public function test_the_bootstrap_returns_only_the_cropped_banner_url(): void { Storage::fake('s3'); $tenant = $this->createTenant(); - $websiteType = WebsiteType::query()->create([ + $websiteType = StorefrontWebsiteType::query()->create([ 'codigo' => 'event-store', 'nombre' => 'Eventos', ]); - $tenant->update(['website_type_code' => $websiteType->codigo]); + $tenant->update(['storefront_website_type_code' => $websiteType->codigo]); $heroDefinition = $websiteType->extras()->create([ 'codigo' => 'heroConfig', 'nombre' => 'Banner', @@ -583,367 +625,6 @@ class BootstrapTenantControllerTest extends TestCase ->assertJsonPath('data.menues.0.submenues.1.code', 'tree.fox'); } - public function test_it_allows_different_domain_paths_and_rejects_duplicate_tenant_keys(): void - { - $base64Image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; - - $firstResponse = $this->postJson('/api/tenants', [ - 'codigo' => 'acme', - 'nombre' => 'Acme', - 'dominio' => 'https://ACME.com/puratendencia/', - 'primary_color' => '#111111', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => '#555555', - 'header_bg_color' => '#444444', - 'footer_bg_color' => '#444444', - 'header_logo' => $base64Image, - 'footer_logo' => $base64Image, - ]); - - $firstResponse - ->assertCreated() - ->assertJsonPath('data.dominio', 'acme.com') - ->assertJsonPath('data.base_path', '/puratendencia') - ->assertJsonPath('data.primary_color', '#111111') - ->assertJsonPath('data.secondary_color', '#222222') - ->assertJsonPath('data.danger_color', '#333333') - ->assertJsonPath('data.success_color', '#555555') - ->assertJsonPath('data.header_bg_color', '#444444')->assertJsonPath('data.footer_bg_color', '#444444'); - - $tenant = Tenant::query()->with(['headerLogo', 'footerLogo'])->where('codigo', 'acme')->firstOrFail(); - - $this->assertNotNull($tenant->header_logo_id); - $this->assertNotNull($tenant->footer_logo_id); - - $this->assertDatabaseHas('tenants', [ - 'codigo' => 'acme', - 'primary_color' => '#111111', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => '#555555', - 'header_bg_color' => '#444444', - 'footer_bg_color' => '#444444', - 'header_logo_id' => $tenant->header_logo_id, - 'footer_logo_id' => $tenant->footer_logo_id, - ]); - - $headerUrl = $firstResponse->json('data.header_logo'); - $footerUrl = $firstResponse->json('data.footer_logo'); - - $this->assertStringContainsString($tenant->headerLogo->key, $headerUrl); - $this->assertStringContainsString($tenant->footerLogo->key, $footerUrl); - $this->assertTrue( - str_contains($headerUrl, 'Expires=') || str_contains($headerUrl, 'expiration=') || str_contains($headerUrl, 'X-Amz-Expires=') - ); - $this->assertTrue( - str_contains($footerUrl, 'Expires=') || str_contains($footerUrl, 'expiration=') || str_contains($footerUrl, 'X-Amz-Expires=') - ); - - $differentPathResponse = $this->postJson('/api/tenants', [ - 'codigo' => 'pura-tendencia', - 'nombre' => 'Pura Tendencia', - 'dominio' => 'acme.com', - 'base_path' => '/sonder/', - 'primary_color' => '#111111', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => '#555555', - 'header_bg_color' => '#444444', - 'footer_bg_color' => '#444444', - 'header_logo' => $base64Image, - 'footer_logo' => $base64Image, - ]); - - $differentPathResponse - ->assertCreated() - ->assertJsonPath('data.dominio', 'acme.com') - ->assertJsonPath('data.base_path', '/sonder'); - - $secondResponse = $this->postJson('/api/tenants', [ - 'codigo' => 'globex', - 'nombre' => 'Globex', - 'dominio' => 'acme.com', - 'base_path' => '/puratendencia', - 'primary_color' => '#111111', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => '#555555', - 'header_bg_color' => '#444444', - 'footer_bg_color' => '#444444', - 'header_logo' => (string) Str::uuid(), - 'footer_logo' => (string) Str::uuid(), - ]); - - $secondResponse - ->assertUnprocessable() - ->assertJsonValidationErrors(['dominio']); - } - - public function test_it_allows_keeping_the_same_domain_on_update_but_rejects_collisions(): void - { - $tenant = $this->createTenant([ - 'codigo' => 'acme', - 'nombre' => 'Acme', - 'dominio' => 'acme.com', - ]); - - $otherTenant = $this->createTenant([ - 'codigo' => 'globex', - 'nombre' => 'Globex', - 'dominio' => 'globex.com', - ]); - - $hdrUuid = (string) Str::uuid(); - $ftrUuid = (string) Str::uuid(); - - $hdrAttachment = Attachment::create([ - 'key' => $hdrUuid, - 'path' => 'tenants/'.$hdrUuid.'.png', - 'filename' => 'hdr.png', - 'type' => AttachmentType::Image, - 'mime_type' => 'image/png', - ]); - $ftrAttachment = Attachment::create([ - 'key' => $ftrUuid, - 'path' => 'tenants/'.$ftrUuid.'.png', - 'filename' => 'ftr.png', - 'type' => AttachmentType::Image, - 'mime_type' => 'image/png', - ]); - - $successfulResponse = $this->putJson("/api/tenants/{$tenant->codigo}", [ - 'codigo' => 'acme', - 'nombre' => 'Acme Updated', - 'dominio' => 'https://ACME.com:443/', - 'primary_color' => '#555555', - 'secondary_color' => '#666666', - 'danger_color' => '#777777', - 'success_color' => '#999999', - 'header_bg_color' => '#888888', - 'footer_bg_color' => '#888888', - 'header_logo' => $hdrUuid, - 'footer_logo' => $ftrUuid, - ]); - - $successfulResponse - ->assertOk() - ->assertJsonPath('data.nombre', 'Acme Updated') - ->assertJsonPath('data.dominio', 'acme.com') - ->assertJsonPath('data.primary_color', '#555555') - ->assertJsonPath('data.secondary_color', '#666666') - ->assertJsonPath('data.danger_color', '#777777') - ->assertJsonPath('data.success_color', '#999999') - ->assertJsonPath('data.header_bg_color', '#888888')->assertJsonPath('data.footer_bg_color', '#888888'); - - $headerUrl = $successfulResponse->json('data.header_logo'); - $footerUrl = $successfulResponse->json('data.footer_logo'); - - $this->assertStringContainsString($hdrUuid, $headerUrl); - $this->assertStringContainsString($ftrUuid, $footerUrl); - $this->assertTrue( - str_contains($headerUrl, 'Expires=') || str_contains($headerUrl, 'expiration=') || str_contains($headerUrl, 'X-Amz-Expires=') - ); - $this->assertTrue( - str_contains($footerUrl, 'Expires=') || str_contains($footerUrl, 'expiration=') || str_contains($footerUrl, 'X-Amz-Expires=') - ); - - $this->assertDatabaseHas('tenants', [ - 'id' => $tenant->id, - 'primary_color' => '#555555', - 'secondary_color' => '#666666', - 'danger_color' => '#777777', - 'success_color' => '#999999', - 'header_bg_color' => '#888888', - 'footer_bg_color' => '#888888', - 'header_logo_id' => $hdrAttachment->id, - 'footer_logo_id' => $ftrAttachment->id, - ]); - - $failingResponse = $this->putJson("/api/tenants/{$otherTenant->codigo}", [ - 'codigo' => 'globex', - 'nombre' => 'Globex', - 'dominio' => 'https://ACME.com/', - ]); - - $failingResponse - ->assertUnprocessable() - ->assertJsonValidationErrors(['dominio']); - } - - public function test_it_allows_partial_update_without_required_fields(): void - { - $tenant = $this->createTenant([ - 'codigo' => 'acme', - 'nombre' => 'Acme', - 'dominio' => 'acme.com', - 'primary_color' => '#ffffff', - ]); - - $response = $this->putJson("/api/tenants/{$tenant->codigo}", [ - 'primary_color' => '#000000', - 'cart_editing_policy' => 'quantity_and_remove', - 'checkout_editing_policy' => 'disabled', - 'display_cart_item_images' => false, - ]); - - $response - ->assertOk() - ->assertJsonPath('data.codigo', 'acme') - ->assertJsonPath('data.nombre', 'Acme') - ->assertJsonPath('data.primary_color', '#000000') - ->assertJsonPath('data.cart_editing_policy.code', 'quantity_and_remove') - ->assertJsonPath('data.cart_editing_policy.allow_modify', true) - ->assertJsonPath('data.cart_editing_policy.allow_delete', true) - ->assertJsonPath('data.cart_editing_policy.allow_update_quantity', true) - ->assertJsonPath('data.cart_editing_policy.allow_update_variant', false) - ->assertJsonPath('data.checkout_editing_policy.code', 'disabled') - ->assertJsonPath('data.checkout_editing_policy.allow_modify', false) - ->assertJsonPath('data.display_cart_item_images', false); - - $this->assertDatabaseHas('tenants', [ - 'id' => $tenant->id, - 'codigo' => 'acme', - 'nombre' => 'Acme', - 'primary_color' => '#000000', - 'cart_editing_policy' => 'quantity_and_remove', - 'checkout_editing_policy' => 'disabled', - 'display_cart_item_images' => false, - ]); - } - - public function test_it_validates_aesthetic_colors(): void - { - $response = $this->postJson('/api/tenants', [ - 'codigo' => 'acme', - 'nombre' => 'Acme', - 'dominio' => 'acme.com', - 'primary_color' => 'invalid-color', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => '#555555', - 'header_bg_color' => '#444444', - 'footer_bg_color' => '#444444', - 'header_logo' => (string) Str::uuid(), - 'footer_logo' => (string) Str::uuid(), - ]); - - $response->assertJsonValidationErrors(['primary_color']); - - $response2 = $this->postJson('/api/tenants', [ - 'codigo' => 'acme', - 'nombre' => 'Acme', - 'dominio' => 'acme.com', - 'primary_color' => '#12345', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => '#555555', - 'header_bg_color' => '#444444', - 'footer_bg_color' => '#444444', - 'header_logo' => (string) Str::uuid(), - 'footer_logo' => (string) Str::uuid(), - ]); - - $response2->assertJsonValidationErrors(['primary_color']); - - $response3 = $this->postJson('/api/tenants', [ - 'codigo' => 'acme', - 'nombre' => 'Acme', - 'dominio' => 'acme.com', - 'primary_color' => '#111111', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => 'invalid-color', - 'header_bg_color' => '#444444', - 'footer_bg_color' => '#444444', - 'header_logo' => (string) Str::uuid(), - 'footer_logo' => (string) Str::uuid(), - ]); - - $response3->assertJsonValidationErrors(['success_color']); - } - - public function test_it_validates_logo_must_be_image_or_svg(): void - { - Storage::fake('s3'); - - $response = $this->postJson('/api/tenants', [ - 'codigo' => 'acme', - 'nombre' => 'Acme', - 'dominio' => 'acme.com', - 'primary_color' => '#111111', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => '#555555', - 'header_bg_color' => '#444444', - 'footer_bg_color' => '#444444', - 'header_logo' => UploadedFile::fake()->create('document.pdf', 10, 'application/pdf'), - 'footer_logo' => (string) Str::uuid(), - ]); - - $response->assertJsonValidationErrors(['header_logo']); - - $response2 = $this->postJson('/api/tenants', [ - 'codigo' => 'acme', - 'nombre' => 'Acme', - 'dominio' => 'acme.com', - 'primary_color' => '#111111', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => '#555555', - 'header_bg_color' => '#444444', - 'footer_bg_color' => '#444444', - 'header_logo' => 'data:application/pdf;base64,JVBERi0xLjQKJdcfqksKMSAwIG9iagogIDw8IC9UeXBlIC9DYXRhbG9nCiAgICAvUGFnZXMgMiAwIFI...', - 'footer_logo' => (string) Str::uuid(), - ]); - - $response2->assertJsonValidationErrors(['header_logo']); - } - - public function test_it_stores_uploaded_file_logos_in_tenants_directory(): void - { - Storage::fake('s3'); - - $header = UploadedFile::fake()->image('header.png'); - $footer = UploadedFile::fake()->image('footer.svg', 100, 100); - - $response = $this->postJson('/api/tenants', [ - 'codigo' => 'acme', - 'nombre' => 'Acme', - 'dominio' => 'acme.com', - 'primary_color' => '#111111', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => '#555555', - 'header_bg_color' => '#444444', - 'footer_bg_color' => '#444444', - 'header_logo' => $header, - 'footer_logo' => $footer, - ]); - - $response->assertCreated(); - - $tenant = Tenant::query()->with(['headerLogo', 'footerLogo'])->where('codigo', 'acme')->firstOrFail(); - - $this->assertNotNull($tenant->header_logo_id); - $this->assertNotNull($tenant->footer_logo_id); - - $headerUrl = $response->json('data.header_logo'); - $footerUrl = $response->json('data.footer_logo'); - - $this->assertStringContainsString($tenant->headerLogo->key, $headerUrl); - $this->assertStringContainsString($tenant->footerLogo->key, $footerUrl); - $this->assertTrue( - str_contains($headerUrl, 'Expires=') || str_contains($headerUrl, 'expiration=') || str_contains($headerUrl, 'X-Amz-Expires=') - ); - $this->assertTrue( - str_contains($footerUrl, 'Expires=') || str_contains($footerUrl, 'expiration=') || str_contains($footerUrl, 'X-Amz-Expires=') - ); - - $this->assertDatabaseHas('attachments', ['key' => $tenant->headerLogo->key]); - $this->assertDatabaseHas('attachments', ['key' => $tenant->footerLogo->key]); - } - private function createTenant(array $attributes = []): Tenant { $hdrKey = (string) Str::uuid(); @@ -978,56 +659,4 @@ class BootstrapTenantControllerTest extends TestCase 'footer_logo_id' => $footerAttachment->id, ], $attributes)); } - - public function test_it_stores_base64_logos_in_tenants_directory(): void - { - Storage::fake('s3'); - - $base64Image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; - - $ftrUuid = (string) Str::uuid(); - $footerAttachment = Attachment::create([ - 'key' => $ftrUuid, - 'path' => 'tenants/'.$ftrUuid.'.png', - 'filename' => 'logo_footer.png', - 'type' => AttachmentType::Image, - 'mime_type' => 'image/png', - ]); - - $response = $this->postJson('/api/tenants', [ - 'codigo' => 'acme', - 'nombre' => 'Acme', - 'dominio' => 'acme.com', - 'primary_color' => '#111111', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => '#555555', - 'header_bg_color' => '#444444', - 'footer_bg_color' => '#444444', - 'header_logo' => $base64Image, - 'footer_logo' => $ftrUuid, - 'site_title' => 'Acme Store', - 'favicon' => $base64Image, - ]); - - $response->assertCreated(); - - $tenant = Tenant::query()->with(['headerLogo', 'favicon'])->where('codigo', 'acme')->firstOrFail(); - - $this->assertNotNull($tenant->header_logo_id); - $this->assertNotNull($tenant->favicon_id); - - $headerUrl = $response->json('data.header_logo'); - $faviconUrl = $response->json('data.favicon'); - - $response->assertJsonPath('data.site_title', 'Acme Store'); - $this->assertStringContainsString($tenant->headerLogo->key, $headerUrl); - $this->assertStringContainsString($tenant->favicon->key, $faviconUrl); - $this->assertTrue( - str_contains($headerUrl, 'Expires=') || str_contains($headerUrl, 'expiration=') || str_contains($headerUrl, 'X-Amz-Expires=') - ); - - $this->assertDatabaseHas('attachments', ['key' => $tenant->headerLogo->key]); - $this->assertDatabaseHas('attachments', ['key' => $tenant->favicon->key]); - } } diff --git a/tests/Feature/Tenant/StoreTenantWithExtrasTest.php b/tests/Feature/Tenant/StoreTenantWithExtrasTest.php deleted file mode 100644 index 6e9ca4e..0000000 --- a/tests/Feature/Tenant/StoreTenantWithExtrasTest.php +++ /dev/null @@ -1,218 +0,0 @@ -seed(WebsiteTypeSeeder::class); - } - - public function test_it_creates_a_tenant_and_validates_and_stores_its_website_extras(): void - { - $response = $this->postJson('/api/tenants', array_merge($this->tenantData(), [ - 'website_type_code' => 'onticket', - 'extras' => [ - 'eventConfig' => [ - 'title' => 'Festival', - 'location' => 'Buenos Aires', - 'dates_text' => '10 y 11 de octubre de 2026', - 'dates' => [ - [ - 'date' => '2026-10-10', - 'start_time' => '09:00', - 'end_time' => '18:00', - ], - [ - 'date' => '2026-10-11', - 'start_time' => '10:00', - 'end_time' => '17:00', - ], - ], - ], - ], - ])); - - $response - ->assertCreated() - ->assertJsonPath('data.website_type_code', 'onticket') - ->assertJsonMissingPath('data.website_type') - ->assertJsonPath('data.extras.eventConfig.title', 'Festival'); - - $tenant = Tenant::query()->where('codigo', 'festival')->sole(); - - $this->assertDatabaseHas('websites_extras', [ - 'website_code' => $tenant->codigo, - ]); - $this->assertSame( - [ - 'title' => 'Festival', - 'location' => 'Buenos Aires', - 'dates_text' => '10 y 11 de octubre de 2026', - 'dates' => [ - [ - 'date' => '2026-10-10', - 'start_time' => '09:00', - 'end_time' => '18:00', - ], - [ - 'date' => '2026-10-11', - 'start_time' => '10:00', - 'end_time' => '17:00', - ], - ], - ], - $tenant->websiteExtras()->sole()->config - ); - } - - public function test_it_rejects_an_extra_not_supported_by_the_selected_website_type(): void - { - $this->postJson('/api/tenants', array_merge($this->tenantData(), [ - 'website_type_code' => 'shopit', - 'extras' => [ - 'eventConfig' => [ - 'title' => 'Not supported', - ], - ], - ])) - ->assertUnprocessable() - ->assertJsonValidationErrors(['extras']); - - $this->assertDatabaseMissing('tenants', ['codigo' => 'festival']); - } - - public function test_it_applies_the_extra_schema_rules(): void - { - $this->postJson('/api/tenants', array_merge($this->tenantData(), [ - 'website_type_code' => 'onticket', - 'extras' => [ - 'eventConfig' => [ - 'dates' => [[ - 'date' => 'not-a-date', - 'start_time' => '09:00', - 'end_time' => '18:00', - ]], - ], - ], - ])) - ->assertUnprocessable() - ->assertJsonValidationErrors(['extras.eventConfig.dates.0.date']); - - $this->assertDatabaseMissing('tenants', ['codigo' => 'festival']); - } - - public function test_it_transforms_extra_images_to_attachment_ids(): void - { - $image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; - - $response = $this->postJson('/api/tenants', array_merge($this->tenantData(), [ - 'website_type_code' => 'shopit', - 'extras' => [ - 'carousel' => [$image], - ], - ])); - - $response->assertCreated(); - - $tenant = Tenant::query() - ->where('codigo', 'festival') - ->sole(); - $config = $tenant->websiteExtras() - ->sole() - ->config; - - $this->assertCount(1, $config); - $this->assertIsInt($config[0]); - $this->assertDatabaseHas('attachments', [ - 'id' => $config[0], - 'type' => 'image', - ]); - - $attachment = Attachment::query()->findOrFail($config[0]); - $carouselUrl = $response->json('data.extras.carousel.0'); - - $this->assertIsString($carouselUrl); - $this->assertStringContainsString($attachment->key, $carouselUrl); - - app(TenantInformationService::class)->load($tenant); - - $this->assertInstanceOf( - Attachment::class, - $tenant->websiteExtras->sole()->resolvedConfig()[0] - ); - } - - public function test_it_returns_scalar_attachment_fields_as_temporary_urls(): void - { - $image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; - - $response = $this->postJson('/api/tenants', array_merge($this->tenantData(), [ - 'website_type_code' => 'onticket', - 'extras' => [ - 'heroConfig' => [ - 'title_html' => '

Festival

', - 'background_image_id' => $image, - ], - ], - ])); - - $response->assertCreated(); - - $heroConfig = Tenant::query() - ->where('codigo', 'festival') - ->sole() - ->websiteExtras() - ->whereHas( - 'websiteTypeExtra', - fn ($query) => $query->where('codigo', 'heroConfig') - ) - ->sole() - ->config; - - $attachment = Attachment::query()->findOrFail($heroConfig['background_image_id']); - $backgroundUrl = $response->json('data.extras.heroConfig.background_image_id'); - - $this->assertIsString($backgroundUrl); - $this->assertStringContainsString($attachment->key, $backgroundUrl); - } - - /** - * @return array - */ - private function tenantData(): array - { - $image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; - - return [ - 'codigo' => 'festival', - 'nombre' => 'Festival', - 'dominio' => 'festival.test', - 'primary_color' => '#111111', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => '#444444', - 'header_bg_color' => '#ffffff', - 'footer_bg_color' => '#ffffff', - 'header_logo' => $image, - 'footer_logo' => $image, - 'search_product_layout' => 'column_with_image', - 'search_group_layout' => 'paginated', - 'search_items_per_page' => 12, - ]; - } -} diff --git a/tests/Feature/Tenant/TenantBootstrapRouteTest.php b/tests/Feature/Tenant/TenantBootstrapRouteTest.php index 2faaebe..424acad 100644 --- a/tests/Feature/Tenant/TenantBootstrapRouteTest.php +++ b/tests/Feature/Tenant/TenantBootstrapRouteTest.php @@ -2,8 +2,9 @@ namespace Tests\Feature\Tenant; -use App\Domains\Bootstrap\Controllers\TenantBootstrapController; +use App\Domains\Core\Bootstrap\Controllers\TenantBootstrapController; use Illuminate\Http\Request; +use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; use Tests\TestCase; class TenantBootstrapRouteTest extends TestCase @@ -16,4 +17,24 @@ class TenantBootstrapRouteTest extends TestCase $this->assertSame(TenantBootstrapController::class, $route->getActionName()); } + + public function test_tenant_creation_and_update_routes_are_not_available(): void + { + $routes = app('router')->getRoutes(); + $this->assertNull($routes->getByName('tenants.store')); + $this->assertNull($routes->getByName('tenants.update')); + + foreach ([['POST', '/api/tenants'], ['PUT', '/api/tenants/demo'], ['PATCH', '/api/tenants/demo']] as [$method, $uri]) { + try { + $routes->match(Request::create($uri, $method)); + $this->fail("Unexpected route: {$method} {$uri}"); + } catch (MethodNotAllowedHttpException $exception) { + $this->assertSame(405, $exception->getStatusCode()); + } + } + + foreach (['tenants.index', 'tenants.show', 'tenants.destroy'] as $name) { + $this->assertNotNull($routes->getByName($name)); + } + } } diff --git a/tests/Feature/Tenant/TenantRefundConfigurationTest.php b/tests/Feature/Tenant/TenantRefundConfigurationTest.php index 6091bbb..1bf3a41 100644 --- a/tests/Feature/Tenant/TenantRefundConfigurationTest.php +++ b/tests/Feature/Tenant/TenantRefundConfigurationTest.php @@ -2,9 +2,9 @@ namespace Tests\Feature\Tenant; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; @@ -31,41 +31,6 @@ class TenantRefundConfigurationTest extends TestCase ->assertJsonPath('data.ticket_partial_refund_percentage', '0.00'); } - public function test_refund_configuration_can_be_updated_and_validates_its_precision(): void - { - $tenant = $this->createTenant('refund-update'); - - $this->putJson("/api/tenants/{$tenant->codigo}", [ - 'allow_ticket_refund' => true, - 'allow_ticket_total_refund' => true, - 'allow_ticket_partial_refund' => true, - 'ticket_partial_refund_percentage' => 25.50, - ]) - ->assertOk() - ->assertJsonPath('data.allow_ticket_refund', true) - ->assertJsonPath('data.allow_ticket_total_refund', true) - ->assertJsonPath('data.allow_ticket_partial_refund', true) - ->assertJsonPath('data.ticket_partial_refund_percentage', '25.50'); - - $this->assertDatabaseHas('tenants', [ - 'id' => $tenant->id, - 'allow_ticket_refund' => true, - 'allow_ticket_total_refund' => true, - 'allow_ticket_partial_refund' => true, - 'ticket_partial_refund_percentage' => 25.50, - ]); - - $this->putJson("/api/tenants/{$tenant->codigo}", [ - 'ticket_partial_refund_percentage' => 100, - ])->assertUnprocessable() - ->assertJsonValidationErrors('ticket_partial_refund_percentage'); - - $this->putJson("/api/tenants/{$tenant->codigo}", [ - 'ticket_partial_refund_percentage' => 12.345, - ])->assertUnprocessable() - ->assertJsonValidationErrors('ticket_partial_refund_percentage'); - } - public function test_tenant_allow_refund_logic(): void { $tenant = new Tenant([ diff --git a/tests/Feature/Tenant/TenantSocialMediaTest.php b/tests/Feature/Tenant/TenantSocialMediaTest.php index 0901eb9..1ba1405 100644 --- a/tests/Feature/Tenant/TenantSocialMediaTest.php +++ b/tests/Feature/Tenant/TenantSocialMediaTest.php @@ -2,10 +2,10 @@ namespace Tests\Feature\Tenant; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Tenant\Models\SocialMedia; -use App\Domains\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\SocialMedia; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Schema; use Tests\TestCase; @@ -79,64 +79,18 @@ class TenantSocialMediaTest extends TestCase ]); } - public function test_tenant_update_synchronizes_social_media_and_returns_pivot_urls(): void - { - $tenant = $this->createTenant(); - $instagram = $this->createSocialMedia('instagram', 'Instagram'); - $facebook = $this->createSocialMedia('facebook', 'Facebook'); - - $tenant->socialMedia()->attach($facebook->code, [ - 'url' => 'https://facebook.com/old-acme', - ]); - - $this->putJson("/api/tenants/{$tenant->codigo}", [ - 'social_media' => [ - [ - 'code' => $instagram->code, - 'url' => 'https://instagram.com/acme', - 'orden' => 4, - ], - ], - ]) - ->assertOk() - ->assertJsonPath('data.social_media.0.code', 'instagram') - ->assertJsonPath('data.social_media.0.icon', 'instagram') - ->assertJsonPath('data.social_media.0.name', 'Instagram') - ->assertJsonPath('data.social_media.0.url', 'https://instagram.com/acme') - ->assertJsonCount(1, 'data.social_media'); - - $this->assertDatabaseHas('tenant_social_media', [ - 'tenant_code' => $tenant->codigo, - 'social_media_code' => $instagram->code, - 'url' => 'https://instagram.com/acme', - 'orden' => 4, - ]); - $this->assertDatabaseMissing('tenant_social_media', [ - 'tenant_code' => $tenant->codigo, - 'social_media_code' => $facebook->code, - ]); - } - public function test_tenant_social_media_are_returned_in_configured_order(): void { $tenant = $this->createTenant(); $instagram = $this->createSocialMedia('instagram', 'Instagram'); $facebook = $this->createSocialMedia('facebook', 'Facebook'); - $this->putJson("/api/tenants/{$tenant->codigo}", [ - 'social_media' => [ - [ - 'code' => $instagram->code, - 'url' => 'https://instagram.com/acme', - 'orden' => 20, - ], - [ - 'code' => $facebook->code, - 'url' => 'https://facebook.com/acme', - 'orden' => 10, - ], - ], - ]) + $tenant->socialMedia()->sync([ + $instagram->code => ['url' => 'https://instagram.com/acme', 'orden' => 20], + $facebook->code => ['url' => 'https://facebook.com/acme', 'orden' => 10], + ]); + + $this->getJson("/api/tenants/{$tenant->codigo}") ->assertOk() ->assertJsonPath('data.social_media.0.code', 'facebook') ->assertJsonPath('data.social_media.1.code', 'instagram') @@ -144,80 +98,6 @@ class TenantSocialMediaTest extends TestCase ->assertJsonMissingPath('data.social_media.1.orden'); } - public function test_tenant_store_synchronizes_social_media(): void - { - $instagram = $this->createSocialMedia('instagram', 'Instagram'); - $headerLogo = $this->createAttachment('new-header.png'); - $footerLogo = $this->createAttachment('new-footer.png'); - - $this->postJson('/api/tenants', [ - 'codigo' => 'new-acme', - 'nombre' => 'New Acme', - 'dominio' => 'new-acme.com', - 'primary_color' => '#111111', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => '#444444', - 'header_bg_color' => '#ffffff', - 'footer_bg_color' => '#ffffff', - 'header_logo' => $headerLogo->key, - 'footer_logo' => $footerLogo->key, - 'social_media' => [ - [ - 'code' => $instagram->code, - 'url' => 'https://instagram.com/new-acme', - ], - ], - ]) - ->assertCreated() - ->assertJsonPath('data.social_media.0.code', 'instagram') - ->assertJsonPath('data.social_media.0.url', 'https://instagram.com/new-acme'); - - $this->assertDatabaseHas('tenant_social_media', [ - 'tenant_code' => 'new-acme', - 'social_media_code' => $instagram->code, - 'url' => 'https://instagram.com/new-acme', - ]); - } - - public function test_tenant_update_can_clear_social_media(): void - { - $tenant = $this->createTenant(); - $instagram = $this->createSocialMedia('instagram', 'Instagram'); - $tenant->socialMedia()->attach($instagram->code, [ - 'url' => 'https://instagram.com/acme', - ]); - - $this->putJson("/api/tenants/{$tenant->codigo}", [ - 'social_media' => [], - ]) - ->assertOk() - ->assertJsonPath('data.social_media', []); - - $this->assertDatabaseMissing('tenant_social_media', [ - 'tenant_code' => $tenant->codigo, - ]); - } - - public function test_tenant_update_validates_social_media_codes_and_urls(): void - { - $tenant = $this->createTenant(); - - $this->putJson("/api/tenants/{$tenant->codigo}", [ - 'social_media' => [ - [ - 'code' => 'unknown', - 'url' => 'not-a-url', - ], - ], - ]) - ->assertUnprocessable() - ->assertJsonValidationErrors([ - 'social_media.0.code', - 'social_media.0.url', - ]); - } - private function createTenant(): Tenant { $headerLogo = $this->createAttachment('header.png'); diff --git a/tests/Feature/Tenant/TenantTimezoneTest.php b/tests/Feature/Tenant/TenantTimezoneTest.php new file mode 100644 index 0000000..d01a432 --- /dev/null +++ b/tests/Feature/Tenant/TenantTimezoneTest.php @@ -0,0 +1,76 @@ +createTenant('timezone'); + $this->getJson("/api/tenants/{$tenant->codigo}")->assertOk() + ->assertJsonPath('data.timezone', 'America/Argentina/Buenos_Aires'); + + } + + public function test_event_stores_utc_and_timezone_changes_preserve_stored_instants(): void + { + $tenant = $this->createTenant('event-zone'); + $date = $tenant->eventDates()->create([ + 'date' => '2026-09-21', 'time_start' => '22:00', 'time_end' => '02:00', + ]); + $this->assertDatabaseHas('validity_times', [ + 'id' => $date->validity_time_id, + 'fixed_starts_at' => '2026-09-22 01:00:00', + 'fixed_expires_at' => '2026-09-22 05:00:00', + ]); + $tenant->update(['timezone' => 'Asia/Tokyo']); + $this->assertSame('2026-09-22 01:00:00', $date->validityTime->fresh()->fixed_starts_at->format('Y-m-d H:i:s')); + } + + private function createTenant(string $code): Tenant + { + $attachmentIds = collect(['header', 'footer'])->map(function (string $name): int { + $key = (string) Str::uuid(); + + return Attachment::query()->create([ + 'key' => $key, + 'path' => "tenants/{$key}.png", + 'filename' => "{$name}.png", + 'type' => AttachmentType::Image, + 'mime_type' => 'image/png', + ])->id; + }); + + $clientId = DB::table('clients')->insertGetId([ + 'code' => $code, + 'name' => Str::headline($code), + ]); + + $tenantId = DB::table('tenants')->insertGetId([ + 'client_id' => $clientId, + 'codigo' => $code, + 'nombre' => Str::headline($code), + 'dominio' => "{$code}.test", + 'primary_color' => '#000000', + 'secondary_color' => '#000000', + 'danger_color' => '#000000', + 'success_color' => '#000000', + 'header_bg_color' => '#ffffff', + 'footer_bg_color' => '#ffffff', + 'header_logo_id' => $attachmentIds[0], + 'footer_logo_id' => $attachmentIds[1], + ]); + + return Tenant::query()->findOrFail($tenantId); + } +} diff --git a/tests/Feature/Tenant/WebsiteExtrasTest.php b/tests/Feature/Tenant/WebsiteExtrasTest.php index 2ab0c6b..755eece 100644 --- a/tests/Feature/Tenant/WebsiteExtrasTest.php +++ b/tests/Feature/Tenant/WebsiteExtrasTest.php @@ -2,10 +2,11 @@ namespace Tests\Feature\Tenant; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Models\WebsiteType; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; +use App\Domains\Core\Tenant\Models\StorefrontWebsiteType; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Schema; use Tests\TestCase; @@ -16,32 +17,15 @@ class WebsiteExtrasTest extends TestCase public function test_website_extra_tables_have_the_expected_columns(): void { - $this->assertTrue(Schema::hasColumn('tenants', 'website_type_code')); + $this->assertTrue(Schema::hasColumn('tenants', 'storefront_website_type_code')); + + $this->assertTrue(Schema::hasColumn('tenants', 'admin_website_type_code')); + $this->assertEqualsCanonicalizing(['id', 'codigo', 'nombre', 'created_at', 'updated_at'], Schema::getColumnListing('storefront_website_types')); + $this->assertTrue(Schema::hasColumn('admin_website_types', 'dominio')); $this->assertEqualsCanonicalizing([ 'id', - 'codigo', - 'nombre', - 'dominio', - 'primary_color', - 'secondary_color', - 'danger_color', - 'success_color', - 'warning_color', - 'body_color', - 'darker_body_color', - 'surface_color', - 'background_color', - 'border_color', - 'login_header_footer_color', - 'site_logo', - 'created_at', - 'updated_at', - ], Schema::getColumnListing('website_type')); - - $this->assertEqualsCanonicalizing([ - 'id', - 'website_type_code', + 'storefront_website_type_code', 'codigo', 'nombre', 'descripcion', @@ -49,7 +33,7 @@ class WebsiteExtrasTest extends TestCase 'config_schema', 'created_at', 'updated_at', - ], Schema::getColumnListing('website_type_extras')); + ], Schema::getColumnListing('storefront_website_type_extras')); $this->assertEqualsCanonicalizing([ 'id', @@ -62,85 +46,45 @@ class WebsiteExtrasTest extends TestCase ], Schema::getColumnListing('websites_extras')); } - public function test_models_map_the_diagram_relations_and_casts(): void + public function test_models_keep_admin_branding_and_storefront_extras_independent(): void { - $siteLogo = Attachment::query()->create([ - 'key' => '92bb3986-fb22-4871-b98c-fc5663d78aa6', - 'path' => 'website-types/site-logo.png', - 'filename' => 'site-logo.png', - 'type' => AttachmentType::Image, - 'mime_type' => 'image/png', + $admin = AdminWebsiteType::query()->create([ + 'codigo' => 'admin-brand', + 'nombre' => 'Admin Brand', + 'dominio' => 'admin.test', ]); - $type = WebsiteType::query()->create([ + $storefront = StorefrontWebsiteType::query()->create([ 'codigo' => 'store', 'nombre' => 'Tienda', - 'dominio' => 'store.test', - 'primary_color' => '#111111', - 'secondary_color' => '#222222', - 'danger_color' => '#ff0000', - 'success_color' => '#00aa55', - 'warning_color' => '#ffaa00', - 'body_color' => '#666666', - 'darker_body_color' => '#333333', - 'surface_color' => '#ffffff', - 'background_color' => '#f8f8f8', - 'border_color' => '#dddddd', - 'login_header_footer_color' => '#333333', - 'site_logo' => $siteLogo->id, ]); - $typeExtra = $type->extras()->create([ + $definition = $storefront->extras()->create([ 'codigo' => 'whatsapp', 'nombre' => 'WhatsApp', - 'descripcion' => 'Configuracion del canal de WhatsApp', + 'descripcion' => 'Canal de WhatsApp', 'is_required' => true, - 'config_schema' => [ - 'type' => 'object', - 'required' => ['phone'], - ], + 'config_schema' => ['type' => 'object'], ]); $tenant = $this->createTenant(); - $tenant->websiteType()->associate($type)->save(); - $websiteExtra = $tenant->websiteExtras()->create([ - 'website_type_extra_id' => $typeExtra->id, + $tenant->adminWebsiteType()->associate($admin); + $tenant->storefrontWebsiteType()->associate($storefront); + $tenant->save(); + $value = $tenant->websiteExtras()->create([ + 'website_type_extra_id' => $definition->id, 'config' => ['phone' => '+5491112345678'], 'is_enabled' => true, ]); - $this->assertTrue($type->extras()->firstOrFail()->is($typeExtra)); - $this->assertSame('store.test', $type->dominio); - $this->assertSame('#111111', $type->primary_color); - $this->assertSame('#222222', $type->secondary_color); - $this->assertSame('#ff0000', $type->danger_color); - $this->assertSame('#00aa55', $type->success_color); - $this->assertSame('#ffaa00', $type->warning_color); - $this->assertSame('#666666', $type->body_color); - $this->assertSame('#333333', $type->darker_body_color); - $this->assertSame('#ffffff', $type->surface_color); - $this->assertSame('#f8f8f8', $type->background_color); - $this->assertSame('#dddddd', $type->border_color); - $this->assertSame('#333333', $type->login_header_footer_color); - $this->assertTrue($type->siteLogo()->firstOrFail()->is($siteLogo)); - $this->assertSame($type->codigo, $typeExtra->website_type_code); - $this->assertSame('whatsapp', $typeExtra->codigo); - $this->assertTrue($type->tenants()->firstOrFail()->is($tenant)); - $this->assertTrue($tenant->websiteType()->firstOrFail()->is($type)); - $this->assertSame($type->codigo, $tenant->website_type_code); - $this->assertTrue($typeExtra->websiteType()->firstOrFail()->is($type)); - $this->assertTrue($typeExtra->websiteExtras()->firstOrFail()->is($websiteExtra)); - $this->assertTrue($websiteExtra->website()->firstOrFail()->is($tenant)); - $this->assertTrue($websiteExtra->websiteTypeExtra()->firstOrFail()->is($typeExtra)); - $this->assertTrue($typeExtra->is_required); - $this->assertSame([ - 'type' => 'object', - 'required' => ['phone'], - ], $typeExtra->config_schema); - $this->assertSame(['phone' => '+5491112345678'], $websiteExtra->config); - $this->assertTrue($websiteExtra->is_enabled); + $this->assertTrue($tenant->adminWebsiteType->is($admin)); + $this->assertTrue($tenant->storefrontWebsiteType->is($storefront)); + $this->assertTrue($definition->storefrontWebsiteType->is($storefront)); + $this->assertTrue($definition->websiteExtras()->firstOrFail()->is($value)); + $this->assertSame('store', $tenant->storefront_website_type_code); + $this->assertSame('admin-brand', $tenant->admin_website_type_code); } public function test_deleting_a_type_cascades_its_definitions_and_website_values(): void { - $type = WebsiteType::query()->create([ + $type = StorefrontWebsiteType::query()->create([ 'codigo' => 'store', 'nombre' => 'Tienda', ]); @@ -157,7 +101,7 @@ class WebsiteExtrasTest extends TestCase $type->delete(); - $this->assertDatabaseMissing('website_type_extras', ['id' => $typeExtra->id]); + $this->assertDatabaseMissing('storefront_website_type_extras', ['id' => $typeExtra->id]); $this->assertDatabaseMissing('websites_extras', ['id' => $websiteExtra->id]); } diff --git a/tests/Feature/Ticket/AdminAppTicketControllerTest.php b/tests/Feature/Ticket/AdminAppTicketControllerTest.php index cba569f..b97b041 100644 --- a/tests/Feature/Ticket/AdminAppTicketControllerTest.php +++ b/tests/Feature/Ticket/AdminAppTicketControllerTest.php @@ -2,22 +2,22 @@ namespace Tests\Feature\Ticket; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Catalog\Models\Attribute; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Inventory; -use App\Domains\Catalog\Models\Variant; -use App\Domains\Menu\Models\Menu; -use App\Domains\Purchase\Models\Purchase; -use App\Domains\Purchase\Models\PurchaseItem; -use App\Domains\Shared\Enums\FieldType; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Models\WebsiteType; -use App\Domains\Ticket\Models\Ticket; -use App\Domains\Ticket\Models\TicketRefund; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Commerce\Catalog\Models\Attribute; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Inventory; +use App\Domains\Commerce\Catalog\Models\Variant; +use App\Domains\Core\Menu\Models\Menu; +use App\Domains\Commerce\Purchase\Models\Purchase; +use App\Domains\Commerce\Purchase\Models\PurchaseItem; +use App\Shared\Enums\FieldType; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; +use App\Domains\Ticketing\Ticket\Models\Ticket; +use App\Domains\Ticketing\Ticket\Models\TicketRefund; use Barryvdh\DomPDF\ServiceProvider as DomPdfServiceProvider; use Database\Seeders\AttributeSeeder; use Database\Seeders\AuthorizationSeeder; @@ -39,7 +39,7 @@ class AdminAppTicketControllerTest extends TestCase $this->app->register(DomPdfServiceProvider::class); $this->seed(AuthorizationSeeder::class); - WebsiteType::query()->create(['codigo' => 'onticket', 'nombre' => 'OnTicket']); + AdminWebsiteType::query()->create(['codigo' => 'onticket', 'nombre' => 'OnTicket']); } public function test_authentication_is_required(): void @@ -1106,7 +1106,7 @@ class AdminAppTicketControllerTest extends TestCase 'codigo' => $code, 'nombre' => ucfirst($code), 'dominio' => "{$code}.test", - 'website_type_code' => 'onticket', + 'admin_website_type_code' => 'onticket', 'primary_color' => '#111111', 'secondary_color' => '#222222', 'danger_color' => '#333333', diff --git a/tests/Feature/Ticket/ConvertValidityTimesToUtcTest.php b/tests/Feature/Ticket/ConvertValidityTimesToUtcTest.php new file mode 100644 index 0000000..4cc3ec0 --- /dev/null +++ b/tests/Feature/Ticket/ConvertValidityTimesToUtcTest.php @@ -0,0 +1,75 @@ +id(); + $table->string('type'); + $table->time('start_time')->nullable(); + $table->time('end_time')->nullable(); + $table->dateTime('fixed_starts_at')->nullable(); + $table->dateTime('fixed_expires_at')->nullable(); + }); + DB::table('validity_times')->insert([ + ['id' => 1, 'type' => 'fixed_window', 'fixed_starts_at' => '2026-09-21 07:00:00', 'fixed_expires_at' => '2026-09-23 23:59:00'], + ['id' => 2, 'type' => 'fixed_window', 'fixed_starts_at' => null, 'fixed_expires_at' => '2026-09-24 23:59:00'], + ]); + DB::table('validity_times')->insert(['id' => 3, 'type' => 'time_window', 'start_time' => '07:00:00', 'end_time' => '10:00:00']); + } + + public function test_all_round_trip_restores_exact_values_across_dates_and_nulls(): void + { + $before = DB::table('validity_times')->orderBy('id')->get()->toJson(); + $this->artisan('tickets:convert-validity-times-to-utc', ['--all' => true, '--apply' => true])->assertSuccessful(); + $this->assertDatabaseHas('validity_times', ['id' => 1, 'fixed_starts_at' => '2026-09-21 10:00:00', 'fixed_expires_at' => '2026-09-24 02:59:00']); + $this->artisan('tickets:convert-validity-times-to-utc', ['--all' => true, '--reverse' => true, '--apply' => true])->assertSuccessful(); + $this->assertSame($before, DB::table('validity_times')->orderBy('id')->get()->toJson()); + } + + public function test_reverse_preview_shows_local_result_without_writing(): void + { + $before = DB::table('validity_times')->orderBy('id')->get()->toJson(); + $this->artisan('tickets:convert-validity-times-to-utc', ['--all' => true, '--reverse' => true]) + ->expectsOutputToContain('UTC -> America/Argentina/Buenos_Aires') + ->expectsTable(['ID', 'Inicio original', 'Fin original', 'Inicio resultante', 'Fin resultante'], [ + [1, '2026-09-21 07:00:00', '2026-09-23 23:59:00', '2026-09-21 04:00:00', '2026-09-23 20:59:00'], + [2, null, '2026-09-24 23:59:00', null, '2026-09-24 20:59:00'], + ])->assertSuccessful(); + $this->assertSame($before, DB::table('validity_times')->orderBy('id')->get()->toJson()); + } + + public function test_reverse_by_ids_uses_selected_timezone_and_preserves_other_rows(): void + { + $this->artisan('tickets:convert-validity-times-to-utc', ['--ids' => '1', '--reverse' => true, '--source-timezone' => 'Asia/Tokyo', '--apply' => true])->assertSuccessful(); + $this->assertDatabaseHas('validity_times', ['id' => 1, 'fixed_starts_at' => '2026-09-21 16:00:00', 'fixed_expires_at' => '2026-09-24 08:59:00']); + $this->assertDatabaseHas('validity_times', ['id' => 2, 'fixed_expires_at' => '2026-09-24 23:59:00']); + $this->assertDatabaseHas('validity_times', ['id' => 3, 'start_time' => '07:00:00']); + } + + public function test_invalid_scope_fails_without_changes(): void + { + $before = DB::table('validity_times')->orderBy('id')->get()->toJson(); + foreach ([[], ['--ids' => '1', '--all' => true], ['--ids' => '1,999'], ['--ids' => '1,3'], ['--ids' => 'bad'], ['--all' => true, '--source-timezone' => 'bad']] as $options) { + $this->artisan('tickets:convert-validity-times-to-utc', $options + ['--reverse' => true, '--apply' => true])->assertFailed(); + } + $this->assertSame($before, DB::table('validity_times')->orderBy('id')->get()->toJson()); + } + + public function test_reverse_failure_rolls_back_entire_operation(): void + { + DB::statement("CREATE TRIGGER fail_second BEFORE UPDATE ON validity_times WHEN OLD.id = 2 BEGIN SELECT RAISE(ABORT, 'test failure'); END"); + $before = DB::table('validity_times')->orderBy('id')->get()->toJson(); + $this->artisan('tickets:convert-validity-times-to-utc', ['--all' => true, '--reverse' => true, '--apply' => true])->assertFailed(); + $this->assertSame($before, DB::table('validity_times')->orderBy('id')->get()->toJson()); + } +} diff --git a/tests/Feature/Ticket/PrepareLoadTestTicketsCommandTest.php b/tests/Feature/Ticket/PrepareLoadTestTicketsCommandTest.php index 0124460..4d3d503 100644 --- a/tests/Feature/Ticket/PrepareLoadTestTicketsCommandTest.php +++ b/tests/Feature/Ticket/PrepareLoadTestTicketsCommandTest.php @@ -2,12 +2,12 @@ namespace Tests\Feature\Ticket; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Category; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Models\WebsiteType; -use App\Domains\Ticket\Models\Ticket; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Category; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; +use App\Domains\Ticketing\Ticket\Models\Ticket; use Database\Seeders\AuthorizationSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Artisan; @@ -23,7 +23,7 @@ class PrepareLoadTestTicketsCommandTest extends TestCase { Storage::fake('local'); $this->seed(AuthorizationSeeder::class); - WebsiteType::query()->create(['codigo' => 'onticket', 'nombre' => 'OnTicket']); + AdminWebsiteType::query()->create(['codigo' => 'onticket', 'nombre' => 'OnTicket']); $tenant = $this->createTenant('acme'); $category = Category::query()->create([ 'tenant_code' => $tenant->codigo, @@ -101,7 +101,7 @@ class PrepareLoadTestTicketsCommandTest extends TestCase 'footer_bg_color' => '#313131', 'header_logo_id' => $logo->id, 'footer_logo_id' => $logo->id, - 'website_type_code' => 'onticket', + 'admin_website_type_code' => 'onticket', ]); } } diff --git a/tests/Feature/Ticket/ScannerTicketControllerTest.php b/tests/Feature/Ticket/ScannerTicketControllerTest.php index 71b7a22..908a606 100644 --- a/tests/Feature/Ticket/ScannerTicketControllerTest.php +++ b/tests/Feature/Ticket/ScannerTicketControllerTest.php @@ -2,17 +2,17 @@ namespace Tests\Feature\Ticket; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\PermissionCode; -use App\Domains\Authorization\Enums\RoleCode; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Category; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Tenant\Models\WebsiteType; -use App\Domains\Ticket\Enums\ScanAttemptResult; -use App\Domains\Ticket\Models\ScanAttempt; -use App\Domains\Ticket\Models\Ticket; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Authorization\Enums\PermissionCode; +use App\Domains\Core\Authorization\Enums\RoleCode; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Category; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; +use App\Domains\Ticketing\Ticket\Enums\ScanAttemptResult; +use App\Domains\Ticketing\Ticket\Models\ScanAttempt; +use App\Domains\Ticketing\Ticket\Models\Ticket; use Database\Seeders\AuthorizationSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Str; @@ -36,7 +36,7 @@ class ScannerTicketControllerTest extends TestCase parent::setUp(); $this->seed(AuthorizationSeeder::class); - WebsiteType::query()->create(['codigo' => 'onticket', 'nombre' => 'OnTicket']); + AdminWebsiteType::query()->create(['codigo' => 'onticket', 'nombre' => 'OnTicket']); $this->tenant = $this->createTenant('acme'); $this->scanner = User::factory()->create([ 'rol_codigo' => RoleCode::Scanner->value, @@ -521,7 +521,7 @@ class ScannerTicketControllerTest extends TestCase 'footer_bg_color' => '#313131', 'header_logo_id' => $logo->id, 'footer_logo_id' => $logo->id, - 'website_type_code' => 'onticket', + 'admin_website_type_code' => 'onticket', ]); } } diff --git a/tests/Feature/Ticket/TicketControllerTest.php b/tests/Feature/Ticket/TicketControllerTest.php index dc9e0a0..39a9dfc 100644 --- a/tests/Feature/Ticket/TicketControllerTest.php +++ b/tests/Feature/Ticket/TicketControllerTest.php @@ -2,18 +2,20 @@ namespace Tests\Feature\Ticket; -use App\Domains\Attachable\Enums\AttachmentType; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Auth\Models\User; -use App\Domains\Catalog\Models\Attribute; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Inventory; -use App\Domains\Event\Events\EventDateRescheduled; -use App\Domains\Event\Events\EventDateSuspended; -use App\Domains\Event\Models\EventDate; -use App\Domains\Event\Services\EventService; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Ticket\Models\Ticket; +use App\Domains\Commerce\Catalog\Models\Attribute; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Inventory; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Tenant\Models\StorefrontWebsiteType; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Ticketing\Event\Events\EventDateRescheduled; +use App\Domains\Ticketing\Event\Events\EventDateSuspended; +use App\Domains\Ticketing\Event\Models\Event as TicketEvent; +use App\Domains\Ticketing\Event\Models\EventDate; +use App\Domains\Ticketing\Event\Services\EventService; +use App\Domains\Ticketing\Ticket\Models\Ticket; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Event; use Illuminate\Support\Str; @@ -164,6 +166,59 @@ class TicketControllerTest extends TestCase ->assertJsonPath('data.0.id', $visibleTicket->id); } + public function test_a_multi_event_user_can_list_only_events_with_their_tickets(): void + { + StorefrontWebsiteType::query()->create([ + 'codigo' => 'onticket_multi_event', + 'nombre' => 'OnTicket multi event', + 'header_type' => 'standard', + ]); + $tenant = $this->createTenant('multi'); + $tenant->update(['storefront_website_type_code' => 'onticket_multi_event']); + $otherTenant = $this->createTenant('other'); + $user = User::factory()->create(); + $otherUser = User::factory()->create(); + $firstEvent = $this->createEvent($tenant, 'First event'); + $secondEvent = $this->createEvent($tenant, 'Second event'); + $eventWithoutTickets = $this->createEvent($tenant, 'No tickets'); + $otherTenantEvent = $this->createEvent($otherTenant, 'Other tenant event'); + + $this->createTicket($tenant, $user, 'First ticket')->update(['event_id' => $firstEvent->id]); + $this->createTicket($tenant, $user, 'Second ticket')->update(['event_id' => $secondEvent->id]); + $this->createTicket($tenant, $otherUser, 'Other user')->update(['event_id' => $eventWithoutTickets->id]); + $this->createTicket($otherTenant, $user, 'Other tenant')->update(['event_id' => $otherTenantEvent->id]); + + $this->actingAs($user, 'sanctum') + ->getJson("/api/tenants/{$tenant->codigo}/tickets/events") + ->assertOk() + ->assertJsonCount(2, 'data') + ->assertJsonPath('data.0.id', $secondEvent->id) + ->assertJsonPath('data.0.title', 'Second event') + ->assertJsonPath('data.1.id', $firstEvent->id) + ->assertJsonMissing(['title' => 'No tickets']) + ->assertJsonMissing(['title' => 'Other tenant event']); + } + + public function test_a_user_can_filter_their_tickets_by_event(): void + { + $tenant = $this->createTenant('multi'); + $user = User::factory()->create(); + $firstEvent = $this->createEvent($tenant, 'First event'); + $secondEvent = $this->createEvent($tenant, 'Second event'); + $visibleTicket = $this->createTicket($tenant, $user, 'Visible ticket'); + $visibleTicket->update(['event_id' => $firstEvent->id]); + $hiddenTicket = $this->createTicket($tenant, $user, 'Hidden ticket'); + $hiddenTicket->update(['event_id' => $secondEvent->id]); + + $this->actingAs($user, 'sanctum') + ->getJson("/api/tenants/{$tenant->codigo}/tickets?event_id={$firstEvent->id}") + ->assertOk() + ->assertJsonCount(1, 'data') + ->assertJsonPath('data.0.id', $visibleTicket->id) + ->assertJsonPath('data.0.event_id', $firstEvent->id) + ->assertJsonMissing(['id' => $hiddenTicket->id]); + } + public function test_authentication_is_required_to_list_tickets(): void { $tenant = $this->createTenant('current'); @@ -248,6 +303,16 @@ class TicketControllerTest extends TestCase ]); } + private function createEvent(Tenant $tenant, string $title): TicketEvent + { + return TicketEvent::query()->create([ + 'tenant_code' => $tenant->codigo, + 'slug' => Str::slug($title).'-'.Str::lower(Str::random(8)), + 'title' => $title, + 'published_at' => now(), + ]); + } + private function createAttachment(string $name): Attachment { return Attachment::query()->create([ diff --git a/tests/Feature/Ticket/TicketGeneratorServiceTest.php b/tests/Feature/Ticket/TicketGeneratorServiceTest.php index 5edbf37..487b5cb 100644 --- a/tests/Feature/Ticket/TicketGeneratorServiceTest.php +++ b/tests/Feature/Ticket/TicketGeneratorServiceTest.php @@ -2,22 +2,23 @@ namespace Tests\Feature\Ticket; -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\Catalog\Services\CatalogService; -use App\Domains\Event\Models\EventDate; -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 App\Domains\Commerce\Catalog\Enums\CatalogItemType; +use App\Domains\Commerce\Catalog\Models\Attribute; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Inventory; +use App\Domains\Commerce\Catalog\Services\CatalogService; +use App\Domains\Commerce\Purchase\Models\Purchase; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Ticketing\Event\Models\Event as TicketEvent; +use App\Domains\Ticketing\Event\Models\EventDate; +use App\Domains\Ticketing\Ticket\Enums\ValidityTimeType; +use App\Domains\Ticketing\Ticket\Exceptions\TicketGenerationException; +use App\Domains\Ticketing\Ticket\Models\ValidityTime; +use App\Domains\Ticketing\Ticket\Services\TicketGeneratorService; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; +use App\Shared\Enums\FieldType; use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\QueryException; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -79,6 +80,26 @@ class TicketGeneratorServiceTest extends TestCase ]); } + public function test_it_preserves_the_catalog_items_event_on_the_ticket(): void + { + $event = TicketEvent::query()->create([ + 'tenant_code' => $this->tenant->codigo, + 'slug' => 'original-event', + 'title' => 'Original event', + 'published_at' => now(), + ]); + $item = $this->createTicketableItem('event-ticket'); + $item->update(['event_id' => $event->id]); + + $ticket = $this->service->generate($item->fresh(), $this->user)->sole(); + + $this->assertSame($event->id, $ticket->event_id); + $this->assertDatabaseHas('tickets', [ + 'id' => $ticket->id, + 'event_id' => $event->id, + ]); + } + public function test_it_rejects_an_item_without_tickets_enabled(): void { $item = $this->createTicketableItem('disabled'); diff --git a/tests/Unit/Auth/ResetPasswordAttemptServiceTest.php b/tests/Unit/Auth/ResetPasswordAttemptServiceTest.php index 0fffb8b..2b58b45 100644 --- a/tests/Unit/Auth/ResetPasswordAttemptServiceTest.php +++ b/tests/Unit/Auth/ResetPasswordAttemptServiceTest.php @@ -2,7 +2,7 @@ namespace Tests\Unit\Auth; -use App\Domains\Auth\Services\ResetPasswordAttemptService; +use App\Domains\Core\Auth\Services\ResetPasswordAttemptService; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use RuntimeException; diff --git a/tests/Unit/Bootstrap/AdminAppBootstrapResourceTest.php b/tests/Unit/Bootstrap/AdminAppBootstrapResourceTest.php index 688f3ab..edd5299 100644 --- a/tests/Unit/Bootstrap/AdminAppBootstrapResourceTest.php +++ b/tests/Unit/Bootstrap/AdminAppBootstrapResourceTest.php @@ -2,15 +2,15 @@ namespace Tests\Unit\Bootstrap; -use App\Domains\Bootstrap\Resources\AdminAppBootstrapResource; -use App\Domains\Tenant\Models\WebsiteType; +use App\Domains\Core\Bootstrap\Resources\AdminAppBootstrapResource; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; use Tests\TestCase; class AdminAppBootstrapResourceTest extends TestCase { public function test_it_exposes_the_website_type(): void { - $websiteType = new WebsiteType([ + $websiteType = new AdminWebsiteType([ 'codigo' => 'shopit', 'nombre' => 'ShopIt', 'dominio' => 'admin.shopit.test', @@ -23,7 +23,7 @@ class AdminAppBootstrapResourceTest extends TestCase 'website_type' => $websiteType, ])->resolve(request()); - $this->assertSame('shopit', $data['website_type_code']); + $this->assertSame('shopit', $data['admin_website_type_code']); $this->assertSame('ShopIt Website Type', $data['site_title']); $this->assertNull($data['favicon']); $this->assertArrayNotHasKey('forms', $data); diff --git a/tests/Unit/Cart/InvalidateEventDateCartsServiceTest.php b/tests/Unit/Cart/InvalidateEventDateCartsServiceTest.php index 3d59a4d..4c3fc64 100644 --- a/tests/Unit/Cart/InvalidateEventDateCartsServiceTest.php +++ b/tests/Unit/Cart/InvalidateEventDateCartsServiceTest.php @@ -2,12 +2,12 @@ namespace Tests\Unit\Cart; -use App\Domains\Cart\Models\Cart; -use App\Domains\Cart\Services\InvalidateEventDateCartsService; -use App\Domains\Catalog\Models\Inventory; -use App\Domains\Catalog\Models\StockReservation; -use App\Domains\Catalog\Services\StockReservationService; -use App\Domains\Tenant\Models\Tenant; +use App\Domains\Commerce\Cart\Models\Cart; +use App\Domains\Commerce\Cart\Services\InvalidateEventDateCartsService; +use App\Domains\Commerce\Catalog\Models\Inventory; +use App\Domains\Commerce\Catalog\Models\StockReservation; +use App\Domains\Commerce\Catalog\Services\StockReservationService; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; diff --git a/tests/Unit/Catalog/CatalogItemAllowanceServiceTest.php b/tests/Unit/Catalog/CatalogItemAllowanceServiceTest.php index 4d882aa..e88625d 100644 --- a/tests/Unit/Catalog/CatalogItemAllowanceServiceTest.php +++ b/tests/Unit/Catalog/CatalogItemAllowanceServiceTest.php @@ -2,7 +2,7 @@ namespace Tests\Unit\Catalog; -use App\Domains\Catalog\Services\CatalogItemAllowanceService; +use App\Domains\Commerce\Catalog\Services\CatalogItemAllowanceService; use ReflectionClass; use Tests\TestCase; diff --git a/tests/Unit/Catalog/CatalogModelsTest.php b/tests/Unit/Catalog/CatalogModelsTest.php index 45ca556..8b39c0a 100644 --- a/tests/Unit/Catalog/CatalogModelsTest.php +++ b/tests/Unit/Catalog/CatalogModelsTest.php @@ -2,29 +2,29 @@ namespace Tests\Unit\Catalog; -use App\Domains\Attachable\Models\Attachment; -use App\Domains\Catalog\Enums\CatalogItemType; -use App\Domains\Catalog\Enums\FeaturedGroupSource; -use App\Domains\Catalog\Enums\GroupLayout; -use App\Domains\Catalog\Enums\InventoryPolicy; -use App\Domains\Catalog\Enums\InventorySubject; -use App\Domains\Catalog\Enums\ProductLayout; -use App\Domains\Catalog\Models\Attribute; -use App\Domains\Catalog\Models\AttributeOption; -use App\Domains\Catalog\Models\Brand; -use App\Domains\Catalog\Models\BundleComponent; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Category; -use App\Domains\Catalog\Models\FeaturedGroup; -use App\Domains\Catalog\Models\FeaturedItem; -use App\Domains\Catalog\Models\Inventory; -use App\Domains\Catalog\Models\ItemAttribute; -use App\Domains\Catalog\Models\Variant; -use App\Domains\Catalog\Models\VariantDefinition; -use App\Domains\Event\Models\EventDate; -use App\Domains\Shared\Enums\FieldType; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Ticket\Models\Ticket; +use App\Shared\Attachable\Models\Attachment; +use App\Domains\Commerce\Catalog\Enums\CatalogItemType; +use App\Domains\Commerce\Catalog\Enums\FeaturedGroupSource; +use App\Domains\Commerce\Catalog\Enums\GroupLayout; +use App\Domains\Commerce\Catalog\Enums\InventoryPolicy; +use App\Domains\Commerce\Catalog\Enums\InventorySubject; +use App\Domains\Commerce\Catalog\Enums\ProductLayout; +use App\Domains\Commerce\Catalog\Models\Attribute; +use App\Domains\Commerce\Catalog\Models\AttributeOption; +use App\Domains\Commerce\Catalog\Models\Brand; +use App\Domains\Commerce\Catalog\Models\BundleComponent; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Category; +use App\Domains\Commerce\Catalog\Models\FeaturedGroup; +use App\Domains\Commerce\Catalog\Models\FeaturedItem; +use App\Domains\Commerce\Catalog\Models\Inventory; +use App\Domains\Commerce\Catalog\Models\ItemAttribute; +use App\Domains\Commerce\Catalog\Models\Variant; +use App\Domains\Commerce\Catalog\Models\VariantDefinition; +use App\Domains\Ticketing\Event\Models\EventDate; +use App\Shared\Enums\FieldType; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Ticketing\Ticket\Models\Ticket; use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Tests\TestCase; diff --git a/tests/Unit/Catalog/ExpireStockReservationsServiceTest.php b/tests/Unit/Catalog/ExpireStockReservationsServiceTest.php index 1abef5c..4ed255d 100644 --- a/tests/Unit/Catalog/ExpireStockReservationsServiceTest.php +++ b/tests/Unit/Catalog/ExpireStockReservationsServiceTest.php @@ -2,12 +2,12 @@ namespace Tests\Unit\Catalog; -use App\Domains\Catalog\Models\Inventory; -use App\Domains\Catalog\Models\StockReservation; -use App\Domains\Catalog\Models\StockReservationLine; -use App\Domains\Catalog\Services\ExpireStockReservationsService; -use App\Domains\Catalog\Services\StockReservationService; -use App\Domains\Purchase\Services\Checkout\ReleaseCheckoutService; +use App\Domains\Commerce\Catalog\Models\Inventory; +use App\Domains\Commerce\Catalog\Models\StockReservation; +use App\Domains\Commerce\Catalog\Models\StockReservationLine; +use App\Domains\Commerce\Catalog\Services\ExpireStockReservationsService; +use App\Domains\Commerce\Catalog\Services\StockReservationService; +use App\Domains\Commerce\Purchase\Services\Checkout\ReleaseCheckoutService; use Illuminate\Foundation\Testing\RefreshDatabase; use RuntimeException; use Tests\TestCase; diff --git a/tests/Unit/Event/EventDateGroupingServiceTest.php b/tests/Unit/Event/EventDateGroupingServiceTest.php index 2662b4c..5bb0f9f 100644 --- a/tests/Unit/Event/EventDateGroupingServiceTest.php +++ b/tests/Unit/Event/EventDateGroupingServiceTest.php @@ -2,10 +2,10 @@ namespace Tests\Unit\Event; -use App\Domains\Event\Models\EventDate; -use App\Domains\Event\Resources\EventResource; -use App\Domains\Event\Services\EventDateGroupingService; -use App\Domains\Tenant\Models\Tenant; +use App\Domains\Ticketing\Event\Models\EventDate; +use App\Domains\Ticketing\Event\Resources\EventResource; +use App\Domains\Ticketing\Event\Services\EventDateGroupingService; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Database\Eloquent\Collection; use Tests\TestCase; diff --git a/tests/Unit/Event/EventDateInfoFormatterTest.php b/tests/Unit/Event/EventDateInfoFormatterTest.php index 0742a73..e4d77a2 100644 --- a/tests/Unit/Event/EventDateInfoFormatterTest.php +++ b/tests/Unit/Event/EventDateInfoFormatterTest.php @@ -2,10 +2,10 @@ namespace Tests\Unit\Event; -use App\Domains\Event\Enums\EventDateChangeType; -use App\Domains\Event\Models\EventDate; -use App\Domains\Event\Models\EventDateChange; -use App\Domains\Event\Services\EventDateInfoFormatter; +use App\Domains\Ticketing\Event\Enums\EventDateChangeType; +use App\Domains\Ticketing\Event\Models\EventDate; +use App\Domains\Ticketing\Event\Models\EventDateChange; +use App\Domains\Ticketing\Event\Services\EventDateInfoFormatter; use Illuminate\Support\Carbon; use Tests\TestCase; diff --git a/tests/Unit/Event/EventDateNoticeFormatterTest.php b/tests/Unit/Event/EventDateNoticeFormatterTest.php index 8ff0c73..551be21 100644 --- a/tests/Unit/Event/EventDateNoticeFormatterTest.php +++ b/tests/Unit/Event/EventDateNoticeFormatterTest.php @@ -2,10 +2,10 @@ namespace Tests\Unit\Event; -use App\Domains\Event\Enums\EventDateChangeType; -use App\Domains\Event\Models\EventDateChange; -use App\Domains\Event\Services\EventDateNoticeFormatter; -use App\Domains\Event\Services\EventDateTextFormatter; +use App\Domains\Ticketing\Event\Enums\EventDateChangeType; +use App\Domains\Ticketing\Event\Models\EventDateChange; +use App\Domains\Ticketing\Event\Services\EventDateNoticeFormatter; +use App\Domains\Ticketing\Event\Services\EventDateTextFormatter; use Tests\TestCase; class EventDateNoticeFormatterTest extends TestCase diff --git a/tests/Unit/Event/EventDateTextFormatterTest.php b/tests/Unit/Event/EventDateTextFormatterTest.php index a5f1f7b..818e242 100644 --- a/tests/Unit/Event/EventDateTextFormatterTest.php +++ b/tests/Unit/Event/EventDateTextFormatterTest.php @@ -2,7 +2,7 @@ namespace Tests\Unit\Event; -use App\Domains\Event\Services\EventDateTextFormatter; +use App\Domains\Ticketing\Event\Services\EventDateTextFormatter; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; diff --git a/tests/Unit/Event/EventModelsTest.php b/tests/Unit/Event/EventModelsTest.php index 7dc90d2..b93e05a 100644 --- a/tests/Unit/Event/EventModelsTest.php +++ b/tests/Unit/Event/EventModelsTest.php @@ -2,11 +2,11 @@ namespace Tests\Unit\Event; -use App\Domains\Catalog\Models\Variant; -use App\Domains\Event\Enums\EventDateStatus; -use App\Domains\Event\Models\EventDate; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Ticket\Models\ValidityTime; +use App\Domains\Commerce\Catalog\Models\Variant; +use App\Domains\Ticketing\Event\Enums\EventDateStatus; +use App\Domains\Ticketing\Event\Models\EventDate; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Ticketing\Ticket\Models\ValidityTime; use Illuminate\Support\Carbon; use Tests\TestCase; diff --git a/tests/Unit/FiestaFutbolInfantil/EntryResourceTest.php b/tests/Unit/FiestaFutbolInfantil/EntryResourceTest.php index 3bbf8d1..c23bfef 100644 --- a/tests/Unit/FiestaFutbolInfantil/EntryResourceTest.php +++ b/tests/Unit/FiestaFutbolInfantil/EntryResourceTest.php @@ -2,11 +2,11 @@ namespace Tests\Unit\FiestaFutbolInfantil; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Inventory; -use App\Domains\Catalog\Models\Variant; -use App\Domains\Event\Models\EventDate; -use App\Domains\FiestaFutbolInfantil\Resources\EntryResource; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Inventory; +use App\Domains\Commerce\Catalog\Models\Variant; +use App\Domains\Ticketing\Event\Models\EventDate; +use App\Domains\Ticketing\FiestaFutbolInfantil\Resources\EntryResource; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; use Tests\TestCase; diff --git a/tests/Unit/Forms/SaleFormServiceTest.php b/tests/Unit/Forms/SaleFormServiceTest.php index 741e2d4..ff94288 100644 --- a/tests/Unit/Forms/SaleFormServiceTest.php +++ b/tests/Unit/Forms/SaleFormServiceTest.php @@ -2,8 +2,8 @@ namespace Tests\Unit\Forms; -use App\Domains\Forms\Services\SaleFormService; -use App\Domains\Purchase\Models\Purchase; +use App\Shared\Forms\Services\SaleFormService; +use App\Domains\Commerce\Purchase\Models\Purchase; use PHPUnit\Framework\TestCase; class SaleFormServiceTest extends TestCase diff --git a/tests/Unit/IntegrationInstanceSchemaTest.php b/tests/Unit/IntegrationInstanceSchemaTest.php index d00560c..a63a0a2 100644 --- a/tests/Unit/IntegrationInstanceSchemaTest.php +++ b/tests/Unit/IntegrationInstanceSchemaTest.php @@ -2,11 +2,11 @@ namespace Tests\Unit; -use App\Domains\Integration\Models\ClientIntegration; -use App\Domains\Integration\Models\Integration; -use App\Domains\Integration\Models\IntegrationInstance; -use App\Domains\Integration\Models\WebsiteTypeIntegration; -use App\Domains\Tenant\Models\WebsiteType; +use App\Shared\Integration\Models\ClientIntegration; +use App\Shared\Integration\Models\Integration; +use App\Shared\Integration\Models\IntegrationInstance; +use App\Shared\Integration\Models\AdminWebsiteTypeIntegration; +use App\Domains\Core\Tenant\Models\AdminWebsiteType; use Illuminate\Config\Repository; use Illuminate\Container\Container; use Illuminate\Database\Capsule\Manager; @@ -75,10 +75,21 @@ class IntegrationInstanceSchemaTest extends TestCase $db->table('client_integrations')->insert([ 'client_id' => 1, 'integration_code' => 'email', 'integration_data' => 'existing-ciphertext', ]); - $this->migrations = array_map(fn (string $file) => require $file, glob(__DIR__.'/../../database/migrations/2026_09_04_*.php')); + $migrationDir = __DIR__.'/../../database/migrations/'; + $this->migrations = array_map(fn (string $file) => require $migrationDir.$file, [ + '2026_09_04_000000_create_integration_instances_table.php', + '2026_09_04_000001_move_client_configuration_to_integration_instances.php', + '2026_09_04_000002_create_website_type_integrations_table.php', + '2026_09_04_000003_rename_integration_configuration_requirement.php', + ]); foreach ($this->migrations as $migration) { $migration->up(); } + $schema->rename('website_type', 'admin_website_types'); + $schema->rename('website_type_integrations', 'admin_website_type_integrations'); + $schema->table('admin_website_type_integrations', function (Blueprint $table): void { + $table->renameColumn('website_type_code', 'admin_website_type_code'); + }); } protected function tearDown(): void @@ -97,6 +108,13 @@ class IntegrationInstanceSchemaTest extends TestCase self::assertSame('existing-ciphertext', $db->table('integration_instances')->where('id', $association->integration_instance_id)->value('integration_data')); self::assertFalse($db->getSchemaBuilder()->hasColumn('client_integrations', 'integration_data')); + $schema = $db->getSchemaBuilder(); + $schema->table('admin_website_type_integrations', function (Blueprint $table): void { + $table->renameColumn('admin_website_type_code', 'website_type_code'); + }); + $schema->rename('admin_website_type_integrations', 'website_type_integrations'); + $schema->rename('admin_website_types', 'website_type'); + foreach (array_reverse($this->migrations) as $migration) { $migration->down(); } @@ -111,11 +129,11 @@ class IntegrationInstanceSchemaTest extends TestCase self::assertNotSame('private-value', $instance->getRawOriginal('integration_data')); self::assertSame(['password' => 'private-value'], $instance->fresh()->integration_data); self::assertArrayNotHasKey('integration_data', $instance->toArray()); - WebsiteTypeIntegration::create([ - 'website_type_code' => 'onticket', 'integration_code' => 'email', 'integration_instance_id' => $instance->id, + AdminWebsiteTypeIntegration::create([ + 'admin_website_type_code' => 'onticket', 'integration_code' => 'email', 'integration_instance_id' => $instance->id, ]); self::assertTrue(ClientIntegration::firstOrFail()->integrationInstance->is($instance)); - self::assertTrue(WebsiteType::firstOrFail()->integrations->first()->integrationInstance->is($instance)); + self::assertTrue(AdminWebsiteType::firstOrFail()->integrations->first()->integrationInstance->is($instance)); self::assertSame(1, $instance->clientIntegrations()->count()); self::assertSame(1, $instance->websiteTypeIntegrations()->count()); self::assertTrue(Integration::where('integration_code', 'email')->firstOrFail()->instances->first()->is($instance)); @@ -147,8 +165,8 @@ class IntegrationInstanceSchemaTest extends TestCase public function test_association_rejects_an_instance_from_another_integration(): void { $this->expectException(QueryException::class); - WebsiteTypeIntegration::create([ - 'website_type_code' => 'onticket', 'integration_code' => 'telepagos', 'integration_instance_id' => IntegrationInstance::firstOrFail()->id, + AdminWebsiteTypeIntegration::create([ + 'admin_website_type_code' => 'onticket', 'integration_code' => 'telepagos', 'integration_instance_id' => IntegrationInstance::firstOrFail()->id, ]); } @@ -161,13 +179,13 @@ class IntegrationInstanceSchemaTest extends TestCase public function test_website_type_cannot_have_two_instances_of_the_same_integration(): void { - WebsiteTypeIntegration::create([ - 'website_type_code' => 'onticket', 'integration_code' => 'email', 'integration_instance_id' => IntegrationInstance::firstOrFail()->id, + AdminWebsiteTypeIntegration::create([ + 'admin_website_type_code' => 'onticket', 'integration_code' => 'email', 'integration_instance_id' => IntegrationInstance::firstOrFail()->id, ]); $instance = IntegrationInstance::create(['integration_code' => 'email', 'name' => 'Second']); $this->expectException(QueryException::class); - WebsiteTypeIntegration::create([ - 'website_type_code' => 'onticket', 'integration_code' => 'email', 'integration_instance_id' => $instance->id, + AdminWebsiteTypeIntegration::create([ + 'admin_website_type_code' => 'onticket', 'integration_code' => 'email', 'integration_instance_id' => $instance->id, ]); } } diff --git a/tests/Unit/Logging/ValueChangeTest.php b/tests/Unit/Logging/ValueChangeTest.php index 8add79d..ea51cf9 100644 --- a/tests/Unit/Logging/ValueChangeTest.php +++ b/tests/Unit/Logging/ValueChangeTest.php @@ -2,10 +2,10 @@ namespace Tests\Unit\Logging; -use App\Domains\Auth\Models\User; -use App\Domains\Logging\Enums\ValueChangeActorType; -use App\Domains\Logging\Models\ValueChange; -use App\Domains\Tenant\Models\Tenant; +use App\Domains\Core\Auth\Models\User; +use App\Shared\Logging\Enums\ValueChangeActorType; +use App\Shared\Logging\Models\ValueChange; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Database\Eloquent\Relations\MorphTo; use Tests\TestCase; diff --git a/tests/Unit/Notification/NotificationMailServiceLoggingTest.php b/tests/Unit/Notification/NotificationMailServiceLoggingTest.php index 9f69677..df9808d 100644 --- a/tests/Unit/Notification/NotificationMailServiceLoggingTest.php +++ b/tests/Unit/Notification/NotificationMailServiceLoggingTest.php @@ -2,11 +2,11 @@ namespace Tests\Unit\Notification; -use App\Domains\Integration\Services\MailService; -use App\Domains\Notification\Services\IdempotentEmailDeliveryService; -use App\Domains\Notification\Services\NotificationMailService; -use App\Domains\Purchase\Models\Purchase; -use App\Domains\Ticket\Services\TicketPdfService; +use App\Shared\Integration\Services\MailService; +use App\Shared\Notification\Services\IdempotentEmailDeliveryService; +use App\Shared\Notification\Services\NotificationMailService; +use App\Domains\Commerce\Purchase\Models\Purchase; +use App\Domains\Ticketing\Ticket\Services\TicketPdfService; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Schema; diff --git a/tests/Unit/Notification/SendPasswordResetEmailTest.php b/tests/Unit/Notification/SendPasswordResetEmailTest.php index 41a5ec7..4df15fa 100644 --- a/tests/Unit/Notification/SendPasswordResetEmailTest.php +++ b/tests/Unit/Notification/SendPasswordResetEmailTest.php @@ -2,9 +2,9 @@ namespace Tests\Unit\Notification; -use App\Domains\Notification\Events\PasswordResetRequested; -use App\Domains\Notification\Listeners\SendPasswordResetEmail; -use App\Domains\Notification\Services\NotificationMailService; +use App\Shared\Notification\Events\PasswordResetRequested; +use App\Shared\Notification\Listeners\SendPasswordResetEmail; +use App\Shared\Notification\Services\NotificationMailService; use RuntimeException; use Tests\TestCase; diff --git a/tests/Unit/Purchase/DniDistanceServiceTest.php b/tests/Unit/Purchase/DniDistanceServiceTest.php index 2e531f7..1615d3b 100644 --- a/tests/Unit/Purchase/DniDistanceServiceTest.php +++ b/tests/Unit/Purchase/DniDistanceServiceTest.php @@ -2,7 +2,7 @@ namespace Tests\Unit\Purchase; -use App\Domains\Purchase\Services\DniDistanceService; +use App\Domains\Commerce\Purchase\Services\DniDistanceService; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; diff --git a/tests/Unit/Purchase/InsufficientStockMessageBuilderTest.php b/tests/Unit/Purchase/InsufficientStockMessageBuilderTest.php index 90b8eda..9cf2c44 100644 --- a/tests/Unit/Purchase/InsufficientStockMessageBuilderTest.php +++ b/tests/Unit/Purchase/InsufficientStockMessageBuilderTest.php @@ -2,15 +2,15 @@ namespace Tests\Unit\Purchase; -use App\Domains\Catalog\Enums\InventorySubject; -use App\Domains\Catalog\Models\Attribute; -use App\Domains\Catalog\Models\AttributeOption; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\ItemAttribute; -use App\Domains\Catalog\Models\Variant; -use App\Domains\Catalog\Models\VariantDefinition; -use App\Domains\Purchase\Exceptions\InsufficientStockException; -use App\Domains\Purchase\Services\Checkout\InsufficientStockMessageBuilder; +use App\Domains\Commerce\Catalog\Enums\InventorySubject; +use App\Domains\Commerce\Catalog\Models\Attribute; +use App\Domains\Commerce\Catalog\Models\AttributeOption; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\ItemAttribute; +use App\Domains\Commerce\Catalog\Models\Variant; +use App\Domains\Commerce\Catalog\Models\VariantDefinition; +use App\Domains\Commerce\Purchase\Exceptions\InsufficientStockException; +use App\Domains\Commerce\Purchase\Services\Checkout\InsufficientStockMessageBuilder; use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Support\Facades\App; use Tests\TestCase; diff --git a/tests/Unit/Purchase/PurchaseAdminStatusTest.php b/tests/Unit/Purchase/PurchaseAdminStatusTest.php index f1fa7f5..875c6d1 100644 --- a/tests/Unit/Purchase/PurchaseAdminStatusTest.php +++ b/tests/Unit/Purchase/PurchaseAdminStatusTest.php @@ -2,7 +2,7 @@ namespace Tests\Unit\Purchase; -use App\Domains\Purchase\Models\Purchase; +use App\Domains\Commerce\Purchase\Models\Purchase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; diff --git a/tests/Unit/Purchase/PurchaseResourceTest.php b/tests/Unit/Purchase/PurchaseResourceTest.php index a7f09f9..f7b1788 100644 --- a/tests/Unit/Purchase/PurchaseResourceTest.php +++ b/tests/Unit/Purchase/PurchaseResourceTest.php @@ -2,9 +2,9 @@ namespace Tests\Unit\Purchase; -use App\Domains\Catalog\Models\StockReservation; -use App\Domains\Purchase\Models\Purchase; -use App\Domains\Purchase\Resources\PurchaseResource; +use App\Domains\Commerce\Catalog\Models\StockReservation; +use App\Domains\Commerce\Purchase\Models\Purchase; +use App\Domains\Commerce\Purchase\Resources\PurchaseResource; use Illuminate\Http\Request; use Illuminate\Support\Carbon; use Tests\TestCase; diff --git a/tests/Unit/Purchase/PurchaseStateGuardTest.php b/tests/Unit/Purchase/PurchaseStateGuardTest.php index 9cead5d..0b45e66 100644 --- a/tests/Unit/Purchase/PurchaseStateGuardTest.php +++ b/tests/Unit/Purchase/PurchaseStateGuardTest.php @@ -2,10 +2,10 @@ namespace Tests\Unit\Purchase; -use App\Domains\Catalog\Models\StockReservation; -use App\Domains\Purchase\Exceptions\PurchaseExpiredException; -use App\Domains\Purchase\Models\Purchase; -use App\Domains\Purchase\Services\PurchaseStateGuard; +use App\Domains\Commerce\Catalog\Models\StockReservation; +use App\Domains\Commerce\Purchase\Exceptions\PurchaseExpiredException; +use App\Domains\Commerce\Purchase\Models\Purchase; +use App\Domains\Commerce\Purchase\Services\PurchaseStateGuard; use Tests\TestCase; class PurchaseStateGuardTest extends TestCase diff --git a/tests/Unit/Sale/AdminAppSaleExcelServiceTest.php b/tests/Unit/Sale/AdminAppSaleExcelServiceTest.php index 34676b0..09182fe 100644 --- a/tests/Unit/Sale/AdminAppSaleExcelServiceTest.php +++ b/tests/Unit/Sale/AdminAppSaleExcelServiceTest.php @@ -2,11 +2,11 @@ namespace Tests\Unit\Sale; -use App\Domains\Auth\Models\User; -use App\Domains\Logging\Models\ValueChange; -use App\Domains\Purchase\Models\Purchase; -use App\Domains\Sale\Services\AdminAppSaleExcelService; -use App\Domains\Tenant\Models\Tenant; +use App\Domains\Core\Auth\Models\User; +use App\Shared\Logging\Models\ValueChange; +use App\Domains\Commerce\Purchase\Models\Purchase; +use App\Domains\Commerce\Sale\Services\AdminAppSaleExcelService; +use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Support\Carbon; use PhpOffice\PhpSpreadsheet\IOFactory; use Symfony\Component\HttpFoundation\StreamedResponse; diff --git a/tests/Unit/Sale/AdminAppSalePdfRequestTest.php b/tests/Unit/Sale/AdminAppSalePdfRequestTest.php index 4852211..577fb98 100644 --- a/tests/Unit/Sale/AdminAppSalePdfRequestTest.php +++ b/tests/Unit/Sale/AdminAppSalePdfRequestTest.php @@ -2,8 +2,8 @@ namespace Tests\Unit\Sale; -use App\Domains\Sale\Requests\AdminAppSaleModificationPdfRequest; -use App\Domains\Sale\Requests\AdminAppSalePdfRequest; +use App\Domains\Commerce\Sale\Requests\AdminAppSaleModificationPdfRequest; +use App\Domains\Commerce\Sale\Requests\AdminAppSalePdfRequest; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Validator; use Tests\TestCase; diff --git a/tests/Unit/Sale/AdminAppSalePdfServiceTest.php b/tests/Unit/Sale/AdminAppSalePdfServiceTest.php index ea80138..ebb8d1b 100644 --- a/tests/Unit/Sale/AdminAppSalePdfServiceTest.php +++ b/tests/Unit/Sale/AdminAppSalePdfServiceTest.php @@ -2,11 +2,11 @@ namespace Tests\Unit\Sale; -use App\Domains\Auth\Models\User; -use App\Domains\Logging\Models\ValueChange; -use App\Domains\Purchase\Models\Purchase; -use App\Domains\Sale\Services\AdminAppSalePdfService; -use App\Domains\Tenant\Models\Tenant; +use App\Domains\Core\Auth\Models\User; +use App\Shared\Logging\Models\ValueChange; +use App\Domains\Commerce\Purchase\Models\Purchase; +use App\Domains\Commerce\Sale\Services\AdminAppSalePdfService; +use App\Domains\Core\Tenant\Models\Tenant; use Barryvdh\DomPDF\ServiceProvider; use Illuminate\Support\Carbon; use Tests\TestCase; diff --git a/tests/Unit/Sale/SaleDetailResourceTest.php b/tests/Unit/Sale/SaleDetailResourceTest.php index 21838f7..8c57bd3 100644 --- a/tests/Unit/Sale/SaleDetailResourceTest.php +++ b/tests/Unit/Sale/SaleDetailResourceTest.php @@ -2,9 +2,9 @@ namespace Tests\Unit\Sale; -use App\Domains\Purchase\Models\Purchase; -use App\Domains\Purchase\Models\PurchaseItem; -use App\Domains\Sale\Resources\AdminApp\SaleDetailResource; +use App\Domains\Commerce\Purchase\Models\Purchase; +use App\Domains\Commerce\Purchase\Models\PurchaseItem; +use App\Domains\Commerce\Sale\Resources\AdminApp\SaleDetailResource; use Illuminate\Http\Request; use Tests\TestCase; diff --git a/tests/Unit/Sale/SaleModificationResourceTest.php b/tests/Unit/Sale/SaleModificationResourceTest.php index 3f2e3a4..2fbb3ba 100644 --- a/tests/Unit/Sale/SaleModificationResourceTest.php +++ b/tests/Unit/Sale/SaleModificationResourceTest.php @@ -2,9 +2,9 @@ namespace Tests\Unit\Sale; -use App\Domains\Logging\Models\ValueChange; -use App\Domains\Purchase\Models\Purchase; -use App\Domains\Sale\Resources\AdminApp\SaleModificationResource; +use App\Shared\Logging\Models\ValueChange; +use App\Domains\Commerce\Purchase\Models\Purchase; +use App\Domains\Commerce\Sale\Resources\AdminApp\SaleModificationResource; use Carbon\CarbonImmutable; use Illuminate\Http\Request; use Tests\TestCase; diff --git a/tests/Unit/Storage/TemporaryUrlServiceTest.php b/tests/Unit/Storage/TemporaryUrlServiceTest.php new file mode 100644 index 0000000..ac2c1b6 --- /dev/null +++ b/tests/Unit/Storage/TemporaryUrlServiceTest.php @@ -0,0 +1,85 @@ +travelTo(Carbon::parse('2026-09-22 12:00:00', 'UTC')); + } + + protected function tearDown(): void + { + Cache::flush(); + $this->travelBack(); + + parent::tearDown(); + } + + public function test_it_reuses_the_signed_url_and_its_original_expiration(): void + { + $disk = Mockery::mock(); + + Storage::shouldReceive('disk') + ->once() + ->with('s3') + ->andReturn($disk); + $disk->shouldReceive('temporaryUrl') + ->once() + ->with( + 'images/product.png', + Mockery::on(fn ($expiration): bool => $expiration->equalTo(now()->addMinutes(10))), + ['ResponseCacheControl' => 'private, max-age=600'], + ) + ->andReturn('https://s3.example.test/product.png?signed=first'); + + $service = app(TemporaryUrlService::class); + $first = $service->generate('images/product.png', 10); + + $this->travel(5)->minutes(); + + $second = $service->generate('images/product.png', 10); + + $this->assertSame($first, $second); + $this->assertSame('https://s3.example.test/product.png?signed=first', $second['temporary_url']); + $this->assertSame('2026-09-22T12:10:00+00:00', $second['temporary_url_expires_at']); + } + + public function test_it_refreshes_the_signed_url_before_it_expires(): void + { + $disk = Mockery::mock(); + + Storage::shouldReceive('disk') + ->twice() + ->with('s3') + ->andReturn($disk); + $disk->shouldReceive('temporaryUrl') + ->twice() + ->andReturn( + 'https://s3.example.test/product.png?signed=first', + 'https://s3.example.test/product.png?signed=second', + ); + + $service = app(TemporaryUrlService::class); + $first = $service->generate('images/product.png', 10); + + $this->travel(9)->minutes(); + $this->travel(1)->seconds(); + + $second = $service->generate('images/product.png', 10); + + $this->assertNotSame($first['temporary_url'], $second['temporary_url']); + $this->assertSame('2026-09-22T12:19:01+00:00', $second['temporary_url_expires_at']); + } +} diff --git a/tests/Unit/Tenant/TenantDomainNormalizerTest.php b/tests/Unit/Tenant/TenantDomainNormalizerTest.php index b736424..0eaa230 100644 --- a/tests/Unit/Tenant/TenantDomainNormalizerTest.php +++ b/tests/Unit/Tenant/TenantDomainNormalizerTest.php @@ -2,7 +2,7 @@ namespace Tests\Unit\Tenant; -use App\Domains\Tenant\Support\TenantDomainNormalizer; +use App\Domains\Core\Tenant\Support\TenantDomainNormalizer; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; diff --git a/tests/Unit/Ticket/AdminAppTicketExportServiceTest.php b/tests/Unit/Ticket/AdminAppTicketExportServiceTest.php index fd352b5..c61011f 100644 --- a/tests/Unit/Ticket/AdminAppTicketExportServiceTest.php +++ b/tests/Unit/Ticket/AdminAppTicketExportServiceTest.php @@ -2,12 +2,12 @@ namespace Tests\Unit\Ticket; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Ticket\Services\AdminAppTicketColumnService; -use App\Domains\Ticket\Services\AdminAppTicketExcelService; -use App\Domains\Ticket\Services\AdminAppTicketPdfService; -use App\Domains\Ticket\Services\AdminAppTicketReportService; -use App\Domains\Ticket\Services\AdminAppTicketRowService; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Ticketing\Ticket\Services\AdminAppTicketColumnService; +use App\Domains\Ticketing\Ticket\Services\AdminAppTicketExcelService; +use App\Domains\Ticketing\Ticket\Services\AdminAppTicketPdfService; +use App\Domains\Ticketing\Ticket\Services\AdminAppTicketReportService; +use App\Domains\Ticketing\Ticket\Services\AdminAppTicketRowService; use Barryvdh\DomPDF\ServiceProvider; use Illuminate\Support\Carbon; use Mockery; diff --git a/tests/Unit/Ticket/TenantTimezoneValidityTest.php b/tests/Unit/Ticket/TenantTimezoneValidityTest.php new file mode 100644 index 0000000..e449f14 --- /dev/null +++ b/tests/Unit/Ticket/TenantTimezoneValidityTest.php @@ -0,0 +1,90 @@ + ValidityTimeType::TimeWindow, 'start_time' => '22:00', 'end_time' => '02:00']); + $group = new ResolvedValidityGroup(collect([$window]), 'America/Argentina/Buenos_Aires'); + foreach (['2026-09-22 01:00' => true, '2026-09-22 04:59' => true, '2026-09-22 05:00' => false, '2026-09-22 18:00' => false] as $time => $expected) { + $at = CarbonImmutable::parse($time, 'UTC'); + $this->assertSame($expected, $group->isValid($at), $time); + $this->assertSame($expected, $window->isValid($at, 'America/Argentina/Buenos_Aires'), $time); + } + $this->assertFalse((new ResolvedValidityGroup(collect([$window]), 'Asia/Tokyo'))->isValid(CarbonImmutable::parse('2026-09-22 01:00', 'UTC'))); + } + + public function test_event_anchor_uses_local_date_when_utc_date_is_next_day(): void + { + $fixed = new ValidityTime(['type' => ValidityTimeType::FixedWindow, + 'fixed_starts_at' => '2026-09-22 01:00', 'fixed_expires_at' => '2026-09-22 06:00']); + $window = new ValidityTime(['type' => ValidityTimeType::TimeWindow, 'start_time' => '22:00', 'end_time' => '02:00']); + $group = new ResolvedValidityGroup(collect([$fixed, $window]), 'America/Argentina/Buenos_Aires'); + $this->assertTrue($group->isValid(CarbonImmutable::parse('2026-09-22 04:00', 'UTC'))); + $this->assertFalse($group->isValid(CarbonImmutable::parse('2026-09-23 04:00', 'UTC'))); + $this->assertSame('2026-09-22 01:00', $group->effectiveStartsAt()->utc()->format('Y-m-d H:i')); + $this->assertSame('2026-09-22 05:00', $group->effectiveExpiresAt()->utc()->format('Y-m-d H:i')); + } + + public function test_local_window_compares_with_utc_now_in_resource(): void + { + $this->travelTo(CarbonImmutable::parse('2026-09-21 22:00', 'UTC')); + $window = new ValidityTime(['type' => ValidityTimeType::TimeWindow, 'start_time' => '18:00', 'end_time' => '20:00']); + $resource = ValidityTimeResource::make($window, 'America/Argentina/Buenos_Aires'); + $this->assertTrue($resource->resolve()['is_valid']); + $this->assertFalse($window->isValid(now(), 'Asia/Tokyo')); + $this->travelBack(); + } + + public function test_daylight_saving_uses_date_specific_offset(): void + { + $window = new ValidityTime(['type' => ValidityTimeType::TimeWindow, 'start_time' => '18:00', 'end_time' => '20:00']); + $this->assertSame('2026-01-21 23:00', $window->startsAt(CarbonImmutable::parse('2026-01-21 12:00', 'UTC'), 'America/New_York')->utc()->format('Y-m-d H:i')); + $this->assertSame('2026-07-21 22:00', $window->startsAt(CarbonImmutable::parse('2026-07-21 12:00', 'UTC'), 'America/New_York')->utc()->format('Y-m-d H:i')); + } + + public function test_resolver_passes_tenant_timezone_to_groups(): void + { + $tenant = new Tenant(['timezone' => 'Asia/Tokyo']); + $item = new CatalogItem; + $item->setRelation('tenant', $tenant); + $date = new EventDate; + $date->setRelation('validityTime', new ValidityTime([ + 'type' => ValidityTimeType::FixedWindow, + 'fixed_starts_at' => '2026-09-21 13:00', + 'fixed_expires_at' => '2026-09-21 17:00', + ])); + $variant = new Variant; + $variant->setRelation('catalogItem', $item); + $variant->setRelation('eventDates', new Collection([$date])); + $variant->setRelation('eventDate', null); + $variant->setRelation('definitions', new Collection); + $resolved = (new TicketValidityResolver)->resolveVariant($variant); + $this->assertSame('Asia/Tokyo', $resolved->groups->first()->timezone); + } + + public function test_event_hours_are_local_and_end_can_be_next_day(): void + { + $date = new EventDate(['date' => '2026-09-21', 'time_start' => '22:00', 'time_end' => '02:00']); + $date->setRelation('tenant', new Tenant(['timezone' => 'America/Argentina/Buenos_Aires'])); + $this->assertSame('2026-09-22 01:00', $date->startsAt()->utc()->format('Y-m-d H:i')); + $this->assertSame('2026-09-22 05:00', $date->endsAt()->utc()->format('Y-m-d H:i')); + $date->setRelation('tenant', new Tenant(['timezone' => 'Asia/Tokyo'])); + $this->assertSame('2026-09-21 13:00', $date->startsAt()->utc()->format('Y-m-d H:i')); + } +} diff --git a/tests/Unit/Ticket/TicketTest.php b/tests/Unit/Ticket/TicketTest.php index dc0b663..0bce1fe 100644 --- a/tests/Unit/Ticket/TicketTest.php +++ b/tests/Unit/Ticket/TicketTest.php @@ -2,16 +2,16 @@ namespace Tests\Unit\Ticket; -use App\Domains\Auth\Models\User; -use App\Domains\Catalog\Models\CatalogItem; -use App\Domains\Catalog\Models\Variant; -use App\Domains\Tenant\Models\Tenant; -use App\Domains\Ticket\Enums\ValidityTimeType; -use App\Domains\Ticket\Models\Ticket; -use App\Domains\Ticket\Models\ValidityTime; -use App\Domains\Ticket\Services\ResolvedTicketValidity; -use App\Domains\Ticket\Services\ResolvedValidityGroup; -use App\Domains\Ticket\Services\TicketValidityResolver; +use App\Domains\Core\Auth\Models\User; +use App\Domains\Commerce\Catalog\Models\CatalogItem; +use App\Domains\Commerce\Catalog\Models\Variant; +use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Ticketing\Ticket\Enums\ValidityTimeType; +use App\Domains\Ticketing\Ticket\Models\Ticket; +use App\Domains\Ticketing\Ticket\Models\ValidityTime; +use App\Domains\Ticketing\Ticket\Services\ResolvedTicketValidity; +use App\Domains\Ticketing\Ticket\Services\ResolvedValidityGroup; +use App\Domains\Ticketing\Ticket\Services\TicketValidityResolver; use Illuminate\Support\Carbon; use Illuminate\Validation\ValidationException; use Tests\TestCase; diff --git a/tests/Unit/Ticket/TicketValidityResolverTest.php b/tests/Unit/Ticket/TicketValidityResolverTest.php index 2401523..e68ac23 100644 --- a/tests/Unit/Ticket/TicketValidityResolverTest.php +++ b/tests/Unit/Ticket/TicketValidityResolverTest.php @@ -2,16 +2,16 @@ namespace Tests\Unit\Ticket; -use App\Domains\Catalog\Models\Attribute; -use App\Domains\Catalog\Models\AttributeOption; -use App\Domains\Catalog\Models\ItemAttribute; -use App\Domains\Catalog\Models\Variant; -use App\Domains\Catalog\Models\VariantDefinition; -use App\Domains\Event\Models\EventDate; -use App\Domains\Shared\Enums\FieldType; -use App\Domains\Ticket\Enums\ValidityTimeType; -use App\Domains\Ticket\Models\ValidityTime; -use App\Domains\Ticket\Services\TicketValidityResolver; +use App\Domains\Commerce\Catalog\Models\Attribute; +use App\Domains\Commerce\Catalog\Models\AttributeOption; +use App\Domains\Commerce\Catalog\Models\ItemAttribute; +use App\Domains\Commerce\Catalog\Models\Variant; +use App\Domains\Commerce\Catalog\Models\VariantDefinition; +use App\Domains\Ticketing\Event\Models\EventDate; +use App\Domains\Ticketing\Ticket\Enums\ValidityTimeType; +use App\Domains\Ticketing\Ticket\Models\ValidityTime; +use App\Domains\Ticketing\Ticket\Services\TicketValidityResolver; +use App\Shared\Enums\FieldType; use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Support\Carbon; use Tests\TestCase; @@ -200,6 +200,7 @@ class TicketValidityResolverTest extends TestCase private function variant(EloquentCollection $eventDates, EloquentCollection $definitions): Variant { $variant = new Variant; + $variant->setRelation('catalogItem', null); $variant->setRelation('eventDates', $eventDates); $variant->setRelation('eventDate', null); $variant->setRelation('definitions', $definitions); diff --git a/tests/Unit/Ticket/ValidityTimeResourceTest.php b/tests/Unit/Ticket/ValidityTimeResourceTest.php index 34f202a..c2056f3 100644 --- a/tests/Unit/Ticket/ValidityTimeResourceTest.php +++ b/tests/Unit/Ticket/ValidityTimeResourceTest.php @@ -2,9 +2,9 @@ namespace Tests\Unit\Ticket; -use App\Domains\Ticket\Enums\ValidityTimeType; -use App\Domains\Ticket\Models\ValidityTime; -use App\Domains\Ticket\Resources\ValidityTimeResource; +use App\Domains\Ticketing\Ticket\Enums\ValidityTimeType; +use App\Domains\Ticketing\Ticket\Models\ValidityTime; +use App\Domains\Ticketing\Ticket\Resources\ValidityTimeResource; use Illuminate\Support\Carbon; use Tests\TestCase; diff --git a/tests/Unit/Ticket/ValidityTimeTest.php b/tests/Unit/Ticket/ValidityTimeTest.php index 41766e0..b0516d0 100644 --- a/tests/Unit/Ticket/ValidityTimeTest.php +++ b/tests/Unit/Ticket/ValidityTimeTest.php @@ -2,9 +2,9 @@ namespace Tests\Unit\Ticket; -use App\Domains\Catalog\Models\AttributeOption; -use App\Domains\Ticket\Enums\ValidityTimeType; -use App\Domains\Ticket\Models\ValidityTime; +use App\Domains\Commerce\Catalog\Models\AttributeOption; +use App\Domains\Ticketing\Ticket\Enums\ValidityTimeType; +use App\Domains\Ticketing\Ticket\Models\ValidityTime; use Illuminate\Support\Carbon; use Tests\TestCase;