/** * WhatsApp Controller * ERP Transportistas * * REST API endpoints for WhatsApp notifications. * Sprint: S5 - TASK-007 * Module: WhatsApp Integration */ import { Request, Response, Router } from 'express'; import { DataSource } from 'typeorm'; import { WhatsAppNotificationService, NotificationRequest, } from '../services/whatsapp-notification.service'; import { TipoTemplateTransporte } from '../templates/transport-templates'; /** * Create WhatsApp controller with DataSource injection */ export function createWhatsAppController(dataSource: DataSource): Router { const router = Router(); const notificationService = new WhatsAppNotificationService(); /** * POST /configurar * Configure WhatsApp API credentials */ router.post('/configurar', async (req: Request, res: Response) => { try { const { apiUrl, accessToken, phoneNumberId, businessAccountId } = req.body; if (!apiUrl || !accessToken || !phoneNumberId || !businessAccountId) { return res.status(400).json({ success: false, error: 'Faltan campos requeridos: apiUrl, accessToken, phoneNumberId, businessAccountId', }); } notificationService.configure({ apiUrl, accessToken, phoneNumberId, businessAccountId, }); res.json({ success: true, data: { message: 'WhatsApp API configurado correctamente', enabled: notificationService.isEnabled(), }, }); } catch (error) { res.status(500).json({ success: false, error: (error as Error).message, }); } }); /** * GET /estado * Get service status */ router.get('/estado', async (_req: Request, res: Response) => { try { res.json({ success: true, data: { enabled: notificationService.isEnabled(), templatesDisponibles: Object.values(TipoTemplateTransporte), }, }); } catch (error) { res.status(500).json({ success: false, error: (error as Error).message, }); } }); /** * POST /enviar * Send a single notification */ router.post('/enviar', async (req: Request, res: Response) => { try { const { telefono, tipoTemplate, parametros, metadata } = req.body; if (!telefono || !tipoTemplate || !parametros) { return res.status(400).json({ success: false, error: 'Faltan campos requeridos: telefono, tipoTemplate, parametros', }); } // Validate template type if (!Object.values(TipoTemplateTransporte).includes(tipoTemplate)) { return res.status(400).json({ success: false, error: `Template inválido. Opciones: ${Object.values(TipoTemplateTransporte).join(', ')}`, }); } const request: NotificationRequest = { telefono, tipoTemplate, parametros, metadata, }; const result = await notificationService.enviarNotificacion(request); res.json({ success: result.success, data: result, }); } catch (error) { res.status(500).json({ success: false, error: (error as Error).message, }); } }); /** * POST /enviar-lote * Send batch notifications */ router.post('/enviar-lote', async (req: Request, res: Response) => { try { const { notificaciones } = req.body; if (!notificaciones || !Array.isArray(notificaciones) || notificaciones.length === 0) { return res.status(400).json({ success: false, error: 'Se requiere un array de notificaciones', }); } // Validate each notification for (const notif of notificaciones) { if (!notif.telefono || !notif.tipoTemplate || !notif.parametros) { return res.status(400).json({ success: false, error: 'Cada notificación requiere: telefono, tipoTemplate, parametros', }); } if (!Object.values(TipoTemplateTransporte).includes(notif.tipoTemplate)) { return res.status(400).json({ success: false, error: `Template inválido: ${notif.tipoTemplate}`, }); } } const result = await notificationService.enviarLote(notificaciones); res.json({ success: true, data: result, }); } catch (error) { res.status(500).json({ success: false, error: (error as Error).message, }); } }); // ========================================== // Transport-Specific Endpoints // ========================================== /** * POST /viaje-asignado * Notify operator of trip assignment */ router.post('/viaje-asignado', async (req: Request, res: Response) => { try { const { telefono, nombreOperador, origen, destino, fecha, horaCita, folioViaje } = req.body; if (!telefono || !nombreOperador || !origen || !destino || !fecha || !horaCita || !folioViaje) { return res.status(400).json({ success: false, error: 'Faltan campos requeridos', }); } const result = await notificationService.notificarViajeAsignado(telefono, { nombreOperador, origen, destino, fecha, horaCita, folioViaje, }); res.json({ success: result.success, data: result, }); } catch (error) { res.status(500).json({ success: false, error: (error as Error).message, }); } }); /** * POST /viaje-confirmado * Notify client of shipment confirmation */ router.post('/viaje-confirmado', async (req: Request, res: Response) => { try { const { telefono, nombreCliente, folio, unidad, operador, fecha, eta, codigoTracking } = req.body; if (!telefono || !nombreCliente || !folio || !unidad || !operador || !fecha || !eta || !codigoTracking) { return res.status(400).json({ success: false, error: 'Faltan campos requeridos', }); } const result = await notificationService.notificarViajeConfirmado(telefono, { nombreCliente, folio, unidad, operador, fecha, eta, codigoTracking, }); res.json({ success: result.success, data: result, }); } catch (error) { res.status(500).json({ success: false, error: (error as Error).message, }); } }); /** * POST /eta-actualizado * Notify ETA update */ router.post('/eta-actualizado', async (req: Request, res: Response) => { try { const { telefono, folio, nuevoEta, motivo } = req.body; if (!telefono || !folio || !nuevoEta || !motivo) { return res.status(400).json({ success: false, error: 'Faltan campos requeridos: telefono, folio, nuevoEta, motivo', }); } const result = await notificationService.notificarEtaActualizado(telefono, { folio, nuevoEta, motivo, }); res.json({ success: result.success, data: result, }); } catch (error) { res.status(500).json({ success: false, error: (error as Error).message, }); } }); /** * POST /viaje-completado * Notify trip completion */ router.post('/viaje-completado', async (req: Request, res: Response) => { try { const { telefono, nombreCliente, folio, destino, fechaHora, receptor } = req.body; if (!telefono || !nombreCliente || !folio || !destino || !fechaHora || !receptor) { return res.status(400).json({ success: false, error: 'Faltan campos requeridos', }); } const result = await notificationService.notificarViajeCompletado(telefono, { nombreCliente, folio, destino, fechaHora, receptor, }); res.json({ success: result.success, data: result, }); } catch (error) { res.status(500).json({ success: false, error: (error as Error).message, }); } }); /** * POST /alerta-retraso * Notify delay alert */ router.post('/alerta-retraso', async (req: Request, res: Response) => { try { const { telefono, nombre, folio, etaOriginal, nuevoEta, motivo } = req.body; if (!telefono || !nombre || !folio || !etaOriginal || !nuevoEta || !motivo) { return res.status(400).json({ success: false, error: 'Faltan campos requeridos', }); } const result = await notificationService.notificarAlertaRetraso(telefono, { nombre, folio, etaOriginal, nuevoEta, motivo, }); res.json({ success: result.success, data: result, }); } catch (error) { res.status(500).json({ success: false, error: (error as Error).message, }); } }); /** * POST /asignacion-carrier * Notify carrier of service request */ router.post('/asignacion-carrier', async (req: Request, res: Response) => { try { const { telefono, nombreCarrier, origen, destino, fecha, tarifa } = req.body; if (!telefono || !nombreCarrier || !origen || !destino || !fecha || !tarifa) { return res.status(400).json({ success: false, error: 'Faltan campos requeridos', }); } const result = await notificationService.notificarAsignacionCarrier(telefono, { nombreCarrier, origen, destino, fecha, tarifa, }); res.json({ success: result.success, data: result, }); } catch (error) { res.status(500).json({ success: false, error: (error as Error).message, }); } }); /** * POST /recordatorio-mantenimiento * Notify maintenance reminder */ router.post('/recordatorio-mantenimiento', async (req: Request, res: Response) => { try { const { telefono, unidad, tipoMantenimiento, fechaLimite, kmActual, kmProgramado } = req.body; if (!telefono || !unidad || !tipoMantenimiento || !fechaLimite || !kmActual || !kmProgramado) { return res.status(400).json({ success: false, error: 'Faltan campos requeridos', }); } const result = await notificationService.notificarRecordatorioMantenimiento(telefono, { unidad, tipoMantenimiento, fechaLimite, kmActual, kmProgramado, }); res.json({ success: result.success, data: result, }); } catch (error) { res.status(500).json({ success: false, error: (error as Error).message, }); } }); return router; }