Saltar a contenido

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

  1. Duplicación de datos: Existen dos tablas para categorías (categories y product_categories) con información redundante
  2. Relación implícita por nombre: La vinculación entre categorías se hace por coincidencia de nombres, no por FK
  3. Gestión compleja: Cada nueva categoría requiere crear entradas en múltiples tablas
  4. Tabla match_categories obsoleta: 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

  1. Eliminación de product_categories: Los campos se migran a metadata
  2. Eliminación de match_categories: Reemplazado por master_category_id
  3. Nueva tabla metadata: Extensión dinámica de cualquier entidad
  4. Nueva tabla product_partner_categories: Relación directa producto → categorías de salida
  5. Nuevo Partner "Compulandia": Categorías de salida para el canal principal
  6. 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

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 categories con categorizable_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

  1. products.category_id: Se mantiene durante transición, pero deprecado
  2. product_categories: Se mantiene como read-only, sin nuevas inserciones
  3. match_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

  1. 100% de productos migrados a product_partner_categories
  2. Sync a TN funcionando con nueva estructura
  3. Cero regresiones en sync a WC/Medusa
  4. 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