14 servicios eliminados, 7 controllers local-only, crons locales, config sin MONDAY_*, frontend sin banners/avisos Monday. 552 tests API + 7 apps construidas.
104 lines
4.3 KiB
TypeScript
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)
|
|
}
|