Skip to content

GUÍA DE INGESTA Y MIGRACIÓN DE DATOS DE CLIENTES

📊 Objetivo: Cuando un cliente entrega un archivo Excel o CSV con miles de filas de clientes, productos o transacciones anteriores (con datos sucios, duplicados o fechas inconsistentes), este protocolo define cómo limpiar, validar e importar la información a la base de datos sin errores ni caídas.


1. El Pipeline de Ingesta en 4 Fases

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│ 1. EXTRACT      │ ──► │ 2. SANITIZE     │ ──► │ 3. VALIDATE     │ ──► │ 4. BULK INSERT  │
│ Leer CSV/Excel  │     │ Limpiar strings │     │ Validar esquema │     │ Inserción en    │
│ en streams      │     │ y formatos fecha│     │ con Zod (S-001) │     │ lotes de 500    │
└─────────────────┘     └─────────────────┘     └─────────────────┘     └─────────────────┘

2. Script Estándar de Ingesta y Limpieza en Node.js

[REQUIRED] Usar este script base (scripts/import-client-data.mjs) para cualquier importación de cliente:

javascript
import fs from 'node:fs'
import { parse } from 'csv-parse'
import { z } from 'zod'
import { createClient } from '@supabase/supabase-js'

// 1. Conexión autoritativa (Usa SERVICE_ROLE para bypass de RLS durante migración inicial)
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_ROLE_KEY)

// 2. Esquema Zod de Sanitización y Validación
const ClientRowSchema = z.object({
  email: z.string().trim().toLowerCase().email(),
  fullName: z.string().trim().min(2).max(100),
  phone: z.string().transform(val => val.replace(/\D/g, '')).pipe(z.string().min(7)),
  balanceCents: z.coerce.number().int().nonnegative().default(0) // MONEY-001: Siempre centavos
})

async function runImport(filePath) {
  const parser = fs.createReadStream(filePath).pipe(parse({ columns: true, trim: true }))
  
  let validRecords = []
  let errorCount = 0
  const BATCH_SIZE = 500
  const seenEmails = new Set()

  for await (const rawRow of parser) {
    // Normalizar nombres de columnas del Excel del cliente
    const normalized = {
      email: rawRow['Correo'] || rawRow['Email'] || rawRow['email'],
      fullName: rawRow['Nombre'] || rawRow['Nombre Completo'] || rawRow['name'],
      phone: rawRow['Telefono'] || rawRow['Celular'] || rawRow['phone'] || '0000000000',
      balanceCents: Math.round(parseFloat(rawRow['Saldo'] || rawRow['balance'] || '0') * 100)
    }

    const result = ClientRowSchema.safeParse(normalized)

    if (!result.success) {
      console.warn(`⚠️ Fila inválida omitida (${normalized.email}):`, result.error.issues[0].message)
      errorCount++
      continue
    }

    // Deduplicación en memoria
    if (seenEmails.has(result.data.email)) {
      console.warn(`⚠️ Correo duplicado omitido: ${result.data.email}`)
      continue
    }
    seenEmails.add(result.data.email)
    validRecords.push(result.data)

    // Inserción en lotes para no saturar memoria ni conexiones de BD
    if (validRecords.length >= BATCH_SIZE) {
      await insertBatch(validRecords)
      validRecords = []
    }
  }

  // Insertar registros restantes
  if (validRecords.length > 0) {
    await insertBatch(validRecords)
  }

  console.log(`✅ Ingesta finalizada: ${seenEmails.size} importados, ${errorCount} errores omitidos.`)
}

async function insertBatch(records) {
  const { error } = await supabase
    .from('clients')
    .upsert(records, { onConflict: 'email' }) // Evita fallos por colisión

  if (error) {
    console.error('❌ Error al insertar lote en base de datos:', error.message)
    throw error
  }
}

// Ejecución: node scripts/import-client-data.mjs datos_cliente.csv
runImport(process.argv[2]).catch(console.error)

3. Checklist de Verificación de Ingesta de Datos

  • [ ] Todos los valores monetarios se convirtieron a enteros en centavos (MONEY-001).
  • [ ] No existen correos o identificadores duplicados (onConflict: 'email').
  • [ ] Los números de teléfono se limpiaron de caracteres especiales (transform(/\D/g, '')).
  • [ ] Se generó un log con las filas que no pudieron ser importadas para entrega al cliente.

261 documentos indexados · generado desde INDEX.json