diff --git a/mvp/b2c/.env.example b/mvp/b2c/.env.example index cc8916c..e59faa9 100644 --- a/mvp/b2c/.env.example +++ b/mvp/b2c/.env.example @@ -7,3 +7,26 @@ DATABASE_URL="postgresql://postgres:reformix@localhost:5432/reformix" RETELL_API_KEY="" RETELL_AGENT_ID="" RETELL_FROM_NUMBER="" # número de origen en E.164, p. ej. +34910000000 + +# EP de ingesta del lead (/api/leads/:id/ingesta). Clave compartida que valida al llamante +# externo (Authorization: Bearer ...). Sin ella, el EP responde 401. +FUNNEL_API_KEY="" + +# Email (SMTP) para enviar el presupuesto y el enlace al formulario. OPCIONALES: sin SMTP_HOST + +# EMAIL_FROM el envío degrada a no-op (la entrega queda marcada como simulada). Mailhog local: +# SMTP_HOST=localhost SMTP_PORT=1025 (sin user/pass). +SMTP_HOST="" +SMTP_PORT="587" +SMTP_USER="" +SMTP_PASS="" +EMAIL_FROM="" # remitente, p. ej. "Reformas Ejemplo " + +# Webhooks salientes hacia el flujo externo (n8n/generador). OPCIONALES: sin URL la señal no se +# manda. PERFIL = perfil completo (generar renders/agente); WHATSAPP = entrega del PDF; +# WHATSAPP_START = arrancar la conversación de WhatsApp con el lead. +PERFIL_WEBHOOK_URL="" +WHATSAPP_WEBHOOK_URL="" +WHATSAPP_START_WEBHOOK_URL="" + +# Base pública de la app, para construir enlaces absolutos (enlace al formulario en el email). +APP_URL="http://localhost:3000" diff --git a/mvp/b2c/package-lock.json b/mvp/b2c/package-lock.json index 59b385a..4ca9c0f 100644 --- a/mvp/b2c/package-lock.json +++ b/mvp/b2c/package-lock.json @@ -13,6 +13,7 @@ "bcryptjs": "^3.0.3", "drizzle-orm": "^0.45.2", "next": "16.2.6", + "nodemailer": "^8.0.10", "postcss": "^8.5.15", "postgres": "^3.4.9", "react": "19.2.4", @@ -24,6 +25,7 @@ }, "devDependencies": { "@types/node": "^20", + "@types/nodemailer": "^8.0.0", "@types/react": "^19", "@types/react-dom": "^19", "@vitest/coverage-v8": "^4.1.7", @@ -2966,6 +2968,16 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/nodemailer": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.0.tgz", + "integrity": "sha512-fyf8jWULsCo0d0BuoQ75i6IeoHs47qcqxWc7yUdUcV0pOZGjUTTOvwdG1PRXUDqN/8A64yQdQdnA2pZgcdi+cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/react": { "version": "19.2.15", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", @@ -7201,6 +7213,15 @@ "node": ">=18" } }, + "node_modules/nodemailer": { + "version": "8.0.10", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.10.tgz", + "integrity": "sha512-BLFuSth7QtHOkBzyqTehWWyub0NTRDuK2Q2SQfnGLsrJnzyU+Yeh4WpV1eZGuARFj1xQJHIdnTuJZLP+b9R1GQ==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/normalize-svg-path": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz", diff --git a/mvp/b2c/package.json b/mvp/b2c/package.json index d9fe1cc..8199518 100644 --- a/mvp/b2c/package.json +++ b/mvp/b2c/package.json @@ -23,6 +23,7 @@ "bcryptjs": "^3.0.3", "drizzle-orm": "^0.45.2", "next": "16.2.6", + "nodemailer": "^8.0.10", "postcss": "^8.5.15", "postgres": "^3.4.9", "react": "19.2.4", @@ -34,6 +35,7 @@ }, "devDependencies": { "@types/node": "^20", + "@types/nodemailer": "^8.0.0", "@types/react": "^19", "@types/react-dom": "^19", "@vitest/coverage-v8": "^4.1.7", diff --git a/mvp/b2c/src/lib/env.ts b/mvp/b2c/src/lib/env.ts index 8fd365b..9fd3606 100644 --- a/mvp/b2c/src/lib/env.ts +++ b/mvp/b2c/src/lib/env.ts @@ -12,12 +12,36 @@ const schema = z.object({ RETELL_API_KEY: opcional, RETELL_AGENT_ID: opcional, RETELL_FROM_NUMBER: opcional, + // EP de ingesta del lead: clave compartida que valida al llamante externo. + FUNNEL_API_KEY: opcional, + // SMTP para enviar el presupuesto y el enlace al formulario. + SMTP_HOST: opcional, + SMTP_PORT: opcional, + SMTP_USER: opcional, + SMTP_PASS: opcional, + EMAIL_FROM: opcional, + // Webhooks salientes hacia el flujo externo (generación, entrega y arranque de WhatsApp). + PERFIL_WEBHOOK_URL: opcional, + WHATSAPP_WEBHOOK_URL: opcional, + WHATSAPP_START_WEBHOOK_URL: opcional, + // Base pública de la app, para construir enlaces (ej. el enlace al formulario en el email). + APP_URL: opcional, }); export const env = schema.parse({ RETELL_API_KEY: process.env.RETELL_API_KEY, RETELL_AGENT_ID: process.env.RETELL_AGENT_ID, RETELL_FROM_NUMBER: process.env.RETELL_FROM_NUMBER, + FUNNEL_API_KEY: process.env.FUNNEL_API_KEY, + SMTP_HOST: process.env.SMTP_HOST, + SMTP_PORT: process.env.SMTP_PORT, + SMTP_USER: process.env.SMTP_USER, + SMTP_PASS: process.env.SMTP_PASS, + EMAIL_FROM: process.env.EMAIL_FROM, + PERFIL_WEBHOOK_URL: process.env.PERFIL_WEBHOOK_URL, + WHATSAPP_WEBHOOK_URL: process.env.WHATSAPP_WEBHOOK_URL, + WHATSAPP_START_WEBHOOK_URL: process.env.WHATSAPP_START_WEBHOOK_URL, + APP_URL: process.env.APP_URL, }); // Mínimo para lanzar una llamada saliente: clave de API + número de origen. El agente puede @@ -26,3 +50,21 @@ export const env = schema.parse({ export function retellConfigurado(): boolean { return Boolean(env.RETELL_API_KEY && env.RETELL_FROM_NUMBER); } + +// Mínimo para enviar email: host SMTP + remitente. Sin esto el envío degrada a no-op. +export function emailConfigurado(): boolean { + return Boolean(env.SMTP_HOST && env.EMAIL_FROM); +} + +// Cada webhook saliente es opcional: si falta su URL, la señal correspondiente no se manda. +export function perfilWebhookConfigurado(): boolean { + return Boolean(env.PERFIL_WEBHOOK_URL); +} + +export function whatsappWebhookConfigurado(): boolean { + return Boolean(env.WHATSAPP_WEBHOOK_URL); +} + +export function whatsappStartConfigurado(): boolean { + return Boolean(env.WHATSAPP_START_WEBHOOK_URL); +}