# Propuesta de Mejora: Progress Bar en /price-cost

## Problemas Identificados

### 1. **Procesamiento Síncrono**
- El backend procesa TODO antes de retornar la respuesta
- El `progress_id` solo se retorna al final del procesamiento
- El frontend empieza a hacer polling cuando el proceso ya está casi/completamente terminado
- Resultado: El progress bar no refleja el progreso real

### 2. **Total Desconocido al Inicio**
- El `total` de variantes solo se conoce al final del procesamiento
- El progress bar muestra `0 / 0` o valores incorrectos al inicio
- No se puede calcular un porcentaje real desde el principio

### 3. **Cache Inconsistente**
- Se usa `Cache::forever()` y `Cache::put()` con TTL simultáneamente
- Esto puede causar inconsistencias en los datos de progress
- No hay un patrón claro de cuándo usar cada método

### 4. **Race Condition**
- El `progress_id` se genera pero el cache puede no estar inicializado cuando el frontend hace el primer poll
- Esto causa que el frontend reciba 404 y detenga el polling prematuramente

### 5. **Polling Excesivo**
- Se hace polling cada 500ms, lo cual es muy frecuente
- Genera carga innecesaria en el servidor
- No hay backoff exponencial en caso de errores

### 6. **Falta de Manejo de Errores**
- Si el proceso falla, el progress puede quedar "stuck" en `processing`
- No hay timeout o cleanup automático
- El frontend puede hacer polling indefinidamente

## Solución Propuesta

### Opción A: Pre-calcular Total y Inicializar Cache Antes (RECOMENDADO)

**IMPORTANTE: NO se hacen consultas adicionales a Shopify. Se usa la data ya obtenida.**

#### Backend Changes:

1. **Pre-calcular el total de variantes ANTES de procesar (usando data ya obtenida)**
   ```php
   // En UpdateShopifyCollectionCostPrice.php
   // ANTES del foreach principal, contar todos los variantes válidos
   // usando los datos que YA se obtuvieron de Shopify (no consulta adicional)
   $totalVariantsToProcess = 0;
   foreach ($productsShop as $items) {
       $productId = $items['node']['id'];
       // Check if excluded (sin consultas adicionales)
       if (in_array($productId, $excludedProductIds)) {
           continue;
       }
       
       $variants = $items['node']['variants']['edges'];
       foreach ($variants as $variant) {
           // Extraer cost price de los datos ya obtenidos
           $inventoryItem = $variant['node']['inventoryItem'] ?? null;
           $unitCost = $inventoryItem['unitCost']['amount'] ?? null;
           $costPrice = $unitCost ? (float)$unitCost : 0;
           if ($costPrice > 0) {
               $totalVariantsToProcess++;
           }
       }
   }
   ```

2. **Inicializar el progress en cache ANTES de procesar (con total real)**
   ```php
   if (!$demo && !$progressIdFromRequest) {
       Cache::put($progressId, [
           'total' => $totalVariantsToProcess,  // Total real desde el inicio
           'processed' => 0,
           'errors' => 0,
           'status' => 'processing',
           'current_variant' => 'Initializing...',
           'started_at' => now()->toIso8601String(),
       ], 3600); // 1 hora de TTL
   }
   ```

3. **Retornar progress_id INMEDIATAMENTE después de inicializar cache**
   
   **NOTA**: Esto requiere modificar el flujo del controller. Hay dos opciones:
   
   **Opción 3A**: Modificar el Action para que retorne el progress_id ANTES de procesar
   ```php
   // En UpdateShopifyCollectionCostPrice.php
   // Después de obtener $productsShop y calcular $totalVariantsToProcess
   // Inicializar cache con total real
   // Luego continuar con el procesamiento (síncrono)
   ```
   
   **Opción 3B**: El controller inicializa el progress, luego llama al Action
   ```php
   // En ShopifyCostPriceController.php
   if (!$demo) {
       $progressId = 'cost_price_progress_' . uniqid();
       // Pre-calcular total (iterar sobre datos que el Action obtendrá)
       // Inicializar cache
       // Llamar al Action pasando el progressId
       // Retornar 202 inmediatamente
   }
   ```
   
   **Problema con Opción 3B**: Requiere duplicar la lógica de obtener productos
   
   **Mejor solución**: Modificar el Action para que inicialice el cache ANTES del procesamiento principal y retorne el progress_id de forma que el controller pueda retornarlo inmediatamente.

4. **Procesar síncronamente pero actualizar cache frecuentemente**
   - El procesamiento sigue siendo síncrono (no requiere queues)
   - Actualizar cache cada N variantes (ej: cada 10) durante el procesamiento
   - El frontend puede hacer polling mientras el backend procesa
   - NO se hacen consultas adicionales a Shopify, solo se actualiza el cache con el progreso

5. **Simplificar cache - usar solo `Cache::put()` con TTL consistente**
   ```php
   // Reemplazar todos los Cache::forever() y Cache::store()->put()
   // Con un solo método:
   Cache::put($progressId, $progressData, 3600); // 1 hora
   ```

#### Frontend Changes:

1. **Mejorar polling con exponential backoff**
   ```javascript
   let pollInterval = 500; // Start with 500ms
   const maxInterval = 5000; // Max 5 seconds
   
   const pollProgress = () => {
       axios.get('/get/collection/cost_price_progress', {
           params: { progress_id: progressId }
       }).then((response) => {
           if (response.data.status === 200) {
               pollInterval = 500; // Reset on success
               // Update UI
               if (data.status === 'completed') {
                   clearInterval(interval);
               }
           }
       }).catch((error) => {
           pollInterval = Math.min(pollInterval * 1.5, maxInterval);
           // Retry with new interval
       });
   };
   ```

2. **Manejar estados de error y timeout**
   ```javascript
   // Timeout después de 15 minutos
   const maxDuration = 15 * 60 * 1000; // 15 minutos
   const startTime = Date.now();
   
   const checkTimeout = () => {
       if (Date.now() - startTime > maxDuration) {
           // Mostrar error de timeout
           clearInterval(interval);
       }
   };
   ```

3. **Manejar estado 404 inicial (cache aún no inicializado)**
   ```javascript
   let retryCount = 0;
   const maxRetries = 10;
   
   if (response.status === 404 && retryCount < maxRetries) {
       retryCount++;
       setTimeout(() => pollProgress(), 200); // Retry después de 200ms
       return;
   }
   ```

### Opción B: Procesamiento Asíncrono Real (CON QUEUES/JOBS)

Si queremos una solución más robusta, podríamos usar Laravel Jobs:

1. **Crear un Job para procesar los precios (usa la misma data ya obtenida)**
   ```php
   php artisan make:job UpdateCollectionCostPriceJob
   ```

2. **Despachar el job desde el controller (pasa los datos ya obtenidos, NO consulta Shopify de nuevo)**
   ```php
   // Los datos de productos/variantes ya están en $productsShop
   // Se pasan al job para que los procese sin consultar Shopify de nuevo
   UpdateCollectionCostPriceJob::dispatch($productsShop, $request->all(), $progressId);
   return response()->json(['progress_id' => $progressId], 202);
   ```

3. **El job actualiza el cache mientras procesa**
   - Más escalable
   - No bloquea el request
   - Permite manejar errores mejor
   - **NO hace consultas adicionales a Shopify**

**Desventaja**: Requiere configurar queues en Laravel

## Recomendación Final

**Implementar Opción A** porque:
- ✅ No requiere cambios en la infraestructura (queues)
- ✅ Es más simple de implementar
- ✅ Resuelve los problemas principales
- ✅ Mantiene la compatibilidad con el código actual

Los cambios clave serían:
1. Pre-calcular total antes de procesar (usando datos ya obtenidos, sin consultas adicionales)
2. Inicializar cache ANTES de procesar con total real
3. Simplificar uso de cache (solo Cache::put con TTL consistente)
4. Actualizar cache durante el procesamiento
5. Mejorar polling en frontend con backoff y timeout

**Nota sobre retornar inmediatamente**: Dado que el procesamiento es síncrono, el controller retornará cuando termine. Sin embargo, como el cache se actualiza durante el procesamiento, podemos mejorar el manejo del progress_id en la respuesta y el polling del frontend para que funcione mejor incluso si el proceso terminó antes de que el frontend empiece a hacer polling.

## Plan de Implementación

### Fase 1: Backend - Pre-cálculo e Inicialización
1. Modificar `UpdateShopifyCollectionCostPrice` para pre-calcular total (iterando sobre datos ya obtenidos)
2. Inicializar cache con total real ANTES del loop principal de procesamiento
3. Simplificar uso de cache (remover Cache::forever y Cache::store()->put, usar solo Cache::put con TTL)
4. El controller sigue retornando normalmente, pero el cache ya estará inicializado con total real

### Fase 2: Backend - Actualización de Progress
1. Actualizar cache cada N variantes (mantener el intervalo actual)
2. Asegurar que siempre se actualiza al final con status 'completed' o 'failed'

### Fase 3: Frontend - Mejoras de Polling
1. Implementar exponential backoff
2. Agregar timeout de seguridad
3. Mejor manejo de errores y estados 404 iniciales
4. Mostrar mensajes de error más descriptivos

### Fase 4: Testing
1. Probar con colecciones pequeñas
2. Probar con colecciones grandes
3. Probar manejo de errores
4. Verificar que el progress bar refleja el progreso real

