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 ' }); } // 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;