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:
@@ -0,0 +1,356 @@
|
||||
<?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-v1/';
|
||||
|
||||
private const BRAND_MARKER = 'Provisioned by Mutual SMEP catalog migration v1.';
|
||||
|
||||
private const FEATURED_GROUP_CODE = 'productos-smep';
|
||||
|
||||
/** @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']);
|
||||
}
|
||||
}
|
||||
|
||||
DB::table('featured_groups')->insert([
|
||||
'tenant_code' => self::TENANT_CODE,
|
||||
'code' => self::FEATURED_GROUP_CODE,
|
||||
'source_type' => 'all',
|
||||
'category_id' => null,
|
||||
'product_layout' => 'column_with_image',
|
||||
'group_layout' => 'paginated',
|
||||
'group_name' => 'Productos',
|
||||
'group_order' => 1,
|
||||
]);
|
||||
});
|
||||
} 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('path', 'like', self::STORAGE_PREFIX.'%')
|
||||
->pluck('path')
|
||||
->all();
|
||||
|
||||
DB::transaction(function () use (
|
||||
$catalogItemIds,
|
||||
$variantInventoryIds,
|
||||
$itemInventoryIds,
|
||||
$attachmentIds,
|
||||
): void {
|
||||
DB::table('featured_groups')
|
||||
->where('tenant_code', self::TENANT_CODE)
|
||||
->where('code', self::FEATURED_GROUP_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,
|
||||
]);
|
||||
}
|
||||
|
||||
/** @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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user