Add new product images and implement SeedMutualSmepCatalogTest for catalog migration

- Added multiple new product images for smart TVs, audio devices, and accessories in WEBP format.
- Created a new test class SeedMutualSmepCatalogTest to validate the catalog migration process.
- Implemented setup and teardown methods to manage database schema and data during tests.
- Verified the integrity of catalog items, inventories, and associated attributes after migration.
This commit is contained in:
2026-09-24 12:41:24 -03:00
parent 16e2ca679c
commit 74b72a7d4e
516 changed files with 3953 additions and 0 deletions

View File

@@ -0,0 +1,308 @@
<?php
declare(strict_types=1);
$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, 1200 / 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, 82)) {
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;