Merge branch 'tenant/smep' into homo

This commit is contained in:
2026-09-28 19:27:39 +00:00
541 changed files with 6294 additions and 9 deletions

View File

@@ -0,0 +1,326 @@
<?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 CLIENT_CODE = 'mutual_smep';
private const TENANT_CODE = 'mutual_smep';
private const EXCLUDED_MENU_CODES = [
'account.tickets',
'adminapp.tickets',
'adminapp.fiesta-futbol-infantil.entradas',
'adminapp.fiesta-futbol-infantil.alojamientos',
'adminapp.fiesta-futbol-infantil.merchandising',
'adminapp.fiesta-futbol-infantil.comida',
'adminapp.desfile.entradas',
'event.index',
'event.category',
'event.detail',
];
/** @var list<string> */
private array $storedPaths = [];
public function up(): void
{
if (DB::table('tenants')->where('codigo', self::TENANT_CODE)->exists()) {
return;
}
if (
! DB::table('admin_website_types')->where('codigo', 'shopit')->exists()
|| ! DB::table('storefront_website_types')->where('codigo', 'shopit')->exists()
) {
// Fresh installations provision website-type reference data separately.
return;
}
try {
DB::transaction(function (): void {
$clientId = $this->clientId();
$headerLogoId = $this->storeImage(
'images/tennants/mutual_smep/mutual_smep_header.png',
'mutual_smep_header.png',
);
$footerLogoId = $this->storeImage(
'images/tennants/mutual_smep/mutual_smep_footer.png',
'mutual_smep_footer.png',
);
$faviconId = $this->storeImage(
'images/tennants/mutual_smep/mutual_smep_favicon.png',
'mutual_smep_favicon.png',
);
$now = now();
DB::table('tenants')->insert([
'client_id' => $clientId,
'codigo' => self::TENANT_CODE,
'nombre' => 'Mutual SMEP',
'timezone' => 'America/Argentina/Buenos_Aires',
'dominio' => 'mutual-smep.localhost',
'base_path' => '/',
'site_title' => 'Tienda Mutual SMEP',
'address' => 'San Lorenzo 1543, Rosario, Santa Fe',
'phone' => '+54 9 341 247-4530',
'primary_color' => '#4A7FF5',
'secondary_color' => '#0051A4',
'danger_color' => '#FF8888',
'success_color' => '#198754',
'header_bg_color' => '#FFFFFF',
'footer_bg_color' => '#0051A4',
'header_logo_id' => $headerLogoId,
'footer_logo_id' => $footerLogoId,
'favicon_id' => $faviconId,
'admin_website_type_code' => 'shopit',
'storefront_website_type_code' => 'shopit',
'display_categories' => true,
'display_seach_bar' => true,
'display_cart' => true,
'cart_editing_policy' => 'quantity_and_remove',
'checkout_editing_policy' => 'disabled',
'created_at' => $now,
'updated_at' => $now,
]);
$this->assignSocialMedia($now);
$this->assignMenus($now);
});
} catch (Throwable $throwable) {
Storage::disk('s3')->delete($this->storedPaths);
throw $throwable;
}
}
public function down(): void
{
$tenant = DB::table('tenants')
->where('codigo', self::TENANT_CODE)
->first(['id', 'client_id', 'header_logo_id', 'footer_logo_id', 'favicon_id']);
if ($tenant === null) {
return;
}
$attachmentIds = collect([
$tenant->header_logo_id,
$tenant->footer_logo_id,
$tenant->favicon_id,
])->filter()->unique()->values();
$storedPaths = DB::table('attachments')
->whereIn('id', $attachmentIds)
->pluck('path')
->all();
DB::transaction(function () use ($tenant, $attachmentIds): void {
DB::table('tenant_social_media')
->where('tenant_code', self::TENANT_CODE)
->delete();
DB::table('tenants_menues')
->where('tenant_code', self::TENANT_CODE)
->delete();
DB::table('tenants')
->where('id', $tenant->id)
->delete();
DB::table('attachments')
->whereIn('id', $attachmentIds)
->delete();
$clientHasTenants = DB::table('tenants')
->where('client_id', $tenant->client_id)
->exists();
if (! $clientHasTenants) {
DB::table('clients')
->where('id', $tenant->client_id)
->where('code', self::CLIENT_CODE)
->delete();
}
});
Storage::disk('s3')->delete($storedPaths);
}
private function clientId(): int
{
$clientId = DB::table('clients')->where('code', self::CLIENT_CODE)->value('id');
if ($clientId !== null) {
DB::table('clients')->where('id', $clientId)->update([
'name' => 'Mutual SMEP',
'updated_at' => now(),
]);
return (int) $clientId;
}
return (int) DB::table('clients')->insertGetId([
'code' => self::CLIENT_CODE,
'name' => 'Mutual SMEP',
'created_at' => now(),
'updated_at' => now(),
]);
}
private function assignSocialMedia(DateTimeInterface $now): void
{
$socialMedia = [
[
'code' => 'instagram',
'icon' => 'fa-brands fa-instagram',
'name' => 'Instagram',
'url' => 'https://www.instagram.com/mutualsmep/',
'orden' => 0,
],
[
'code' => 'facebook',
'icon' => 'fa-brands fa-facebook',
'name' => 'Facebook',
'url' => 'https://www.facebook.com/smeprosario',
'orden' => 1,
],
[
'code' => 'whatsapp',
'icon' => 'fa-brands fa-whatsapp',
'name' => 'WhatsApp',
'url' => 'https://wa.me/5493412474530',
'orden' => 2,
],
];
foreach ($socialMedia as $network) {
DB::table('social_media')->insertOrIgnore([
'code' => $network['code'],
'icon' => $network['icon'],
'name' => $network['name'],
'created_at' => $now,
'updated_at' => $now,
]);
DB::table('tenant_social_media')->insertOrIgnore([
'tenant_code' => self::TENANT_CODE,
'social_media_code' => $network['code'],
'url' => $network['url'],
'orden' => $network['orden'],
'created_at' => $now,
'updated_at' => $now,
]);
}
}
private function assignMenus(DateTimeInterface $now): void
{
DB::table('menues')
->whereNotIn('code', self::EXCLUDED_MENU_CODES)
->pluck('code')
->each(function (string $menuCode) use ($now): void {
$staticContent = match ($menuCode) {
'help.faq' => $this->frequentlyAskedQuestions(),
'help.contact' => $this->contactContent(),
default => null,
};
DB::table('tenants_menues')->updateOrInsert(
[
'tenant_code' => self::TENANT_CODE,
'menu_code' => $menuCode,
],
[
'static_content' => $staticContent === null
? null
: json_encode($staticContent, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'created_at' => $now,
'updated_at' => $now,
],
);
});
}
/** @return list<array{pregunta: string, respuesta: string, is_active: bool}> */
private function frequentlyAskedQuestions(): array
{
return [
[
'pregunta' => '¿Hay algún límite de compra?',
'respuesta' => 'La cantidad disponible depende del stock de cada producto.',
'is_active' => true,
],
[
'pregunta' => '¿Cuáles son los medios de pago disponibles?',
'respuesta' => 'Podés consultar y seleccionar los medios de pago habilitados al finalizar tu compra.',
'is_active' => false,
],
[
'pregunta' => '¿Cómo puedo recibir asesoramiento antes de comprar?',
'respuesta' => 'Podés comunicarte con Mutual SMEP por WhatsApp al +54 9 341 247-4530.',
'is_active' => false,
],
];
}
/** @return array<string, mixed> */
private function contactContent(): array
{
return [
'whatsapp' => [
'whatsapp_url' => 'https://wa.me/5493412474530',
'whatsapp_label' => 'Chateá con Mutual SMEP',
],
'phone' => '+54 9 341 247-4530',
'locations' => [
'rosario' => [
'label' => 'Rosario',
'addresses' => [
[
'label' => 'Mutual SMEP',
'address' => 'San Lorenzo 1543, Rosario, Santa Fe',
'coordinates' => [-32.9431184, -60.6437991],
],
],
],
],
];
}
private function storeImage(string $relativePath, string $filename): int
{
$sourcePath = public_path($relativePath);
if (! is_file($sourcePath)) {
throw new RuntimeException("Image not found at path: {$sourcePath}");
}
$contents = file_get_contents($sourcePath);
if ($contents === false) {
throw new RuntimeException("Could not read image at path: {$sourcePath}");
}
$key = (string) Str::uuid();
$storedPath = 'tenants/'.self::TENANT_CODE."/{$key}.png";
if (! Storage::disk('s3')->put($storedPath, $contents)) {
throw new RuntimeException("Could not store image at path: {$storedPath}");
}
$this->storedPaths[] = $storedPath;
return DB::table('attachments')->insertGetId([
'key' => $key,
'path' => $storedPath,
'filename' => $filename,
'type' => 'image',
'mime_type' => 'image/png',
'extension' => 'png',
'size' => strlen($contents),
'created_at' => now(),
'updated_at' => now(),
]);
}
};

View File

@@ -0,0 +1,141 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
private const TENANT_CODE = 'mutual_smep';
/** @var array<string, list<string>> */
private const CATEGORIES = [
'Dormitorio y Blanco' => [
'Colchones',
'Bases y Sommiers',
'Acolchados y Edredones',
'Frazadas y Mantas',
'Infantil y Cuna',
],
'Electrodomésticos' => [
'Heladeras',
'Lavarropas y Secarropas',
'Cocinas',
'Microondas',
'Hornos Eléctricos',
'Calefones y Termotanques',
'Purificadores y Extractores de Cocina',
'Preparación de Alimentos',
'Café y Desayuno',
'Freidoras y Cocción',
'Cuidado Personal',
'Limpieza y Planchado',
],
'Climatización' => [
'Aires Acondicionados',
'Calefactores a Gas con Salida',
'Calefactores a Gas sin Salida',
'Calefacción Eléctrica',
],
'Tecnología' => [
'Celulares',
'Notebooks',
'Smart TV',
'Audio',
'Accesorios de Computación',
'Soportes para TV',
],
'Bicicletas y Aire Libre' => [
'Bicicletas Infantiles',
'Bicicletas para Adultos',
'Piletas',
],
'Bazar' => [
'Termos',
],
];
public function up(): void
{
if (! DB::table('tenants')->where('codigo', self::TENANT_CODE)->exists()) {
return;
}
DB::transaction(function (): void {
foreach (self::CATEGORIES as $parentName => $subcategoryNames) {
$parentId = $this->upsertCategory($parentName, null);
foreach ($subcategoryNames as $subcategoryName) {
$this->upsertCategory($subcategoryName, $parentId);
}
}
});
}
public function down(): void
{
DB::transaction(function (): void {
foreach (self::CATEGORIES as $parentName => $subcategoryNames) {
$parentId = DB::table('categorias')
->where('tenant_code', self::TENANT_CODE)
->where('nombre', $parentName)
->whereNull('categoria_id')
->value('id');
if ($parentId === null) {
continue;
}
$subcategoryIds = DB::table('categorias')
->where('tenant_code', self::TENANT_CODE)
->where('categoria_id', $parentId)
->whereIn('nombre', $subcategoryNames)
->pluck('id');
foreach ($subcategoryIds as $subcategoryId) {
$hasChildren = DB::table('categorias')
->where('categoria_id', $subcategoryId)
->exists();
if (! $hasChildren) {
DB::table('categorias')->where('id', $subcategoryId)->delete();
}
}
$hasChildren = DB::table('categorias')
->where('categoria_id', $parentId)
->exists();
if (! $hasChildren) {
DB::table('categorias')->where('id', $parentId)->delete();
}
}
});
}
private function upsertCategory(string $name, ?int $parentId): int
{
$categoryId = DB::table('categorias')
->where('tenant_code', self::TENANT_CODE)
->where('nombre', $name)
->value('id');
if ($categoryId !== null) {
DB::table('categorias')->where('id', $categoryId)->update([
'categoria_id' => $parentId,
'is_enabled' => true,
'updated_at' => now(),
]);
return (int) $categoryId;
}
return (int) DB::table('categorias')->insertGetId([
'tenant_code' => self::TENANT_CODE,
'categoria_id' => $parentId,
'nombre' => $name,
'is_enabled' => true,
'created_at' => now(),
'updated_at' => now(),
]);
}
};

View File

@@ -0,0 +1,139 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
private const TENANT_CODE = 'mutual_smep';
/** @var list<array<string, mixed>> */
private const ATTRIBUTES = [
[
'codigo' => 'color',
'nombre' => 'Color',
'metadata_schema' => [
'hex' => ['type' => 'string'],
],
'options' => [
['value' => 'Negro', 'label' => 'Negro', 'metadata' => ['hex' => '#000000']],
['value' => 'Blanco', 'label' => 'Blanco', 'metadata' => ['hex' => '#FFFFFF']],
['value' => 'Gris', 'label' => 'Gris', 'metadata' => ['hex' => '#808080']],
['value' => 'Plata', 'label' => 'Plata', 'metadata' => ['hex' => '#C0C0C0']],
['value' => 'Azul', 'label' => 'Azul', 'metadata' => ['hex' => '#0000FF']],
['value' => 'Rojo', 'label' => 'Rojo', 'metadata' => ['hex' => '#DC3545']],
['value' => 'Rosa', 'label' => 'Rosa', 'metadata' => ['hex' => '#FFC0CB']],
['value' => 'Multicolor', 'label' => 'Multicolor'],
],
],
[
'codigo' => 'medida_cama',
'nombre' => 'Medida',
'options' => [
['value' => 'Cuna', 'label' => 'Cuna'],
['value' => '1 Plaza', 'label' => '1 Plaza'],
['value' => '1 1/2 Plazas', 'label' => '1 1/2 Plazas'],
['value' => '2 Plazas', 'label' => '2 Plazas'],
['value' => '2 1/2 Plazas', 'label' => '2 1/2 Plazas'],
['value' => 'Queen', 'label' => 'Queen'],
['value' => 'King', 'label' => 'King'],
],
],
[
'codigo' => 'almacenamiento',
'nombre' => 'Almacenamiento',
'options' => [
['value' => '128 GB', 'label' => '128 GB'],
['value' => '256 GB', 'label' => '256 GB'],
['value' => '512 GB', 'label' => '512 GB'],
['value' => '1 TB', 'label' => '1 TB'],
],
],
[
'codigo' => 'memoria_ram',
'nombre' => 'Memoria RAM',
'options' => [
['value' => '4 GB', 'label' => '4 GB'],
['value' => '8 GB', 'label' => '8 GB'],
['value' => '16 GB', 'label' => '16 GB'],
],
],
[
'codigo' => 'rodado',
'nombre' => 'Rodado',
'options' => [
['value' => '16', 'label' => '16'],
['value' => '26', 'label' => '26'],
['value' => '29', 'label' => '29'],
],
],
];
public function up(): void
{
if (! DB::table('tenants')->where('codigo', self::TENANT_CODE)->exists()) {
return;
}
DB::transaction(function (): void {
foreach (self::ATTRIBUTES as $definition) {
$attributeId = $this->upsertAttribute($definition);
DB::table('attribute_options')->where('attribute_id', $attributeId)->delete();
foreach ($definition['options'] as $index => $option) {
DB::table('attribute_options')->insert([
'attribute_id' => $attributeId,
'value' => $option['value'],
'label' => $option['label'],
'sort_order' => $index + 1,
'metadata' => isset($option['metadata'])
? json_encode($option['metadata'], JSON_THROW_ON_ERROR)
: null,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
});
}
public function down(): void
{
DB::table('attribute')
->where('tenant_codigo', self::TENANT_CODE)
->whereIn('codigo', array_column(self::ATTRIBUTES, 'codigo'))
->delete();
}
/** @param array<string, mixed> $definition */
private function upsertAttribute(array $definition): int
{
$attributeId = DB::table('attribute')
->where('tenant_codigo', self::TENANT_CODE)
->where('codigo', $definition['codigo'])
->value('id');
$values = [
'nombre' => $definition['nombre'],
'is_required' => false,
'metadata_schema' => isset($definition['metadata_schema'])
? json_encode($definition['metadata_schema'], JSON_THROW_ON_ERROR)
: null,
'type' => 'select',
'updated_at' => now(),
];
if ($attributeId !== null) {
DB::table('attribute')->where('id', $attributeId)->update($values);
return (int) $attributeId;
}
return (int) DB::table('attribute')->insertGetId([
'tenant_codigo' => self::TENANT_CODE,
'codigo' => $definition['codigo'],
...$values,
'created_at' => now(),
]);
}
};

View File

@@ -0,0 +1,462 @@
<?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-v2/';
private const LEGACY_STORAGE_PREFIX = 'tenants/mutual_smep/catalog-v1/';
private const BRAND_MARKER = 'Provisioned by Mutual SMEP catalog migration v1.';
/** @var list<array{code: string, category: string}> */
private const FEATURED_GROUPS = [
['code' => 'smep-dormitorio-y-blanco', 'category' => 'Dormitorio y Blanco'],
['code' => 'smep-electrodomesticos', 'category' => 'Electrodomésticos'],
['code' => 'smep-climatizacion', 'category' => 'Climatización'],
['code' => 'smep-tecnologia', 'category' => 'Tecnología'],
['code' => 'smep-bicicletas-y-aire-libre', 'category' => 'Bicicletas y Aire Libre'],
['code' => 'smep-bazar', 'category' => 'Bazar'],
];
private const HIGHLIGHTED_GROUP_CODE = 'productos-destacados-smep';
private const CATEGORY_FEATURED_ITEM_LIMIT = 6;
/** @var list<string> */
private const HIGHLIGHTED_PRODUCT_SLUGS = [
'acolchado-kavanagh-simil-plumon-reversible',
'acolchado-sense-duo-bitono-corderito',
'colchon-inducol-constanza',
'edredon-lisboa-jean-cartier',
'frazada-kavanagh-premium-soft',
'kit-edredon-alaska-corderito',
'motorola-g15',
'smart-tv-noblex-50-dr50-x8500-google-tv',
'heladera-gafa-hgf-368afp-330-lt-color-plata',
'cafetera-express-atma-ceat-5418p-1-litro',
'split-3300-w-tcl-taca-3300fcsa-frio-calor',
'bicicleta-venzo-loki-r29-fd-21-v-shimano',
'termo-stanley-1-l-adventure-go-to-con-tapon',
];
/** @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']);
}
}
$this->createFeaturedGroups();
});
} 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(fn ($query) => $query
->where('path', 'like', self::STORAGE_PREFIX.'%')
->orWhere('path', 'like', self::LEGACY_STORAGE_PREFIX.'%'))
->pluck('path')
->all();
DB::transaction(function () use (
$catalogItemIds,
$variantInventoryIds,
$itemInventoryIds,
$attachmentIds,
): void {
DB::table('featured_groups')
->where('tenant_code', self::TENANT_CODE)
->whereIn('code', [
self::HIGHLIGHTED_GROUP_CODE,
...array_column(self::FEATURED_GROUPS, '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,
]);
}
private function createFeaturedGroups(): void
{
$this->createHighlightedGroup();
foreach (self::FEATURED_GROUPS as $order => $group) {
$categoryId = DB::table('categorias')
->where('tenant_code', self::TENANT_CODE)
->whereNull('categoria_id')
->where('nombre', $group['category'])
->value('id');
if ($categoryId === null) {
throw new RuntimeException("Parent category '{$group['category']}' not found for Mutual SMEP.");
}
$featuredGroupId = DB::table('featured_groups')->insertGetId([
'tenant_code' => self::TENANT_CODE,
'code' => $group['code'],
'source_type' => 'manual',
'category_id' => $categoryId,
'product_layout' => 'column_with_image',
'group_layout' => 'carousel',
'group_name' => $group['category'],
'group_order' => $order + 2,
]);
$catalogItemIds = DB::table('catalog_items')
->join('categorias', 'categorias.id', '=', 'catalog_items.category_id')
->where('catalog_items.tenant_code', self::TENANT_CODE)
->where('categorias.tenant_code', self::TENANT_CODE)
->where('categorias.categoria_id', $categoryId)
->orderBy('catalog_items.group_order')
->orderBy('catalog_items.id')
->limit(self::CATEGORY_FEATURED_ITEM_LIMIT)
->pluck('catalog_items.id');
foreach ($catalogItemIds as $itemOrder => $catalogItemId) {
DB::table('featured_items')->insert([
'featured_group_id' => $featuredGroupId,
'catalog_item_id' => $catalogItemId,
'order' => $itemOrder + 1,
]);
}
}
}
private function createHighlightedGroup(): void
{
$featuredGroupId = DB::table('featured_groups')->insertGetId([
'tenant_code' => self::TENANT_CODE,
'code' => self::HIGHLIGHTED_GROUP_CODE,
'source_type' => 'manual',
'category_id' => null,
'product_layout' => 'column_with_image',
'group_layout' => 'carousel',
'group_name' => 'Productos destacados',
'group_order' => 1,
]);
$catalogItemIds = DB::table('catalog_items')
->where('tenant_code', self::TENANT_CODE)
->whereIn('slug', self::HIGHLIGHTED_PRODUCT_SLUGS)
->pluck('id', 'slug');
foreach (self::HIGHLIGHTED_PRODUCT_SLUGS as $order => $slug) {
$catalogItemId = $catalogItemIds->get($slug);
if ($catalogItemId === null) {
throw new RuntimeException("Highlighted product '{$slug}' not found for Mutual SMEP.");
}
DB::table('featured_items')->insert([
'featured_group_id' => $featuredGroupId,
'catalog_item_id' => $catalogItemId,
'order' => $order + 1,
]);
}
}
/** @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,
]);
}
}
};

View File

@@ -0,0 +1,140 @@
<?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 EXTRA_CODE = 'carousel';
private const IMAGE_DIRECTORY = 'images/tennants/mutual_smep/carousel';
private const STORAGE_PREFIX = 'tenants/mutual_smep/extras/carousel-v1/';
/** @var list<string> */
private const IMAGES = [
'01-tecnologia.webp',
'02-hogar.webp',
'03-vida-activa.webp',
'04-descanso.webp',
];
/** @var list<string> */
private array $storedPaths = [];
public function up(): void
{
$tenant = DB::table('tenants')
->where('codigo', self::TENANT_CODE)
->first(['codigo', 'storefront_website_type_code']);
if ($tenant === null) {
return;
}
$extraId = DB::table('storefront_website_type_extras')
->where('storefront_website_type_code', $tenant->storefront_website_type_code)
->where('codigo', self::EXTRA_CODE)
->value('id');
if ($extraId === null || DB::table('websites_extras')->where([
'website_code' => self::TENANT_CODE,
'website_type_extra_id' => $extraId,
])->exists()) {
return;
}
try {
DB::transaction(function () use ($extraId): void {
$attachmentIds = [];
foreach (self::IMAGES as $filename) {
$attachmentIds[] = $this->storeImage($filename);
}
DB::table('websites_extras')->insert([
'website_code' => self::TENANT_CODE,
'website_type_extra_id' => $extraId,
'config' => json_encode($attachmentIds, JSON_THROW_ON_ERROR),
'is_enabled' => true,
'created_at' => now(),
'updated_at' => now(),
]);
});
} catch (Throwable $throwable) {
Storage::disk('s3')->delete($this->storedPaths);
throw $throwable;
}
}
public function down(): void
{
$attachments = DB::table('attachments')
->where('path', 'like', self::STORAGE_PREFIX.'%')
->get(['id', 'path']);
if ($attachments->isEmpty()) {
return;
}
$attachmentIds = $attachments->pluck('id')->map(fn ($id): int => (int) $id)->all();
$extraId = DB::table('storefront_website_type_extras')
->where('storefront_website_type_code', 'shopit')
->where('codigo', self::EXTRA_CODE)
->value('id');
DB::transaction(function () use ($attachmentIds, $extraId): void {
if ($extraId !== null) {
DB::table('websites_extras')
->where('website_code', self::TENANT_CODE)
->where('website_type_extra_id', $extraId)
->where('config', json_encode($attachmentIds, JSON_THROW_ON_ERROR))
->delete();
}
DB::table('attachments')->whereIn('id', $attachmentIds)->delete();
});
Storage::disk('s3')->delete($attachments->pluck('path')->all());
}
private function storeImage(string $filename): int
{
$sourcePath = public_path(self::IMAGE_DIRECTORY."/{$filename}");
if (! is_file($sourcePath)) {
throw new RuntimeException("Carousel image not found: {$sourcePath}");
}
$contents = file_get_contents($sourcePath);
if ($contents === false) {
throw new RuntimeException("Carousel image could not be read: {$sourcePath}");
}
$storedPath = self::STORAGE_PREFIX.$filename;
if (! Storage::disk('s3')->put($storedPath, $contents)) {
throw new RuntimeException("Carousel image could not be stored: {$storedPath}");
}
$this->storedPaths[] = $storedPath;
return (int) DB::table('attachments')->insertGetId([
'key' => (string) Str::uuid(),
'path' => $storedPath,
'filename' => $filename,
'type' => 'image',
'mime_type' => 'image/webp',
'extension' => 'webp',
'size' => strlen($contents),
'created_at' => now(),
'updated_at' => now(),
]);
}
};

View File

@@ -0,0 +1,88 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
private const TENANT_CODE = 'mutual_smep';
private const ITEM_LIMIT = 6;
/** @var array<string, string> */
private const GROUPS = [
'smep-dormitorio-y-blanco' => 'Dormitorio y Blanco',
'smep-electrodomesticos' => 'Electrodomésticos',
'smep-climatizacion' => 'Climatización',
'smep-tecnologia' => 'Tecnología',
'smep-bicicletas-y-aire-libre' => 'Bicicletas y Aire Libre',
'smep-bazar' => 'Bazar',
];
public function up(): void
{
foreach (array_keys(self::GROUPS) as $code) {
$groupId = DB::table('featured_groups')
->where('tenant_code', self::TENANT_CODE)
->where('code', $code)
->value('id');
if ($groupId === null) {
continue;
}
$keptItemIds = DB::table('featured_items')
->where('featured_group_id', $groupId)
->orderBy('order')
->orderBy('id')
->limit(self::ITEM_LIMIT)
->pluck('id');
DB::table('featured_items')
->where('featured_group_id', $groupId)
->when(
$keptItemIds->isNotEmpty(),
fn ($query) => $query->whereNotIn('id', $keptItemIds),
)
->delete();
}
}
public function down(): void
{
foreach (self::GROUPS as $code => $parentCategoryName) {
$groupId = DB::table('featured_groups')
->where('tenant_code', self::TENANT_CODE)
->where('code', $code)
->value('id');
$parentCategoryId = DB::table('categorias')
->where('tenant_code', self::TENANT_CODE)
->whereNull('categoria_id')
->where('nombre', $parentCategoryName)
->value('id');
if ($groupId === null || $parentCategoryId === null) {
continue;
}
$catalogItemIds = DB::table('catalog_items')
->join('categorias', 'categorias.id', '=', 'catalog_items.category_id')
->where('catalog_items.tenant_code', self::TENANT_CODE)
->where('categorias.tenant_code', self::TENANT_CODE)
->where('categorias.categoria_id', $parentCategoryId)
->orderBy('catalog_items.group_order')
->orderBy('catalog_items.id')
->pluck('catalog_items.id');
DB::table('featured_items')->where('featured_group_id', $groupId)->delete();
foreach ($catalogItemIds as $order => $catalogItemId) {
DB::table('featured_items')->insert([
'featured_group_id' => $groupId,
'catalog_item_id' => $catalogItemId,
'order' => $order + 1,
]);
}
}
}
};

View File

@@ -0,0 +1,112 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Collection;
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 LEGACY_PREFIX = 'tenants/mutual_smep/catalog-v1/';
private const OPTIMIZED_PREFIX = 'tenants/mutual_smep/catalog-v2/';
private const LOCAL_PREFIX = 'images/tennants/mutual_smep/catalog/';
public function up(): void
{
$attachments = $this->attachments(self::LEGACY_PREFIX);
$uploadedPaths = [];
try {
foreach ($attachments as $attachment) {
$relativePath = Str::after($attachment->path, self::LEGACY_PREFIX);
$localPath = public_path(self::LOCAL_PREFIX.$relativePath);
if (! is_file($localPath)) {
throw new RuntimeException("Optimized catalog image not found: {$localPath}");
}
$contents = file_get_contents($localPath);
if ($contents === false) {
throw new RuntimeException("Optimized catalog image could not be read: {$localPath}");
}
$optimizedPath = self::OPTIMIZED_PREFIX.$relativePath;
if (! Storage::disk('s3')->put($optimizedPath, $contents)) {
throw new RuntimeException("Optimized catalog image could not be stored: {$optimizedPath}");
}
$uploadedPaths[] = $optimizedPath;
$attachment->optimized_path = $optimizedPath;
$attachment->optimized_size = strlen($contents);
}
DB::transaction(function () use ($attachments): void {
foreach ($attachments as $attachment) {
DB::table('attachments')->where('id', $attachment->id)->update([
'path' => $attachment->optimized_path,
'size' => $attachment->optimized_size,
'updated_at' => now(),
]);
}
});
} catch (Throwable $throwable) {
Storage::disk('s3')->delete($uploadedPaths);
throw $throwable;
}
}
public function down(): void
{
$attachments = $this->attachments(self::OPTIMIZED_PREFIX)
->filter(function (object $attachment): bool {
$relativePath = Str::after($attachment->path, self::OPTIMIZED_PREFIX);
$legacyPath = self::LEGACY_PREFIX.$relativePath;
if (! Storage::disk('s3')->exists($legacyPath)) {
return false;
}
$attachment->legacy_path = $legacyPath;
$attachment->legacy_size = Storage::disk('s3')->size($legacyPath);
return true;
})
->values();
DB::transaction(function () use ($attachments): void {
foreach ($attachments as $attachment) {
DB::table('attachments')->where('id', $attachment->id)->update([
'path' => $attachment->legacy_path,
'size' => $attachment->legacy_size,
'updated_at' => now(),
]);
}
});
Storage::disk('s3')->delete($attachments->pluck('path')->all());
}
private function attachments(string $prefix): Collection
{
return DB::table('attachments')
->join(
'catalog_items_attachments',
'catalog_items_attachments.attachment_id',
'=',
'attachments.id',
)
->join('catalog_items', 'catalog_items.id', '=', 'catalog_items_attachments.catalog_item_id')
->where('catalog_items.tenant_code', self::TENANT_CODE)
->where('attachments.path', 'like', $prefix.'%')
->select('attachments.id', 'attachments.path', 'attachments.size')
->distinct()
->orderBy('attachments.id')
->get();
}
};