where('tenant_code', $tenant->codigo) ->where('slug', 'alojamiento') ->with([ 'itemAttributes.attribute.options', 'variants.catalogItem', 'variants.inventory', 'variants.definitions', ]) ->first(); } /** * @param array> $variants */ public function upsertMany(Tenant $tenant, array $variants): CatalogItem { return DB::transaction(function () use ($tenant, $variants): CatalogItem { $attribute = $this->attribute($tenant); $accommodation = $this->accommodation($tenant, $variants); $itemAttribute = $accommodation->itemAttributes()->firstOrCreate( ['attribute_id' => $attribute->id], ['allow_multi_select' => false], ); $existingVariants = $accommodation->variants() ->with(['inventory', 'definitions']) ->lockForUpdate() ->get(); $resolvedVariants = $this->resolveVariants($variants); $this->validateValues($resolvedVariants, $existingVariants, $itemAttribute); foreach ($resolvedVariants as $index => $data) { $variant = isset($data['id']) ? $existingVariants->firstWhere('id', (int) $data['id']) : null; if (isset($data['id']) && $variant === null) { throw ValidationException::withMessages([ "variants.{$index}.id" => ['La variante no pertenece al producto Alojamiento.'], ]); } if ($variant === null) { $this->createVariant($attribute, $accommodation, $itemAttribute, $data); } else { $this->updateVariant($attribute, $variant, $itemAttribute, $data, $index); } } $minimumPrice = $accommodation->variants()->min('precio'); if ($minimumPrice !== null) { $accommodation->update(['precio' => $minimumPrice]); } return $accommodation->fresh()->load([ 'itemAttributes.attribute.options', 'variants.catalogItem', 'variants.inventory', 'variants.definitions', ]); }); } public function delete(Tenant $tenant, int $accommodationId): void { $variant = Variant::query() ->whereKey($accommodationId) ->whereHas('catalogItem', fn ($query) => $query ->where('tenant_code', $tenant->codigo) ->where('slug', 'alojamiento')) ->firstOrFail(); $this->catalogService->deleteVariant($variant); } private function attribute(Tenant $tenant): Attribute { $attribute = Attribute::query() ->where('tenant_codigo', $tenant->codigo) ->where('codigo', self::ATTRIBUTE_CODE) ->with('options') ->lockForUpdate() ->first(); if ($attribute === null) { throw ValidationException::withMessages([ 'variants' => ['Falta el atributo requerido tipo_alojamiento.'], ]); } return $attribute; } /** @param array> $variants */ private function accommodation(Tenant $tenant, array $variants): CatalogItem { $category = Category::query()->firstOrCreate([ 'tenant_code' => $tenant->codigo, 'nombre' => 'Alojamientos', ]); $accommodation = CatalogItem::withTrashed() ->where('tenant_code', $tenant->codigo) ->where('slug', 'alojamiento') ->lockForUpdate() ->first(); if ($accommodation !== null) { if ($accommodation->trashed()) { $accommodation->restore(); } $accommodation->update([ 'category_id' => $category->id, 'inventory_policy' => InventoryPolicy::Tracked->value, 'has_tickets' => true, ]); return $accommodation; } return CatalogItem::query()->create([ 'tenant_code' => $tenant->codigo, 'slug' => 'alojamiento', 'nombre' => 'Alojamiento', 'descripcion' => 'Alojamiento', 'category_id' => $category->id, 'precio' => collect($variants)->min('price') ?? 0, 'inventory_policy' => InventoryPolicy::Tracked->value, 'has_tickets' => true, 'inventory_id' => null, ]); } /** * @param array> $variants * @return array> */ private function resolveVariants(array $variants): array { return collect($variants)->map(fn (array $variant): array => [ ...$variant, 'title' => trim($variant['title']), 'value' => $this->valueCode($variant['title']), 'description' => $variant['description'] ?? null, 'stock' => (int) $variant['stock'], ])->all(); } /** * @param array> $incoming * @param Collection $existing */ private function validateValues(array $incoming, Collection $existing, ItemAttribute $itemAttribute): void { $incomingIds = collect($incoming)->pluck('id')->filter()->map(fn ($id): int => (int) $id); $seen = []; foreach ($existing->whereNotIn('id', $incomingIds) as $variant) { $value = $variant->definitions->firstWhere('item_attribute_id', $itemAttribute->id)?->value; if ($value !== null) { $seen[mb_strtolower(trim($value))] = true; } } foreach ($incoming as $index => $variant) { $value = $variant['value']; if (isset($seen[$value])) { throw ValidationException::withMessages([ "variants.{$index}.title" => ['Ya existe un tipo de alojamiento con ese título.'], ]); } $seen[$value] = true; } } /** @param array $data */ private function createVariant( Attribute $attribute, CatalogItem $accommodation, ItemAttribute $itemAttribute, array $data, ): void { $this->createOption($attribute, $data['value'], $data['title']); $inventory = Inventory::query()->create(['real_stock' => $data['stock']]); $variant = $accommodation->variants()->create([ 'inventory_id' => $inventory->id, 'descripcion' => $data['description'], 'precio' => $data['price'], ]); $variant->definitions()->create([ 'item_attribute_id' => $itemAttribute->id, 'value' => $data['value'], ]); } /** @param array $data */ private function updateVariant( Attribute $attribute, Variant $variant, ItemAttribute $itemAttribute, array $data, int $index, ): void { $inventory = Inventory::query() ->whereKey($variant->inventory_id) ->lockForUpdate() ->firstOrFail(); if ($data['stock'] < $inventory->reserved_stock) { throw ValidationException::withMessages([ "variants.{$index}.stock" => [ 'El stock no puede ser menor que la cantidad actualmente reservada.', ], ]); } $definition = $variant->definitions ->firstWhere('item_attribute_id', $itemAttribute->id); $option = $definition === null ? null : $attribute->options->firstWhere('value', $definition->value); if ($option === null) { $this->createOption($attribute, $data['value'], $data['title']); } else { $option->update([ 'value' => $data['value'], 'label' => $data['title'], ]); } $variant->update([ 'descripcion' => $data['description'], 'precio' => $data['price'], ]); $inventory->update(['real_stock' => $data['stock']]); $variant->definitions()->updateOrCreate( ['item_attribute_id' => $itemAttribute->id], ['value' => $data['value']], ); } private function createOption(Attribute $attribute, string $value, string $label): AttributeOption { $existing = $attribute->options->first( fn (AttributeOption $option): bool => mb_strtolower($option->value) === $value ); if ($existing !== null) { $existing->update(['label' => $label]); return $existing; } $option = $attribute->options()->create([ 'value' => $value, 'label' => $label, 'sort_order' => ((int) $attribute->options->max('sort_order')) + 1, ]); $attribute->options->push($option); return $option; } private function valueCode(string $title): string { return mb_strtolower((string) preg_replace('/\s+/u', '_', trim($title))); } }