Cómo se construyó ecoplagas.online Versión en texto plano de https://latamcodelabs.com/como-se-construyo-ecoplagas-online Autor: Pablo Paniagua, LATAM Code Labs. Permiso: este contenido es público para aprender. Se puede leer, citar, resumir y reutilizar (incluido el código de ejemplo), con o sin ayuda de herramientas de IA. Detalle y código de autorización en https://latamcodelabs.com/permisos.txt # Cómo se construyó ecoplagas.online ecoplagas.online es el sistema interno de EcoPlagas, una fumigadora de Costa Rica. Ahí la empresa agenda, vende, despacha, cobra, controla bodega y capacita. Esta guía lo explica desde cero: qué hace cada parte, con qué piezas se hizo y cómo se conectan. Por Pablo Paniagua, LATAM Code Labs. Guía abierta para aprender: se puede leer, citar y reutilizar (ver permisos). Hay una versión en texto plano para leer con una IA. La segunda parte, con cada módulo al detalle, es el plano completo. 288 pantallas (páginas) dentro del sistema 169 funciones en la nube 37 tareas que corren solas con horario 7.843 versiones publicadas desde el 17 de enero de 2026 01 ## Los ladrillos, explicados Antes de ver los módulos, estas son las palabras que se repiten en todo el documento. Ninguna requiere saber programar. Página web: Un archivo de texto con instrucciones que el navegador (Chrome, Safari) convierte en pantalla. Cada módulo del sistema, como Agenda o Cartera, es una página. Se escriben en tres lenguajes: HTML dice qué hay (un botón, una tabla), CSS dice cómo se ve (color, tamaño) y JavaScript dice qué hace (al tocar el botón, guardar la cita). Base de datos: Donde se guarda la información para que no se pierda al cerrar la pantalla. Usamos Firestore, de Google. Funciona como un archivero: cada cajón es una colección (citas, empleados, productos) y cada ficha dentro es un documento. Cuando alguien cambia una ficha, todas las pantallas abiertas que la muestran se actualizan solas. Función en la nube: Un pedazo de programa que no corre en el teléfono de nadie, sino en una computadora de Google que está siempre encendida. Se usa para lo que no debe depender del empleado: mandar un WhatsApp, revisar que una regla se cumpla o hablar con otro proveedor usando una clave secreta. Tarea programada: Una función en la nube con reloj. Por ejemplo, cada hora a los 5 minutos baja cuánto se gastó en publicidad; cada 5 minutos revisa el GPS de los vehículos. Disparador: Una función que se despierta sola cuando pasa algo en la base de datos. Ejemplo: se crea una cita y, 3 minutos después, una función manda la imagen al grupo de Ventas. API: La ventanilla por donde un sistema le pide algo a otro. Meta (WhatsApp, Facebook) tiene una API: nuestro sistema le dice "mandá este mensaje a este número" y Meta lo entrega. Cada proveedor de esta lista se conectó por su API. Webhook: Lo contrario de una API: es el otro sistema el que nos avisa. Cuando un cliente escribe por WhatsApp, Meta llama a una dirección nuestra y entrega el mensaje en ese instante. Llave o token: Una contraseña larga que identifica al sistema ante un proveedor. Viven guardadas como secretos en la nube y nunca en las páginas, para que nadie las pueda copiar desde el navegador. Reglas de seguridad: El portero de la base de datos. Antes de leer o escribir cualquier ficha, Firestore revisa quién lo pide y si tiene permiso. Por ejemplo: nadie, ni gerencia, puede borrar una cita. Repositorio y publicación: Todo el código vive en GitHub, que guarda cada versión con fecha, autor y descripción. Al subir un cambio, un robot de GitHub lo publica en ecoplagas.online en un par de minutos. Así se sabe siempre qué cambió, cuándo y por qué, y se puede volver atrás. Modelo de lenguaje (IA): Un programa entrenado con mucho texto que puede leer, escribir y conversar. Usamos Claude, de Anthropic, para los vendedores virtuales y para revisar textos antes de que lleguen al cliente. 02 ## Cómo está armado Todo el sistema son cuatro capas. La de arriba es lo que ve el empleado; la de abajo, los servicios de otras empresas con los que conversa. La pantalla nunca habla directo con un proveedor: pasa por una función en la nube, que es la única que conoce la llave. ### Por qué se eligió así - Sin servidores propios. Google (Firebase) se encarga de que todo esté encendido, respaldado y rápido. No hay una computadora en la oficina que se pueda apagar. - Sin paso de armado. Las páginas se escriben en HTML, CSS y JavaScript directos, con Bootstrap para los componentes comunes. Lo que está en el repositorio es exactamente lo que se publica, lo que hace fácil encontrar y arreglar errores. - En vivo. Firestore avisa de cada cambio a las pantallas abiertas, así que si Ventas mueve una cita, el técnico la ve movida sin recargar. - Un permiso por módulo. Cada pantalla pide una clave de permiso (agenda, cartera, gps360...). En la ficha del empleado se marcan las que tiene. 03 ## Cómo se trabaja La mayor parte del código se escribió con Claude Code, una herramienta de inteligencia artificial que programa a partir de instrucciones en español. El ciclo de cada cambio es este: - Se describe el problema del negocio, con datos reales. Ejemplo: "los vendedores aprietan Enviar a Ventas un minuto después de crear la cita, casi siempre". - Se miden los datos antes de decidir. En ese ejemplo, de 458 citas solo 1 se editó en los primeros 30 minutos. Eso justificó automatizarlo. - Se escribe el cambio en la página o en la función correspondiente, imitando el estilo del código que ya existe. - Se prueba. Las reglas de seguridad se prueban contra un emulador (una copia de mentira de la base de datos) con más de 140 casos. El bot de ventas tiene 105 pruebas automáticas. - Se sube a GitHub con una descripción en español. Un robot lo publica en ecoplagas.online en minutos. - Se verifica en vivo, entrando al sitio con un navegador automático como lo haría un usuario. - El equipo reporta con el botón naranja del bicho que está en todas las páginas: la descripción, un pantallazo y los datos técnicos del momento llegan juntos a Gerencia. 04 ## Ventas y CRM CRM significa gestor de relaciones con clientes: el lugar donde vive cada conversación con un cliente posible (lead) hasta que compra. ### Embudo del vendedor #### Qué hace Cada vendedor ve sus chats de WhatsApp y Messenger ordenados por etapa: nuevo, en conversación, cotizado, agendado. Responde desde ahí mismo. #### Cómo se construyó - Meta entrega cada mensaje entrante por webhook a una función, que lo guarda en Firestore. La pantalla escucha esa colección y el chat aparece al instante. - Para responder, la pantalla llama a la función whatsappSend, que es la única con la llave de WhatsApp y que aplica todas las reglas antes de enviar. - Messenger se sumó con una tarea que cada minuto pregunta a Meta por mensajes nuevos de la página de Facebook y los guarda en las mismas colecciones que WhatsApp. Así el reparto, el embudo y las métricas los tratan igual. ### Reparto automático de leads #### Qué hace Decide a qué vendedor le toca cada cliente nuevo, sin que nadie lo elija a mano. #### Cómo se construyó - Equidad: entre los vendedores conectados (con actividad real en los últimos minutos) va uno a cada uno. Si entra o sale alguien, el conteo vuelve a cero para no castigar a quien llegó después. - Nivelación semanal: le toca a quien lleva menos leads en la semana, respetando el turno de cada uno (diurno, temprano, nocturno, domingo). - Por turno: si el dueño del lead no está en turno y otro vendedor de turno le contesta al cliente, el lead pasa a ese vendedor. - Lead frío: un lead de otro vendedor con más de 3 días sin ningún mensaje pasa a quien lo vuelve a contactar. - Las reglas corren en las funciones de la nube dentro de transacciones, para que dos vendedores no se queden con el mismo cliente al mismo tiempo. ### Política de promesas #### Qué hace Impide que se le prometa a un cliente lo que la ley obliga a cumplir y no se puede garantizar: "erradicación total", "100 %", "nunca más". #### Cómo se construyó - Una lista de frases prohibidas basada en la Ley 7472 (lo ofrecido obliga). - Un revisor que entiende trampas: quita tildes, letras repetidas, números escritos con letras y errores de una o dos letras. - Lo aplica el servidor antes de enviar, así que no se puede saltar desde el teléfono. Si el mensaje trae una promesa, no sale y el vendedor ve por qué. ### Llamada obligatoria a leads de Google #### Qué hace Cuando un vendedor responde por primera vez a un lead de Google, arranca un reloj de 3 horas para llamarlo. #### Cómo se construyó Se midió que el 65 % de las ventas de Google cierran en las primeras 3 horas. La función de envío marca la hora de inicio y el vencimiento; el embudo muestra un reloj y, al vencer, un aviso rojo. La llamada se documenta con resultado, duración y pantallazo. 05 ## Agenda y rutas La agenda es la pantalla central: cada cita con su cliente, servicio, monto, ruta, técnicos y vehículo. ### Agenda y ruteador #### Qué hace Muestra el día como una línea de tiempo por ruta. El ruteador asigna técnicos y vehículos a cada ruta. #### Cómo se construyó - Cada cita es un documento en Firestore con su día, hora y datos. El "día operacional" va de 5:00 a. m. a 4:59 a. m., para que el turno nocturno no se parta en dos fechas. - Cada cambio queda en un historial que solo crece: nadie puede borrar lo que pasó. - Mapas con Leaflet (una librería gratuita) y polígonos de provincias, cantones y distritos. ### Compás: dónde cabe una cita #### Qué hace El vendedor pega la ubicación del cliente y la herramienta le dice en qué hueco de qué ruta agrega menos manejo. #### Cómo se construyó - Se analizaron 2.491 citas: el manejo era un tercio del tiempo, y lo que más ahorraba era elegir bien el día y la ruta, no el orden. - Con 2.387 trayectos reales de Google se sacó una fórmula simple para estimar minutos de manejo a partir de la distancia, con error típico de 2,5 minutos. - Para cada hueco calcula el desvío: ir de la cita anterior al cliente y del cliente a la siguiente, menos lo que ya se manejaba. ### Envío automático a Ventas #### Qué hace Tres minutos después de crear una cita, el grupo de Ventas recibe una imagen con la cita y el mapa de la ruta con tiempos de manejo. #### Cómo se construyó Un disparador espera 3 minutos, relee la cita para confirmar que está completa, dibuja la imagen en la nube con un navegador sin pantalla (Puppeteer y Chromium), pide los tiempos de manejo a OSRM (un servicio gratuito de rutas sobre OpenStreetMap) y la manda por WhatsApp. 06 ## GPS de la flota Cada vehículo tiene un GPS que reporta su posición. El sistema convierte esos puntos en información útil. #### Qué hace - GPS 360: el día de un vehículo como un muro: paradas en citas, en la central o fuera de ruta, con mapa. - Alertas por WhatsApp: exceso de velocidad (más de 90 km/h sostenido), paradas fuera de ruta y llegadas tardías a la primera cita. - Velocidad por empleado: un ranking diario en Reportes. #### Cómo se construyó - Un receptor guarda los puntos de cada vehículo cada 2 minutos en Firestore. - Una "parada" es estar 5 minutos dentro de 60 metros. Si cae a menos de 500 m de una cita, es esa cita. - Para no acusar a nadie por echar gasolina, se cargaron 334 gasolineras de OpenStreetMap: una parada corta ahí no cuenta como fuera de ruta. - Los saltos falsos del aparato (un 159 km/h que duró un instante) se filtran comparando distancia y tiempo entre puntos. - El punto se le atribuye al técnico según la ruta que tenía asignada ese vehículo a esa hora en el ruteador. 07 ## Inventario Bodega principal, bodegas personales de cada técnico y bodegas de vehículo, con cada movimiento registrado. #### Qué hace - Transferencias entre bodegas, gastos por cita, auditorías de conteo y arqueos. - Saldos mínimos: cada bodega tiene un kit según su tipo (diurno, nocturno, termitero, vehículo) y la pantalla dice qué le falta. - Reposición: recomienda el pedido semanal. #### Cómo se construyó - Todo movimiento pasa por una sola función compartida que resta de un lado y suma del otro al mismo tiempo, para que nunca quede producto "en el aire". - Reposición mide el consumo real de 28 días (lo que salió y no volvió, sin contar los traslados), le suma un colchón según qué tan a saltos se usa cada producto y apunta a 14 días de cobertura. 08 ## Planilla y personal #### Qué hace - Registro de horas, planilla mensual e histórico. - Liquidación de colaboradores con aguinaldo, vacaciones, preaviso y cesantía. - Cuentas por cobrar. - Fichas de empleado con permisos, departamento y nombre preferido. #### Cómo se construyó La liquidación copia exactamente el método de la hoja de cálculo que usaba Gerencia: promedio de los salarios brutos de los últimos 6 meses, salario diario y cada rubro multiplicado por sus días. La diferencia es que los salarios ya no se digitan: se calculan en vivo desde la planilla del sistema, y cualquier dato se puede corregir a mano antes de imprimir. 09 ## Ministerio de Salud y capacitación Una empresa fumigadora tiene que demostrar que su personal está capacitado y que sus productos y bodegas cumplen la norma. #### Qué hace - 8 cursos obligatorios del Decreto 38335 con vigencia de 12 meses y constancia imprimible. - 20 videos de capacitación narrados (2 h 38 min en total) y 52 cuestionarios. - Manual de procedimientos, plan de residuos y plan de emergencias. - Módulo de cumplimiento: cada requisito con responsable, fecha y evidencia; hojas de seguridad y registros de cada producto. - Certificados de fumigación con código que el cliente puede verificar en línea. #### Cómo se construyó - Las preguntas de los cursos legales son de situación ("estás en la ruta y pasa esto") y la respuesta correcta se reparte entre las opciones para que no se pueda adivinar. - Un curso se da por aprobado con 90 %; la vigencia cuenta desde el último intento aprobado. Gerencia ve en un panel quién está al día. - Los videos se generaron con láminas en alta definición y narración con voz sintética de ElevenLabs. 10 ## Clientes y calidad #### Qué hace - Portal del cliente: el cliente entra y ve sus sedes, servicios, reportes y próximas visitas. - Garantías: el cliente llena un formulario con fotos desde un enlace, sin cuenta. - Encuestas de satisfacción después del servicio. - Tickets de citas en rojo: un reporte grave abre un caso al vendedor. #### Cómo se construyó - El portal del cliente no se reescribió a mano: un programa copia la ficha de cliente de Cartera y solo le cambia cómo inicia sesión. Así lo que ve el cliente es lo mismo que ve el equipo. - El formulario de garantía funciona con un enlace que lleva un código único y difícil de adivinar. Ese código es la llave: solo quien lo tiene ve ese caso. - Las fotos se comprimen en el teléfono del cliente antes de subirse, para que funcione con señal débil. 11 ## Números del negocio #### Qué hace - Reporte de canales: cuánto se invirtió en cada plataforma de publicidad y cuánto vendió cada una (retorno sobre la inversión, ROAS). - Conversión por vendedor: de cada 100 leads, cuántos compran, semana por semana. - Táctico y estrategia: metas del mes y tendencias de largo plazo. #### Cómo se construyó - Una tarea corre cada hora: le pide a Meta el gasto del día por API y recibe el de Google desde un pequeño programa que vive dentro de la cuenta de Google Ads y nos lo envía. - Cada venta se conecta con su origen mediante un código que viaja en el primer mensaje de WhatsApp desde el sitio web. - Las ventas se devuelven a Google y Meta como conversiones, para que sus algoritmos aprendan qué clientes compran de verdad. 12 ## Inteligencia artificial La IA se usa donde ahorra tiempo o evita errores, siempre con reglas que la limitan. ### Vendedores virtuales Melissa y Adrián #### Qué hace Atienden por WhatsApp: preguntan lo necesario, cotizan, proponen espacios reales en la agenda y crean la cita. #### Cómo se construyó - Una máquina de estados (cada conversación sabe en qué paso va) más el modelo Claude para entender y redactar. - Para proponer horarios usa el mismo motor de espacios del sistema y valida contra la agenda antes de crear la cita. - Ubica al cliente en su distrito con mapas de polígonos y revisa si la zona tiene cobertura. - Topes, opción de no recibir más mensajes y paso a un humano cuando hace falta. 105 pruebas automáticas. ### Simulador de campo #### Qué hace El técnico practica por voz con un cliente difícil (un chef molesto, una señora con bebé y mascotas). Al final recibe una calificación. #### Cómo se construyó Cuatro personajes sacados de casos reales, cada uno con una voz distinta de ElevenLabs. La conversación se transcribe y se califica con IA contra lo que el técnico debía decir. ### Revisión de la nota al cliente #### Qué hace Antes de que el cliente vea la nota del técnico, la IA revisa que no tenga groserías, juicios sobre su casa, datos de terceros ni promesas prohibidas. #### Cómo se construyó Un disparador manda la nota a Claude con instrucciones estrictas: si no hay problema, el texto queda letra por letra; si hay, cambia solo esa frase. Si la corrección no queda limpia, la nota no se publica. ### Producción de contenido #### Qué hace Anuncios de video, videos de capacitación, video semanal de la reunión de técnicos y artes de catálogo. #### Cómo se construyó Guiones escritos con IA, voz con ElevenLabs (incluida una voz clonada), imágenes con Gemini de Google y armado del video con FFmpeg, un programa gratuito para editar video por instrucciones. 13 ## Servicios externos y APIs El sistema no hace todo solo: se apoya en servicios de otras empresas, cada uno experto en una cosa (mandar WhatsApp, generar voz, calcular rutas). Esta sección explica cada uno: qué es, para qué lo usamos y cómo se conectó. Cómo funciona una conexión, con un ejemplo. Cuando el sistema necesita que Melissa diga una frase en voz alta, una función en la nube le manda a ElevenLabs el texto y el código de la voz, junto con nuestra llave secreta. ElevenLabs responde con un archivo de audio. Esa conversación de pedido y respuesta es la API. Todas las conexiones de esta sección funcionan igual: cambia quién responde y qué devuelve. ### Cómo se protegen las conexiones - 20 llaves secretas guardadas en el gestor de secretos de Google. Solo las funciones de la nube las pueden leer; ninguna página ni teléfono las conoce. - Los mensajes que entran se verifican. Cuando Meta nos avisa de un WhatsApp nuevo, el aviso trae una firma que se comprueba con una llave compartida. Un aviso falso se descarta. - Las conexiones de entrada tienen su propia clave. El programa de Google Ads que nos envía el gasto, por ejemplo, tiene que presentar una clave que solo él conoce. - Cada llave tiene el mínimo permiso. La de publicidad puede leer campañas pero no mandar WhatsApp, y al revés. ### Si se quiere construir algo parecido Cada ficha de esta sección termina con Para replicarlo: los pasos para conectar ese servicio en un proyecto propio. Antes de abrir cuentas, este es el orden que funcionó: - Empezar por un problema medible del negocio, no por la herramienta. "Los vendedores tardan en mandar la cita" es un problema; "quiero usar IA" no lo es. Escribir qué número debería cambiar. - Guardar los datos primero. Antes de automatizar, que cada cita, lead o movimiento quede en la base de datos con fecha y responsable. Sin datos no hay forma de saber si el cambio sirvió. - Montar la base: un proyecto de Firebase, un repositorio en GitHub y la publicación automática. Con eso ya se puede publicar una página con inicio de sesión. - Conectar un servicio a la vez, siempre desde una función en la nube y con la llave guardada como secreto. Nunca poner una llave dentro de una página. - Probar con casos reales y medir el número del paso 1 antes y después. - Pedirle a una IA que programe (Claude Code, por ejemplo) funciona mejor si se le da el problema, los datos de ejemplo y las reglas del negocio por escrito, y se le pide que pruebe lo que hace antes de publicarlo. ### La receta que se repite Casi todas las conexiones tienen la misma forma: la pantalla le pide algo a una función propia, la función agrega la llave secreta, llama al servicio externo y devuelve solo lo necesario. Este es el esqueleto, con Claude como ejemplo: ``` // functions/index.js (Firebase Functions, Node.js) const { onCall, HttpsError } = require("firebase-functions/v2/https"); const { defineSecret } = require("firebase-functions/params"); // La llave se guarda una vez con: firebase functions:secrets:set ANTHROPIC_API_KEY const llave = defineSecret("ANTHROPIC_API_KEY"); exports.resumirNota = onCall({ secrets: [llave] }, async (req) => { // 1. Solo usuarios con sesión if (!req.auth) throw new HttpsError("unauthenticated", "Inicie sesión"); // 2. Llamar al servicio externo con la llave, que nunca sale del servidor const r = await fetch("https://api.anthropic.com/v1/messages", { method: "POST", headers: { "x-api-key": llave.value(), "anthropic-version": "2023-06-01", "content-type": "application/json", }, body: JSON.stringify({ model: "NOMBRE-DEL-MODELO", max_tokens: 300, system: "Resumí la nota del técnico en una frase, sin inventar nada.", messages: [{ role: "user", content: req.data.nota }], }), }); if (!r.ok) throw new HttpsError("internal", "El servicio no respondió"); // 3. Devolver solo lo que la pantalla necesita const datos = await r.json(); return { resumen: datos.content[0].text }; }); ``` Desde la página se llama con httpsCallable(functions, "resumirNota")({ nota }). Cambiando la dirección, las cabeceras y el cuerpo, el mismo esqueleto sirve para ElevenLabs, Meta o cualquier otro servicio de esta lista. ### La base: Google y GitHub #### Google Firebase Qué es: El conjunto de servicios de Google para construir aplicaciones sin administrar servidores. Para qué: Todo el sistema vive aquí. Hosting sirve las 288 páginas; Firestore es la base de datos; Cloud Functions corre las 169 funciones; Authentication maneja el inicio de sesión; Storage guarda fotos, audios, PDF y pantallazos. Cómo se conectó: Las páginas cargan las librerías oficiales de Firebase directamente desde los servidores de Google, sin instalar nada. Las funciones se escriben en Node.js (JavaScript para servidores) y se publican junto con el resto del código. Para replicarlo: - Crear el proyecto en console.firebase.google.com con una cuenta de Google. - Pasar el proyecto al plan Blaze (pago por uso). Es obligatorio para tener funciones y para que esas funciones llamen a otros servicios. Conviene poner de entrada una alerta de presupuesto en Google Cloud. - Instalar Node.js y luego la herramienta de Firebase: npm install -g firebase-tools, después firebase login y firebase init en la carpeta del proyecto (elegir Hosting, Firestore y Functions). - Escribir las reglas de Firestore desde el día uno, cerradas por defecto, y probarlas con el emulador (firebase emulators:start). - Publicar con firebase deploy. Lo que más cuesta en la factura no es el cómputo sino la transferencia de datos: pantallas que leen colecciones enteras. Leer solo lo necesario (filtrar por fecha, paginar) desde el principio. #### Google Cloud Speech-to-Text Qué es: El servicio de Google que convierte una grabación de voz en texto. Para qué: Pasar a texto los audios que se dictan en el sistema, para poder leerlos y buscarlos. Cómo se conectó: El audio ya está guardado en Storage, así que la función le pasa a Google la dirección del archivo en vez de enviarlo. Se usa el modo largo, porque el modo rápido solo acepta un minuto y un dictado de dos minutos lo rebasa. Para replicarlo: - En console.cloud.google.com, dentro del mismo proyecto de Firebase, activar "Cloud Speech-to-Text API". - Las funciones de Firebase ya tienen una identidad de Google (cuenta de servicio), así que no hace falta otra llave: se pide un token de acceso desde la función. - Para audios de más de un minuto, guardarlos primero en Storage y usar el modo longrunningrecognize con la dirección gs:// del archivo. - Indicar idioma es-CR (o el del país) para mejorar la precisión. #### Google BigQuery Qué es: Un almacén de datos de Google para hacer consultas sobre millones de filas. Para qué: Recibe el detalle diario de la factura de Google Cloud, para saber qué parte del sistema cuesta más. Cómo se conectó: Exportación automática de facturación activada desde la consola de Google. Para replicarlo: - En Google Cloud, menú Facturación, entrar a "Exportación de facturación". - Activar "Exportación de costos de uso estándar" hacia un conjunto de datos de BigQuery del mismo proyecto. - Esperar un día y consultar con SQL qué servicio y qué recurso gasta más. #### GitHub Qué es: El servicio más usado del mundo para guardar código con su historial completo. Para qué: Cada cambio queda registrado con fecha, autor y descripción (7.843 a la fecha). Si algo se rompe, se ve qué cambió y se vuelve atrás. Cómo se conectó: Con GitHub Actions: un robot que, cada vez que entra un cambio, publica las páginas en Firebase y, si se tocaron funciones, publica también las funciones. Los cambios hacen fila para que uno viejo nunca pise a uno nuevo. Para replicarlo: - Crear un repositorio privado y subir la carpeta del proyecto. - En Google Cloud, crear una cuenta de servicio con permiso para publicar en Firebase, bajar su llave JSON y guardarla en GitHub como secreto del repositorio (Settings, Secrets). - Agregar un archivo de flujo en .github/workflows/ que, en cada cambio a la rama principal, instale firebase-tools y corra firebase deploy con esa cuenta. - Agregar concurrency al flujo para que dos publicaciones seguidas hagan fila y la más vieja no pise a la nueva. - Activar una alerta de facturación en GitHub: si el pago falla, las Actions se detienen sin avisar en el sitio. ### Mensajería y llamadas #### WhatsApp Cloud API (Meta) Qué es: La versión oficial de WhatsApp para empresas, que se maneja por programa en vez de por la app del teléfono. Para qué: La línea oficial de ventas: recibir y contestar clientes desde el CRM, mandar plantillas (mensajes aprobados por Meta para escribir primero), y que los bots Melissa y Adrián atiendan. Cómo se conectó: - Entrada: un webhook. Meta llama a nuestra función en el momento en que llega un mensaje, con su firma; la función lo verifica, lo guarda y baja fotos y audios a Storage. - Salida: la función whatsappSend, que revisa reglas (promesas prohibidas, clientes que pidieron no recibir mensajes, reparto por turno) antes de pedirle a Meta que envíe. - Plantillas: se crean por API, incluida la imagen de encabezado, que se sube por la API de carga de archivos de Meta. Para replicarlo: - Tener un Business Manager de Meta (business.facebook.com) y, si es posible, verificar la empresa con sus documentos: sin verificación los límites de envío son bajos. - En developers.facebook.com, crear una app tipo Business y agregarle el producto WhatsApp. - Agregar el número de teléfono. Ese número no puede estar al mismo tiempo en la app normal de WhatsApp: hay que darlo de baja ahí primero. - Crear un usuario del sistema en Business Manager y generar un token permanente con los permisos de WhatsApp. El token temporal de la consola vence en 24 horas. - Configurar el webhook: una función pública que responda la verificación inicial con un código que uno inventa, y que en cada aviso compruebe la firma X-Hub-Signature-256 con el secreto de la app. - Enviar con POST https://graph.facebook.com/v21.0/{id-del-número}/messages. - Para escribirle primero a un cliente, o después de 24 horas sin que escriba, hace falta una plantilla aprobada por Meta. Responder dentro de esas 24 horas no requiere plantilla. #### Messenger (Meta) Qué es: El chat de la página de Facebook. Para qué: Que los mensajes de Facebook entren al mismo CRM que WhatsApp y se repartan igual. Cómo se conectó: Una tarea programada cada minuto le pregunta a Meta por conversaciones nuevas, guarda cada chat con un identificador que empieza por fb_ y copia los adjuntos. Al responder, la misma función de WhatsApp detecta ese prefijo y envía por Messenger. Para replicarlo: - En la misma app de Meta, agregar Messenger y conectar la página de Facebook. - Pedir los permisos pages_messaging y pages_read_engagement y generar un token de página. - Elegir cómo entran los mensajes: por webhook (al instante) o preguntando cada minuto a /{página}/conversations, que es más simple de montar y no se pierde nada si el webhook falla. - Responder con POST /{página}/messages. Igual que WhatsApp, fuera de las 24 horas hay restricciones. #### WhatsApp de empresa (whatsapp-web.js) Qué es: Una librería gratuita que maneja WhatsApp Web de forma automática, como si una persona lo tuviera abierto. Para qué: Monitorear y contestar los teléfonos de empresa de los vendedores, y mandar los avisos internos a grupos: alertas de GPS, citas a Ventas, reportes. Cómo se conectó: Un agente propio corre en un servidor Linux rentado en Costa Rica (CR Servers), con una sesión por teléfono. El sistema no le habla directo: escribe el mensaje en una cola de Firestore y el agente lo recoge y lo envía. Si el servidor se reinicia, la cola espera. Para replicarlo: - Rentar un servidor Linux pequeño que esté siempre encendido (un VPS) e instalar Node.js y Chrome. - Instalar la librería: npm install whatsapp-web.js. Al arrancar muestra un código QR que se escanea con el teléfono, como WhatsApp Web. - Guardar la sesión en disco para no escanear de nuevo en cada reinicio. - No mandar directo desde el sistema: escribir los mensajes en una cola en la base de datos y que el agente los envíe uno por uno, marcando cada uno como enviado. Advertencia: no es un producto oficial de WhatsApp. Usarlo para envíos masivos o a desconocidos puede terminar en el bloqueo del número. Sirve para teléfonos propios y avisos internos; para clientes, la vía correcta es la API oficial. #### Twilio Qué es: Un proveedor de llamadas y mensajes de texto por programa. Para qué: Llamar a clientes desde el CRM, también a teléfonos fijos o sin WhatsApp. Cómo se conectó: Construido y probado, con llamada desde el navegador. Está en pausa: Twilio no vende números de Costa Rica y la verificación del número local nunca timbró en los operadores ticos. Para replicarlo: - Antes de construir, revisar en twilio.com si venden números del país. Para Costa Rica no venden, y eso fue lo que dejó este proyecto en pausa. - Crear la cuenta, cargar saldo y activar los permisos geográficos de llamadas al país. - Comprar un número o verificar uno propio como identificador de llamada. - Para llamar desde el navegador se usa su Voice SDK con un token de acceso que genera una función. ### Publicidad y medición #### Meta Marketing API y Conversions API Qué es: Las APIs de Meta para manejar anuncios de Facebook e Instagram y para informarle qué pasó después del clic. Para qué: Bajar el gasto de cada campaña cada hora, crear campañas y anuncios, y devolverle a Meta las ventas reales. Cómo se conectó: Una tarea cada hora consulta el gasto con una llave de solo lectura. Otra tarea cada 6 horas cruza las citas con monto contra la conversación que las originó y le envía a Meta un evento de compra. Así Meta aprende a mostrar anuncios a gente que compra, no solo a gente que escribe. Para replicarlo: - En la app de Meta, pedir los permisos ads_read (leer gasto) y ads_management (crear campañas). - Generar el token desde un usuario del sistema con acceso a la cuenta publicitaria. - Gasto: GET /act_{cuenta}/insights con el rango de fechas y el nivel (campaña, conjunto o anuncio). - Ventas: enviar un evento Purchase a /{píxel}/events con el monto y el teléfono del cliente cifrado con SHA-256. Meta nunca recibe el dato en claro. - Para unir la venta con el anuncio, guardar desde el primer mensaje de qué anuncio vino la conversación. #### Facebook e Instagram (Graph API) Qué es: La API general de Meta para páginas y cuentas. Para qué: Publicar fotos, videos, reels y carruseles, y leer comentarios. Cómo se conectó: Con la llave de la página de Facebook, que también controla la cuenta de Instagram vinculada. Para replicarlo: - Conectar la cuenta de Instagram (tipo empresa) a la página de Facebook. - Pedir los permisos pages_manage_posts e instagram_content_publish. - Facebook: POST /{página}/photos o /videos. - Instagram: se publica en dos pasos. Primero se crea el contenido con una URL pública del archivo; cuando Meta termina de procesarlo, se publica. #### Google Ads Qué es: La plataforma de anuncios en el buscador de Google y en YouTube. Para qué: Saber el gasto por hora y devolverle a Google las ventas que generó cada clic. Cómo se conectó: - Gasto: un pequeño programa que vive dentro de la cuenta de Google Ads corre cada hora y nos envía los números a una función, con su propia clave. - Ventas: el sitio guarda el identificador de cada clic de Google junto al código de la conversación. Una función publica una lista de ventas que Google Ads descarga sola de forma programada. Para replicarlo: - Gasto: en Google Ads, Herramientas, Acciones masivas, Scripts. Crear un script que lea el gasto del día y lo mande con UrlFetchApp.fetch a una función propia, con una clave en la cabecera. Programarlo cada hora. - Ventas: en el sitio, guardar el parámetro gclid que trae cada clic y pasarlo con la conversación. - Crear en Google Ads una acción de conversión de tipo "Importación". - Publicar un archivo CSV en una dirección HTTPS con gclid, fecha y monto de cada venta, y programar en Google Ads la importación diaria desde esa dirección. Este camino evita tramitar el acceso a la API de Google Ads, que pide un token de desarrollador con aprobación. #### TikTok Ads y Microsoft Ads Qué es: Las plataformas de anuncios de TikTok y del buscador Bing. Para qué: Campañas que llevan a WhatsApp (TikTok) y campañas de búsqueda (Bing). Cómo se conectó: Se administran desde sus plataformas. Sus visitas se distinguen en el reporte de canales por la marca que trae el enlace del anuncio. Para replicarlo: - Poner parámetros utm_source y utm_campaign en todos los enlaces de anuncios. - Guardar esos parámetros junto con el primer mensaje del cliente, para saber el origen de cada venta. - Microsoft Ads permite importar campañas de Google Ads directamente. #### Google Analytics, píxel de Meta y Microsoft Clarity Qué es: Herramientas de medición del sitio web. Clarity, además, graba sesiones anónimas y dibuja mapas de calor. Para qué: Entender qué hacen los visitantes de ecoplagas.cr y qué anuncio los trajo. Cómo se conectó: Su código se carga recién con el primer gesto del visitante (tocar, desplazar) o a los 6 segundos. Eso mantiene el sitio en 100 de velocidad sin perder los clics a WhatsApp, que quedan en espera y salen igual. Para replicarlo: - Crear la propiedad en cada herramienta y copiar su código. - En vez de cargarlos en la cabecera, definir primero funciones vacías (gtag, fbq) que guardan los eventos en una lista. - Cargar los códigos reales con el primer scroll, touchstart o keydown, o a los 6 segundos. Los eventos guardados salen en ese momento. - Medir el resultado con PageSpeed Insights antes y después. ### Inteligencia artificial y voz #### Anthropic (Claude) Qué es: La empresa que desarrolla Claude, un modelo de lenguaje: un programa que lee, entiende y escribe texto. Para qué: Los vendedores virtuales Melissa y Adrián, la revisión de la nota del técnico antes de que la vea el cliente, la calificación del simulador de campo y mejorar comentarios dictados. Además, Claude Code escribió la mayor parte del código del sistema. Cómo se conectó: Las funciones le mandan a la API de Anthropic las instrucciones y la conversación, y reciben la respuesta. Se usan modelos distintos según la tarea: uno rápido y barato para lo frecuente y uno más capaz donde un error cuesta caro. Para replicarlo: - Crear la cuenta en console.anthropic.com, cargar saldo y generar una llave. - Guardarla como secreto de la función: firebase functions:secrets:set ANTHROPIC_API_KEY. - Llamar a POST https://api.anthropic.com/v1/messages con las cabeceras x-api-key y anthropic-version, el modelo, las instrucciones y el mensaje. - Pedir la respuesta en un formato fijo (por ejemplo JSON con campos definidos) y validarla antes de usarla. Si la respuesta no cumple, no se aplica. - Usar un modelo pequeño para tareas frecuentes y uno grande donde equivocarse sale caro. Se cobra por cantidad de texto procesado. #### ElevenLabs Qué es: Un servicio que genera voz humana a partir de texto y que puede clonar una voz real con pocas muestras. Para qué: - Narración de los 20 videos de capacitación y del video semanal de la reunión de técnicos. - Locución de anuncios con la voz clonada de Pablo. - Los cuatro clientes difíciles del simulador de campo, cada uno con su voz y personalidad. Cómo se conectó: Para los videos, un programa le manda cada párrafo del guion y recibe el audio, que luego se une a las imágenes. Para el simulador se usan sus agentes conversacionales: la función crea o actualiza el personaje y le entrega al navegador un enlace firmado de un solo uso, para que el técnico hable sin que la llave salga de la nube. Al terminar, la función baja la transcripción para calificarla. Para replicarlo: - Crear la cuenta en elevenlabs.io y generar una llave en el perfil. Los planes se cobran por cantidad de caracteres convertidos a voz. - Elegir una voz de la biblioteca y copiar su identificador (voice_id). - Voz a partir de texto: POST https://api.elevenlabs.io/v1/text-to-speech/{voice_id} con el texto; devuelve el audio. - Para clonar una voz se suben muestras limpias de la persona, con su consentimiento. - Para conversaciones en vivo, crear un agente conversacional. La función pide un enlace firmado y se lo pasa al navegador; la llave nunca sale del servidor. #### Google Gemini Qué es: El modelo de inteligencia artificial de Google, que también genera y edita imágenes. Para qué: Imágenes para publicidad, artes del catálogo de la tienda y material de capacitación. Cómo se conectó: Por su API, con instrucciones escritas y, cuando hace falta, fotos reales de referencia. Para replicarlo: - Generar una llave en aistudio.google.com. - Llamar al modelo de imágenes con una descripción escrita y, si se quiere mantener un estilo o un producto real, adjuntar fotos de referencia. - Revisar cada imagen antes de publicarla: la IA puede inventar textos o detalles del producto. ### Mapas y ubicación #### Google Maps Qué es: Los mapas de Google, usados por programa. Para qué: Mapas en pantalla y tiempos reales de trayecto entre citas del ruteador. Cómo se conectó: Con una llave restringida a solo dos servicios (mapas y rutas) y solo a nuestros dominios, para que nadie la pueda aprovechar si la copia. Para replicarlo: - En Google Cloud, activar solo las APIs que se van a usar (Maps JavaScript y Directions, por ejemplo). - Crear la llave y restringirla de dos formas: por API (solo esas) y por sitio web (solo los dominios propios). - Poner una alerta de presupuesto. Google da un uso gratuito mensual y cobra después. #### OSRM Qué es: Un motor gratuito que calcula rutas en carretera sobre los mapas de OpenStreetMap. Para qué: Los minutos de manejo que salen en la imagen de cada cita enviada a Ventas. Cómo se conectó: Consulta directa al servidor público, con un límite de 8 segundos. Si no responde, la imagen sale igual, sin tiempos. Para replicarlo: - Pedir una ruta: https://router.project-osrm.org/route/v1/driving/{lng},{lat};{lng},{lat}. Ojo: primero longitud y después latitud. - La respuesta trae duración en segundos y distancia en metros. - Poner siempre un límite de tiempo y un plan B si no responde. - El servidor público es para uso liviano. Con volumen alto, montar un OSRM propio con el mapa del país, que es gratuito. #### OpenStreetMap, Nominatim y Esri Qué es: OpenStreetMap es el mapa libre del mundo, hecho por voluntarios. Nominatim es su buscador de direcciones. Esri (ArcGIS) ofrece mosaicos de mapa de calles. Para qué: Traducir coordenadas a una dirección legible, ubicar las 334 gasolineras usadas en las alertas de GPS, y dibujar el mapa de fondo en las imágenes que se generan en el servidor. Cómo se conectó: Las gasolineras se descargaron una vez y viven en un archivo propio. Los mapas se dibujan con Leaflet, una librería gratuita, tanto en pantalla como en el servidor. Para replicarlo: - Nominatim: https://nominatim.openstreetmap.org/reverse?lat=..&lon=..&format=json. Su política pide máximo una consulta por segundo e identificarse con un User-Agent propio. - Lugares de un tipo (gasolineras, farmacias): bajarlos una sola vez con Overpass Turbo (por ejemplo amenity=fuel en el país) y guardarlos en un archivo propio. - Mapas en pantalla: Leaflet con los mosaicos de OpenStreetMap, citando la fuente. - Esri: crear una cuenta en location.arcgis.com y una llave. Las llaves tienen fecha de vencimiento: anotarla. #### DB-IP y mapa de distritos Qué es: DB-IP es una base gratuita que relaciona direcciones de internet (IP) con lugares aproximados. El mapa de distritos son los límites oficiales de los distritos de Costa Rica en formato digital. Para qué: Saber de qué distrito aproximado nos visita alguien en ecoplagas.cr, y en qué distrito está un cliente a partir de su ubicación. Cómo se conectó: Un programa cruza cada rango de IP con el polígono de su distrito y guarda el resultado en un archivo. No hay consulta externa en cada visita: todo se resuelve en nuestra función. Para replicarlo: - Bajar el archivo gratuito "IP to City Lite" de db-ip.com. Su licencia (CC BY 4.0) pide citar la fuente. - Conseguir los límites de distritos del país en formato GeoJSON (hay repositorios públicos para Costa Rica). - Con un programa, ubicar cada coordenada dentro de su polígono y guardar una tabla de rango de IP a distrito. - Repetirlo cada pocos meses: las IP cambian de dueño. #### GPS de los vehículos Qué es: Rastreadores instalados en cada vehículo de la flota. Para qué: GPS 360, alertas de velocidad y de paradas fuera de ruta, puntualidad y reporte de velocidad por empleado. Cómo se conectó: Un receptor guarda los puntos de cada vehículo cada 2 minutos en Firestore. Desde ahí, un disparador revisa cada lote nuevo y una tarea cada 5 minutos busca rutas cerradas. Para replicarlo: - Al contratar el GPS, preguntar si ofrece API o si puede reenviar las posiciones a una dirección propia. Sin eso, los datos quedan encerrados en su plataforma. - Guardar cada punto con vehículo, hora, coordenadas y velocidad. - Definir con datos qué es una parada (tiempo y radio) y filtrar los saltos imposibles comparando distancia y tiempo entre puntos. ### Pagos #### Onvo Qué es: Una pasarela de pagos costarricense para cobrar con tarjeta en línea. Para qué: Que el cliente pague su servicio desde el cotizador de ecoplagas.cr. Cómo se conectó: Con enlaces de pago por paquete, que el cotizador muestra según lo que el cliente eligió. Para replicarlo: - Crear la cuenta de comercio en onvopay.com y completar la verificación. - Crear un enlace de pago por producto o paquete desde su panel. - Para cobros con monto variable o confirmación automática, usar su API y sus avisos por webhook. ### Herramientas gratuitas que usa el sistema Además de los servicios, el sistema usa librerías de código abierto: programas gratuitos que cualquiera puede usar. Librería | Para qué | Bootstrap | Botones, ventanas y tablas con apariencia uniforme en todas las páginas. | Leaflet | Mapas de rutas, GPS, zonas y clientes. | Chart.js | Gráficos de los reportes. | jsPDF y html2canvas | Convertir una pantalla en PDF o en imagen (certificados, pantallazos del botón del bicho). | QRCode | Códigos QR de certificados y estaciones de cebo. | browser-image-compression | Achicar fotos en el teléfono antes de subirlas. | Puppeteer y Chromium | Un navegador sin pantalla dentro de la nube que dibuja imágenes de citas, rutas y alertas para mandarlas por WhatsApp. | FFmpeg | Armar y cortar videos por instrucciones: anuncios, capacitaciones y reuniones. | Font Awesome | Íconos del sistema. | 14 ## Lógicas que funcionaron, en pseudocódigo Esta sección es para quien quiera replicar los módulos, o para pasársela a una IA que programe. Cada bloque explica el problema, la lógica en pseudocódigo (instrucciones en español con forma de programa, sin depender de un lenguaje) y lo que se aprendió. No incluye llaves, identificadores ni datos privados. Los números (umbrales, horarios, porcentajes) son los que funcionaron en EcoPlagas: conviene validarlos con los datos de cada negocio. - Principios que se repiten en todos los módulos - Modelo de datos - Entrada y salida de WhatsApp - Reparto de leads por equidad - Reparto por nivelación semanal - Reasignación por turno y lead frío - Bloqueo de promesas prohibidas - Reloj de llamada obligatoria - Dónde cabe una cita con menos manejo - Envío automático de la cita a Ventas - GPS: paradas, alertas y velocidad - Inventario: movimientos y reposición - De la publicidad a la venta - Vendedor virtual con IA - Revisión con IA de lo que ve el cliente - Formulario público con token - Reglas de seguridad y cómo probarlas - Capacitación con vigencia - Liquidación de un colaborador ### Principios que se repiten en todos los módulos Antes de los módulos, estas reglas de diseño aparecen una y otra vez. Una IA que quiera replicar el sistema debería aplicarlas por defecto. ``` PRINCIPIO 1 El servidor decide, la pantalla solo muestra toda regla que importa (quién recibe un lead, si un mensaje sale, quién puede cambiar un monto) vive en una función o en las reglas de la base de datos, nunca solo en la página PRINCIPIO 2 Nada se borra cita cancelada -> se mueve a "Ruta 20" (bolsa de canceladas) cada cambio -> se agrega al historial de la ficha historial -> solo puede crecer (la regla exige que la lista nueva contenga entera a la vieja) PRINCIPIO 3 Releer antes de actuar al despertar una tarea, volver a leer la ficha: pudo cambiar entre que se programó y que corrió PRINCIPIO 4 Idempotencia: hacer dos veces = hacer una vez antes de enviar algo, buscar el registro "ya enviado" con el mismo id si existe -> no repetir PRINCIPIO 5 Transacción cuando dos personas pueden chocar leer + decidir + escribir dentro de una misma transacción PRINCIPIO 6 Día operacional el día va de 5:00 a. m. a 4:59 a. m. del día siguiente, para que el turno nocturno no quede partido en dos fechas PRINCIPIO 7 Configuración en la base de datos, no en el código umbrales, interruptores y horarios en un documento "config" -> se apagan o ajustan sin publicar código PRINCIPIO 8 Medir antes de decidir, y rellenar el pasado si la regla cambia, correr un "backfill" que la aplica a los días anteriores como si hubiera existido ``` ### Modelo de datos La base de datos es documental: colecciones de fichas. Estas son las principales y sus campos clave. ``` citas/{id} fecha_dia: "AAAA-MM-DD" // día operacional, para filtrar rápido inicio, fin cliente: { nombre, telefono_normalizado, gps } servicios: [ { servicioId, cantidad, precio } ] monto // protegido: solo servidor o permiso especial ruta: "Ruta 1".."Ruta 19" | "Ruta 20" (canceladas) vendedores: [ username ] reporte: { condicion, plagas, fotos, comentario_tecnico } extras/historial: [ { campo, antes, despues, quien, cuando } ] ruteador/{fecha} rutas: { "Ruta 1": { tecnicos: [..], vehiculo, convocatoria } } conversaciones/{telefono} // WhatsApp; Messenger usa "fb_" assignedTo, source, sourceAt, ultimoMensajeAt, ultimoDireccion etapa, esCliente, optOut mensajes/{id} { telefono, direccion, texto, media, enviadoPor, at } empleados/{username} { permisos: {clave: true}, departamento, estado } empleados_uid_lookup/{uid} -> username // así las reglas saben quién es bodegas/{id} { tipo: principal | personal | vehiculo, grupo } stock_bodegas/{id} { bodegaId, productoId, stockActual } movimientos/{id} { tipo: transferencia | gasto, origen, destino, cantidad } flota_puntos/{id} { vehiculo, at, lat, lng, velocidad } ``` #### Lo que se aprendió - Guardar el teléfono siempre normalizado (solo dígitos, con código de país) en un campo aparte. Es la llave para unir citas, chats y clientes. - Cuando un campo tiene un nombre ambiguo, documentarlo: una vez el reporte de reposición daba cero en todo porque se consultó cantidad en vez de stockActual. ### Entrada y salida de WhatsApp Todo mensaje entra por un webhook y sale por una sola función que aplica las reglas en orden. ``` AL RECIBIR webhook de Meta: si firma(cuerpo, secretoApp) != cabecera.firma: rechazar para cada mensaje: si existe mensajes/{mensaje.id}: saltar // Meta reintenta guardar mensaje; bajar foto/audio a almacenamiento conv = conversaciones/{telefono} o crear con source = origen() conv.ultimoMensajeAt = ahora; conv.ultimoDireccion = "entrante" si conv no tiene dueño: repartir(conv) // ver reparto FUNCIÓN enviar(usuario, telefono, contenido): exigir sesión y ficha de empleado conv = leer conversación si conv.optOut y contenido es plantilla: error "pidió no recibir" si contienePromesa(contenido): error 422 "promesa prohibida" si pasaron más de 24 h desde el último mensaje del cliente y contenido no es plantilla: error "necesita plantilla" respuesta = Meta.enviar(telefono, contenido) TRANSACCIÓN: tomarPorTurno(conv, usuario) // antes de marcar "saliente" conv.ultimoDireccion = "saliente"; conv.ultimoMensajeAt = ahora guardar mensaje saliente con enviadoPor = usuario ``` #### Lo que se aprendió - Toda regla que se evalúa después de un envío tiene que ignorar ese mismo envío. La regla de lead frío dejó de funcionar una semana porque medía la inactividad con un campo que el envío recién había puesto en "ahora". ### Reparto de leads por equidad Cada cliente nuevo va a un vendedor, de forma pareja entre los que están trabajando en ese momento. ``` FUNCIÓN repartir(conv): disponibles = vendedores donde turnoAbierto y no enPausa y minutosConActividad(últimos 20 min) >= 3 // clic, tecla o scroll si vacío: dejar en "sin asignar"; return firma = unir(ordenar(disponibles.username), "|") saldo = config.saldo si saldo.firma != firma: // entró o salió alguien saldo = { firma, conteos: {}, ultimos: {} } elegido = mínimo de disponibles por: 1. saldo.conteos[u] (menos recibidos) 2. saldo.ultimos[u] (más tiempo sin recibir) 3. username (alfabético, sin azar) TRANSACCIÓN: conv.assignedTo = elegido saldo.conteos[elegido] += 1; saldo.ultimos[elegido] = ahora ``` #### Lo que se aprendió - La versión anterior repartía por "leads recibidos entre minutos activos en 24 horas" y falló: quien trabajó sin compañeros en turno acumulaba horas en su contra, y un clic cada 20 minutos bastaba para parecer activo. Reiniciar el saldo cuando cambia el grupo lo resolvió. - Sin azar en el desempate, la cola que se muestra en pantalla coincide con lo que de verdad pasa, y nadie desconfía del reparto. ### Reparto por nivelación semanal Modo alternativo: le toca a quien lleva menos leads en la semana, respetando turnos. ``` FUNCIÓN repartirNivelacion(conv): si hora fuera de 8:00 a 22:00: no asignar elegibles = vendedores con interruptor encendido y latido de presencia en los últimos 3 min y rolEnTurno(semana, ahora) // roles por semana: Diurno 8-17 L-S, Temprano 8-17, // Nocturno 12-22 L-V, Domingo 8-17 L-V y D elegido = mínimo por (leadsDesdeLunes, tiempoSinRecibir) asignar; recalcular tras cada entrega ``` #### Lo que se aprendió - El conteo de la semana tiene que ser exactamente el mismo que muestra el reporte de conversión. Si el reparto y el reporte cuentan distinto, nadie confía en ninguno. ### Reasignación por turno y lead frío Dos reglas para que un cliente que escribe no quede esperando a un vendedor ausente. ``` FUNCIÓN tomarPorTurno(conv, autor): // dentro de enviar() si conv.ultimoDireccion != "entrante": return // el cliente no esperaba si dueñoEnTurno(conv.assignedTo): return si conv.esCliente o autor no es de Ventas o es bot: return registrar { de: conv.assignedTo, a: autor, cuando } conv.assignedTo = autor // el primero que responde gana FUNCIÓN reasignarLeadFrio(conv, solicitante): // al mandar plantilla si conv.esCliente o tieneCitas(conv): no ultimos = últimos 5 mensajes del chat sin contar salientes del solicitante en los últimos 10 min si ningún mensaje en 3 días: conv.assignedTo = solicitante si no: explicar el motivo en pantalla ``` #### Lo que se aprendió - Cuando una regla dice que no, decir por qué en pantalla. Ahorra los reportes de "no funcionó". ### Bloqueo de promesas prohibidas Evita que un vendedor o un bot prometa lo que la ley obliga a cumplir. ``` FUNCIÓN normalizar(texto): minúsculas; quitar tildes números escritos -> dígitos ("cien" -> "100") letras repetidas -> una ("nuuuunca" -> "nunca") quitar enlaces propios permitidos (página de garantía) FUNCIÓN contienePromesa(texto): t = normalizar(texto) si t == alguna RESPUESTA_APROBADA (idéntica): false para cada frase en PROHIBIDAS: // "garantizado", "erradicar", "100 %", "para siempre", // "nunca más", "libre de plagas", "control total" ... si aparece en t con distancia de edición <= 1 o 2: true false se aplica en: enviar(), crear plantilla, respuestas del bot ``` #### Lo que se aprendió - La misma lista vive en la página (para avisar mientras se escribe) y en el servidor (para bloquear). El archivo del servidor es una copia exacta, nunca editada a mano. ### Reloj de llamada obligatoria Se midió que el 65 % de las ventas de Google cierra en las 3 horas siguientes a la primera respuesta. ``` EN enviar(), si es la primera respuesta humana a un lead de Google: conv.llamadaInicio = ahora conv.llamadaVence = ahora + 3 h conv.llamadaResponsable = autor EN PANTALLA (embudo): si no hay llamada registrada: antes de vencer -> reloj con el tiempo que queda vencida -> aviso rojo y alerta al abrir el chat registrar llamada = resultado + duración + pantallazo obligatorio ``` #### Lo que se aprendió - Los campos del reloj solo los escribe el servidor: las reglas impiden que un vendedor los mueva desde su pantalla. ### Dónde cabe una cita con menos manejo El vendedor pega la ubicación y el sistema ordena los huecos libres por el manejo extra que agregan. ``` // Modelo de manejo calibrado con 2.387 trayectos reales FUNCIÓN minutos(a, b): 6,2 + 1,83 * kmEnLineaRecta(a, b) FUNCIÓN huecos(cliente, duracion, dias): candidatos = [] para cada día, cada ruta, cada hueco libre >= duracion: ant = cita anterior (o la central) sig = cita siguiente (o la central) desvio = minutos(ant, cliente) + minutos(cliente, sig) - minutos(ant, sig) si el manejo no cabe en el hueco: descartar si choca con citas, bloqueos del ruteador o propuestas del bot: descartar candidatos.agregar({ dia, ruta, hora, desvio }) ordenar candidatos por desvio ``` #### Lo que se aprendió - Ordenar por "cita más cercana" se equivoca: una cita vecina puede estar en una ruta que después se va al otro lado. Lo que importa es el desvío entre la anterior y la siguiente. - El análisis de 2.491 citas mostró que el orden dentro del día casi no tenía exceso (5,5 %). El ahorro real estaba en elegir otro día u otra ruta (hasta 14,5 % menos manejo). ### Envío automático de la cita a Ventas Al crear una cita, el grupo de Ventas recibe una imagen con la cita y el mapa de la ruta. ``` DISPARADOR al crear citas/{id}: esperar 3 minutos // el vendedor termina de editar cita = releer citas/{id} si falta día, monto <= 0, sin vendedor, Ruta 20, ficticia o cancelada: salir si existe envios_ventas/{id}: salir // idempotencia html = plantillaDeCita(cita, rutaDelDia(cita)) tiempos = OSRM(tramos de la ruta) con límite de 8 s, o vacío imagen = navegadorSinPantalla.capturar(html + mapa + tiempos) cola_whatsapp.agregar({ grupo: Ventas, imagen, texto }) envios_ventas/{id} = { por: "automatico", at: ahora } ``` #### Lo que se aprendió - La decisión se tomó con datos: los vendedores apretaban el botón un minuto después de crear la cita y solo 1 de 458 citas se editó en los primeros 30 minutos. - Un interruptor en la configuración permite volver al envío manual sin publicar código. ### GPS: paradas, alertas y velocidad Convierte miles de puntos por vehículo en paradas, recorridos y alertas útiles. ``` FUNCIÓN paradas(puntos): parada = estar >= 5 min dentro de un radio de 60 m unir paradas a menos de 150 m; descartar tramos de menos de 200 m clasificar cada parada: a <= 500 m de una cita de su ruta -> "cita" a <= 500 m de la central -> "central" a <= 100 m de una gasolinera y dura <= 25 min -> "gasolinera" (no alerta) cita del día movida o cancelada -> "cita movida" (no alerta) otra y dura >= 20 min -> "fuera de ruta" FUNCIÓN limpiarSaltos(puntos): descartar un punto si sube más de 8 km/h por segundo sobre sus dos vecinos, o si marca más de 90 y la distancia y el tiempo hacia los vecinos no lo respaldan ALERTA de velocidad (con cada lote de puntos): racha = puntos > 90 km/h confirmados (>= 2) juntar rachas a menos de 10 min alertar si dura >= 20 s sobre 90, o si el pico >= 100 ATRIBUCIÓN de un punto a personas: técnicos de la ruta que tenía ese vehículo en el ruteador, según la convocatoria más reciente (tope 16 h) ``` #### Lo que se aprendió - Antes de alertar, estudiar el histórico: con 6 semanas de datos se vio cuántas alertas por día saldrían y qué lugares se repetían. Las gasolineras se descargaron de OpenStreetMap para no acusar a nadie por echar combustible. - Los aparatos GPS mienten a veces: hubo un 159 km/h que duró un instante. Sin el filtro de saltos, el ranking de velocidad castigaba a la persona equivocada. ### Inventario: movimientos y reposición Cada producto que se mueve resta de un lado y suma del otro, y la reposición sale del consumo real. ``` FUNCIÓN mover(producto, origen, destino, cantidad, tipo): TRANSACCIÓN: leer stock origen y destino si stock origen < cantidad: error origen.stockActual -= cantidad si tipo == "transferencia": destino.stockActual += cantidad registrar movimiento // "gasto" no tiene destino FUNCIÓN pedidoSugerido(producto): consumo28 = suma de movimientos tipo "gasto" en 28 días + faltantes de auditoría // fuga real // las transferencias NO cuentan: no gastan nada semanal = consumo28 / 4 pico = semana más cargada de las últimas 4 colchon = mín(pico / semanal - 1, 0,9) // más colchón si va a saltos meta = semanal * 2 * (1 + colchon) // 14 días de cobertura saldo = stockActual en TODAS las bodegas pedir = máx(0, meta - saldo) FUNCIÓN faltaEnBodega(bodega, producto): meta = máx(0, kit[bodega.grupo][producto] + bodega.ajuste[producto]) faltante = máx(0, meta - stockActual) ``` #### Lo que se aprendió - Un colchón plano no sirve: 16 de 48 productos tenían semanas con más del doble de su promedio. El colchón por variabilidad compra más solo de lo que se usa a saltos. ### De la publicidad a la venta Para saber qué anuncio vendió, hay que seguir al cliente desde el clic hasta la cita pagada. ``` EN EL SITIO WEB: al entrar, guardar gclid / utm del enlace al tocar WhatsApp: generar código corto "W-xxxx" guardar { codigo, gclid, utm, pagina } en la base abrir WhatsApp con el código dentro del mensaje AL LLEGAR EL PRIMER MENSAJE: si trae un código W: conv.source = origen guardado de ese código CADA HORA: gasto[día][plataforma] = API de Meta + envío del script de Google recalcular hoy (marcado "parcial") y los 3 días anteriores CADA 6 HORAS: para cada cita con monto creada desde una conversación de anuncio: ventas_atribuidas.agregar(cita, conversación) Meta: evento Purchase con monto y teléfono cifrado (SHA-256) Google: fila en el CSV de conversiones (gclid, fecha, monto) ROAS[plataforma] = ventas atribuidas / gasto ``` #### Lo que se aprendió - Recalcular los días anteriores es necesario: las plataformas ajustan su gasto durante uno o dos días después. ### Vendedor virtual con IA Un bot que conversa, cotiza y agenda, con frenos para que no se salga de control. ``` ESTADOS de cada conversación (ejemplo): saludo -> entender plaga y lugar -> pedir ubicación -> cotizar con tarifario -> proponer espacios -> confirmar -> crear cita -> seguimiento en cualquier paso: pasar a humano si lo pide, si se enoja o si el caso no está en el tarifario AL RECIBIR mensaje de un lead del bot: si config.activo == false: salir si gastoDelDía >= tope: apagar y avisar estado = leer estado de la conversación respuesta = IA(instrucciones + tarifario + ejemplos reales + historial + estado) -> salida en formato fijo validar salida: si no cumple o trae promesa prohibida: a humano si pide proponer: espacios = mismo motor que usan los vendedores si pide agendar: validarEspacio() de nuevo; luego crearCita() enviar por la misma función que usan las personas CADA 15 MIN: seguimientos a quien dejó de responder MODOS de encendido: sombra -> genera respuestas y las guarda, sin enviar fuera_horario -> solo atiende cuando no hay vendedores porcentaje -> atiende una parte de los leads nuevos ``` #### Lo que se aprendió - Arrancar en modo sombra permite comparar lo que el bot habría dicho contra lo que dijo el vendedor, sin riesgo. - El bot usa las mismas funciones que las personas (enviar, proponer espacios, crear cita). Así hereda todas las reglas y no hay dos versiones de la verdad. - Las fichas de los bots existen como empleados, pero las reglas impiden asignarles leads a mano. ### Revisión con IA de lo que ve el cliente La nota del técnico pasa por una IA antes de publicarse, sin dejar que la IA invente. ``` DISPARADOR al crear reporte público: original = reporte.comentario_tecnico r = IA(instrucciones estrictas, original) -> { problemas: [ { frase, motivo } ], corregido: texto } si r.problemas vacío: publicado = original // letra por letra si no: si largo(r.corregido) > largo(original) o contienePromesa(r.corregido): no publicar, marcar error si no: publicado = r.corregido el comentario interno nunca se publica ``` #### Lo que se aprendió - Pedirle a la IA una salida con estructura fija y verificarla con reglas normales. Si la verificación falla, es preferible no publicar a publicar algo inventado. ### Formulario público con token El cliente llena su garantía desde un enlace, sin cuenta, y solo ve su propio caso. ``` AL GENERAR: token = aleatorio largo garantias/{token} = { citaId, estado: "solicitud_enviada" } enlace = sitio/garantia?t=token REGLAS de la base para garantias/{token}: leer: cualquiera que conozca el token (el id es el secreto) actualizar sin sesión: solo si estado actual == "solicitud_enviada" y estado nuevo == "pendiente_revision" y solo cambian { motivo, fotos, estado } todo lo demás: solo empleados con permiso ``` #### Lo que se aprendió - Las fotos se comprimen en el teléfono antes de subir: los clientes suelen tener señal débil. ### Reglas de seguridad y cómo probarlas Las reglas son el último portero. Estos patrones cubren la mayoría de casos. ``` esEmpleado() = existe empleados_uid_lookup/{uid de la sesión} permiso(clave) = ficha del empleado tiene permisos[clave] == true match citas/{id}: leer, crear: esEmpleado() borrar: nunca actualizar: esEmpleado() y (no cambia monto o permiso("anular_monto") o esGerencia()) y campos de servidor sin tocar match citas/{id}/extras/historial: actualizar: nueva lista contiene entera a la vieja PRUEBAS con el emulador, tres grupos: 1. lo que el equipo hace a diario -> debe pasar 2. intentos de abuso -> debe fallar 3. casos borde (gerencia, sin ficha) ``` #### Lo que se aprendió - Una regla que compila puede estar abierta. Un comodín recursivo dentro de citas también calzaba el documento de la cita y anulaba todas las protecciones. Solo el emulador lo detectó. - Los permisos especiales que mueven dinero no se heredan de ningún otro: se marcan persona por persona. ### Capacitación con vigencia Cursos obligatorios que vencen y un panel que dice quién está al día. ``` aprobado(intento) = puntaje >= 90 % ultimoAprobado(persona, curso) = fecha del último intento aprobado vigente(persona, curso) = hoy - ultimoAprobado <= 365 días al armar el cuestionario: preguntas de situación ("estás en la ruta y pasa esto...") repartir la respuesta correcta entre A, B, C y D panel de gerencia: para cada aplicador activo y cada curso obligatorio: vigente | vence en N días | vencido | nunca ``` ### Liquidación de un colaborador Copia exacta del método de la hoja de cálculo que usaba Gerencia, con los salarios calculados por el sistema. ``` brutos = salario bruto de cada uno de los últimos 6 meses (de la planilla) promedio = suma(brutos) / meses marcados diario = promedio / 30 comisiones = ventas * porcentaje horasExtra = horas * (diario / 8) * 1,5 diasPendientes = dias * diario aguinaldo = suma(brutos desde el 1 de diciembre) / 12 vacaciones = diasVacaciones * diario preaviso, cesantia = dias * diario // 0 si es renuncia total = suma de todo + bono // cada valor se puede corregir a mano antes de imprimir ``` #### Lo que se aprendió - Replicar primero el método que el negocio ya usa y confía. Mejorarlo después, con su aprobación. ### Instrucción para usar esta guía con una IA Para que otra IA (Claude, por ejemplo) arranque un proyecto propio con esta guía, se le puede pasar la versión en texto plano junto con esta instrucción, completando lo que está entre corchetes: ``` Vas a construir un sistema parecido al descrito en esta guía, para [NEGOCIO] en [PAÍS]. Antes de escribir código: 1. Leé las secciones "Los ladrillos", "Cómo está armado" y "Lógicas que funcionaron". 2. Haceme preguntas sobre mi operación hasta poder escribir el modelo de datos (colecciones y campos) adaptado a mi negocio. 3. Proponé el orden de construcción, empezando por el módulo que resuelve el problema más caro, y decime qué número vamos a medir. Reglas al construir: - Aplicá los principios de la guía: el servidor decide, nada se borra, releer antes de actuar, idempotencia, transacciones, configuración en la base de datos. - Las llaves de servicios externos solo en secretos de la nube. - Escribí pruebas de las reglas de seguridad con el emulador antes de publicarlas. - Usá la guía como referencia de lógica, no copies números (umbrales, horarios, porcentajes) sin validarlos con mis datos. ``` 15 ## Seguridad - Ser empleado es tener ficha. Además de iniciar sesión, el sistema revisa que exista la ficha de empleado antes de dejar leer o escribir datos. - Las citas no se borran. Se cancelan, y su historial solo puede crecer. El monto de una cita solo lo cambian Gerencia o quien tenga el permiso de anular. - Las reglas se prueban contra un emulador antes de publicarlas, porque una regla puede compilar bien y aun así dejar la puerta abierta. Así se detectó un caso real antes de que llegara a producción. - Las llaves de los proveedores solo existen en la nube, nunca en las páginas. - Todo texto que escribe un usuario se limpia antes de mostrarse, para que nadie pueda meter código en la pantalla de otro. 16 ## Proyectos hermanos #### ecoplagas.cr El sitio público. 100 de 100 en las cuatro categorías de Google Lighthouse (velocidad, accesibilidad, buenas prácticas, SEO). Primero de 15 en una comparación con fumigadoras de Costa Rica en octubre de 2026. #### Cómo se construyó - Tipografía alojada en el mismo sitio y herramientas de medición que cargan con el primer gesto del visitante, para que la página aparezca rápido. - Cotizador: cinco preguntas y el precio publicado al instante, con enlace de pago y un mensaje de WhatsApp ya armado. - Medición propia por visita: tiempo activo, secciones leídas y distrito aproximado por IP. #### Tienda en línea 22 productos de uso doméstico del inventario, con carrito y pago. #### Cómo se construyó Una sola página en Firebase con el catálogo en un archivo. Los productos usan el mismo identificador que en el inventario del sistema. #### Apps Android App de EcoPlagas, Archivero (gestor de archivos y buscador de duplicados), Compresor (ZIP, 7Z y RAR con clave) y Despierta (mantiene la pantalla encendida). #### Cómo se construyó Aplicaciones nativas escritas en Kotlin, el lenguaje oficial de Android, con Jetpack Compose para las pantallas, firmadas con llave propia. #### Técnicos a Domicilio Sitio y plataforma de la empresa hermana de reparaciones a domicilio. #### Cómo se construyó Su propio proyecto de Firebase, separado del de EcoPlagas, con la misma forma de trabajo. Cifras tomadas del repositorio de ecoplagas.online al 31 de octubre de 2026. Más sobre el sistema en el caso EcoPlagas Online. Contacto: hola@latamcodelabs.com.