fix(auditoria general): 25 errores encontrados en la revision modulo por modulo

SEGURIDAD (agente IA):
- Conversaciones: GET /ai/conversations/:id y el reanudar chat verifican
  propiedad (antes cualquier usuario leia/continuaba la conversacion de otro).
- /ai/documents pasa a solo-admin (antes cualquien podia inyectar o borrar
  documentos de la base de conocimiento).

DATOS / CORRUPCION:
- Cotizador: elegir un cliente del maestro en el buscador ya no crea un lead
  fantasma "Registro previo (CRM anterior)" en el embudo.
- Afluencia con oportunidadId inexistente: 404 en vez de crear sombra fantasma.
- Cobranza: los 103 planes importados quedaron con vendedor asignado (7 con su
  asesor real segun el Excel, el resto al admin) y el motor de recordatorios
  cae al admin cuando un plan no tiene vendedor: antes los 411 recordatorios
  se registraban 'sin-destinatario' y no los veia nadie.
- Backups: script reescrito para el host real (sqlite .backup + uploads + env
  en /root/backups) y cron instalado (antes: script que asumia docker y sin
  cron programado = cero respaldos).

REPORTES / METRICAS:
- Mi tablero: el embudo ya excluye cancelaciones reales (Cancelo/Cancelacion
  Apartado) y cuenta como vendidos Firma Cliente/Escriturado/Titulado (antes
  41 "apartados" donde habia 13 activos y vendidos buscaba una etapa que no
  existe); tasa de descarte ya compara el mismo periodo.
- Scoring: etapa real 'Agendo Cita' (antes 'AGENDO' nunca aplicaba) y los
  descartados/perdidos ya no reciben score.
- ver_cobranza (IA): ignora planes cancelados y no cuenta como vencido el pago
  que vence hoy.

UI / FLUJOS:
- mi-dia: typo CANCO -> CANCELO (los cancelados reaparecen en Escrituracion);
  confirmar cita ya no retrocede la etapa de Cotizador/Aparto (boton y kanban).
- alta-clientes: la firma ya no se borra al navegar fuera del paso de firmas;
  toast de exito solo cuando el API confirma; prefill de apartado trae
  direccion, estado civil y fecha de nacimiento desde el alta.
- Auditoria: el filtro por usuario funciona (el front enviaba 'usuario' y el
  API esperaba 'usuarioId') y 'hasta' incluye todo el dia.
- Contactos: paginacion aplicada despues de fusionar fuentes (antes saltaban o
  se repetian contactos entre paginas y 'Siguiente' se deshabilitaba antes de
  tiempo).
- Buscador global: leads -> /hoy/, contactos -> /; el cotizador entiende
  ?cotizacion=<id> para retomar desde la liga.
- Export mi-dia: columnas Desarrollo/Origen rellenas y sin duplicados.
- Recibo de cobranza: "Recibio" es FRASA (el cliente paga, no recibe).
- Alta de plan de cobranza: proyecto libre con sugerencias del catalogo real
  (antes limitado a 3 proyectos que no cubrian la cartera).

Verificado en vivo: autorizacion IA (3x 403), filtro de auditoria, 404 de
afluencia. Suites: 717 API + 124 alta-clientes + 115 cotizador + 92 cobranza +
89 landing + 32 mi-dia en verde. Backup de hoy generado en /root/backups.
This commit is contained in:
2026-09-24 10:54:09 +00:00
parent cfc0acb5d3
commit 8a2584438d
30 changed files with 258 additions and 102 deletions

View File

@@ -114,6 +114,12 @@ export async function obtenerConversacion(req: Request, res: Response): Promise<
include: { mensajes: { orderBy: { createdAt: 'asc' } } },
})
if (!conversacion) throw new NotFoundError('Conversación no encontrada')
// Propiedad: misma regla que al borrar — sólo el dueño o un rol de
// supervisión puede leer la conversación.
const tokenLectura = req.usuarioToken
if (tokenLectura && !esAdminOManager(tokenLectura.rol) && conversacion.usuarioId !== tokenLectura.sub) {
throw new AppError(403, 'Solo puedes ver tus conversaciones')
}
res.json({
id: conversacion.id,
createdAt: conversacion.createdAt,

View File

@@ -68,7 +68,7 @@ describe('listarAuditoriaEndpoint (GET /api/auditoria)', () => {
expect(prismaMock.auditoriaLog.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
createdAt: expect.objectContaining({ gte: expect.any(Date), lte: expect.any(Date) }),
createdAt: expect.objectContaining({ gte: expect.any(Date), lt: expect.any(Date) }),
}),
}),
)

View File

@@ -525,6 +525,22 @@ async function prefillApartadoLocal(itemId: string) {
})
}
// Datos del alta (payloadJson) que el prefill del front consume con claves
// legacy de Monday: dirección, estado civil y fecha de nacimiento
const contactoExtra: Record<string, string> = {}
if (cliente?.payloadJson) {
try {
const payload = JSON.parse(cliente.payloadJson) as { datosGenerales?: Record<string, unknown> }
const g = payload.datosGenerales ?? {}
const str = (v: unknown): string => (typeof v === 'string' && v.trim() !== '' ? v : '')
if (str(g['direccion']) !== '') contactoExtra['Dirección'] = str(g['direccion'])
if (str(g['estadoCivil']) !== '') contactoExtra['Estado Civil'] = str(g['estadoCivil'])
if (str(g['fechaNacimiento']) !== '') contactoExtra['Fecha Nacimiento'] = str(g['fechaNacimiento'])
} catch {
// payload inválido: el prefill sale parcial, como antes
}
}
return {
apartado: {
id: captura.leadId,
@@ -545,6 +561,7 @@ async function prefillApartadoLocal(itemId: string) {
Email: cliente.email ?? '',
CURP: cliente.curp,
RFC: cliente.rfc,
...contactoExtra,
}
: null,
oportunidad: oportunidadLocal

View File

@@ -149,8 +149,12 @@ export async function crearCotizacion(req: Request, res: Response): Promise<void
mondayItemId = opExistente
await prisma.cotizacion.update({ where: { id: cotizacion.id }, data: { mondayItemId } })
try {
// sombra:false: sólo se marca la etapa si el id corresponde a una
// oportunidad real. Antes (sombra para ids no locales) elegir un cliente
// del maestro en el buscador del cotizador creaba un LeadCaptura
// fantasma ("Registro previo (CRM anterior)") con pendienteSync.
await crmLocal.cambiarEstado(opExistente, 'Cotizador', 'OPORTUNIDADES', {
sombra: !esIdLocal(opExistente),
sombra: false,
usuarioId: req.usuarioToken?.sub,
})
} catch (err) {