> ## Documentation Index
> Fetch the complete documentation index at: https://docs.appreval.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks de Mercado Pago y Creación de Pedidos

> Ciclo de cobro recurrente: verificación de webhooks de MP con x-signature, idempotencia por metafield, creación de pedidos en Shopify y reintentos.

Cada cobro de una suscripción recorre el mismo ciclo: el servidor de Reval inicia el pago via API de MP, MP procesa la transacción y notifica el resultado mediante un webhook, el servidor verifica la autenticidad del webhook y — si el pago fue aprobado — crea un pedido real en Shopify. Este documento describe cada etapa de ese ciclo, incluyendo los mecanismos de idempotencia que evitan duplicados y el job de reintentos que recupera cobros fallidos.

***

## Diagrama del ciclo de cobro recurrente

```
SERVIDOR DE REVAL (job scheduler)
        │
        │  En la fecha programada del ciclo:
        │
        ├─ 1. Consulta API de Shopify
        │      → precios, envío e impuestos actualizados
        │
        ├─ 2. Calcula el monto total del ciclo
        │
        ├─ 3. POST /v1/payments (API de MP)
        │      Body: { transaction_amount, preapproval_id, description, external_reference }
        │
        ▼
MERCADO PAGO
        │
        ├─ Procesa el pago contra la tarjeta tokenizada
        │
        ├─ Resultado: approved | rejected
        │
        └─ 4. Envía webhook al servidor de Reval
               POST /webhooks/mercadopago

        │
        ▼
SERVIDOR DE REVAL (webhook handler)
        │
        ├─ 5. Verifica firma x-signature
        │
        ├─ 6. GET /payments/{payment_id} (API de MP)
        │      → obtiene detalle completo del pago
        │
        ├─ Si status = "approved":
        │   ├─ 7. Verifica idempotencia (¿ya existe pedido con este payment_id?)
        │   ├─ 8. Crea pedido en Shopify con line_items, shipping, taxes, metafields
        │   └─ 9. Actualiza ciclo → status: completed
        │
        └─ Si status = "rejected":
            └─ 10. Actualiza ciclo → status: failed
                   (el job diario de reintentos lo retomará)
```

***

## Webhook de pago de MP

### Endpoint receptor

```
POST /webhooks/mercadopago
```

El endpoint es público (MP no puede autenticarse con un token de sesión), pero cada request es verificado criptográficamente mediante la firma del header `x-signature`.

### Verificación de firma

MP incluye dos headers en cada webhook:

| Header         | Descripción                                            |
| -------------- | ------------------------------------------------------ |
| `x-signature`  | Firma del payload. Formato: `ts=<timestamp>,v1=<hmac>` |
| `x-request-id` | ID único del request (útil para logging)               |

El proceso de verificación:

```javascript theme={null}
function verifyMercadoPagoSignature(req, secret) {
  const xSignature = req.headers['x-signature'];
  const xRequestId = req.headers['x-request-id'];
  const dataId = req.query.data?.id; // o req.body.data?.id

  // Extraer ts y v1 del header x-signature
  const parts = Object.fromEntries(
    xSignature.split(',').map(part => part.split('='))
  );
  const ts = parts['ts'];
  const receivedHash = parts['v1'];

  // Construir la cadena a firmar
  // Formato: "id:<data.id>;request-id:<x-request-id>;ts:<ts>;"
  const manifest = `id:${dataId};request-id:${xRequestId};ts:${ts};`;

  // Calcular HMAC-SHA256
  const computedHash = crypto
    .createHmac('sha256', secret)
    .update(manifest)
    .digest('hex');

  // Comparar en tiempo constante
  return crypto.timingSafeEqual(
    Buffer.from(computedHash),
    Buffer.from(receivedHash)
  );
}
```

<Warning>
  Nunca proceses un webhook de MP sin verificar la firma primero. Un actor malicioso podría enviar requests forjados para disparar la creación de pedidos sin que haya un pago real de por medio.
</Warning>

### Estructura del payload del webhook

MP envía un payload minimalista — contiene el tipo de evento y el ID del objeto, pero **no el detalle completo del pago**:

```json theme={null}
{
  "action": "payment.updated",
  "api_version": "v1",
  "data": {
    "id": "1234567890"
  },
  "date_created": "2024-01-15T10:30:00.000-03:00",
  "id": 109876543,
  "live_mode": true,
  "type": "payment",
  "user_id": "123456789"
}
```

<Note>
  MP envía el **ID del pago** (`data.id`), no el payload completo. El servidor de Reval debe hacer un `GET /v1/payments/{id}` a la API de MP para obtener el detalle completo: monto, status, método de pago, `external_reference` (que vincula el pago con el ciclo de Reval), y demás campos necesarios para crear el pedido. Esto es intencional por parte de MP para reducir el tamaño del payload y forzar una consulta autenticada.
</Note>

### Detalle completo del pago (GET a MP)

Después de verificar la firma, el servidor hace el GET para obtener:

```json theme={null}
{
  "id": 1234567890,
  "status": "approved",
  "status_detail": "accredited",
  "transaction_amount": 4850.00,
  "currency_id": "ARS",
  "external_reference": "reval_cycle_abc123_xyz789",
  "payer": {
    "id": 123456789,
    "email": "cliente@ejemplo.com"
  },
  "payment_method_id": "visa",
  "payment_type_id": "credit_card",
  "date_approved": "2024-01-15T10:30:05.000-03:00"
}
```

El campo `external_reference` contiene el ID del ciclo de Reval, que el servidor usó al ejecutar el cobro. Esto vincula unívocamente el pago con la suscripción y el ciclo correspondiente.

***

## Garantía de idempotencia

MP puede enviar el mismo webhook más de una vez (reintentos por timeout, fallos de red). La idempotencia garantiza que, sin importar cuántas veces llegue el webhook de un pago aprobado, solo se crea **un único pedido en Shopify**.

### Mecanismo de deduplicación

```
1. El servidor recibe el webhook con data.id = "1234567890"
2. Hace GET a MP → obtiene payment_id = 1234567890
3. Busca en Shopify si existe algún pedido con metafield:
      namespace: "reval"
      key: "mp_payment_id"
      value: "1234567890"
4. Si existe → devuelve HTTP 200 sin crear nada (idempotente)
5. Si no existe → crea el pedido con ese metafield incluido
```

El metafield `reval.mp_payment_id` actúa como clave de deduplicación durable. Al estar en Shopify (la fuente de verdad), persiste incluso si la base de datos de Reval se reconstruye.

***

## Creación de pedido en Shopify

Cuando el pago es aprobado y el check de idempotencia confirma que no existe un pedido previo, el servidor crea un pedido estándar en Shopify via la Admin API.

### Estructura del pedido creado

```json theme={null}
POST /admin/api/2024-01/orders.json
{
  "order": {
    "customer": {
      "id": 123456
    },
    "line_items": [
      {
        "variant_id": 789,
        "quantity": 2,
        "price": "1500.00",
        "title": "Café de especialidad - 250g",
        "requires_shipping": true
      }
    ],
    "shipping_lines": [
      {
        "title": "Envío estándar",
        "price": "350.00",
        "code": "standard"
      }
    ],
    "tax_lines": [
      {
        "title": "IVA",
        "price": "500.00",
        "rate": 0.21
      }
    ],
    "financial_status": "paid",
    "source_name": "reval",
    "note": "Suscripción mensual — Ciclo #4",
    "metafields": [
      {
        "namespace": "reval",
        "key": "mp_payment_id",
        "value": "1234567890",
        "type": "single_line_text_field"
      },
      {
        "namespace": "reval",
        "key": "subscription_id",
        "value": "reval_sub_abc123",
        "type": "single_line_text_field"
      },
      {
        "namespace": "reval",
        "key": "cycle_number",
        "value": "4",
        "type": "number_integer"
      }
    ]
  }
}
```

### Por qué cada cobro genera un pedido separado

Cada ciclo de suscripción genera su propio pedido en Shopify por las siguientes razones:

* **Fulfillment**: cada envío mensual es una entrega distinta que necesita su propio tracking
* **Inventario**: Shopify descuenta el stock al crear el pedido; sin pedido separado, el inventario no se actualiza
* **Analítica**: los reportes de ventas de Shopify (y apps conectadas como Google Analytics) necesitan un evento de compra por cada cobro real
* **Contabilidad**: cada pedido genera su propia línea en los exports de ventas y en las integraciones con ERPs o facturación electrónica
* **Soporte**: el equipo de atención al cliente puede ver el historial completo de envíos del suscriptor en la timeline del cliente en Shopify

***

## Job de reintentos

El job de reintentos es un proceso que se ejecuta diariamente y recupera los ciclos que fallaron por rechazo de la tarjeta o error temporal de MP.

### Frecuencia y criterios de ejecución

```
Frecuencia: diaria (configurable via cron)
Hora de ejecución: configurable (default: 09:00 hora local del merchant)

Criterios para reintentar un ciclo:
  - status = "failed"
  - fecha del primer fallo ≤ N días atrás (N configurable, default: 7)
  - intentos_fallidos < MAX_INTENTOS (configurable, default: 3)
  - suscripción en status "active" (no pausada ni cancelada)
```

### Flujo del job

```
Job de reintentos (diario)
    │
    ├─ Consulta DB: ciclos con status=failed dentro de la ventana
    │
    ├─ Para cada ciclo fallido:
    │   ├─ Calcula monto actual (reconsulta precios/envío/impuestos a Shopify)
    │   ├─ Ejecuta cobro via API de MP (mismo mandato, monto recalculado)
    │   │
    │   ├─ Si aprobado:
    │   │   ├─ Crea pedido en Shopify (con idempotencia)
    │   │   ├─ Ciclo → status: completed
    │   │   └─ Reinicia contador de intentos fallidos consecutivos
    │   │
    │   └─ Si rechazado:
    │       ├─ Incrementa intentos_fallidos_consecutivos
    │       └─ Si intentos_fallidos_consecutivos >= MAX_INTENTOS:
    │           ├─ Suscripción → status: paused
    │           ├─ Envía email de notificación al suscriptor
    │           │  con link al portal para actualizar método de pago
    │           └─ Registra evento en el log de la suscripción
    │
    └─ Fin del job
```

### Comportamiento tras múltiples fallos consecutivos

<Tabs>
  <Tab title="Pausa automática">
    Cuando una suscripción supera el máximo de intentos fallidos consecutivos:

    1. La suscripción pasa a `status: paused` en la DB de Reval
    2. No se programan nuevos ciclos hasta que el suscriptor reactive
    3. Se envía un email automático al suscriptor con un link firmado al portal
    4. Desde el portal, el suscriptor puede actualizar su método de pago y reactivar la suscripción
    5. Al reactivar, se programa el próximo ciclo desde la fecha actual (no se cobran los períodos pausados)
  </Tab>

  <Tab title="Notificación al merchant">
    El admin embebido muestra las suscripciones pausadas por fallos en un panel dedicado. El merchant puede:

    * Ver el historial de intentos fallidos con los códigos de error de MP
    * Contactar manualmente al suscriptor
    * Reactivar la suscripción manualmente (si ya coordinó el pago por otro medio)
    * Cancelar la suscripción definitivamente
  </Tab>
</Tabs>

<Accordion title="Códigos de rechazo comunes de MP y su tratamiento">
  | Código MP                            | Descripción                              | Acción de Reval                               |
  | ------------------------------------ | ---------------------------------------- | --------------------------------------------- |
  | `cc_rejected_insufficient_amount`    | Fondos insuficientes                     | Reintento en el próximo ciclo del job         |
  | `cc_rejected_bad_filled_card_number` | Número de tarjeta inválido               | Reintento — puede ser error transitorio de MP |
  | `cc_rejected_card_disabled`          | Tarjeta bloqueada/vencida                | Reintento; si falla N veces, pausa y notifica |
  | `cc_rejected_call_for_authorize`     | Requiere autorización del banco          | Reintento en siguiente ciclo                  |
  | `cc_rejected_duplicated_payment`     | Posible cobro duplicado detectado por MP | No reintenta; se investiga manualmente        |
  | `cc_rejected_high_risk`              | Transacción marcada como alto riesgo     | Reintento; si persiste, escalar               |

  Los códigos de rechazo están disponibles en el detalle del pago bajo `status_detail` en la respuesta de `GET /v1/payments/{id}`.
</Accordion>

***

## Resumen de estados de un ciclo

```
scheduled → (en fecha programada) → executing
                                          │
                              ┌───────────┴───────────┐
                              ▼                       ▼
                          approved               rejected
                              │                       │
                              ▼                       ▼
                         completed                 failed
                                                      │
                                          (job de reintentos)
                                                      │
                                        ┌─────────────┴──────────┐
                                        ▼                        ▼
                                   completed            (max intentos)
                                                              │
                                                              ▼
                                                   suscripción: paused
```
