From e18e1d4e8ec586b6450577860e416a8125813180 Mon Sep 17 00:00:00 2001 From: Isaac Aracena Date: Mon, 11 May 2026 08:30:22 -0400 Subject: [PATCH] feat: initial commit - lealtad demo n8n --- .gitignore | 16 + README.md | 49 +++ docs/configuracion.md | 499 ++++++++++++++++++++++++++++ n8n/plan-de-incentivo.template.json | 325 ++++++++++++++++++ supabase/schema.sql | 83 +++++ 5 files changed, 972 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 docs/configuracion.md create mode 100644 n8n/plan-de-incentivo.template.json create mode 100644 supabase/schema.sql diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..404c642 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +.env +.env.* +*.zip +*.rar +*.7z +*.log +*.tmp +.DS_Store +Thumbs.db +.vscode/ +.idea/ +exports/ +backups/ +node_modules/ +dist/ +build/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..69dc748 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# Lealtad Demo n8n + +Este proyecto es una solución automatizada para la gestión de programas de fidelización (lealtad) utilizando WhatsApp como canal de interacción, n8n como motor de flujos y Supabase como backend escalable. + +## Funcionalidades Principales + +* **Registro Automatizado**: Identifica clientes nuevos y solicita su nombre para personalizar la billetera digital. +* **Gamificación**: Actualización automática de niveles (`Member`, `Gold`, `Black`) según el saldo acumulado. +* **Consulta de Saldo**: Respuesta instantánea con el saldo actual y nivel de tier al escribir por WhatsApp. +* **Digital Wallet Integration**: Entrega enlaces personalizados para pases de Apple Wallet y Google Wallet. + +## Arquitectura del Sistema + +1. **WhatsApp (Meta API)**: Recibe y envía mensajes. +2. **n8n**: Orquesta la lógica de negocio, procesa los datos y conecta los servicios. +3. **Supabase**: Almacena clientes, empresas, transacciones y ejecuta la lógica de gamificación vía Triggers de SQL. + +## Requisitos + +* Instancia de **n8n** (v1.0+ recomendada). +* Proyecto en **Supabase**. +* Cuenta de desarrollador en **Meta** con WhatsApp Business API configurado. +* Portal de Wallet configurado para generar los pases. + +## Guía de Configuración + +### 1. Supabase +Ejecuta el script situado en `supabase/schema.sql` en tu editor SQL de Supabase. Esto creará la estructura de tablas y las funciones necesarias. + +### 2. Importación en n8n +1. Importa el archivo `n8n/plan-de-incentivo.template.json` en un nuevo workflow de n8n. +2. Configura las siguientes credenciales: + * **WhatsApp Trigger**: Credenciales OAuth2 o API Token de Meta. + * **Supabase Auth**: Configura un nodo de Header Auth con tu `apikey` y `Authorization: Bearer [TOKEN]`. + +### 3. Reemplazo de Parámetros +Busca y reemplaza los siguientes placeholders en el workflow: +* `TU_PROYECTO`: El subdominio de tu instancia de Supabase. +* `TU_PHONE_NUMBER_ID`: El ID del número de teléfono en Meta Developer Portal. +* `COMPANY_ID_DEMO`: El UUID de la empresa creada en la tabla `companies`. +* `TU_PORTAL`: El dominio de tu portal de generación de Wallet. + +## Seguridad y Privacidad + +> [!CAUTION] +> Nunca subas credenciales reales, tokens de acceso o datos personales (`pinData`) al repositorio. Utiliza variables de entorno o el gestor de credenciales de n8n. + +--- +*Desarrollado como parte de la infraestructura de fidelización digital de Digital Compass Agency.* diff --git a/docs/configuracion.md b/docs/configuracion.md new file mode 100644 index 0000000..42980ed --- /dev/null +++ b/docs/configuracion.md @@ -0,0 +1,499 @@ +# Configuración — Lealtad Demo n8n + +Este documento resume la configuración del demo de sistema de lealtad usando **Supabase**, **n8n** y **WhatsApp Business API / Meta Graph**. + +> **Nota de seguridad:** esta versión está preparada para subirse a Gitea. +> No contiene tokens, teléfonos reales, credenciales, IDs reales de producción ni datos personales. + +--- + +## 1. Objetivo del proyecto + +El flujo permite que un cliente escriba por WhatsApp y que n8n: + +1. Reciba el mensaje desde WhatsApp. +2. Extraiga el teléfono y el texto del mensaje. +3. Consulte en Supabase si el cliente existe. +4. Cree el cliente si es nuevo. +5. Maneje el estado del bot: + - `NEW` + - `AWAITING_NAME` + - `ACTIVE` +6. Responda por WhatsApp según el estado del cliente. +7. Entregue una URL de Wallet / pase digital. + +--- + +## 2. Arquitectura general + +```mermaid +flowchart LR + A[WhatsApp / Meta] --> B[n8n WhatsApp Trigger] + B --> C[Code Node: parsear mensaje] + C --> D[Supabase RPC: process_wa_customer] + D --> E{Switch por estado} + E -->|NEW| F[Actualizar a AWAITING_NAME] + F --> G[Enviar mensaje solicitando nombre] + E -->|AWAITING_NAME| H[Guardar nombre y activar cliente] + H --> I[Enviar link de Wallet] + E -->|ACTIVE| J[Responder saldo, tier y link de Wallet] +``` + +--- + +## 3. Valores que deben reemplazarse + +Antes de usar este proyecto en una demo real o producción, reemplaza los siguientes placeholders: + +| Placeholder | Descripción | +|---|---| +| `TU_PROYECTO` | Subdominio del proyecto en Supabase | +| `TU_SUPABASE_ANON_KEY` | Supabase anon key configurada en credenciales de n8n | +| `TU_TOKEN_DE_META` | Token de Meta / WhatsApp Business API | +| `TU_PHONE_NUMBER_ID` | ID del número de WhatsApp en Meta | +| `COMPANY_ID_DEMO` | UUID de la empresa creada en la tabla `companies` | +| `TU_PORTAL` | Dominio del portal donde se consulta/genera el Wallet | + +--- + +# Parte 1 — Configuración en Supabase + +En Supabase, abre el **SQL Editor**, pega el siguiente script y ejecútalo. + +Este script crea: + +- Tabla `companies` +- Tabla `customers` +- Tabla `transactions` +- Trigger de gamificación automática +- RPC `process_wa_customer` + +```sql +-- 1. Habilitar extensión para IDs seguros (UUID) +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- 2. TABLA: Empresas +CREATE TABLE companies ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name TEXT NOT NULL, + wa_phone_id TEXT -- ID del número de WhatsApp en Meta +); + +-- 3. TABLA: Clientes finales +CREATE TABLE customers ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + company_id UUID REFERENCES companies(id) ON DELETE CASCADE, + phone TEXT NOT NULL, + name TEXT, + balance NUMERIC(10,2) DEFAULT 0.00, + tier TEXT DEFAULT 'Member', + bot_state TEXT DEFAULT 'NEW', + pass_id TEXT UNIQUE DEFAULT uuid_generate_v4(), + created_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(company_id, phone) +); + +-- 4. TABLA: Transacciones +CREATE TABLE transactions ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + customer_id UUID REFERENCES customers(id) ON DELETE CASCADE, + amount_spent NUMERIC(10,2) NOT NULL, + cashback_earned NUMERIC(10,2) NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 5. TRIGGER: Gamificación automática +CREATE OR REPLACE FUNCTION update_customer_stats() RETURNS TRIGGER AS $$ +DECLARE + new_balance NUMERIC; +BEGIN + -- Sumar cashback al cliente + UPDATE customers + SET balance = balance + NEW.cashback_earned + WHERE id = NEW.customer_id + RETURNING balance INTO new_balance; + + -- Lógica de estatus dinámico + IF new_balance >= 100 THEN + UPDATE customers SET tier = 'Black' WHERE id = NEW.customer_id; + ELSIF new_balance >= 50 THEN + UPDATE customers SET tier = 'Gold' WHERE id = NEW.customer_id; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER after_transaction_insert +AFTER INSERT ON transactions +FOR EACH ROW EXECUTE FUNCTION update_customer_stats(); + +-- 6. RPC: función que n8n llama para consultar/crear cliente +CREATE OR REPLACE FUNCTION process_wa_customer( + p_phone TEXT, + p_company_id UUID +) +RETURNS TABLE ( + customer_id UUID, + state TEXT, + c_name TEXT, + pass_id TEXT, + c_balance NUMERIC, + c_tier TEXT +) AS $$ +DECLARE + v_id UUID; + v_state TEXT; + v_name TEXT; + v_pass TEXT; + v_bal NUMERIC; + v_tier TEXT; +BEGIN + -- Buscar cliente existente + SELECT id, bot_state, name, customers.pass_id, balance, tier + INTO v_id, v_state, v_name, v_pass, v_bal, v_tier + FROM customers + WHERE phone = p_phone + AND company_id = p_company_id; + + -- Si es la primera vez que escribe, crearlo en estado NEW + IF v_id IS NULL THEN + INSERT INTO customers (phone, company_id, bot_state) + VALUES (p_phone, p_company_id, 'NEW') + RETURNING id, bot_state, name, customers.pass_id, balance, tier + INTO v_id, v_state, v_name, v_pass, v_bal, v_tier; + END IF; + + RETURN QUERY SELECT v_id, v_state, v_name, v_pass, v_bal, v_tier; +END; +$$ LANGUAGE plpgsql; +``` + +--- + +## 4. Crear empresa demo + +Después de crear las tablas, inserta una empresa de prueba en `companies`. + +Ejemplo: + +```sql +INSERT INTO companies (name, wa_phone_id) +VALUES ('Empresa Demo', 'TU_PHONE_NUMBER_ID'); +``` + +Luego copia el `id` generado por Supabase. Ese valor será usado en n8n como: + +```text +COMPANY_ID_DEMO +``` + +> No subas a Git/Gitea el UUID real de producción si este repositorio se usará como plantilla. + +--- + +# Parte 2 — Flujo en n8n + +El flujo en n8n procesa mensajes de WhatsApp, consulta Supabase y responde según el estado del cliente. + +--- + +## Nodo 1 — WhatsApp Trigger + +Configura el nodo de entrada con las credenciales de WhatsApp / Meta. + +Responsabilidad: + +- Escuchar mensajes entrantes. +- Recibir payloads de WhatsApp. +- Disparar el flujo cuando llega un mensaje. + +--- + +## Nodo 2 — Code Node: Parsear JSON de Meta + +Este nodo limpia el payload de Meta y devuelve solo los campos necesarios: + +- Teléfono del cliente +- Texto recibido +- ID del número de WhatsApp +- ID de empresa demo + +```javascript +const body = $input.first().json; + +// Evitar procesar payloads sin mensajes reales +if (!body?.messages?.length) { + return []; +} + +const msg = body.messages[0]; +const phone_id = body.metadata?.phone_number_id; + +return [ + { + json: { + phone: msg.from, + text: msg.type === 'text' ? (msg.text?.body || '').trim() : '', + phone_id, + company_id: 'COMPANY_ID_DEMO' + } + } +]; +``` + +> Reemplaza `COMPANY_ID_DEMO` por el UUID real de la empresa creada en Supabase, o configura este valor mediante variables/credenciales según tu estándar interno. + +--- + +## Nodo 3 — HTTP Request: Consultar a Supabase RPC + +Este nodo consulta la función `process_wa_customer` en Supabase. + +Configuración sugerida: + +| Campo | Valor | +|---|---| +| Method | `POST` | +| URL | `https://TU_PROYECTO.supabase.co/rest/v1/rpc/process_wa_customer` | +| Authentication | Header Auth / Custom Auth | +| Header `apikey` | `TU_SUPABASE_ANON_KEY` | +| Header `Authorization` | `Bearer TU_SUPABASE_ANON_KEY` | +| Send Body | Activado | +| Body Type | JSON | + +Body: + +```json +{ + "p_phone": "{{ $json.phone }}", + "p_company_id": "{{ $json.company_id }}" +} +``` + +--- + +## Nodo 4 — Switch Node: Enrutador por estado + +Conecta la salida del HTTP Request de Supabase al Switch. + +El Switch debe evaluar el campo: + +```text +{{ $json.state }} +``` + +Rutas: + +| Ruta | Condición | Descripción | +|---|---|---| +| Ruta 0 | `state == NEW` | Cliente nuevo | +| Ruta 1 | `state == AWAITING_NAME` | Cliente está dando su nombre | +| Ruta 2 | `state == ACTIVE` | Cliente ya está activo | + +--- + +# Parte 3 — Ramas del Switch + +--- + +## Rama 0 — Estado `NEW` + +Objetivo: pedir el nombre del cliente. + +### Paso 1 — Actualizar estado en Supabase + +Nodo: HTTP Request + +| Campo | Valor | +|---|---| +| Method | `PATCH` | +| URL | `https://TU_PROYECTO.supabase.co/rest/v1/customers?id=eq.{{ $json.customer_id }}` | +| Auth | Misma configuración de Supabase | +| Header recomendado | `Prefer: return=minimal` | +| Body | JSON | + +Body: + +```json +{ + "bot_state": "AWAITING_NAME" +} +``` + +### Paso 2 — Enviar WhatsApp solicitando nombre + +Nodo: HTTP Request + +| Campo | Valor | +|---|---| +| Method | `POST` | +| URL | `https://graph.facebook.com/v19.0/{{ $('Code').item.json.phone_id }}/messages` | +| Header `Authorization` | `Bearer TU_TOKEN_DE_META` | +| Body | JSON | + +Body: + +```json +{ + "messaging_product": "whatsapp", + "to": "{{ $('Code').item.json.phone }}", + "type": "text", + "text": { + "body": "¡Hola! 👋 Bienvenido a nuestro Club VIP. Para personalizar tu Billetera Digital, ¿cómo te gusta que te llamen?" + } +} +``` + +--- + +## Rama 1 — Estado `AWAITING_NAME` + +Objetivo: guardar el nombre del cliente y activar su perfil. + +### Paso 1 — Guardar nombre y pasar a `ACTIVE` + +Nodo: HTTP Request + +| Campo | Valor | +|---|---| +| Method | `PATCH` | +| URL | `https://TU_PROYECTO.supabase.co/rest/v1/customers?id=eq.{{ $json.customer_id }}` | +| Auth | Misma configuración de Supabase | +| Body | JSON | + +Body: + +```json +{ + "bot_state": "ACTIVE", + "name": "{{ $('Code').item.json.text }}" +} +``` + +### Paso 2 — Enviar link del Wallet + +Nodo: HTTP Request + +| Campo | Valor | +|---|---| +| Method | `POST` | +| URL | `https://graph.facebook.com/v19.0/{{ $('Code').item.json.phone_id }}/messages` | +| Header `Authorization` | `Bearer TU_TOKEN_DE_META` | +| Body | JSON | + +Body: + +```json +{ + "messaging_product": "whatsapp", + "to": "{{ $('Code').item.json.phone }}", + "type": "text", + "text": { + "body": "¡Listo, {{ $('Code').item.json.text }}! 🎉\n\nTu perfil está activo. Descarga tu pase VIP y agrégalo a tu Apple/Google Wallet tocando este enlace:\n\n👉 https://TU_PORTAL/wallet/{{ $('HTTP Request').item.json.pass_id }}" + } +} +``` + +--- + +## Rama 2 — Estado `ACTIVE` + +Objetivo: responder saldo, nivel y link de recuperación del Wallet. + +Nodo: HTTP Request + +| Campo | Valor | +|---|---| +| Method | `POST` | +| URL | `https://graph.facebook.com/v19.0/{{ $('Code').item.json.phone_id }}/messages` | +| Header `Authorization` | `Bearer TU_TOKEN_DE_META` | +| Body | JSON | + +Body: + +```json +{ + "messaging_product": "whatsapp", + "to": "{{ $('Code').item.json.phone }}", + "type": "text", + "text": { + "body": "¡Hola de nuevo, {{ $json.c_name }}! 💳\n\nEres nivel *{{ $json.c_tier }}* y tu saldo actual para gastar es de *$ {{ $json.c_balance }}*.\n\nSi perdiste tu tarjeta, recupérala aquí:\n👉 https://TU_PORTAL/wallet/{{ $json.pass_id }}" + } +} +``` + +--- + +# Parte 4 — Credenciales necesarias en n8n + +Configura manualmente las credenciales dentro de n8n. No deben versionarse en Gitea. + +## WhatsApp / Meta + +- WhatsApp Trigger OAuth o credencial equivalente. +- Token de Meta para llamadas a Graph API. +- Phone Number ID de Meta. + +## Supabase + +Configura headers para HTTP Request: + +```text +apikey: TU_SUPABASE_ANON_KEY +Authorization: Bearer TU_SUPABASE_ANON_KEY +``` + +--- + +# Parte 5 — Importación del workflow + +1. Entra a n8n. +2. Importa el archivo `n8n/plan-de-incentivo.template.json`. +3. Asigna manualmente las credenciales de WhatsApp y Supabase. +4. Reemplaza los placeholders: + - `TU_PROYECTO` + - `TU_PHONE_NUMBER_ID` + - `COMPANY_ID_DEMO` + - `TU_PORTAL` +5. Ejecuta una prueba con un número autorizado de WhatsApp. +6. Verifica que el cliente se cree en Supabase. +7. Verifica que el bot responda según el estado correcto. + +--- + +# Parte 6 — Seguridad antes de subir a Gitea + +Antes de hacer commit, verifica que no existan: + +- `pinData` +- Teléfonos reales +- Nombres reales de clientes/personas +- Tokens de Meta +- Supabase anon key real +- Supabase service role key +- Credential IDs reales de n8n +- Credential names reales de n8n +- `webhookId` +- `instanceId` +- Workflow ID real +- `versionId` +- UUID real de `company_id` +- URLs reales internas o sensibles + +El workflow que se suba al repositorio debe funcionar como **template reutilizable**, no como exportación exacta de producción. + +--- + +# Parte 7 — Checklist rápido + +- [ ] Supabase schema creado. +- [ ] Empresa demo creada en `companies`. +- [ ] `COMPANY_ID_DEMO` reemplazado en n8n. +- [ ] Credenciales de WhatsApp configuradas manualmente. +- [ ] Credenciales de Supabase configuradas manualmente. +- [ ] Workflow importado en n8n. +- [ ] `pinData` eliminado del JSON antes de subirlo. +- [ ] IDs, tokens y teléfonos reales eliminados. +- [ ] Prueba end-to-end ejecutada correctamente. + diff --git a/n8n/plan-de-incentivo.template.json b/n8n/plan-de-incentivo.template.json new file mode 100644 index 0000000..05ae1ea --- /dev/null +++ b/n8n/plan-de-incentivo.template.json @@ -0,0 +1,325 @@ +{ + "name": "plan de incentivo", + "nodes": [ + { + "parameters": { + "updates": [ + "messages" + ], + "options": {} + }, + "type": "n8n-nodes-base.whatsAppTrigger", + "typeVersion": 1, + "position": [ + -368, + -16 + ], + "name": "WhatsApp Trigger" + }, + { + "parameters": { + "jsCode": "const body = $input.first().json;\n\n// Evitar procesar payloads sin mensajes reales\nif (!body?.messages?.length) {\n return [];\n}\n\nconst msg = body.messages[0];\nconst phone_id = body.metadata?.phone_number_id;\n\nreturn [\n {\n json: {\n phone: msg.from,\n text: msg.type === 'text' ? (msg.text?.body || '').trim() : '',\n phone_id,\n company_id: 'COMPANY_ID_DEMO'\n }\n }\n];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -176, + -16 + ], + "name": "Code in JavaScript" + }, + { + "parameters": { + "method": "POST", + "url": "https://TU_PROYECTO.supabase.co/rest/v1/rpc/process_wa_customer", + "authentication": "genericCredentialType", + "genericAuthType": "httpCustomAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"p_phone\": \"{{ $('Code in JavaScript').item.json.phone }}\",\n \"p_company_id\": \"{{ $('Code in JavaScript').item.json.company_id }}\"\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 32, + -16 + ], + "name": "HTTP Request" + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "leftValue": "={{ $json.state }}", + "rightValue": "NEW", + "operator": { + "type": "string", + "operation": "equals" + }, + "id": "261c308d-79b6-4229-958f-99ba6b2a14f9" + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "NEW" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "5db18bfd-716c-4961-bb65-c3791f8ad202", + "leftValue": "={{ $json.state }}", + "rightValue": "AWAITING_NAME", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "AWAITING_NAME" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "dee9b7b0-f7ca-4ef0-87ea-f43a15a2e52e", + "leftValue": "={{ $json.state }}", + "rightValue": "ACTIVE", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "ACTIVE" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.switch", + "typeVersion": 3.4, + "position": [ + 176, + -32 + ], + "name": "Switch" + }, + { + "parameters": { + "method": "POST", + "url": "https://graph.facebook.com/v22.0/TU_PHONE_NUMBER_ID/messages", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"messaging_product\": \"whatsapp\",\n \"to\": \"={{ $('Code in JavaScript').item.json.phone }}\",\n \"type\": \"text\",\n \"text\": { \"body\": \"¡Hola! 👋 Bienvenido a nuestro Club VIP. Para personalizar tu Billetera Digital, ¿cómo te gusta que te llamen?\" }\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 624, + -240 + ], + "name": "HTTP Request2" + }, + { + "parameters": { + "method": "PATCH", + "url": "=https://TU_PROYECTO.supabase.co/rest/v1/customers?id=eq.{{ $json.customer_id }}", + "authentication": "genericCredentialType", + "genericAuthType": "httpCustomAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={ \"bot_state\": \"AWAITING_NAME\" }", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 432, + -240 + ], + "name": "HTTP Request3" + }, + { + "parameters": { + "method": "PATCH", + "url": "=https://TU_PROYECTO.supabase.co/rest/v1/customers?id=eq.{{ $json.customer_id }}", + "authentication": "genericCredentialType", + "genericAuthType": "httpCustomAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={ \n \"bot_state\": \"ACTIVE\",\n \"name\": \"{{ $('Code in JavaScript').item.json.text }}\"\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 448, + -16 + ], + "name": "HTTP Request4" + }, + { + "parameters": { + "method": "POST", + "url": "https://graph.facebook.com/v22.0/TU_PHONE_NUMBER_ID/messages", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"messaging_product\": \"whatsapp\",\n \"to\": \"{{ $('Code in JavaScript').item.json.phone }}\",\n \"type\": \"text\",\n \"text\": { \"body\": \"¡Listo, {{ $('Code in JavaScript').item.json.text }}! 🎉\\n\\nTu perfil está activo. Descarga tu pase VIP y agrégalo a tu Apple/Google Wallet tocando este enlace:\\n\\n👉 https://TU_PORTAL.com/wallet/{{ $('HTTP Request').item.json.pass_id }}\" }\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 656, + -16 + ], + "name": "HTTP Request5" + }, + { + "parameters": { + "method": "POST", + "url": "https://graph.facebook.com/v22.0/TU_PHONE_NUMBER_ID/messages", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"messaging_product\": \"whatsapp\",\n \"to\": \"={{ $('Code in JavaScript').item.json.phone }}\",\n \"type\": \"text\",\n \"text\": { \"body\": \"¡Hola de nuevo, {{ $('Switch').item.json.c_name }}! 💳\\n\\nEres nivel *{{ $('Switch').item.json.c_tier }}* y tu saldo actual para gastar es de *$ {{ $('Switch').item.json.c_balance }}*.\\n\\nSi perdiste tu tarjeta, recupérala aquí:\\n👉 https://TU_PORTAL.com/wallet/{{ $('Switch').item.json.pass_id }}\" }\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 448, + 160 + ], + "name": "HTTP Request6" + } + ], + "pinData": {}, + "connections": { + "WhatsApp Trigger": { + "main": [ + [ + { + "node": "Code in JavaScript", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code in JavaScript": { + "main": [ + [ + { + "node": "HTTP Request", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP Request": { + "main": [ + [ + { + "node": "Switch", + "type": "main", + "index": 0 + } + ] + ] + }, + "Switch": { + "main": [ + [ + { + "node": "HTTP Request3", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "HTTP Request4", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "HTTP Request6", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP Request3": { + "main": [ + [ + { + "node": "HTTP Request2", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP Request4": { + "main": [ + [ + { + "node": "HTTP Request5", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate", + "availableInMCP": false + } +} \ No newline at end of file diff --git a/supabase/schema.sql b/supabase/schema.sql new file mode 100644 index 0000000..e573f9b --- /dev/null +++ b/supabase/schema.sql @@ -0,0 +1,83 @@ +-- 1. Habilitar extensión para IDs seguros (UUID) +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- 2. TABLA: Empresas +CREATE TABLE companies ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name TEXT NOT NULL, + wa_phone_id TEXT -- El ID del número de WhatsApp en Meta +); + +-- 3. TABLA: Clientes Finales +CREATE TABLE customers ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + company_id UUID REFERENCES companies(id) ON DELETE CASCADE, + phone TEXT NOT NULL, + name TEXT, + balance NUMERIC(10,2) DEFAULT 0.00, + tier TEXT DEFAULT 'Member', -- Niveles: Member, Gold, Black + bot_state TEXT DEFAULT 'NEW', -- Máquina de estados: NEW, AWAITING_NAME, ACTIVE + pass_id TEXT UNIQUE DEFAULT uuid_generate_v4(), + created_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(company_id, phone) +); + +-- 4. TABLA: Transacciones +CREATE TABLE transactions ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + customer_id UUID REFERENCES customers(id) ON DELETE CASCADE, + amount_spent NUMERIC(10,2) NOT NULL, + cashback_earned NUMERIC(10,2) NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 5. TRIGGER: Gamificación Automática +CREATE OR REPLACE FUNCTION update_customer_stats() RETURNS TRIGGER AS $$ +DECLARE + new_balance NUMERIC; +BEGIN + -- Sumar cashback al cliente + UPDATE customers SET balance = balance + NEW.cashback_earned + WHERE id = NEW.customer_id RETURNING balance INTO new_balance; + + -- Lógica de Estatus Dinámico + IF new_balance >= 100 THEN + UPDATE customers SET tier = 'Black' WHERE id = NEW.customer_id; + ELSIF new_balance >= 50 THEN + UPDATE customers SET tier = 'Gold' WHERE id = NEW.customer_id; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER after_transaction_insert +AFTER INSERT ON transactions +FOR EACH ROW EXECUTE FUNCTION update_customer_stats(); + +-- 6. RPC: Función de procesamiento para n8n +CREATE OR REPLACE FUNCTION process_wa_customer(p_phone TEXT, p_company_id UUID) +RETURNS TABLE (customer_id UUID, state TEXT, c_name TEXT, pass_id TEXT, c_balance NUMERIC, c_tier TEXT) AS $$ +DECLARE + v_id UUID; v_state TEXT; v_name TEXT; v_pass TEXT; v_bal NUMERIC; v_tier TEXT; +BEGIN + -- Buscar cliente existente + SELECT id, bot_state, name, customers.pass_id, balance, tier + INTO v_id, v_state, v_name, v_pass, v_bal, v_tier + FROM customers WHERE phone = p_phone AND company_id = p_company_id; + + -- Si es la primera vez que escribe, lo creamos en estado 'NEW' + IF v_id IS NULL THEN + INSERT INTO customers (phone, company_id, bot_state) + VALUES (p_phone, p_company_id, 'NEW') + RETURNING id, bot_state, name, customers.pass_id, balance, tier + INTO v_id, v_state, v_name, v_pass, v_bal, v_tier; + END IF; + + RETURN QUERY SELECT v_id, v_state, v_name, v_pass, v_bal, v_tier; +END; +$$ LANGUAGE plpgsql; + +-- Ejemplo de inserción de empresa (Descomentar y reemplazar) +-- INSERT INTO companies (name, wa_phone_id) +-- VALUES ('TU_EMPRESA', 'TU_PHONE_NUMBER_ID');