Compare commits
55 Commits
v1.0.0
...
tenant/sme
| Author | SHA1 | Date | |
|---|---|---|---|
| a40a2065a9 | |||
| 57677f55e6 | |||
| a8afd92f46 | |||
| 1df0d2e2ef | |||
| d3cac7a590 | |||
| df13a636ba | |||
| d85841742e | |||
| 0e3fdc5460 | |||
| 0dc2675dae | |||
| 28d1de072c | |||
| b09e8a4b96 | |||
| 7f754037bb | |||
| f29c5692b9 | |||
| 57d278336d | |||
| b309738222 | |||
| b3eb59983a | |||
| cf3f0ceb1b | |||
| 41c0902b83 | |||
| a2439bf43b | |||
| 8d5a12f212 | |||
| 92170b72f2 | |||
| c3def0678f | |||
| 74b72a7d4e | |||
| 16e2ca679c | |||
| 6b0efbc6ee | |||
| 2db83271fa | |||
| e259bba1d1 | |||
| b3cd9f5de5 | |||
| 64820fe12b | |||
| e72b6cfdde | |||
| 0e870776c4 | |||
| d8d8354070 | |||
| 5bb61fc74c | |||
| 9e9af70ba8 | |||
| 884da2c89a | |||
| 3671b95a83 | |||
| 5e78f2bb1e | |||
| 5296d595f1 | |||
| 9901d449e1 | |||
| 57605a56c4 | |||
| d9f19b42f1 | |||
| 90b2784859 | |||
| 6f191b27f9 | |||
| 3401c934b2 | |||
| 6efe8439c7 | |||
| 506f8a5176 | |||
| 03bb7709c4 | |||
| c90a393081 | |||
| a4a8a3ef8f | |||
| f5f4cb4e87 | |||
| d33b435ba5 | |||
| 969e99b3ae | |||
| 3e3b3bfac0 | |||
| f4de2ff5b2 | |||
| bd77727f58 |
@@ -2470,45 +2470,6 @@
|
|||||||
},
|
},
|
||||||
"response": []
|
"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 \"admin_website_type_code\": \"shopit\",\n \"storefront_website_type_code\": \"shopit\"\n}",
|
|
||||||
"options": {
|
|
||||||
"raw": {
|
|
||||||
"language": "json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Get Tenant",
|
"name": "Get Tenant",
|
||||||
"request": {
|
"request": {
|
||||||
@@ -2535,86 +2496,6 @@
|
|||||||
},
|
},
|
||||||
"response": []
|
"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",
|
"name": "Delete Tenant",
|
||||||
"request": {
|
"request": {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Domains\Commerce\Catalog\Models;
|
namespace App\Domains\Commerce\Catalog\Models;
|
||||||
|
|
||||||
use App\Shared\Attachable\Models\Attachment;
|
|
||||||
use App\Domains\Commerce\Catalog\Enums\CatalogItemType;
|
use App\Domains\Commerce\Catalog\Enums\CatalogItemType;
|
||||||
use App\Domains\Commerce\Catalog\Enums\InventoryPolicy;
|
use App\Domains\Commerce\Catalog\Enums\InventoryPolicy;
|
||||||
use App\Domains\Commerce\Catalog\Enums\InventorySubject;
|
use App\Domains\Commerce\Catalog\Enums\InventorySubject;
|
||||||
@@ -10,6 +9,7 @@ use App\Domains\Commerce\Catalog\Services\CatalogInventoryService;
|
|||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
use App\Domains\Ticketing\Event\Models\Event;
|
use App\Domains\Ticketing\Event\Models\Event;
|
||||||
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
||||||
|
use App\Shared\Attachable\Models\Attachment;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -237,7 +237,7 @@ class CatalogItem extends Model
|
|||||||
->whereHas(
|
->whereHas(
|
||||||
'inventory',
|
'inventory',
|
||||||
fn (Builder $inventoryQuery): Builder => $inventoryQuery
|
fn (Builder $inventoryQuery): Builder => $inventoryQuery
|
||||||
->whereColumn('inventories.real_stock', '>', 'inventories.reserved_stock')
|
->whereRaw('inventories.real_stock > inventories.reserved_stock + inventories.entry_reserved_stock')
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
->orWhere(function (Builder $directItemQuery): void {
|
->orWhere(function (Builder $directItemQuery): void {
|
||||||
@@ -249,7 +249,7 @@ class CatalogItem extends Model
|
|||||||
->orWhereHas(
|
->orWhereHas(
|
||||||
'inventory',
|
'inventory',
|
||||||
fn (Builder $availableInventoryQuery): Builder => $availableInventoryQuery
|
fn (Builder $availableInventoryQuery): Builder => $availableInventoryQuery
|
||||||
->whereColumn('inventories.real_stock', '>', 'inventories.reserved_stock')
|
->whereRaw('inventories.real_stock > inventories.reserved_stock + inventories.entry_reserved_stock')
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -264,9 +264,15 @@ class CatalogItem extends Model
|
|||||||
/** @return Collection<int, Variant> */
|
/** @return Collection<int, Variant> */
|
||||||
public function visibleVariants(?int $includedVariantId = null): Collection
|
public function visibleVariants(?int $includedVariantId = null): Collection
|
||||||
{
|
{
|
||||||
|
$this->variants
|
||||||
|
->filter(fn (Variant $variant): bool => $variant->exists)
|
||||||
|
->loadMissing('desfileEntryReservations');
|
||||||
|
|
||||||
return $this->variants
|
return $this->variants
|
||||||
->each(fn (Variant $variant) => $variant->setRelation('catalogItem', $this))
|
->each(fn (Variant $variant) => $variant->setRelation('catalogItem', $this))
|
||||||
->filter(fn (Variant $variant): bool => $variant->hasOnlyActiveEventDates()
|
->filter(fn (Variant $variant): bool => $variant->hasOnlyActiveEventDates()
|
||||||
|
&& (! $variant->relationLoaded('desfileEntryReservations')
|
||||||
|
|| $variant->desfileEntryReservations->isEmpty())
|
||||||
&& (($includedVariantId !== null && $variant->id === $includedVariantId)
|
&& (($includedVariantId !== null && $variant->id === $includedVariantId)
|
||||||
|| ($variant->isSellable() && (
|
|| ($variant->isSellable() && (
|
||||||
$this->inventory_policy === InventoryPolicy::Unlimited
|
$this->inventory_policy === InventoryPolicy::Unlimited
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
|
|||||||
'sold_units',
|
'sold_units',
|
||||||
'refunded_units',
|
'refunded_units',
|
||||||
'reserved_stock',
|
'reserved_stock',
|
||||||
|
'entry_reserved_stock',
|
||||||
'real_stock',
|
'real_stock',
|
||||||
])]
|
])]
|
||||||
class Inventory extends Model
|
class Inventory extends Model
|
||||||
@@ -35,6 +36,7 @@ class Inventory extends Model
|
|||||||
'sold_units' => 'integer',
|
'sold_units' => 'integer',
|
||||||
'refunded_units' => 'integer',
|
'refunded_units' => 'integer',
|
||||||
'reserved_stock' => 'integer',
|
'reserved_stock' => 'integer',
|
||||||
|
'entry_reserved_stock' => 'integer',
|
||||||
'real_stock' => 'integer',
|
'real_stock' => 'integer',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -59,7 +61,26 @@ class Inventory extends Model
|
|||||||
|
|
||||||
public function availableStock(): int
|
public function availableStock(): int
|
||||||
{
|
{
|
||||||
return max(0, $this->real_stock - $this->reserved_stock);
|
return max(0, $this->real_stock - $this->reserved_stock - $this->entry_reserved_stock);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reserveEntry(int $amount, bool $tracksInventory): void
|
||||||
|
{
|
||||||
|
if ($amount < 1 || ($tracksInventory && $this->availableStock() < $amount)) {
|
||||||
|
throw new \InvalidArgumentException('No hay stock disponible para la reserva de entradas.');
|
||||||
|
}
|
||||||
|
$this->entry_reserved_stock += $amount;
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function releaseEntry(int $amount): void
|
||||||
|
{
|
||||||
|
if ($amount < 1 || $this->entry_reserved_stock < $amount) {
|
||||||
|
throw new \InvalidArgumentException('La cantidad de entradas reservadas no es válida.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entry_reserved_stock -= $amount;
|
||||||
|
$this->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function reserve(int $amount, bool $tracksInventory): void
|
public function reserve(int $amount, bool $tracksInventory): void
|
||||||
@@ -92,7 +113,7 @@ class Inventory extends Model
|
|||||||
throw new \InvalidArgumentException('La cantidad reservada no alcanza para confirmar la compra.');
|
throw new \InvalidArgumentException('La cantidad reservada no alcanza para confirmar la compra.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($tracksInventory && $this->real_stock < $amount) {
|
if ($tracksInventory && $this->real_stock - $this->entry_reserved_stock < $amount) {
|
||||||
throw new \InvalidArgumentException('No hay suficiente stock real para confirmar la compra.');
|
throw new \InvalidArgumentException('No hay suficiente stock real para confirmar la compra.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Domains\Commerce\Catalog\Models;
|
namespace App\Domains\Commerce\Catalog\Models;
|
||||||
|
|
||||||
use App\Shared\Attachable\Models\Attachment;
|
use App\Domains\Ticketing\Desfile\Models\EntryReservation;
|
||||||
use App\Domains\Ticketing\Event\Models\EventDate;
|
use App\Domains\Ticketing\Event\Models\EventDate;
|
||||||
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
||||||
|
use App\Shared\Attachable\Models\Attachment;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
@@ -74,6 +75,12 @@ class Variant extends Model
|
|||||||
return $this->hasMany(Ticket::class, 'source_variant_id');
|
return $this->hasMany(Ticket::class, 'source_variant_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return HasMany<EntryReservation, $this> */
|
||||||
|
public function desfileEntryReservations(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(EntryReservation::class);
|
||||||
|
}
|
||||||
|
|
||||||
/** @return BelongsTo<Inventory, $this> */
|
/** @return BelongsTo<Inventory, $this> */
|
||||||
public function inventory(): BelongsTo
|
public function inventory(): BelongsTo
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
|||||||
use App\Domains\Commerce\Catalog\Models\ItemAttribute;
|
use App\Domains\Commerce\Catalog\Models\ItemAttribute;
|
||||||
use App\Domains\Commerce\Catalog\Models\Variant;
|
use App\Domains\Commerce\Catalog\Models\Variant;
|
||||||
use App\Domains\Commerce\Catalog\Services\CatalogItemAllowanceService;
|
use App\Domains\Commerce\Catalog\Services\CatalogItemAllowanceService;
|
||||||
use App\Shared\Enums\FieldType;
|
|
||||||
use App\Domains\Ticketing\Ticket\Resources\ValidityTimeResource;
|
use App\Domains\Ticketing\Ticket\Resources\ValidityTimeResource;
|
||||||
|
use App\Shared\Enums\FieldType;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
@@ -140,7 +140,7 @@ class CatalogItemDetailResource extends JsonResource
|
|||||||
'validity_time_id' => $option->validity_time_id,
|
'validity_time_id' => $option->validity_time_id,
|
||||||
'validity_time' => $option->validityTime === null
|
'validity_time' => $option->validityTime === null
|
||||||
? null
|
? null
|
||||||
: ValidityTimeResource::make($option->validityTime),
|
: ValidityTimeResource::make($option->validityTime, $this->tenant->timezone),
|
||||||
'metadata' => $option->metadata,
|
'metadata' => $option->metadata,
|
||||||
])
|
])
|
||||||
->values();
|
->values();
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ class CatalogInventoryService
|
|||||||
|
|
||||||
if ($operation === 'commit'
|
if ($operation === 'commit'
|
||||||
&& $requirement['tracks_inventory']
|
&& $requirement['tracks_inventory']
|
||||||
&& $inventory->real_stock < $requiredQuantity) {
|
&& $inventory->real_stock - $inventory->entry_reserved_stock < $requiredQuantity) {
|
||||||
throw new \InvalidArgumentException('No hay suficiente stock real para confirmar la compra.');
|
throw new \InvalidArgumentException('No hay suficiente stock real para confirmar la compra.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Domains\Commerce\Catalog\Services;
|
namespace App\Domains\Commerce\Catalog\Services;
|
||||||
|
|
||||||
use App\Shared\Attachable\Models\Attachment;
|
|
||||||
use App\Shared\Attachable\Services\AttachmentService;
|
|
||||||
use App\Domains\Commerce\Catalog\Enums\CatalogItemType;
|
use App\Domains\Commerce\Catalog\Enums\CatalogItemType;
|
||||||
use App\Domains\Commerce\Catalog\Models\Attribute;
|
use App\Domains\Commerce\Catalog\Models\Attribute;
|
||||||
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
||||||
@@ -11,8 +9,10 @@ use App\Domains\Commerce\Catalog\Models\Category;
|
|||||||
use App\Domains\Commerce\Catalog\Models\Inventory;
|
use App\Domains\Commerce\Catalog\Models\Inventory;
|
||||||
use App\Domains\Commerce\Catalog\Models\ItemAttribute;
|
use App\Domains\Commerce\Catalog\Models\ItemAttribute;
|
||||||
use App\Domains\Commerce\Catalog\Models\Variant;
|
use App\Domains\Commerce\Catalog\Models\Variant;
|
||||||
use App\Domains\Ticketing\Event\Models\EventDate;
|
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Ticketing\Event\Models\EventDate;
|
||||||
|
use App\Shared\Attachable\Models\Attachment;
|
||||||
|
use App\Shared\Attachable\Services\AttachmentService;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Pagination\LengthAwarePaginator;
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
@@ -286,9 +286,11 @@ class CatalogService
|
|||||||
int $perPage,
|
int $perPage,
|
||||||
int $page,
|
int $page,
|
||||||
): LengthAwarePaginator {
|
): LengthAwarePaginator {
|
||||||
|
$categoryIds = $this->categoryAndDescendantIds($tenant, $category);
|
||||||
|
|
||||||
return CatalogItem::query()
|
return CatalogItem::query()
|
||||||
->forTenantCatalog($tenant)
|
->forTenantCatalog($tenant)
|
||||||
->where('category_id', $category->id)
|
->whereIn('category_id', $categoryIds)
|
||||||
->whereAvailable()
|
->whereAvailable()
|
||||||
->with([
|
->with([
|
||||||
'attachments',
|
'attachments',
|
||||||
@@ -306,6 +308,34 @@ class CatalogService
|
|||||||
->paginate(perPage: $perPage, pageName: 'page', page: $page);
|
->paginate(perPage: $perPage, pageName: 'page', page: $page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return array<int, int> */
|
||||||
|
private function categoryAndDescendantIds(Tenant $tenant, Category $category): array
|
||||||
|
{
|
||||||
|
$childrenByParent = Category::query()
|
||||||
|
->where('tenant_code', $tenant->codigo)
|
||||||
|
->get(['id', 'categoria_id'])
|
||||||
|
->groupBy('categoria_id');
|
||||||
|
|
||||||
|
$categoryIds = [];
|
||||||
|
$pendingIds = [$category->id];
|
||||||
|
|
||||||
|
while ($pendingIds !== []) {
|
||||||
|
$categoryId = array_shift($pendingIds);
|
||||||
|
|
||||||
|
if (isset($categoryIds[$categoryId])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$categoryIds[$categoryId] = $categoryId;
|
||||||
|
|
||||||
|
foreach ($childrenByParent->get($categoryId, collect()) as $child) {
|
||||||
|
$pendingIds[] = $child->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values($categoryIds);
|
||||||
|
}
|
||||||
|
|
||||||
public function delete(CatalogItem $catalogItem): void
|
public function delete(CatalogItem $catalogItem): void
|
||||||
{
|
{
|
||||||
DB::transaction(function () use ($catalogItem): void {
|
DB::transaction(function () use ($catalogItem): void {
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ class StockReservationService
|
|||||||
$inventory = $inventories->get($line->inventory_id)
|
$inventory = $inventories->get($line->inventory_id)
|
||||||
?? throw new \InvalidArgumentException('No se encontró el inventario reservado.');
|
?? throw new \InvalidArgumentException('No se encontró el inventario reservado.');
|
||||||
if ($inventory->reserved_stock < $line->quantity
|
if ($inventory->reserved_stock < $line->quantity
|
||||||
|| ($line->tracks_inventory && $inventory->real_stock < $line->quantity)) {
|
|| ($line->tracks_inventory && $inventory->real_stock - $inventory->entry_reserved_stock < $line->quantity)) {
|
||||||
throw new \InvalidArgumentException('La reserva de stock no alcanza para confirmar la compra.');
|
throw new \InvalidArgumentException('La reserva de stock no alcanza para confirmar la compra.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use App\Domains\Commerce\Catalog\Models\Inventory;
|
|||||||
use App\Domains\Commerce\Catalog\Models\StockReservation;
|
use App\Domains\Commerce\Catalog\Models\StockReservation;
|
||||||
use App\Domains\Commerce\Catalog\Models\StockReservationLine;
|
use App\Domains\Commerce\Catalog\Models\StockReservationLine;
|
||||||
use App\Domains\Commerce\Catalog\Models\Variant;
|
use App\Domains\Commerce\Catalog\Models\Variant;
|
||||||
|
use App\Domains\Ticketing\Desfile\Models\EntryReservation;
|
||||||
use App\Domains\Ticketing\Event\Models\EventDate;
|
use App\Domains\Ticketing\Event\Models\EventDate;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
@@ -190,6 +191,7 @@ class VariantReplacementService
|
|||||||
'sold_units' => $sourceInventory->sold_units,
|
'sold_units' => $sourceInventory->sold_units,
|
||||||
'refunded_units' => $sourceInventory->refunded_units,
|
'refunded_units' => $sourceInventory->refunded_units,
|
||||||
'reserved_stock' => $reservedStock,
|
'reserved_stock' => $reservedStock,
|
||||||
|
'entry_reserved_stock' => $sourceInventory->entry_reserved_stock,
|
||||||
'real_stock' => $sourceInventory->real_stock,
|
'real_stock' => $sourceInventory->real_stock,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -198,7 +200,9 @@ class VariantReplacementService
|
|||||||
->whereKey($activeLines->modelKeys())
|
->whereKey($activeLines->modelKeys())
|
||||||
->update(['inventory_id' => $replacementInventory->getKey()]);
|
->update(['inventory_id' => $replacementInventory->getKey()]);
|
||||||
}
|
}
|
||||||
$sourceInventory->update(['reserved_stock' => 0]);
|
EntryReservation::query()->where('inventory_id', $sourceInventory->id)
|
||||||
|
->update(['inventory_id' => $replacementInventory->id]);
|
||||||
|
$sourceInventory->update(['reserved_stock' => 0, 'entry_reserved_stock' => 0]);
|
||||||
|
|
||||||
return $replacementInventory;
|
return $replacementInventory;
|
||||||
}
|
}
|
||||||
@@ -235,6 +239,7 @@ class VariantReplacementService
|
|||||||
$destinationInventory->update([
|
$destinationInventory->update([
|
||||||
'real_stock' => $destinationInventory->real_stock + $sourceInventory->real_stock,
|
'real_stock' => $destinationInventory->real_stock + $sourceInventory->real_stock,
|
||||||
'reserved_stock' => $destinationInventory->reserved_stock + $sourceInventory->reserved_stock,
|
'reserved_stock' => $destinationInventory->reserved_stock + $sourceInventory->reserved_stock,
|
||||||
|
'entry_reserved_stock' => $destinationInventory->entry_reserved_stock + $sourceInventory->entry_reserved_stock,
|
||||||
'sold_units' => $destinationInventory->sold_units + $sourceInventory->sold_units,
|
'sold_units' => $destinationInventory->sold_units + $sourceInventory->sold_units,
|
||||||
'refunded_units' => $destinationInventory->refunded_units + $sourceInventory->refunded_units,
|
'refunded_units' => $destinationInventory->refunded_units + $sourceInventory->refunded_units,
|
||||||
]);
|
]);
|
||||||
@@ -243,9 +248,12 @@ class VariantReplacementService
|
|||||||
->whereKey($activeLines->modelKeys())
|
->whereKey($activeLines->modelKeys())
|
||||||
->update(['inventory_id' => $destinationInventory->getKey()]);
|
->update(['inventory_id' => $destinationInventory->getKey()]);
|
||||||
}
|
}
|
||||||
|
EntryReservation::query()->where('inventory_id', $sourceInventory->id)
|
||||||
|
->update(['inventory_id' => $destinationInventory->id]);
|
||||||
$sourceInventory->update([
|
$sourceInventory->update([
|
||||||
'real_stock' => 0,
|
'real_stock' => 0,
|
||||||
'reserved_stock' => 0,
|
'reserved_stock' => 0,
|
||||||
|
'entry_reserved_stock' => 0,
|
||||||
'sold_units' => 0,
|
'sold_units' => 0,
|
||||||
'refunded_units' => 0,
|
'refunded_units' => 0,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ class TenantTransactionResetService
|
|||||||
'carts' => $scope['cart_ids']->count(),
|
'carts' => $scope['cart_ids']->count(),
|
||||||
'cart_items' => $scope['cart_item_ids']->count(),
|
'cart_items' => $scope['cart_item_ids']->count(),
|
||||||
'tickets' => DB::table('tickets')->where('tenant_code', $tenantCode)->count(),
|
'tickets' => DB::table('tickets')->where('tenant_code', $tenantCode)->count(),
|
||||||
|
'entry_reservations' => DB::table('desfile_entry_reservations')
|
||||||
|
->whereIn('variant_id', DB::table('variantes')->whereIn('catalog_item_id',
|
||||||
|
DB::table('catalog_items')->where('tenant_code', $tenantCode)->select('id'))->select('id'))->count(),
|
||||||
'stock_reservations' => $this->reservationQuery($scope)->count(),
|
'stock_reservations' => $this->reservationQuery($scope)->count(),
|
||||||
'purchase_changes' => DB::table('value_changes')
|
'purchase_changes' => DB::table('value_changes')
|
||||||
->where('tenant_code', $tenantCode)
|
->where('tenant_code', $tenantCode)
|
||||||
@@ -47,6 +50,10 @@ class TenantTransactionResetService
|
|||||||
$telepagosQr = DB::table('telepagos_qr')->whereIn('compra_id', $scope['purchase_ids'])->count();
|
$telepagosQr = DB::table('telepagos_qr')->whereIn('compra_id', $scope['purchase_ids'])->count();
|
||||||
$summary = [
|
$summary = [
|
||||||
'stock_reservations_deleted' => $this->reservationQuery($scope)->delete(),
|
'stock_reservations_deleted' => $this->reservationQuery($scope)->delete(),
|
||||||
|
'entry_reservations_deleted' => DB::table('desfile_entry_reservations')
|
||||||
|
->whereIn('variant_id', DB::table('variantes')->whereIn('catalog_item_id',
|
||||||
|
DB::table('catalog_items')->where('tenant_code', $tenantCode)->select('id'))->select('id'))->delete(),
|
||||||
|
'entry_reservation_batches_deleted' => DB::table('desfile_reservation_batches')->where('tenant_code', $tenantCode)->delete(),
|
||||||
'tickets_deleted' => DB::table('tickets')->where('tenant_code', $tenantCode)->delete(),
|
'tickets_deleted' => DB::table('tickets')->where('tenant_code', $tenantCode)->delete(),
|
||||||
'purchase_changes_deleted' => DB::table('value_changes')
|
'purchase_changes_deleted' => DB::table('value_changes')
|
||||||
->where('tenant_code', $tenantCode)
|
->where('tenant_code', $tenantCode)
|
||||||
@@ -67,6 +74,7 @@ class TenantTransactionResetService
|
|||||||
->update([
|
->update([
|
||||||
'real_stock' => DB::raw('real_stock + sold_units - refunded_units'),
|
'real_stock' => DB::raw('real_stock + sold_units - refunded_units'),
|
||||||
'reserved_stock' => 0,
|
'reserved_stock' => 0,
|
||||||
|
'entry_reserved_stock' => 0,
|
||||||
'sold_units' => 0,
|
'sold_units' => 0,
|
||||||
'refunded_units' => 0,
|
'refunded_units' => 0,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -3,11 +3,8 @@
|
|||||||
namespace App\Domains\Core\Tenant\Controllers;
|
namespace App\Domains\Core\Tenant\Controllers;
|
||||||
|
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
use App\Domains\Core\Tenant\Requests\StoreTenantRequest;
|
|
||||||
use App\Domains\Core\Tenant\Requests\UpdateTenantRequest;
|
|
||||||
use App\Domains\Core\Tenant\Resources\TenantResource;
|
use App\Domains\Core\Tenant\Resources\TenantResource;
|
||||||
use App\Domains\Core\Tenant\Services\TenantInformationService;
|
use App\Domains\Core\Tenant\Services\TenantInformationService;
|
||||||
use App\Domains\Core\Tenant\Services\TenantService;
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
@@ -15,7 +12,6 @@ use Illuminate\Http\Response;
|
|||||||
class TenantController extends Controller
|
class TenantController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected TenantService $tenantService,
|
|
||||||
protected TenantInformationService $tenantInformationService,
|
protected TenantInformationService $tenantInformationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -30,15 +26,6 @@ class TenantController extends Controller
|
|||||||
return TenantResource::collection($tenants)->response();
|
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
|
public function show(Tenant $tenant): TenantResource
|
||||||
{
|
{
|
||||||
return TenantResource::make(
|
return TenantResource::make(
|
||||||
@@ -46,15 +33,6 @@ class TenantController extends Controller
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
public function destroy(Tenant $tenant): Response
|
||||||
{
|
{
|
||||||
$tenant->delete();
|
$tenant->delete();
|
||||||
|
|||||||
@@ -2,20 +2,20 @@
|
|||||||
|
|
||||||
namespace App\Domains\Core\Tenant\Models;
|
namespace App\Domains\Core\Tenant\Models;
|
||||||
|
|
||||||
use App\Shared\Attachable\Models\Attachment;
|
|
||||||
use App\Domains\Commerce\Catalog\Enums\GroupLayout;
|
use App\Domains\Commerce\Catalog\Enums\GroupLayout;
|
||||||
use App\Domains\Commerce\Catalog\Enums\ProductLayout;
|
use App\Domains\Commerce\Catalog\Enums\ProductLayout;
|
||||||
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
||||||
use App\Domains\Commerce\Catalog\Models\Category;
|
use App\Domains\Commerce\Catalog\Models\Category;
|
||||||
use App\Domains\Core\Client\Models\Client;
|
use App\Domains\Core\Client\Models\Client;
|
||||||
use App\Domains\Ticketing\Event\Models\EventDate;
|
|
||||||
use App\Domains\Ticketing\Event\Models\EventDateChange;
|
|
||||||
use App\Domains\Ticketing\Event\Models\Event;
|
|
||||||
use App\Domains\Ticketing\Event\Models\EventCategory;
|
|
||||||
use App\Domains\Core\Menu\Models\Menu;
|
use App\Domains\Core\Menu\Models\Menu;
|
||||||
use App\Domains\Core\Menu\Models\TenantMenu;
|
use App\Domains\Core\Menu\Models\TenantMenu;
|
||||||
use App\Domains\Core\Tenant\Enums\CartEditingPolicy;
|
use App\Domains\Core\Tenant\Enums\CartEditingPolicy;
|
||||||
|
use App\Domains\Ticketing\Event\Models\Event;
|
||||||
|
use App\Domains\Ticketing\Event\Models\EventCategory;
|
||||||
|
use App\Domains\Ticketing\Event\Models\EventDate;
|
||||||
|
use App\Domains\Ticketing\Event\Models\EventDateChange;
|
||||||
use App\Domains\Ticketing\Ticket\Models\ScanAttempt;
|
use App\Domains\Ticketing\Ticket\Models\ScanAttempt;
|
||||||
|
use App\Shared\Attachable\Models\Attachment;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
@@ -28,6 +28,7 @@ use Illuminate\Support\Facades\Schema;
|
|||||||
'client_id',
|
'client_id',
|
||||||
'codigo',
|
'codigo',
|
||||||
'nombre',
|
'nombre',
|
||||||
|
'timezone',
|
||||||
'dominio',
|
'dominio',
|
||||||
'base_path',
|
'base_path',
|
||||||
'site_title',
|
'site_title',
|
||||||
@@ -66,7 +67,10 @@ class Tenant extends Model
|
|||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
public const DEFAULT_TIMEZONE = 'America/Argentina/Buenos_Aires';
|
||||||
|
|
||||||
protected $attributes = [
|
protected $attributes = [
|
||||||
|
'timezone' => self::DEFAULT_TIMEZONE,
|
||||||
'base_path' => '/',
|
'base_path' => '/',
|
||||||
'search_product_layout' => ProductLayout::ColumnWithImage->value,
|
'search_product_layout' => ProductLayout::ColumnWithImage->value,
|
||||||
'search_group_layout' => GroupLayout::Paginated->value,
|
'search_group_layout' => GroupLayout::Paginated->value,
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Core\Tenant\Requests;
|
|
||||||
|
|
||||||
use App\Domains\Commerce\Catalog\Enums\GroupLayout;
|
|
||||||
use App\Domains\Commerce\Catalog\Enums\ProductLayout;
|
|
||||||
use App\Shared\Rules\ImageOrBase64Rule;
|
|
||||||
use App\Domains\Core\Tenant\Enums\CartEditingPolicy;
|
|
||||||
use App\Domains\Core\Tenant\Services\WebsiteExtraService;
|
|
||||||
use App\Domains\Core\Tenant\Support\TenantDomainNormalizer;
|
|
||||||
use Closure;
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Illuminate\Validation\Rule;
|
|
||||||
|
|
||||||
class StoreTenantRequest extends FormRequest
|
|
||||||
{
|
|
||||||
protected bool $hasInvalidDomain = false;
|
|
||||||
|
|
||||||
protected bool $hasInvalidBasePath = false;
|
|
||||||
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function prepareForValidation(): void
|
|
||||||
{
|
|
||||||
$rawDomain = $this->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<string, mixed>
|
|
||||||
*/
|
|
||||||
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',
|
|
||||||
],
|
|
||||||
'storefront_website_type_code' => [
|
|
||||||
'required_with:extras',
|
|
||||||
'sometimes',
|
|
||||||
'string',
|
|
||||||
Rule::exists('storefront_website_types', 'codigo'),
|
|
||||||
],
|
|
||||||
'admin_website_type_code' => ['sometimes', 'nullable', 'string', Rule::exists('admin_website_types', 'codigo')],
|
|
||||||
], app(WebsiteExtraService::class)->requestRules($this->input('storefront_website_type_code')));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Core\Tenant\Requests;
|
|
||||||
|
|
||||||
use App\Domains\Commerce\Catalog\Enums\GroupLayout;
|
|
||||||
use App\Domains\Commerce\Catalog\Enums\ProductLayout;
|
|
||||||
use App\Shared\Rules\ImageOrBase64Rule;
|
|
||||||
use App\Domains\Core\Tenant\Enums\CartEditingPolicy;
|
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
|
||||||
use App\Domains\Core\Tenant\Support\TenantDomainNormalizer;
|
|
||||||
use Closure;
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Illuminate\Validation\Rule;
|
|
||||||
|
|
||||||
class UpdateTenantRequest extends FormRequest
|
|
||||||
{
|
|
||||||
protected bool $hasInvalidDomain = false;
|
|
||||||
|
|
||||||
protected bool $hasInvalidBasePath = false;
|
|
||||||
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function prepareForValidation(): void
|
|
||||||
{
|
|
||||||
if ($this->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<string, mixed>
|
|
||||||
*/
|
|
||||||
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',
|
|
||||||
],
|
|
||||||
'admin_website_type_code' => ['sometimes', 'nullable', 'string', Rule::exists('admin_website_types', 'codigo')],
|
|
||||||
'storefront_website_type_code' => [
|
|
||||||
'sometimes',
|
|
||||||
'nullable',
|
|
||||||
'string',
|
|
||||||
Rule::exists('storefront_website_types', 'codigo'),
|
|
||||||
function (string $attribute, mixed $value, Closure $fail) use ($tenant): void {
|
|
||||||
if ($tenant && $value !== $tenant->storefront_website_type_code && $tenant->websiteExtras()->exists()) {
|
|
||||||
$fail('The storefront website type cannot be changed while the tenant has extras.');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -35,6 +35,7 @@ class TenantResource extends JsonResource
|
|||||||
'client_id' => $this->client_id,
|
'client_id' => $this->client_id,
|
||||||
'codigo' => $this->codigo,
|
'codigo' => $this->codigo,
|
||||||
'nombre' => $this->nombre,
|
'nombre' => $this->nombre,
|
||||||
|
'timezone' => $this->timezone,
|
||||||
'dominio' => $this->dominio,
|
'dominio' => $this->dominio,
|
||||||
'base_path' => $this->base_path,
|
'base_path' => $this->base_path,
|
||||||
'site_title' => $this->site_title ?? 'ShopitFront',
|
'site_title' => $this->site_title ?? 'ShopitFront',
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Domains\Core\Tenant\Services;
|
namespace App\Domains\Core\Tenant\Services;
|
||||||
|
|
||||||
|
use App\Domains\Core\Tenant\Models\AdminWebsiteType;
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
use App\Shared\Attachable\Models\Attachment;
|
use App\Shared\Attachable\Models\Attachment;
|
||||||
use App\Shared\Attachable\Services\AttachmentService;
|
use App\Shared\Attachable\Services\AttachmentService;
|
||||||
use App\Domains\Core\Tenant\Models\AdminWebsiteType;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
|||||||
125
app/Domains/Core/Tenant/Services/TenantProvisioningService.php
Normal file
125
app/Domains/Core/Tenant/Services/TenantProvisioningService.php
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Core\Tenant\Services;
|
||||||
|
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use App\Shared\Attachable\Models\Attachment;
|
||||||
|
use App\Shared\Attachable\Services\AttachmentService;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal provisioning used by migrations and seeders.
|
||||||
|
*
|
||||||
|
* General tenant writes are intentionally not exposed through the HTTP API.
|
||||||
|
*/
|
||||||
|
class TenantProvisioningService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected AttachmentService $attachmentService,
|
||||||
|
protected WebsiteExtraService $websiteExtraService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $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<string, mixed> $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<int, array{code: string, url: string, orden?: int}> $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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Core\Tenant\Services;
|
|
||||||
|
|
||||||
use App\Shared\Attachable\Models\Attachment;
|
|
||||||
use App\Shared\Attachable\Services\AttachmentService;
|
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
|
|
||||||
class TenantService
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
protected AttachmentService $attachmentService,
|
|
||||||
protected WebsiteExtraService $websiteExtraService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new tenant and store its logos.
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $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<string, mixed> $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<int, array{code: string, url: string, orden?: int}> $socialMedia
|
|
||||||
*/
|
|
||||||
private function syncSocialMedia(Tenant $tenant, array $socialMedia): void
|
|
||||||
{
|
|
||||||
$owner = $tenant->activeEvent ?? $tenant;
|
|
||||||
$associations = [];
|
|
||||||
|
|
||||||
foreach (array_values($socialMedia) as $index => $item) {
|
|
||||||
$associations[$item['code']] = [
|
|
||||||
'url' => $item['url'],
|
|
||||||
'orden' => $item['orden'] ?? $index,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$owner->socialMedia()->sync($associations);
|
|
||||||
$owner->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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,16 +2,14 @@
|
|||||||
|
|
||||||
namespace App\Domains\Core\Tenant\Services;
|
namespace App\Domains\Core\Tenant\Services;
|
||||||
|
|
||||||
|
use App\Domains\Core\Tenant\Models\StorefrontWebsiteTypeExtra;
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Core\Tenant\Models\WebsiteExtra;
|
||||||
use App\Shared\Attachable\Enums\AttachmentType;
|
use App\Shared\Attachable\Enums\AttachmentType;
|
||||||
use App\Shared\Attachable\Models\Attachment;
|
use App\Shared\Attachable\Models\Attachment;
|
||||||
use App\Shared\Attachable\Services\AttachmentService;
|
use App\Shared\Attachable\Services\AttachmentService;
|
||||||
use App\Shared\Rules\CroppedImageOrBase64Rule;
|
use App\Shared\Rules\CroppedImageOrBase64Rule;
|
||||||
use App\Shared\Rules\ImageOrBase64Rule;
|
use App\Shared\Rules\ImageOrBase64Rule;
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
|
||||||
use App\Domains\Core\Tenant\Models\WebsiteExtra;
|
|
||||||
use App\Domains\Core\Tenant\Models\StorefrontWebsiteType;
|
|
||||||
use App\Domains\Core\Tenant\Models\StorefrontWebsiteTypeExtra;
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
@@ -21,109 +19,6 @@ class WebsiteExtraService
|
|||||||
{
|
{
|
||||||
public function __construct(protected AttachmentService $attachmentService) {}
|
public function __construct(protected AttachmentService $attachmentService) {}
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the request rules declared by the selected website type.
|
|
||||||
*
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
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 (StorefrontWebsiteTypeExtra $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<string, mixed> $extras
|
|
||||||
*/
|
|
||||||
public function createForTenant(Tenant $tenant, array $extras): void
|
|
||||||
{
|
|
||||||
if ($extras === []) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$definitions = $this->definitionsFor((string) $tenant->storefront_website_type_code)
|
|
||||||
->keyBy('codigo');
|
|
||||||
|
|
||||||
foreach ($extras as $name => $config) {
|
|
||||||
/** @var StorefrontWebsiteTypeExtra|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.
|
* Build request rules for one extra addressed by its stable code.
|
||||||
*
|
*
|
||||||
@@ -201,19 +96,6 @@ class WebsiteExtraService
|
|||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Collection<int, StorefrontWebsiteTypeExtra>
|
|
||||||
*/
|
|
||||||
private function definitionsFor(string $websiteTypeCode): Collection
|
|
||||||
{
|
|
||||||
$websiteType = StorefrontWebsiteType::query()
|
|
||||||
->where('codigo', $websiteTypeCode)
|
|
||||||
->with('extras')
|
|
||||||
->first();
|
|
||||||
|
|
||||||
return $websiteType?->extras ?? collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string|array<int, mixed> $rules
|
* @param string|array<int, mixed> $rules
|
||||||
* @return array<int, mixed>
|
* @return array<int, mixed>
|
||||||
@@ -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
|
private function configAttribute(string $root, string $path): string
|
||||||
{
|
{
|
||||||
if ($path === '$') {
|
if ($path === '$') {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ Es la raíz del modelo multi-tenant. Gestiona organizaciones/sitios, tipos de we
|
|||||||
|
|
||||||
## Servicios
|
## Servicios
|
||||||
|
|
||||||
- `TenantService`: crea y actualiza tenants, incluyendo sus recursos asociados.
|
|
||||||
- `TenantInformationService`: carga un tenant y las relaciones requeridas por cada contexto.
|
- `TenantInformationService`: carga un tenant y las relaciones requeridas por cada contexto.
|
||||||
- `AdminWebsiteTypeService`: crea o actualiza los tipos de admin y su marca.
|
- `AdminWebsiteTypeService`: crea o actualiza los tipos de admin y su marca.
|
||||||
- `WebsiteExtraService`: construye reglas dinámicas, crea, actualiza y habilita/deshabilita extras.
|
- `WebsiteExtraService`: construye reglas dinámicas, crea, actualiza y habilita/deshabilita extras.
|
||||||
@@ -25,7 +24,8 @@ El par `(dominio, base_path)` es único. Un mismo dominio puede alojar el tenant
|
|||||||
|
|
||||||
## Endpoints
|
## Endpoints
|
||||||
|
|
||||||
- Recurso REST público/administrativo `/tenants`.
|
- `GET /tenants`, `GET /tenants/{codigo}` y `DELETE /tenants/{codigo}`.
|
||||||
|
- La creación y actualización general de tenants se administran mediante seeders o base de datos; no se exponen por API.
|
||||||
- Bajo `/v1/adminapp/tenant/website-extras`, con autenticación y contexto de tenant: consulta general, detalle, actualización y activación/desactivación.
|
- Bajo `/v1/adminapp/tenant/website-extras`, con autenticación y contexto de tenant: consulta general, detalle, actualización y activación/desactivación.
|
||||||
|
|
||||||
## Dependencias y reglas
|
## Dependencias y reglas
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
use App\Domains\Core\Tenant\Controllers\TenantController;
|
use App\Domains\Core\Tenant\Controllers\TenantController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::apiResource('tenants', TenantController::class);
|
Route::apiResource('tenants', TenantController::class)->except(['store', 'update']);
|
||||||
|
|
||||||
require __DIR__.'/adminapp.php';
|
require __DIR__.'/adminapp.php';
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Ticketing\Desfile\Controllers;
|
||||||
|
|
||||||
|
use App\Domains\Ticketing\Desfile\Requests\ExportEntryReservationsRequest;
|
||||||
|
use App\Domains\Ticketing\Desfile\Requests\IndexEntryReservationsRequest;
|
||||||
|
use App\Domains\Ticketing\Desfile\Requests\StoreEntryReservationsRequest;
|
||||||
|
use App\Domains\Ticketing\Desfile\Resources\EntryReservationResource;
|
||||||
|
use App\Domains\Ticketing\Desfile\Services\EntryReservationExcelService;
|
||||||
|
use App\Domains\Ticketing\Desfile\Services\EntryReservationPdfService;
|
||||||
|
use App\Domains\Ticketing\Desfile\Services\EntryReservationService;
|
||||||
|
use App\Domains\Ticketing\Ticket\Services\TicketPdfService;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
|
||||||
|
class EntryReservationController extends Controller
|
||||||
|
{
|
||||||
|
public function index(IndexEntryReservationsRequest $request, EntryReservationService $service): AnonymousResourceCollection
|
||||||
|
{
|
||||||
|
return EntryReservationResource::collection(
|
||||||
|
$service->reservations($request->user(), $request->validated()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function downloadPdf(
|
||||||
|
ExportEntryReservationsRequest $request,
|
||||||
|
EntryReservationService $service,
|
||||||
|
EntryReservationPdfService $pdf,
|
||||||
|
): Response {
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
return $pdf->download(
|
||||||
|
$user->tenant()->firstOrFail(),
|
||||||
|
$service->reservationsForExport($user, $request->validated()),
|
||||||
|
$request->validated('timezone'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function downloadExcel(
|
||||||
|
ExportEntryReservationsRequest $request,
|
||||||
|
EntryReservationService $service,
|
||||||
|
EntryReservationExcelService $excel,
|
||||||
|
): StreamedResponse {
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
return $excel->download(
|
||||||
|
$user->tenant()->firstOrFail(),
|
||||||
|
$service->reservationsForExport($user, $request->validated()),
|
||||||
|
$request->validated('timezone'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function downloadTicketPdf(
|
||||||
|
IndexEntryReservationsRequest $request,
|
||||||
|
int $reservation,
|
||||||
|
EntryReservationService $service,
|
||||||
|
TicketPdfService $pdf,
|
||||||
|
): Response {
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
return $pdf->download(
|
||||||
|
$user->tenant()->firstOrFail(),
|
||||||
|
collect([$service->reservationTicket($user, $reservation)]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(StoreEntryReservationsRequest $request, EntryReservationService $service): AnonymousResourceCollection
|
||||||
|
{
|
||||||
|
return EntryReservationResource::collection($service->reserve(
|
||||||
|
$request->user(), $request->validated('idempotency_key'), $request->validated('rows'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(
|
||||||
|
IndexEntryReservationsRequest $request,
|
||||||
|
int $reservation,
|
||||||
|
EntryReservationService $service,
|
||||||
|
): Response {
|
||||||
|
$service->cancel($request->user(), $reservation);
|
||||||
|
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Ticketing\Desfile\Enums;
|
||||||
|
|
||||||
|
enum EntryReservationPaymentType: string
|
||||||
|
{
|
||||||
|
case Free = 'sin_cargo';
|
||||||
|
case Other = 'otro_metodo';
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::Free => 'Sin cargo',
|
||||||
|
self::Other => 'Otro método',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<array{value: string, label: string}> */
|
||||||
|
public static function options(): array
|
||||||
|
{
|
||||||
|
return array_map(
|
||||||
|
fn (self $type): array => [
|
||||||
|
'value' => $type->value,
|
||||||
|
'label' => $type->label(),
|
||||||
|
],
|
||||||
|
self::cases(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
54
app/Domains/Ticketing/Desfile/Models/EntryReservation.php
Normal file
54
app/Domains/Ticketing/Desfile/Models/EntryReservation.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Ticketing\Desfile\Models;
|
||||||
|
|
||||||
|
use App\Domains\Commerce\Catalog\Models\Inventory;
|
||||||
|
use App\Domains\Commerce\Catalog\Models\Variant;
|
||||||
|
use App\Domains\Ticketing\Desfile\Enums\EntryReservationPaymentType;
|
||||||
|
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
#[Fillable([
|
||||||
|
'variant_id',
|
||||||
|
'ticket_id',
|
||||||
|
'inventory_id',
|
||||||
|
'batch_id',
|
||||||
|
'fecha_reserva',
|
||||||
|
'importe',
|
||||||
|
'tipo_pago',
|
||||||
|
])]
|
||||||
|
class EntryReservation extends Model
|
||||||
|
{
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
|
protected $table = 'desfile_entry_reservations';
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'variant_id' => 'integer',
|
||||||
|
'fecha_reserva' => 'datetime',
|
||||||
|
'importe' => 'decimal:2',
|
||||||
|
'tipo_pago' => EntryReservationPaymentType::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<Variant, $this> */
|
||||||
|
public function variant(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Variant::class)->withTrashed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ticket(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Ticket::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inventory(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Inventory::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Ticketing\Desfile\Requests;
|
||||||
|
|
||||||
|
use App\Shared\Rules\ValidTimezone;
|
||||||
|
|
||||||
|
class ExportEntryReservationsRequest extends IndexEntryReservationsRequest
|
||||||
|
{
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
...parent::rules(),
|
||||||
|
'timezone' => ['required', 'string', new ValidTimezone],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Ticketing\Desfile\Requests;
|
||||||
|
|
||||||
|
use App\Domains\Ticketing\Desfile\Enums\EntryReservationPaymentType;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class IndexEntryReservationsRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return $this->user()?->tenant_codigo === 'desfile_pura_tendencia';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'tipo_pago' => ['sometimes', 'nullable', Rule::enum(EntryReservationPaymentType::class)],
|
||||||
|
'page' => ['sometimes', 'integer', 'min:1'],
|
||||||
|
'per_page' => ['sometimes', 'integer', 'min:1', 'max:100'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Ticketing\Desfile\Requests;
|
||||||
|
|
||||||
|
use App\Domains\Ticketing\Desfile\Enums\EntryReservationPaymentType;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class StoreEntryReservationsRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return $this->user()?->tenant_codigo === 'desfile_pura_tendencia';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'idempotency_key' => ['required', 'uuid'],
|
||||||
|
'rows' => ['required', 'array', 'min:1', 'max:100'],
|
||||||
|
'rows.*' => ['required', 'array:variant_id,tipo_pago'],
|
||||||
|
'rows.*.variant_id' => ['required', 'integer', 'min:1', 'distinct'],
|
||||||
|
'rows.*.tipo_pago' => ['required', Rule::enum(EntryReservationPaymentType::class)],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Ticketing\Desfile\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class EntryReservationResource extends JsonResource
|
||||||
|
{
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
$selection = $this->relationLoaded('variant')
|
||||||
|
? $this->variant->selectionOptions()
|
||||||
|
: collect();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'variant_id' => $this->variant_id,
|
||||||
|
'ticket_id' => $this->ticket_id,
|
||||||
|
'fecha_reserva' => $this->fecha_reserva->toIso8601String(),
|
||||||
|
'tipo_pago' => $this->tipo_pago->value,
|
||||||
|
'tipo_pago_label' => $this->tipo_pago->label(),
|
||||||
|
'importe' => $this->importe,
|
||||||
|
'entrada' => [
|
||||||
|
'tipo' => $this->selectionLabel($selection->get('tipo')),
|
||||||
|
'sector' => $this->selectionLabel($selection->get('sector')),
|
||||||
|
'fila' => $this->selectionLabel($selection->get('fila')),
|
||||||
|
'asiento' => $this->selectionLabel($selection->get('asiento')),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function selectionLabel(mixed $selection): ?string
|
||||||
|
{
|
||||||
|
if (! is_array($selection)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_is_list($selection)) {
|
||||||
|
$labels = collect($selection)->pluck('label')->filter()->implode(', ');
|
||||||
|
|
||||||
|
return $labels !== '' ? $labels : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isset($selection['label']) ? (string) $selection['label'] : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Ticketing\Desfile\Services;
|
||||||
|
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Ticketing\Desfile\Models\EntryReservation;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||||
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
|
||||||
|
class EntryReservationExcelService
|
||||||
|
{
|
||||||
|
public function __construct(private readonly EntryReservationReportService $report) {}
|
||||||
|
|
||||||
|
/** @param Collection<int, EntryReservation> $reservations */
|
||||||
|
public function download(Tenant $tenant, Collection $reservations, string $timeZone): StreamedResponse
|
||||||
|
{
|
||||||
|
$generatedAt = now();
|
||||||
|
$rows = $this->report->rows($reservations);
|
||||||
|
$spreadsheet = new Spreadsheet;
|
||||||
|
$spreadsheet->getProperties()
|
||||||
|
->setCreator('Shopit')
|
||||||
|
->setTitle('Reservas de entradas')
|
||||||
|
->setSubject($tenant->nombre);
|
||||||
|
$sheet = $spreadsheet->getActiveSheet();
|
||||||
|
$sheet->setTitle('Reservas');
|
||||||
|
$sheet->fromArray([
|
||||||
|
'Tipo',
|
||||||
|
'Sector',
|
||||||
|
'Fila',
|
||||||
|
'Asiento',
|
||||||
|
'ID',
|
||||||
|
'Fecha',
|
||||||
|
'Importe',
|
||||||
|
'Pago',
|
||||||
|
], null, 'A1');
|
||||||
|
|
||||||
|
foreach ($rows as $index => $reservation) {
|
||||||
|
$row = $index + 2;
|
||||||
|
foreach (['A' => 'tipo', 'B' => 'sector', 'C' => 'fila', 'D' => 'asiento'] as $column => $key) {
|
||||||
|
$sheet->setCellValueExplicit("{$column}{$row}", $reservation[$key], DataType::TYPE_STRING);
|
||||||
|
}
|
||||||
|
$sheet->setCellValueExplicit(
|
||||||
|
"E{$row}",
|
||||||
|
$reservation['ticket_id'] === null ? '-' : (string) $reservation['ticket_id'],
|
||||||
|
DataType::TYPE_STRING,
|
||||||
|
);
|
||||||
|
$sheet->setCellValue(
|
||||||
|
"F{$row}",
|
||||||
|
Date::dateTimeToExcel($reservation['fecha_reserva']->copy()->timezone($timeZone)),
|
||||||
|
);
|
||||||
|
if ($reservation['importe'] !== null) {
|
||||||
|
$sheet->setCellValue("G{$row}", (float) $reservation['importe']);
|
||||||
|
}
|
||||||
|
$sheet->setCellValueExplicit("H{$row}", $reservation['pago'], DataType::TYPE_STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastRow = max(2, $rows->count() + 1);
|
||||||
|
$sheet->getStyle("F2:F{$lastRow}")->getNumberFormat()->setFormatCode('dd/mm/yyyy hh:mm');
|
||||||
|
$sheet->getStyle("G2:G{$lastRow}")->getNumberFormat()->setFormatCode('$ #,##0.00');
|
||||||
|
$sheet->getStyle('A1:H1')->applyFromArray([
|
||||||
|
'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']],
|
||||||
|
'fill' => [
|
||||||
|
'fillType' => Fill::FILL_SOLID,
|
||||||
|
'startColor' => ['rgb' => '26382E'],
|
||||||
|
],
|
||||||
|
'alignment' => ['vertical' => Alignment::VERTICAL_CENTER],
|
||||||
|
]);
|
||||||
|
$sheet->getRowDimension(1)->setRowHeight(24);
|
||||||
|
$sheet->freezePane('A2');
|
||||||
|
$sheet->setAutoFilter("A1:H{$lastRow}");
|
||||||
|
foreach (['A' => 20, 'B' => 22, 'C' => 12, 'D' => 12, 'E' => 16, 'F' => 20, 'G' => 16, 'H' => 18] as $column => $width) {
|
||||||
|
$sheet->getColumnDimension($column)->setWidth($width);
|
||||||
|
}
|
||||||
|
|
||||||
|
$filename = 'reservas_entradas_'.$tenant->codigo.'_'
|
||||||
|
.$generatedAt->copy()->timezone($timeZone)->format('Ymd_His').'.xlsx';
|
||||||
|
|
||||||
|
return response()->streamDownload(function () use ($spreadsheet): void {
|
||||||
|
(new Xlsx($spreadsheet))->save('php://output');
|
||||||
|
$spreadsheet->disconnectWorksheets();
|
||||||
|
}, $filename, [
|
||||||
|
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Ticketing\Desfile\Services;
|
||||||
|
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Ticketing\Desfile\Models\EntryReservation;
|
||||||
|
use Barryvdh\DomPDF\Facade\Pdf;
|
||||||
|
use Barryvdh\DomPDF\PDF as DomPdf;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
class EntryReservationPdfService
|
||||||
|
{
|
||||||
|
public function __construct(private readonly EntryReservationReportService $report) {}
|
||||||
|
|
||||||
|
/** @param Collection<int, EntryReservation> $reservations */
|
||||||
|
public function download(Tenant $tenant, Collection $reservations, string $timeZone): Response
|
||||||
|
{
|
||||||
|
$generatedAt = now();
|
||||||
|
$rows = $this->report->rows($reservations);
|
||||||
|
$pdf = Pdf::loadView('pdf.adminapp.desfile-entry-reservations', [
|
||||||
|
'tenant' => $tenant,
|
||||||
|
'reservations' => $rows,
|
||||||
|
'generatedAt' => $generatedAt,
|
||||||
|
'timeZone' => $timeZone,
|
||||||
|
])->setPaper('a4', 'landscape');
|
||||||
|
|
||||||
|
$this->addPageNumbers($pdf);
|
||||||
|
|
||||||
|
return $pdf->download(
|
||||||
|
'reservas_entradas_'.$tenant->codigo.'_'
|
||||||
|
.$generatedAt->copy()->timezone($timeZone)->format('Ymd_His').'.pdf'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function addPageNumbers(DomPdf $pdf): void
|
||||||
|
{
|
||||||
|
$pdf->render();
|
||||||
|
$domPdf = $pdf->getDomPDF();
|
||||||
|
$font = $domPdf->getFontMetrics()->getFont('DejaVu Sans');
|
||||||
|
|
||||||
|
$domPdf->getCanvas()->page_text(
|
||||||
|
385,
|
||||||
|
575,
|
||||||
|
'Página {PAGE_NUM} de {PAGE_COUNT}',
|
||||||
|
$font,
|
||||||
|
7,
|
||||||
|
[0.48, 0.52, 0.49],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Ticketing\Desfile\Services;
|
||||||
|
|
||||||
|
use App\Domains\Ticketing\Desfile\Models\EntryReservation;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
class EntryReservationReportService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param Collection<int, EntryReservation> $reservations
|
||||||
|
* @return Collection<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function rows(Collection $reservations): Collection
|
||||||
|
{
|
||||||
|
return $reservations->values()->map(function (EntryReservation $reservation): array {
|
||||||
|
$selection = $reservation->variant->selectionOptions();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'tipo' => $this->selectionLabel($selection->get('tipo')),
|
||||||
|
'sector' => $this->selectionLabel($selection->get('sector')),
|
||||||
|
'fila' => $this->selectionLabel($selection->get('fila')),
|
||||||
|
'asiento' => $this->selectionLabel($selection->get('asiento')),
|
||||||
|
'ticket_id' => $reservation->ticket_id,
|
||||||
|
'fecha_reserva' => $reservation->fecha_reserva,
|
||||||
|
'importe' => $reservation->importe,
|
||||||
|
'pago' => $reservation->tipo_pago->label(),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function selectionLabel(mixed $selection): string
|
||||||
|
{
|
||||||
|
if (! is_array($selection)) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_is_list($selection)) {
|
||||||
|
return collect($selection)->pluck('label')->filter()->implode(', ') ?: '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
return isset($selection['label']) ? (string) $selection['label'] : '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Ticketing\Desfile\Services;
|
||||||
|
|
||||||
|
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\Core\Auth\Models\User;
|
||||||
|
use App\Domains\Ticketing\Desfile\Enums\EntryReservationPaymentType;
|
||||||
|
use App\Domains\Ticketing\Desfile\Models\EntryReservation;
|
||||||
|
use App\Domains\Ticketing\Ticket\Exceptions\TicketGenerationException;
|
||||||
|
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
||||||
|
use App\Domains\Ticketing\Ticket\Services\TicketGeneratorService;
|
||||||
|
use App\Domains\Ticketing\Ticket\Services\TicketPresentationResolver;
|
||||||
|
use App\Domains\Ticketing\Ticket\Services\TicketValidityResolver;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class EntryReservationService
|
||||||
|
{
|
||||||
|
public function __construct(private readonly TicketGeneratorService $tickets) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{tipo_pago?: string|null, page?: int, per_page?: int} $filters
|
||||||
|
* @return LengthAwarePaginator<EntryReservation>
|
||||||
|
*/
|
||||||
|
public function reservations(User $user, array $filters = []): LengthAwarePaginator
|
||||||
|
{
|
||||||
|
abort_unless($user->tenant_codigo === 'desfile_pura_tendencia', 403);
|
||||||
|
|
||||||
|
return $this->reservationsQuery($user, $filters)
|
||||||
|
->paginate(
|
||||||
|
perPage: $filters['per_page'] ?? 15,
|
||||||
|
pageName: 'page',
|
||||||
|
page: $filters['page'] ?? 1,
|
||||||
|
)
|
||||||
|
->withQueryString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{tipo_pago?: string|null} $filters
|
||||||
|
* @return Collection<int, EntryReservation>
|
||||||
|
*/
|
||||||
|
public function reservationsForExport(User $user, array $filters = []): Collection
|
||||||
|
{
|
||||||
|
abort_unless($user->tenant_codigo === 'desfile_pura_tendencia', 403);
|
||||||
|
|
||||||
|
return $this->reservationsQuery($user, $filters)->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reservationTicket(User $user, int $reservationId): Ticket
|
||||||
|
{
|
||||||
|
abort_unless($user->tenant_codigo === 'desfile_pura_tendencia', 403);
|
||||||
|
|
||||||
|
$reservation = EntryReservation::query()
|
||||||
|
->whereKey($reservationId)
|
||||||
|
->whereNotNull('ticket_id')
|
||||||
|
->whereHas('variant.catalogItem', fn (Builder $query): Builder => $query
|
||||||
|
->where('tenant_code', $user->tenant_codigo)
|
||||||
|
->where('slug', 'entrada'))
|
||||||
|
->with(['ticket' => fn ($query) => $query->with([
|
||||||
|
...TicketValidityResolver::RELATIONS,
|
||||||
|
...TicketPresentationResolver::RELATIONS,
|
||||||
|
])])
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
return $reservation->ticket;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cancel(User $user, int $reservationId): void
|
||||||
|
{
|
||||||
|
abort_unless($user->tenant_codigo === 'desfile_pura_tendencia', 403);
|
||||||
|
|
||||||
|
DB::transaction(function () use ($user, $reservationId): void {
|
||||||
|
$reservation = EntryReservation::query()
|
||||||
|
->whereKey($reservationId)
|
||||||
|
->whereHas('variant.catalogItem', fn (Builder $query): Builder => $query
|
||||||
|
->where('tenant_code', $user->tenant_codigo)
|
||||||
|
->where('slug', 'entrada'))
|
||||||
|
->lockForUpdate()
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
if ($reservation->ticket_id !== null) {
|
||||||
|
$ticket = Ticket::query()->lockForUpdate()->findOrFail($reservation->ticket_id);
|
||||||
|
|
||||||
|
if (! $ticket->can_cancel()) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'status' => 'El ticket debe estar activo para poder cancelar la reserva.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ticket->markAsCancelled();
|
||||||
|
$ticket->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($reservation->inventory_id !== null) {
|
||||||
|
$inventory = Inventory::query()->lockForUpdate()->findOrFail($reservation->inventory_id);
|
||||||
|
$inventory->releaseEntry(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$reservation->delete();
|
||||||
|
}, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array{tipo_pago?: string|null} $filters */
|
||||||
|
private function reservationsQuery(User $user, array $filters = []): Builder
|
||||||
|
{
|
||||||
|
return EntryReservation::query()
|
||||||
|
->whereHas('variant.catalogItem', fn (Builder $query): Builder => $query
|
||||||
|
->where('tenant_code', $user->tenant_codigo)
|
||||||
|
->where('slug', 'entrada'))
|
||||||
|
->when(
|
||||||
|
$filters['tipo_pago'] ?? null,
|
||||||
|
fn (Builder $query, string $paymentType): Builder => $query->where('tipo_pago', $paymentType),
|
||||||
|
)
|
||||||
|
->with([
|
||||||
|
'variant.catalogItem.itemAttributes.attribute.options',
|
||||||
|
'variant.definitions.itemAttribute.attribute.options',
|
||||||
|
'variant.eventDates',
|
||||||
|
'variant.eventDate',
|
||||||
|
])
|
||||||
|
->orderByDesc('fecha_reserva')
|
||||||
|
->orderByDesc('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param list<array{variant_id: int, tipo_pago: string}> $rows */
|
||||||
|
public function reserve(User $user, string $key, array $rows): Collection
|
||||||
|
{
|
||||||
|
abort_unless($user->tenant_codigo === 'desfile_pura_tendencia', 403);
|
||||||
|
$normalized = collect($rows)->map(fn (array $row): array => [
|
||||||
|
'variant_id' => (int) $row['variant_id'], 'tipo_pago' => $row['tipo_pago'],
|
||||||
|
])->sortBy('variant_id')->values()->all();
|
||||||
|
$hash = hash('sha256', json_encode($normalized, JSON_THROW_ON_ERROR));
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($user, $key, $rows, $hash): Collection {
|
||||||
|
// Serialize retries by the same administrator, including the first insert.
|
||||||
|
User::query()->whereKey($user->id)->lockForUpdate()->firstOrFail();
|
||||||
|
$batch = DB::table('desfile_reservation_batches')
|
||||||
|
->where('user_id', $user->id)->where('idempotency_key', $key)->lockForUpdate()->first();
|
||||||
|
if ($batch !== null) {
|
||||||
|
abort_unless($batch->tenant_code === $user->tenant_codigo && hash_equals($batch->request_hash, $hash), 409,
|
||||||
|
'La clave de envío ya fue utilizada con otras entradas.');
|
||||||
|
|
||||||
|
return EntryReservation::query()->where('batch_id', $batch->id)->with('ticket')->orderBy('id')->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenant = $user->tenant()->firstOrFail();
|
||||||
|
$entry = CatalogItem::query()->forTenantCatalog($tenant)->where('slug', 'entrada')
|
||||||
|
->lockForUpdate()->firstOrFail();
|
||||||
|
$ids = array_column($rows, 'variant_id');
|
||||||
|
$variants = $entry->variants()->whereKey($ids)->orderBy('id')->lockForUpdate()->get();
|
||||||
|
$inventories = Inventory::query()->whereKey($variants->pluck('inventory_id')->filter()->unique())
|
||||||
|
->orderBy('id')->lockForUpdate()->get()->keyBy('id');
|
||||||
|
$variants->load([
|
||||||
|
'eventDates', 'eventDate',
|
||||||
|
'desfileEntryReservations' => fn ($query) => $query->lockForUpdate(),
|
||||||
|
]);
|
||||||
|
foreach ($variants as $variant) {
|
||||||
|
$variant->setRelation('inventory', $inventories->get($variant->inventory_id));
|
||||||
|
}
|
||||||
|
$entry->setRelation('variants', $variants);
|
||||||
|
$available = $entry->visibleVariants()->keyBy('id');
|
||||||
|
$requirements = [];
|
||||||
|
$errors = [];
|
||||||
|
foreach ($rows as $index => $row) {
|
||||||
|
$variant = $available->get($row['variant_id']);
|
||||||
|
if ($variant === null || $variant->inventory === null) {
|
||||||
|
$errors["rows.{$index}.variant_id"] = $this->entryLabel($variants->firstWhere('id', $row['variant_id']), $index).': la entrada ya no está disponible.';
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$requirements[$variant->inventory_id] = ($requirements[$variant->inventory_id] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
if ($errors !== []) {
|
||||||
|
throw ValidationException::withMessages($errors);
|
||||||
|
}
|
||||||
|
$tracked = $entry->inventory_policy !== InventoryPolicy::Unlimited;
|
||||||
|
foreach ($requirements as $inventoryId => $quantity) {
|
||||||
|
if ($tracked && $inventories[$inventoryId]->availableStock() < $quantity) {
|
||||||
|
foreach ($rows as $index => $row) {
|
||||||
|
$variant = $available[$row['variant_id']];
|
||||||
|
if ($variant->inventory_id === $inventoryId) {
|
||||||
|
$errors["rows.{$index}.variant_id"] = $this->entryLabel($variant, $index).': no hay stock suficiente para reservar las entradas seleccionadas.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($errors !== []) {
|
||||||
|
throw ValidationException::withMessages($errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
$batchId = DB::table('desfile_reservation_batches')->insertGetId([
|
||||||
|
'user_id' => $user->id, 'tenant_code' => $tenant->codigo,
|
||||||
|
'idempotency_key' => $key, 'request_hash' => $hash,
|
||||||
|
'created_at' => now(), 'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
foreach ($requirements as $inventoryId => $quantity) {
|
||||||
|
$inventories[$inventoryId]->reserveEntry($quantity, $tracked);
|
||||||
|
}
|
||||||
|
$reservations = collect();
|
||||||
|
foreach ($rows as $index => $row) {
|
||||||
|
$variant = $available[$row['variant_id']];
|
||||||
|
$payment = EntryReservationPaymentType::from($row['tipo_pago']);
|
||||||
|
try {
|
||||||
|
$ticket = $this->tickets->generate($entry, $user, 1, $variant->id)->sole();
|
||||||
|
} catch (TicketGenerationException $exception) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
"rows.{$index}.variant_id" => $this->entryLabel($variant, $index).': no se pudo emitir el ticket. '.$exception->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
$reservation = EntryReservation::query()->create([
|
||||||
|
'batch_id' => $batchId, 'ticket_id' => $ticket->id,
|
||||||
|
'variant_id' => $variant->id, 'inventory_id' => $variant->inventory_id,
|
||||||
|
'fecha_reserva' => now(), 'tipo_pago' => $payment,
|
||||||
|
'importe' => $payment === EntryReservationPaymentType::Free ? 0 : $variant->getPrice(),
|
||||||
|
]);
|
||||||
|
$reservations->push($reservation->setRelation('ticket', $ticket));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $reservations;
|
||||||
|
}, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function entryLabel(?Variant $variant, int $index): string
|
||||||
|
{
|
||||||
|
if ($variant === null) {
|
||||||
|
return 'Entrada '.($index + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$values = $variant->selectionValues();
|
||||||
|
|
||||||
|
return collect(['tipo' => 'Tipo', 'sector' => 'Sector', 'fila' => 'Fila', 'asiento' => 'Asiento'])
|
||||||
|
->map(fn (string $label, string $key): string => $label.': '.($values->get($key) ?? 'sin especificar'))
|
||||||
|
->implode(', ');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
namespace App\Domains\Ticketing\Desfile\Services;
|
namespace App\Domains\Ticketing\Desfile\Services;
|
||||||
|
|
||||||
use App\Shared\Attachable\Models\Attachment;
|
|
||||||
use App\Shared\Attachable\Services\AttachmentService;
|
|
||||||
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
||||||
use App\Domains\Commerce\Catalog\Models\Inventory;
|
use App\Domains\Commerce\Catalog\Models\Inventory;
|
||||||
use App\Domains\Commerce\Catalog\Models\ItemAttribute;
|
use App\Domains\Commerce\Catalog\Models\ItemAttribute;
|
||||||
use App\Domains\Commerce\Catalog\Models\Variant;
|
use App\Domains\Commerce\Catalog\Models\Variant;
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use App\Shared\Attachable\Models\Attachment;
|
||||||
|
use App\Shared\Attachable\Services\AttachmentService;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
@@ -254,7 +254,7 @@ class EntryService
|
|||||||
{
|
{
|
||||||
$inventory = $variant->inventory;
|
$inventory = $variant->inventory;
|
||||||
|
|
||||||
if (($inventory?->reserved_stock ?? 0) > 0 || ($inventory?->sold_units ?? 0) > 0) {
|
if (($inventory?->reserved_stock ?? 0) > 0 || ($inventory?->entry_reserved_stock ?? 0) > 0 || ($inventory?->sold_units ?? 0) > 0) {
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
$key => [
|
$key => [
|
||||||
'No se puede modificar ni eliminar un asiento con ventas o reservas.',
|
'No se puede modificar ni eliminar un asiento con ventas o reservas.',
|
||||||
|
|||||||
@@ -393,7 +393,7 @@ class InvitationPurchaseProvisioner
|
|||||||
|
|
||||||
$inventory = DB::table('inventories')->where('id', $variant->inventory_id)->lockForUpdate()->first();
|
$inventory = DB::table('inventories')->where('id', $variant->inventory_id)->lockForUpdate()->first();
|
||||||
|
|
||||||
if ($inventory === null || $inventory->real_stock < 1 || $inventory->reserved_stock > 0) {
|
if ($inventory === null || $inventory->real_stock < 1 || $inventory->reserved_stock > 0 || ($inventory->entry_reserved_stock ?? 0) > 0) {
|
||||||
throw new RuntimeException("El asiento {$variant->descripcion} ya no está disponible.");
|
throw new RuntimeException("El asiento {$variant->descripcion} ya no está disponible.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,30 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Domains\Ticketing\Desfile\Controllers\EntryController;
|
use App\Domains\Ticketing\Desfile\Controllers\EntryController;
|
||||||
|
use App\Domains\Ticketing\Desfile\Controllers\EntryReservationController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::get('v1/adminapp/tenant/desfile/entry-reservations', [EntryReservationController::class, 'index'])
|
||||||
|
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:adminapp.desfile.reservas'])
|
||||||
|
->name('adminapp.desfile.entry-reservations.index');
|
||||||
|
Route::get('v1/adminapp/tenant/desfile/entry-reservations/pdf', [EntryReservationController::class, 'downloadPdf'])
|
||||||
|
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:adminapp.desfile.reservas'])
|
||||||
|
->name('adminapp.desfile.entry-reservations.pdf');
|
||||||
|
Route::get('v1/adminapp/tenant/desfile/entry-reservations/excel', [EntryReservationController::class, 'downloadExcel'])
|
||||||
|
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:adminapp.desfile.reservas'])
|
||||||
|
->name('adminapp.desfile.entry-reservations.excel');
|
||||||
|
Route::get('v1/adminapp/tenant/desfile/entry-reservations/{reservation}/ticket/pdf', [EntryReservationController::class, 'downloadTicketPdf'])
|
||||||
|
->whereNumber('reservation')
|
||||||
|
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:adminapp.desfile.reservas'])
|
||||||
|
->name('adminapp.desfile.entry-reservations.ticket.pdf');
|
||||||
|
Route::post('v1/adminapp/tenant/desfile/entry-reservations', [EntryReservationController::class, 'store'])
|
||||||
|
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:adminapp.desfile.reservas'])
|
||||||
|
->name('adminapp.desfile.entry-reservations.store');
|
||||||
|
Route::delete('v1/adminapp/tenant/desfile/entry-reservations/{reservation}', [EntryReservationController::class, 'destroy'])
|
||||||
|
->whereNumber('reservation')
|
||||||
|
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:adminapp.desfile.reservas'])
|
||||||
|
->name('adminapp.desfile.entry-reservations.destroy');
|
||||||
|
|
||||||
Route::prefix('v1/adminapp/tenant/desfile')
|
Route::prefix('v1/adminapp/tenant/desfile')
|
||||||
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:adminapp.desfile.entradas'])
|
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:adminapp.desfile.entradas'])
|
||||||
->group(function (): void {
|
->group(function (): void {
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Domains\Ticketing\Event\Models;
|
namespace App\Domains\Ticketing\Event\Models;
|
||||||
|
|
||||||
|
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
||||||
use App\Domains\Core\Tenant\Models\SocialMedia;
|
use App\Domains\Core\Tenant\Models\SocialMedia;
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
||||||
use App\Shared\Attachable\Models\Attachment;
|
use App\Shared\Attachable\Models\Attachment;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -13,7 +14,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
#[Fillable(['tenant_code', 'event_category_id', 'title', 'subtitle', 'description', 'location', 'exact_location', 'date_text', 'published_at', 'attachment_id'])]
|
#[Fillable(['tenant_code', 'slug', 'event_category_id', 'title', 'subtitle', 'description', 'location', 'exact_location', 'date_text', 'published_at', 'attachment_id'])]
|
||||||
class Event extends Model
|
class Event extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
@@ -53,6 +54,12 @@ class Event extends Model
|
|||||||
return $this->hasMany(CatalogItem::class)->whereAvailable()->orderBy('group_order')->orderBy('id');
|
return $this->hasMany(CatalogItem::class)->whereAvailable()->orderBy('group_order')->orderBy('id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return HasMany<Ticket, $this> */
|
||||||
|
public function tickets(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Ticket::class);
|
||||||
|
}
|
||||||
|
|
||||||
/** @return BelongsToMany<SocialMedia, $this> */
|
/** @return BelongsToMany<SocialMedia, $this> */
|
||||||
public function socialMedia(): BelongsToMany
|
public function socialMedia(): BelongsToMany
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
namespace App\Domains\Ticketing\Event\Models;
|
namespace App\Domains\Ticketing\Event\Models;
|
||||||
|
|
||||||
use App\Domains\Commerce\Catalog\Models\Variant;
|
use App\Domains\Commerce\Catalog\Models\Variant;
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
use App\Domains\Ticketing\Event\Enums\EventDateStatus;
|
use App\Domains\Ticketing\Event\Enums\EventDateStatus;
|
||||||
use App\Domains\Ticketing\Event\Services\EffectiveEventDateResolver;
|
use App\Domains\Ticketing\Event\Services\EffectiveEventDateResolver;
|
||||||
use App\Domains\Ticketing\Event\Services\EventDateTextFormatter;
|
use App\Domains\Ticketing\Event\Services\EventDateTextFormatter;
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
|
||||||
use App\Domains\Ticketing\Ticket\Enums\ValidityTimeType;
|
use App\Domains\Ticketing\Ticket\Enums\ValidityTimeType;
|
||||||
use App\Domains\Ticketing\Ticket\Models\ValidityTime;
|
use App\Domains\Ticketing\Ticket\Models\ValidityTime;
|
||||||
use Carbon\CarbonInterface;
|
use Carbon\CarbonInterface;
|
||||||
@@ -131,12 +131,12 @@ class EventDate extends Model
|
|||||||
|
|
||||||
public function startsAt(): CarbonInterface
|
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
|
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())
|
return $endsAt->lessThanOrEqualTo($this->startsAt())
|
||||||
? $endsAt->addDay()
|
? $endsAt->addDay()
|
||||||
@@ -184,8 +184,8 @@ class EventDate extends Model
|
|||||||
|
|
||||||
private function syncValidityTime(): void
|
private function syncValidityTime(): void
|
||||||
{
|
{
|
||||||
$startsAt = $this->startsAt();
|
$startsAt = $this->startsAt()->utc();
|
||||||
$expiresAt = $this->endsAt();
|
$expiresAt = $this->endsAt()->utc();
|
||||||
|
|
||||||
$attributes = [
|
$attributes = [
|
||||||
'type' => ValidityTimeType::FixedWindow,
|
'type' => ValidityTimeType::FixedWindow,
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Domains\Ticketing\Event\Resources;
|
namespace App\Domains\Ticketing\Event\Resources;
|
||||||
|
|
||||||
use App\Domains\Ticketing\Event\Services\EventDateGroupingService;
|
|
||||||
use App\Domains\Ticketing\Event\Models\Event;
|
use App\Domains\Ticketing\Event\Models\Event;
|
||||||
|
use App\Domains\Ticketing\Event\Services\EventDateGroupingService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ class EventResource extends JsonResource
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
|
'slug' => $this->slug,
|
||||||
'title' => $this->title,
|
'title' => $this->title,
|
||||||
'subtitle' => $this->subtitle,
|
'subtitle' => $this->subtitle,
|
||||||
'description' => $this->description,
|
'description' => $this->description,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ namespace App\Domains\Ticketing\Event\Resources;
|
|||||||
use App\Domains\Commerce\Catalog\Enums\InventoryPolicy;
|
use App\Domains\Commerce\Catalog\Enums\InventoryPolicy;
|
||||||
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
||||||
use App\Domains\Commerce\Catalog\Models\Variant;
|
use App\Domains\Commerce\Catalog\Models\Variant;
|
||||||
use Illuminate\Support\Carbon;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
@@ -15,6 +14,7 @@ class PublicEventResource extends JsonResource
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
|
'slug' => $this->slug,
|
||||||
'event_category_id' => $this->event_category_id,
|
'event_category_id' => $this->event_category_id,
|
||||||
'title' => $this->title,
|
'title' => $this->title,
|
||||||
'subtitle' => $this->subtitle,
|
'subtitle' => $this->subtitle,
|
||||||
@@ -29,10 +29,7 @@ class PublicEventResource extends JsonResource
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Carbon::parse(
|
return $first->startsAt()->toISOString();
|
||||||
$first->date->format('Y-m-d').' '.$first->time_start,
|
|
||||||
config('app.event_timezone'),
|
|
||||||
)->toISOString();
|
|
||||||
}),
|
}),
|
||||||
'dates' => $this->whenLoaded('dates', fn () => $this->dates->map(fn ($date): array => [
|
'dates' => $this->whenLoaded('dates', fn () => $this->dates->map(fn ($date): array => [
|
||||||
'id' => $date->id,
|
'id' => $date->id,
|
||||||
@@ -52,8 +49,7 @@ class PublicEventResource extends JsonResource
|
|||||||
'description' => $item->descripcion,
|
'description' => $item->descripcion,
|
||||||
'price' => $item->precio,
|
'price' => $item->precio,
|
||||||
'image' => $item->attachments->first()?->getTemporaryUrl(1440),
|
'image' => $item->attachments->first()?->getTemporaryUrl(1440),
|
||||||
'requires_selection' => $item->itemAttributes->contains(fn ($attribute): bool =>
|
'requires_selection' => $item->itemAttributes->contains(fn ($attribute): bool => $attribute->show_in_selector && $attribute->attribute?->codigo !== 'event_date'),
|
||||||
$attribute->show_in_selector && $attribute->attribute?->codigo !== 'event_date'),
|
|
||||||
'maximum_quantity' => $item->inventory_policy === InventoryPolicy::Unlimited
|
'maximum_quantity' => $item->inventory_policy === InventoryPolicy::Unlimited
|
||||||
? null
|
? null
|
||||||
: $item->availableStock(),
|
: $item->availableStock(),
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ use Illuminate\Support\Facades\Route;
|
|||||||
|
|
||||||
require __DIR__.'/adminapp.php';
|
require __DIR__.'/adminapp.php';
|
||||||
|
|
||||||
Route::prefix('tenants/{tenant:codigo}')->group(function (): void {
|
Route::prefix('tenants/{tenant:codigo}')->scopeBindings()->group(function (): void {
|
||||||
Route::get('events', [PublicEventController::class, 'index']);
|
Route::get('events', [PublicEventController::class, 'index']);
|
||||||
Route::get('events/{event}', [PublicEventController::class, 'show']);
|
Route::get('events/{event:slug}', [PublicEventController::class, 'show']);
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::middleware('auth:sanctum')->post(
|
Route::middleware('auth:sanctum')->post(
|
||||||
|
|||||||
@@ -6,31 +6,47 @@ use App\Domains\Core\Tenant\Models\Tenant;
|
|||||||
use App\Domains\Ticketing\Ticket\Exceptions\TicketNotAvailableException;
|
use App\Domains\Ticketing\Ticket\Exceptions\TicketNotAvailableException;
|
||||||
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
||||||
use App\Domains\Ticketing\Ticket\Requests\DownloadTicketsPdfRequest;
|
use App\Domains\Ticketing\Ticket\Requests\DownloadTicketsPdfRequest;
|
||||||
|
use App\Domains\Ticketing\Ticket\Requests\ListTicketsRequest;
|
||||||
|
use App\Domains\Ticketing\Ticket\Resources\TicketEventResource;
|
||||||
use App\Domains\Ticketing\Ticket\Resources\TicketResource;
|
use App\Domains\Ticketing\Ticket\Resources\TicketResource;
|
||||||
use App\Domains\Ticketing\Ticket\Services\TicketPdfService;
|
use App\Domains\Ticketing\Ticket\Services\TicketPdfService;
|
||||||
use App\Domains\Ticketing\Ticket\Services\TicketPresentationResolver;
|
use App\Domains\Ticketing\Ticket\Services\TicketPresentationResolver;
|
||||||
use App\Domains\Ticketing\Ticket\Services\TicketValidityResolver;
|
use App\Domains\Ticketing\Ticket\Services\TicketValidityResolver;
|
||||||
|
use App\Domains\Ticketing\Ticket\Services\UserTicketQueryService;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
|
|
||||||
class TicketController extends Controller
|
class TicketController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(private readonly TicketPdfService $ticketPdfService) {}
|
public function __construct(
|
||||||
|
private readonly TicketPdfService $ticketPdfService,
|
||||||
|
private readonly UserTicketQueryService $userTickets,
|
||||||
|
) {}
|
||||||
|
|
||||||
public function index(Request $request, Tenant $tenant): JsonResponse
|
public function index(ListTicketsRequest $request, Tenant $tenant): JsonResponse
|
||||||
{
|
{
|
||||||
$tickets = Ticket::query()
|
$eventId = $request->validated('event_id');
|
||||||
->where('tenant_code', $tenant->codigo)
|
$tickets = $this->userTickets->tickets(
|
||||||
->where('user_id', $request->user()->getKey())
|
$tenant,
|
||||||
->with([...TicketValidityResolver::RELATIONS, ...TicketPresentationResolver::RELATIONS])
|
$request->user(),
|
||||||
->orderByDesc('id')
|
$eventId === null ? null : (int) $eventId,
|
||||||
->get();
|
);
|
||||||
|
|
||||||
return TicketResource::collection($tickets)->response();
|
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
|
public function downloadPdf(DownloadTicketsPdfRequest $request, Tenant $tenant): Response
|
||||||
{
|
{
|
||||||
$ticketIds = $request->validated('ticket_ids');
|
$ticketIds = $request->validated('ticket_ids');
|
||||||
|
|||||||
@@ -2,16 +2,18 @@
|
|||||||
|
|
||||||
namespace App\Domains\Ticketing\Ticket\Models;
|
namespace App\Domains\Ticketing\Ticket\Models;
|
||||||
|
|
||||||
use App\Domains\Core\Auth\Models\User;
|
|
||||||
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
||||||
use App\Domains\Commerce\Catalog\Models\Variant;
|
use App\Domains\Commerce\Catalog\Models\Variant;
|
||||||
use App\Shared\Logging\Models\Concerns\LogsValueChanges;
|
|
||||||
use App\Domains\Commerce\Purchase\Models\PurchaseItem;
|
use App\Domains\Commerce\Purchase\Models\PurchaseItem;
|
||||||
|
use App\Domains\Core\Auth\Models\User;
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Ticketing\Desfile\Models\EntryReservation;
|
||||||
|
use App\Domains\Ticketing\Event\Models\Event;
|
||||||
use App\Domains\Ticketing\Ticket\Services\ResolvedTicketValidity;
|
use App\Domains\Ticketing\Ticket\Services\ResolvedTicketValidity;
|
||||||
use App\Domains\Ticketing\Ticket\Services\ResolvedValidityGroup;
|
use App\Domains\Ticketing\Ticket\Services\ResolvedValidityGroup;
|
||||||
use App\Domains\Ticketing\Ticket\Services\TicketPresentationResolver;
|
use App\Domains\Ticketing\Ticket\Services\TicketPresentationResolver;
|
||||||
use App\Domains\Ticketing\Ticket\Services\TicketValidityResolver;
|
use App\Domains\Ticketing\Ticket\Services\TicketValidityResolver;
|
||||||
|
use App\Shared\Logging\Models\Concerns\LogsValueChanges;
|
||||||
use Carbon\CarbonInterface;
|
use Carbon\CarbonInterface;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -24,6 +26,7 @@ use Illuminate\Validation\ValidationException;
|
|||||||
|
|
||||||
#[Fillable([
|
#[Fillable([
|
||||||
'tenant_code',
|
'tenant_code',
|
||||||
|
'event_id',
|
||||||
'ticket',
|
'ticket',
|
||||||
'source_purchase_item_id',
|
'source_purchase_item_id',
|
||||||
'source_catalog_item_id',
|
'source_catalog_item_id',
|
||||||
@@ -75,6 +78,7 @@ class Ticket extends Model
|
|||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
'event_id' => 'integer',
|
||||||
'source_catalog_item_id' => 'integer',
|
'source_catalog_item_id' => 'integer',
|
||||||
'source_variant_id' => 'integer',
|
'source_variant_id' => 'integer',
|
||||||
'source_purchase_item_id' => 'integer',
|
'source_purchase_item_id' => 'integer',
|
||||||
@@ -136,6 +140,12 @@ class Ticket extends Model
|
|||||||
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
|
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<Event, $this> */
|
||||||
|
public function event(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Event::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function allow_refund(): bool
|
public function allow_refund(): bool
|
||||||
{
|
{
|
||||||
return $this->tenant?->allow_refund() ?? false;
|
return $this->tenant?->allow_refund() ?? false;
|
||||||
@@ -183,7 +193,9 @@ class Ticket extends Model
|
|||||||
|
|
||||||
public function can_refund(): bool
|
public function can_refund(): bool
|
||||||
{
|
{
|
||||||
return $this->is_active() && $this->allow_refund();
|
return $this->is_active()
|
||||||
|
&& $this->allow_refund()
|
||||||
|
&& ! $this->hasEntryReservation();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function canRefund(): bool
|
public function canRefund(): bool
|
||||||
@@ -226,6 +238,12 @@ class Ticket extends Model
|
|||||||
return $this->hasOne(TicketRefund::class);
|
return $this->hasOne(TicketRefund::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return HasOne<EntryReservation, $this> */
|
||||||
|
public function entryReservation(): HasOne
|
||||||
|
{
|
||||||
|
return $this->hasOne(EntryReservation::class);
|
||||||
|
}
|
||||||
|
|
||||||
/** @return BelongsTo<CatalogItem, $this> */
|
/** @return BelongsTo<CatalogItem, $this> */
|
||||||
public function sourceCatalogItem(): BelongsTo
|
public function sourceCatalogItem(): BelongsTo
|
||||||
{
|
{
|
||||||
@@ -238,6 +256,15 @@ class Ticket extends Model
|
|||||||
return $this->belongsTo(Variant::class, 'source_variant_id')->withTrashed();
|
return $this->belongsTo(Variant::class, 'source_variant_id')->withTrashed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function hasEntryReservation(): bool
|
||||||
|
{
|
||||||
|
if ($this->relationLoaded('entryReservation')) {
|
||||||
|
return $this->getRelation('entryReservation') instanceof EntryReservation;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->exists && $this->entryReservation()->exists();
|
||||||
|
}
|
||||||
|
|
||||||
public function isValid(): bool
|
public function isValid(): bool
|
||||||
{
|
{
|
||||||
if ($this->hasTerminalStatus() || $this->used_at !== null) {
|
if ($this->hasTerminalStatus() || $this->used_at !== null) {
|
||||||
|
|||||||
@@ -47,30 +47,42 @@ class ValidityTime extends Model
|
|||||||
|
|
||||||
public function startsAt(
|
public function startsAt(
|
||||||
?CarbonInterface $at = null,
|
?CarbonInterface $at = null,
|
||||||
|
string $timezone = 'UTC',
|
||||||
): ?CarbonInterface {
|
): ?CarbonInterface {
|
||||||
if ($this->type === ValidityTimeType::FixedWindow) {
|
if ($this->type === ValidityTimeType::FixedWindow) {
|
||||||
return $this->fixed_starts_at;
|
return $this->fixed_starts_at;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->atCurrentDate($this->start_time, $at);
|
return $this->atCurrentDate($this->start_time, $at, $timezone);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function expiresAt(
|
public function expiresAt(
|
||||||
?CarbonInterface $at = null,
|
?CarbonInterface $at = null,
|
||||||
|
string $timezone = 'UTC',
|
||||||
): ?CarbonInterface {
|
): ?CarbonInterface {
|
||||||
if ($this->type === ValidityTimeType::FixedWindow) {
|
if ($this->type === ValidityTimeType::FixedWindow) {
|
||||||
return $this->fixed_expires_at;
|
return $this->fixed_expires_at;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->atCurrentDate($this->end_time, $at);
|
return $this->atCurrentDate($this->end_time, $at, $timezone);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isValid(
|
public function isValid(
|
||||||
?CarbonInterface $at = null,
|
?CarbonInterface $at = null,
|
||||||
|
string $timezone = 'UTC',
|
||||||
): bool {
|
): bool {
|
||||||
$at ??= now();
|
$at ??= now();
|
||||||
$startsAt = $this->startsAt($at);
|
$startsAt = $this->startsAt($at, $timezone);
|
||||||
$expiresAt = $this->expiresAt($at);
|
$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))
|
return ($startsAt === null || $startsAt->lessThanOrEqualTo($at))
|
||||||
&& ($expiresAt === null || $expiresAt->greaterThan($at));
|
&& ($expiresAt === null || $expiresAt->greaterThan($at));
|
||||||
@@ -79,6 +91,7 @@ class ValidityTime extends Model
|
|||||||
private function atCurrentDate(
|
private function atCurrentDate(
|
||||||
?string $time,
|
?string $time,
|
||||||
?CarbonInterface $at,
|
?CarbonInterface $at,
|
||||||
|
string $timezone,
|
||||||
): ?CarbonInterface {
|
): ?CarbonInterface {
|
||||||
if ($time === null) {
|
if ($time === null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -86,8 +99,9 @@ class ValidityTime extends Model
|
|||||||
|
|
||||||
$at ??= now();
|
$at ??= now();
|
||||||
$localDate = CarbonImmutable::instance($at)
|
$localDate = CarbonImmutable::instance($at)
|
||||||
|
->setTimezone($timezone)
|
||||||
->format('Y-m-d');
|
->format('Y-m-d');
|
||||||
|
|
||||||
return CarbonImmutable::parse($localDate.' '.$time);
|
return CarbonImmutable::parse($localDate.' '.$time, $timezone);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Domains\Ticketing\Ticket\Requests;
|
namespace App\Domains\Ticketing\Ticket\Requests;
|
||||||
|
|
||||||
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
||||||
|
use App\Domains\Ticketing\Ticket\Services\AdminAppTicketAttributeService;
|
||||||
use App\Domains\Ticketing\Ticket\Services\AdminAppTicketColumnService;
|
use App\Domains\Ticketing\Ticket\Services\AdminAppTicketColumnService;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
@@ -22,7 +23,7 @@ class AdminAppTicketIndexRequest extends FormRequest
|
|||||||
? []
|
? []
|
||||||
: app(AdminAppTicketColumnService::class)->sortableKeys($tenant);
|
: app(AdminAppTicketColumnService::class)->sortableKeys($tenant);
|
||||||
|
|
||||||
return [
|
$rules = [
|
||||||
'q' => ['sometimes', 'nullable', 'string', 'max:255'],
|
'q' => ['sometimes', 'nullable', 'string', 'max:255'],
|
||||||
'category' => ['sometimes', 'nullable', 'string', 'max:255'],
|
'category' => ['sometimes', 'nullable', 'string', 'max:255'],
|
||||||
'product' => ['sometimes', 'nullable', 'string', 'max:255'],
|
'product' => ['sometimes', 'nullable', 'string', 'max:255'],
|
||||||
@@ -39,5 +40,18 @@ class AdminAppTicketIndexRequest extends FormRequest
|
|||||||
'sort_by' => ['sometimes', 'nullable', 'string', Rule::in($sortableKeys)],
|
'sort_by' => ['sometimes', 'nullable', 'string', Rule::in($sortableKeys)],
|
||||||
'sort_direction' => ['sometimes', 'nullable', 'string', Rule::in(['asc', 'desc'])],
|
'sort_direction' => ['sometimes', 'nullable', 'string', Rule::in(['asc', 'desc'])],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if ($tenant !== null) {
|
||||||
|
foreach (app(AdminAppTicketAttributeService::class)->attributes($tenant) as $attribute) {
|
||||||
|
$rules[$attribute['code']] = [
|
||||||
|
'sometimes',
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
Rule::in(array_column($attribute['options'], 'value')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rules;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
30
app/Domains/Ticketing/Ticket/Requests/ListTicketsRequest.php
Normal file
30
app/Domains/Ticketing/Ticket/Requests/ListTicketsRequest.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Ticketing\Ticket\Requests;
|
||||||
|
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class ListTicketsRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Ticketing\Ticket\Resources;
|
||||||
|
|
||||||
|
use App\Domains\Ticketing\Event\Models\Event;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/** @mixin Event */
|
||||||
|
class TicketEventResource extends JsonResource
|
||||||
|
{
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'title' => $this->title,
|
||||||
|
'image' => $this->attachment?->getTemporaryUrl(1440),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ class TicketResource extends JsonResource
|
|||||||
return [
|
return [
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'tenant_code' => $this->tenant_code,
|
'tenant_code' => $this->tenant_code,
|
||||||
|
'event_id' => $this->event_id,
|
||||||
'ticket' => $this->ticket,
|
'ticket' => $this->ticket,
|
||||||
'status' => $this->status,
|
'status' => $this->status,
|
||||||
'status_label' => $this->status_label,
|
'status_label' => $this->status_label,
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
|||||||
/** @mixin ValidityTime */
|
/** @mixin ValidityTime */
|
||||||
class ValidityTimeResource extends JsonResource
|
class ValidityTimeResource extends JsonResource
|
||||||
{
|
{
|
||||||
|
public function __construct($resource, private readonly string $timezone = 'UTC')
|
||||||
|
{
|
||||||
|
parent::__construct($resource);
|
||||||
|
}
|
||||||
|
|
||||||
/** @return array<string, mixed> */
|
/** @return array<string, mixed> */
|
||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
@@ -27,7 +32,7 @@ class ValidityTimeResource extends JsonResource
|
|||||||
return [
|
return [
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'type' => $this->type->value,
|
'type' => $this->type->value,
|
||||||
'is_valid' => $this->isValid(),
|
'is_valid' => $this->isValid(timezone: $this->timezone),
|
||||||
...array_filter($fields, fn (mixed $value): bool => $value !== null),
|
...array_filter($fields, fn (mixed $value): bool => $value !== null),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Ticketing\Ticket\Services;
|
||||||
|
|
||||||
|
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
|
class AdminAppTicketAttributeService
|
||||||
|
{
|
||||||
|
private const DESFILE_PURA_TENDENCIA = 'desfile_pura_tendencia';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{
|
||||||
|
* code: string,
|
||||||
|
* label: string,
|
||||||
|
* options: list<array{value: string, label: string}>
|
||||||
|
* }>
|
||||||
|
*/
|
||||||
|
public function attributes(Tenant $tenant): array
|
||||||
|
{
|
||||||
|
if ($tenant->codigo !== self::DESFILE_PURA_TENDENCIA) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = CatalogItem::withTrashed()
|
||||||
|
->where('tenant_code', $tenant->codigo)
|
||||||
|
->where(function (Builder $query): void {
|
||||||
|
$query
|
||||||
|
->where(function (Builder $activeQuery): void {
|
||||||
|
$activeQuery
|
||||||
|
->whereNull('catalog_items.deleted_at')
|
||||||
|
->where('has_tickets', true);
|
||||||
|
})
|
||||||
|
->orWhereHas('sourceTickets');
|
||||||
|
})
|
||||||
|
->with([
|
||||||
|
'itemAttributes' => fn ($query) => $query->orderBy('sort_order')->orderBy('id'),
|
||||||
|
'itemAttributes.attribute.options',
|
||||||
|
'variants' => fn ($query) => $query
|
||||||
|
->withTrashed()
|
||||||
|
->where(fn (Builder $variantQuery): Builder => $variantQuery
|
||||||
|
->whereNull('variantes.deleted_at')
|
||||||
|
->orWhereHas('sourceTickets'))
|
||||||
|
->orderBy('id'),
|
||||||
|
'variants.definitions.itemAttribute.attribute.options',
|
||||||
|
])
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$attributes = [];
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
foreach ($item->itemAttributes as $itemAttribute) {
|
||||||
|
$attribute = $itemAttribute->attribute;
|
||||||
|
if ($attribute === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = $attribute->codigo;
|
||||||
|
$attributes[$code] ??= [
|
||||||
|
'code' => $code,
|
||||||
|
'label' => $itemAttribute->ticket_label ?: $attribute->nombre,
|
||||||
|
'sort_order' => $itemAttribute->sort_order,
|
||||||
|
'options' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($item->variants as $variant) {
|
||||||
|
$definition = $variant->definitions->firstWhere('item_attribute_id', $itemAttribute->id);
|
||||||
|
if ($definition === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = (string) $definition->value;
|
||||||
|
$option = $attribute->options->firstWhere('value', $value);
|
||||||
|
$attributes[$code]['options'][$value] = [
|
||||||
|
'value' => $value,
|
||||||
|
'label' => (string) ($option?->label ?: $value),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uasort($attributes, fn (array $left, array $right): int => $left['sort_order'] <=> $right['sort_order']
|
||||||
|
?: $left['label'] <=> $right['label']);
|
||||||
|
|
||||||
|
return array_values(array_map(fn (array $attribute): array => [
|
||||||
|
'code' => $attribute['code'],
|
||||||
|
'label' => $attribute['label'],
|
||||||
|
'options' => array_values($attribute['options']),
|
||||||
|
], $attributes));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,9 +8,17 @@ class AdminAppTicketColumnService
|
|||||||
{
|
{
|
||||||
private const FIESTA_FUTBOL_INFANTIL = 'fiesta_futbol_infantil';
|
private const FIESTA_FUTBOL_INFANTIL = 'fiesta_futbol_infantil';
|
||||||
|
|
||||||
|
private const DESFILE_PURA_TENDENCIA = 'desfile_pura_tendencia';
|
||||||
|
|
||||||
|
public function __construct(private readonly AdminAppTicketAttributeService $attributeService) {}
|
||||||
|
|
||||||
/** @return list<array{key: string, label: string, type: string, sortable: bool, sort_param: string, width: string, excel_width: int}> */
|
/** @return list<array{key: string, label: string, type: string, sortable: bool, sort_param: string, width: string, excel_width: int}> */
|
||||||
public function columns(Tenant $tenant): array
|
public function columns(Tenant $tenant): array
|
||||||
{
|
{
|
||||||
|
if ($tenant->codigo === self::DESFILE_PURA_TENDENCIA) {
|
||||||
|
return $this->desfileColumns($tenant);
|
||||||
|
}
|
||||||
|
|
||||||
$keys = $tenant->codigo === self::FIESTA_FUTBOL_INFANTIL
|
$keys = $tenant->codigo === self::FIESTA_FUTBOL_INFANTIL
|
||||||
? ['order_number', 'category', 'product', 'type', 'date', 'size', 'amount', 'client', 'id', 'status', 'scanned_by']
|
? ['order_number', 'category', 'product', 'type', 'date', 'size', 'amount', 'client', 'id', 'status', 'scanned_by']
|
||||||
: ['order_number', 'product', 'amount', 'client', 'id', 'status', 'scanned_by'];
|
: ['order_number', 'product', 'amount', 'client', 'id', 'status', 'scanned_by'];
|
||||||
@@ -45,6 +53,31 @@ class AdminAppTicketColumnService
|
|||||||
return $columns;
|
return $columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return list<array{key: string, label: string, type: string, sortable: bool, sort_param: string, width: string, excel_width: int}> */
|
||||||
|
private function desfileColumns(Tenant $tenant): array
|
||||||
|
{
|
||||||
|
$attributeColumns = array_map(fn (array $attribute): array => [
|
||||||
|
'key' => $attribute['code'],
|
||||||
|
'label' => $attribute['label'],
|
||||||
|
'type' => 'text',
|
||||||
|
'sortable' => false,
|
||||||
|
'sort_param' => $attribute['code'],
|
||||||
|
'width' => '8%',
|
||||||
|
'excel_width' => 15,
|
||||||
|
], $this->attributeService->attributes($tenant));
|
||||||
|
|
||||||
|
return [
|
||||||
|
$this->definitions()['order_number'],
|
||||||
|
$this->definitions()['product'],
|
||||||
|
...$attributeColumns,
|
||||||
|
$this->definitions()['amount'],
|
||||||
|
$this->definitions()['client'],
|
||||||
|
$this->definitions()['id'],
|
||||||
|
$this->definitions()['status'],
|
||||||
|
$this->definitions()['scanned_by'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/** @return list<array{key: string, label: string, type: string, sortable: bool, sort_param: string, width: string}> */
|
/** @return list<array{key: string, label: string, type: string, sortable: bool, sort_param: string, width: string}> */
|
||||||
public function publicColumns(Tenant $tenant): array
|
public function publicColumns(Tenant $tenant): array
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ class AdminAppTicketRowService
|
|||||||
{
|
{
|
||||||
private const FIESTA_FUTBOL_INFANTIL = 'fiesta_futbol_infantil';
|
private const FIESTA_FUTBOL_INFANTIL = 'fiesta_futbol_infantil';
|
||||||
|
|
||||||
|
private const DESFILE_PURA_TENDENCIA = 'desfile_pura_tendencia';
|
||||||
|
|
||||||
private const CATEGORY_PRESENTATIONS = [
|
private const CATEGORY_PRESENTATIONS = [
|
||||||
'alojamientos' => ['category' => 'Camping', 'product' => 'tipo_alojamiento', 'type' => null, 'size' => null],
|
'alojamientos' => ['category' => 'Camping', 'product' => 'tipo_alojamiento', 'type' => null, 'size' => null],
|
||||||
'camping' => ['category' => null, 'product' => 'tipo_alojamiento', 'type' => null, 'size' => null],
|
'camping' => ['category' => null, 'product' => 'tipo_alojamiento', 'type' => null, 'size' => null],
|
||||||
@@ -25,31 +27,65 @@ class AdminAppTicketRowService
|
|||||||
{
|
{
|
||||||
$purchaseItem = $ticket->sourcePurchaseItem;
|
$purchaseItem = $ticket->sourcePurchaseItem;
|
||||||
$refund = $ticket->refund;
|
$refund = $ticket->refund;
|
||||||
|
$variantProperties = $this->variantProperties($ticket);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'source_purchase_item_id' => $ticket->source_purchase_item_id,
|
'source_purchase_item_id' => $ticket->source_purchase_item_id,
|
||||||
'order_number' => $purchaseItem?->compra_id,
|
'order_number' => $purchaseItem?->compra_id,
|
||||||
'product' => $purchaseItem?->item_nombre
|
'product' => $this->productName($ticket, $purchaseItem?->item_nombre, $variantProperties),
|
||||||
?? $ticket->sourceCatalogItem?->nombre
|
'amount' => $purchaseItem?->precio_unitario ?? $ticket->entryReservation?->importe,
|
||||||
?? $ticket->name,
|
|
||||||
'amount' => $purchaseItem?->precio_unitario,
|
|
||||||
'refund_type' => $refund?->type,
|
'refund_type' => $refund?->type,
|
||||||
'refund_type_label' => $refund?->typeLabel(),
|
'refund_type_label' => $refund?->typeLabel(),
|
||||||
'client' => $purchaseItem?->purchase?->nombre_apellido ?? $ticket->user?->nombre_apellido,
|
'client' => $purchaseItem?->purchase?->nombre_apellido ?? $ticket->user?->nombre_apellido,
|
||||||
'status' => $ticket->status,
|
'status' => $ticket->status,
|
||||||
'scanned_by' => $ticket->scannerUser?->nombre_apellido,
|
'scanned_by' => $ticket->scannerUser?->nombre_apellido,
|
||||||
'variant_properties' => $this->variantProperties($ticket),
|
'variant_properties' => $variantProperties,
|
||||||
'allow_refund' => $ticket->allow_refund(),
|
'allow_refund' => $ticket->allow_refund(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array{code: string, label: string, values: list<array{value: string, label: string}>}> $properties
|
||||||
|
*/
|
||||||
|
private function productName(Ticket $ticket, ?string $snapshotName, array $properties): string
|
||||||
|
{
|
||||||
|
$baseName = $ticket->sourceCatalogItem?->nombre ?: $ticket->name;
|
||||||
|
|
||||||
|
if ($ticket->tenant_code !== self::DESFILE_PURA_TENDENCIA) {
|
||||||
|
return $snapshotName ?: $baseName;
|
||||||
|
}
|
||||||
|
|
||||||
|
$seatDescription = collect(['sector', 'fila', 'asiento'])
|
||||||
|
->map(function (string $code) use ($properties): ?string {
|
||||||
|
$property = collect($properties)->firstWhere('code', $code);
|
||||||
|
$values = collect($property['values'] ?? [])->pluck('label')->filter()->implode(', ');
|
||||||
|
|
||||||
|
return $property === null || $values === ''
|
||||||
|
? null
|
||||||
|
: $property['label'].' '.$values;
|
||||||
|
})
|
||||||
|
->filter()
|
||||||
|
->implode(', ');
|
||||||
|
|
||||||
|
return $seatDescription === '' ? ($snapshotName ?: $baseName) : "{$baseName} ({$seatDescription})";
|
||||||
|
}
|
||||||
|
|
||||||
/** @param array<string, mixed>|null $details */
|
/** @param array<string, mixed>|null $details */
|
||||||
public function values(Ticket $ticket, ?array $details = null): array
|
public function values(Ticket $ticket, ?array $details = null): array
|
||||||
{
|
{
|
||||||
$details ??= $this->details($ticket);
|
$details ??= $this->details($ticket);
|
||||||
$presentation = $this->presentation($ticket, $details);
|
$presentation = $this->presentation($ticket, $details);
|
||||||
|
$attributeValues = collect($details['variant_properties'] ?? [])
|
||||||
|
->mapWithKeys(fn (array $property): array => [
|
||||||
|
$property['code'] => collect($property['values'] ?? [])
|
||||||
|
->pluck('label')
|
||||||
|
->filter()
|
||||||
|
->implode(', ') ?: '-',
|
||||||
|
])
|
||||||
|
->all();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
...$attributeValues,
|
||||||
'order_number' => $details['order_number'],
|
'order_number' => $details['order_number'],
|
||||||
'category' => $presentation['category'],
|
'category' => $presentation['category'],
|
||||||
'product' => $presentation['product'],
|
'product' => $presentation['product'],
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
namespace App\Domains\Ticketing\Ticket\Services;
|
namespace App\Domains\Ticketing\Ticket\Services;
|
||||||
|
|
||||||
use App\Domains\Core\Auth\Models\User;
|
|
||||||
use App\Domains\Commerce\Catalog\Enums\InventoryPolicy;
|
use App\Domains\Commerce\Catalog\Enums\InventoryPolicy;
|
||||||
use App\Domains\Commerce\Catalog\Models\Inventory;
|
use App\Domains\Commerce\Catalog\Models\Inventory;
|
||||||
use App\Domains\Commerce\Catalog\Models\Variant;
|
use App\Domains\Commerce\Catalog\Models\Variant;
|
||||||
use App\Domains\Commerce\Purchase\Models\PurchaseItem;
|
use App\Domains\Commerce\Purchase\Models\PurchaseItem;
|
||||||
use App\Domains\Commerce\Purchase\Services\PurchaseRefundSummaryService;
|
use App\Domains\Commerce\Purchase\Services\PurchaseRefundSummaryService;
|
||||||
|
use App\Domains\Core\Auth\Models\User;
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
||||||
use App\Domains\Ticketing\Ticket\Models\TicketRefund;
|
use App\Domains\Ticketing\Ticket\Models\TicketRefund;
|
||||||
@@ -27,11 +27,13 @@ class AdminAppTicketService
|
|||||||
'scannerUser',
|
'scannerUser',
|
||||||
'sourceCatalogItem.category',
|
'sourceCatalogItem.category',
|
||||||
'sourcePurchaseItem.purchase',
|
'sourcePurchaseItem.purchase',
|
||||||
|
'entryReservation',
|
||||||
'refund.createdBy',
|
'refund.createdBy',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly AdminAppTicketColumnService $columnService,
|
private readonly AdminAppTicketColumnService $columnService,
|
||||||
|
private readonly AdminAppTicketAttributeService $attributeService,
|
||||||
private readonly AdminAppTicketRowService $rowService,
|
private readonly AdminAppTicketRowService $rowService,
|
||||||
private readonly PurchaseRefundSummaryService $refundSummaryService,
|
private readonly PurchaseRefundSummaryService $refundSummaryService,
|
||||||
) {}
|
) {}
|
||||||
@@ -347,6 +349,13 @@ class AdminAppTicketService
|
|||||||
|
|
||||||
$this->applyStatusFilter($query, $filters['status'] ?? null);
|
$this->applyStatusFilter($query, $filters['status'] ?? null);
|
||||||
|
|
||||||
|
foreach ($this->attributeService->attributes($tenant) as $attribute) {
|
||||||
|
$value = $filters[$attribute['code']] ?? null;
|
||||||
|
if ($value !== null && $value !== '') {
|
||||||
|
$this->whereVariantDefinition($query, $attribute['code'], (string) $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Ticketing\Ticket\Services;
|
||||||
|
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
|
use DateTimeZone;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
class ConvertValidityTimesToUtcService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<int, int> $ids
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ use Illuminate\Support\Collection;
|
|||||||
final readonly class ResolvedValidityGroup
|
final readonly class ResolvedValidityGroup
|
||||||
{
|
{
|
||||||
/** @param Collection<int, ValidityTime> $validityTimes */
|
/** @param Collection<int, ValidityTime> $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. */
|
/** Comprueba si el instante pertenece a la intersección efectiva del grupo. */
|
||||||
public function isValid(?CarbonInterface $at = null): bool
|
public function isValid(?CarbonInterface $at = null): bool
|
||||||
@@ -50,7 +50,17 @@ final readonly class ResolvedValidityGroup
|
|||||||
$anchor = $this->dateAnchor() ?? $at;
|
$anchor = $this->dateAnchor() ?? $at;
|
||||||
|
|
||||||
return $this->validityTimes
|
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()
|
->filter()
|
||||||
->sortByDesc(fn (CarbonInterface $startsAt): int => $startsAt->getTimestamp())
|
->sortByDesc(fn (CarbonInterface $startsAt): int => $startsAt->getTimestamp())
|
||||||
->first();
|
->first();
|
||||||
@@ -67,11 +77,13 @@ final readonly class ResolvedValidityGroup
|
|||||||
|
|
||||||
return $this->validityTimes
|
return $this->validityTimes
|
||||||
->map(function (ValidityTime $validityTime) use ($anchor): ?CarbonInterface {
|
->map(function (ValidityTime $validityTime) use ($anchor): ?CarbonInterface {
|
||||||
$startsAt = $validityTime->startsAt($anchor);
|
$startsAt = $validityTime->startsAt($anchor, $this->timezone);
|
||||||
$expiresAt = $validityTime->expiresAt($anchor);
|
$expiresAt = $validityTime->expiresAt($anchor, $this->timezone);
|
||||||
|
|
||||||
if ($startsAt !== null && $expiresAt !== null && $expiresAt->lessThanOrEqualTo($startsAt)) {
|
if ($startsAt !== null && $expiresAt !== null && $expiresAt->lessThanOrEqualTo($startsAt)) {
|
||||||
return $expiresAt->addDay();
|
return $this->dateAnchor() === null && $anchor->lessThan($expiresAt)
|
||||||
|
? $expiresAt
|
||||||
|
: $expiresAt->copy()->addDay();
|
||||||
}
|
}
|
||||||
|
|
||||||
return $expiresAt;
|
return $expiresAt;
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Domains\Ticketing\Ticket\Services;
|
namespace App\Domains\Ticketing\Ticket\Services;
|
||||||
|
|
||||||
use App\Domains\Core\Auth\Models\User;
|
|
||||||
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
||||||
use App\Domains\Commerce\Catalog\Models\Variant;
|
use App\Domains\Commerce\Catalog\Models\Variant;
|
||||||
|
use App\Domains\Core\Auth\Models\User;
|
||||||
use App\Domains\Ticketing\Ticket\Exceptions\TicketGenerationException;
|
use App\Domains\Ticketing\Ticket\Exceptions\TicketGenerationException;
|
||||||
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
@@ -44,6 +44,7 @@ class TicketGeneratorService
|
|||||||
$variant = $target['variant'];
|
$variant = $target['variant'];
|
||||||
$ticket = Ticket::query()->create([
|
$ticket = Ticket::query()->create([
|
||||||
'tenant_code' => $item->tenant_code,
|
'tenant_code' => $item->tenant_code,
|
||||||
|
'event_id' => $item->event_id,
|
||||||
'ticket' => (string) Str::uuid(),
|
'ticket' => (string) Str::uuid(),
|
||||||
'source_purchase_item_id' => $sourcePurchaseItemId,
|
'source_purchase_item_id' => $sourcePurchaseItemId,
|
||||||
'source_catalog_item_id' => $item->getKey(),
|
'source_catalog_item_id' => $item->getKey(),
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class TicketValidityResolver
|
|||||||
|
|
||||||
/** Relaciones necesarias para resolver tickets sin consultas N+1. */
|
/** Relaciones necesarias para resolver tickets sin consultas N+1. */
|
||||||
public const RELATIONS = [
|
public const RELATIONS = [
|
||||||
|
'sourceVariant.catalogItem.tenant',
|
||||||
'sourceVariant.eventDates.validityTime',
|
'sourceVariant.eventDates.validityTime',
|
||||||
'sourceVariant.eventDate.validityTime',
|
'sourceVariant.eventDate.validityTime',
|
||||||
'sourceVariant.definitions.itemAttribute.attribute.options.validityTime',
|
'sourceVariant.definitions.itemAttribute.attribute.options.validityTime',
|
||||||
@@ -60,6 +61,7 @@ class TicketValidityResolver
|
|||||||
public function resolveVariant(Variant $variant): ResolvedTicketValidity
|
public function resolveVariant(Variant $variant): ResolvedTicketValidity
|
||||||
{
|
{
|
||||||
$variant->loadMissing([
|
$variant->loadMissing([
|
||||||
|
'catalogItem.tenant',
|
||||||
'eventDates.validityTime',
|
'eventDates.validityTime',
|
||||||
'eventDate.validityTime',
|
'eventDate.validityTime',
|
||||||
'definitions.itemAttribute.attribute.options.validityTime',
|
'definitions.itemAttribute.attribute.options.validityTime',
|
||||||
@@ -149,7 +151,7 @@ class TicketValidityResolver
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new ResolvedTicketValidity(
|
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'))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Ticketing\Ticket\Services;
|
||||||
|
|
||||||
|
use App\Domains\Core\Auth\Models\User;
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Ticketing\Event\Models\Event;
|
||||||
|
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
class UserTicketQueryService
|
||||||
|
{
|
||||||
|
/** @return Collection<int, Ticket> */
|
||||||
|
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<int, Event> */
|
||||||
|
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<Ticket> */
|
||||||
|
private function ticketQuery(Tenant $tenant, User $user): Builder
|
||||||
|
{
|
||||||
|
return Ticket::query()
|
||||||
|
->where('tenant_code', $tenant->codigo)
|
||||||
|
->where('user_id', $user->getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,29 @@ Genera, valida, consulta y exporta entradas asociadas a compras pagadas de produ
|
|||||||
se combinan con OR y las dimensiones diferentes se combinan con AND. El modelo calcula si un ticket está
|
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.
|
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
|
## Flujo de generación
|
||||||
|
|
||||||
1. `Purchase` emite `PurchasePaid` al confirmarse el pago.
|
1. `Purchase` emite `PurchasePaid` al confirmarse el pago.
|
||||||
@@ -69,3 +92,48 @@ Bajo `/v1/adminapp/tenant`, protegido por `auth:sanctum`, `adminapp.tenant` y el
|
|||||||
## Dependencias y reglas
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Route;
|
|||||||
Route::prefix('tenants/{tenant:codigo}')
|
Route::prefix('tenants/{tenant:codigo}')
|
||||||
->middleware('auth:sanctum')
|
->middleware('auth:sanctum')
|
||||||
->group(function (): void {
|
->group(function (): void {
|
||||||
|
Route::get('tickets/events', [TicketController::class, 'events']);
|
||||||
Route::get('tickets', [TicketController::class, 'index']);
|
Route::get('tickets', [TicketController::class, 'index']);
|
||||||
Route::post('tickets/pdf', [TicketController::class, 'downloadPdf']);
|
Route::post('tickets/pdf', [TicketController::class, 'downloadPdf']);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
namespace App\Shared\Attachable\Models;
|
namespace App\Shared\Attachable\Models;
|
||||||
|
|
||||||
use App\Shared\Attachable\Enums\AttachmentType;
|
use App\Shared\Attachable\Enums\AttachmentType;
|
||||||
|
use App\Shared\Storage\Services\TemporaryUrlService;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
#[Fillable([
|
#[Fillable([
|
||||||
@@ -70,9 +70,9 @@ class Attachment extends Model
|
|||||||
*/
|
*/
|
||||||
public function getTemporaryUrl(int $expiresInMinutes = 10): string
|
public function getTemporaryUrl(int $expiresInMinutes = 10): string
|
||||||
{
|
{
|
||||||
return Storage::disk('s3')->temporaryUrl(
|
return app(TemporaryUrlService::class)->generate(
|
||||||
$this->path,
|
$this->path,
|
||||||
now()->addMinutes($expiresInMinutes)
|
$expiresInMinutes,
|
||||||
);
|
)['temporary_url'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Shared\Forms\Controllers\AdminApp;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Shared\Forms\Resources\DesfileEntryReservationFormResource;
|
||||||
|
use App\Shared\Forms\Services\DesfileEntryReservationFormService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class DesfileEntryReservationFormController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly DesfileEntryReservationFormService $formService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function __invoke(Request $request): DesfileEntryReservationFormResource
|
||||||
|
{
|
||||||
|
return DesfileEntryReservationFormResource::make(
|
||||||
|
$this->formService->get($request->user()->tenant()->firstOrFail()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Shared\Forms\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class DesfileEntryReservationFormResource extends JsonResource
|
||||||
|
{
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'payment_types' => $this->resource['payment_types'],
|
||||||
|
'fields' => $this->resource['fields'],
|
||||||
|
'variants' => $this->resource['variants'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Shared\Forms\Services;
|
||||||
|
|
||||||
|
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\Desfile\Enums\EntryReservationPaymentType;
|
||||||
|
|
||||||
|
class DesfileEntryReservationFormService
|
||||||
|
{
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function get(Tenant $tenant): array
|
||||||
|
{
|
||||||
|
$entry = CatalogItem::query()->forTenantCatalog($tenant)->where('slug', 'entrada')
|
||||||
|
->with([
|
||||||
|
'variants' => fn ($query) => $query->orderBy('id'),
|
||||||
|
'variants.inventory',
|
||||||
|
'variants.definitions.itemAttribute.attribute',
|
||||||
|
'variants.eventDates',
|
||||||
|
'variants.eventDate',
|
||||||
|
])->first();
|
||||||
|
$variants = $entry?->visibleVariants()
|
||||||
|
->map(function (Variant $variant): array {
|
||||||
|
$values = $variant->selectionValues();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $variant->id,
|
||||||
|
'tipo' => (string) $values->get('tipo'),
|
||||||
|
'sector' => (string) $values->get('sector'),
|
||||||
|
'fila' => (string) $values->get('fila'),
|
||||||
|
'asiento' => (string) $values->get('asiento'),
|
||||||
|
'price' => $variant->getPrice(),
|
||||||
|
];
|
||||||
|
})->values() ?? collect();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'payment_types' => EntryReservationPaymentType::options(),
|
||||||
|
'fields' => collect(['tipo' => 'Tipo', 'sector' => 'Sector', 'fila' => 'Fila', 'asiento' => 'Asiento'])
|
||||||
|
->map(fn (string $label, string $key): array => [
|
||||||
|
'key' => $key,
|
||||||
|
'label' => $label,
|
||||||
|
'options' => $variants->pluck($key)->unique()->sort(SORT_NATURAL)->values()
|
||||||
|
->map(fn (string $value): array => ['value' => $value, 'label' => $value])->all(),
|
||||||
|
])->values()->all(),
|
||||||
|
'variants' => $variants->all(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,15 +4,19 @@ namespace App\Shared\Forms\Services;
|
|||||||
|
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
||||||
|
use App\Domains\Ticketing\Ticket\Services\AdminAppTicketAttributeService;
|
||||||
use App\Domains\Ticketing\Ticket\Services\AdminAppTicketColumnService;
|
use App\Domains\Ticketing\Ticket\Services\AdminAppTicketColumnService;
|
||||||
|
|
||||||
class TicketFilterFormService
|
class TicketFilterFormService
|
||||||
{
|
{
|
||||||
private const FIESTA_FUTBOL_INFANTIL = 'fiesta_futbol_infantil';
|
private const FIESTA_FUTBOL_INFANTIL = 'fiesta_futbol_infantil';
|
||||||
|
|
||||||
|
private const DESFILE_PURA_TENDENCIA = 'desfile_pura_tendencia';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly TicketFormService $ticketFormService,
|
private readonly TicketFormService $ticketFormService,
|
||||||
private readonly AdminAppTicketColumnService $columnService,
|
private readonly AdminAppTicketColumnService $columnService,
|
||||||
|
private readonly AdminAppTicketAttributeService $attributeService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** @return array<string, mixed> */
|
/** @return array<string, mixed> */
|
||||||
@@ -27,6 +31,13 @@ class TicketFilterFormService
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($tenant->codigo === self::DESFILE_PURA_TENDENCIA) {
|
||||||
|
$fields = [
|
||||||
|
...$this->desfileFields($tenant),
|
||||||
|
...$this->commonFields(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'code' => 'tickets_filter',
|
'code' => 'tickets_filter',
|
||||||
'action' => '/api/v1/adminapp/tenant/tickets',
|
'action' => '/api/v1/adminapp/tenant/tickets',
|
||||||
@@ -36,6 +47,21 @@ class TicketFilterFormService
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return list<array<string, mixed>> */
|
||||||
|
private function desfileFields(Tenant $tenant): array
|
||||||
|
{
|
||||||
|
return array_map(fn (array $attribute): array => [
|
||||||
|
'name' => $attribute['code'],
|
||||||
|
'query_param' => $attribute['code'],
|
||||||
|
'label' => $attribute['label'],
|
||||||
|
'type' => 'select',
|
||||||
|
'required' => false,
|
||||||
|
'default' => null,
|
||||||
|
'placeholder' => $attribute['label'],
|
||||||
|
'options' => $attribute['options'],
|
||||||
|
], $this->attributeService->attributes($tenant));
|
||||||
|
}
|
||||||
|
|
||||||
/** @return list<array<string, mixed>> */
|
/** @return list<array<string, mixed>> */
|
||||||
private function fiestaFutbolInfantilFields(Tenant $tenant): array
|
private function fiestaFutbolInfantilFields(Tenant $tenant): array
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Shared\Forms\Controllers\AdminApp\DesfileEntryReservationFormController;
|
||||||
use App\Shared\Forms\Controllers\AdminApp\EntryFormController;
|
use App\Shared\Forms\Controllers\AdminApp\EntryFormController;
|
||||||
use App\Shared\Forms\Controllers\AdminApp\EventFormController;
|
use App\Shared\Forms\Controllers\AdminApp\EventFormController;
|
||||||
use App\Shared\Forms\Controllers\AdminApp\FoodFormController;
|
use App\Shared\Forms\Controllers\AdminApp\FoodFormController;
|
||||||
@@ -14,6 +15,11 @@ Route::prefix('v1/adminapp/forms')
|
|||||||
->middleware(['auth:sanctum', 'adminapp.tenant'])
|
->middleware(['auth:sanctum', 'adminapp.tenant'])
|
||||||
->group(function (): void {
|
->group(function (): void {
|
||||||
Route::get('event', EventFormController::class);
|
Route::get('event', EventFormController::class);
|
||||||
|
Route::get(
|
||||||
|
'desfile/entry-reservation',
|
||||||
|
DesfileEntryReservationFormController::class
|
||||||
|
)->middleware('tenant.menu:adminapp.desfile.reservas')
|
||||||
|
->name('adminapp.forms.desfile.entry-reservation');
|
||||||
Route::get('sale', SaleFormController::class);
|
Route::get('sale', SaleFormController::class);
|
||||||
Route::get('staff', StaffFormController::class);
|
Route::get('staff', StaffFormController::class);
|
||||||
Route::get('tickets-filter', TicketFilterFormController::class)
|
Route::get('tickets-filter', TicketFilterFormController::class)
|
||||||
|
|||||||
60
app/Shared/Storage/Services/TemporaryUrlService.php
Normal file
60
app/Shared/Storage/Services/TemporaryUrlService.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Shared\Storage\Services;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
class TemporaryUrlService
|
||||||
|
{
|
||||||
|
protected const REFRESH_MARGIN_SECONDS = 60;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{temporary_url: string, temporary_url_expires_at: string}
|
||||||
|
*/
|
||||||
|
public function generate(string $path, int $expiresInMinutes = 10): array
|
||||||
|
{
|
||||||
|
if ($expiresInMinutes < 1) {
|
||||||
|
throw new InvalidArgumentException('The temporary URL expiration must be at least one minute.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$cacheTtlInSeconds = max(
|
||||||
|
1,
|
||||||
|
($expiresInMinutes * 60) - self::REFRESH_MARGIN_SECONDS,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Cache::remember(
|
||||||
|
$this->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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Shared\StorageTest\Services;
|
namespace App\Shared\StorageTest\Services;
|
||||||
|
|
||||||
|
use App\Shared\Storage\Services\TemporaryUrlService;
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
@@ -10,6 +11,10 @@ use RuntimeException;
|
|||||||
|
|
||||||
class S3TestService
|
class S3TestService
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected TemporaryUrlService $temporaryUrlService,
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, int|string|null>
|
* @return array<string, int|string|null>
|
||||||
*/
|
*/
|
||||||
@@ -44,8 +49,7 @@ class S3TestService
|
|||||||
'mime_type' => $file->getClientMimeType(),
|
'mime_type' => $file->getClientMimeType(),
|
||||||
'extension' => $file->extension(),
|
'extension' => $file->extension(),
|
||||||
'size' => $file->getSize(),
|
'size' => $file->getSize(),
|
||||||
'temporary_url' => $disk->temporaryUrl($path, now()->addMinutes($expiresInMinutes)),
|
...$this->temporaryUrlService->generate($path, $expiresInMinutes),
|
||||||
'temporary_url_expires_at' => now()->addMinutes($expiresInMinutes)->toIso8601String(),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,16 +62,10 @@ class S3TestService
|
|||||||
'disk' => 's3',
|
'disk' => 's3',
|
||||||
'key' => $path,
|
'key' => $path,
|
||||||
'path' => $path,
|
'path' => $path,
|
||||||
'temporary_url' => $this->temporaryUrlForPath($path, $expiresInMinutes),
|
...$this->temporaryUrlService->generate($path, $expiresInMinutes),
|
||||||
'temporary_url_expires_at' => now()->addMinutes($expiresInMinutes)->toIso8601String(),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function temporaryUrlForPath(string $path, int $expiresInMinutes): string
|
|
||||||
{
|
|
||||||
return Storage::disk('s3')->temporaryUrl($path, now()->addMinutes($expiresInMinutes));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function normalizeDirectory(?string $directory): string
|
protected function normalizeDirectory(?string $directory): string
|
||||||
{
|
{
|
||||||
$directory = trim((string) $directory, '/');
|
$directory = trim((string) $directory, '/');
|
||||||
|
|||||||
@@ -67,9 +67,6 @@ return [
|
|||||||
|
|
||||||
'timezone' => 'UTC',
|
'timezone' => 'UTC',
|
||||||
|
|
||||||
// Las fechas de los eventos se ingresan como horarios locales de Argentina.
|
|
||||||
'event_timezone' => env('EVENT_TIMEZONE', 'America/Argentina/Buenos_Aires'),
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Application Locale Configuration
|
| Application Locale Configuration
|
||||||
|
|||||||
2691
database/data/mutual_smep_catalog_v1.json
Normal file
2691
database/data/mutual_smep_catalog_v1.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('tenants', function (Blueprint $table): void {
|
||||||
|
$table->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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Database\Seeders\FiestaTradicionArrufoSeeder;
|
||||||
|
use Database\Seeders\OnTicketTenantSeeder;
|
||||||
|
use Database\Seeders\WebsiteTypeSeeder;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
private const TENANT_CODE = 'onticket';
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
if (app()->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.
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Domains\Core\Tenant\Models\AdminWebsiteType;
|
||||||
|
use Database\Seeders\FiestaTradicionArrufoSeeder;
|
||||||
|
use Database\Seeders\OnTicketTenantSeeder;
|
||||||
|
use Database\Seeders\WebsiteTypeSeeder;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (app()->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.
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Database\Seeders\OptimizeOnticketImagesSeeder;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (app()->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.
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('events', function (Blueprint $table): void {
|
||||||
|
$table->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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('tickets', function (Blueprint $table): void {
|
||||||
|
$table->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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
private const MENU_CODE = 'adminapp.desfile.reservas';
|
||||||
|
|
||||||
|
private const TENANT_CODE = 'desfile_pura_tendencia';
|
||||||
|
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (! DB::table('menues')->where('code', 'main.adminapp')->exists()) {
|
||||||
|
// Reference data is added by seeders on fresh installations.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = now();
|
||||||
|
|
||||||
|
DB::transaction(function () use ($now): void {
|
||||||
|
DB::table('menues')->updateOrInsert(
|
||||||
|
['code' => self::MENU_CODE],
|
||||||
|
[
|
||||||
|
'label' => 'Reserva de Tickets',
|
||||||
|
'parent_menu_code' => 'main.adminapp',
|
||||||
|
'content_type' => 'dynamic',
|
||||||
|
'static_content_schema' => null,
|
||||||
|
'route' => '/admin/desfile/reservas',
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
DB::table('tenants_menues')
|
||||||
|
->where('menu_code', self::MENU_CODE)
|
||||||
|
->where('tenant_code', '!=', self::TENANT_CODE)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
if (DB::table('tenants')->where('codigo', self::TENANT_CODE)->exists()) {
|
||||||
|
DB::table('tenants_menues')->updateOrInsert(
|
||||||
|
[
|
||||||
|
'tenant_code' => self::TENANT_CODE,
|
||||||
|
'menu_code' => self::MENU_CODE,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'static_content' => null,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('roles')
|
||||||
|
->whereIn('codigo', ['admin', 'adminapp'])
|
||||||
|
->pluck('codigo')
|
||||||
|
->each(function (string $roleCode): void {
|
||||||
|
DB::table('roles_menues')->updateOrInsert([
|
||||||
|
'rol_codigo' => $roleCode,
|
||||||
|
'menu_codigo' => self::MENU_CODE,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
DB::transaction(function (): void {
|
||||||
|
DB::table('tenants_menues')
|
||||||
|
->where('menu_code', self::MENU_CODE)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
DB::table('roles_menues')
|
||||||
|
->where('menu_codigo', self::MENU_CODE)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
DB::table('menues')
|
||||||
|
->where('code', self::MENU_CODE)
|
||||||
|
->delete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('desfile_entry_reservations', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('variant_id')
|
||||||
|
->constrained('variantes')
|
||||||
|
->restrictOnDelete();
|
||||||
|
$table->timestamp('fecha_reserva');
|
||||||
|
$table->decimal('importe', 10, 2)->nullable();
|
||||||
|
$table->string('tipo_pago', 24);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index(['fecha_reserva', 'tipo_pago']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('desfile_entry_reservations');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('inventories', function (Blueprint $table): void {
|
||||||
|
$table->unsignedInteger('entry_reserved_stock')->default(0);
|
||||||
|
});
|
||||||
|
Schema::create('desfile_reservation_batches', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained('users')->restrictOnDelete();
|
||||||
|
$table->string('tenant_code');
|
||||||
|
$table->uuid('idempotency_key');
|
||||||
|
$table->string('request_hash', 64);
|
||||||
|
$table->timestamps();
|
||||||
|
$table->unique(['user_id', 'idempotency_key']);
|
||||||
|
});
|
||||||
|
Schema::table('desfile_entry_reservations', function (Blueprint $table): void {
|
||||||
|
$table->foreignId('batch_id')->nullable()->constrained('desfile_reservation_batches')->restrictOnDelete();
|
||||||
|
$table->foreignId('ticket_id')->nullable()->unique()->constrained('tickets')->restrictOnDelete();
|
||||||
|
$table->foreignId('inventory_id')->nullable()->constrained('inventories')->restrictOnDelete();
|
||||||
|
});
|
||||||
|
// Legacy reservations remain excluded by visibleVariants(). Their past stock
|
||||||
|
// movements cannot be inferred safely; only new reservations use this counter.
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('desfile_entry_reservations', function (Blueprint $table): void {
|
||||||
|
$table->dropConstrainedForeignId('batch_id');
|
||||||
|
$table->dropUnique(['ticket_id']);
|
||||||
|
$table->dropConstrainedForeignId('ticket_id');
|
||||||
|
$table->dropConstrainedForeignId('inventory_id');
|
||||||
|
});
|
||||||
|
Schema::dropIfExists('desfile_reservation_batches');
|
||||||
|
Schema::table('inventories', fn (Blueprint $table) => $table->dropColumn('entry_reserved_stock'));
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,326 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
private const CLIENT_CODE = 'mutual_smep';
|
||||||
|
|
||||||
|
private const TENANT_CODE = 'mutual_smep';
|
||||||
|
|
||||||
|
private const EXCLUDED_MENU_CODES = [
|
||||||
|
'account.tickets',
|
||||||
|
'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',
|
||||||
|
'event.index',
|
||||||
|
'event.category',
|
||||||
|
'event.detail',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @var list<string> */
|
||||||
|
private array $storedPaths = [];
|
||||||
|
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (DB::table('tenants')->where('codigo', self::TENANT_CODE)->exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
! DB::table('admin_website_types')->where('codigo', 'shopit')->exists()
|
||||||
|
|| ! DB::table('storefront_website_types')->where('codigo', 'shopit')->exists()
|
||||||
|
) {
|
||||||
|
// Fresh installations provision website-type reference data separately.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::transaction(function (): void {
|
||||||
|
$clientId = $this->clientId();
|
||||||
|
$headerLogoId = $this->storeImage(
|
||||||
|
'images/tennants/mutual_smep/mutual_smep_header.png',
|
||||||
|
'mutual_smep_header.png',
|
||||||
|
);
|
||||||
|
$footerLogoId = $this->storeImage(
|
||||||
|
'images/tennants/mutual_smep/mutual_smep_footer.png',
|
||||||
|
'mutual_smep_footer.png',
|
||||||
|
);
|
||||||
|
$faviconId = $this->storeImage(
|
||||||
|
'images/tennants/mutual_smep/mutual_smep_favicon.png',
|
||||||
|
'mutual_smep_favicon.png',
|
||||||
|
);
|
||||||
|
$now = now();
|
||||||
|
|
||||||
|
DB::table('tenants')->insert([
|
||||||
|
'client_id' => $clientId,
|
||||||
|
'codigo' => self::TENANT_CODE,
|
||||||
|
'nombre' => 'Mutual SMEP',
|
||||||
|
'timezone' => 'America/Argentina/Buenos_Aires',
|
||||||
|
'dominio' => 'mutual-smep.localhost',
|
||||||
|
'base_path' => '/',
|
||||||
|
'site_title' => 'Tienda Mutual SMEP',
|
||||||
|
'address' => 'San Lorenzo 1543, Rosario, Santa Fe',
|
||||||
|
'phone' => '+54 9 341 247-4530',
|
||||||
|
'primary_color' => '#4A7FF5',
|
||||||
|
'secondary_color' => '#0051A4',
|
||||||
|
'danger_color' => '#FF8888',
|
||||||
|
'success_color' => '#198754',
|
||||||
|
'header_bg_color' => '#FFFFFF',
|
||||||
|
'footer_bg_color' => '#0051A4',
|
||||||
|
'header_logo_id' => $headerLogoId,
|
||||||
|
'footer_logo_id' => $footerLogoId,
|
||||||
|
'favicon_id' => $faviconId,
|
||||||
|
'admin_website_type_code' => 'shopit',
|
||||||
|
'storefront_website_type_code' => 'shopit',
|
||||||
|
'display_categories' => true,
|
||||||
|
'display_seach_bar' => true,
|
||||||
|
'display_cart' => true,
|
||||||
|
'cart_editing_policy' => 'quantity_and_remove',
|
||||||
|
'checkout_editing_policy' => 'disabled',
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assignSocialMedia($now);
|
||||||
|
$this->assignMenus($now);
|
||||||
|
});
|
||||||
|
} catch (Throwable $throwable) {
|
||||||
|
Storage::disk('s3')->delete($this->storedPaths);
|
||||||
|
|
||||||
|
throw $throwable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$tenant = DB::table('tenants')
|
||||||
|
->where('codigo', self::TENANT_CODE)
|
||||||
|
->first(['id', 'client_id', 'header_logo_id', 'footer_logo_id', 'favicon_id']);
|
||||||
|
|
||||||
|
if ($tenant === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attachmentIds = collect([
|
||||||
|
$tenant->header_logo_id,
|
||||||
|
$tenant->footer_logo_id,
|
||||||
|
$tenant->favicon_id,
|
||||||
|
])->filter()->unique()->values();
|
||||||
|
$storedPaths = DB::table('attachments')
|
||||||
|
->whereIn('id', $attachmentIds)
|
||||||
|
->pluck('path')
|
||||||
|
->all();
|
||||||
|
|
||||||
|
DB::transaction(function () use ($tenant, $attachmentIds): void {
|
||||||
|
DB::table('tenant_social_media')
|
||||||
|
->where('tenant_code', self::TENANT_CODE)
|
||||||
|
->delete();
|
||||||
|
DB::table('tenants_menues')
|
||||||
|
->where('tenant_code', self::TENANT_CODE)
|
||||||
|
->delete();
|
||||||
|
DB::table('tenants')
|
||||||
|
->where('id', $tenant->id)
|
||||||
|
->delete();
|
||||||
|
DB::table('attachments')
|
||||||
|
->whereIn('id', $attachmentIds)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
$clientHasTenants = DB::table('tenants')
|
||||||
|
->where('client_id', $tenant->client_id)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if (! $clientHasTenants) {
|
||||||
|
DB::table('clients')
|
||||||
|
->where('id', $tenant->client_id)
|
||||||
|
->where('code', self::CLIENT_CODE)
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Storage::disk('s3')->delete($storedPaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function clientId(): int
|
||||||
|
{
|
||||||
|
$clientId = DB::table('clients')->where('code', self::CLIENT_CODE)->value('id');
|
||||||
|
|
||||||
|
if ($clientId !== null) {
|
||||||
|
DB::table('clients')->where('id', $clientId)->update([
|
||||||
|
'name' => 'Mutual SMEP',
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (int) $clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) DB::table('clients')->insertGetId([
|
||||||
|
'code' => self::CLIENT_CODE,
|
||||||
|
'name' => 'Mutual SMEP',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assignSocialMedia(DateTimeInterface $now): void
|
||||||
|
{
|
||||||
|
$socialMedia = [
|
||||||
|
[
|
||||||
|
'code' => 'instagram',
|
||||||
|
'icon' => 'fa-brands fa-instagram',
|
||||||
|
'name' => 'Instagram',
|
||||||
|
'url' => 'https://www.instagram.com/mutualsmep/',
|
||||||
|
'orden' => 0,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'code' => 'facebook',
|
||||||
|
'icon' => 'fa-brands fa-facebook',
|
||||||
|
'name' => 'Facebook',
|
||||||
|
'url' => 'https://www.facebook.com/smeprosario',
|
||||||
|
'orden' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'code' => 'whatsapp',
|
||||||
|
'icon' => 'fa-brands fa-whatsapp',
|
||||||
|
'name' => 'WhatsApp',
|
||||||
|
'url' => 'https://wa.me/5493412474530',
|
||||||
|
'orden' => 2,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($socialMedia as $network) {
|
||||||
|
DB::table('social_media')->insertOrIgnore([
|
||||||
|
'code' => $network['code'],
|
||||||
|
'icon' => $network['icon'],
|
||||||
|
'name' => $network['name'],
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('tenant_social_media')->insertOrIgnore([
|
||||||
|
'tenant_code' => self::TENANT_CODE,
|
||||||
|
'social_media_code' => $network['code'],
|
||||||
|
'url' => $network['url'],
|
||||||
|
'orden' => $network['orden'],
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assignMenus(DateTimeInterface $now): void
|
||||||
|
{
|
||||||
|
DB::table('menues')
|
||||||
|
->whereNotIn('code', self::EXCLUDED_MENU_CODES)
|
||||||
|
->pluck('code')
|
||||||
|
->each(function (string $menuCode) use ($now): void {
|
||||||
|
$staticContent = match ($menuCode) {
|
||||||
|
'help.faq' => $this->frequentlyAskedQuestions(),
|
||||||
|
'help.contact' => $this->contactContent(),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
DB::table('tenants_menues')->updateOrInsert(
|
||||||
|
[
|
||||||
|
'tenant_code' => self::TENANT_CODE,
|
||||||
|
'menu_code' => $menuCode,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'static_content' => $staticContent === null
|
||||||
|
? null
|
||||||
|
: json_encode($staticContent, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<array{pregunta: string, respuesta: string, is_active: bool}> */
|
||||||
|
private function frequentlyAskedQuestions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'pregunta' => '¿Hay algún límite de compra?',
|
||||||
|
'respuesta' => 'La cantidad disponible depende del stock de cada producto.',
|
||||||
|
'is_active' => true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'pregunta' => '¿Cuáles son los medios de pago disponibles?',
|
||||||
|
'respuesta' => 'Podés consultar y seleccionar los medios de pago habilitados al finalizar tu compra.',
|
||||||
|
'is_active' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'pregunta' => '¿Cómo puedo recibir asesoramiento antes de comprar?',
|
||||||
|
'respuesta' => 'Podés comunicarte con Mutual SMEP por WhatsApp al +54 9 341 247-4530.',
|
||||||
|
'is_active' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
private function contactContent(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'whatsapp' => [
|
||||||
|
'whatsapp_url' => 'https://wa.me/5493412474530',
|
||||||
|
'whatsapp_label' => 'Chateá con Mutual SMEP',
|
||||||
|
],
|
||||||
|
'phone' => '+54 9 341 247-4530',
|
||||||
|
'locations' => [
|
||||||
|
'rosario' => [
|
||||||
|
'label' => 'Rosario',
|
||||||
|
'addresses' => [
|
||||||
|
[
|
||||||
|
'label' => 'Mutual SMEP',
|
||||||
|
'address' => 'San Lorenzo 1543, Rosario, Santa Fe',
|
||||||
|
'coordinates' => [-32.9431184, -60.6437991],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function storeImage(string $relativePath, string $filename): int
|
||||||
|
{
|
||||||
|
$sourcePath = public_path($relativePath);
|
||||||
|
|
||||||
|
if (! is_file($sourcePath)) {
|
||||||
|
throw new RuntimeException("Image not found at path: {$sourcePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$contents = file_get_contents($sourcePath);
|
||||||
|
|
||||||
|
if ($contents === false) {
|
||||||
|
throw new RuntimeException("Could not read image at path: {$sourcePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = (string) Str::uuid();
|
||||||
|
$storedPath = 'tenants/'.self::TENANT_CODE."/{$key}.png";
|
||||||
|
|
||||||
|
if (! Storage::disk('s3')->put($storedPath, $contents)) {
|
||||||
|
throw new RuntimeException("Could not store image at path: {$storedPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->storedPaths[] = $storedPath;
|
||||||
|
|
||||||
|
return DB::table('attachments')->insertGetId([
|
||||||
|
'key' => $key,
|
||||||
|
'path' => $storedPath,
|
||||||
|
'filename' => $filename,
|
||||||
|
'type' => 'image',
|
||||||
|
'mime_type' => 'image/png',
|
||||||
|
'extension' => 'png',
|
||||||
|
'size' => strlen($contents),
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('desfile_entry_reservations', function (Blueprint $table): void {
|
||||||
|
$table->softDeletes();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('desfile_entry_reservations', function (Blueprint $table): void {
|
||||||
|
$table->dropSoftDeletes();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
private const TENANT_CODE = 'mutual_smep';
|
||||||
|
|
||||||
|
/** @var array<string, list<string>> */
|
||||||
|
private const CATEGORIES = [
|
||||||
|
'Dormitorio y Blanco' => [
|
||||||
|
'Colchones',
|
||||||
|
'Bases y Sommiers',
|
||||||
|
'Acolchados y Edredones',
|
||||||
|
'Frazadas y Mantas',
|
||||||
|
'Infantil y Cuna',
|
||||||
|
],
|
||||||
|
'Electrodomésticos' => [
|
||||||
|
'Heladeras',
|
||||||
|
'Lavarropas y Secarropas',
|
||||||
|
'Cocinas',
|
||||||
|
'Microondas',
|
||||||
|
'Hornos Eléctricos',
|
||||||
|
'Calefones y Termotanques',
|
||||||
|
'Purificadores y Extractores de Cocina',
|
||||||
|
'Preparación de Alimentos',
|
||||||
|
'Café y Desayuno',
|
||||||
|
'Freidoras y Cocción',
|
||||||
|
'Cuidado Personal',
|
||||||
|
'Limpieza y Planchado',
|
||||||
|
],
|
||||||
|
'Climatización' => [
|
||||||
|
'Aires Acondicionados',
|
||||||
|
'Calefactores a Gas con Salida',
|
||||||
|
'Calefactores a Gas sin Salida',
|
||||||
|
'Calefacción Eléctrica',
|
||||||
|
],
|
||||||
|
'Tecnología' => [
|
||||||
|
'Celulares',
|
||||||
|
'Notebooks',
|
||||||
|
'Smart TV',
|
||||||
|
'Audio',
|
||||||
|
'Accesorios de Computación',
|
||||||
|
'Soportes para TV',
|
||||||
|
],
|
||||||
|
'Bicicletas y Aire Libre' => [
|
||||||
|
'Bicicletas Infantiles',
|
||||||
|
'Bicicletas para Adultos',
|
||||||
|
'Piletas',
|
||||||
|
],
|
||||||
|
'Bazar' => [
|
||||||
|
'Termos',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (! DB::table('tenants')->where('codigo', self::TENANT_CODE)->exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::transaction(function (): void {
|
||||||
|
foreach (self::CATEGORIES as $parentName => $subcategoryNames) {
|
||||||
|
$parentId = $this->upsertCategory($parentName, null);
|
||||||
|
|
||||||
|
foreach ($subcategoryNames as $subcategoryName) {
|
||||||
|
$this->upsertCategory($subcategoryName, $parentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
DB::transaction(function (): void {
|
||||||
|
foreach (self::CATEGORIES as $parentName => $subcategoryNames) {
|
||||||
|
$parentId = DB::table('categorias')
|
||||||
|
->where('tenant_code', self::TENANT_CODE)
|
||||||
|
->where('nombre', $parentName)
|
||||||
|
->whereNull('categoria_id')
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
if ($parentId === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$subcategoryIds = DB::table('categorias')
|
||||||
|
->where('tenant_code', self::TENANT_CODE)
|
||||||
|
->where('categoria_id', $parentId)
|
||||||
|
->whereIn('nombre', $subcategoryNames)
|
||||||
|
->pluck('id');
|
||||||
|
|
||||||
|
foreach ($subcategoryIds as $subcategoryId) {
|
||||||
|
$hasChildren = DB::table('categorias')
|
||||||
|
->where('categoria_id', $subcategoryId)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if (! $hasChildren) {
|
||||||
|
DB::table('categorias')->where('id', $subcategoryId)->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasChildren = DB::table('categorias')
|
||||||
|
->where('categoria_id', $parentId)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if (! $hasChildren) {
|
||||||
|
DB::table('categorias')->where('id', $parentId)->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function upsertCategory(string $name, ?int $parentId): int
|
||||||
|
{
|
||||||
|
$categoryId = DB::table('categorias')
|
||||||
|
->where('tenant_code', self::TENANT_CODE)
|
||||||
|
->where('nombre', $name)
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
if ($categoryId !== null) {
|
||||||
|
DB::table('categorias')->where('id', $categoryId)->update([
|
||||||
|
'categoria_id' => $parentId,
|
||||||
|
'is_enabled' => true,
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (int) $categoryId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) DB::table('categorias')->insertGetId([
|
||||||
|
'tenant_code' => self::TENANT_CODE,
|
||||||
|
'categoria_id' => $parentId,
|
||||||
|
'nombre' => $name,
|
||||||
|
'is_enabled' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
private const TENANT_CODE = 'mutual_smep';
|
||||||
|
|
||||||
|
/** @var list<array<string, mixed>> */
|
||||||
|
private const ATTRIBUTES = [
|
||||||
|
[
|
||||||
|
'codigo' => 'color',
|
||||||
|
'nombre' => 'Color',
|
||||||
|
'metadata_schema' => [
|
||||||
|
'hex' => ['type' => 'string'],
|
||||||
|
],
|
||||||
|
'options' => [
|
||||||
|
['value' => 'Negro', 'label' => 'Negro', 'metadata' => ['hex' => '#000000']],
|
||||||
|
['value' => 'Blanco', 'label' => 'Blanco', 'metadata' => ['hex' => '#FFFFFF']],
|
||||||
|
['value' => 'Gris', 'label' => 'Gris', 'metadata' => ['hex' => '#808080']],
|
||||||
|
['value' => 'Plata', 'label' => 'Plata', 'metadata' => ['hex' => '#C0C0C0']],
|
||||||
|
['value' => 'Azul', 'label' => 'Azul', 'metadata' => ['hex' => '#0000FF']],
|
||||||
|
['value' => 'Rojo', 'label' => 'Rojo', 'metadata' => ['hex' => '#DC3545']],
|
||||||
|
['value' => 'Rosa', 'label' => 'Rosa', 'metadata' => ['hex' => '#FFC0CB']],
|
||||||
|
['value' => 'Multicolor', 'label' => 'Multicolor'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'codigo' => 'medida_cama',
|
||||||
|
'nombre' => 'Medida',
|
||||||
|
'options' => [
|
||||||
|
['value' => 'Cuna', 'label' => 'Cuna'],
|
||||||
|
['value' => '1 Plaza', 'label' => '1 Plaza'],
|
||||||
|
['value' => '1 1/2 Plazas', 'label' => '1 1/2 Plazas'],
|
||||||
|
['value' => '2 Plazas', 'label' => '2 Plazas'],
|
||||||
|
['value' => '2 1/2 Plazas', 'label' => '2 1/2 Plazas'],
|
||||||
|
['value' => 'Queen', 'label' => 'Queen'],
|
||||||
|
['value' => 'King', 'label' => 'King'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'codigo' => 'almacenamiento',
|
||||||
|
'nombre' => 'Almacenamiento',
|
||||||
|
'options' => [
|
||||||
|
['value' => '128 GB', 'label' => '128 GB'],
|
||||||
|
['value' => '256 GB', 'label' => '256 GB'],
|
||||||
|
['value' => '512 GB', 'label' => '512 GB'],
|
||||||
|
['value' => '1 TB', 'label' => '1 TB'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'codigo' => 'memoria_ram',
|
||||||
|
'nombre' => 'Memoria RAM',
|
||||||
|
'options' => [
|
||||||
|
['value' => '4 GB', 'label' => '4 GB'],
|
||||||
|
['value' => '8 GB', 'label' => '8 GB'],
|
||||||
|
['value' => '16 GB', 'label' => '16 GB'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'codigo' => 'rodado',
|
||||||
|
'nombre' => 'Rodado',
|
||||||
|
'options' => [
|
||||||
|
['value' => '16', 'label' => '16'],
|
||||||
|
['value' => '26', 'label' => '26'],
|
||||||
|
['value' => '29', 'label' => '29'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (! DB::table('tenants')->where('codigo', self::TENANT_CODE)->exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::transaction(function (): void {
|
||||||
|
foreach (self::ATTRIBUTES as $definition) {
|
||||||
|
$attributeId = $this->upsertAttribute($definition);
|
||||||
|
|
||||||
|
DB::table('attribute_options')->where('attribute_id', $attributeId)->delete();
|
||||||
|
|
||||||
|
foreach ($definition['options'] as $index => $option) {
|
||||||
|
DB::table('attribute_options')->insert([
|
||||||
|
'attribute_id' => $attributeId,
|
||||||
|
'value' => $option['value'],
|
||||||
|
'label' => $option['label'],
|
||||||
|
'sort_order' => $index + 1,
|
||||||
|
'metadata' => isset($option['metadata'])
|
||||||
|
? json_encode($option['metadata'], JSON_THROW_ON_ERROR)
|
||||||
|
: null,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
DB::table('attribute')
|
||||||
|
->where('tenant_codigo', self::TENANT_CODE)
|
||||||
|
->whereIn('codigo', array_column(self::ATTRIBUTES, 'codigo'))
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $definition */
|
||||||
|
private function upsertAttribute(array $definition): int
|
||||||
|
{
|
||||||
|
$attributeId = DB::table('attribute')
|
||||||
|
->where('tenant_codigo', self::TENANT_CODE)
|
||||||
|
->where('codigo', $definition['codigo'])
|
||||||
|
->value('id');
|
||||||
|
$values = [
|
||||||
|
'nombre' => $definition['nombre'],
|
||||||
|
'is_required' => false,
|
||||||
|
'metadata_schema' => isset($definition['metadata_schema'])
|
||||||
|
? json_encode($definition['metadata_schema'], JSON_THROW_ON_ERROR)
|
||||||
|
: null,
|
||||||
|
'type' => 'select',
|
||||||
|
'updated_at' => now(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($attributeId !== null) {
|
||||||
|
DB::table('attribute')->where('id', $attributeId)->update($values);
|
||||||
|
|
||||||
|
return (int) $attributeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) DB::table('attribute')->insertGetId([
|
||||||
|
'tenant_codigo' => self::TENANT_CODE,
|
||||||
|
'codigo' => $definition['codigo'],
|
||||||
|
...$values,
|
||||||
|
'created_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,462 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
private const TENANT_CODE = 'mutual_smep';
|
||||||
|
|
||||||
|
private const MANIFEST = 'data/mutual_smep_catalog_v1.json';
|
||||||
|
|
||||||
|
private const STORAGE_PREFIX = 'tenants/mutual_smep/catalog-v2/';
|
||||||
|
|
||||||
|
private const LEGACY_STORAGE_PREFIX = 'tenants/mutual_smep/catalog-v1/';
|
||||||
|
|
||||||
|
private const BRAND_MARKER = 'Provisioned by Mutual SMEP catalog migration v1.';
|
||||||
|
|
||||||
|
/** @var list<array{code: string, category: string}> */
|
||||||
|
private const FEATURED_GROUPS = [
|
||||||
|
['code' => 'smep-dormitorio-y-blanco', 'category' => 'Dormitorio y Blanco'],
|
||||||
|
['code' => 'smep-electrodomesticos', 'category' => 'Electrodomésticos'],
|
||||||
|
['code' => 'smep-climatizacion', 'category' => 'Climatización'],
|
||||||
|
['code' => 'smep-tecnologia', 'category' => 'Tecnología'],
|
||||||
|
['code' => 'smep-bicicletas-y-aire-libre', 'category' => 'Bicicletas y Aire Libre'],
|
||||||
|
['code' => 'smep-bazar', 'category' => 'Bazar'],
|
||||||
|
];
|
||||||
|
|
||||||
|
private const HIGHLIGHTED_GROUP_CODE = 'productos-destacados-smep';
|
||||||
|
|
||||||
|
private const CATEGORY_FEATURED_ITEM_LIMIT = 6;
|
||||||
|
|
||||||
|
/** @var list<string> */
|
||||||
|
private const HIGHLIGHTED_PRODUCT_SLUGS = [
|
||||||
|
'acolchado-kavanagh-simil-plumon-reversible',
|
||||||
|
'acolchado-sense-duo-bitono-corderito',
|
||||||
|
'colchon-inducol-constanza',
|
||||||
|
'edredon-lisboa-jean-cartier',
|
||||||
|
'frazada-kavanagh-premium-soft',
|
||||||
|
'kit-edredon-alaska-corderito',
|
||||||
|
'motorola-g15',
|
||||||
|
'smart-tv-noblex-50-dr50-x8500-google-tv',
|
||||||
|
'heladera-gafa-hgf-368afp-330-lt-color-plata',
|
||||||
|
'cafetera-express-atma-ceat-5418p-1-litro',
|
||||||
|
'split-3300-w-tcl-taca-3300fcsa-frio-calor',
|
||||||
|
'bicicleta-venzo-loki-r29-fd-21-v-shimano',
|
||||||
|
'termo-stanley-1-l-adventure-go-to-con-tapon',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @var list<string> */
|
||||||
|
private array $storedPaths = [];
|
||||||
|
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (! DB::table('tenants')->where('codigo', self::TENANT_CODE)->exists()) {
|
||||||
|
throw new RuntimeException("Tenant '".self::TENANT_CODE."' not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$manifest = $this->manifest();
|
||||||
|
$slugs = array_column($manifest['items'], 'slug');
|
||||||
|
$existingSlug = DB::table('catalog_items')
|
||||||
|
->where('tenant_code', self::TENANT_CODE)
|
||||||
|
->whereIn('slug', $slugs)
|
||||||
|
->value('slug');
|
||||||
|
|
||||||
|
if ($existingSlug !== null) {
|
||||||
|
throw new RuntimeException("Catalog item '{$existingSlug}' already exists for Mutual SMEP.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::transaction(function () use ($manifest): void {
|
||||||
|
$brandIds = [];
|
||||||
|
$categoryIds = [];
|
||||||
|
$attributeIds = [];
|
||||||
|
|
||||||
|
foreach ($manifest['items'] as $order => $item) {
|
||||||
|
$brandId = $this->brandId($item['brand'], $brandIds);
|
||||||
|
$categoryId = $this->categoryId(
|
||||||
|
$item['parent_category'],
|
||||||
|
$item['category'],
|
||||||
|
$categoryIds,
|
||||||
|
);
|
||||||
|
$hasVariants = $item['variants'] !== [];
|
||||||
|
$inventoryId = $hasVariants
|
||||||
|
? null
|
||||||
|
: $this->createInventory((int) $item['stock']);
|
||||||
|
$catalogItemId = DB::table('catalog_items')->insertGetId([
|
||||||
|
'tenant_code' => self::TENANT_CODE,
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'brand_id' => $brandId,
|
||||||
|
'inventory_id' => $inventoryId,
|
||||||
|
'type' => 'standard',
|
||||||
|
'slug' => $item['slug'],
|
||||||
|
'nombre' => $item['name'],
|
||||||
|
'group_order' => $order + 1,
|
||||||
|
'descripcion' => $item['description'],
|
||||||
|
'precio' => $item['price'],
|
||||||
|
'inventory_policy' => 'tracked',
|
||||||
|
'inventory_subject' => 'product',
|
||||||
|
'has_tickets' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->attachImages($catalogItemId, null, $item['images']);
|
||||||
|
|
||||||
|
if (! $hasVariants) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$itemAttributeIds = [];
|
||||||
|
foreach ($item['attribute_codes'] as $attributeOrder => $attributeCode) {
|
||||||
|
$attributeId = $this->attributeId($attributeCode, $attributeIds);
|
||||||
|
$itemAttributeIds[$attributeCode] = DB::table('item_attributes')->insertGetId([
|
||||||
|
'catalog_item_id' => $catalogItemId,
|
||||||
|
'attribute_id' => $attributeId,
|
||||||
|
'allow_multi_select' => false,
|
||||||
|
'sort_order' => $attributeOrder + 1,
|
||||||
|
'show_in_selector' => true,
|
||||||
|
'ticket_label' => null,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($item['variants'] as $variant) {
|
||||||
|
$variantId = DB::table('variantes')->insertGetId([
|
||||||
|
'catalog_item_id' => $catalogItemId,
|
||||||
|
'inventory_id' => $this->createInventory((int) $variant['stock']),
|
||||||
|
'descripcion' => $variant['description'],
|
||||||
|
'precio' => $variant['price'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($variant['values'] as $attributeCode => $value) {
|
||||||
|
$itemAttributeId = $itemAttributeIds[$attributeCode] ?? null;
|
||||||
|
|
||||||
|
if ($itemAttributeId === null) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Attribute '{$attributeCode}' is not assigned to '{$item['slug']}'."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('variant_values')->insert([
|
||||||
|
'variant_id' => $variantId,
|
||||||
|
'item_attribute_id' => $itemAttributeId,
|
||||||
|
'value' => $value,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->attachImages($catalogItemId, $variantId, $variant['images']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->createFeaturedGroups();
|
||||||
|
});
|
||||||
|
} catch (Throwable $throwable) {
|
||||||
|
Storage::disk('s3')->delete($this->storedPaths);
|
||||||
|
|
||||||
|
throw $throwable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$manifest = $this->manifest();
|
||||||
|
$catalogItemIds = DB::table('catalog_items')
|
||||||
|
->where('tenant_code', self::TENANT_CODE)
|
||||||
|
->whereIn('slug', array_column($manifest['items'], 'slug'))
|
||||||
|
->pluck('id');
|
||||||
|
$variantInventoryIds = DB::table('variantes')
|
||||||
|
->whereIn('catalog_item_id', $catalogItemIds)
|
||||||
|
->pluck('inventory_id');
|
||||||
|
$itemInventoryIds = DB::table('catalog_items')
|
||||||
|
->whereIn('id', $catalogItemIds)
|
||||||
|
->whereNotNull('inventory_id')
|
||||||
|
->pluck('inventory_id');
|
||||||
|
$attachmentIds = DB::table('catalog_items_attachments')
|
||||||
|
->whereIn('catalog_item_id', $catalogItemIds)
|
||||||
|
->pluck('attachment_id')
|
||||||
|
->unique()
|
||||||
|
->values();
|
||||||
|
$storedPaths = DB::table('attachments')
|
||||||
|
->whereIn('id', $attachmentIds)
|
||||||
|
->where(fn ($query) => $query
|
||||||
|
->where('path', 'like', self::STORAGE_PREFIX.'%')
|
||||||
|
->orWhere('path', 'like', self::LEGACY_STORAGE_PREFIX.'%'))
|
||||||
|
->pluck('path')
|
||||||
|
->all();
|
||||||
|
|
||||||
|
DB::transaction(function () use (
|
||||||
|
$catalogItemIds,
|
||||||
|
$variantInventoryIds,
|
||||||
|
$itemInventoryIds,
|
||||||
|
$attachmentIds,
|
||||||
|
): void {
|
||||||
|
DB::table('featured_groups')
|
||||||
|
->where('tenant_code', self::TENANT_CODE)
|
||||||
|
->whereIn('code', [
|
||||||
|
self::HIGHLIGHTED_GROUP_CODE,
|
||||||
|
...array_column(self::FEATURED_GROUPS, 'code'),
|
||||||
|
])
|
||||||
|
->delete();
|
||||||
|
DB::table('catalog_items')->whereIn('id', $catalogItemIds)->delete();
|
||||||
|
DB::table('inventories')
|
||||||
|
->whereIn('id', $variantInventoryIds->merge($itemInventoryIds)->unique())
|
||||||
|
->delete();
|
||||||
|
DB::table('attachments')->whereIn('id', $attachmentIds)->delete();
|
||||||
|
DB::table('brands')
|
||||||
|
->where('tenant_codigo', self::TENANT_CODE)
|
||||||
|
->where('descripcion', self::BRAND_MARKER)
|
||||||
|
->whereNotExists(fn ($query) => $query
|
||||||
|
->selectRaw('1')
|
||||||
|
->from('catalog_items')
|
||||||
|
->whereColumn('catalog_items.brand_id', 'brands.id'))
|
||||||
|
->delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
Storage::disk('s3')->delete($storedPaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
private function manifest(): array
|
||||||
|
{
|
||||||
|
$path = database_path(self::MANIFEST);
|
||||||
|
|
||||||
|
if (! is_file($path)) {
|
||||||
|
throw new RuntimeException("Mutual SMEP catalog manifest not found: {$path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$manifest = json_decode(
|
||||||
|
(string) file_get_contents($path),
|
||||||
|
true,
|
||||||
|
flags: JSON_THROW_ON_ERROR,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
($manifest['version'] ?? null) !== 1
|
||||||
|
|| ($manifest['tenant_code'] ?? null) !== self::TENANT_CODE
|
||||||
|
|| count($manifest['items'] ?? []) !== 142
|
||||||
|
|| ($manifest['image_count'] ?? null) !== 511
|
||||||
|
) {
|
||||||
|
throw new RuntimeException('Mutual SMEP catalog manifest is invalid.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $manifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, int> $cache */
|
||||||
|
private function brandId(?string $name, array &$cache): ?int
|
||||||
|
{
|
||||||
|
if ($name === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($cache[$name])) {
|
||||||
|
return $cache[$name];
|
||||||
|
}
|
||||||
|
|
||||||
|
$brandId = DB::table('brands')
|
||||||
|
->where('tenant_codigo', self::TENANT_CODE)
|
||||||
|
->where('nombre', $name)
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
if ($brandId === null) {
|
||||||
|
$brandId = DB::table('brands')->insertGetId([
|
||||||
|
'tenant_codigo' => self::TENANT_CODE,
|
||||||
|
'nombre' => $name,
|
||||||
|
'descripcion' => self::BRAND_MARKER,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cache[$name] = (int) $brandId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, int> $cache */
|
||||||
|
private function categoryId(string $parentName, string $name, array &$cache): int
|
||||||
|
{
|
||||||
|
$key = "{$parentName}|{$name}";
|
||||||
|
|
||||||
|
if (isset($cache[$key])) {
|
||||||
|
return $cache[$key];
|
||||||
|
}
|
||||||
|
|
||||||
|
$parentId = DB::table('categorias')
|
||||||
|
->where('tenant_code', self::TENANT_CODE)
|
||||||
|
->where('nombre', $parentName)
|
||||||
|
->whereNull('categoria_id')
|
||||||
|
->value('id');
|
||||||
|
$categoryId = $parentId === null
|
||||||
|
? null
|
||||||
|
: DB::table('categorias')
|
||||||
|
->where('tenant_code', self::TENANT_CODE)
|
||||||
|
->where('categoria_id', $parentId)
|
||||||
|
->where('nombre', $name)
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
if ($categoryId === null) {
|
||||||
|
throw new RuntimeException("Category '{$parentName} > {$name}' not found for Mutual SMEP.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cache[$key] = (int) $categoryId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, int> $cache */
|
||||||
|
private function attributeId(string $code, array &$cache): int
|
||||||
|
{
|
||||||
|
if (isset($cache[$code])) {
|
||||||
|
return $cache[$code];
|
||||||
|
}
|
||||||
|
|
||||||
|
$attributeId = DB::table('attribute')
|
||||||
|
->where('tenant_codigo', self::TENANT_CODE)
|
||||||
|
->where('codigo', $code)
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
if ($attributeId === null) {
|
||||||
|
throw new RuntimeException("Attribute '{$code}' not found for Mutual SMEP.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cache[$code] = (int) $attributeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createInventory(int $stock): int
|
||||||
|
{
|
||||||
|
return (int) DB::table('inventories')->insertGetId([
|
||||||
|
'sold_units' => 0,
|
||||||
|
'refunded_units' => 0,
|
||||||
|
'reserved_stock' => 0,
|
||||||
|
'real_stock' => $stock,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createFeaturedGroups(): void
|
||||||
|
{
|
||||||
|
$this->createHighlightedGroup();
|
||||||
|
|
||||||
|
foreach (self::FEATURED_GROUPS as $order => $group) {
|
||||||
|
$categoryId = DB::table('categorias')
|
||||||
|
->where('tenant_code', self::TENANT_CODE)
|
||||||
|
->whereNull('categoria_id')
|
||||||
|
->where('nombre', $group['category'])
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
if ($categoryId === null) {
|
||||||
|
throw new RuntimeException("Parent category '{$group['category']}' not found for Mutual SMEP.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$featuredGroupId = DB::table('featured_groups')->insertGetId([
|
||||||
|
'tenant_code' => self::TENANT_CODE,
|
||||||
|
'code' => $group['code'],
|
||||||
|
'source_type' => 'manual',
|
||||||
|
'category_id' => $categoryId,
|
||||||
|
'product_layout' => 'column_with_image',
|
||||||
|
'group_layout' => 'carousel',
|
||||||
|
'group_name' => $group['category'],
|
||||||
|
'group_order' => $order + 2,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$catalogItemIds = DB::table('catalog_items')
|
||||||
|
->join('categorias', 'categorias.id', '=', 'catalog_items.category_id')
|
||||||
|
->where('catalog_items.tenant_code', self::TENANT_CODE)
|
||||||
|
->where('categorias.tenant_code', self::TENANT_CODE)
|
||||||
|
->where('categorias.categoria_id', $categoryId)
|
||||||
|
->orderBy('catalog_items.group_order')
|
||||||
|
->orderBy('catalog_items.id')
|
||||||
|
->limit(self::CATEGORY_FEATURED_ITEM_LIMIT)
|
||||||
|
->pluck('catalog_items.id');
|
||||||
|
|
||||||
|
foreach ($catalogItemIds as $itemOrder => $catalogItemId) {
|
||||||
|
DB::table('featured_items')->insert([
|
||||||
|
'featured_group_id' => $featuredGroupId,
|
||||||
|
'catalog_item_id' => $catalogItemId,
|
||||||
|
'order' => $itemOrder + 1,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createHighlightedGroup(): void
|
||||||
|
{
|
||||||
|
$featuredGroupId = DB::table('featured_groups')->insertGetId([
|
||||||
|
'tenant_code' => self::TENANT_CODE,
|
||||||
|
'code' => self::HIGHLIGHTED_GROUP_CODE,
|
||||||
|
'source_type' => 'manual',
|
||||||
|
'category_id' => null,
|
||||||
|
'product_layout' => 'column_with_image',
|
||||||
|
'group_layout' => 'carousel',
|
||||||
|
'group_name' => 'Productos destacados',
|
||||||
|
'group_order' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$catalogItemIds = DB::table('catalog_items')
|
||||||
|
->where('tenant_code', self::TENANT_CODE)
|
||||||
|
->whereIn('slug', self::HIGHLIGHTED_PRODUCT_SLUGS)
|
||||||
|
->pluck('id', 'slug');
|
||||||
|
|
||||||
|
foreach (self::HIGHLIGHTED_PRODUCT_SLUGS as $order => $slug) {
|
||||||
|
$catalogItemId = $catalogItemIds->get($slug);
|
||||||
|
|
||||||
|
if ($catalogItemId === null) {
|
||||||
|
throw new RuntimeException("Highlighted product '{$slug}' not found for Mutual SMEP.");
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('featured_items')->insert([
|
||||||
|
'featured_group_id' => $featuredGroupId,
|
||||||
|
'catalog_item_id' => $catalogItemId,
|
||||||
|
'order' => $order + 1,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param list<string> $images */
|
||||||
|
private function attachImages(int $catalogItemId, ?int $variantId, array $images): void
|
||||||
|
{
|
||||||
|
foreach ($images as $order => $relativePath) {
|
||||||
|
$sourcePath = public_path($relativePath);
|
||||||
|
|
||||||
|
if (! is_file($sourcePath)) {
|
||||||
|
throw new RuntimeException("Catalog image not found: {$sourcePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$contents = file_get_contents($sourcePath);
|
||||||
|
if ($contents === false) {
|
||||||
|
throw new RuntimeException("Catalog image could not be read: {$sourcePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$catalogRelativePath = Str::after(
|
||||||
|
str_replace('\\', '/', $relativePath),
|
||||||
|
'images/tennants/mutual_smep/catalog/',
|
||||||
|
);
|
||||||
|
$storedPath = self::STORAGE_PREFIX.$catalogRelativePath;
|
||||||
|
|
||||||
|
if (! Storage::disk('s3')->put($storedPath, $contents)) {
|
||||||
|
throw new RuntimeException("Catalog image could not be stored: {$storedPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->storedPaths[] = $storedPath;
|
||||||
|
$attachmentId = DB::table('attachments')->insertGetId([
|
||||||
|
'key' => (string) Str::uuid(),
|
||||||
|
'path' => $storedPath,
|
||||||
|
'filename' => basename($relativePath),
|
||||||
|
'type' => 'image',
|
||||||
|
'mime_type' => 'image/webp',
|
||||||
|
'extension' => 'webp',
|
||||||
|
'size' => strlen($contents),
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('catalog_items_attachments')->insert([
|
||||||
|
'catalog_item_id' => $catalogItemId,
|
||||||
|
'variant_id' => $variantId,
|
||||||
|
'attachment_id' => $attachmentId,
|
||||||
|
'orden' => $order,
|
||||||
|
'is_enabled' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
private const TENANT_CODE = 'mutual_smep';
|
||||||
|
|
||||||
|
private const EXTRA_CODE = 'carousel';
|
||||||
|
|
||||||
|
private const IMAGE_DIRECTORY = 'images/tennants/mutual_smep/carousel';
|
||||||
|
|
||||||
|
private const STORAGE_PREFIX = 'tenants/mutual_smep/extras/carousel-v1/';
|
||||||
|
|
||||||
|
/** @var list<string> */
|
||||||
|
private const IMAGES = [
|
||||||
|
'01-tecnologia.webp',
|
||||||
|
'02-hogar.webp',
|
||||||
|
'03-vida-activa.webp',
|
||||||
|
'04-descanso.webp',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @var list<string> */
|
||||||
|
private array $storedPaths = [];
|
||||||
|
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$tenant = DB::table('tenants')
|
||||||
|
->where('codigo', self::TENANT_CODE)
|
||||||
|
->first(['codigo', 'storefront_website_type_code']);
|
||||||
|
|
||||||
|
if ($tenant === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$extraId = DB::table('storefront_website_type_extras')
|
||||||
|
->where('storefront_website_type_code', $tenant->storefront_website_type_code)
|
||||||
|
->where('codigo', self::EXTRA_CODE)
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
if ($extraId === null || DB::table('websites_extras')->where([
|
||||||
|
'website_code' => self::TENANT_CODE,
|
||||||
|
'website_type_extra_id' => $extraId,
|
||||||
|
])->exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::transaction(function () use ($extraId): void {
|
||||||
|
$attachmentIds = [];
|
||||||
|
|
||||||
|
foreach (self::IMAGES as $filename) {
|
||||||
|
$attachmentIds[] = $this->storeImage($filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('websites_extras')->insert([
|
||||||
|
'website_code' => self::TENANT_CODE,
|
||||||
|
'website_type_extra_id' => $extraId,
|
||||||
|
'config' => json_encode($attachmentIds, JSON_THROW_ON_ERROR),
|
||||||
|
'is_enabled' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
} catch (Throwable $throwable) {
|
||||||
|
Storage::disk('s3')->delete($this->storedPaths);
|
||||||
|
|
||||||
|
throw $throwable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$attachments = DB::table('attachments')
|
||||||
|
->where('path', 'like', self::STORAGE_PREFIX.'%')
|
||||||
|
->get(['id', 'path']);
|
||||||
|
|
||||||
|
if ($attachments->isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attachmentIds = $attachments->pluck('id')->map(fn ($id): int => (int) $id)->all();
|
||||||
|
$extraId = DB::table('storefront_website_type_extras')
|
||||||
|
->where('storefront_website_type_code', 'shopit')
|
||||||
|
->where('codigo', self::EXTRA_CODE)
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
DB::transaction(function () use ($attachmentIds, $extraId): void {
|
||||||
|
if ($extraId !== null) {
|
||||||
|
DB::table('websites_extras')
|
||||||
|
->where('website_code', self::TENANT_CODE)
|
||||||
|
->where('website_type_extra_id', $extraId)
|
||||||
|
->where('config', json_encode($attachmentIds, JSON_THROW_ON_ERROR))
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('attachments')->whereIn('id', $attachmentIds)->delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
Storage::disk('s3')->delete($attachments->pluck('path')->all());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function storeImage(string $filename): int
|
||||||
|
{
|
||||||
|
$sourcePath = public_path(self::IMAGE_DIRECTORY."/{$filename}");
|
||||||
|
|
||||||
|
if (! is_file($sourcePath)) {
|
||||||
|
throw new RuntimeException("Carousel image not found: {$sourcePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$contents = file_get_contents($sourcePath);
|
||||||
|
|
||||||
|
if ($contents === false) {
|
||||||
|
throw new RuntimeException("Carousel image could not be read: {$sourcePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$storedPath = self::STORAGE_PREFIX.$filename;
|
||||||
|
|
||||||
|
if (! Storage::disk('s3')->put($storedPath, $contents)) {
|
||||||
|
throw new RuntimeException("Carousel image could not be stored: {$storedPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->storedPaths[] = $storedPath;
|
||||||
|
|
||||||
|
return (int) DB::table('attachments')->insertGetId([
|
||||||
|
'key' => (string) Str::uuid(),
|
||||||
|
'path' => $storedPath,
|
||||||
|
'filename' => $filename,
|
||||||
|
'type' => 'image',
|
||||||
|
'mime_type' => 'image/webp',
|
||||||
|
'extension' => 'webp',
|
||||||
|
'size' => strlen($contents),
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
private const TENANT_CODE = 'mutual_smep';
|
||||||
|
|
||||||
|
private const ITEM_LIMIT = 6;
|
||||||
|
|
||||||
|
/** @var array<string, string> */
|
||||||
|
private const GROUPS = [
|
||||||
|
'smep-dormitorio-y-blanco' => 'Dormitorio y Blanco',
|
||||||
|
'smep-electrodomesticos' => 'Electrodomésticos',
|
||||||
|
'smep-climatizacion' => 'Climatización',
|
||||||
|
'smep-tecnologia' => 'Tecnología',
|
||||||
|
'smep-bicicletas-y-aire-libre' => 'Bicicletas y Aire Libre',
|
||||||
|
'smep-bazar' => 'Bazar',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
foreach (array_keys(self::GROUPS) as $code) {
|
||||||
|
$groupId = DB::table('featured_groups')
|
||||||
|
->where('tenant_code', self::TENANT_CODE)
|
||||||
|
->where('code', $code)
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
if ($groupId === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$keptItemIds = DB::table('featured_items')
|
||||||
|
->where('featured_group_id', $groupId)
|
||||||
|
->orderBy('order')
|
||||||
|
->orderBy('id')
|
||||||
|
->limit(self::ITEM_LIMIT)
|
||||||
|
->pluck('id');
|
||||||
|
|
||||||
|
DB::table('featured_items')
|
||||||
|
->where('featured_group_id', $groupId)
|
||||||
|
->when(
|
||||||
|
$keptItemIds->isNotEmpty(),
|
||||||
|
fn ($query) => $query->whereNotIn('id', $keptItemIds),
|
||||||
|
)
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
foreach (self::GROUPS as $code => $parentCategoryName) {
|
||||||
|
$groupId = DB::table('featured_groups')
|
||||||
|
->where('tenant_code', self::TENANT_CODE)
|
||||||
|
->where('code', $code)
|
||||||
|
->value('id');
|
||||||
|
$parentCategoryId = DB::table('categorias')
|
||||||
|
->where('tenant_code', self::TENANT_CODE)
|
||||||
|
->whereNull('categoria_id')
|
||||||
|
->where('nombre', $parentCategoryName)
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
if ($groupId === null || $parentCategoryId === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$catalogItemIds = DB::table('catalog_items')
|
||||||
|
->join('categorias', 'categorias.id', '=', 'catalog_items.category_id')
|
||||||
|
->where('catalog_items.tenant_code', self::TENANT_CODE)
|
||||||
|
->where('categorias.tenant_code', self::TENANT_CODE)
|
||||||
|
->where('categorias.categoria_id', $parentCategoryId)
|
||||||
|
->orderBy('catalog_items.group_order')
|
||||||
|
->orderBy('catalog_items.id')
|
||||||
|
->pluck('catalog_items.id');
|
||||||
|
|
||||||
|
DB::table('featured_items')->where('featured_group_id', $groupId)->delete();
|
||||||
|
|
||||||
|
foreach ($catalogItemIds as $order => $catalogItemId) {
|
||||||
|
DB::table('featured_items')->insert([
|
||||||
|
'featured_group_id' => $groupId,
|
||||||
|
'catalog_item_id' => $catalogItemId,
|
||||||
|
'order' => $order + 1,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
private const TENANT_CODE = 'mutual_smep';
|
||||||
|
|
||||||
|
private const LEGACY_PREFIX = 'tenants/mutual_smep/catalog-v1/';
|
||||||
|
|
||||||
|
private const OPTIMIZED_PREFIX = 'tenants/mutual_smep/catalog-v2/';
|
||||||
|
|
||||||
|
private const LOCAL_PREFIX = 'images/tennants/mutual_smep/catalog/';
|
||||||
|
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$attachments = $this->attachments(self::LEGACY_PREFIX);
|
||||||
|
$uploadedPaths = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
foreach ($attachments as $attachment) {
|
||||||
|
$relativePath = Str::after($attachment->path, self::LEGACY_PREFIX);
|
||||||
|
$localPath = public_path(self::LOCAL_PREFIX.$relativePath);
|
||||||
|
|
||||||
|
if (! is_file($localPath)) {
|
||||||
|
throw new RuntimeException("Optimized catalog image not found: {$localPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$contents = file_get_contents($localPath);
|
||||||
|
if ($contents === false) {
|
||||||
|
throw new RuntimeException("Optimized catalog image could not be read: {$localPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$optimizedPath = self::OPTIMIZED_PREFIX.$relativePath;
|
||||||
|
if (! Storage::disk('s3')->put($optimizedPath, $contents)) {
|
||||||
|
throw new RuntimeException("Optimized catalog image could not be stored: {$optimizedPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$uploadedPaths[] = $optimizedPath;
|
||||||
|
$attachment->optimized_path = $optimizedPath;
|
||||||
|
$attachment->optimized_size = strlen($contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::transaction(function () use ($attachments): void {
|
||||||
|
foreach ($attachments as $attachment) {
|
||||||
|
DB::table('attachments')->where('id', $attachment->id)->update([
|
||||||
|
'path' => $attachment->optimized_path,
|
||||||
|
'size' => $attachment->optimized_size,
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Throwable $throwable) {
|
||||||
|
Storage::disk('s3')->delete($uploadedPaths);
|
||||||
|
|
||||||
|
throw $throwable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$attachments = $this->attachments(self::OPTIMIZED_PREFIX)
|
||||||
|
->filter(function (object $attachment): bool {
|
||||||
|
$relativePath = Str::after($attachment->path, self::OPTIMIZED_PREFIX);
|
||||||
|
$legacyPath = self::LEGACY_PREFIX.$relativePath;
|
||||||
|
|
||||||
|
if (! Storage::disk('s3')->exists($legacyPath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attachment->legacy_path = $legacyPath;
|
||||||
|
$attachment->legacy_size = Storage::disk('s3')->size($legacyPath);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
->values();
|
||||||
|
|
||||||
|
DB::transaction(function () use ($attachments): void {
|
||||||
|
foreach ($attachments as $attachment) {
|
||||||
|
DB::table('attachments')->where('id', $attachment->id)->update([
|
||||||
|
'path' => $attachment->legacy_path,
|
||||||
|
'size' => $attachment->legacy_size,
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Storage::disk('s3')->delete($attachments->pluck('path')->all());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function attachments(string $prefix): Collection
|
||||||
|
{
|
||||||
|
return DB::table('attachments')
|
||||||
|
->join(
|
||||||
|
'catalog_items_attachments',
|
||||||
|
'catalog_items_attachments.attachment_id',
|
||||||
|
'=',
|
||||||
|
'attachments.id',
|
||||||
|
)
|
||||||
|
->join('catalog_items', 'catalog_items.id', '=', 'catalog_items_attachments.catalog_item_id')
|
||||||
|
->where('catalog_items.tenant_code', self::TENANT_CODE)
|
||||||
|
->where('attachments.path', 'like', $prefix.'%')
|
||||||
|
->select('attachments.id', 'attachments.path', 'attachments.size')
|
||||||
|
->distinct()
|
||||||
|
->orderBy('attachments.id')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
private const MENU_CODE = 'adminapp.tickets';
|
||||||
|
|
||||||
|
private const TENANT_CODE = 'desfile_pura_tendencia';
|
||||||
|
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
! DB::table('menues')->where('code', self::MENU_CODE)->exists()
|
||||||
|
|| ! DB::table('tenants')->where('codigo', self::TENANT_CODE)->exists()
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = now();
|
||||||
|
|
||||||
|
DB::table('tenants_menues')->updateOrInsert(
|
||||||
|
[
|
||||||
|
'tenant_code' => self::TENANT_CODE,
|
||||||
|
'menu_code' => self::MENU_CODE,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'static_content' => null,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
DB::table('tenants_menues')
|
||||||
|
->where('tenant_code', self::TENANT_CODE)
|
||||||
|
->where('menu_code', self::MENU_CODE)
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
};
|
||||||
311
database/scripts/build_mutual_smep_catalog_assets.php
Normal file
311
database/scripts/build_mutual_smep_catalog_assets.php
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
const CATALOG_IMAGE_MAX_DIMENSION = 960;
|
||||||
|
const CATALOG_IMAGE_WEBP_QUALITY = 76;
|
||||||
|
|
||||||
|
$sourceRoot = 'C:\\Users\\ncoronel\\Documents\\catalogo_smep_limpio';
|
||||||
|
$projectRoot = dirname(__DIR__, 2);
|
||||||
|
$assetRoot = $projectRoot.'/public/images/tennants/mutual_smep/catalog';
|
||||||
|
$manifestPath = $projectRoot.'/database/data/mutual_smep_catalog_v1.json';
|
||||||
|
$defaultStock = 10;
|
||||||
|
|
||||||
|
if (! is_dir($sourceRoot)) {
|
||||||
|
throw new RuntimeException("Source catalog not found: {$sourceRoot}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{0: string, 1: string} */
|
||||||
|
function catalogDestination(string $source, string $name): array
|
||||||
|
{
|
||||||
|
return match ($source) {
|
||||||
|
'acolchados-frazadas-y-edredon' => preg_match('/^(Frazada|Manta)/iu', $name)
|
||||||
|
? ['Dormitorio y Blanco', 'Frazadas y Mantas']
|
||||||
|
: ['Dormitorio y Blanco', 'Acolchados y Edredones'],
|
||||||
|
'acolchados-y-sabanas-infantil' => ['Dormitorio y Blanco', 'Infantil y Cuna'],
|
||||||
|
'base-sommier-1-pl-0-80x1-90', 'base-sommier-2-pl-1-40x1-90' => ['Dormitorio y Blanco', 'Bases y Sommiers'],
|
||||||
|
'colchon-1-plaza-0-80x1-90', 'colchon-2-plazas-1-40x1-90' => str_starts_with(mb_strtolower($name), 'soporte')
|
||||||
|
? ['Tecnología', 'Soportes para TV']
|
||||||
|
: ['Dormitorio y Blanco', 'Colchones'],
|
||||||
|
'aires-acondicionados' => ['Climatización', 'Aires Acondicionados'],
|
||||||
|
'calefaccion-con-salida-al-ext' => ['Climatización', 'Calefactores a Gas con Salida'],
|
||||||
|
'calefactor-sin-salida-al-ext' => ['Climatización', 'Calefactores a Gas sin Salida'],
|
||||||
|
'calefaccion-electrica' => ['Climatización', 'Calefacción Eléctrica'],
|
||||||
|
'calefones-y-termotanques' => ['Electrodomésticos', 'Calefones y Termotanques'],
|
||||||
|
'heladeras' => ['Electrodomésticos', 'Heladeras'],
|
||||||
|
'lavarropas-automaticos' => ['Electrodomésticos', 'Lavarropas y Secarropas'],
|
||||||
|
'cocinas' => ['Electrodomésticos', 'Cocinas'],
|
||||||
|
'purificador-de-aire' => ['Electrodomésticos', 'Purificadores y Extractores de Cocina'],
|
||||||
|
'microondas-y-hornos-electricos' => str_starts_with(mb_strtolower($name), 'horno')
|
||||||
|
? ['Electrodomésticos', 'Hornos Eléctricos']
|
||||||
|
: ['Electrodomésticos', 'Microondas'],
|
||||||
|
'electrodomesticos-pequenos' => smallApplianceCategory($name),
|
||||||
|
'celulares' => ['Tecnología', 'Celulares'],
|
||||||
|
'notebooks-y-tablets' => ['Tecnología', 'Notebooks'],
|
||||||
|
'smart-tv' => str_starts_with(mb_strtolower($name), 'soporte')
|
||||||
|
? ['Tecnología', 'Soportes para TV']
|
||||||
|
: ['Tecnología', 'Smart TV'],
|
||||||
|
'tecnologia' => preg_match('/^(Mouse|Teclado)/iu', $name)
|
||||||
|
? ['Tecnología', 'Accesorios de Computación']
|
||||||
|
: ['Tecnología', 'Audio'],
|
||||||
|
'bicicletas' => preg_match('/\bR16\b/iu', $name)
|
||||||
|
? ['Bicicletas y Aire Libre', 'Bicicletas Infantiles']
|
||||||
|
: ['Bicicletas y Aire Libre', 'Bicicletas para Adultos'],
|
||||||
|
'piletas' => ['Bicicletas y Aire Libre', 'Piletas'],
|
||||||
|
'bazar' => ['Bazar', 'Termos'],
|
||||||
|
default => throw new RuntimeException("Unmapped source category: {$source}"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{0: string, 1: string} */
|
||||||
|
function smallApplianceCategory(string $name): array
|
||||||
|
{
|
||||||
|
return match (true) {
|
||||||
|
preg_match('/^(Cafetera|Pava eléctrica|Tostadora)/iu', $name) === 1 => ['Electrodomésticos', 'Café y Desayuno'],
|
||||||
|
preg_match('/^(Freidora|Waflera|Pochoclera)/iu', $name) === 1 => ['Electrodomésticos', 'Freidoras y Cocción'],
|
||||||
|
preg_match('/^(Secador|Cortacabello|Cortabarba)/iu', $name) === 1 => ['Electrodomésticos', 'Cuidado Personal'],
|
||||||
|
preg_match('/^(Lustraspiradora|Plancha)/iu', $name) === 1 => ['Electrodomésticos', 'Limpieza y Planchado'],
|
||||||
|
default => ['Electrodomésticos', 'Preparación de Alimentos'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function productBrand(string $name): ?string
|
||||||
|
{
|
||||||
|
if (preg_match('/\bJC\b/u', $name)) {
|
||||||
|
return 'Jean Cartier';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mb_stripos($name, 'Liliana') !== false) {
|
||||||
|
return 'Liliana';
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ([
|
||||||
|
'Rosario Central', 'Jean Cartier', 'King Koil', 'Fire Bird', 'Xtrike Me',
|
||||||
|
'Whitenblack', 'Electrolux', 'Kavanagh', 'Pelopincho', 'Suavegom',
|
||||||
|
'Motorola', 'Samsung', 'Xiaomi', 'Moulinex', 'Peabody', 'Philips',
|
||||||
|
'Florencia', 'Kohinoor', 'Inducol', 'Longvie', 'Stanley', 'Noblex',
|
||||||
|
'Moonki', 'Lenovo', 'Piero', 'Eskabe', 'Oster', 'Drean', 'Gafa', 'Atma',
|
||||||
|
'Spar', 'Stark', 'Venzo', 'Havit', 'Acer', 'ASUS', 'MSI', 'Philco',
|
||||||
|
'Gama', 'GIGO', 'Ross', 'Nakan', 'TCL', 'BGH', 'JBL', 'TST', 'GBS', 'BLU',
|
||||||
|
] as $brand) {
|
||||||
|
if (mb_stripos($name, $brand) !== false) {
|
||||||
|
return $brand;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{key: string, name: string, values: array<string, string>}|null */
|
||||||
|
function variantGroup(string $name): ?array
|
||||||
|
{
|
||||||
|
$measure = bedMeasureValue($name);
|
||||||
|
$groups = [
|
||||||
|
'/(?:Kavanagh.*[Ss][ií]mil plumón reversible|[Ss][ií]mil plumón reversible.*Kavanagh)/iu' => ['acolchado-kavanagh-simil-plumon-reversible', 'Acolchado Kavanagh Símil Plumón Reversible'],
|
||||||
|
'/Edredón.*Lisboa/iu' => ['edredon-lisboa-jean-cartier', 'Edredón Lisboa Jean Cartier'],
|
||||||
|
'/Sense Dúo Bitono con corderito/iu' => ['acolchado-sense-duo-bitono-corderito', 'Acolchado Sense Dúo Bitono con Corderito'],
|
||||||
|
'/Frazada polar.*Kavanagh Premium Soft/iu' => ['frazada-kavanagh-premium-soft', 'Frazada Kavanagh Premium Soft'],
|
||||||
|
'/Kit Edredón.*Alaska.*corderito/iu' => ['kit-edredon-alaska-corderito', 'Kit Edredón Alaska con Corderito'],
|
||||||
|
'/Colch[oó]n Inducol Constanza/iu' => ['colchon-inducol-constanza', 'Colchón Inducol Constanza'],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($groups as $pattern => [$key, $itemName]) {
|
||||||
|
if ($measure !== null && preg_match($pattern, $name)) {
|
||||||
|
return [
|
||||||
|
'key' => $key,
|
||||||
|
'name' => $itemName,
|
||||||
|
'values' => ['medida_cama' => $measure],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/Motorola G15\s+4\/(256|512)\s*GB/iu', $name, $match)) {
|
||||||
|
return [
|
||||||
|
'key' => 'motorola-g15',
|
||||||
|
'name' => 'Motorola G15',
|
||||||
|
'values' => [
|
||||||
|
'memoria_ram' => '4 GB',
|
||||||
|
'almacenamiento' => "{$match[1]} GB",
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bedMeasureValue(string $name): ?string
|
||||||
|
{
|
||||||
|
return match (true) {
|
||||||
|
preg_match('/\bcuna\b/iu', $name) === 1 => 'Cuna',
|
||||||
|
preg_match('/\bKing\b/iu', $name) === 1 => 'King',
|
||||||
|
preg_match('/\bQueen\b/iu', $name) === 1 => 'Queen',
|
||||||
|
preg_match('/2\s*1\/2\s*pl/iu', $name) === 1 => '2 1/2 Plazas',
|
||||||
|
preg_match('/1\s*1\/2\s*pl/iu', $name) === 1 => '1 1/2 Plazas',
|
||||||
|
preg_match('/1[,.]40\s*x?\s*1[,.]90|140x190|140x24/iu', $name) === 1 => '2 Plazas',
|
||||||
|
preg_match('/0[,.](80|90)\s*x?\s*1[,.]90|080x|090x|0,8x/iu', $name) === 1 => '1 Plaza',
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function compressCatalogImage(string $source, string $destination): void
|
||||||
|
{
|
||||||
|
$sourceImage = @imagecreatefromjpeg($source);
|
||||||
|
if ($sourceImage === false) {
|
||||||
|
throw new RuntimeException("Unable to read JPEG image: {$source}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$width = imagesx($sourceImage);
|
||||||
|
$height = imagesy($sourceImage);
|
||||||
|
$scale = min(1, CATALOG_IMAGE_MAX_DIMENSION / max($width, $height));
|
||||||
|
$targetWidth = max(1, (int) round($width * $scale));
|
||||||
|
$targetHeight = max(1, (int) round($height * $scale));
|
||||||
|
$targetImage = imagecreatetruecolor($targetWidth, $targetHeight);
|
||||||
|
|
||||||
|
if ($targetImage === false) {
|
||||||
|
throw new RuntimeException("Unable to allocate image: {$source}");
|
||||||
|
}
|
||||||
|
|
||||||
|
imagefill($targetImage, 0, 0, imagecolorallocate($targetImage, 255, 255, 255));
|
||||||
|
imagecopyresampled(
|
||||||
|
$targetImage,
|
||||||
|
$sourceImage,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
$targetWidth,
|
||||||
|
$targetHeight,
|
||||||
|
$width,
|
||||||
|
$height,
|
||||||
|
);
|
||||||
|
|
||||||
|
$destinationDirectory = dirname($destination);
|
||||||
|
if (! is_dir($destinationDirectory) && ! mkdir($destinationDirectory, 0777, true) && ! is_dir($destinationDirectory)) {
|
||||||
|
throw new RuntimeException("Unable to create directory: {$destinationDirectory}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! imagewebp($targetImage, $destination, CATALOG_IMAGE_WEBP_QUALITY)) {
|
||||||
|
throw new RuntimeException("Unable to write WebP image: {$destination}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$rawProducts = [];
|
||||||
|
$jsonFiles = glob($sourceRoot.'\\*\\*.json') ?: [];
|
||||||
|
sort($jsonFiles);
|
||||||
|
|
||||||
|
foreach ($jsonFiles as $jsonFile) {
|
||||||
|
$data = json_decode((string) file_get_contents($jsonFile), true, flags: JSON_THROW_ON_ERROR);
|
||||||
|
$sourceCategory = (string) $data['categoria']['slug'];
|
||||||
|
|
||||||
|
foreach ($data['productos'] as $product) {
|
||||||
|
[$parentCategory, $category] = catalogDestination($sourceCategory, $product['nombre']);
|
||||||
|
$images = [];
|
||||||
|
|
||||||
|
foreach ($product['imagenes'] as $relativeImage) {
|
||||||
|
$sourceImage = dirname($jsonFile).'/'.str_replace('/', DIRECTORY_SEPARATOR, $relativeImage);
|
||||||
|
$basename = pathinfo($relativeImage, PATHINFO_FILENAME).'.webp';
|
||||||
|
$publicRelativePath = "images/tennants/mutual_smep/catalog/{$sourceCategory}/{$basename}";
|
||||||
|
$destination = $projectRoot.'/public/'.$publicRelativePath;
|
||||||
|
compressCatalogImage($sourceImage, $destination);
|
||||||
|
$images[] = $publicRelativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rawProducts[] = [
|
||||||
|
'source_slug' => $product['slug'],
|
||||||
|
'name' => $product['nombre'],
|
||||||
|
'description' => $product['descripcion'] ?? null,
|
||||||
|
'price' => (float) $product['precio']['importe'],
|
||||||
|
'brand' => productBrand($product['nombre']),
|
||||||
|
'parent_category' => $parentCategory,
|
||||||
|
'category' => $category,
|
||||||
|
'images' => $images,
|
||||||
|
'variant_group' => variantGroup($product['nombre']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = [];
|
||||||
|
|
||||||
|
foreach ($rawProducts as $product) {
|
||||||
|
$group = $product['variant_group'];
|
||||||
|
|
||||||
|
if ($group === null) {
|
||||||
|
$items[$product['source_slug']] = [
|
||||||
|
'slug' => $product['source_slug'],
|
||||||
|
'name' => $product['name'],
|
||||||
|
'description' => $product['description'],
|
||||||
|
'price' => $product['price'],
|
||||||
|
'stock' => $defaultStock,
|
||||||
|
'brand' => $product['brand'],
|
||||||
|
'parent_category' => $product['parent_category'],
|
||||||
|
'category' => $product['category'],
|
||||||
|
'images' => $product['images'],
|
||||||
|
'attribute_codes' => [],
|
||||||
|
'variants' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = $group['key'];
|
||||||
|
if (! isset($items[$key])) {
|
||||||
|
$items[$key] = [
|
||||||
|
'slug' => $key,
|
||||||
|
'name' => $group['name'],
|
||||||
|
'description' => $product['description'],
|
||||||
|
'price' => $product['price'],
|
||||||
|
'stock' => null,
|
||||||
|
'brand' => $product['brand'],
|
||||||
|
'parent_category' => $product['parent_category'],
|
||||||
|
'category' => $product['category'],
|
||||||
|
'images' => [],
|
||||||
|
'attribute_codes' => [],
|
||||||
|
'variants' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$items[$key]['price'] = min($items[$key]['price'], $product['price']);
|
||||||
|
$items[$key]['description'] ??= $product['description'];
|
||||||
|
$items[$key]['brand'] ??= $product['brand'];
|
||||||
|
$items[$key]['attribute_codes'] = array_values(array_unique([
|
||||||
|
...$items[$key]['attribute_codes'],
|
||||||
|
...array_keys($group['values']),
|
||||||
|
]));
|
||||||
|
$items[$key]['variants'][] = [
|
||||||
|
'source_slug' => $product['source_slug'],
|
||||||
|
'description' => $product['description'],
|
||||||
|
'price' => $product['price'],
|
||||||
|
'stock' => $defaultStock,
|
||||||
|
'values' => $group['values'],
|
||||||
|
'images' => $product['images'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
ksort($items);
|
||||||
|
|
||||||
|
$manifest = [
|
||||||
|
'version' => 1,
|
||||||
|
'tenant_code' => 'mutual_smep',
|
||||||
|
'default_stock' => $defaultStock,
|
||||||
|
'source_product_count' => count($rawProducts),
|
||||||
|
'catalog_item_count' => count($items),
|
||||||
|
'image_count' => array_sum(array_map(fn (array $product): int => count($product['images']), $rawProducts)),
|
||||||
|
'items' => array_values($items),
|
||||||
|
];
|
||||||
|
|
||||||
|
$manifestDirectory = dirname($manifestPath);
|
||||||
|
if (! is_dir($manifestDirectory) && ! mkdir($manifestDirectory, 0777, true) && ! is_dir($manifestDirectory)) {
|
||||||
|
throw new RuntimeException("Unable to create directory: {$manifestDirectory}");
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents(
|
||||||
|
$manifestPath,
|
||||||
|
json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR).PHP_EOL,
|
||||||
|
);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'manifest' => $manifestPath,
|
||||||
|
'source_products' => count($rawProducts),
|
||||||
|
'catalog_items' => count($items),
|
||||||
|
'images' => $manifest['image_count'],
|
||||||
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), PHP_EOL;
|
||||||
@@ -3,10 +3,10 @@
|
|||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Domains\Commerce\Catalog\Models\Attribute;
|
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\Tenant;
|
||||||
use App\Domains\Ticketing\Ticket\Enums\ValidityTimeType;
|
use App\Domains\Ticketing\Ticket\Enums\ValidityTimeType;
|
||||||
use App\Domains\Ticketing\Ticket\Models\ValidityTime;
|
use App\Domains\Ticketing\Ticket\Models\ValidityTime;
|
||||||
|
use App\Shared\Enums\FieldType;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class AttributeSeeder extends Seeder
|
class AttributeSeeder extends Seeder
|
||||||
@@ -25,6 +25,10 @@ class AttributeSeeder extends Seeder
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($tenant->codigo === 'mutual_smep') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$this->seedAttribute($tenant, [
|
$this->seedAttribute($tenant, [
|
||||||
'codigo' => 'color',
|
'codigo' => 'color',
|
||||||
'nombre' => 'Color',
|
'nombre' => 'Color',
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ class DatabaseSeeder extends Seeder
|
|||||||
TenantSeeder::class,
|
TenantSeeder::class,
|
||||||
DesfilePuraTendenciaSeeder::class,
|
DesfilePuraTendenciaSeeder::class,
|
||||||
AttributeSeeder::class,
|
AttributeSeeder::class,
|
||||||
|
MutualSmepAttributeSeeder::class,
|
||||||
CategorySeeder::class,
|
CategorySeeder::class,
|
||||||
|
MutualSmepCategorySeeder::class,
|
||||||
BrandSeeder::class,
|
BrandSeeder::class,
|
||||||
ProductCatalogFromImagesSeeder::class,
|
ProductCatalogFromImagesSeeder::class,
|
||||||
FiestaFutbolInfantilProductSeeder::class,
|
FiestaFutbolInfantilProductSeeder::class,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use App\Domains\Commerce\Catalog\Models\FeaturedGroup;
|
|||||||
use App\Domains\Commerce\Catalog\Services\CatalogService;
|
use App\Domains\Commerce\Catalog\Services\CatalogService;
|
||||||
use App\Domains\Core\Client\Models\Client;
|
use App\Domains\Core\Client\Models\Client;
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
use App\Domains\Core\Tenant\Services\TenantService;
|
use App\Domains\Core\Tenant\Services\TenantProvisioningService;
|
||||||
use App\Domains\Ticketing\Desfile\Services\InvitationPurchaseProvisioner;
|
use App\Domains\Ticketing\Desfile\Services\InvitationPurchaseProvisioner;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
@@ -27,7 +27,7 @@ class DesfilePuraTendenciaSeeder extends Seeder
|
|||||||
private const TENANT_CODE = 'desfile_pura_tendencia';
|
private const TENANT_CODE = 'desfile_pura_tendencia';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly TenantService $tenantService,
|
private readonly TenantProvisioningService $tenantService,
|
||||||
private readonly CatalogService $catalogService,
|
private readonly CatalogService $catalogService,
|
||||||
private readonly InvitationPurchaseProvisioner $invitationPurchaseProvisioner,
|
private readonly InvitationPurchaseProvisioner $invitationPurchaseProvisioner,
|
||||||
) {}
|
) {}
|
||||||
@@ -115,6 +115,7 @@ class DesfilePuraTendenciaSeeder extends Seeder
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$event = $tenant->events()->create([
|
$event = $tenant->events()->create([
|
||||||
|
'slug' => 'desfile-pura-tendencia',
|
||||||
'title' => 'Desfile Pura Tendencia',
|
'title' => 'Desfile Pura Tendencia',
|
||||||
'location' => 'Salón Centro Recreativo Luz y Fuerza',
|
'location' => 'Salón Centro Recreativo Luz y Fuerza',
|
||||||
'date_text' => '16 de Octubre 2026',
|
'date_text' => '16 de Octubre 2026',
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Domains\Commerce\Catalog\Enums\InventoryPolicy;
|
use App\Domains\Commerce\Catalog\Enums\InventoryPolicy;
|
||||||
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
|
||||||
use App\Domains\Commerce\Catalog\Models\Attribute;
|
use App\Domains\Commerce\Catalog\Models\Attribute;
|
||||||
|
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
||||||
use App\Domains\Commerce\Catalog\Services\CatalogService;
|
use App\Domains\Commerce\Catalog\Services\CatalogService;
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
use App\Domains\Ticketing\Event\Models\Event;
|
use App\Domains\Ticketing\Event\Models\Event;
|
||||||
@@ -13,13 +13,14 @@ use App\Shared\Enums\FieldType;
|
|||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
||||||
class FiestaTradicionArrufoSeeder extends Seeder
|
class FiestaTradicionArrufoSeeder extends Seeder
|
||||||
{
|
{
|
||||||
private const TITLE = '26.º Fiesta de la Tradición y 4.º Encuentro de Agrupaciones Gauchas';
|
private const TITLE = '26.º Fiesta de la Tradición y 4.º Encuentro de Agrupaciones Gauchas';
|
||||||
|
|
||||||
private const IMAGE = __DIR__.'/assets/fiesta-tradicion-arrufo-2026.png';
|
private const IMAGE = 'images/tennants/onticket/events/fiesta-tradicion-arrufo-2026.avif';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly CatalogService $catalog,
|
private readonly CatalogService $catalog,
|
||||||
@@ -51,7 +52,8 @@ class FiestaTradicionArrufoSeeder extends Seeder
|
|||||||
'location' => 'Predio de Doma del Club Unión Deportiva Arrufó',
|
'location' => 'Predio de Doma del Club Unión Deportiva Arrufó',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($event->attachment_id === null) {
|
$eventImage = $event->attachment;
|
||||||
|
if ($eventImage === null || ! Storage::disk('s3')->exists($eventImage->path)) {
|
||||||
$event->update(['attachment_id' => $this->attachments->store($this->image(), 'events')->id]);
|
$event->update(['attachment_id' => $this->attachments->store($this->image(), 'events')->id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +87,7 @@ class FiestaTradicionArrufoSeeder extends Seeder
|
|||||||
$existing = CatalogItem::query()->where('tenant_code', $tenant->codigo)->where('slug', $data['slug'])->first();
|
$existing = CatalogItem::query()->where('tenant_code', $tenant->codigo)->where('slug', $data['slug'])->first();
|
||||||
if ($existing !== null) {
|
if ($existing !== null) {
|
||||||
$existing->update(['sales_end_at' => $data['sales_end_at'] ?? null]);
|
$existing->update(['sales_end_at' => $data['sales_end_at'] ?? null]);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +107,12 @@ class FiestaTradicionArrufoSeeder extends Seeder
|
|||||||
|
|
||||||
private function image(): UploadedFile
|
private function image(): UploadedFile
|
||||||
{
|
{
|
||||||
return new UploadedFile(self::IMAGE, basename(self::IMAGE), 'image/png', null, true);
|
$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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class MenuSeeder extends Seeder
|
|||||||
[
|
[
|
||||||
'code' => 'event.detail',
|
'code' => 'event.detail',
|
||||||
'label' => 'Detalle de evento',
|
'label' => 'Detalle de evento',
|
||||||
'route' => '/evento/:id',
|
'route' => '/evento/:slug',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'code' => 'product.category',
|
'code' => 'product.category',
|
||||||
@@ -130,6 +130,12 @@ class MenuSeeder extends Seeder
|
|||||||
'parent_menu_code' => 'main.adminapp',
|
'parent_menu_code' => 'main.adminapp',
|
||||||
'route' => '/admin/desfile/entradas',
|
'route' => '/admin/desfile/entradas',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'code' => 'adminapp.desfile.reservas',
|
||||||
|
'label' => 'Reserva de Tickets',
|
||||||
|
'parent_menu_code' => 'main.adminapp',
|
||||||
|
'route' => '/admin/desfile/reservas',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'code' => 'account',
|
'code' => 'account',
|
||||||
'label' => 'Mi cuenta',
|
'label' => 'Mi cuenta',
|
||||||
@@ -290,9 +296,12 @@ class MenuSeeder extends Seeder
|
|||||||
$helpTenantCodes = [
|
$helpTenantCodes = [
|
||||||
'sonder',
|
'sonder',
|
||||||
'fiesta_futbol_infantil',
|
'fiesta_futbol_infantil',
|
||||||
|
'mutual_smep',
|
||||||
|
];
|
||||||
|
$ticketAdminMenuCodes = [
|
||||||
|
'adminapp.tickets',
|
||||||
];
|
];
|
||||||
$fiestaCategoryMenuCodes = [
|
$fiestaCategoryMenuCodes = [
|
||||||
'adminapp.tickets',
|
|
||||||
'adminapp.fiesta-futbol-infantil.entradas',
|
'adminapp.fiesta-futbol-infantil.entradas',
|
||||||
'adminapp.fiesta-futbol-infantil.alojamientos',
|
'adminapp.fiesta-futbol-infantil.alojamientos',
|
||||||
'adminapp.fiesta-futbol-infantil.merchandising',
|
'adminapp.fiesta-futbol-infantil.merchandising',
|
||||||
@@ -306,6 +315,7 @@ class MenuSeeder extends Seeder
|
|||||||
];
|
];
|
||||||
$desfileMenuCodes = [
|
$desfileMenuCodes = [
|
||||||
'adminapp.desfile.entradas',
|
'adminapp.desfile.entradas',
|
||||||
|
'adminapp.desfile.reservas',
|
||||||
];
|
];
|
||||||
$onTicketMenuCodes = [
|
$onTicketMenuCodes = [
|
||||||
'event.index',
|
'event.index',
|
||||||
@@ -356,6 +366,42 @@ class MenuSeeder extends Seeder
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
$mutualSmepFrequentlyAskedQuestions = [
|
||||||
|
[
|
||||||
|
'pregunta' => '¿Hay algún límite de compra?',
|
||||||
|
'respuesta' => 'La cantidad disponible depende del stock de cada producto.',
|
||||||
|
'is_active' => true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'pregunta' => '¿Cuáles son los medios de pago disponibles?',
|
||||||
|
'respuesta' => 'Podés consultar y seleccionar los medios de pago habilitados al finalizar tu compra.',
|
||||||
|
'is_active' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'pregunta' => '¿Cómo puedo recibir asesoramiento antes de comprar?',
|
||||||
|
'respuesta' => 'Podés comunicarte con Mutual SMEP por WhatsApp al +54 9 341 247-4530.',
|
||||||
|
'is_active' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$mutualSmepContactContent = [
|
||||||
|
'whatsapp' => [
|
||||||
|
'whatsapp_url' => 'https://wa.me/5493412474530',
|
||||||
|
'whatsapp_label' => 'Chateá con Mutual SMEP',
|
||||||
|
],
|
||||||
|
'phone' => '+54 9 341 247-4530',
|
||||||
|
'locations' => [
|
||||||
|
'rosario' => [
|
||||||
|
'label' => 'Rosario',
|
||||||
|
'addresses' => [
|
||||||
|
[
|
||||||
|
'label' => 'Mutual SMEP',
|
||||||
|
'address' => 'San Lorenzo 1543, Rosario, Santa Fe',
|
||||||
|
'coordinates' => [-32.9431184, -60.6437991],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
foreach ($tenants as $tenant) {
|
foreach ($tenants as $tenant) {
|
||||||
$menuCodes = $allMenus;
|
$menuCodes = $allMenus;
|
||||||
@@ -372,6 +418,10 @@ class MenuSeeder extends Seeder
|
|||||||
$menuCodes = array_diff($menuCodes, $helpMenuCodes);
|
$menuCodes = array_diff($menuCodes, $helpMenuCodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! in_array($tenant->codigo, ['fiesta_futbol_infantil', 'desfile_pura_tendencia'], true)) {
|
||||||
|
$menuCodes = array_diff($menuCodes, $ticketAdminMenuCodes);
|
||||||
|
}
|
||||||
|
|
||||||
if ($tenant->codigo !== 'fiesta_futbol_infantil') {
|
if ($tenant->codigo !== 'fiesta_futbol_infantil') {
|
||||||
$menuCodes = array_diff($menuCodes, $fiestaCategoryMenuCodes);
|
$menuCodes = array_diff($menuCodes, $fiestaCategoryMenuCodes);
|
||||||
} else {
|
} else {
|
||||||
@@ -393,10 +443,14 @@ class MenuSeeder extends Seeder
|
|||||||
|
|
||||||
if (in_array($tenant->codigo, $helpTenantCodes, true)) {
|
if (in_array($tenant->codigo, $helpTenantCodes, true)) {
|
||||||
$tenant->menues()->updateExistingPivot('help.faq', [
|
$tenant->menues()->updateExistingPivot('help.faq', [
|
||||||
'static_content' => $frequentlyAskedQuestions,
|
'static_content' => $tenant->codigo === 'mutual_smep'
|
||||||
|
? $mutualSmepFrequentlyAskedQuestions
|
||||||
|
: $frequentlyAskedQuestions,
|
||||||
]);
|
]);
|
||||||
$tenant->menues()->updateExistingPivot('help.contact', [
|
$tenant->menues()->updateExistingPivot('help.contact', [
|
||||||
'static_content' => $contactContent,
|
'static_content' => $tenant->codigo === 'mutual_smep'
|
||||||
|
? $mutualSmepContactContent
|
||||||
|
: $contactContent,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
122
database/seeders/MutualSmepAttributeSeeder.php
Normal file
122
database/seeders/MutualSmepAttributeSeeder.php
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Domains\Commerce\Catalog\Models\Attribute;
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use App\Shared\Enums\FieldType;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class MutualSmepAttributeSeeder extends Seeder
|
||||||
|
{
|
||||||
|
private const TENANT_CODE = 'mutual_smep';
|
||||||
|
|
||||||
|
/** @var list<array<string, mixed>> */
|
||||||
|
private const ATTRIBUTES = [
|
||||||
|
[
|
||||||
|
'codigo' => 'color',
|
||||||
|
'nombre' => 'Color',
|
||||||
|
'metadata_schema' => [
|
||||||
|
'hex' => ['type' => 'string'],
|
||||||
|
],
|
||||||
|
'options' => [
|
||||||
|
['value' => 'Negro', 'label' => 'Negro', 'metadata' => ['hex' => '#000000']],
|
||||||
|
['value' => 'Blanco', 'label' => 'Blanco', 'metadata' => ['hex' => '#FFFFFF']],
|
||||||
|
['value' => 'Gris', 'label' => 'Gris', 'metadata' => ['hex' => '#808080']],
|
||||||
|
['value' => 'Plata', 'label' => 'Plata', 'metadata' => ['hex' => '#C0C0C0']],
|
||||||
|
['value' => 'Azul', 'label' => 'Azul', 'metadata' => ['hex' => '#0000FF']],
|
||||||
|
['value' => 'Rojo', 'label' => 'Rojo', 'metadata' => ['hex' => '#DC3545']],
|
||||||
|
['value' => 'Rosa', 'label' => 'Rosa', 'metadata' => ['hex' => '#FFC0CB']],
|
||||||
|
['value' => 'Multicolor', 'label' => 'Multicolor'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'codigo' => 'medida_cama',
|
||||||
|
'nombre' => 'Medida',
|
||||||
|
'options' => [
|
||||||
|
['value' => 'Cuna', 'label' => 'Cuna'],
|
||||||
|
['value' => '1 Plaza', 'label' => '1 Plaza'],
|
||||||
|
['value' => '1 1/2 Plazas', 'label' => '1 1/2 Plazas'],
|
||||||
|
['value' => '2 Plazas', 'label' => '2 Plazas'],
|
||||||
|
['value' => '2 1/2 Plazas', 'label' => '2 1/2 Plazas'],
|
||||||
|
['value' => 'Queen', 'label' => 'Queen'],
|
||||||
|
['value' => 'King', 'label' => 'King'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'codigo' => 'almacenamiento',
|
||||||
|
'nombre' => 'Almacenamiento',
|
||||||
|
'options' => [
|
||||||
|
['value' => '128 GB', 'label' => '128 GB'],
|
||||||
|
['value' => '256 GB', 'label' => '256 GB'],
|
||||||
|
['value' => '512 GB', 'label' => '512 GB'],
|
||||||
|
['value' => '1 TB', 'label' => '1 TB'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'codigo' => 'memoria_ram',
|
||||||
|
'nombre' => 'Memoria RAM',
|
||||||
|
'options' => [
|
||||||
|
['value' => '4 GB', 'label' => '4 GB'],
|
||||||
|
['value' => '8 GB', 'label' => '8 GB'],
|
||||||
|
['value' => '16 GB', 'label' => '16 GB'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'codigo' => 'rodado',
|
||||||
|
'nombre' => 'Rodado',
|
||||||
|
'options' => [
|
||||||
|
['value' => '16', 'label' => '16'],
|
||||||
|
['value' => '26', 'label' => '26'],
|
||||||
|
['value' => '29', 'label' => '29'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
if (! Tenant::query()->where('codigo', self::TENANT_CODE)->exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::transaction(function (): void {
|
||||||
|
foreach (self::ATTRIBUTES as $definition) {
|
||||||
|
$options = $definition['options'];
|
||||||
|
unset($definition['options']);
|
||||||
|
|
||||||
|
$attribute = Attribute::query()->updateOrCreate(
|
||||||
|
[
|
||||||
|
'tenant_codigo' => self::TENANT_CODE,
|
||||||
|
'codigo' => $definition['codigo'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
...$definition,
|
||||||
|
'type' => FieldType::Select->value,
|
||||||
|
'is_required' => false,
|
||||||
|
'metadata_schema' => $definition['metadata_schema'] ?? null,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
$attribute->options()->delete();
|
||||||
|
$attribute->options()->createMany(
|
||||||
|
collect($options)
|
||||||
|
->values()
|
||||||
|
->map(fn (array $option, int $index): array => [
|
||||||
|
...$option,
|
||||||
|
'sort_order' => $index + 1,
|
||||||
|
])
|
||||||
|
->all()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Attribute::query()
|
||||||
|
->where('tenant_codigo', self::TENANT_CODE)
|
||||||
|
->whereIn('codigo', array_column(self::ATTRIBUTES, 'codigo'))
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
123
database/seeders/MutualSmepCategorySeeder.php
Normal file
123
database/seeders/MutualSmepCategorySeeder.php
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Domains\Commerce\Catalog\Models\Category;
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class MutualSmepCategorySeeder extends Seeder
|
||||||
|
{
|
||||||
|
private const TENANT_CODE = 'mutual_smep';
|
||||||
|
|
||||||
|
/** @var array<string, list<string>> */
|
||||||
|
private const CATEGORIES = [
|
||||||
|
'Dormitorio y Blanco' => [
|
||||||
|
'Colchones',
|
||||||
|
'Bases y Sommiers',
|
||||||
|
'Acolchados y Edredones',
|
||||||
|
'Frazadas y Mantas',
|
||||||
|
'Infantil y Cuna',
|
||||||
|
],
|
||||||
|
'Electrodomésticos' => [
|
||||||
|
'Heladeras',
|
||||||
|
'Lavarropas y Secarropas',
|
||||||
|
'Cocinas',
|
||||||
|
'Microondas',
|
||||||
|
'Hornos Eléctricos',
|
||||||
|
'Calefones y Termotanques',
|
||||||
|
'Purificadores y Extractores de Cocina',
|
||||||
|
'Preparación de Alimentos',
|
||||||
|
'Café y Desayuno',
|
||||||
|
'Freidoras y Cocción',
|
||||||
|
'Cuidado Personal',
|
||||||
|
'Limpieza y Planchado',
|
||||||
|
],
|
||||||
|
'Climatización' => [
|
||||||
|
'Aires Acondicionados',
|
||||||
|
'Calefactores a Gas con Salida',
|
||||||
|
'Calefactores a Gas sin Salida',
|
||||||
|
'Calefacción Eléctrica',
|
||||||
|
],
|
||||||
|
'Tecnología' => [
|
||||||
|
'Celulares',
|
||||||
|
'Notebooks',
|
||||||
|
'Smart TV',
|
||||||
|
'Audio',
|
||||||
|
'Accesorios de Computación',
|
||||||
|
'Soportes para TV',
|
||||||
|
],
|
||||||
|
'Bicicletas y Aire Libre' => [
|
||||||
|
'Bicicletas Infantiles',
|
||||||
|
'Bicicletas para Adultos',
|
||||||
|
'Piletas',
|
||||||
|
],
|
||||||
|
'Bazar' => [
|
||||||
|
'Termos',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
if (! Tenant::query()->where('codigo', self::TENANT_CODE)->exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::transaction(function (): void {
|
||||||
|
foreach (self::CATEGORIES as $parentName => $subcategoryNames) {
|
||||||
|
$parent = Category::query()->updateOrCreate(
|
||||||
|
[
|
||||||
|
'tenant_code' => self::TENANT_CODE,
|
||||||
|
'nombre' => $parentName,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'categoria_id' => null,
|
||||||
|
'is_enabled' => true,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($subcategoryNames as $subcategoryName) {
|
||||||
|
Category::query()->updateOrCreate(
|
||||||
|
[
|
||||||
|
'tenant_code' => self::TENANT_CODE,
|
||||||
|
'nombre' => $subcategoryName,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'categoria_id' => $parent->id,
|
||||||
|
'is_enabled' => true,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
DB::transaction(function (): void {
|
||||||
|
foreach (self::CATEGORIES as $parentName => $subcategoryNames) {
|
||||||
|
$parent = Category::query()
|
||||||
|
->where('tenant_code', self::TENANT_CODE)
|
||||||
|
->where('nombre', $parentName)
|
||||||
|
->whereNull('categoria_id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $parent instanceof Category) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Category::query()
|
||||||
|
->where('tenant_code', self::TENANT_CODE)
|
||||||
|
->where('categoria_id', $parent->id)
|
||||||
|
->whereIn('nombre', $subcategoryNames)
|
||||||
|
->whereDoesntHave('subCategories')
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
if (! $parent->subCategories()->exists()) {
|
||||||
|
$parent->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
117
database/seeders/OnTicketTenantSeeder.php
Normal file
117
database/seeders/OnTicketTenantSeeder.php
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Domains\Core\Client\Models\Client;
|
||||||
|
use App\Domains\Core\Tenant\Models\AdminWebsiteType;
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Core\Tenant\Services\TenantProvisioningService;
|
||||||
|
use App\Domains\Core\Tenant\Services\WebsiteExtraService;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
class OnTicketTenantSeeder extends Seeder
|
||||||
|
{
|
||||||
|
private const IMAGE_DIRECTORY = 'images/tennants/onticket_otpimizado';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly TenantProvisioningService $tenantProvisioningService,
|
||||||
|
private readonly WebsiteExtraService $websiteExtraService,
|
||||||
|
private readonly OnticketImmersiveHeroCarouselSeeder $carouselSeeder,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$client = Client::query()->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,13 +10,15 @@ use RuntimeException;
|
|||||||
|
|
||||||
class OnticketImmersiveHeroCarouselSeeder extends Seeder
|
class OnticketImmersiveHeroCarouselSeeder extends Seeder
|
||||||
{
|
{
|
||||||
|
private const IMAGE_DIRECTORY = 'images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel';
|
||||||
|
|
||||||
private const IMAGES = [
|
private const IMAGES = [
|
||||||
'onticket_hero_carousel_1.png',
|
'onticket_hero_carousel_1.avif',
|
||||||
'onticket_hero_carousel_2.png',
|
'onticket_hero_carousel_2.avif',
|
||||||
'onticket_hero_carousel_3.avif',
|
'onticket_hero_carousel_3.avif',
|
||||||
'onticket_hero_carousel_4.jfif',
|
'onticket_hero_carousel_4.avif',
|
||||||
'onticket_hero_carousel_5.jfif',
|
'onticket_hero_carousel_5.avif',
|
||||||
'onticket_hero_carousel_6.jfif',
|
'onticket_hero_carousel_6.avif',
|
||||||
'onticket_hero_carousel_7.avif',
|
'onticket_hero_carousel_7.avif',
|
||||||
'onticket_hero_carousel_8.avif',
|
'onticket_hero_carousel_8.avif',
|
||||||
];
|
];
|
||||||
@@ -39,7 +41,7 @@ class OnticketImmersiveHeroCarouselSeeder extends Seeder
|
|||||||
}
|
}
|
||||||
|
|
||||||
$images = array_map(function (string $filename): UploadedFile {
|
$images = array_map(function (string $filename): UploadedFile {
|
||||||
$path = public_path("images/tennants/onticket/onticket_tennant_hero_carousel/{$filename}");
|
$path = public_path(self::IMAGE_DIRECTORY."/{$filename}");
|
||||||
|
|
||||||
if (! is_file($path)) {
|
if (! is_file($path)) {
|
||||||
throw new RuntimeException("Image not found at path: {$path}");
|
throw new RuntimeException("Image not found at path: {$path}");
|
||||||
|
|||||||
155
database/seeders/OptimizeOnticketImagesSeeder.php
Normal file
155
database/seeders/OptimizeOnticketImagesSeeder.php
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use App\Shared\Attachable\Models\Attachment;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use RuntimeException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class OptimizeOnticketImagesSeeder extends Seeder
|
||||||
|
{
|
||||||
|
private const IMAGE_DIRECTORY = 'images/tennants/onticket_otpimizado';
|
||||||
|
|
||||||
|
private const CAROUSEL_IMAGES = [
|
||||||
|
'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',
|
||||||
|
];
|
||||||
|
|
||||||
|
private const EVENT_IMAGES = [
|
||||||
|
'[Prueba] Noche de Música en Vivo' => '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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,10 +3,8 @@
|
|||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Domains\Core\Client\Models\Client;
|
use App\Domains\Core\Client\Models\Client;
|
||||||
use App\Domains\Core\Tenant\Models\AdminWebsiteType;
|
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
use App\Domains\Core\Tenant\Services\TenantService;
|
use App\Domains\Core\Tenant\Services\TenantProvisioningService;
|
||||||
use App\Domains\Core\Tenant\Services\WebsiteExtraService;
|
|
||||||
use App\Shared\Attachable\Models\Attachment;
|
use App\Shared\Attachable\Models\Attachment;
|
||||||
use App\Shared\Attachable\Services\AttachmentService;
|
use App\Shared\Attachable\Services\AttachmentService;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
@@ -16,8 +14,6 @@ use Throwable;
|
|||||||
|
|
||||||
class TenantSeeder extends Seeder
|
class TenantSeeder extends Seeder
|
||||||
{
|
{
|
||||||
private const ONTICKET_CLIENT_CODE = 'onticket';
|
|
||||||
|
|
||||||
private const PYME_RURAL_CLIENT_CODE = 'pyme_rural';
|
private const PYME_RURAL_CLIENT_CODE = 'pyme_rural';
|
||||||
|
|
||||||
private const SONDER_CLIENT_CODE = 'sonder';
|
private const SONDER_CLIENT_CODE = 'sonder';
|
||||||
@@ -46,8 +42,7 @@ class TenantSeeder extends Seeder
|
|||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected TenantService $tenantService,
|
protected TenantProvisioningService $tenantService,
|
||||||
protected WebsiteExtraService $websiteExtraService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function run(): void
|
public function run(): void
|
||||||
@@ -57,89 +52,12 @@ class TenantSeeder extends Seeder
|
|||||||
['name' => 'Sonder'],
|
['name' => 'Sonder'],
|
||||||
);
|
);
|
||||||
|
|
||||||
$onTicketClient = Client::query()->firstOrCreate(
|
|
||||||
['code' => self::ONTICKET_CLIENT_CODE],
|
|
||||||
['name' => 'OnTicket'],
|
|
||||||
);
|
|
||||||
|
|
||||||
$pymeRuralClient = Client::query()->updateOrCreate(
|
$pymeRuralClient = Client::query()->updateOrCreate(
|
||||||
['code' => self::PYME_RURAL_CLIENT_CODE],
|
['code' => self::PYME_RURAL_CLIENT_CODE],
|
||||||
['name' => 'Pyme Rural'],
|
['name' => 'Pyme Rural'],
|
||||||
);
|
);
|
||||||
|
|
||||||
$onTicketType = AdminWebsiteType::query()->where('codigo', 'onticket')->firstOrFail();
|
$this->call(OnTicketTenantSeeder::class);
|
||||||
|
|
||||||
$onTicketTenant = Tenant::query()->where('codigo', 'onticket')->first();
|
|
||||||
|
|
||||||
if ($onTicketTenant === null) {
|
|
||||||
$this->tenantService->create([
|
|
||||||
'client_id' => $onTicketClient->id,
|
|
||||||
'codigo' => 'onticket',
|
|
||||||
'nombre' => $onTicketType->nombre,
|
|
||||||
'dominio' => $onTicketType->dominio,
|
|
||||||
'site_title' => $onTicketType->site_title,
|
|
||||||
'primary_color' => $onTicketType->primary_color,
|
|
||||||
'secondary_color' => $onTicketType->secondary_color,
|
|
||||||
'danger_color' => $onTicketType->danger_color,
|
|
||||||
'success_color' => $onTicketType->success_color,
|
|
||||||
'header_bg_color' => $onTicketType->surface_color,
|
|
||||||
'footer_bg_color' => $onTicketType->primary_color,
|
|
||||||
'display_categories' => true,
|
|
||||||
'display_seach_bar' => true,
|
|
||||||
'admin_website_type_code' => $onTicketType->codigo,
|
|
||||||
'storefront_website_type_code' => 'onticket_multi_event',
|
|
||||||
'header_logo' => $this->uploadedImage(
|
|
||||||
'images/tennants/onticket/onticket_logo.png',
|
|
||||||
'onticket_logo.png',
|
|
||||||
),
|
|
||||||
'footer_logo' => $this->uploadedImage(
|
|
||||||
'images/tennants/onticket/onticket_footer_logo.png',
|
|
||||||
'onticket_footer_logo.png',
|
|
||||||
),
|
|
||||||
'favicon' => $this->uploadedImage(
|
|
||||||
'images/tennants/onticket/onticket_favicon.svg',
|
|
||||||
'onticket_favicon.svg',
|
|
||||||
),
|
|
||||||
'header_bg_image' => $this->uploadedImage(
|
|
||||||
'images/tennants/onticket/onticket_header_background.png',
|
|
||||||
'onticket_header_background.png',
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
} elseif ($onTicketTenant->header_bg_image_id === null) {
|
|
||||||
$this->tenantService->update($onTicketTenant, [
|
|
||||||
'header_bg_image' => $this->uploadedImage(
|
|
||||||
'images/tennants/onticket/onticket_header_background.png',
|
|
||||||
'onticket_header_background.png',
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($onTicketTenant !== null && $onTicketTenant->storefront_website_type_code === 'onticket') {
|
|
||||||
$onTicketTenant->update(['storefront_website_type_code' => 'onticket_multi_event']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$onTicketTenant = Tenant::query()->where('codigo', 'onticket')->firstOrFail();
|
|
||||||
|
|
||||||
foreach (['Música', 'Teatro', 'Deportes', 'Infantiles', 'Otros'] as $nombre) {
|
|
||||||
$onTicketTenant->eventCategories()->firstOrCreate(['nombre' => $nombre]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($onTicketTenant->storefront_website_type_code === 'onticket_multi_event' && ! $onTicketTenant->display_seach_bar) {
|
|
||||||
$onTicketTenant->update(['display_seach_bar' => true]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
$onTicketTenant->storefront_website_type_code === 'onticket_multi_event'
|
|
||||||
&& ! $onTicketTenant->websiteExtras()->whereHas('websiteTypeExtra', fn ($query) => $query->where('codigo', 'immersiveHero'))->exists()
|
|
||||||
) {
|
|
||||||
$this->websiteExtraService->updateForTenant($onTicketTenant, 'immersiveHero', [
|
|
||||||
'eyebrow' => 'Encendé tu',
|
|
||||||
'title' => 'experiencia',
|
|
||||||
'description' => 'Reservá tu entrada y formá parte',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->call(OnticketImmersiveHeroCarouselSeeder::class);
|
|
||||||
|
|
||||||
$this->deleteTenant('sonder');
|
$this->deleteTenant('sonder');
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,15 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
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\Category;
|
||||||
|
use App\Domains\Commerce\Catalog\Services\CatalogService;
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Ticketing\Event\Models\Event;
|
||||||
use App\Shared\Attachable\Services\AttachmentService;
|
use App\Shared\Attachable\Services\AttachmentService;
|
||||||
|
use App\Shared\Enums\FieldType;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
@@ -11,7 +18,12 @@ use RuntimeException;
|
|||||||
|
|
||||||
class TestEventsSeeder extends Seeder
|
class TestEventsSeeder extends Seeder
|
||||||
{
|
{
|
||||||
public function __construct(private readonly AttachmentService $attachmentService) {}
|
private const IMAGE_DIRECTORY = 'images/tennants/onticket_otpimizado/test-events';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly AttachmentService $attachmentService,
|
||||||
|
private readonly CatalogService $catalogService,
|
||||||
|
) {}
|
||||||
|
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
@@ -20,45 +32,114 @@ class TestEventsSeeder extends Seeder
|
|||||||
$events = [
|
$events = [
|
||||||
[
|
[
|
||||||
'title' => '[Prueba] Noche de Música en Vivo',
|
'title' => '[Prueba] Noche de Música en Vivo',
|
||||||
|
'slug' => 'prueba-noche-de-musica-en-vivo',
|
||||||
'category' => 'Música',
|
'category' => 'Música',
|
||||||
'subtitle' => 'Una noche para cantar y bailar',
|
'subtitle' => 'Una noche para cantar y bailar',
|
||||||
'description' => 'Evento de prueba para explorar la cartelera de OnTicket.',
|
'description' => 'Evento de prueba para explorar la cartelera de OnTicket.',
|
||||||
'location' => 'Teatro Broadway, Rosario',
|
'location' => 'Teatro Broadway, Rosario',
|
||||||
'image' => 'live-music.png',
|
'image' => 'live-music.avif',
|
||||||
'weeks_from_now' => [2],
|
'weeks_from_now' => [2],
|
||||||
'time_start' => '21:00:00',
|
'time_start' => '21:00:00',
|
||||||
'time_end' => '23:30: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',
|
'title' => '[Prueba] Festival de Sabores',
|
||||||
|
'slug' => 'prueba-festival-de-sabores',
|
||||||
'category' => 'Otros',
|
'category' => 'Otros',
|
||||||
'subtitle' => 'Gastronomía y música para toda la familia',
|
'subtitle' => 'Gastronomía y música para toda la familia',
|
||||||
'description' => 'Evento de prueba con dos jornadas disponibles.',
|
'description' => 'Evento de prueba con dos jornadas disponibles.',
|
||||||
'location' => 'Parque de España, Rosario',
|
'location' => 'Parque de España, Rosario',
|
||||||
'image' => 'food-festival.png',
|
'image' => 'food-festival.avif',
|
||||||
'weeks_from_now' => [3, 3],
|
'weeks_from_now' => [3, 3],
|
||||||
'day_offsets' => [0, 1],
|
'day_offsets' => [0, 1],
|
||||||
'time_start' => '12:00:00',
|
'time_start' => '12:00:00',
|
||||||
'time_end' => '20: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',
|
'title' => '[Prueba] Stand Up en el Centro',
|
||||||
|
'slug' => 'prueba-stand-up-en-el-centro',
|
||||||
'category' => 'Teatro',
|
'category' => 'Teatro',
|
||||||
'subtitle' => 'Humor para compartir',
|
'subtitle' => 'Humor para compartir',
|
||||||
'description' => 'Evento de prueba para visualizar distintas propuestas.',
|
'description' => 'Evento de prueba para visualizar distintas propuestas.',
|
||||||
'location' => 'Centro Cultural La Comedia, Rosario',
|
'location' => 'Centro Cultural La Comedia, Rosario',
|
||||||
'image' => 'stand-up.png',
|
'image' => 'stand-up.avif',
|
||||||
'weeks_from_now' => [4],
|
'weeks_from_now' => [4],
|
||||||
'time_start' => '20:30:00',
|
'time_start' => '20:30:00',
|
||||||
'time_end' => '22:00: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 {
|
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) {
|
foreach ($events as $definition) {
|
||||||
$event = $tenant->events()->firstOrCreate(
|
$event = $tenant->events()->firstOrCreate(
|
||||||
['title' => $definition['title']],
|
['title' => $definition['title']],
|
||||||
[
|
[
|
||||||
|
'slug' => $definition['slug'],
|
||||||
'subtitle' => $definition['subtitle'],
|
'subtitle' => $definition['subtitle'],
|
||||||
'description' => $definition['description'],
|
'description' => $definition['description'],
|
||||||
'location' => $definition['location'],
|
'location' => $definition['location'],
|
||||||
@@ -68,29 +149,30 @@ class TestEventsSeeder extends Seeder
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if ($event->slug !== $definition['slug']) {
|
||||||
|
$event->update(['slug' => $definition['slug']]);
|
||||||
|
}
|
||||||
|
|
||||||
if ($event->event_category_id === null) {
|
if ($event->event_category_id === null) {
|
||||||
$event->update(['event_category_id' => $tenant->eventCategories()
|
$event->update(['event_category_id' => $tenant->eventCategories()
|
||||||
->where('nombre', $definition['category'])->value('id')]);
|
->where('nombre', $definition['category'])->value('id')]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($event->attachment_id === null) {
|
if ($event->attachment_id === null) {
|
||||||
$path = public_path('images/tennants/onticket/test-events/'.$definition['image']);
|
$path = public_path(self::IMAGE_DIRECTORY.'/'.$definition['image']);
|
||||||
|
|
||||||
if (! is_file($path)) {
|
if (! is_file($path)) {
|
||||||
throw new RuntimeException("Image not found at path: {$path}");
|
throw new RuntimeException("Image not found at path: {$path}");
|
||||||
}
|
}
|
||||||
|
|
||||||
$attachment = $this->attachmentService->store(
|
$attachment = $this->attachmentService->store(
|
||||||
new UploadedFile($path, $definition['image'], 'image/png', null, true),
|
new UploadedFile($path, $definition['image'], 'image/avif', null, true),
|
||||||
'tenants/onticket/events',
|
'tenants/onticket/events',
|
||||||
);
|
);
|
||||||
$event->update(['attachment_id' => $attachment->id]);
|
$event->update(['attachment_id' => $attachment->id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($event->dates()->exists()) {
|
if (! $event->dates()->exists()) {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($definition['weeks_from_now'] as $index => $weeks) {
|
foreach ($definition['weeks_from_now'] as $index => $weeks) {
|
||||||
$date = now()->startOfDay()->addWeeks($weeks)
|
$date = now()->startOfDay()->addWeeks($weeks)
|
||||||
->addDays($definition['day_offsets'][$index] ?? 0);
|
->addDays($definition['day_offsets'][$index] ?? 0);
|
||||||
@@ -103,6 +185,52 @@ class TestEventsSeeder extends Seeder
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->seedCatalogItems($tenant, $event, $entryCategory, $definition['catalog_items']);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param array<int, array<string, mixed>> $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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user