---
name: shopify-rendimiento-variantes
description: "Solo lectura: clasifica cada variante por ingresos, unidades vendidas y tasa de reembolso, y la cruza con el inventario actual para separar las que más venden de las que solo ocupan espacio. Úsala antes de volver a pedir o rebajar tallas y colores."
---

## Propósito
Va más allá de los ingresos por producto: clasifica cada variante individual (talla, color, combinación de opciones) por ingresos, unidades vendidas y tasa de reembolso, y la cruza con los niveles de inventario actuales. Muestra qué SKU específicos mueven el negocio y cuáles tienen capital quieto en el estante. Solo lectura: no ejecuta mutaciones.

## Requisitos
- Sesión autenticada de Shopify CLI: `shopify store auth --store <tu-tienda>.myshopify.com --scopes read_orders,read_products`
- Las consultas se ejecutan con `shopify store execute`.
- Permisos de la API: `read_orders`, `read_products` (confirmado por el validador: la consulta de pedidos recorre la relación variante→producto)

## Parámetros

| Parámetro | Tipo | Obligatorio | Predeterminado | Descripción |
|-----------|------|-------------|----------------|-------------|
| store | texto | sí | — | Dominio de la tienda (p. ej., tu-tienda.myshopify.com) |
| format | texto | no | human | Formato de salida: `human` o `json` |
| dry_run | booleano | no | false | Muestra las operaciones sin ejecutar mutaciones |
| date_range_start | texto | sí | — | Fecha de inicio en ISO 8601 (p. ej., `2025-01-01`) |
| date_range_end | texto | sí | — | Fecha de fin en ISO 8601 (p. ej., `2025-01-31`) |
| top_n | entero | no | 30 | Cantidad de variantes que se muestran en la parte alta y en la parte baja de la clasificación |
| sort_by | texto | no | revenue | Métrica de clasificación: `revenue`, `units` o `refund_rate` |
| min_units | entero | no | 1 | Excluye las variantes con menos de N unidades vendidas en el periodo |

## Pasos

1. **OPERACIÓN:** `orders` — consulta
   **Entradas:** `first: 250`, `query: "created_at:>='<date_range_start>' created_at:<='<date_range_end>'"` y el cursor de paginación; selecciona `lineItems` con `variant { id, sku, title, selectedOptions }`, `quantity`, `originalTotalSet`; y `refunds.refundLineItems` con el ID de la variante y `subtotalSet`
   **Resultado esperado:** Todos los pedidos del rango; pagina hasta que `hasNextPage: false`; totaliza en memoria por `variant.id`: unidades vendidas, ingresos brutos, unidades reembolsadas, monto reembolsado y tasa de reembolso

2. **OPERACIÓN:** `productVariants` — consulta
   **Entradas:** Lista de los IDs de variantes reunidos en el paso 1, `first: 250` y el cursor de paginación; selecciona `id`, `sku`, `title`, `selectedOptions`, `inventoryQuantity`, `product { id, title }`, `price`
   **Resultado esperado:** Niveles de inventario actuales y datos de cada variante vendida, unidos con los totales del paso 1; las variantes con inventario pero sin ventas se marcan como candidatas a inventario muerto

3. **Cálculo en memoria:** Ordena el conjunto unido según la métrica `sort_by`; calcula los ingresos por unidad en inventario (ingresos netos ÷ cantidad en inventario) para resaltar las variantes que generan poco en relación con el espacio que ocupan; divide la salida en las N mejores y las N peores según la misma métrica

## Operaciones GraphQL

```graphql
# orders:query (líneas por variante + reembolsos) — validada con api_version 2025-01
query OrdersForVariantPerformance($first: Int!, $after: String, $query: String) {
  orders(first: $first, after: $after, query: $query) {
    edges {
      node {
        id
        createdAt
        lineItems(first: 50) {
          edges {
            node {
              quantity
              originalTotalSet {
                shopMoney { amount currencyCode }
              }
              variant {
                id
                sku
                title
                selectedOptions { name value }
                product { id title }
              }
            }
          }
        }
        refunds {
          refundLineItems(first: 50) {
            edges {
              node {
                quantity
                subtotalSet {
                  shopMoney { amount currencyCode }
                }
                lineItem {
                  variant { id }
                }
              }
            }
          }
        }
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}
```

```graphql
# productVariants:query (foto del inventario) — validada con api_version 2025-01
query VariantInventorySnapshot($first: Int!, $after: String, $query: String) {
  productVariants(first: $first, after: $after, query: $query) {
    edges {
      node {
        id
        sku
        title
        price
        selectedOptions { name value }
        inventoryQuantity
        product {
          id
          title
        }
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}
```

## Seguimiento de la sesión

**Claude DEBE mostrar la siguiente salida en cada etapa. Es obligatorio.**

**Al iniciar**, muestra:
```
╔══════════════════════════════════════════════╗
║  SKILL: shopify-rendimiento-variantes        ║
║  Tienda: <dominio de la tienda>              ║
║  Inicio: <YYYY-MM-DD HH:MM UTC>              ║
╚══════════════════════════════════════════════╝
```

**Después de cada paso**, muestra:
```
[N/TOTAL] <QUERY|MUTATION>  <OperationName>
          → Parámetros: <resumen breve de las entradas clave>
          → Resultado: <cantidad o desenlace>
```

**Al terminar**, muestra:

Para `format: human` (predeterminado):
```
══════════════════════════════════════════════
RESUMEN DEL RESULTADO
  Pedidos procesados:     <n>
  Variantes analizadas:   <n>
  Rango de fechas:        <inicio> a <fin>
  Ordenado por:           <métrica>
  Errores:                0
  Archivo:                variant_performance_<fecha>.csv
══════════════════════════════════════════════
```

Para `format: json`, muestra:
```json
{
  "skill": "shopify-rendimiento-variantes",
  "store": "<dominio>",
  "started_at": "<ISO8601>",
  "completed_at": "<ISO8601>",
  "dry_run": false,
  "steps": [
    { "step": 1, "operation": "OrdersForVariantPerformance", "type": "query", "params_summary": "<date_range_start> a <date_range_end>", "result_summary": "<n> pedidos, <n> variantes totalizadas", "skipped": false },
    { "step": 2, "operation": "VariantInventorySnapshot", "type": "query", "params_summary": "<n> IDs de variantes", "result_summary": "<n> variantes con datos de inventario", "skipped": false }
  ],
  "outcome": {
    "orders_processed": 0,
    "variants_analysed": 0,
    "date_range_start": "<date_range_start>",
    "date_range_end": "<date_range_end>",
    "sort_by": "revenue",
    "top_performers": [],
    "dead_weight": [],
    "errors": 0,
    "output_file": "variant_performance_<fecha>.csv"
  }
}
```

## Formato de salida
Archivo CSV `variant_performance_<YYYY-MM-DD>.csv` con una fila por variante:

| Columna | Descripción |
|---------|-------------|
| `product_id` | GID del producto en Shopify |
| `product_title` | Nombre del producto |
| `variant_id` | GID de la variante en Shopify |
| `variant_title` | Combinación de opciones (p. ej., "Azul / Grande") |
| `sku` | SKU de la variante |
| `units_sold` | Total de unidades vendidas en el periodo |
| `gross_revenue` | Ingresos antes de reembolsos |
| `refunded_amount` | Valor total reembolsado |
| `net_revenue` | Ingresos brutos menos reembolsos |
| `refund_rate_pct` | Unidades reembolsadas ÷ unidades vendidas × 100 |
| `inventory_qty` | Stock disponible actual |
| `revenue_per_inventory_unit` | Ingresos netos ÷ cantidad en inventario (vacío si el inventario es 0) |

Para `format: human`, se muestran dos tablas clasificadas en la respuesta:
1. **Las que más rinden**: las `top_n` mejores variantes según la métrica `sort_by`
2. **Peso muerto**: las `top_n` variantes con menor `revenue_per_inventory_unit` (con al menos `min_units` vendidas e inventario > 0)

## Manejo de errores
| Error | Causa | Solución |
|-------|-------|----------|
| `THROTTLED` | Límite de solicitudes de la API al paginar un historial de pedidos grande | Espera 2 s y reintenta hasta 3 veces; si persiste, acorta el rango de fechas |
| `variant` es nulo en la línea del pedido | El producto o la variante se eliminó después de la compra | Totaliza por el título de la línea con `variant_id: null`; igual cuenta en los totales |
| `inventoryQuantity` es nulo | La variante usa un servicio de preparación de pedidos externo (sin inventario con seguimiento) | Regístralo como `inventory_qty: null` y exclúyelo de los ingresos por unidad en inventario |
| No se devuelven pedidos | No hay pedidos en el rango de fechas | Amplía el rango de fechas |

## Buenas prácticas
1. Empieza con una ventana de 30 a 90 días. Las ventanas muy amplias generan cadenas largas de paginación y hacen el paso 1 mucho más lento.
2. La columna `revenue_per_inventory_unit` es la señal más clara de peso muerto: mucho inventario con ingresos casi nulos es un candidato claro a rebaja.
3. Una `refund_rate_pct` alta en una talla o un color específico suele indicar un problema de horma o de calidad: investígalo antes de volver a pedir esa opción.
4. Usa `min_units: 5` para filtrar el ruido estadístico de las variantes con muy pocas ventas antes de tomar decisiones de surtido.
5. Combínala con `shopify-inventario-muerto` para ver la salud del inventario más allá del periodo de ventas que cubre este reporte.
