355 lines
12 KiB
JavaScript
355 lines
12 KiB
JavaScript
const express = require('express');
|
|
const router = express.Router();
|
|
const Ticket = require('../models/Ticket');
|
|
const Company = require('../models/Company');
|
|
const Department = require('../models/Department');
|
|
const User = require('../models/User');
|
|
|
|
/**
|
|
* Middleware para validar llamadas M2M mediante API Key o Token de Agente
|
|
*/
|
|
async function authenticateM2M(req, res, next) {
|
|
const apiKey = req.header('x-api-key');
|
|
const authHeader = req.header('Authorization');
|
|
const token = authHeader && authHeader.startsWith('Bearer ') ? authHeader.substring(7) : null;
|
|
const provided = apiKey || token;
|
|
|
|
if (!provided) {
|
|
return res.status(401).json({
|
|
ok: false,
|
|
success: false,
|
|
error: 'Autenticación requerida. Proporcione header x-api-key o Authorization: Bearer <token>'
|
|
});
|
|
}
|
|
|
|
// 1. Validar contra variables locales de entorno autorizadas
|
|
const validTokens = [
|
|
process.env.WEBCONTROL_API_KEY,
|
|
process.env.SIGMA_AGENTE_TOKEN
|
|
].filter(Boolean);
|
|
|
|
if (validTokens.includes(provided)) {
|
|
req.authSource = 'local_env';
|
|
return next();
|
|
}
|
|
|
|
// 2. Validar contra la base de datos central webControl si está disponible
|
|
try {
|
|
const mysql = require('mysql2/promise');
|
|
const conn = await mysql.createConnection({
|
|
host: '192.168.10.149',
|
|
user: 'admin',
|
|
password: 'Dx.1706%',
|
|
database: 'webControl'
|
|
});
|
|
const [rows] = await conn.query(
|
|
'SELECT * FROM api_key WHERE key_value = ? AND is_active = 1 AND revoked_at IS NULL',
|
|
[provided]
|
|
);
|
|
await conn.end();
|
|
|
|
if (rows.length > 0) {
|
|
req.authSource = 'webcontrol_db';
|
|
req.apiKeyData = rows[0];
|
|
return next();
|
|
}
|
|
} catch (e) {
|
|
console.warn('[API Auth] Error consultando webControl.api_key:', e.message);
|
|
}
|
|
|
|
return res.status(403).json({
|
|
ok: false,
|
|
success: false,
|
|
error: 'Credencial inválida o no autorizada para el servicio de Help Desk'
|
|
});
|
|
}
|
|
|
|
// Aplicar autenticación M2M a todas las rutas de /api
|
|
router.use(authenticateM2M);
|
|
|
|
/**
|
|
* Función auxiliar para resolver o crear el usuario técnico de integración
|
|
*/
|
|
async function getTechnicalUser(companyId) {
|
|
const email = `bot_sigma-robot@sigmac.app`;
|
|
let user = await User.findByEmail(email);
|
|
if (!user) {
|
|
const id = await User.create(
|
|
companyId,
|
|
'Bot Soporte (sigma-robot)',
|
|
email,
|
|
null,
|
|
'client',
|
|
'bot',
|
|
'sigma-robot-m2m'
|
|
);
|
|
user = await User.findById(id);
|
|
}
|
|
return user;
|
|
}
|
|
|
|
/**
|
|
* Función auxiliar para resolver la empresa por ID, slug o dominio de app_origen
|
|
*/
|
|
async function resolveCompany(empresa, app_origen, company_id) {
|
|
if (company_id) {
|
|
const c = await Company.findById(parseInt(company_id));
|
|
if (c) return c;
|
|
}
|
|
if (empresa) {
|
|
if (!isNaN(empresa)) {
|
|
const c = await Company.findById(parseInt(empresa));
|
|
if (c) return c;
|
|
} else {
|
|
const c = await Company.findBySlug(String(empresa).toLowerCase().trim());
|
|
if (c) return c;
|
|
}
|
|
}
|
|
if (app_origen) {
|
|
const originLower = String(app_origen).toLowerCase();
|
|
if (originLower.includes('domus')) {
|
|
const c = await Company.findBySlug('domus');
|
|
if (c) return c;
|
|
}
|
|
if (originLower.includes('xsystem') || originLower.includes('webcontrol')) {
|
|
const c = await Company.findBySlug('xsystem');
|
|
if (c) return c;
|
|
}
|
|
}
|
|
// Default a Telcotronics (ID 1)
|
|
return (await Company.findById(1)) || { id: 1, name: 'Telcotronics', slug: 'telcotronics' };
|
|
}
|
|
|
|
/**
|
|
* Función auxiliar para resolver departamento según tipo
|
|
*/
|
|
async function resolveDepartment(companyId, tipo, departamento) {
|
|
const depts = await Department.findAll(companyId);
|
|
if (depts.length === 0) return null;
|
|
|
|
if (departamento) {
|
|
const found = depts.find(d =>
|
|
d.nombre.toLowerCase().includes(String(departamento).toLowerCase())
|
|
);
|
|
if (found) return found.id;
|
|
}
|
|
|
|
const tipoLower = String(tipo || '').toLowerCase();
|
|
let preferredName = 'Soporte Técnico';
|
|
if (tipoLower === 'cobro' || tipoLower === 'facturacion') preferredName = 'Facturación';
|
|
if (tipoLower === 'consulta' || tipoLower === 'otro') preferredName = 'General';
|
|
|
|
const match = depts.find(d => d.nombre.toLowerCase().includes(preferredName.toLowerCase()));
|
|
return match ? match.id : depts[0].id;
|
|
}
|
|
|
|
/**
|
|
* GET /api/empresas — Lista de empresas del ecosistema para canalizar tickets
|
|
*/
|
|
router.get('/empresas', async (req, res) => {
|
|
try {
|
|
const companies = await Company.findAll();
|
|
const simplified = companies.map(c => ({
|
|
id: c.id,
|
|
name: c.name,
|
|
slug: c.slug,
|
|
plan: c.plan
|
|
}));
|
|
res.json({ ok: true, success: true, empresas: simplified });
|
|
} catch (err) {
|
|
res.status(500).json({ ok: false, success: false, error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/external/tickets y POST /api/tickets
|
|
* Crea un ticket M2M con soporte para idempotencia, solicitante y categorización
|
|
*/
|
|
async function handleCreateTicket(req, res) {
|
|
try {
|
|
const {
|
|
origen = 'sigma-robot',
|
|
origen_externo_id = null,
|
|
empresa,
|
|
company_id,
|
|
app_origen = null,
|
|
solicitante = {},
|
|
asunto,
|
|
mensaje,
|
|
descripcion,
|
|
prioridad = 'media',
|
|
tipo = 'consulta',
|
|
departamento,
|
|
department_id
|
|
} = req.body;
|
|
|
|
const ticketAsunto = asunto || (mensaje ? String(mensaje).substring(0, 60) + '...' : null);
|
|
const ticketMensaje = mensaje || descripcion;
|
|
|
|
if (!ticketAsunto || !ticketMensaje) {
|
|
return res.status(400).json({
|
|
ok: false,
|
|
success: false,
|
|
error: 'Los campos asunto y mensaje son obligatorios'
|
|
});
|
|
}
|
|
|
|
// 1. Resolver Empresa
|
|
const company = await resolveCompany(empresa, app_origen, company_id);
|
|
|
|
// 2. Idempotencia: Verificar si ya existe este origen_externo_id
|
|
if (origen_externo_id) {
|
|
const existing = await Ticket.findByOrigenExterno(company.id, origen, origen_externo_id);
|
|
if (existing) {
|
|
return res.status(200).json({
|
|
ok: true,
|
|
success: true,
|
|
duplicado: true,
|
|
ticket_id: existing.id,
|
|
estado: existing.status,
|
|
empresa: company.name,
|
|
url: `https://soporte.sigmac.app/agent/ticket/${existing.id}`
|
|
});
|
|
}
|
|
}
|
|
|
|
// 3. Resolver Usuario Técnico y Solicitante
|
|
const techUser = await getTechnicalUser(company.id);
|
|
const solNombre = solicitante.nombre || req.body.cliente_nombre || null;
|
|
const solEmail = solicitante.email || req.body.cliente_email || null;
|
|
const solTelefono = solicitante.telefono || req.body.cliente_telefono || null;
|
|
|
|
// 4. Resolver Departamento
|
|
const deptId = department_id ? parseInt(department_id) : await resolveDepartment(company.id, tipo, departamento);
|
|
|
|
// 5. Prioridad normalizada
|
|
const cleanPriority = ['baja', 'media', 'alta', 'critica'].includes(String(prioridad).toLowerCase())
|
|
? String(prioridad).toLowerCase()
|
|
: 'media';
|
|
|
|
// 6. Crear Ticket
|
|
const ticketId = await Ticket.create(
|
|
company.id,
|
|
techUser.id,
|
|
deptId,
|
|
ticketAsunto.trim(),
|
|
ticketMensaje.trim(),
|
|
cleanPriority,
|
|
{
|
|
origen,
|
|
origen_externo_id: origen_externo_id ? String(origen_externo_id) : null,
|
|
app_origen,
|
|
solicitante_nombre: solNombre,
|
|
solicitante_email: solEmail,
|
|
solicitante_telefono: solTelefono,
|
|
tipo
|
|
}
|
|
);
|
|
|
|
const url = `https://soporte.sigmac.app/agent/ticket/${ticketId}`;
|
|
|
|
return res.status(201).json({
|
|
ok: true,
|
|
success: true,
|
|
ticket_id: ticketId,
|
|
estado: 'abierto',
|
|
empresa: company.name,
|
|
company_id: company.id,
|
|
asunto: ticketAsunto.trim(),
|
|
prioridad: cleanPriority,
|
|
url
|
|
});
|
|
|
|
} catch (err) {
|
|
console.error('[API Tickets] Error:', err);
|
|
return res.status(500).json({ ok: false, success: false, error: err.message });
|
|
}
|
|
}
|
|
|
|
router.post('/external/tickets', handleCreateTicket);
|
|
router.post('/tickets', handleCreateTicket);
|
|
|
|
/**
|
|
* GET /api/external/tickets/:id y GET /api/tickets/:id
|
|
* Consulta el estado y última respuesta de un ticket
|
|
*/
|
|
async function handleGetTicket(req, res) {
|
|
try {
|
|
const ticketId = parseInt(req.params.id);
|
|
const ticket = await Ticket.findById(ticketId);
|
|
if (!ticket) {
|
|
return res.status(404).json({ ok: false, success: false, error: 'Ticket no encontrado' });
|
|
}
|
|
|
|
const respuestas = await Ticket.getResponses(ticketId);
|
|
const ultimaRespuesta = respuestas.length > 0 ? respuestas[respuestas.length - 1] : null;
|
|
|
|
res.json({
|
|
ok: true,
|
|
success: true,
|
|
ticket_id: ticket.id,
|
|
estado: ticket.status,
|
|
asunto: ticket.asunto,
|
|
prioridad: ticket.prioridad,
|
|
asignado_a: ticket.asignado_nombre || null,
|
|
actualizado: ticket.updated_at || ticket.created_at,
|
|
origen: ticket.origen,
|
|
solicitante: {
|
|
nombre: ticket.solicitante_nombre || ticket.usuario_nombre,
|
|
email: ticket.solicitante_email || ticket.usuario_email
|
|
},
|
|
ultima_respuesta: ultimaRespuesta ? {
|
|
autor: ultimaRespuesta.usuario_nombre,
|
|
mensaje: ultimaRespuesta.mensaje,
|
|
fecha: ultimaRespuesta.created_at
|
|
} : null,
|
|
url: `https://soporte.sigmac.app/agent/ticket/${ticket.id}`
|
|
});
|
|
} catch (err) {
|
|
res.status(500).json({ ok: false, success: false, error: err.message });
|
|
}
|
|
}
|
|
|
|
router.get('/external/tickets/:id', handleGetTicket);
|
|
router.get('/tickets/:id', handleGetTicket);
|
|
|
|
/**
|
|
* POST /api/external/tickets/:id/notas y POST /api/tickets/:id/respuestas
|
|
* Agrega una nota o seguimiento al ticket
|
|
*/
|
|
async function handleAddNote(req, res) {
|
|
try {
|
|
const ticketId = parseInt(req.params.id);
|
|
const { texto, mensaje, autor = 'sigma-robot' } = req.body;
|
|
const contenido = texto || mensaje;
|
|
|
|
if (!contenido) {
|
|
return res.status(400).json({ ok: false, success: false, error: 'El campo texto es obligatorio' });
|
|
}
|
|
|
|
const ticket = await Ticket.findById(ticketId);
|
|
if (!ticket) {
|
|
return res.status(404).json({ ok: false, success: false, error: 'Ticket no encontrado' });
|
|
}
|
|
|
|
const techUser = await getTechnicalUser(ticket.company_id);
|
|
const notaCompleta = `[${autor}]: ${contenido}`;
|
|
const respId = await Ticket.addResponse(ticketId, techUser.id, notaCompleta);
|
|
|
|
res.json({
|
|
ok: true,
|
|
success: true,
|
|
message: 'Nota agregada exitosamente',
|
|
respuesta_id: respId,
|
|
ticket_id: ticketId
|
|
});
|
|
} catch (err) {
|
|
res.status(500).json({ ok: false, success: false, error: err.message });
|
|
}
|
|
}
|
|
|
|
router.post('/external/tickets/:id/notas', handleAddNote);
|
|
router.post('/tickets/:id/respuestas', handleAddNote);
|
|
|
|
module.exports = router;
|