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:
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user