fix(cobranza): reimport del maestro con fechas reales y abonos (AplicacionPago)
This commit is contained in:
304
api-gateway/scripts/importar-cobranza-maestro.js
Normal file
304
api-gateway/scripts/importar-cobranza-maestro.js
Normal file
@@ -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) })
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user