---
name: shopify-cancelar-y-reponer-stock
description: "Cancela un pedido sin despachar y, si quieres, devuelve el stock al inventario, reembolsa y avisa al cliente, todo en un solo flujo validado. Úsala cuando necesites cancelar un pedido por fraude, falta de stock o porque el cliente lo pidió antes del despacho."
---

## Propósito
Cancela un pedido sin despachar o despachado solo en parte, con opciones configurables para reponer el stock, reembolsar y notificar al cliente, sin tener que navegar por el panel de Shopify. Sirve para manejar excepciones por fraude, cancelaciones por falta de stock o cancelaciones que pide el cliente antes del despacho. No puede cancelar pedidos que ya se despacharon por completo.

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

## Parámetros

| Parámetro | Tipo | Obligatorio | Predeterminado | Descripción |
|-----------|------|-------------|----------------|-------------|
| store | string | sí | — | Dominio de la tienda (p. ej., mitienda.myshopify.com) |
| format | string | no | human | Formato de salida: `human` o `json` |
| dry_run | bool | no | false | Muestra las operaciones sin ejecutar las mutaciones |
| order_id | string | sí | — | GID del pedido (p. ej., `gid://shopify/Order/12345`) |
| reason | string | no | `OTHER` | Motivo de la cancelación: `CUSTOMER`, `DECLINED`, `FRAUD`, `INVENTORY`, `STAFF`, `OTHER` |
| restock | bool | no | true | Devuelve al inventario las unidades de los artículos cancelados |
| refund | bool | no | true | Reembolsa cualquier pago ya cobrado |
| notify_customer | bool | no | true | Envía al cliente el correo de cancelación |
| staff_note | string | no | — | Nota interna que queda registrada en la cancelación |

## Seguridad

> ⚠️ El paso 2 ejecuta `orderCancel`, que es irreversible. Un pedido cancelado no se puede reabrir. Si `refund: true`, cualquier pago cobrado se reembolsa automáticamente. Si `restock: true`, las cantidades de inventario se restauran de inmediato. Ejecuta con `dry_run: true` para verificar el estado del pedido y confirmar que se puede cancelar antes de hacerlo de verdad.

## Pasos

1. **OPERACIÓN:** `order` — consulta
   **Entradas:** `id: <order_id>`
   **Resultado esperado:** `name` del pedido, `displayFulfillmentStatus`, `displayFinancialStatus`, `cancelledAt` (debe ser null), `fulfillmentOrders.status` (debe ser `OPEN` u `ON_HOLD`; detén el proceso si alguna orden de preparación está en `IN_PROGRESS` o `CLOSED`)

2. **OPERACIÓN:** `orderCancel` — mutación
   **Entradas:** `orderId`, `reason`, `restock`, `refund`, `notifyCustomer`, `staffNote`
   **Resultado esperado:** `orderCancelUserErrors` vacío si todo sale bien; el pedido queda cancelado con la marca de tiempo `cancelledAt`

## Operaciones GraphQL

```graphql
# order:query — validada con api_version 2025-01
query OrderForCancel($id: ID!) {
  order(id: $id) {
    id
    name
    displayFulfillmentStatus
    displayFinancialStatus
    cancelledAt
    totalPriceSet {
      shopMoney { amount currencyCode }
    }
    lineItems(first: 50) {
      edges {
        node {
          id
          title
          quantity
          variant {
            id
            sku
            inventoryQuantity
          }
        }
      }
    }
    fulfillmentOrders(first: 5) {
      edges {
        node {
          id
          status
        }
      }
    }
    customer {
      id
      defaultEmailAddress {
        emailAddress
      }
      firstName
      lastName
    }
  }
}
```

```graphql
# orderCancel:mutation — validada con api_version 2025-01
mutation OrderCancel(
  $orderId: ID!
  $reason: OrderCancelReason!
  $restock: Boolean!
  $refund: Boolean!
  $notifyCustomer: Boolean!
  $staffNote: String
) {
  orderCancel(
    orderId: $orderId
    reason: $reason
    restock: $restock
    refund: $refund
    notifyCustomer: $notifyCustomer
    staffNote: $staffNote
  ) {
    orderCancelUserErrors {
      field
      message
    }
    userErrors {
      field
      message
    }
  }
}
```

## Seguimiento de la sesión

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

**Al iniciar**, muestra:
```
╔══════════════════════════════════════════════╗
║  SKILL: shopify-cancelar-y-reponer-stock     ║
║  Tienda: <dominio de la tienda>              ║
║  Inicio: <AAAA-MM-DD HH:MM UTC>              ║
╚══════════════════════════════════════════════╝
```

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

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

**Al terminar**, muestra:

Para `format: human` (predeterminado):
```
══════════════════════════════════════════════
RESUMEN DEL RESULTADO
  Pedido:                  <name>
  Motivo de cancelación:   <reason>
  Stock repuesto:          <true|false>
  Reembolso emitido:       <true|false>
  Cliente notificado:      <true|false>
  Errores:                 0
  Archivo:                 ninguno
══════════════════════════════════════════════
```

Para `format: json`, muestra:
```json
{
  "skill": "shopify-cancelar-y-reponer-stock",
  "store": "<dominio>",
  "started_at": "<ISO8601>",
  "completed_at": "<ISO8601>",
  "dry_run": false,
  "steps": [
    { "step": 1, "operation": "OrderForCancel", "type": "query", "params_summary": "pedido <id>", "result_summary": "<estado>", "skipped": false },
    { "step": 2, "operation": "OrderCancel", "type": "mutation", "params_summary": "reason: <reason>, restock: <bool>, refund: <bool>, notifyCustomer: <bool>", "result_summary": "cancelado el <timestamp>", "skipped": false }
  ],
  "outcome": {
    "order_name": "<name>",
    "reason": "<reason>",
    "restocked": true,
    "refund_issued": true,
    "customer_notified": true,
    "errors": 0,
    "output_file": null
  }
}
```

## Formato de salida
No genera CSV. El resumen de la sesión muestra el resultado de la cancelación. Si `restock: true`, lista los SKU de las variantes y las cantidades repuestas.

## Manejo de errores
| Error | Causa | Solución |
|-------|-------|----------|
| `cancelledAt` no es null | El pedido ya está cancelado | No hay que hacer nada |
| Orden de preparación con `status: IN_PROGRESS` | El pedido se está alistando o empacando | Pide a la bodega que lo detenga: una vez en IN_PROGRESS no se puede cancelar desde aquí |
| `orderCancelUserErrors` | El pedido ya se despachó por completo | Usa la skill `shopify-reembolso-y-reposicion` |
| Pedido no encontrado | GID de pedido no válido | Usa la skill `shopify-buscar-pedido` para encontrar el ID correcto |

## Buenas prácticas
1. Ejecuta siempre primero con `dry_run: true`: revisa `displayFulfillmentStatus` y el estado de las órdenes de preparación antes de cancelar.
2. Usa `reason: FRAUD` en pedidos de alto riesgo: ese motivo queda registrado en los análisis de fraude de Shopify.
3. Si el pago ya se cobró (estado `PAID`), usa `refund: true`: una cancelación sin reembolso termina en reclamos del cliente.
4. Para cancelaciones en lote (p. ej., cuando se agota un producto), recorre los IDs de pedido con `format: json` para guardar cada resultado como registro de auditoría.
5. Si la bodega ya empezó a alistar el pedido, no lo canceles desde aquí: comunícate directamente con ellos y cancela solo cuando confirmen que no se ha hecho ningún trabajo físico.
