*/ private const ALLOCATIONS = [ ['sector' => 'A', 'row' => 1, 'seats' => 16, 'type' => 'NORMAL'], ['sector' => 'A', 'row' => 3, 'seats' => 14, 'type' => 'NORMAL'], ['sector' => 'C', 'row' => 1, 'seats' => 16, 'type' => 'VIP + LUNCH'], ]; public function provision(): void { if (! $this->prerequisitesExist()) { return; } DB::transaction(function (): void { $now = now(); $userId = $this->userId($now); $purchaseId = $this->purchaseId($userId, $now); $catalogItem = DB::table('catalog_items') ->where('tenant_code', self::TENANT_CODE) ->where('slug', 'entrada') ->first(); if ($catalogItem === null) { throw new RuntimeException('No se encontró el catálogo de entradas del desfile.'); } foreach (self::ALLOCATIONS as $allocation) { foreach (range(1, $allocation['seats']) as $seat) { $variant = $this->variant( (int) $catalogItem->id, $allocation['sector'], $allocation['row'], $seat, $allocation['type'], ); $this->createPurchaseItem( $purchaseId, $catalogItem, $variant, $allocation['sector'], $allocation['row'], $seat, $allocation['type'], $now, ); $this->createTicketAndCommitStock( $purchaseId, $userId, (int) $catalogItem->id, $variant, $now, ); } } }); } private function prerequisitesExist(): bool { if (! DB::table('tenants')->where('codigo', self::TENANT_CODE)->exists()) { return false; } if (! DB::table('roles')->where('codigo', 'user')->exists()) { throw new RuntimeException('No se encontró el rol de usuario común.'); } if (! DB::table('catalog_items') ->where('tenant_code', self::TENANT_CODE) ->where('slug', 'entrada') ->exists()) { throw new RuntimeException('No se encontró el catálogo de entradas del desfile.'); } return true; } private function userId(DateTimeInterface $now): int { $user = DB::table('users')->where('email', self::USER_EMAIL)->first(); if ($user !== null) { if ($user->tenant_codigo !== self::TENANT_CODE) { throw new RuntimeException('El email de invitados ya pertenece a otro tenant.'); } DB::table('users')->where('id', $user->id)->update([ 'rol_codigo' => 'user', 'nombre_apellido' => 'Invitados Pura Tendencia', 'updated_at' => $now, ]); return (int) $user->id; } return DB::table('users')->insertGetId([ 'rol_codigo' => 'user', 'tenant_codigo' => self::TENANT_CODE, 'nombre_apellido' => 'Invitados Pura Tendencia', 'email' => self::USER_EMAIL, 'email_verified_at' => $now, 'password' => Hash::make(Str::random(64)), 'dni' => null, 'telefono' => null, 'remember_token' => null, 'created_at' => $now, 'updated_at' => $now, ]); } private function purchaseId(int $userId, DateTimeInterface $now): int { $purchaseId = DB::table('compras') ->where('tenant_codigo', self::TENANT_CODE) ->where('user_id', $userId) ->where('payment_method', self::PAYMENT_METHOD) ->value('id'); if ($purchaseId !== null) { DB::table('compras')->where('id', $purchaseId)->update([ 'status' => 'paid', 'expires_at' => null, 'total' => 0, 'updated_at' => $now, ]); return (int) $purchaseId; } return DB::table('compras')->insertGetId([ 'tenant_codigo' => self::TENANT_CODE, 'user_id' => $userId, 'cart_id' => null, 'status' => 'paid', 'payment_method' => self::PAYMENT_METHOD, 'expires_at' => null, 'total' => 0, 'dni' => null, 'transfer_payer_dni' => null, 'telefono' => null, 'nombre_apellido' => 'Invitados Pura Tendencia', 'email' => self::USER_EMAIL, 'created_at' => $now, 'updated_at' => $now, ]); } private function variant( int $catalogItemId, string $sector, int $row, int $seat, string $type, ): object { $variant = $this->findVariant($catalogItemId, $sector, $row, $seat); if ($variant === null) { $variant = $this->createVariant($catalogItemId, $sector, $row, $seat, $type); } $this->ensureAttributeOptions($catalogItemId, [ 'tipo' => $type, 'sector' => $sector, 'fila' => (string) $row, 'asiento' => (string) $seat, ]); $typeValueId = DB::table('variant_values as variant_value') ->join('item_attributes as item_attribute', 'item_attribute.id', '=', 'variant_value.item_attribute_id') ->join('attribute', 'attribute.id', '=', 'item_attribute.attribute_id') ->where('variant_value.variant_id', $variant->id) ->where('attribute.codigo', 'tipo') ->value('variant_value.id'); if ($typeValueId === null) { throw new RuntimeException("La variante {$variant->id} no tiene definido el tipo de entrada."); } DB::table('variant_values')->where('id', $typeValueId)->update(['value' => $type]); DB::table('variantes')->where('id', $variant->id)->update([ 'descripcion' => "Sector {$sector} - Fila {$row} - Asiento {$seat} - {$type}", ]); return DB::table('variantes')->where('id', $variant->id)->first(); } /** @param array $values */ private function ensureAttributeOptions(int $catalogItemId, array $values): void { $attributes = DB::table('item_attributes') ->join('attribute', 'attribute.id', '=', 'item_attributes.attribute_id') ->where('item_attributes.catalog_item_id', $catalogItemId) ->pluck('attribute.id', 'attribute.codigo'); foreach ($values as $code => $value) { $attributeId = $attributes[$code] ?? null; if ($attributeId === null) { throw new RuntimeException("Falta el atributo {$code} en el catálogo de entradas."); } if (DB::table('attribute_options') ->where('attribute_id', $attributeId) ->where('value', $value) ->exists()) { continue; } $lastSortOrder = (int) DB::table('attribute_options') ->where('attribute_id', $attributeId) ->max('sort_order'); DB::table('attribute_options')->insert([ 'attribute_id' => $attributeId, 'validity_time_id' => null, 'value' => $value, 'label' => $value, 'sort_order' => $lastSortOrder + 1, 'metadata' => null, 'created_at' => now(), 'updated_at' => now(), ]); } } private function findVariant(int $catalogItemId, string $sector, int $row, int $seat): ?object { $query = DB::table('variantes')->where('catalog_item_id', $catalogItemId); foreach (['sector' => $sector, 'fila' => (string) $row, 'asiento' => (string) $seat] as $code => $value) { $query->whereExists(fn ($subquery) => $subquery ->selectRaw('1') ->from('variant_values as selected_value') ->join('item_attributes as selected_item_attribute', 'selected_item_attribute.id', '=', 'selected_value.item_attribute_id') ->join('attribute as selected_attribute', 'selected_attribute.id', '=', 'selected_item_attribute.attribute_id') ->whereColumn('selected_value.variant_id', 'variantes.id') ->where('selected_attribute.codigo', $code) ->where('selected_value.value', $value)); } return $query->first(); } private function createVariant( int $catalogItemId, string $sector, int $row, int $seat, string $type, ): object { $itemAttributes = DB::table('item_attributes') ->join('attribute', 'attribute.id', '=', 'item_attributes.attribute_id') ->where('item_attributes.catalog_item_id', $catalogItemId) ->pluck('item_attributes.id', 'attribute.codigo'); foreach (['tipo', 'sector', 'fila', 'asiento'] as $requiredCode) { if (! isset($itemAttributes[$requiredCode])) { throw new RuntimeException("Falta el atributo {$requiredCode} en el catálogo de entradas."); } } $inventoryId = DB::table('inventories')->insertGetId([ 'sold_units' => 0, 'reserved_stock' => 0, 'real_stock' => 1, ]); $variantId = DB::table('variantes')->insertGetId([ 'catalog_item_id' => $catalogItemId, 'event_date_id' => null, 'inventory_id' => $inventoryId, 'descripcion' => "Sector {$sector} - Fila {$row} - Asiento {$seat} - {$type}", 'precio' => $this->catalogPrice($sector, $row), ]); foreach (['tipo' => $type, 'sector' => $sector, 'fila' => (string) $row, 'asiento' => (string) $seat] as $code => $value) { DB::table('variant_values')->insert([ 'variant_id' => $variantId, 'item_attribute_id' => $itemAttributes[$code], 'value' => $value, 'created_at' => now(), 'updated_at' => now(), ]); } return DB::table('variantes')->where('id', $variantId)->first(); } private function catalogPrice(string $sector, int $row): int { $prices = in_array($sector, ['A', 'C'], true) ? [1 => 250000, 2 => 200000, 3 => 100000, 4 => 75000, 5 => 50000] : [1 => 240000, 2 => 190000, 3 => 90000, 4 => 65000, 5 => 40000]; return $prices[$row]; } private function createPurchaseItem( int $purchaseId, object $catalogItem, object $variant, string $sector, int $row, int $seat, string $type, DateTimeInterface $now, ): void { if (DB::table('compra_items') ->where('compra_id', $purchaseId) ->where('source_variant_id', $variant->id) ->exists()) { return; } $attributes = [ ['name' => 'Tipo', 'value' => $type], ['name' => 'Sector', 'value' => $sector], ['name' => 'Fila', 'value' => (string) $row], ['name' => 'Asiento', 'value' => (string) $seat], ]; DB::table('compra_items')->insert([ 'compra_id' => $purchaseId, 'source_catalog_item_id' => $catalogItem->id, 'source_variant_id' => $variant->id, 'image_attachment_id' => DB::table('catalog_items_attachments') ->where('catalog_item_id', $catalogItem->id) ->whereNull('variant_id') ->orderBy('orden') ->value('attachment_id'), 'nombre' => $catalogItem->nombre, 'descripcion' => $variant->descripcion, 'slug' => $catalogItem->slug, 'item_nombre' => "Entrada (Sector {$sector}, Fila {$row}, Asiento {$seat})", 'variant_attributes' => json_encode($attributes, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR), 'cantidad' => 1, 'precio_unitario' => 0, 'discount_total' => 0, 'tax_total' => 0, 'total' => 0, 'created_at' => $now, 'updated_at' => $now, ]); } private function createTicketAndCommitStock( int $purchaseId, int $userId, int $catalogItemId, object $variant, DateTimeInterface $now, ): void { if (DB::table('tickets') ->where('source_purchase_id', $purchaseId) ->where('source_variant_id', $variant->id) ->exists()) { return; } $inventory = DB::table('inventories')->where('id', $variant->inventory_id)->lockForUpdate()->first(); if ($inventory === null || $inventory->real_stock < 1 || $inventory->reserved_stock > 0) { throw new RuntimeException("El asiento {$variant->descripcion} ya no está disponible."); } DB::table('inventories')->where('id', $inventory->id)->update([ 'real_stock' => $inventory->real_stock - 1, 'sold_units' => $inventory->sold_units + 1, ]); DB::table('stock_reservations')->insert([ 'inventory_id' => $inventory->id, 'cart_item_id' => null, 'purchase_id' => $purchaseId, 'quantity' => 1, 'status' => 'committed', 'expires_at' => null, 'committed_at' => $now, 'released_at' => null, 'created_at' => $now, 'updated_at' => $now, ]); DB::table('tickets')->insert([ 'tenant_code' => self::TENANT_CODE, 'ticket' => (string) Str::uuid(), 'name' => null, 'description' => null, 'source_purchase_id' => $purchaseId, 'source_catalog_item_id' => $catalogItemId, 'source_variant_id' => $variant->id, 'used_at' => null, 'scanner_user_id' => null, 'user_id' => $userId, ]); } }