fix(admin): reasignar mueve toda la cadena del cliente (lead+oportunidad+apartado) y sus actividades pendientes; 404 si el id no existe

This commit is contained in:
2026-09-29 19:42:42 +00:00
parent 17afcc9d8f
commit 1c1e6b04e4
4 changed files with 137 additions and 11 deletions

View File

@@ -34,6 +34,7 @@ const prismaMock = vi.hoisted(() => ({
findMany: vi.fn(),
},
automatizacionState: {
findFirst: vi.fn(),
findUnique: vi.fn(),
upsert: vi.fn(),
},
@@ -83,6 +84,7 @@ beforeEach(() => {
prismaMock.usuario.findUnique.mockResolvedValue(null)
prismaMock.cliente.findMany.mockResolvedValue([])
prismaMock.automatizacionState.findUnique.mockResolvedValue(null)
prismaMock.automatizacionState.findFirst.mockResolvedValue(null)
prismaMock.automatizacionState.upsert.mockResolvedValue({})
prismaMock.$transaction.mockImplementation(async (ops: Promise<unknown>[]) => Promise.all(ops))
})
@@ -449,17 +451,30 @@ describe('admin CRUD de leads — local', () => {
expect(json).toHaveBeenCalledWith({ eliminado: true, itemId: 'loc_1' })
})
it('asignarLeadEndpoint reasigna el lead localmente y responde con el nombre', async () => {
it('asignarLeadEndpoint reasigna la cadena completa y responde con el nombre', async () => {
prismaMock.usuario.findUnique.mockResolvedValue({ nombre: 'Martin Zavala' })
prismaMock.leadCaptura.findUnique.mockResolvedValue({ leadId: 'loc_1', board: 'LEADS', oportunidadId: null })
prismaMock.leadCaptura.updateMany.mockResolvedValue({ count: 1 })
const { res, json } = resFake()
await asignarLeadEndpoint(reqCon({ params: { itemId: 'loc_1' }, body: { usuarioId: 'usr-9' } }), res)
expect(prismaMock.leadCaptura.updateMany).toHaveBeenCalledWith({
where: { leadId: 'loc_1' },
where: { leadId: { in: ['loc_1'] } },
data: { usuarioId: 'usr-9' },
})
expect(json).toHaveBeenCalledWith({ itemId: 'loc_1', usuarioId: 'usr-9', nombre: 'Martin Zavala' })
expect(json).toHaveBeenCalledWith({ itemId: 'loc_1', usuarioId: 'usr-9', nombre: 'Martin Zavala', movidas: 1 })
})
it('asignarLeadEndpoint con lead inexistente → 404 (ya no es exito silencioso)', async () => {
prismaMock.usuario.findUnique.mockResolvedValue({ nombre: 'Martin Zavala' })
prismaMock.leadCaptura.findUnique.mockResolvedValue(null)
const { res } = resFake()
await expect(
asignarLeadEndpoint(reqCon({ params: { itemId: 'loc_fantasma' }, body: { usuarioId: 'usr-9' } }), res),
).rejects.toThrow('Lead no encontrado')
expect(prismaMock.leadCaptura.updateMany).not.toHaveBeenCalled()
})
it('asignarLeadEndpoint con usuario inexistente → 404', async () => {

View File

@@ -340,7 +340,9 @@ const asignarLeadSchema = z.object({
usuarioId: z.string().trim().min(1, 'usuarioId es obligatorio'),
})
/** PATCH /api/leads/:itemId/asignar — reasignar lead a otro vendedor (solo admin) */
/** PATCH /api/leads/:itemId/asignar — reasignar lead a otro vendedor (solo admin).
* Reasigna TODA la cadena del cliente (lead → oportunidad → apartado) y sus
* actividades pendientes, para no dejar el registro partido entre vendedores. */
export async function asignarLeadEndpoint(req: Request, res: Response): Promise<void> {
const itemId = String(req.params['itemId'])
const { usuarioId } = asignarLeadSchema.parse(req.body)
@@ -349,13 +351,9 @@ export async function asignarLeadEndpoint(req: Request, res: Response): Promise<
select: { nombre: true },
})
if (!usuario) throw new NotFoundError('Usuario no encontrado')
// La asignación local es la verdad: se actualiza la fila del lead
await prisma.leadCaptura.updateMany({
where: { leadId: itemId },
data: { usuarioId },
})
res.json({ itemId, usuarioId, nombre: usuario.nombre })
auditoria.registrar(req.usuarioToken?.sub ?? 'sistema', 'ASIGNAR_LEAD', 'lead', itemId, usuarioId)
const movidas = await crmLocal.reasignarCadena(itemId, usuarioId)
res.json({ itemId, usuarioId, nombre: usuario.nombre, movidas })
auditoria.registrar(req.usuarioToken?.sub ?? 'sistema', 'ASIGNAR_LEAD', 'lead', itemId, `${usuario.nombre} (${movidas} fila(s))`)
}
const moverLeadSchema = z.object({