refactor(catalog): Complete catalog refactor to simplify its data model and its querying.

source commits: refactor/catalog
This commit is contained in:
2026-07-21 09:23:19 -03:00
parent d3182fbd13
commit 29a2ca19a3
116 changed files with 5141 additions and 5217 deletions

View File

@@ -1,6 +1,5 @@
<?php
use App\Domains\Catalog\Models\ProductVariant;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
@@ -8,6 +7,8 @@ use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
private const LEGACY_PRODUCT_VARIANT = 'App\\Domains\\Catalog\\Models\\ProductVariant';
/**
* Run the migrations.
*/
@@ -18,7 +19,7 @@ return new class extends Migration
});
DB::table('group_items')->update([
'groupable_type' => ProductVariant::class,
'groupable_type' => self::LEGACY_PRODUCT_VARIANT,
'groupable_id' => DB::raw('product_variant_id'),
]);
@@ -39,7 +40,7 @@ return new class extends Migration
*/
public function down(): void
{
if (DB::table('group_items')->where('groupable_type', '!=', ProductVariant::class)->exists()) {
if (DB::table('group_items')->where('groupable_type', '!=', self::LEGACY_PRODUCT_VARIANT)->exists()) {
throw new RuntimeException('Cannot roll back groupable group items while bundle items exist.');
}

View File

@@ -0,0 +1,50 @@
<?php
use App\Domains\Catalog\Enums\InventoryPolicy;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('inventories', function (Blueprint $table): void {
$table->id();
$table->unsignedBigInteger('sold_units')->default(0);
$table->unsignedInteger('reserved_stock')->default(0);
$table->unsignedInteger('real_stock')->default(0);
});
Schema::create('catalog_items', function (Blueprint $table): void {
$table->id();
$table->string('tenant_code');
$table->foreignId('category_id')->nullable()->constrained('categorias')->nullOnDelete();
$table->foreignId('brand_id')->nullable()->constrained('brands')->nullOnDelete();
$table->foreignId('inventory_id')->nullable()->unique()->constrained('inventories')->restrictOnDelete();
$table->string('slug');
$table->string('nombre');
$table->text('descripcion')->nullable();
$table->decimal('precio', 10, 2);
$table->enum('inventory_policy', InventoryPolicy::values())
->default(InventoryPolicy::Tracked->value);
$table->boolean('has_tickets')->default(false);
$table->dateTime('maximum_use_date')->nullable();
$table->dateTime('minimum_use_date')->nullable();
$table->foreign('tenant_code')
->references('codigo')
->on('tenants')
->cascadeOnUpdate()
->restrictOnDelete();
$table->unique(['tenant_code', 'slug']);
});
}
public function down(): void
{
Schema::dropIfExists('catalog_items');
Schema::dropIfExists('inventories');
}
};

View File

@@ -0,0 +1,193 @@
<?php
use App\Domains\Catalog\Enums\InventoryPolicy;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
$inventoryIds = [];
DB::table('productos_variantes')->orderBy('id')->each(
function (object $variant) use (&$inventoryIds): void {
$inventoryIds[$variant->id] = DB::table('inventories')->insertGetId([
'sold_units' => $variant->cantidad_vendida,
'reserved_stock' => $variant->stock_reservado,
'real_stock' => $variant->stock_real,
]);
}
);
DB::table('productos')->orderBy('id')->each(
function (object $product): void {
$variants = DB::table('productos_variantes')
->where('producto_id', $product->id)
->orderBy('id');
$firstVariant = (clone $variants)->first();
$inventoryId = null;
if ($firstVariant === null) {
$inventoryId = DB::table('inventories')->insertGetId([
'sold_units' => 0,
'reserved_stock' => 0,
'real_stock' => 0,
]);
}
DB::table('catalog_items')->insert([
'id' => $product->id,
'tenant_code' => $product->tenant_codigo,
'category_id' => $product->categoria_id,
'brand_id' => $product->brand_id,
'inventory_id' => $inventoryId,
'slug' => $product->slug,
'nombre' => $product->nombre,
'descripcion' => $product->descripcion,
'precio' => $product->precio,
'inventory_policy' => $firstVariant->inventory_policy
?? InventoryPolicy::Tracked->value,
'has_tickets' => $firstVariant->has_tickets ?? false,
'minimum_use_date' => $firstVariant->minimum_use_date ?? null,
'maximum_use_date' => $firstVariant->maximum_use_date ?? null,
]);
}
);
Schema::table('productos_variantes', function (Blueprint $table): void {
$table->dropForeign(['producto_id']);
$table->foreignId('inventory_id')->nullable()->after('producto_id');
});
foreach ($inventoryIds as $variantId => $inventoryId) {
DB::table('productos_variantes')->where('id', $variantId)->update([
'inventory_id' => $inventoryId,
]);
}
Schema::table('productos_variantes', function (Blueprint $table): void {
$table->renameColumn('producto_id', 'catalog_item_id');
$table->dropColumn([
'inventory_policy',
'stock_real',
'stock_reservado',
'cantidad_vendida',
'is_placeholder',
'has_tickets',
'minimum_use_date',
'maximum_use_date',
'created_at',
'updated_at',
]);
});
Schema::rename('productos_variantes', 'variantes');
Schema::table('variantes', function (Blueprint $table): void {
$table->unsignedBigInteger('inventory_id')->nullable(false)->change();
$table->foreign('catalog_item_id')->references('id')->on('catalog_items')->cascadeOnDelete();
$table->foreign('inventory_id')->references('id')->on('inventories')->restrictOnDelete();
$table->unique('inventory_id');
});
Schema::table('products_attributes', function (Blueprint $table): void {
$table->dropForeign(['product_id']);
$table->renameColumn('product_id', 'catalog_item_id');
});
Schema::rename('products_attributes', 'item_attributes');
Schema::table('item_attributes', function (Blueprint $table): void {
$table->foreign('catalog_item_id')->references('id')->on('catalog_items')->cascadeOnDelete();
});
Schema::table('productos_variantes_values', function (Blueprint $table): void {
$table->dropForeign('productos_variantes_definiciones_producto_variante_id_foreign');
$table->dropForeign(['products_attribute_id']);
$table->dropUnique('prod_var_values_variant_product_attr_unique');
$table->renameColumn('producto_variante_id', 'variant_id');
$table->renameColumn('products_attribute_id', 'item_attribute_id');
});
Schema::rename('productos_variantes_values', 'variant_values');
Schema::table('variant_values', function (Blueprint $table): void {
$table->foreign('variant_id')->references('id')->on('variantes')->cascadeOnDelete();
$table->foreign('item_attribute_id')->references('id')->on('item_attributes')->cascadeOnDelete();
$table->unique(['variant_id', 'item_attribute_id']);
});
Schema::table('productos_attachments', function (Blueprint $table): void {
$table->dropForeign(['producto_id']);
$table->dropUnique('productos_attachments_producto_id_attachment_id_unique');
$table->renameColumn('producto_id', 'catalog_item_id');
});
Schema::rename('productos_attachments', 'catalog_items_attachments');
Schema::table('catalog_items_attachments', function (Blueprint $table): void {
$table->foreignId('variant_id')
->nullable()
->after('id')
->constrained('variantes')
->cascadeOnDelete();
$table->unsignedInteger('orden')->default(0)->after('attachment_id');
$table->foreign('catalog_item_id')->references('id')->on('catalog_items')->cascadeOnDelete();
});
$catalogAttachmentOrder = [];
DB::table('catalog_items_attachments')
->orderBy('catalog_item_id')
->orderBy('id')
->each(function (object $catalogAttachment) use (&$catalogAttachmentOrder): void {
$catalogItemId = $catalogAttachment->catalog_item_id;
$order = $catalogAttachmentOrder[$catalogItemId] ?? 0;
DB::table('catalog_items_attachments')
->where('id', $catalogAttachment->id)
->update(['orden' => $order]);
$catalogAttachmentOrder[$catalogItemId] = $order + 1;
});
$variantAttachmentOrder = [];
DB::table('variantes_attachments')->orderBy('id')->each(
function (object $variantAttachment) use (&$variantAttachmentOrder): void {
$variant = DB::table('variantes')
->select('catalog_item_id')
->where('id', $variantAttachment->variante_id)
->first();
if ($variant === null) {
return;
}
DB::table('catalog_items_attachments')->insert([
'variant_id' => $variantAttachment->variante_id,
'catalog_item_id' => $variant->catalog_item_id,
'attachment_id' => $variantAttachment->attachment_id,
'orden' => $variantAttachmentOrder[$variantAttachment->variante_id] ?? 0,
'created_at' => $variantAttachment->created_at,
'updated_at' => $variantAttachment->updated_at,
]);
$variantAttachmentOrder[$variantAttachment->variante_id] =
($variantAttachmentOrder[$variantAttachment->variante_id] ?? 0) + 1;
}
);
Schema::drop('variantes_attachments');
Schema::table('catalog_items_attachments', function (Blueprint $table): void {
$table->dropColumn(['created_at', 'updated_at']);
$table->unique(
['catalog_item_id', 'variant_id', 'attachment_id'],
'catalog_item_variant_attachment_unique'
);
});
Schema::drop('productos');
}
public function down(): void
{
throw new RuntimeException('This destructive catalog migration cannot be reversed.');
}
};

View File

@@ -0,0 +1,95 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
private const LEGACY_PRODUCT_VARIANT = 'App\\Domains\\Catalog\\Models\\ProductVariant';
private const LEGACY_BUNDLE = 'App\\Domains\\Bundle\\Models\\Bundle';
public function up(): void
{
Schema::table('carrito_items', function (Blueprint $table): void {
$table->foreignId('catalog_item_id')->nullable()->after('cart_id');
$table->foreignId('variant_id')->nullable()->after('catalog_item_id');
});
DB::table('carrito_items')->orderBy('id')->each(function (object $cartItem): void {
if ($cartItem->buyable_type !== self::LEGACY_PRODUCT_VARIANT) {
if ($cartItem->buyable_type === self::LEGACY_BUNDLE) {
DB::table('bundle_items')
->where('bundle_id', $cartItem->buyable_id)
->orderBy('id')
->each(function (object $bundleItem) use ($cartItem): void {
$variant = DB::table('variantes')
->select('inventory_id')
->where('id', $bundleItem->producto_variante_id)
->first();
if ($variant === null) {
return;
}
$inventory = DB::table('inventories')
->select('reserved_stock')
->where('id', $variant->inventory_id)
->first();
if ($inventory === null) {
return;
}
$reservedAmount = $cartItem->cantidad * $bundleItem->cantidad;
DB::table('inventories')->where('id', $variant->inventory_id)->update([
'reserved_stock' => max(0, $inventory->reserved_stock - $reservedAmount),
]);
});
}
DB::table('carrito_items')->where('id', $cartItem->id)->delete();
return;
}
$variant = DB::table('variantes')
->select(['id', 'catalog_item_id'])
->where('id', $cartItem->buyable_id)
->first();
if ($variant === null) {
DB::table('carrito_items')->where('id', $cartItem->id)->delete();
return;
}
DB::table('carrito_items')->where('id', $cartItem->id)->update([
'catalog_item_id' => $variant->catalog_item_id,
'variant_id' => $variant->id,
]);
});
Schema::table('carrito_items', function (Blueprint $table): void {
$table->dropUnique(['cart_id', 'buyable_type', 'buyable_id']);
$table->dropColumn(['buyable_type', 'buyable_id']);
});
Schema::table('carrito_items', function (Blueprint $table): void {
$table->unsignedBigInteger('catalog_item_id')->nullable(false)->change();
$table->foreign('catalog_item_id')->references('id')->on('catalog_items')->cascadeOnDelete();
$table->foreign('variant_id')->references('id')->on('variantes')->cascadeOnDelete();
$table->unique(
['cart_id', 'catalog_item_id', 'variant_id'],
'cart_catalog_item_variant_unique'
);
});
}
public function down(): void
{
throw new RuntimeException('This destructive cart migration cannot be reversed.');
}
};

View File

@@ -0,0 +1,119 @@
<?php
use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Catalog\Models\Variant;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
private const LEGACY_PRODUCT_VARIANT = 'App\\Domains\\Catalog\\Models\\ProductVariant';
private const PREVIOUS_CATALOG_ITEM = 'App\\Domains\\CatalogNew\\Models\\CatalogItem';
private const PREVIOUS_VARIANT = 'App\\Domains\\CatalogNew\\Models\\Variant';
public function up(): void
{
Schema::table('compra_items', function (Blueprint $table): void {
$table->unsignedBigInteger('source_catalog_item_id')->nullable()->after('compra_id');
$table->unsignedBigInteger('source_variant_id')->nullable()->after('source_catalog_item_id');
$table->foreignId('image_attachment_id')->nullable()->after('source_variant_id');
$table->string('nombre')->nullable()->after('image_attachment_id');
$table->text('descripcion')->nullable()->after('nombre');
$table->string('slug')->nullable()->after('descripcion');
$table->string('item_nombre')->nullable()->after('slug');
$table->json('variant_attributes')->nullable()->after('item_nombre');
});
DB::table('compra_items')->orderBy('id')->each(function (object $purchaseItem): void {
if (in_array($purchaseItem->buyable_type, [CatalogItem::class, self::PREVIOUS_CATALOG_ITEM], true)) {
$catalogItem = DB::table('catalog_items')
->where('id', $purchaseItem->buyable_id)
->first();
if ($catalogItem !== null) {
DB::table('compra_items')->where('id', $purchaseItem->id)->update([
'source_catalog_item_id' => $catalogItem->id,
'nombre' => $catalogItem->nombre,
'descripcion' => $catalogItem->descripcion,
'slug' => $catalogItem->slug,
'item_nombre' => $catalogItem->nombre,
'variant_attributes' => json_encode([]),
]);
return;
}
}
if (in_array($purchaseItem->buyable_type, [self::LEGACY_PRODUCT_VARIANT, Variant::class, self::PREVIOUS_VARIANT], true)) {
$variant = DB::table('variantes')
->select(['id', 'catalog_item_id'])
->where('id', $purchaseItem->buyable_id)
->first();
if ($variant !== null) {
$catalogItem = DB::table('catalog_items')->find($variant->catalog_item_id);
if ($catalogItem === null) {
DB::table('compra_items')->where('id', $purchaseItem->id)->delete();
return;
}
$attributes = DB::table('variant_values as variant_value')
->join('item_attributes as item_attribute', 'item_attribute.id', '=', 'variant_value.item_attribute_id')
->leftJoin('attribute', 'attribute.id', '=', 'item_attribute.attribute_id')
->where('variant_value.variant_id', $variant->id)
->orderBy('variant_value.id')
->get(['attribute.nombre as name', 'variant_value.value'])
->map(fn (object $attribute): array => [
'name' => (string) ($attribute->name ?? ''),
'value' => $attribute->value,
])
->all();
$attributeDescription = collect($attributes)
->map(fn (array $attribute): string => $attribute['name'] !== ''
? "{$attribute['name']}: {$attribute['value']}"
: (string) $attribute['value'])
->filter()
->implode(', ');
DB::table('compra_items')->where('id', $purchaseItem->id)->update([
'source_catalog_item_id' => $catalogItem->id,
'source_variant_id' => $variant->id,
'nombre' => $catalogItem->nombre,
'descripcion' => $catalogItem->descripcion,
'slug' => $catalogItem->slug,
'item_nombre' => $attributeDescription === ''
? $catalogItem->nombre
: "{$catalogItem->nombre} ({$attributeDescription})",
'variant_attributes' => json_encode($attributes),
]);
return;
}
}
DB::table('compra_items')->where('id', $purchaseItem->id)->delete();
});
Schema::table('compra_items', function (Blueprint $table): void {
$table->dropColumn(['buyable_type', 'buyable_id']);
});
Schema::table('compra_items', function (Blueprint $table): void {
$table->unsignedBigInteger('source_catalog_item_id')->nullable(false)->change();
$table->string('nombre')->nullable(false)->change();
$table->string('item_nombre')->nullable(false)->change();
$table->foreign('image_attachment_id')->references('id')->on('attachments')->restrictOnDelete();
});
}
public function down(): void
{
throw new RuntimeException('This destructive purchase migration cannot be reversed.');
}
};

View File

@@ -0,0 +1,48 @@
<?php
use App\Domains\Catalog\Enums\ProductLayout;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::dropIfExists('featured_items');
Schema::dropIfExists('featured_products');
Schema::dropIfExists('group_items');
Schema::dropIfExists('featured_variants');
Schema::dropIfExists('featured_groups');
Schema::create('featured_groups', function (Blueprint $table): void {
$table->id();
$table->string('tenant_code');
$table->enum('product_layout', ProductLayout::values());
$table->string('group_name');
$table->unsignedInteger('group_order')->default(0);
$table->foreign('tenant_code')
->references('codigo')
->on('tenants')
->cascadeOnUpdate()
->restrictOnDelete();
});
Schema::create('featured_items', function (Blueprint $table): void {
$table->id();
$table->foreignId('featured_group_id')
->constrained('featured_groups')
->cascadeOnDelete();
$table->foreignId('catalog_item_id')
->constrained('catalog_items')
->cascadeOnDelete();
$table->unsignedInteger('order')->default(0);
});
}
public function down(): void
{
throw new RuntimeException('This destructive featured catalog migration cannot be reversed.');
}
};

View File

@@ -0,0 +1,51 @@
<?php
use App\Domains\Catalog\Enums\CatalogItemType;
use App\Domains\Catalog\Enums\InventoryPolicy;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('catalog_items', function (Blueprint $table): void {
$table->enum('type', CatalogItemType::values())
->default(CatalogItemType::Standard->value)
->after('inventory_id');
$table->enum('inventory_policy', InventoryPolicy::values())
->nullable()
->default(InventoryPolicy::Tracked->value)
->change();
});
Schema::create('bundle_components', function (Blueprint $table): void {
$table->id();
$table->foreignId('bundle_catalog_item_id')
->constrained('catalog_items')
->cascadeOnDelete();
$table->foreignId('component_catalog_item_id')
->constrained('catalog_items')
->restrictOnDelete();
$table->foreignId('component_variant_id')
->nullable()
->constrained('variantes')
->restrictOnDelete();
$table->unsignedInteger('quantity');
});
}
public function down(): void
{
Schema::dropIfExists('bundle_components');
Schema::table('catalog_items', function (Blueprint $table): void {
$table->dropColumn('type');
$table->enum('inventory_policy', InventoryPolicy::values())
->nullable(false)
->default(InventoryPolicy::Tracked->value)
->change();
});
}
};

View File

@@ -0,0 +1,18 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::dropIfExists('bundle_items');
Schema::dropIfExists('bundles');
}
public function down(): void
{
throw new RuntimeException('Legacy bundle data cannot be restored.');
}
};