Saltar a contenido

Análisis de Confiabilidad del Sistema de Sincronización

Nota de vigencia (2026-08-11): este análisis es anterior a los fixes de junio 2026 (lookup scopeado de product_item_view y política de reintentos — ver incidente 2026-06). Partes pueden estar desactualizadas; vigencia a confirmar por el equipo.

Resumen

Este documento identifica puntos de fallo potenciales, inconsistencias y áreas de mejora en el sistema de sincronización del Integrador Compulandia.


1. Puntos de Fallo Identificados

1.1 🔴 Crítico: Pérdida de Sincronización por Gate de publish

Ubicación: ProductSyncDispatcherService.php

Problema:

if ($productItem->publish === 0) {
    return; // Se silencia completamente
}

Riesgo: Si un producto tiene publish = 0 pero debería sincronizarse (por ejemplo, para desactivarlo en los canales), el sync se ignora silenciosamente sin logging.

Impacto: Productos pueden quedar activos en canales externos cuando deberían estar desactivados.

Recomendación: - Agregar logging cuando se omite un sync por publish = 0 - Considerar si product_status sync debería ejecutarse incluso con publish = 0


1.2 🔴 Crítico: Inconsistencia en Nombres de Cola

Ubicación: config/queue.php

Problema:

'redis_contimarket' => [
    'queue' => 'contimaket',  // ← Typo: "contimaket" vs "contimarket"
],

Riesgo: Si algún job se despacha a la cola correcta contimarket, no será procesado por el worker que escucha contimaket.

Recomendación: Corregir el typo y verificar consistencia en todos los nombres de colas.


1.3 🟠 Alto: Falta de Transaccionalidad en Cadena de Pricing

Ubicación: - CalculatePricesJob.php - EvaluateSelectedSupplierProductJob.php

Problema: La cadena CalculatePrices → EvaluateSelected → ProductItemObserver → Sync no es transaccional. Si falla en medio:

CalculatePricesJob ✓
    ↓
EvaluateSelectedJob ✓ (actualiza selected_supplier_product_id)
    ↓
ProductItemObserver ✓ (detecta cambio)
    ↓
ProductSyncDispatcher ✗ (falla)

Riesgo: El ProductItem queda con datos actualizados pero el canal externo no se sincroniza.

Impacto: Desincronización entre base de datos local y canales externos.

Recomendación: - Implementar patrón Saga con compensación - O usar eventos con retry garantizado - Al menos registrar ProductSyncLog en estado PENDING antes de cualquier cambio


1.4 🟠 Alto: Race Condition en Observer de ProductItem

Ubicación: ProductItemObserver.php

Problema: El observer usa wasChanged() que depende del estado en memoria. Si múltiples procesos actualizan el mismo ProductItem:

Proceso A: lee ProductItem (selected = 1)
Proceso B: lee ProductItem (selected = 1)
Proceso A: actualiza selected = 2, save() → Observer dispara sync
Proceso B: actualiza selected = 3, save() → Observer dispara sync

Riesgo: Ambos syncs se disparan pero con datos diferentes, posible inconsistencia final.

Recomendación: - Usar locks optimistas (version column) - O locks pesimistas en operaciones críticas - Agregar deduplicación en ProductSyncLog (unique constraint ya existe pero verificar uso)


1.5 🟠 Alto: Comparación de Precios con Pérdida de Precisión

Ubicación: SupplierProductEvaluationStrategy.php

Problema:

$oldPrice = (int) $supplierProduct->getOriginal('regular_price');
$newPrice = (int) $supplierProduct->regular_price;

Convierte a entero antes de comparar, perdiendo decimales.

Riesgo: Cambios de precio significativos (ej: 1000.50 → 1000.99) no disparan recálculo.

Impacto: Precios desactualizados en canales.

Recomendación: - Usar bccomp() para comparación decimal precisa - O definir un threshold mínimo de cambio (ej: 1% o valor absoluto)


1.6 🟡 Medio: Ausencia de Idempotencia en Jobs

Ubicación: Múltiples jobs de sync

Problema: Los jobs no verifican si el sync ya fue realizado exitosamente. Si un job se reintenta (por timeout, etc.):

SyncToMedusaJob ejecuta
    → API Medusa OK (pero respuesta no llega)
    → Job timeout, se reintenta
    → API Medusa recibe duplicado

Riesgo: Datos duplicados o inconsistentes en canales externos.

Recomendación: - Implementar idempotency keys en las APIs - Verificar ProductSyncLog.synced_at antes de ejecutar - Usar uniqueId() en jobs para prevenir duplicados en cola


1.7 🟡 Medio: Sincronización de Imágenes sin Validación

Ubicación: ProductImageObserver.php

Problema:

public function created(ProductItemImage $image)
{
    if ($image->is_public) {
        SyncToWooCommerceJob::dispatch('product_image', $product);
        // No se despacha a otros canales (Medusa, etc.)
    }
}

Riesgo: Las imágenes solo se sincronizan a WooCommerce directamente, otros canales dependen de ProductSyncDispatcherService que se llama en updated().

Impacto: Inconsistencia en imágenes entre canales.

Recomendación: Usar ProductSyncDispatcherService consistentemente para todos los eventos de imagen.


1.8 🟡 Medio: Falta de Validación en ProductCategory Sync

Ubicación: ProductCategoryObserver.php

Problema:

private function syncMedusaCategory(ProductCategory $category, bool $forceUpdate = false): void
{
    try {
        // ... sync logic
    } catch (\Throwable $e) {
        Log::channel(LogChannels::MEDUSA)->error("...");
        // Error se loguea pero no se propaga ni se registra en ProductSyncLog
    }
}

Riesgo: Fallos en sync de categorías a Medusa se pierden silenciosamente.

Impacto: Categorías desincronizadas sin manera de detectarlo o reintentarlo.

Recomendación: - Crear un CategorySyncLog similar a ProductSyncLog - O al menos registrar en una tabla de errores con retry automático


1.9 🟡 Medio: Content Generation Selectivo sin Fallback

Ubicación: SupplierProductEvaluationStrategy.php

Problema:

if (!str_starts_with($sku, 'PC-') && !str_starts_with($sku, 'PCM-')) {
    return; // Solo genera contenido para estos prefijos
}
if ($supplier->short_name !== 'CL') {
    return; // Solo proveedor CL
}

Riesgo: Productos de otros proveedores nunca tienen contenido generado por AI.

Impacto: Calidad inconsistente de contenido entre proveedores.

Recomendación: Documentar esta decisión de negocio o extender a más proveedores.


1.10 🟢 Bajo: Logs Dispersos sin Correlación

Problema: Cada componente loguea a su propio canal (PRICEJOB, SYNC_STRATEGY, MEDUSA, etc.) sin un ID de correlación.

Riesgo: Difícil rastrear el flujo completo de un sync específico.

Recomendación: - Agregar correlation_id o trace_id en todos los logs de un mismo flujo - Considerar usar el ProductSyncLog.id como correlation ID


2. Inconsistencias de Diseño

2.1 Mezcla de Dispatch Directo y a través de Dispatcher

Observación: Algunos observers despachan jobs directamente:

// ProductImageObserver
SyncToWooCommerceJob::dispatch('product_image', $product);

Mientras otros usan el dispatcher:

// ProductEvaluationStrategy
ProductSyncDispatcherService::dispatchSync($productItem, $syncContext);

Impacto: - Jobs despachados directamente no registran en ProductSyncLog - No se aplican filtros del dispatcher (publish check) - Inconsistencia en qué canales reciben el sync

Recomendación: Estandarizar en usar siempre ProductSyncDispatcherService.


2.2 Nombres de Jobs Inconsistentes

Job Patrón
SyncToWooCommerceJob SyncTo{Channel}Job
SyncToMedusaJob SyncTo{Channel}Job
SyncToContimarketJob SyncTo{Channel}Job
SyntToTiendaNaranjaJob SyntTo{Channel}Job ← Typo
ReindexInAlgoliaJob Patrón diferente

Recomendación: Renombrar a SyncToTiendaNaranjaJob por consistencia.


2.3 Constantes de Canal Inconsistentes

class SyncChannels
{
    const WOO = 'woocommerce';
    const TN = 'Tienda Naranja';  // ← Con espacio y mayúsculas
    const CONTI = 'contimarket';
    const ALGOLIA = 'algolia reindex';  // ← Con espacio
    const MEDUSA = 'medusa';
}

Impacto: Dificulta búsquedas y filtros en base de datos.

Recomendación: Normalizar a snake_case sin espacios: tienda_naranja, algolia.


3. Gaps de Cobertura

3.1 Eventos No Observados

Modelo Evento ¿Observado? Impacto
ProductItem deleted ❌ Producto eliminado no se desactiva en canales
SupplierProduct deleted ❌ Proveedor eliminado no reevalúa selección
ProductItemImage forceDeleted ❌ Imagen eliminada permanentemente no se limpia
ProductCategory deleted ❌ Categoría eliminada persiste en canales

3.2 Cambios No Detectados

Campo Modelo Detectado Impacto
brand ProductItem ❌ Marca no se sincroniza
weight, dimensions ProductItem ❌ Datos de envío desactualizados
meta_title, meta_description ProductItem ❌ SEO no sincronizado

4. Recomendaciones de Mejora

4.1 Inmediatas (Quick Wins)

  1. Corregir typos:
  2. contimaket → contimarket en queue.php
  3. SyntToTiendaNaranjaJob → SyncToTiendaNaranjaJob

  4. Agregar logging en gates:

    if ($productItem->publish === 0) {
        Log::channel('SYNC_STRATEGY')->info("Sync omitido: publish=0", [
            'product_id' => $productItem->id
        ]);
        return;
    }
    

  5. Normalizar constantes de canal

4.2 Corto Plazo

  1. Implementar observer para deleted events
  2. Unificar dispatch a través de ProductSyncDispatcherService
  3. Agregar correlation ID en logs
  4. Mejorar comparación de precios con precisión decimal

4.3 Mediano Plazo

  1. Implementar patrón Outbox para garantía de entrega:

    DB Transaction:
      1. Guardar cambio en modelo
      2. Insertar en OutboxEvents table
    
    Proceso separado:
      1. Leer OutboxEvents
      2. Dispatch jobs
      3. Marcar como procesado
    

  2. Agregar idempotency keys en jobs:

    public function uniqueId(): string
    {
        return "sync:{$this->channel}:{$this->productId}:{$this->syncType}";
    }
    

  3. Implementar circuit breaker para APIs externas

  4. Dashboard de health check de sincronización:

  5. Syncs pendientes por más de X tiempo
  6. Tasa de fallos por canal
  7. Alertas automáticas

4.4 Largo Plazo

  1. Event Sourcing para historial completo de cambios
  2. Reconciliación periódica automática con canales externos
  3. Dry-run mode para preview de syncs

5. Matriz de Riesgos

Riesgo Probabilidad Impacto Prioridad Mitigación
Sync perdido por publish=0 Alta Alto 🔴 P1 Logging + revisar lógica
Typo en cola contimarket Media Alto 🔴 P1 Corregir typo
Race condition en observers Media Medio 🟠 P2 Locks optimistas
Precios no detectados Alta Medio 🟠 P2 Comparación decimal
Jobs no idempotentes Baja Alto 🟡 P3 uniqueId()
Categorías no tracked Baja Bajo 🟢 P4 CategorySyncLog

6. Checklist de Verificación

Antes de Cada Sync Manual

  • [ ] Verificar que publish = 1
  • [ ] Verificar que existe ProductItemView para el canal
  • [ ] Verificar que tiene imágenes públicas (para Contimarket)
  • [ ] Verificar que tiene categoría activa

Monitoreo Diario

  • [ ] Revisar ProductSyncLog con status = 'failed'
  • [ ] Revisar logs de SYNC_STRATEGY para errores
  • [ ] Verificar que colas no tienen jobs atascados (Horizon)
  • [ ] Comparar conteo de productos activos vs canal externo

Monitoreo Semanal

  • [ ] Ejecutar sync:retry-pending para syncs estancados
  • [ ] Revisar app:clean-product-sync-logs para mantener tabla limpia
  • [ ] Auditar productos sin sync reciente exitoso

Documento generado: 2025-12-30 Análisis basado en revisión de código del repositorio