RFC: Migración del Sistema de Categorías¶
Versión: 1.1 Fecha: 2026-01-14 Estado: En Implementación Autor: Equipo de Desarrollo
Nota v1.1: Se decidió reutilizar el Partner "Woocommerce" (ID=2) existente, renombrándolo a "Compulandia", en lugar de crear uno nuevo. Esto mantiene la integridad de los precios ya calculados para este canal.
Resumen Ejecutivo¶
Este documento describe la migración del sistema de categorías del Integrador Compulandia. El objetivo es unificar la gestión de categorías eliminando la duplicación entre product_categories y categories, estableciendo relaciones directas entre productos y categorías de Partners (canales de salida).
Problema Actual¶
- Duplicación de datos: Existen dos tablas para categorías (
categoriesyproduct_categories) con información redundante - Relación implícita por nombre: La vinculación entre categorías se hace por coincidencia de nombres, no por FK
- Gestión compleja: Cada nueva categoría requiere crear entradas en múltiples tablas
- Tabla
match_categoriesobsoleta: Se usa para vincular categorías de diferentes proveedores/partners de forma poco eficiente
Estado Actual¶
Tablas Involucradas¶
┌──────────────────────┐ ┌──────────────────────────┐
│ categories │ │ product_categories │
├──────────────────────┤ ├──────────────────────────┤
│ id │ │ id │
│ categorizable_type │ │ name │
│ categorizable_id │ │ parent_id_category │
│ name │ │ id_woocommerce │
│ factor │ │ id_medusa_category │
│ margin_min │ │ seo_formula │
│ weight/width/... │ │ description_prompt │
│ parent_id │ │ short_description_prompt │
│ scale_id │ │ seo_name_prompt │
└──────────────────────┘ └──────────────────────────┘
│ │
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────────┐
│ match_categories │ │ products │
├──────────────────────┤ ├──────────────────────────┤
│ category_a_id │ │ category_id (FK) │
│ category_b_id │ │ → product_categories │
└──────────────────────┘ └──────────────────────────┘
Estadísticas Actuales¶
| Entidad | Cantidad |
|---|---|
Products con category_id |
14,179 |
| ProductCategories | 96 |
| Categories (Supplier CL) | 103 |
| Categories (Partner TN) | 376 |
| MatchCategories total | 162 |
| MatchCategories con TN | 37 |
Flujo Actual de Asignación de Categoría¶
SupplierProduct (nuevo)
│
▼
CategoryService::getCLCategoryForProduct()
│
▼
Busca Category CL por nombre ────► match_categories
│ │
▼ ▼
ProductCategory::where('name', $clCategory->name) ◄── Relación por NOMBRE
│
▼
Product::create(['category_id' => $category->id])
Problemas: - La búsqueda por nombre es frágil - No permite múltiples categorías de salida por producto - Requiere coincidencia exacta de nombres
Solución Propuesta¶
Arquitectura Nueva¶
┌────────────────────────────────────────────────────────────────────┐
│ categories │
├────────────────────────────────────────────────────────────────────┤
│ id │
│ categorizable_type (Supplier::class | Partner::class) │
│ categorizable_id │
│ name │
│ parent_id (FK self) ─────────────────► Jerarquía │
│ master_category_id (FK self) ────────► Herencia de dimensiones │
│ factor, margin_min, weight, width, length, height │
│ scale_id │
└────────────────────────────────────────────────────────────────────┘
│
│ categorizable_type = Partner::class
▼
┌────────────────────────────────────────────────────────────────────┐
│ metadata │
├────────────────────────────────────────────────────────────────────┤
│ id │
│ metable_type (Category::class, etc.) │
│ metable_id │
│ key (ej: 'id_woocommerce', 'seo_formula', 'description_prompt') │
│ value │
└────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────┐
│ product_partner_categories │
├────────────────────────────────────────────────────────────────────┤
│ id │
│ product_id (FK → products) │
│ category_id (FK → categories WHERE categorizable_type=Partner) │
│ timestamps │
└────────────────────────────────────────────────────────────────────┘
Cambios Clave¶
- Eliminación de
product_categories: Los campos se migran ametadata - Eliminación de
match_categories: Reemplazado pormaster_category_id - Nueva tabla
metadata: Extensión dinámica de cualquier entidad - Nueva tabla
product_partner_categories: Relación directa producto → categorías de salida - Nuevo Partner "Compulandia": Categorías de salida para el canal principal
- Renombrar Supplier "Compulandia" → "SAP": Claridad en el origen de datos
Nuevo Flujo de Asignación¶
SupplierProduct (nuevo)
│
▼
UI: Modal de confirmación
│
├── Seleccionar categorías de Partners
│ ├── Partner "Compulandia" (obligatorio)
│ ├── Partner "TiendaNaranja" (opcional)
│ └── Partner "Medusa" (opcional)
│
▼
Product::create() + pivot en product_partner_categories
Migraciones de Base de Datos¶
Fase 1: Estructura¶
Migración 1.1: Partner Compulandia¶
// Crear Partner "Compulandia" con id = PartnerConstants::CL
Partner::create([
'id' => PartnerConstants::CL, // Nuevo ID constante
'name' => 'Compulandia',
'active' => true,
]);
Migración 1.2: Renombrar Supplier¶
// Renombrar Supplier "Compulandia" a "SAP"
Supplier::where('id', SupplierConstants::CL)
->update(['name' => 'SAP']);
Migración 1.3: Campo master_category_id¶
Schema::table('categories', function (Blueprint $table) {
$table->foreignId('master_category_id')
->nullable()
->after('parent_id')
->constrained('categories')
->nullOnDelete();
});
Migración 1.4: Tabla metadata¶
Schema::create('metadata', function (Blueprint $table) {
$table->id();
$table->morphs('metable'); // metable_type, metable_id
$table->string('key');
$table->text('value')->nullable();
$table->timestamps();
$table->unique(['metable_type', 'metable_id', 'key']);
$table->index(['metable_type', 'metable_id']);
});
Migración 1.5: Tabla product_partner_categories¶
Schema::create('product_partner_categories', function (Blueprint $table) {
$table->id();
$table->foreignId('product_id')
->constrained('products')
->cascadeOnDelete();
$table->foreignId('category_id')
->constrained('categories')
->cascadeOnDelete();
$table->timestamps();
$table->unique(['product_id', 'category_id']);
$table->index('category_id');
});
Fase 2: Migración de Datos¶
Comando: MigrateCategoriesToPartnerCL¶
Para cada ProductCategory:
1. Crear Category para Partner CL con mismo nombre
2. Migrar campos a metadata:
- id_woocommerce
- id_medusa_category
- seo_formula
- description_prompt
- short_description_prompt
- seo_name_prompt
3. Copiar dimensiones de Category CL (Supplier) correspondiente
Comando: MigrateProductsToPartnerCategories¶
Para cada Product con category_id:
1. Obtener ProductCategory actual
2. Buscar nueva Category Partner CL por nombre
3. Insertar en product_partner_categories
4. (Opcional) Insertar categorías TN si hay match
Cambios en Modelos¶
Modelo Category (Actualizado)¶
// Nuevas relaciones
public function masterCategory()
{
return $this->belongsTo(Category::class, 'master_category_id');
}
public function dependentCategories()
{
return $this->hasMany(Category::class, 'master_category_id');
}
public function metadata()
{
return $this->morphMany(Metadata::class, 'metable');
}
public function products()
{
return $this->belongsToMany(Product::class, 'product_partner_categories');
}
// Accessor para herencia de dimensiones
public function getEffectiveDimensionsAttribute()
{
if ($this->master_category_id) {
return $this->masterCategory->only(['weight', 'width', 'length', 'height']);
}
return $this->only(['weight', 'width', 'length', 'height']);
}
Nuevo Modelo Metadata¶
class Metadata extends Model
{
protected $table = 'metadata';
protected $fillable = ['metable_type', 'metable_id', 'key', 'value'];
public function metable()
{
return $this->morphTo();
}
}
Modelo Product (Actualizado)¶
// Nueva relación (reemplaza category())
public function partnerCategories()
{
return $this->belongsToMany(Category::class, 'product_partner_categories')
->withTimestamps();
}
// Categoría principal (Partner CL)
public function mainCategory()
{
return $this->partnerCategories()
->where('categorizable_type', Partner::class)
->where('categorizable_id', PartnerConstants::CL)
->first();
}
Cambios en UI¶
Modal de Confirmación para Productos Pendientes¶
Ubicación: resources/views/pending_products/index.blade.php
Componentes: 1. Modal con lista de Partners activos 2. Multiselect de categorías por Partner 3. Partner "Compulandia" como obligatorio 4. Búsqueda/filtrado de categorías 5. Preview de categorías seleccionadas
Wireframe:
┌────────────────────────────────────────────────────────────┐
│ Confirmar Creación de Producto [X] │
├────────────────────────────────────────────────────────────┤
│ │
│ Producto: [SKU] - [Nombre del producto] │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ COMPULANDIA (Obligatorio) ▼ │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ [x] Computación > Notebooks │ │ │
│ │ │ [ ] Computación > Desktop │ │ │
│ │ │ [ ] Periféricos > Monitores │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ TIENDA NARANJA (Opcional) ▼ │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ [ ] Notebooks y Laptops │ │ │
│ │ │ [ ] Computadores │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ [Cancelar] [Crear Producto] │
└────────────────────────────────────────────────────────────┘
Impacto en Servicios¶
TiendaNaranja ProductTransformer¶
Antes:
// Flujo complejo: Product → ProductCategory → Category CL → match_categories → TN
$catModel = Category::where('name', $pivot->name)
->where('categorizable_type', Supplier::class)
->first();
$tnIds = ProductService::getAllRelatedTNCategories($catModel->id);
Después:
// Flujo directo: Product → product_partner_categories → Category TN
$tnCategories = $product->partnerCategories()
->where('categorizable_id', PartnerConstants::TN)
->pluck('external_code');
CategoryService¶
getCLCategoryForProduct(): Se mantiene para calcular precios (usa Category Supplier)- Nuevos métodos para obtener categorías de Partner
PriceCalculation¶
- Sin cambios: Sigue usando
categoriesconcategorizable_type = Supplier - El factor y margin_min permanecen en categorías de Supplier
Plan de Migración¶
Estrategia: Despliegue en Fases¶
PRODUCCIÓN ACTUAL
│
┌──────────────────┴──────────────────┐
│ │
▼ ▼
FASE 1: Migraciones FASE 2: Comandos
(Estructura BD) (Migración datos)
│ │
└──────────────────┬──────────────────┘
│
▼
FASE 3
(Modelos + Relaciones)
│
▼
FASE 4
(UI + Controller)
│
▼
FASE 5
(Testing + Deploy)
Checklist de Despliegue¶
- [ ] Backup de base de datos
- [ ] Ejecutar migraciones de estructura
- [ ] Ejecutar comandos de migración de datos
- [ ] Verificar integridad de datos migrados
- [ ] Desplegar código actualizado
- [ ] Probar flujo de productos pendientes
- [ ] Verificar sincronización a TN, WC, Medusa
- [ ] Monitorear logs por 24h
Compatibilidad Hacia Atrás¶
Periodo de Transición¶
products.category_id: Se mantiene durante transición, pero deprecadoproduct_categories: Se mantiene como read-only, sin nuevas insercionesmatch_categories: Se mantiene para consultas legacy
Rollback Plan¶
En caso de problemas críticos: 1. Restaurar backup de BD 2. Revertir código a versión anterior 3. Los datos originales permanecen intactos
Riesgos y Mitigación¶
| Riesgo | Probabilidad | Impacto | Mitigación |
|---|---|---|---|
| Pérdida de datos en migración | Baja | Alto | Backup previo + validación post-migración |
| Inconsistencia en categorías TN | Media | Medio | Comando de verificación + logs detallados |
| Regresión en sync a canales | Media | Alto | Tests de integración previos |
| Downtime durante migración | Baja | Medio | Migración en horario bajo tráfico |
Métricas de Éxito¶
- 100% de productos migrados a
product_partner_categories - Sync a TN funcionando con nueva estructura
- Cero regresiones en sync a WC/Medusa
- UI de pendientes operativa con nuevo modal
Archivos Afectados¶
Base de Datos¶
database/migrations/XX/create_partner_compulandia.php(nuevo)database/migrations/XX/rename_supplier_compulandia.php(nuevo)database/migrations/XX/add_master_category_id_to_categories.php(nuevo)database/migrations/XX/create_metadata_table.php(nuevo)database/migrations/XX/create_product_partner_categories_table.php(nuevo)
Modelos¶
app/Models/Category.php(modificado)app/Models/Product.php(modificado)app/Models/Metadata.php(nuevo)app/Constants/PartnerConstants.php(modificado)
Comandos¶
app/Console/Commands/MigrateCategoriesToPartnerCL.php(nuevo)app/Console/Commands/MigrateProductsToPartnerCategories.php(nuevo)
Controllers¶
app/Http/Controllers/PendingProductsController.php(modificado)
Vistas¶
resources/views/pending_products/index.blade.php(modificado)
Servicios¶
app/Services/TiendaNaranja/Transformers/ProductTransformer.php(modificado)
Apéndice A: Constantes¶
// app/Constants/PartnerConstants.php
class PartnerConstants
{
public const CL = 1; // Compulandia (nuevo)
public const TN = 2; // TiendaNaranja
public const MEDUSA = 3; // Medusa
// ... otros partners
}
Apéndice B: Queries de Verificación¶
-- Verificar migración de categorías
SELECT
pc.name as product_category,
c.name as partner_category,
c.categorizable_id as partner_id
FROM product_categories pc
LEFT JOIN categories c ON c.name = pc.name
AND c.categorizable_type = 'App\\Models\\Partner'
AND c.categorizable_id = 1;
-- Verificar productos migrados
SELECT
p.id,
p.name,
COUNT(ppc.id) as total_partner_categories
FROM products p
LEFT JOIN product_partner_categories ppc ON p.id = ppc.product_id
GROUP BY p.id, p.name
HAVING total_partner_categories = 0;
-- Verificar metadata migrada
SELECT
c.name,
GROUP_CONCAT(m.key) as metadata_keys
FROM categories c
JOIN metadata m ON m.metable_id = c.id AND m.metable_type = 'App\\Models\\Category'
WHERE c.categorizable_type = 'App\\Models\\Partner'
GROUP BY c.id, c.name;
Documento generado: 2026-01-14 Siguiente revisión: Antes de implementación