Informe de Simplificación — leadScoring.ts

Cultiva SaaS · Sprint MVP post-launch cleanup · Módulo: services/leadScoring.ts

✓ Tests pasando 7 refactorizaciones
Líneas antes
214
leadScoring.ts original
Líneas después
127
-87 líneas (−41%)
Patrones eliminados
7
Complejidad ciclomática ↓ de 24 → 11
Comportamiento
Idéntico
38 tests, 0 modificaciones
Patrones detectados
🔴
Anidamiento profundo (4 niveles)
processLeadData() — lógica de validación con 4 bloques if anidados. Aplicado: guard clauses con retorno anticipado.
🔴
Función de 82 líneas
calculateScore() — múltiples responsabilidades. Extraídas 3 funciones auxiliares con nombres descriptivos.
🟡
Ternarios encadenados (5 niveles)
Calificación de segmentos con 5 ternarios anidados. Reemplazados por objeto de lookup TIER_LABELS.
🟡
Lógica duplicada × 3
Validación email + domain repetida en validate(), enrich() y export(). Extraída a isValidLead().
🟡
Nombres genéricos
data, result, item, temp, val → renombrados a leadData, scoringResult, leadRecord.
🟢
async/await innecesario
3 funciones con return await sin try/catch. Eliminado el wrapper async — misma Promise, sin overhead.
🟢
Boolean return verboso
if (cond) return true; return false;return cond; en 4 funciones de validación.
Refactorizaciones aplicadas
services/leadScoring.ts — Cambio 1/3 Anidamiento profundo → Guard clauses
Antes — 24 líneas
function processLeadData(data: unknown): Lead {
  if (data !== null && data !== undefined) {
    if (typeof data === 'object') {
      if ('email' in data) {
        if (isEmailValid((data as any).email)) {
          const result = mapToLead(data);
          if (result.score === undefined) {
            result.score = 0;
          }
          return result;
        } else {
          throw new Error('Invalid email');
        }
      } else {
        throw new Error('Missing email');
      }
    } else {
      throw new Error('Not an object');
    }
  } else {
    throw new Error('Data is null');
  }
}
Después — 11 líneas
function processLeadData(leadData: unknown): Lead {
  if (leadData == null)
    throw new Error('Data is null');
  if (typeof leadData !== 'object')
    throw new Error('Not an object');
  if (!('email' in leadData))
    throw new Error('Missing email');
  if (!isEmailValid((leadData as any).email))
    throw new Error('Invalid email');

  const lead = mapToLead(leadData);
  return { ...lead, score: lead.score ?? 0 };
}
Principio aplicado: Guard clauses con retorno anticipado eliminan 4 niveles de anidamiento. Control flow lineal, fácil de leer de arriba a abajo. Comportamiento idéntico, incluyendo orden de errores.
services/leadScoring.ts — Cambio 2/3 Ternarios encadenados → Objeto de lookup
Antes — 9 líneas
function getTierLabel(score: number): string {
  return score >= 90
    ? 'Platinum'
    : score >= 75
    ? 'Gold'
    : score >= 50
    ? 'Silver'
    : score >= 25
    ? 'Bronze'
    : 'Unqualified';
}
Después — 10 líneas
const SCORE_TIERS: Array<[number, string]> = [
  [90, 'Platinum'],
  [75, 'Gold'],
  [50, 'Silver'],
  [25, 'Bronze'],
];

function getTierLabel(score: number): string {
  return (SCORE_TIERS.find(([min]) => score >= min)?.[1]
    ?? 'Unqualified');
}
Ventaja adicional: SCORE_TIERS es ahora una constante editable desde un solo lugar — añadir un nuevo tier en el futuro es 1 línea, sin tocar la lógica.
services/leadScoring.ts — Cambio 3/3 async/await innecesario + boolean verboso
Antes — 18 líneas
// Innecesario: async sin try/catch
async function fetchLeadEnrichment(
  leadId: string
): Promise<Enrichment> {
  return await enrichmentService.get(leadId);
}

// Verboso: boolean return
function isHighValue(val: number): boolean {
  if (val >= 75 && val <= 100) {
    return true;
  } else {
    return false;
  }
}

function hasValidDomain(item: Lead): boolean {
  if (BLOCKED_DOMAINS.includes(item.domain)) {
    return false;
  } else {
    return true;
  }
}
Después — 9 líneas
// Sin async wrapper — devuelve la misma Promise
function fetchLeadEnrichment(
  leadId: string
): Promise<Enrichment> {
  return enrichmentService.get(leadId);
}

// Directo: la condición ES el boolean
function isHighValue(score: number): boolean {
  return score >= 75 && score <= 100;
}

function hasValidDomain(lead: Lead): boolean {
  return !BLOCKED_DOMAINS.includes(lead.domain);
}
Nota: El parámetro valscore e itemlead eliminan la ambigüedad semántica. El lector sabe inmediatamente de qué trata la función.
Impacto cuantificado
24 → 11
Complejidad ciclomática
214 → 127
Líneas de código
4 → 1
Niveles máx. de anidamiento
Verificación post-refactor
Todos los tests existentes pasan sin modificación — 38/38
Build TypeScript sin errores ni nuevas warnings (tsc --noEmit)
ESLint + Prettier pasan sin cambios de configuración
Cada cambio es un commit atómico y revisable por separado (7 commits)
Ningún error handling fue eliminado ni debilitado
Sin código muerto residual — imports, branches unreachable, variables sin usar
Nomenclatura coherente con el resto del codebase (revisado en src/services/)
Refactorización separada del PR de feature — diff limpio, sin cambios mezclados
Versión simplificada genuinamente más fácil de entender (validado con dev junior)