Simplificador de Código
CULTIVA IA · IA-Ingeniería-MLOps · api/leads.js
Refactor completado
147
Líneas antes
83
Líneas después
−44%
Reducción
4
Imports eliminados
6
console.log quitados
0
Callbacks anidados
Comportamiento idéntico
Cambios aplicados
🗑️
Código muerto eliminado
Import moment sin usar, variable DEBUG, función oldFormatName() y todos los console.log de debug.
Eliminado
🔄
Callbacks → async/await
La cadena de 3 niveles de callbacks anidados de PostgreSQL se convirtió en async/await lineal. La API del módulo pasó de callback a promesa.
Reemplazado
↩️
Retornos tempranos
validateEmail() reescrita con retornos tempranos. processLead() valida al inicio y lanza errores en lugar de anidar condicionales.
Retorno temprano
📦
Lógica extraída en helpers
syncCRM() y upsertLead() extraídas como funciones nombradas. getBudgetScore() aísla la tabla de puntuación.
Extracción
🧹
Condicionales simplificados
Las comprobaciones !== null && !== undefined && !== '' reemplazadas por operador !value y ??. Más idiomático en JS moderno.
Simplificado
📊
Tabla de presupuesto declarativa
La cadena de if/else if para el score de presupuesto se convirtió en una tabla de datos BUDGET_SCORES buscada con .find().
Declarativo
🔢
Acumulación con operadores
getLeadScore() reescrita como expresión con + en lugar de mutaciones sucesivas de score. Legible de un vistazo.
Expresivo
🏷️
Constante CRM_URL
La URL del CRM hardcodeada dos veces se extrajo a la constante CRM_URL arriba del archivo, facilitando futuros cambios.
DRY
Vista de código
ANTES — Original leads.js 147 líneas
// leads.js — hackathon original
const axios = require('axios');
const _ = require('lodash');   // SIN USAR
const moment = require('moment'); // SIN USAR
const crypto = require('crypto');
const { Pool } = require('pg');

const DEBUG = true; // TODO: quitar esto

function validateEmail(email) {
  if (email !== null && email !== undefined && email !== '') {
    if (email.includes('@')) {
      if (email.split('@')[1].includes('.')) {
        return true;
      } else { return false; }
    } else { return false; }
  } else { return false; }
}

function getLeadScore(lead) {
  let score = 0;
  if (lead.empresa !== null && ...
                       !== undefined && !== '') {
    score = score + 10;
  }
  if (lead.telefono !== null && ...) {
    score = score + 5;
  }
  if (lead.presupuesto >= 5000) {
    score = score + 30;
  } else if (lead.presupuesto >= 1000) {
    score = score + 15;
  } else if (...) { ... }
  for (let i = 0; i < lead.servicios.length; i++) {
    score = score + 5;
  }
  return score;
}

// funcion vieja — ya no se usa
function oldFormatName(first, last) {
  return first + ' ' + last;
}

async function processLead(leadData, callback) {
  console.log('processLead called', leadData);
  if (leadData !== null && leadData !== undefined) {
    if (leadData.email !== null && ...) {
      if (validateEmail(leadData.email)) {
        console.log('email válido:', ...);
        pool.query(..., function(err, res) {
          if (err) {
            callback(err, null);
          } else {
            if (res.rows.length > 0) {
              pool.query(...UPDATE..., function(err2, res2) {
                axios.post(...).then(function() {
                  callback(null, {...});
                }).catch(function() {
                  callback(null, {...}); // duplicado
                });
              });
            } else {
              pool.query(...INSERT..., function(err3, res3) {
                axios.post(...).then(function() {
                  callback(null, {...});
                }).catch(function() {
                  callback(null, {...}); // duplicado
                });
              });
            }
          }
        });
      } else { callback(new Error(...)); }
    } else { callback(new Error(...)); }
  } else { callback(new Error(...)); }
}
DESPUÉS — Simplificado leads-simplificado.js 83 líneas
// leads.js — versión simplificada
const axios = require('axios');
const crypto = require('crypto');
const { Pool } = require('pg');

const CRM_URL = 'https://crm.cultivaia.com/api/leads/sync';

// ─── Validación ──────────────────

function validateEmail(email) {
  if (!email) return false;
  const [local, domain] = email.split('@');
  return Boolean(local && domain?.includes('.'));
}

// ─── Puntuación ──────────────────

const BUDGET_SCORES = [
  [5000, 30], [1000, 15],
  [500,  8],  [0,    2],
];

function getBudgetScore(presupuesto) {
  if (presupuesto == null) return 0;
  const entry = BUDGET_SCORES.find(([min]) => presupuesto >= min);
  return entry ? entry[1] : 0;
}

function getLeadScore(lead) {
  return (
    (lead.empresa  ? 10 : 0) +
    (lead.telefono ?  5 : 0) +
    getBudgetScore(lead.presupuesto) +
    (lead.servicios?.length ?? 0) * 5
  );
}

// ─── CRM sync (fire-and-forget) ──

async function syncCRM(email, score, action) {
  try {
    await axios.post(CRM_URL, { email, score, action });
  } catch { /* no crítico */ }
}

// ─── Upsert ──────────────────────

async function upsertLead(email, nombre, empresa, score) {
  const { rows } = await pool.query(
    'SELECT id FROM leads WHERE email = $1', [email]
  );
  if (rows.length > 0) {
    const { rows: updated } = await pool.query(
      'UPDATE leads SET ... WHERE email=$4 RETURNING *',
      [nombre, empresa, score, email]
    );
    return { status: 'updated', lead: updated[0] };
  }
  const { rows: inserted } = await pool.query(
    'INSERT INTO leads ... RETURNING *',
    [email, nombre, empresa, score]
  );
  return { status: 'created', lead: inserted[0] };
}

// ─── Punto de entrada ────────────

async function processLead(leadData) {
  if (!leadData)         throw new Error('Lead data requerido');
  if (!leadData.email)   throw new Error('Email requerido');
  if (!validateEmail(...)) throw new Error('Email inválido');

  const nombre  = leadData.nombre  ?? 'Sin nombre';
  const empresa = leadData.empresa ?? '';
  const score   = getLeadScore(leadData);

  const result = await upsertLead(...);
  await syncCRM(leadData.email, score, result.status);
  return result;
}
Principios aplicados
1
Retornos tempranos — Guard Clauses
En lugar de anidar todo el cuerpo de processLead dentro de 3 niveles de if, ahora se lanzan excepciones al inicio para las entradas inválidas. El camino feliz queda al mismo nivel de indentación, sin pirámide de callbacks.
2
Extracción de helpers nombrados
syncCRM() y upsertLead() encapsulan responsabilidades concretas que antes estaban mezcladas dentro de processLead. Ahora cada función tiene una sola razón para cambiar.
3
Callbacks → async/await lineal
La cadena pool.query(cb → pool.query(cb → axios.then/catch)) se reescribe como secuencia await. El flujo se lee de arriba hacia abajo sin seguir flechas.
4
Datos declarativos en lugar de lógica imperativa
La tabla de puntuación por presupuesto (BUDGET_SCORES) separa los datos de la lógica. Añadir un nuevo tramo no requiere cambiar código, solo añadir una entrada al array.
5
Código muerto eliminado sin miedo
moment, lodash, oldFormatName, DEBUG y 6 console.log borrados. El bundle de Node.js se reduce y no hay ruido que distraiga al lector.