---
name: shopify-retener-pedidos
description: "Pone o quita retenciones de preparación en lote sobre pedidos abiertos, con un motivo y una fecha de vencimiento opcional. Úsala para frenar pedidos en revisión por fraude, falta de stock o pago pendiente, y liberarlos cuando se resuelvan."
---

## Propósito
Pone o quita retenciones en las órdenes de preparación (fulfillment orders) de forma automatizada, sin navegar por el admin de Shopify. Sirve para colas de revisión por fraude, faltantes de inventario o flujos de verificación de pago. Funciona con pedidos cuyas órdenes de preparación están en estado `OPEN`.

## Requisitos
- Sesión autenticada de Shopify CLI: `shopify store auth --store <tu-tienda>.myshopify.com --scopes read_orders,write_merchant_managed_fulfillment_orders`. Las consultas se ejecutan con `shopify store execute`.
- Permisos (scopes) de API: `read_orders`, `write_merchant_managed_fulfillment_orders`

## Parámetros
Generales (store, format, dry_run) + propios de esta skill:

| Parámetro | Tipo | Obligatorio | Predeterminado | Descripción |
|-----------|------|-------------|----------------|-------------|
| action | string | sí | — | `hold` (retener) o `release` (liberar) |
| order_ids | array | no* | — | Lista de GID de pedidos a procesar (p. ej., `["gid://shopify/Order/123"]`) |
| query_filter | string | no* | — | Búsqueda de pedidos de Shopify para seleccionarlos (p. ej., `"tag:fraud-review"`) |
| reason | string | no | `OTHER` | Motivo de la retención: `AWAITING_PAYMENT`, `HIGH_RISK_OF_FRAUD`, `INCORRECT_ADDRESS`, `INVENTORY_OUT_OF_STOCK`, `OTHER` |
| reason_notes | string | no | — | Nota de texto libre visible para el equipo de despacho |
| hold_until | string | no | — | Fecha ISO 8601 en la que la retención vence sola (opcional) |

*Se necesita `order_ids` o `query_filter`.

## Seguridad

> ⚠️ El paso 2 pone o quita retenciones en órdenes de preparación reales. Retener un pedido impide despacharlo y puede retrasar la entrega. Liberar una retención permite que la preparación siga de inmediato. Ejecuta con `dry_run: true` para ver qué pedidos se verán afectados antes de confirmar.

## Pasos

1. **OPERACIÓN:** `orders` — query
   **Entradas:** Lista `order_ids` o texto `query_filter`; trae los `fulfillmentOrders` de cada pedido para obtener los IDs de las órdenes de preparación y su `status` actual
   **Resultado esperado:** Lista de GID de órdenes de preparación con su `status` actual; omite las que ya están en el estado buscado (ya retenidas / no retenidas)

2. **OPERACIÓN:** `fulfillmentOrderHold` — mutation (si `action: hold`)
   **Entradas:** `id: <fulfillmentOrderId>`, `fulfillmentHold: { reason, reasonNotes, holdUntilDate }` por cada orden de preparación
   **Resultado esperado:** `fulfillmentOrder.status: ON_HOLD` actualizado, `userErrors`

   **O BIEN**

2. **OPERACIÓN:** `fulfillmentOrderReleaseHold` — mutation (si `action: release`)
   **Entradas:** `id: <fulfillmentOrderId>` por cada orden de preparación retenida
   **Resultado esperado:** `fulfillmentOrder.status: OPEN` actualizado, `userErrors`

## Operaciones GraphQL

```graphql
# orders:query — validado con api_version 2025-01
query OrdersForHold($first: Int!, $after: String, $query: String) {
  orders(first: $first, after: $after, query: $query) {
    edges {
      node {
        id
        name
        displayFulfillmentStatus
        fulfillmentOrders(first: 5) {
          edges {
            node {
              id
              status
              requestStatus
            }
          }
        }
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}
```

```graphql
# fulfillmentOrderHold:mutation — validado con api_version 2025-01
mutation FulfillmentOrderHold($id: ID!, $fulfillmentHold: FulfillmentOrderHoldInput!) {
  fulfillmentOrderHold(id: $id, fulfillmentHold: $fulfillmentHold) {
    fulfillmentOrder {
      id
      status
    }
    remainingFulfillmentOrder {
      id
      status
    }
    userErrors {
      field
      message
    }
  }
}
```

```graphql
# fulfillmentOrderReleaseHold:mutation — validado con api_version 2025-01
mutation FulfillmentOrderReleaseHold($id: ID!) {
  fulfillmentOrderReleaseHold(id: $id) {
    fulfillmentOrder {
      id
      status
    }
    userErrors {
      field
      message
    }
  }
}
```

## Seguimiento de la sesión

**Claude DEBE mostrar lo siguiente en cada etapa. Es obligatorio.**

**Al iniciar**, muestra:
```
╔══════════════════════════════════════════════╗
║  SKILL: Retén y libera pedidos               ║
║  Tienda: <dominio de la tienda>              ║
║  Inicio: <AAAA-MM-DD HH:MM UTC>              ║
╚══════════════════════════════════════════════╝
```

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

Si `dry_run: true`, antepón `[DRY RUN]` a cada paso de mutación y no lo ejecutes.

**Al terminar**, muestra:

Para `format: human` (predeterminado):
```
══════════════════════════════════════════════
RESUMEN DEL RESULTADO
  Pedidos procesados:                          <n>
  Órdenes de preparación retenidas/liberadas:  <n>
  Omitidas (ya estaban en el estado buscado):  <n>
  Errores:                                     0
  Archivo:                                     ninguno
══════════════════════════════════════════════
```

Para `format: json`, muestra:
```json
{
  "skill": "shopify-retener-pedidos",
  "store": "<dominio>",
  "started_at": "<ISO8601>",
  "completed_at": "<ISO8601>",
  "dry_run": false,
  "steps": [
    { "step": 1, "operation": "OrdersForHold", "type": "query", "params_summary": "<filtro o ids>", "result_summary": "<n> órdenes de preparación encontradas", "skipped": false },
    { "step": 2, "operation": "FulfillmentOrderHold|FulfillmentOrderReleaseHold", "type": "mutation", "params_summary": "<n> órdenes de preparación, action: <hold|release>", "result_summary": "<n> actualizadas", "skipped": false }
  ],
  "outcome": {
    "action": "<hold|release>",
    "orders_targeted": "<n>",
    "fulfillment_orders_affected": "<n>",
    "skipped": "<n>",
    "errors": 0,
    "output_file": null
  }
}
```

## Formato de salida
Sin CSV. Tabla resumen en pantalla:
| Pedido | ID de la orden de preparación | Estado anterior | Estado nuevo |
|--------|-------------------------------|-----------------|--------------|
| #1001 | gid://...FulfillmentOrder/456 | OPEN | ON_HOLD |

## Manejo de errores
| Error | Causa | Solución |
|-------|-------|----------|
| `fulfillmentOrder.status` ya está en `ON_HOLD` | El pedido ya estaba retenido | Se omite automáticamente y queda registrado en la salida |
| `fulfillmentOrder.status` no es `OPEN` ni `ON_HOLD` | El pedido ya se despachó, se canceló o está en proceso | No se puede retener ni liberar; omítelo y repórtalo |
| Motivo `INVENTORY_OUT_OF_STOCK` sin nota | Algunas tiendas exigen una nota para este motivo | Agrega el parámetro `reason_notes` |
| `userErrors` en la mutación | Permisos insuficientes o estado inválido de la orden de preparación | Verifica el permiso `write_merchant_managed_fulfillment_orders` |

## Buenas prácticas
1. Usa `query_filter: "tag:fraud-review"` para retener en un solo comando todos los pedidos que tu proceso antifraude haya etiquetado.
2. Define `hold_until` para que las retenciones se liberen solas al terminar el periodo de revisión: así ningún pedido de un cliente se queda retenido por olvido.
3. Cuando uses `query_filter`, ejecuta siempre primero con `dry_run: true`: confirma la cantidad de pedidos antes de retener docenas de una vez.
4. Combínala con `shopify-resumen-despachos` para identificar los pedidos retenidos y liberarlos en lote cuando resuelvas las excepciones.
5. El motivo `AWAITING_PAYMENT` es visible para el equipo de despacho y explica por qué un pedido está en pausa.
