diff --git a/api-gateway/scripts/importar-cobranza-maestro.js b/api-gateway/scripts/importar-cobranza-maestro.js new file mode 100644 index 0000000..c871570 --- /dev/null +++ b/api-gateway/scripts/importar-cobranza-maestro.js @@ -0,0 +1,304 @@ +#!/usr/bin/env node +/** + * Reimporta COBRANZA PREVENTA 060926.xlsx a PlanPago + Pago + AplicacionPago + * con mapeo EXPLÍCITO por encabezado (reemplaza a importar-cobranza-preventa.js, + * que fabricaba fechas 'ago 2025' y dejaba montoPagado en 0). + * + * Uso: + * node scripts/importar-cobranza-maestro.js [--dry] [ruta.xlsx] + * + * Reglas: + * - Limpia TODO PlanPago/Pago/AplicacionPago antes de insertar (salvo --dry). + * - Fila 3 de cada hoja = encabezados (búsqueda por nombre, las hojas varían + * de ancho 45-76 cols). Columnas con encabezado fecha (01/12/24…) = grid + * mensual de pagos. + * - Plan: CTE, NOMBRE, APARTADO PAGADO + ENGANCHE PAGADO → engancheTotal, + * MESES + MES INICIO + MONTO MENSUALIDAD → calendario programado. + * - Pagos programados: numPagos mensualidades desde MES INICIO + * (montoPagado=0; el módulo deriva VENCIDO por fecha). + * - Celda de grid con valor > 0 = abono real del mes → AplicacionPago + * (fechaPago = mes de la columna) sobre la mensualidad de ese mes; + * si no hay mensualidad programada en ese mes → Pago standalone pagado. + */ + +import { PrismaClient } from '@prisma/client' +import XLSX from 'xlsx' +import { readFileSync } from 'fs' +import { fileURLToPath } from 'url' +import { dirname, join } from 'path' + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +if (!process.env.DATABASE_URL) { + try { + const env = readFileSync(join(__dirname, '..', '.env'), 'utf8') + const linea = env.split('\n').find((l) => l.startsWith('DATABASE_URL=')) + if (linea) process.env.DATABASE_URL = linea.slice('DATABASE_URL='.length).trim().replace(/^"|"$/g, '') + } catch { /* usar el del entorno */ } +} + +const prisma = new PrismaClient() +const DRY = process.argv.includes('--dry') +const RUTA = process.argv.slice(2).find((a) => !a.startsWith('--')) || '/root/COBRANZA PREVENTA 060926.xlsx' + +const HOJAS = ['ZAFIRO', 'MUTUO', 'SOLE', 'ATELIER', 'ESTANZA', 'SOLANA I', 'SOLANA II', 'UMBRAL', 'DIAGONAL 40', 'SENDA'] + +// ── Helpers ────────────────────────────────────────────────────────── + +function norm(h) { + return String(h ?? '') + .replace(/\n/g, ' ') + .trim() + .toUpperCase() + .replace(/\./g, ' ') + .replace(/\s+/g, ' ') + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') +} + +const toStr = (v) => (v == null ? null : String(v).trim() || null) +const toFloat = (v) => { + if (v == null || v === '') return null + if (typeof v === 'number') return v + const n = parseFloat(String(v).replace(/[$,\s]/g, '')) + return Number.isFinite(n) ? n : null +} + +/** Date UTC media-noche desde Date | 'dd/mm/yy' | Date-like; null si inválida */ +function aFecha(v) { + if (v instanceof Date) return isNaN(v.getTime()) ? null : v + if (v == null) return null + const s = String(v).trim() + if (!s) return null + const dmy = s.match(/^(\d{1,2})[/\-](\d{1,2})[/\-](\d{2,4})$/) + if (dmy) { + let y = parseInt(dmy[3], 10) + if (y < 100) y += 2000 + const m = parseInt(dmy[2], 10) + if (m < 1 || m > 12) return null + return new Date(Date.UTC(y, m - 1, parseInt(dmy[1], 10))) + } + const n = Date.parse(s) + return isNaN(n) ? null : new Date(n) +} + +function addMeses(fecha, n) { + return new Date(Date.UTC(fecha.getUTCFullYear(), fecha.getUTCMonth() + n, fecha.getUTCDate())) +} + +const claveMes = (f) => `${f.getUTCFullYear()}-${f.getUTCMonth()}` + +// Encabezado → campo del plan (los que el modelo tiene) +const CAMPOS = { + 'CASA': 'unidad', + 'DEPTO': 'unidad', + 'CTE': 'ventaRef', + 'NOMBRE': 'clienteNombre', + 'FECHA VENTA': 'fechaVenta', + 'APARTADO PAGADO': 'apartado', + 'APARTADO': 'apartado', + 'ENGANCHE PAGADO': 'enganche', + 'ENGANCHE': 'enganche', + 'MESES': 'meses', + 'MENSUALIDADES': 'meses', + 'MES INICIO': 'mesInicio', + 'MES FINAL': 'mesFinal', + 'MONTO MENSUALIDAD': 'mensualidad', + 'MONTO MENSUALID': 'mensualidad', + 'MONTO MENSUAL': 'mensualidad', +} + +// ── Lectura ────────────────────────────────────────────────────────── + +function leer() { + console.log(`Leyendo: ${RUTA} ${DRY ? '(DRY-RUN, no escribe)' : '(BORRA y reimporta)'}`) + const wb = XLSX.readFile(RUTA, { cellDates: true }) + + const planes = [] + let sinFechaInicio = 0 + + for (const nombreHoja of HOJAS) { + const sheet = wb.Sheets[nombreHoja] + if (!sheet) { console.log(`⚠ Hoja "${nombreHoja}" no existe, saltando`); continue } + const filas = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: null }) + + // buscar fila de encabezados: la que trae NOMBRE y CTE + let headerIdx = -1 + for (let i = 0; i < Math.min(filas.length, 6); i++) { + const hs = (filas[i] || []).map(norm) + if (hs.includes('NOMBRE') && hs.includes('CTE')) { headerIdx = i; break } + } + if (headerIdx === -1) { console.log(`⚠ "${nombreHoja}": sin encabezados, saltando`); continue } + const headers = (filas[headerIdx] || []).map(norm) + + const col = {} + const mesesGrid = [] // { colIdx, fecha } + const vistos = new Set() + for (let c = 0; c < headers.length; c++) { + const h = headers[c] + if (!h) continue + // encabezado con fecha (Date o 'dd/mm/yy') → columna del grid mensual + const raw = filas[headerIdx][c] + const f = aFecha(raw) ?? aFecha(h) + if (f) { mesesGrid.push({ colIdx: c, fecha: f }); continue } + const campo = CAMPOS[h] ?? (h.startsWith('MONTO MENSUALID') ? 'mensualidad' : null) + if (!campo || vistos.has(campo)) continue + vistos.add(campo) + col[campo] = c + } + + let planesHoja = 0, pagosProg = 0, abonos = 0, standalone = 0 + for (let r = headerIdx + 1; r < filas.length; r++) { + const fila = filas[r] + if (!fila) continue + const clienteNombre = toStr(fila[col.clienteNombre]) + if (!clienteNombre || clienteNombre.length < 3 || /TOTAL/i.test(clienteNombre)) continue + + const unidadCodigo = col.unidad != null ? (toStr(fila[col.unidad]) ?? '') : '' + const ventaRef = col.ventaRef != null ? toStr(fila[col.ventaRef]) : null + const apartado = col.apartado != null ? (toFloat(fila[col.apartado]) ?? 0) : 0 + const enganche = col.enganche != null ? (toFloat(fila[col.enganche]) ?? 0) : 0 + const meses = col.meses != null ? (parseInt(String(fila[col.meses] ?? '0'), 10) || 0) : 0 + const mesInicio = col.mesInicio != null ? aFecha(fila[col.mesInicio]) : null + const mensualidad = col.mensualidad != null ? (toFloat(fila[col.mensualidad]) ?? 0) : 0 + + // abonos del grid: valor > 0 por columna-mes + const abonosGrid = [] + for (const g of mesesGrid) { + const monto = toFloat(fila[g.colIdx]) + if (monto && monto > 0) abonosGrid.push({ fecha: g.fecha, monto }) + } + + const fechaInicio = mesInicio ?? (col.fechaVenta != null ? aFecha(fila[col.fechaVenta]) : null) ?? abonosGrid[0]?.fecha ?? null + if (!fechaInicio) { sinFechaInicio++; continue } + + planes.push({ + proyecto: nombreHoja, + plan: { + ventaRef, clienteNombre, clienteTelefono: null, unidadCodigo, + proyecto: nombreHoja, fechaInicio, + engancheTotal: apartado + enganche, + numPagos: meses, mensualidad, tasaMoraMensual: 0, status: 'ACTIVO', + }, + abonosGrid, + }) + planesHoja++ + } + console.log(` ${nombreHoja}: ${planesHoja} planes, ${mesesGrid.length} columnas-mes`) + } + + if (sinFechaInicio) console.log(`⚠ ${sinFechaInicio} filas sin fecha de inicio ni venta, saltadas`) + console.log(`Total planes: ${planes.length}`) + return planes +} + +// ── Escritura ──────────────────────────────────────────────────────── + +async function escribir(planesLeidos) { + console.log('\nBorrando planes/pagos/aplicaciones/notas previos…') + const borrados = await prisma.planPago.deleteMany({}) // cascada: Pago, AplicacionPago, NotaPlan + console.log(`Planes borrados: ${borrados.count}`) + + let totalPagos = 0, totalAbonos = 0, totalStandalone = 0 + + for (const { plan, abonosGrid } of planesLeidos) { + const creado = await prisma.planPago.create({ data: plan }) + + // 1) calendario programado: mensualidades desde MES INICIO + const pagosProg = [] + if (plan.numPagos > 0 && plan.mensualidad > 0) { + for (let i = 0; i < plan.numPagos; i++) { + pagosProg.push({ + numPago: i + 1, + fechaVencimiento: addMeses(plan.fechaInicio, i), + monto: plan.mensualidad, + montoPagado: 0, + }) + } + } + + // 2) abonos del grid sobre la mensualidad del mismo mes; sobrantes → standalone + const aplicaciones = [] // { idxPago, fechaPago, monto } + const standalone = [] + for (const ab of abonosGrid) { + const idx = pagosProg.findIndex((p) => claveMes(p.fechaVencimiento) === claveMes(ab.fecha)) + if (idx !== -1) { + aplicaciones.push({ idx, fechaPago: ab.fecha, monto: ab.monto }) + } else { + standalone.push({ fecha: ab.fecha, monto: ab.monto }) + } + } + + // aplicar abonos al montoPagado (sin pasar del monto programado) + for (const ap of aplicaciones) { + const p = pagosProg[ap.idx] + p.montoPagado = Math.min(p.monto, p.montoPagado + ap.monto) + } + + // crear pagos programados + let numSiguiente = pagosProg.length + for (const p of pagosProg) { + await prisma.pago.create({ + data: { planId: creado.id, numPago: p.numPago, fechaVencimiento: p.fechaVencimiento, monto: p.monto, montoPagado: p.montoPagado }, + }) + } + // standalone: abonos de meses sin mensualidad programada + const idsCreados = await prisma.pago.findMany({ where: { planId: creado.id }, select: { id: true, numPago: true } }) + const porNum = new Map(idsCreados.map((p) => [p.numPago, p.id])) + for (const s of standalone) { + numSiguiente++ + const pago = await prisma.pago.create({ + data: { planId: creado.id, numPago: numSiguiente, fechaVencimiento: s.fecha, monto: s.monto, montoPagado: s.monto }, + }) + porNum.set(pago.numPago, pago.id) + totalStandalone++ + } + totalPagos += pagosProg.length + + // 3) aplicaciones de pago (abonos con fecha real) + for (const ap of aplicaciones) { + const pagoId = porNum.get(ap.idx + 1) + if (!pagoId) continue + await prisma.aplicacionPago.create({ + data: { pagoId, fechaPago: ap.fechaPago, monto: ap.monto, referencia: 'Importación maestro cobranza 06/09/26', capturadoPorId: null }, + }) + totalAbonos++ + } + } + + console.log(`\nPlanes: ${planesLeidos.length} | Pagos programados: ${totalPagos} | Standalone: ${totalStandalone} | Abonos (AplicacionPago): ${totalAbonos}`) +} + +async function verificar() { + const planes = await prisma.planPago.count() + const pagos = await prisma.pago.count() + const abonos = await prisma.aplicacionPago.count() + const conAbono = await prisma.pago.count({ where: { montoPagado: { gt: 0 } } }) + console.log(`\nVerificación: planes=${planes} pagos=${pagos} abonos=${abonos} pagosConAbono=${conAbono}`) + + const muestra = await prisma.planPago.findFirst({ where: { numPagos: { gte: 4 } }, include: { pagos: { orderBy: { numPago: 'asc' }, take: 6, include: { aplicaciones: true } } } }) + if (muestra) { + console.log(`\nMuestra ${muestra.proyecto} ${muestra.unidadCodigo} ${muestra.clienteNombre} (numPagos=${muestra.numPagos}, mensualidad=${muestra.mensualidad}):`) + for (const p of muestra.pagos) { + const apps = p.aplicaciones.map((a) => `${a.monto}@${a.fechaPago.toISOString().slice(0, 10)}`).join(', ') + console.log(` #${p.numPago} venc=${p.fechaVencimiento.toISOString().slice(0, 10)} monto=${p.monto} pagado=${p.montoPagado}${apps ? ` abonos: ${apps}` : ''}`) + } + } + + const porProyecto = await prisma.planPago.groupBy({ by: ['proyecto'], _count: true, orderBy: { proyecto: 'asc' } }) + console.log('Planes por proyecto:', porProyecto.map((x) => `${x.proyecto}=${x._count}`).join(' ')) +} + +async function main() { + const planesLeidos = leer() + if (!DRY) { + await escribir(planesLeidos) + await verificar() + } else { + const conGrid = planesLeidos.filter((p) => p.abonosGrid.length > 0).length + console.log(`\n(dry-run) planes con abonos en grid: ${conGrid}; nada escrito`) + } + await prisma.$disconnect() +} + +main().catch((err) => { console.error('Error:', err); process.exit(1) }) diff --git a/api-gateway/scripts/importar-cobranza-preventa.js b/api-gateway/scripts/importar-cobranza-preventa.js deleted file mode 100755 index 2174d2e..0000000 --- a/api-gateway/scripts/importar-cobranza-preventa.js +++ /dev/null @@ -1,128 +0,0 @@ -#!/usr/bin/env node -/** - * Importa el archivo COBRANZA PREVENTA a PlanPago + Pago. - * Lee cada hoja de desarrollo, extrae clientes y sus pagos mensuales. - */ -import { PrismaClient } from '@prisma/client' -import XLSX from 'xlsx' - -const prisma = new PrismaClient() -const toStr = (v) => v == null ? null : String(v).trim() || null -const toFloat = (v) => { - if (v == null) return null - if (typeof v === 'number') return v - const n = parseFloat(String(v).replace(/[$,\s]/g, '')) - return Number.isFinite(n) ? n : null -} -const toDate = (v) => { - if (v == null) return null - if (v instanceof Date) return v - const n = Date.parse(String(v).trim()) - return isNaN(n) ? null : new Date(n) -} - -const DESARROLLOS = ['ZAFIRO', 'MUTUO', 'SOLE', 'ATELIER', 'ESTANZA', 'SOLANA I', 'SOLANA II', 'UMBRAL', 'DIAGONAL 40', 'SENDA'] - -const ruta = '/root/COBRANZA PREVENTA 060926.xlsx' -console.log(`Leyendo: ${ruta}`) -const wb = XLSX.readFile(ruta) - -let totalPlanes = 0, totalPagos = 0, errores = 0 - -for (const nombreHoja of DESARROLLOS) { - const sheet = wb.Sheets[nombreHoja] - if (!sheet) { console.log(` Hoja "${nombreHoja}" no encontrada, saltando`); continue } - - const rows = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: null }) - console.log(`\n ${nombreHoja}: ${rows.length} filas`) - - // Header row is typically row 2 (index 2), data starts at row 3 (index 3) - // Find the header row with 'NOMBRE' or 'CTE' - let headerIdx = -1 - for (let i = 0; i < Math.min(rows.length, 5); i++) { - if (rows[i]?.[2] && String(rows[i][2]).includes('NOMBRE')) { - headerIdx = i - break - } - } - if (headerIdx === -1) { - console.log(` No se encontró header en ${nombreHoja}, saltando`) - continue - } - - const dataRows = rows.slice(headerIdx + 1) - let hojaPlanes = 0, hojaPagos = 0 - - for (const row of dataRows) { - const unidadCodigo = toStr(row[0]) - const clienteId = toStr(row[1]) - const clienteNombre = toStr(row[2]) - if (!clienteNombre || clienteNombre.length < 3) continue - - const precioVenta = toFloat(row[3]) || 0 - const fechaVenta = toDate(row[4]) - const tipoVenta = toStr(row[5]) || '' - const asesor = toStr(row[6]) || '' - const apartadoPagado = toFloat(row[10]) || 0 - const enganchePagado = toFloat(row[11]) || 0 - const meses = parseInt(String(row[13] || '0')) || 0 - const mesInicio = toDate(row[14]) - const mensualidad = toFloat(row[16]) || 0 - const totalPagado = toFloat(row[58]) || 0 - - // Create PlanPago - try { - const plan = await prisma.planPago.create({ - data: { - ventaRef: clienteId || null, - clienteNombre, - clienteTelefono: null, - unidadCodigo: unidadCodigo || '', - proyecto: nombreHoja, - fechaInicio: mesInicio || fechaVenta || new Date(), - engancheTotal: apartadoPagado + enganchePagado, - numPagos: meses || 1, - mensualidad, - tasaMoraMensual: 0, - status: 'ACTIVO', - vendedorId: null, - }, - }) - hojaPlanes++ - - // Create Pago records for each month with a value - let numPago = 1 - for (let col = 20; col <= 43; col++) { - const montoMes = toFloat(row[col]) - if (montoMes && montoMes > 0) { - // Calculate date for this payment month - const fechaMes = new Date(2025, 7 + (col - 20), 1) // Starting from Aug 2025 - await prisma.pago.create({ - data: { - planId: plan.id, - numPago, - fechaVencimiento: fechaMes, - monto: montoMes, - montoPagado: 0, // Will be updated if we have payment data - }, - }) - hojaPagos++ - numPago++ - } - } - } catch (err) { - errores++ - if (errores <= 3) console.log(` Error: ${clienteNombre} - ${err.message?.slice(0, 100)}`) - } - } - totalPlanes += hojaPlanes - totalPagos += hojaPagos - console.log(` ${hojaPlanes} planes, ${hojaPagos} pagos`) -} - -console.log(`\n=== Resumen ===`) -console.log(`Planes creados: ${totalPlanes}`) -console.log(`Pagos creados: ${totalPagos}`) -console.log(`Errores: ${errores}`) - -await prisma.$disconnect()