feat(purga): Monday eliminado completamente del sistema

14 servicios eliminados, 7 controllers local-only, crons locales,
config sin MONDAY_*, frontend sin banners/avisos Monday.
552 tests API + 7 apps construidas.
This commit is contained in:
2026-09-11 11:47:17 +00:00
parent 64b01246d6
commit 89d95eb53b
130 changed files with 2018 additions and 14747 deletions

View File

@@ -1,18 +1,59 @@
import { statusPago, statusPlan, type PlanPago } from './cobranza.service.js'
import type { LeadMonday, MondayService, OportunidadMonday, Unidad } from './monday.service.js'
import type { UnidadCotizable } from './cotizacion.service.js'
import { ETAPAS_PIPELINE } from './etapas.js'
import { prisma } from './prisma.js'
import { saludMonday, type SaludMondayService } from './salud-monday.service.js'
import { unidadLocalADto } from './unidades-local.service.js'
import { AppError } from '../utils/errors.js'
/** Lead del embudo tal como lo consume el agregador (fila de LeadCaptura mapeada) */
export type LeadFila = {
id: string
nombre: string
createdAt: string // ISO
updatedAt: string // ISO
estado: string
origen: string
telefono: string
email: string
vendedorIds: number[]
desarrollo?: string
modeloInteres?: string
}
/** Oportunidad/afluencia tal como la consume el agregador (fila de LeadCaptura mapeada) */
export type OportunidadFila = {
id: string
nombre: string
createdAt: string // ISO
updatedAt: string // ISO
etapa: string
unidadCodigo: string
precioFinal: number
asesorIds: number[]
telefono: string
email: string
origen: string
desarrollo?: string
modeloInteres?: string
medioContacto?: string
edad?: number | null
estadoCivil?: string
tipoEmpleo?: string
credito?: string
tipoEconomia?: string
califica?: string
rangoSalarial?: string
loQuierePara?: string
}
export type Unidad = UnidadCotizable
/**
* Agregador de reportes para la app de tableros (/dash/).
* En UNA respuesta junta: leads (Monday, tablero Leads — captura ligera),
* afluencia/perfilamiento (Monday, tablero Oportunidades — el Alta Afluencia
* vive ahí), oportunidades (Monday), unidades (Monday), cobranza (Prisma
* local) y actividades abiertas (Monday).
* Cache en memoria de 60 s para no martillar la API de Monday.
* En UNA respuesta junta: leads (captura ligera, tabla LeadCaptura board
* LEADS), afluencia/perfilamiento (board OPORTUNIDADES — el Alta Afluencia
* vive ahí), oportunidades, unidades (espejo local), cobranza (Prisma local)
* y actividades abiertas (ActividadLocal).
* Cache en memoria de 60 s para no martillar SQLite en cada refresh.
*/
export const CACHE_REPORTES_MS = 60 * 1000
@@ -77,7 +118,7 @@ export interface ResumenReportes {
montoVencido: number
}
actividadesAbiertas: number
/** true cuando el resumen se calculó desde tablas locales (Monday caído): métricas parciales */
/** false: el resumen se calculó desde tablas locales completas */
parcial?: boolean
}
@@ -86,8 +127,8 @@ export interface DatosResumen {
// La sección `leads` (total/nuevos/porEstado/porOrigen) lee el tablero Leads
// (captura ligera); el `perfil` de la afluencia lee Oportunidades (Alta
// Afluencia), que es también la fuente de la sección `oportunidades`
leads: LeadMonday[]
oportunidades: OportunidadMonday[]
leads: LeadFila[]
oportunidades: OportunidadFila[]
unidades: Unidad[]
planes: PlanPago[] // dominio de cobranza (con pagos)
actividadesAbiertas: number
@@ -301,15 +342,13 @@ export function construirResumen(datos: DatosResumen, ahora: Date, dias?: number
// ---------- Servicio con cache ----------
type MondayReportes = Pick<MondayService, 'getLeads' | 'getOportunidades' | 'getUnidades' | 'getActividadesAbiertas'>
/** Carga los planes de cobranza en forma de dominio (Prisma en prod, fixture en tests) */
export type CargadorPlanes = () => Promise<PlanPago[]>
/** Datos del embudo/inventario leídos de tablas locales (modo degradado) */
export interface DatosLocalesReportes {
leads: LeadMonday[]
oportunidades: OportunidadMonday[]
leads: LeadFila[]
oportunidades: OportunidadFila[]
unidades: Unidad[]
actividadesAbiertas?: number
}
@@ -322,9 +361,7 @@ export interface OpcionesReportes {
now?: () => number
/** TTL del cache en ms (default 60 s) */
cacheMs?: number
/** Salud de Monday (default: singleton saludMonday) */
salud?: Pick<SaludMondayService, 'estaDisponible'>
/** Datos locales para el fallback (default: tablas LeadCaptura/Unidad vía Prisma) */
/** Datos locales del embudo/inventario (default: tablas LeadCaptura/Unidad vía Prisma) */
cargarDatosLocales?: CargadorDatosLocales
}
@@ -337,7 +374,7 @@ function campoTexto(datos: Record<string, unknown>, campo: string): string {
/**
* Cargador local por defecto: leads/oportunidades desde LeadCaptura (board +
* etapa; el perfil de la afluencia va en datosJson) e inventario desde la
* tabla Unidad (espejo). Misma forma de DTO que las lecturas Monday.
* tabla Unidad (espejo).
*/
export async function cargarDatosLocalesPrisma(): Promise<DatosLocalesReportes> {
const capturas = await prisma.leadCaptura.findMany({
@@ -345,7 +382,7 @@ export async function cargarDatosLocalesPrisma(): Promise<DatosLocalesReportes>
})
const unidadesRows = await prisma.unidad.findMany()
const leads: LeadMonday[] = capturas
const leads: LeadFila[] = capturas
.filter((c) => c.board === 'LEADS')
.map((c) => ({
id: c.leadId,
@@ -358,7 +395,7 @@ export async function cargarDatosLocalesPrisma(): Promise<DatosLocalesReportes>
email: c.email ?? '',
vendedorIds: [],
}))
const oportunidades: OportunidadMonday[] = capturas
const oportunidades: OportunidadFila[] = capturas
.filter((c) => c.board === 'OPORTUNIDADES')
.map((c) => {
let datos: Record<string, unknown> = {}
@@ -404,19 +441,16 @@ export async function cargarDatosLocalesPrisma(): Promise<DatosLocalesReportes>
export class ReportesService {
private readonly now: () => number
private readonly cacheMs: number
private readonly salud: Pick<SaludMondayService, 'estaDisponible'>
private readonly cargarDatosLocales: CargadorDatosLocales
/** Cache por rango (clave '0' = todo el histórico) para no mezclar ventanas */
private cache: Map<string, { resumen: ResumenReportes; expira: number }> = new Map()
constructor(
private readonly monday: MondayReportes,
private readonly cargarPlanes: CargadorPlanes,
opciones: OpcionesReportes = {},
) {
this.now = opciones.now ?? Date.now
this.cacheMs = opciones.cacheMs ?? CACHE_REPORTES_MS
this.salud = opciones.salud ?? saludMonday
this.cargarDatosLocales = opciones.cargarDatosLocales ?? cargarDatosLocalesPrisma
}
@@ -438,36 +472,17 @@ export class ReportesService {
}
/**
* Camino normal: Monday + planes locales en paralelo (con el cache de 60 s
* de siempre). Camino degradado: Monday caído (circuit breaker) o AppError
* 502 a media lectura → resumen desde tablas locales con `parcial: true`.
* Resumen desde tablas locales (LeadCaptura por board, Unidad espejo,
* PlanPago y ActividadLocal), en paralelo y con el cache de 60 s de siempre.
*/
private async cargarResumen(ahoraMs: number, dias?: number): Promise<ResumenReportes> {
if (this.salud.estaDisponible()) {
try {
// Las fuentes de Monday se piden en paralelo; Prisma va por su lado
const [leads, oportunidades, unidades, actividadesAbiertas, planes] = await Promise.all([
this.monday.getLeads(),
this.monday.getOportunidades(),
this.monday.getUnidades(),
this.monday.getActividadesAbiertas(),
this.cargarPlanes(),
])
return construirResumen({ leads, oportunidades, unidades, planes, actividadesAbiertas }, new Date(ahoraMs), dias)
} catch (err) {
if (!(err instanceof AppError && err.status === 502)) throw err
// Monday se cayó a media lectura: cae al camino local
}
}
const [locales, planes] = await Promise.all([this.cargarDatosLocales(), this.cargarPlanes()])
const resumen = construirResumen(
{ ...locales, planes, actividadesAbiertas: locales.actividadesAbiertas ?? 0 },
new Date(ahoraMs),
dias,
)
// En modo local, los datos son completos (no parciales): todas las métricas
// vienen de tablas locales. `parcial` solo se marca true cuando Monday cayó
// a media lectura (ya no aplica aquí).
// Los datos son completos (todas las métricas vienen de tablas locales)
return { ...resumen, parcial: false }
}
}