*/ private const PRODUCT_CATALOG = [ 'buzo_blanco_adidas' => ['price' => 89999, 'stock' => 12], 'buzo_gris_nike' => ['price' => 94999, 'stock' => 10], 'buzo_gris_under_armour' => ['price' => 96999, 'stock' => 8], 'buzo_negro_adidas' => ['price' => 91999, 'stock' => 9], 'camiseta_rosario_central' => ['price' => 69999, 'stock' => 20], 'pantalon_blanco' => ['price' => 55999, 'stock' => 14], 'pantalon_negro' => ['price' => 57999, 'stock' => 14], 'remera_blanca' => ['price' => 35999, 'stock' => 22], 'remera_negra' => ['price' => 36999, 'stock' => 22], 'zapatillas_lecoq' => ['price' => 109999, 'stock' => 6], 'zapatillas_topper' => ['price' => 104999, 'stock' => 7], ]; /** * @var array */ private const CLOTHING_SIZES = ['S', 'M', 'L', 'XL']; /** * @var array */ private const SHOE_SIZES = ['38', '40', '42', '44']; /** * @var array */ private const IGNORED_FILES = [ 'istockphoto-1675347112-2048x2048.jpg', ]; public function __construct(private readonly ProductService $productService) { } /** * Run the database seeds. */ public function run(): void { $tenant = Tenant::query()->where('codigo', 'sonder')->first(); if (! $tenant instanceof Tenant) { throw new RuntimeException("Tenant 'sonder' not found."); } $catalogPath = storage_path('public/catalog'); if (! is_dir($catalogPath)) { throw new RuntimeException("Catalog directory not found at path: {$catalogPath}"); } $files = collect(File::files($catalogPath)) ->filter(fn (\SplFileInfo $file) => ! in_array($file->getFilename(), self::IGNORED_FILES, true)) ->values(); if ($files->isEmpty()) { throw new RuntimeException("Catalog directory is empty at path: {$catalogPath}"); } $groupedFiles = $files->groupBy(function (\SplFileInfo $file): string { return $this->normalizeGroupKey($file->getBasename('.'.$file->getExtension())); }); $productsToSeed = collect(self::PRODUCT_CATALOG) ->map(function (array $pricing, string $groupKey) use ($groupedFiles, $tenant): array { $filesForGroup = $groupedFiles->get($groupKey); if ($filesForGroup === null || $filesForGroup->isEmpty()) { throw new RuntimeException("No images found for catalog group '{$groupKey}'."); } $metadata = $this->parseGroupKey($groupKey); return [ 'group_key' => $groupKey, 'pricing' => $pricing, 'tenant' => $tenant, 'files' => $filesForGroup->all(), 'metadata' => $metadata, ]; }) ->values(); $existingProducts = Product::query() ->where('tenant_codigo', $tenant->codigo) ->whereIn('slug', $productsToSeed->pluck('metadata.product_slug')) ->with(['attachments', 'variants.attachments']) ->get(); foreach ($existingProducts as $existingProduct) { $this->productService->delete($existingProduct); } foreach ($productsToSeed as $catalogProduct) { $this->seedProduct( $tenant, $catalogProduct['metadata'], $catalogProduct['pricing'], $catalogProduct['files'], ); } } /** * @param array{attribute_codes: array, brand_name: string|null, category_name: string, color: string|null, description: string, product_name: string, product_slug: string, type: string} $metadata * @param array{price: int, stock: int} $pricing * @param array $files */ private function seedProduct(Tenant $tenant, array $metadata, array $pricing, array $files): void { $brand = null; if ($metadata['brand_name'] !== null) { $brand = Brand::query() ->where('tenant_codigo', $tenant->codigo) ->where('nombre', $metadata['brand_name']) ->first(); if (! $brand instanceof Brand) { throw new RuntimeException("Brand '{$metadata['brand_name']}' not found for tenant '{$tenant->codigo}'."); } } $category = Category::query() ->where('nombre', $metadata['category_name']) ->whereNull('tenant_code') ->first(); if (! $category instanceof Category) { throw new RuntimeException("Category '{$metadata['category_name']}' not found."); } $attributeIds = Attribute::query() ->where('tenant_codigo', $tenant->codigo) ->whereIn('codigo', $metadata['attribute_codes']) ->pluck('id', 'codigo'); if ($attributeIds->count() !== count($metadata['attribute_codes'])) { throw new RuntimeException("Missing attributes for product '{$metadata['product_slug']}'."); } $product = $this->productService->create($tenant, [ 'categoria_id' => $category->id, 'brand_id' => $brand?->id, 'slug' => $metadata['product_slug'], 'nombre' => $metadata['product_name'], 'descripcion' => $metadata['description'], 'precio' => $pricing['price'], 'attribute_ids' => array_values($attributeIds->all()), ]); $sizes = $metadata['type'] === 'zapatillas' ? self::SHOE_SIZES : self::CLOTHING_SIZES; $images = array_map( fn (\SplFileInfo $file): UploadedFile => $this->createUploadedFile($file->getPathname()), $files ); foreach ($sizes as $size) { $definitions = []; if (isset($attributeIds['color']) && $metadata['color'] !== null) { $definitions[] = [ 'attribute_id' => $attributeIds['color'], 'value' => $metadata['color'], ]; } $sizeAttributeCode = $metadata['type'] === 'zapatillas' ? 'talle_numerico' : 'talle'; $definitions[] = [ 'attribute_id' => $attributeIds[$sizeAttributeCode], 'value' => $size, ]; $this->productService->createVariant($product, [ 'slug' => $metadata['product_slug'].'-'.Str::slug((string) $size), 'nombre' => $metadata['product_name'].' - Talle '.$size, 'stock' => $pricing['stock'], 'descripcion' => $metadata['description'], 'precio' => $pricing['price'], 'definitions' => $definitions, 'images' => $images, ]); } } /** * @return array{attribute_codes: array, brand_name: string|null, category_name: string, color: string|null, description: string, product_name: string, product_slug: string, type: string} */ private function parseGroupKey(string $groupKey): array { if ($groupKey === 'camiseta_rosario_central') { return [ 'type' => 'camiseta', 'category_name' => 'Remeras', 'brand_name' => 'Rosario Central', 'color' => null, 'attribute_codes' => ['talle'], 'product_slug' => $groupKey, 'product_name' => 'Camiseta Rosario Central', 'description' => 'Camiseta Rosario Central con variantes por talle.', ]; } $segments = explode('_', $groupKey); $type = array_shift($segments); if (! is_string($type) || $type === '') { throw new RuntimeException("Unable to infer product type from '{$groupKey}'."); } $color = null; $brandSegments = $segments; if (isset($segments[0]) && in_array($segments[0], ['blanco', 'blanca', 'negro', 'negra', 'gris'], true)) { $color = $this->normalizeColor($segments[0]); $brandSegments = array_slice($segments, 1); } $brandKey = implode('_', $brandSegments); $brandName = $brandKey !== '' ? $this->normalizeBrandName($brandKey) : null; $categoryName = $this->resolveCategoryName($type); $productName = $this->buildProductName($type, $color, $brandName); $description = $this->buildDescription($type, $color, $brandName); $attributeCodes = $type === 'zapatillas' ? ['talle_numerico'] : ['talle']; if ($color !== null) { array_unshift($attributeCodes, 'color'); } return [ 'type' => $type, 'category_name' => $categoryName, 'brand_name' => $brandName, 'color' => $color, 'attribute_codes' => $attributeCodes, 'product_slug' => $groupKey, 'product_name' => $productName, 'description' => $description, ]; } private function normalizeGroupKey(string $basename): string { $normalized = Str::of($basename) ->lower() ->replace('-', '_') ->replace(' ', '_') ->replaceMatches('/_\d+$/', '') ->value(); return str_replace('nije', 'nike', $normalized); } private function normalizeColor(string $color): string { return match ($color) { 'blanco', 'blanca' => 'Blanco', 'negro', 'negra' => 'Negro', 'gris' => 'Gris', default => throw new RuntimeException("Unsupported color '{$color}'."), }; } private function normalizeBrandName(string $brandKey): string { return match ($brandKey) { 'adidas' => 'Adidas', 'nike' => 'Nike', 'under_armour' => 'Under Armour', 'topper' => 'Topper', 'lecoq' => 'Le Coq', default => throw new RuntimeException("Unsupported brand '{$brandKey}'."), }; } private function resolveCategoryName(string $type): string { return match ($type) { 'buzo' => 'Buzos', 'remera', 'camiseta' => 'Remeras', 'pantalon' => 'Pantalones', 'zapatillas' => 'Zapatillas', default => throw new RuntimeException("Unsupported product type '{$type}'."), }; } private function buildProductName(string $type, ?string $color, ?string $brandName): string { $typeLabel = match ($type) { 'buzo' => 'Buzo', 'remera' => 'Remera', 'camiseta' => 'Camiseta', 'pantalon' => 'Pantalon', 'zapatillas' => 'Zapatillas', default => Str::headline($type), }; return trim(collect([$typeLabel, $color, $brandName])->filter()->implode(' ')); } private function buildDescription(string $type, ?string $color, ?string $brandName): string { $category = Str::lower($this->resolveCategoryName($type)); $colorText = $color !== null ? " color {$color}" : ''; $brandText = $brandName !== null ? " {$brandName}" : ''; return "Producto seed de {$category}{$brandText}{$colorText} con variantes por talle."; } private function createUploadedFile(string $path): UploadedFile { $mimeType = mime_content_type($path); return new UploadedFile( $path, basename($path), is_string($mimeType) ? $mimeType : null, null, true ); } }