# Integración Tienda Nube — CD-System

> Demo target: `demo-supplements-shop`  
> Tienda de referencia: `suplementosybtuc.mitiendanube.com`  
> Fecha de implementación: 2026-05-28

---

## 1. Qué se integró

El módulo de productos de CD-System permite que el carrito de compras (`/shop/cart`) ofrezca dos caminos de pago:

1. **WhatsApp** — flujo existente (genera link wa.me con resumen del pedido).
2. **Tienda Nube** — crea un draft order en TN vía API con los datos del cliente y redirige al checkout de TN.

---

## 2. Variables de entorno requeridas (`.env`)

```dotenv
# Tienda Nube — integración de checkout para demo-supplements-shop
TIENDANUBE_STORE_ID=4409024
TIENDANUBE_ACCESS_TOKEN=<token_oauth>
TIENDANUBE_USER_AGENT="CDSystem (hola@bewpro.com)"
```

| Variable | Dónde obtenerla |
|---|---|
| `TIENDANUBE_STORE_ID` | Panel TN → Configuración → Mi cuenta → ID de la tienda (número) |
| `TIENDANUBE_ACCESS_TOKEN` | OAuth flow (ver sección 3) |
| `TIENDANUBE_USER_AGENT` | String libre con nombre de la app y email de contacto |

Estas variables se leen desde `config/services.php`:

```php
'tiendanube' => [
    'store_id'     => env('TIENDANUBE_STORE_ID', ''),
    'access_token' => env('TIENDANUBE_ACCESS_TOKEN', ''),
    'user_agent'   => env('TIENDANUBE_USER_AGENT', 'CDSystem (hola@bewpro.com)'),
],
```

---

## 3. OAuth — cómo obtener el token

### 3.1 Pre-requisitos
- Cuenta en `partners.tiendanube.com`
- App creada con los scopes: `write_products`, `write_orders`, `read_orders`
- `APP_ID` y `CLIENT_SECRET` de la app (visibles en partners → tu app)

### 3.2 Flujo de autorización

**Paso 1 — Abrir ventana incógnito** y loguearse como dueño de la tienda en `https://{tienda}.mitiendanube.com/admin`.

**Paso 2 — Abrir la URL de autorización** en la misma ventana incógnito:
```
https://www.tiendanube.com/apps/{APP_ID}/authorize?response_type=code&redirect_uri=https%3A%2F%2Fbewpro.com
```

**Paso 3 — Copiar el `code`** de la URL de redirección (`https://bewpro.com?code=XXXXXX`).

**Paso 4 — Exchangear el code por token:**
```bash
curl -X POST https://www.tiendanube.com/apps/authorize/token \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "{APP_ID}",
    "client_secret": "{CLIENT_SECRET}",
    "grant_type": "authorization_code",
    "code": "XXXXXX"
  }'
```

**Respuesta:**
```json
{
  "access_token": "3670973ed04e2f480817970afcd7f6c6d39daeba",
  "token_type": "bearer",
  "scope": "write_products write_orders read_orders",
  "user_id": 4409024
}
```

- `access_token` → `TIENDANUBE_ACCESS_TOKEN`
- `user_id` → `TIENDANUBE_STORE_ID`

### 3.3 Re-autorizar para cambiar scopes

Si el token ya está instalado y necesitás más scopes:
1. Admin TN → Configuración → Aplicaciones → Desinstalar la app
2. Partners portal → Editar app → Agregar nuevos scopes → Guardar
3. Repetir el flujo OAuth desde el paso 1

---

## 4. Migración de base de datos

Agregar la columna `tiendanube_variant_id` a la tabla `products`:

```bash
php artisan migrate
```

Archivo: `database/migrations/2026_05_28_000000_add_tiendanube_variant_id_to_products_table.php`

```php
$table->unsignedBigInteger('tiendanube_variant_id')
    ->nullable()
    ->after('sku')
    ->comment('ID de variante en Tienda Nube. Necesario para crear checkouts via API.');
```

Esta columna es la que vincula cada `Product` de CD-System con el `variant_id` correspondiente en TN. Sin ella el checkout TN no puede mapear los productos del carrito.

---

## 5. Comando de sincronización de productos

Importa / actualiza todos los productos de la tienda TN al catálogo de CD-System, incluyendo nombre, descripción, precio, stock e imágenes.

```bash
# Ver qué haría sin guardar
php artisan tiendanube:sync-products --dry-run

# Ejecutar el sync real
php artisan tiendanube:sync-products
```

**Qué hace:**
- Trae todos los productos de TN (paginados, 50 por página)
- Para cada producto, usa el primer `variant_id` (la mayoría de las tiendas no tiene multi-variante)
- Upsert por `tiendanube_variant_id`: si ya existe lo actualiza, si no lo crea
- Crea una categoría `Tienda Nube` como fallback si el producto no tiene categoría mapeada
- Sincroniza las imágenes (URLs del CDN de TN, reemplaza las anteriores)
- El slug se genera automáticamente desde el nombre (mutator en `Product`)

**Resultado esperado:**
```
Sync completado: 73 creados · 1 actualizados · 0 sin variantes (omitidos).
```

**Cuándo correrlo:**
- Después de la migración inicial
- Cuando se agregan productos nuevos en TN
- Se puede cronificar en el scheduler: `$schedule->command('tiendanube:sync-products')->daily()`

---

## 6. Archivos creados / modificados

### Nuevos
| Archivo | Función |
|---|---|
| `app/Services/TiendaNubeService.php` | Wrapper de la API de TN: `getProducts()`, `createDraftOrder()`, `getStoreDomain()`, `buildStorefrontCheckoutUrl()` |
| `app/Modules/Products/Controllers/Frontend/TiendaNubeCheckoutController.php` | Endpoint GET `/shop/checkout/tiendanube` — actualmente no se usa en el flujo principal (ver sección 7) |
| `app/Console/Commands/TiendaNubeSyncProducts.php` | Comando `tiendanube:sync-products` |
| `database/migrations/2026_05_28_000000_add_tiendanube_variant_id_to_products_table.php` | Agrega columna `tiendanube_variant_id` |

### Modificados
| Archivo | Cambio |
|---|---|
| `app/Modules/Products/Models/Product.php` | Agrega `tiendanube_variant_id` a `$fillable` |
| `app/Modules/Products/Controllers/Frontend/CheckoutController.php` | Agrega rama `checkout_method=tiendanube` → `handleTiendaNubeCheckout()` |
| `config/services.php` | Agrega bloque `tiendanube` con store_id, access_token, user_agent |
| `routes/modules/product.php` | Agrega ruta `GET /shop/checkout/tiendanube` |
| `resources/views/modules/products/frontend/demos/demo-supplements-shop/cart.blade.php` | Agrega botón "Pagar con Tienda Nube" (condicional a `config('services.tiendanube.store_id')`) |
| `resources/views/modules/products/frontend/demos/demo-supplements-shop/checkout.blade.php` | Agrega botón TN en el sidebar + hidden input `checkout_method` |

---

## 7. Flujo de checkout implementado (única estrategia viable)

> **Camino único**: Draft order via API. Las alternativas (bridge page con iframes y POST chain a `/comprar/`) fueron descartadas por imposibilidad técnica — ver § 7.X.

### Flujo activo

Trigger: link `GET /shop/checkout/tiendanube` desde el botón "Pagar con Tienda Nube" en `cart.blade.php`.

```
[Carrito CD-System con items] → click "Pagar con Tienda Nube"
    ↓
GET /shop/checkout/tiendanube
    ↓
TiendaNubeCheckoutController::redirect()
    → resuelve items del cart → array { variant_id, quantity }
    → omite productos sin tiendanube_variant_id (loggea warning)
    → si no quedan items válidos → redirect a /shop/cart con error
    → obtiene domain via TiendaNubeService::getStoreDomain()
    → llama TiendaNubeService::createDraftOrder(items, domain, customer=[])
        POST /v1/{store_id}/orders  [scope write_orders]
        body: { products: [{variant_id, quantity}, ...] }
        ← TN responde { id, token, total, ... }
    → vacía el cart local CD-System
    → redirect()->away('https://{tienda}/checkout/v3/start/{order_id}/{token}?from_store=1&country=AR')
    ↓
[Checkout NATIVO de TN]
    El cliente entra al wizard:
    1. Datos (nombre, email, teléfono)
    2. Envío (zona, método)
    3. Pago (MP / tarjeta / transferencia)  ← REQUIERE GATEWAY DIGITAL EN ADMIN TN
    4. Confirmación → /checkout/v3/success/...
```

### ⚠️ Requisito de gateway digital en TN

El draft order que creamos vía API queda con `gateway: not-provided`. Cuando TN procesa `/checkout/v3/start/{order_id}/{token}`:

- **Si el admin TN tiene Mercado Pago u otro gateway digital activo** → el cliente recorre el wizard normalmente: datos → envío → pago → success. Funciona end-to-end.

- **Si NO hay gateway digital configurado** → TN salta directo a `/checkout/v3/success/` marcando el pedido como "acordar pago externo" (transferencia/efectivo coordinado por el comercio). Para el cliente es confuso: parece que la compra se efectuó sin pagar.

**Acción requerida en el cliente Yerba Buena (suplementosybtuc)**: activar Mercado Pago en `admin TN → Configuración → Medios de pago`. Sin esto, el flow técnico funciona pero la UX al cliente final es mala.

### Variables del flow

| Campo Cart CD-System | Campo TN API |
|---|---|
| `Product::tiendanube_variant_id` (poblado por `tiendanube:sync-products`) | `variant_id` |
| `CartItem::quantity` | `quantity` |
| Customer data | **No se envía** — TN se la pide al cliente en el wizard |
| Subtotal CD | Total recalculado por TN según `variant_id`+precio actual TN |

## 7.X. Estrategias descartadas (referencia histórica)

Durante la implementación se evaluaron 4 caminos. Solo uno funciona en el plan `AR-plan-B`. Documentamos las razones por las que los otros 3 fallan, para evitar reintentar lo mismo.

### Bridge page con iframes (descartada 2026-05-29)

Vista `resources/views/modules/products/frontend/tiendanube-bridge.blade.php` (borrada). JavaScript iteraba por items del cart, hacía POST a `https://{tienda}/comprar/` vía iframes ocultos con campos `variant_id + quantity`, luego redirigía al cliente a `/comprar/` esperando ver el carrito armado.

**Por qué falla**: cuando un iframe en `bewpro.com` postea a `mitiendanube.com`, las cookies de respuesta de TN se tratan como third-party. Safari/Chrome modernos las bloquean o aíslan al iframe — no las vuelcan al window principal del cliente. Al navegar a `/comprar/` en el window principal, TN no encuentra la cookie de carrito y muestra carrito vacío.

### POST chain con `redirect_to` (descartada 2026-05-29)

Hipótesis: encadenar POSTs a `/comprar/` con un parámetro `redirect_to` que regrese al cliente a CD-System para postear el siguiente item, manteniendo cookies first-party.

**Por qué falla**: TN ignora `redirect_to` en POST (devuelve 200 + body vacío sin redirigir). Además el endpoint `/comprar/` del storefront está protegido por Cloudflare Turnstile + reCAPTCHA — un POST plano sin pasar el challenge anti-bot retorna 200 pero NO procesa el agregado al carrito (verificado: el subtotal sigue $0 después del POST).

### Querystring auto-add (`?add-to-cart=X`, `?buy=1&variant_id=X`) (descartada 2026-05-29)

Hipótesis: TN podría tener un querystring que dispare auto-add desde la página del producto.

**Por qué falla**: ningún querystring probado (`add-to-cart`, `buy`, `add`, `cart_add`, `variant_id` solo) genera item en el carrito. TN solo agrega items mediante el JS de su tema (`linkedstore-v2-*.js`) que hace AJAX a un endpoint interno (`addToCart()` minificado) — endpoint que requiere pasar el Turnstile challenge.

### Checkouts API (`POST /v1/{store}/checkouts`) (no disponible en plan)

Endpoint nativo de TN que crea una sesión de checkout con flow completo de selección de pago. Requiere plan superior a `AR-plan-B` (devuelve 404 en este plan). Es la solución limpia que recomienda TN — si se upgradeara el plan, reemplazaría a `createDraftOrder` por `createCheckout` y desaparecería el problema del gateway.

---

## 8. Plan actual y problemática

### Plan actual
- **Plan:** `AR-plan-B`
- **País:** Argentina

### Endpoint `/checkouts` — no disponible en este plan

TN tiene una API específica para crear sesiones de checkout (`POST /v1/{store_id}/checkouts`) que devuelve una `checkout_url` que lleva al cliente por el flujo completo de TN: contacto → envío → selección de método de pago → confirmación.

**Este endpoint devuelve `404` en el plan `AR-plan-B`.** No es un problema de scopes ni de token — es una limitación del plan.

```bash
# Verificado el 2026-05-28:
curl -X POST "https://api.tiendanube.com/v1/4409024/checkouts" ...
# → {"code":404,"message":"Not Found","description":null}

curl -X GET "https://api.tiendanube.com/v1/4409024/orders?per_page=1" ...
# → 200 OK (orders sí funciona con write_orders scope)
```

### Consecuencia del plan actual

El flujo implementado usa `POST /orders` que crea un **pedido completo** (no una sesión de checkout). Esto significa:

- ✅ El pedido aparece en el admin de TN con nombre, email y teléfono del cliente
- ✅ El cliente ve una pantalla de confirmación con los productos y el total
- ⚠️ El cliente **no pasa por selección de método de pago** — TN asigna el gateway por defecto (transferencia/efectivo para `suplementosybtuc`)
- ⚠️ Se crean pedidos en TN incluso si el cliente abandona después de ver la confirmación
- ❌ No hay selección de Mercado Pago, tarjeta, etc. dentro del flujo de TN

### Para el flujo de pago completo

**Opción 1 — Upgrade de plan en TN:**
Admin TN → Configuración → Plan → subir a un plan que incluya la Checkouts API.
Una vez en ese plan, cambiar `TiendaNubeCheckoutController::redirect()` para llamar a `createCheckout()` en lugar de `createDraftOrder()`. El endpoint `/checkouts` empezará a funcionar y el flujo será idéntico al de un cliente comprando directamente en TN.

**Opción 2 — Configurar Mercado Pago en TN (workaround):**
Admin TN → Configuración → Medios de pago → agregar Mercado Pago u otro gateway digital.
Con cualquier gateway digital activo, el draft order muestra opciones de pago. El cliente puede pagar con tarjeta o transferencia directamente desde TN sin que nosotros necesitemos el endpoint `/checkouts`.

---

## 9. Credenciales de referencia (tienda de prueba)

> ⚠️ **Token rotado** (2026-05-28): el `TIENDANUBE_ACCESS_TOKEN` original quedó expuesto en el commit `24f43713` del `.env.example` y debe rotarse antes de cualquier deploy a producción. Pasos:
> 1. Admin TN del cliente → Configuración → Aplicaciones → Desinstalar "CDSystem"
> 2. Repetir flujo OAuth (§ 3) para obtener nuevo `access_token`
> 3. Actualizar `.env` local + producción (NO commitear)
> 4. El `.env.example` ya quedó limpio (sin token).

| Campo | Valor |
|---|---|
| APP_ID | 32930 |
| Store ID | 4409024 |
| Store domain | `suplementosybtuc.mitiendanube.com` |
| Plan | AR-plan-B |
| Scopes del token | `write_products write_orders read_orders` |

---

## 10. Comandos de diagnóstico

```bash
# Verificar que el token funciona y tiene los scopes correctos
curl -s "https://api.tiendanube.com/v1/4409024/orders?per_page=1" \
  -H "Authentication: bearer {TOKEN}" \
  -H "User-Agent: CDSystem (hola@bewpro.com)"
# → 200 OK = token válido con read_orders
# → 403 Missing scope = falta write_orders/read_orders
# → 401 = token inválido

# Verificar disponibilidad del endpoint checkouts
curl -s -X POST "https://api.tiendanube.com/v1/4409024/checkouts" \
  -H "Authentication: bearer {TOKEN}" \
  -H "User-Agent: CDSystem (hola@bewpro.com)" \
  -H "Content-Type: application/json" \
  -d '{"items":[{"variant_id":882104211,"quantity":1}]}'
# → 404 = endpoint no disponible en este plan
# → 200 = funciona, flujo completo disponible

# Limpiar config cache después de cambiar .env
php artisan config:clear

# Re-sincronizar productos desde TN
php artisan tiendanube:sync-products
```
