---
name: posfel-ecommerce-integration
description: Integrar una web/ecommerce externo con POSFEL para que POSFEL administre pedidos, stock, clientes y factura FEL. Usa el checkout alojado del marketplace — la web nunca toca tarjetas ni crea ventas por su cuenta.
---

# Skill: integrar una web externa con POSFEL

Eres una IA construyendo o modificando la web de un comercio que usa POSFEL
(posfel.com). Sigue estas instrucciones AL PIE DE LA LETRA. La guía extendida
para humanos es https://posfel.com/integraciones/guia.

## Principios innegociables

1. **POSFEL es la fuente de verdad** de productos, precios, stock, pedidos y
   facturas. La web solo muestra y redirige.
2. **NUNCA generes un formulario de tarjeta** (número/exp/CVC) en la web. El
   cobro lo hace la pasarela configurada en POSFEL vía el checkout alojado.
3. **NUNCA inventes endpoints de POSFEL.** Los únicos puntos de entrada son:
   el sync de catálogo (`/api/integrations/catalog-sync`, Bearer `pfi_…`) y el
   checkout alojado (abajo). No hay stock en vivo con CORS, ni checkout
   embebido, ni webhooks salientes — si el usuario los pide, dile que es fase 2
   de POSFEL y ofrece la alternativa de esta skill.
4. Los precios que pintes en el HTML son decorativos: el checkout re-lee el
   precio real de la BD de POSFEL. Mantenlos sincronizados, pero jamás asumas
   que controlan el cobro.
5. **El token `pfi_…` es un secreto de servidor.** Va en `POSFEL_TOKEN` o
   `.posfel-token` (agrégalo a `.gitignore`). Nunca en el JS del navegador,
   nunca commiteado.

## El flujo completo (en orden)

1. **Token**: pide al dueño que lo genere en POSFEL → Configuración →
   Integraciones → WEB PROPIA → Conectar (se muestra una sola vez).
2. **Extraer el catálogo del código**: analiza el proyecto (HTML con
   data-attributes, arrays JS, JSON, CMS…) y genera `posfel-catalog.json`
   en la raíz (schema abajo). Cada combinación vendible (color × talla) es
   UNA variante. Usa como `externalId` un slug estable derivado del código —
   nunca lo cambies entre syncs o se duplicarán productos.
3. **Push (análisis)**: corre `POSFEL_TOKEN=pfi_xxx npx posfel-sync@latest push`
   en la raíz (Node ≥ 18; sin npm: descarga
   `https://posfel.com/integrations/posfel-sync.mjs` y corre
   `node posfel-sync.mjs push`). El push NO aplica nada: genera un ANÁLISIS
   (crear/actualizar, errores con fix, checklist de configuración de la
   tienda) que queda PENDIENTE DE APROBACIÓN. Corrige los errores que
   reporte y vuelve a hacer push si es necesario.
4. **Aprobación del dueño**: avísale al dueño que apruebe el sync en
   POSFEL → Configuración → Integraciones → Web propia ("REVISAR SYNC
   PENDIENTE"). TÚ NO puedes aprobarlo — es su catálogo. Espera su OK.
5. **Cablear IDs**: tras la aprobación, corre `npx posfel-sync status` — 
   escribe `posfel.map.json` con `externalId → productId/variantId`, el
   `storeId` y la moneda. Usa ESOS IDs. Re-corre `push` (+ aprobación)
   cuando cambie el catálogo.
6. **Checkout**: Modo A (alojado) o Modo B (propio) — ver abajo.

## Schema de posfel-catalog.json

```json
{
  "version": 1,
  "site": { "name": "MI TIENDA", "url": "https://mitienda.com" },
  "removeMissing": false,
  "products": [
    {
      "externalId": "chaqueta-alpina",
      "name": "Chaqueta Alpina Pro",
      "description": "texto plano, máx 4000",
      "images": ["https://…/chaqueta-negra.webp"],
      "publish": true,
      "options": ["Color", "Talla"],
      "variants": [
        { "externalId": "negro-s", "name": "Negro / S", "price": 189.0, "stock": 5, "sku": "SHELL-NEGRO-S", "images": ["https://…/negro-frontal.webp", "https://…/negro-posterior.webp"], "optionValues": ["Negro", "S"] }
      ]
    },
    { "externalId": "gorra", "name": "Gorra básica", "price": 79.0, "stock": 20, "sku": null, "images": ["https://…"] }
  ]
}
```

Reglas duras del manifiesto:
- Con `variants[]` ⇒ el producto NO lleva `price`/`stock`/`sku` propios.
- Si la web tiene 2+ dimensiones (color y talla): declara SIEMPRE
  `options: ["Color","Talla"]` (máx 3) y en cada variante
  `optionValues: ["Negro","M"]` (mismo orden). Si omites optionValues, se
  derivan del `name` partido por `" / "` — así que nombra las variantes
  `"Negro / M"` con ese separador exacto. Esto habilita los selectores
  agrupados en el marketplace; el `name` sigue siendo el display en POS.
- `options` solo aplica a productos CON variantes.
- Sin variantes ⇒ `price` > 0 obligatorio.
- `externalId`: `a-z A-Z 0-9 . _ - |`, máx 120, estable entre syncs.
- `images`: URLs https públicas y accesibles (POSFEL las descarga a su CDN;
  máx 10/producto, la primera es la principal). Rutas relativas NO sirven.
- Cada VARIANTE también acepta `images: []` (máx 5, la primera es la
  principal) — si la web tiene foto frontal Y posterior por color, pon AMBAS
  en las `images` de CADA variante de ese color (`image` singular sigue
  funcionando pero solo carga una). Las fotos que identifican a una variante
  van en la variante, no revueltas en la galería del producto.
- La galería del producto (`images`) lleva FOTOS DEL PRODUCTO (packshots
  sobre fondo neutro). NO metas imágenes editoriales del landing (heros,
  banners, close-ups de secciones de diseño): la primera imagen es la cara
  del producto en el POS y el marketplace.
- `stock`: si la web es estática y no maneja inventario, igual manda el stock
  REAL inicial (pídeselo al dueño). Con `stock: 0` el producto queda creado
  pero NO comprable hasta que el dueño lo fije en POSFEL.
- `stock` solo aplica al CREAR; después lo administra POSFEL y el re-sync no
  lo toca. No intentes "actualizar stock" desde la web.
- Campos OMITIDOS en el manifiesto se conservan en POSFEL (sku, barcode,
  descripción): omitir ≠ borrar. Solo lo que mandas explícito sobrescribe.
- Productos desactivados a mano en POSFEL: el push los omite y los reporta
  como `skippedInactive` — NO los reactives tú; avísale al dueño para que
  decida (reactivar en POSFEL o quitarlos del manifiesto).
- Límites: 500 productos/sync, 100 variantes/producto, 5 MB, 10 syncs/10 min.

## Datos que debes pedirle al dueño de la tienda

- El **token de integración** `pfi_…` (POSFEL → Configuración → Integraciones
  → WEB PROPIA). Con el token ya no necesitas pedir el `storeId`: viene en
  `posfel.map.json`.
- Confirmación de que la tienda tiene una **pasarela con el canal "Web
  propia" encendido** (Stripe/Tilopay/CyberSource, en Configuración → Pagos —
  cada pasarela se activa POR CANAL) — el único requisito duro; sin ella el
  checkout rechaza todo. El **marketplace público es OPCIONAL**: con la `pk`
  en el cart (Modo A) o la Storefront API (Modo B), la web vende aunque esté
  deshabilitado.
- Si NO te dan token (integración manual): los IDs los sacas de
  `/web-pos/productos` o del feed `https://posfel.com/google-merchant.xml`
  (`g:id` = `p_<productId>` / `v_<variantId>`).

## Regla de variantes (causa #1 de errores)

- Producto con variantes ⇒ el ítem del carrito lleva `productId` **y**
  `variantId`. El padre tiene precio 0 y stock 0 a propósito.
- Producto sin variantes ⇒ solo `productId`.
- Si la web tiene selectores separados (color y talla), mapea cada combinación
  a su `variantId` con un diccionario generado desde `posfel.map.json`:

```js
// Generado desde posfel.map.json — regenerar tras cada `posfel-sync push`
const POSFEL_VARIANTS = {
  "Citron|S": "cmXXXXXXXXXXXXXXXXXXXXXXX",
  "Citron|M": "cmYYYYYYYYYYYYYYYYYYYYYYY",
  // …una entrada por combinación; sin fallback: si falta, deshabilita el botón
};
```

## Marcado HTML recomendado

```html
<button data-posfel-store-id="STORE_ID"
        data-posfel-product-id="PRODUCT_ID"
        data-posfel-variant-id="VARIANT_ID">Comprar</button>
```

## Carrito

Guarda por línea: `{ productId, variantId?, quantity }` (+ nombre/precio/imagen
solo para pintar). Identifica la línea por `productId|variantId`, no por nombre.

## Elegir el modo de checkout

- **Modo A — Checkout alojado (default)**: la web redirige al checkout de
  POSFEL con el carrito. Úsalo si la web NO tiene checkout propio o si quieres
  el camino más corto. Contrato abajo.
- **Modo B — Checkout PROPIO (Storefront API)**: la web ya tiene carrito y
  página de checkout y quiere conservarlos. POSFEL pone catálogo/stock en
  vivo, tarifas de envío de la tienda (nacionales + Envia intl) y la creación
  del pago con regreso a la web. Ver sección "Modo B" abajo. En AMBOS modos la
  tarjeta se ingresa SOLO en la página de la pasarela — nunca en la web.

## Modo A — Checkout alojado (el contrato por defecto)

Redirige el navegador a:

```
https://posfel.com/marketplace/checkout/multi?cart=<encodeURIComponent(JSON)>
```

```json
{ "storeId": "…", "pk": "pk_…", "items": [ { "productId": "…", "variantId": "…", "quantity": 1 } ] }
```

Incluye SIEMPRE la `pk` (viene en `posfel.map.json`): con ella los productos
de la integración son comprables aunque el marketplace público esté apagado
o no estén publicados. Sin `pk` aplican las reglas del marketplace público.

```js
function posfelCheckout(storeId, pk, items) {
  const cart = JSON.stringify({ storeId, pk, items });
  window.location.href =
    "https://posfel.com/marketplace/checkout/multi?cart=" + encodeURIComponent(cart);
}
```

Restricciones: todos los ítems de la MISMA tienda; `quantity` entero ≥ 1;
`variantId` obligatorio si hay variantes. Producto único alternativo:
`https://posfel.com/marketplace/checkout/<productId>`.

POSFEL se encarga desde ahí de: datos del comprador (incluido **NIT/CF para la
factura FEL** — no lo pidas tú), envío nacional/internacional, reserva de stock
(15 min), cobro, creación de la venta, factura FEL o recibo, y notificaciones.
Éxito: `/marketplace/checkout/exito/<orderId>`. Portal del comprador:
`/marketplace/orden/<orderId>`.

Al regresar el usuario a la web (p. ej. en `pageshow`/`visibilitychange` o en la
página a la que enlace "volver a la tienda"), vacía el carrito local solo si el
usuario confirma que pagó — o simplemente déjalo: el stock lo protege POSFEL.

## Modo B — Checkout PROPIO (Storefront API)

Cuando la web conserva su carrito y su formulario de checkout:

1. Genera el SDK: `POSFEL_TOKEN=pfi_xxx npx posfel-sync@latest sdk` → escribe
   `posfel-storefront.js` YA configurado (clave publicable pk_, moneda,
   endpoints). Inclúyelo con `<script src="posfel-storefront.js"></script>`.
   La pk_ no es secreta; el token pfi_ JAMÁS va en el navegador.
2. **Precios/stock**: pinta la vitrina con `POSFEL.catalog()` (en vivo, con
   `disponible` por variante) y formatea con `POSFEL.format(n)`. Si el
   producto tiene opciones, `catalog()` trae `optionNames` (["Color","Talla"])
   y cada variante sus `optionValues` — construye tus dos selectores con eso
   y resuelve el `variantExternalId` de la combinación elegida.
3. **Envío en el checkout de la web** — REGLA DURA: departamento, municipio,
   país y estado se eligen con SELECTS poblados desde `POSFEL.geo()`, NUNCA
   con inputs de texto libre (un municipio mal escrito no matchea la tarifa
   y Envia rechaza destinos inventados):
   - `POSFEL.geo()` → `departments` (código + nombre + municipios oficiales
     de Guatemala), `countries` (selector internacional) e `intlEnabled`.
   - Nacional: dos selects (departamento → municipio) y luego
     `POSFEL.quoteEnvio(dept.code, municipioExacto)`.
   - Internacional (solo si `intlEnabled`): select de país desde
     `geo().countries`, select de estado desde `POSFEL.states(countryCode)`,
     y `POSFEL.quoteEnvioIntl(items, {country, state, city, postalCode,
     address})` — muestra las `options` y guarda `carrier`/`service` de la
     elegida.
4. **NIT/CF**: en este modo SÍ lo pide tu formulario — NIT limpio (sin
   guiones, puede terminar en K) o vacío = Consumidor Final.
5. **Teléfono**: con SELECTOR de código de país + input numérico, NUNCA un
   solo input libre. El selector lista TODOS los países de `geo().countries`
   (cada uno trae `dial`: +502, +52, +1…) con `geo().phonePrefix` (el país
   de la tienda) preseleccionado. Envía `buyer.phone` con el código
   concatenado (`"+50255551234"`) — sin código, WhatsApp y Envia fallan con
   números extranjeros.
6. **Pagar**:
   ```js
   POSFEL.checkout({
     items: [{ externalId: "chaqueta-alpina", variantExternalId: "negro-m", quantity: 1 }],
     buyer: { name, email, phone, nit }, // phone CON código de país: "+50255551234"
     shipping: { address, department: "GU", municipality: "Mixco" }, // o {international:true,...} o null (retiro)
     successUrl: location.origin + "/gracias.html",
     cancelUrl: location.origin + "/carrito.html"
   });
   ```
   Redirige a la pasarela; el comprador vuelve a `successUrl?posfel_order=<id>`.
7. **Página de gracias** (successUrl): `POSFEL.confirmFromUrl()` espera solo
   mientras el pago siga "pending" (el webhook de la pasarela tarda unos
   segundos más que el redirect; pollea ~20s). Maneja los 3 resultados:
   - `o.paid === true` → vaciar carrito y mostrar el éxito.
   - `o.status === "pending"` (siguió pendiente) → "Estamos verificando tu
     pago…" con botón "Ver mi pedido" a `o.portalUrl`. NO vaciar el carrito.
   - `failed` / `cancelled` / `expired` → "El pago no se completó" con botón
     REINTENTAR a `o.portalUrl` (paga el MISMO pedido, stock aún reservado)
     y link de vuelta al carrito. NO vaciar el carrito.
8. **Página de cancelación** (cancelUrl): aquí cae quien canceló o le
   rechazaron la tarjeta y abandonó la pasarela; llega con `?posfel_order=`.
   El carrito local sigue intacto: muestra "No completaste el pago" con
   botón de reintentar (el `portalUrl` de `POSFEL.orderStatus(id)`) o dejar
   que pague de nuevo desde el carrito (crea pedido nuevo; el anterior
   expira solo a los 15 min y libera su stock).

Reglas duras del Modo B:
- `successUrl`/`cancelUrl` deben apuntar al dominio de la `siteUrl` de la
  integración (pide al dueño configurarla en POSFEL → Integraciones).
- Los `items` usan TUS `externalId`/`variantExternalId` del manifiesto.
- MONEDA: solo GTQ (Guatemala) o USD (El Salvador) — `POSFEL.currency` te dice
  cuál; nunca muestres precios de compra en otra moneda.
- NUNCA campos de tarjeta en la web, ni en Modo B.

## Stock y precios en la vitrina (Modo A, opcional)

Con checkout alojado la vitrina puede refrescarse igual con
`POSFEL.catalog()` (SDK) o, server-side/build, con el feed
`https://posfel.com/google-merchant.xml` (filtrar los `g:id` propios). El feed
no tiene CORS: NO fetchearlo desde el navegador.

## Checklist final antes de entregar

- [ ] `posfel-catalog.json` generado y `posfel-sync push` corrido sin errores;
      `publishBlocked` vacío (o razones resueltas: imagen/precio/stock).
- [ ] Token `pfi_…` fuera del repo y del JS del navegador (`.posfel-token` en `.gitignore`).
- [ ] Ningún input de tarjeta en la web; ninguna llamada a Stripe/Tilopay/CyberSource.
- [ ] Cada botón de compra resuelve a `productId` (+ `variantId` si aplica) de `posfel.map.json`.
- [ ] El carrito serializa `{storeId, items:[{productId, variantId?, quantity}]}`.
- [ ] La redirección usa `encodeURIComponent` sobre el JSON completo.
- [ ] Modo A: sin NIT ni formulario de facturación en la web (lo pide el
      checkout de POSFEL). Modo B: NIT/CF en tu formulario con formato limpio.
- [ ] Modo B: la página de gracias confirma con `confirmFromUrl()` y solo
      vacía el carrito si `paid === true`; pendiente/rechazo muestran estado
      y botón de reintento (`portalUrl`); existe página de cancelación con el
      carrito intacto; precios mostrados con `POSFEL.format()` en la moneda
      de la tienda (GTQ o USD, nunca otra).
- [ ] Modo B: teléfono con selector de código de país (default
      `geo().phonePrefix`) y geo con selects — nada de texto libre.
- [ ] Variantes con foto propia (ej. por color): sus `images` van EN la
      variante (frontal + posterior), no solo en la galería del producto.
- [ ] Probado: un ítem sin variante, un ítem con variante, y un ítem agotado
      (el checkout debe rechazarlo con mensaje claro).
