Files
FRASA-CAS/api-gateway/src/controllers/oportunidades.controller.ts
Consult AS Technology 89d95eb53b 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.
2026-09-11 11:47:17 +00:00

104 lines
4.3 KiB
TypeScript

import type { Request, Response } from 'express'
import { z } from 'zod'
import { VOCABULARIO, type MotivoNoCompra } from '../config/vocabulario.js'
import { prisma } from '../services/prisma.js'
import { crmLocal, esIdLocal } from '../services/crm-local.service.js'
import * as auditoria from '../services/auditoria.service.js'
/** Categorías del dropdown "Motivo No Compra" (vocabulario CRM FRASA) */
export const MOTIVOS_NO_COMPRA = VOCABULARIO.MOTIVOS_NO_COMPRA
/**
* Subcategorías por categoría, tomadas del catálogo del tablero de referencia
* "Motivos No Compra" del workspace CRM FRASA. NOTA: 'Mesualidad' y 'Cambo de
* Planes' llevan las faltas de ortografía originales del catálogo del cliente.
*/
export const SUBCATEGORIAS_NO_COMPRA: Record<MotivoNoCompra, readonly string[]> = VOCABULARIO.SUBCATEGORIAS
const perderSchema = z.object({
motivo: z.enum(MOTIVOS_NO_COMPRA),
comentario: z.string().optional(),
})
/**
* Marca una oportunidad como perdida (etapa 'Perdido' + motivo/submotivo en la
* fila local) y encola el DESCARTE. SIEMPRE local.
*/
export async function perderOportunidadEndpoint(req: Request, res: Response): Promise<void> {
const body = perderSchema.parse(req.body)
const mondayItemId = String(req.params['mondayItemId'])
await crmLocal.descartar(mondayItemId, body.motivo, {
fuente: 'oportunidad',
comentario: body.comentario,
usuarioId: req.usuarioToken?.sub,
sombra: !esIdLocal(mondayItemId),
})
res.json({ mondayItemId, etapa: 'Perdido', motivo: body.motivo, motivoItemId: null })
auditoria.registrar(req.usuarioToken?.sub ?? 'sistema', 'PERDER_OPORTUNIDAD', 'oportunidad', mondayItemId, body.motivo)
}
// ---------- Cambio de etapa desde Mi día ----------
/**
* Etapas del funnel que el asesor puede mover desde los botones de
* "Mis oportunidades" (vocabulario canónico del CRM — el frontend las manda
* tal cual).
*/
export const ETAPAS_OPORTUNIDAD_PERMITIDAS = VOCABULARIO.FASES_OPORTUNIDAD
const cambiarEtapaSchema = z.object({
etapa: z.enum(ETAPAS_OPORTUNIDAD_PERMITIDAS),
// En 'Agendo Cita' puede setear la fecha de la cita
fecha: z.string().optional(),
})
/**
* Cambio de etapa local: guarda la etapa en la fila local y encola
* CAMBIO_ETAPA. En 'Aparto' crea además el apartado local (crmLocal.apartar).
* Devuelve el unidadCodigo de la oportunidad si existe (para bloquear la unidad).
*/
async function cambiarEtapaLocal(
itemId: string,
etapa: (typeof ETAPAS_OPORTUNIDAD_PERMITIDAS)[number],
fecha: string | undefined,
usuarioId: string | undefined,
): Promise<string | null> {
if (etapa === 'Aparto') {
await crmLocal.apartar(itemId, usuarioId)
// Leer la oportunidad local para obtener el unidadCodigo y bloquear la unidad
const captura = await prisma.leadCaptura.findUnique({ where: { leadId: itemId } })
let unidadCodigo = ''
if (captura?.datosJson) {
try {
const datos = JSON.parse(captura.datosJson) as Record<string, unknown>
if (typeof datos['unidadCodigo'] === 'string') unidadCodigo = datos['unidadCodigo']
} catch { /* datosJson inválido: se ignora */ }
}
return unidadCodigo.trim() !== '' ? unidadCodigo.trim() : null
}
await crmLocal.cambiarEstado(itemId, etapa, 'OPORTUNIDADES', {
fecha,
usuarioId,
sombra: !esIdLocal(itemId),
})
return null
}
/** POST /api/oportunidades/:itemId/etapa { etapa, fecha? } */
export async function cambiarEtapaOportunidadEndpoint(req: Request, res: Response): Promise<void> {
const body = cambiarEtapaSchema.parse(req.body)
const itemId = String(req.params['itemId'])
const unidadCodigo = await cambiarEtapaLocal(itemId, body.etapa, body.fecha, req.usuarioToken?.sub)
// Bloquear la unidad del inventario local si la oportunidad tiene una asignada
if (body.etapa === 'Aparto' && unidadCodigo !== null) {
try {
await prisma.unidad.updateMany({ where: { codigo: unidadCodigo }, data: { status: 'APARTADA' } })
} catch (err) {
// eslint-disable-next-line no-console
console.error(`No se pudo bloquear la unidad ${unidadCodigo} de la oportunidad ${itemId}:`, err)
}
}
res.json({ itemId, etapa: body.etapa, ...(body.fecha !== undefined ? { fecha: body.fecha } : {}) })
auditoria.registrar(req.usuarioToken?.sub ?? 'sistema', 'CAMBIAR_ETAPA', 'oportunidad', itemId, body.etapa)
}