# POST /v1/alerts

Contrato del aviso «Avisame cuando vuelva». El servidor lo implementa el backend de Nube Lift. `features/alerts` no levanta otro server: solo arma el body, valida el email y lo muestra en el storefront.

El formulario es por variante. Si esa variante tiene stock, no se muestra. No hay WhatsApp ni botón de chat.

## Request

`POST /v1/alerts`

`Content-Type: application/json`

`Accept: application/json`

Sin header de autorización y sin cookies (`credentials: "omit"`). Solo captura de email. No hay WhatsApp, SMS ni bandeja.

```json
{
  "storeId": "1042",
  "productId": "8831",
  "variantId": "902",
  "email": "ana@tienda.com",
  "newsletter": false
}
```

| Campo | Tipo | Regla |
| --- | --- | --- |
| `storeId` | string | Obligatorio. Sale de `data-store-id`. Sin espacios, 1 a 64 caracteres. Un número en JS se convierte a string. |
| `productId` | string | Obligatorio. Sale de `data-product-id` en el momento del envío. Misma regla que `storeId`. |
| `variantId` | string | Obligatorio. Sale de `data-variant-id` en el aviso, o del nodo `product-form-{productId}` si el aviso no lo trae. |
| `email` | string | Obligatorio. Se recorta y se pasa a minúsculas. Tiene que pasar `validateEmail`. |
| `newsletter` | boolean | La casilla de novedades, aparte del aviso. Nace desmarcada, así que el default es `false`. Solo `true` si la marcaron. No es condición para anotar el aviso. |

No viajan otras claves. El título del producto no se envía: es solo texto visible y se escapa antes de entrar al DOM.

El formulario se muestra solo cuando el stock es `"0"`. Otro valor (`"4"`, `"infinite"`, vacío) lo esconde.

## De dónde sale la variante

Si el aviso trae `data-variant-id` y `data-variant-stock`, se usan esos y no se pisan.

Si falta uno de los dos, se leen del nodo `data-store="product-form-{productId}"` (`productId` es `data-product-id`):

| Dato | Orden |
| --- | --- |
| `variantId` | `data-variant-id` en ese nodo, después en un hijo, después `input[name="variant_id"]` dentro del nodo. |
| stock | `data-variant-stock` en ese nodo, después en un hijo, después el sufijo de `data-store="stock-product-{productId}-{stock}"` dentro del nodo. Si ahí no está, se usa el mismo ancla en el documento: es el que la tienda reescribe al cambiar de variante (`0`, un entero o `infinite`). |

Sin un `variantId` o sin stock `"0"`, el formulario no aparece.

## Respuestas que entiende el cliente

El cliente no exige un body.

| Status | UI |
| --- | --- |
| 200–299 | «Anotamos tu email. Cuando esta variante vuelva, te vamos a avisar por email.» No dice que el envío ya está activo. |
| 409 | El mismo texto. El email ya estaba anotado para esa variante. |
| Otro 4xx o 5xx | Error: «No pudimos registrarte, probá de nuevo» |
| `fetch` rechazado, o `navigator.onLine === false` | En el demo (`data-show-payload` o `?offline=1`) se muestra el JSON que se habría enviado. En el snippet de tienda, el mismo fallo se ve como error. |

Un email inválido no dispara el POST. El formulario muestra «Revisá el email».

## Dónde corre

`storefront.mjs` usa `document`. Sirve para la demo y para el script de página. No es un render de NubeSDK: ese worker no tiene DOM. El id del producto se lee de `data-product-id`. La variante y el stock, si el aviso no los trae, salen de `data-store="product-form-{productId}"` como dice la sección de arriba.

## Dónde se usa

- Storefront: `storefront.mjs` monta `[data-nube-alert][data-product-id]`.
- Demo en español: `demo/index.html`. Talle S con stock, M y L sin stock. Postea a `/v1/alerts` y, si no puede, muestra el payload.
- Funciones puras: `buildPayload`, `validateEmail`, `variantNeedsAlert`, `readVariant`, `escapeHtml`, `interpretStatus` en `client.mjs`.

```bash
node --test features/alerts/client.test.mjs
```
