/** * WhatsApp Notification Service * ERP Transportistas * * Service for sending WhatsApp notifications using transport templates. * Sprint: S5 - TASK-007 * Module: WhatsApp Integration */ import { TipoTemplateTransporte, TRANSPORT_TEMPLATES, buildMessagePayload, WhatsAppTemplate, } from '../templates/transport-templates'; /** * Notification result */ export interface NotificationResult { success: boolean; messageId?: string; error?: string; timestamp: Date; } /** * Notification request */ export interface NotificationRequest { telefono: string; tipoTemplate: TipoTemplateTransporte; parametros: Record; metadata?: Record; } /** * Batch notification result */ export interface BatchNotificationResult { total: number; exitosos: number; fallidos: number; resultados: Array<{ telefono: string; result: NotificationResult; }>; } /** * WhatsApp API configuration */ export interface WhatsAppConfig { apiUrl: string; accessToken: string; phoneNumberId: string; businessAccountId: string; } /** * WhatsApp Notification Service * * Handles sending transport-related notifications via WhatsApp Business API. * In production, this would integrate with Meta's Cloud API. */ export class WhatsAppNotificationService { private config: WhatsAppConfig | null = null; private enabled: boolean = false; constructor(config?: WhatsAppConfig) { if (config) { this.config = config; this.enabled = true; } } /** * Configure the service */ configure(config: WhatsAppConfig): void { this.config = config; this.enabled = true; } /** * Check if service is enabled */ isEnabled(): boolean { return this.enabled && this.config !== null; } /** * Send a single notification */ async enviarNotificacion(request: NotificationRequest): Promise { const template = TRANSPORT_TEMPLATES[request.tipoTemplate]; if (!template) { return { success: false, error: `Template ${request.tipoTemplate} no encontrado`, timestamp: new Date(), }; } // Validate phone number const telefonoLimpio = this.limpiarTelefono(request.telefono); if (!telefonoLimpio) { return { success: false, error: 'Número de teléfono inválido', timestamp: new Date(), }; } // If not configured, simulate success for development if (!this.isEnabled()) { return this.simularEnvio(telefonoLimpio, template, request.parametros); } // Build and send message try { const payload = buildMessagePayload(template, request.parametros); const result = await this.enviarMensajeAPI(telefonoLimpio, payload); return result; } catch (error) { return { success: false, error: (error as Error).message, timestamp: new Date(), }; } } /** * Send batch notifications */ async enviarLote(requests: NotificationRequest[]): Promise { const resultados: Array<{ telefono: string; result: NotificationResult }> = []; let exitosos = 0; let fallidos = 0; for (const request of requests) { const result = await this.enviarNotificacion(request); resultados.push({ telefono: request.telefono, result }); if (result.success) { exitosos++; } else { fallidos++; } // Rate limiting - wait between messages await this.delay(100); } return { total: requests.length, exitosos, fallidos, resultados, }; } // ========================================== // Transport-Specific Notification Methods // ========================================== /** * Notify operator of trip assignment */ async notificarViajeAsignado( telefono: string, datos: { nombreOperador: string; origen: string; destino: string; fecha: string; horaCita: string; folioViaje: string; } ): Promise { return this.enviarNotificacion({ telefono, tipoTemplate: TipoTemplateTransporte.VIAJE_ASIGNADO, parametros: { nombre_operador: datos.nombreOperador, origen: datos.origen, destino: datos.destino, fecha: datos.fecha, hora_cita: datos.horaCita, folio_viaje: datos.folioViaje, }, }); } /** * Notify client of shipment confirmation */ async notificarViajeConfirmado( telefono: string, datos: { nombreCliente: string; folio: string; unidad: string; operador: string; fecha: string; eta: string; codigoTracking: string; } ): Promise { return this.enviarNotificacion({ telefono, tipoTemplate: TipoTemplateTransporte.VIAJE_CONFIRMADO, parametros: { nombre_cliente: datos.nombreCliente, folio: datos.folio, unidad: datos.unidad, operador: datos.operador, fecha: datos.fecha, eta: datos.eta, codigo_tracking: datos.codigoTracking, }, }); } /** * Notify ETA update */ async notificarEtaActualizado( telefono: string, datos: { folio: string; nuevoEta: string; motivo: string; } ): Promise { return this.enviarNotificacion({ telefono, tipoTemplate: TipoTemplateTransporte.ETA_ACTUALIZADO, parametros: { folio: datos.folio, nuevo_eta: datos.nuevoEta, motivo: datos.motivo, }, }); } /** * Notify trip completion */ async notificarViajeCompletado( telefono: string, datos: { nombreCliente: string; folio: string; destino: string; fechaHora: string; receptor: string; } ): Promise { return this.enviarNotificacion({ telefono, tipoTemplate: TipoTemplateTransporte.VIAJE_COMPLETADO, parametros: { nombre_cliente: datos.nombreCliente, folio: datos.folio, destino: datos.destino, fecha_hora: datos.fechaHora, receptor: datos.receptor, }, }); } /** * Notify delay alert */ async notificarAlertaRetraso( telefono: string, datos: { nombre: string; folio: string; etaOriginal: string; nuevoEta: string; motivo: string; } ): Promise { return this.enviarNotificacion({ telefono, tipoTemplate: TipoTemplateTransporte.ALERTA_RETRASO, parametros: { nombre: datos.nombre, folio: datos.folio, eta_original: datos.etaOriginal, nuevo_eta: datos.nuevoEta, motivo: datos.motivo, }, }); } /** * Notify carrier of service request */ async notificarAsignacionCarrier( telefono: string, datos: { nombreCarrier: string; origen: string; destino: string; fecha: string; tarifa: string; } ): Promise { return this.enviarNotificacion({ telefono, tipoTemplate: TipoTemplateTransporte.ASIGNACION_CARRIER, parametros: { nombre_carrier: datos.nombreCarrier, origen: datos.origen, destino: datos.destino, fecha: datos.fecha, tarifa: datos.tarifa, }, }); } /** * Notify maintenance reminder */ async notificarRecordatorioMantenimiento( telefono: string, datos: { unidad: string; tipoMantenimiento: string; fechaLimite: string; kmActual: string; kmProgramado: string; } ): Promise { return this.enviarNotificacion({ telefono, tipoTemplate: TipoTemplateTransporte.RECORDATORIO_MANTENIMIENTO, parametros: { unidad: datos.unidad, tipo_mantenimiento: datos.tipoMantenimiento, fecha_limite: datos.fechaLimite, km_actual: datos.kmActual, km_programado: datos.kmProgramado, }, }); } // ========================================== // Private Helpers // ========================================== /** * Clean and validate phone number */ private limpiarTelefono(telefono: string): string | null { // Remove non-numeric characters const limpio = telefono.replace(/\D/g, ''); // Mexican numbers: 10 digits (without country code) or 12 (with 52) if (limpio.length === 10) { return `52${limpio}`; } else if (limpio.length === 12 && limpio.startsWith('52')) { return limpio; } else if (limpio.length === 13 && limpio.startsWith('521')) { // Remove old mobile prefix return `52${limpio.slice(3)}`; } return null; } /** * Simulate message sending for development */ private simularEnvio( telefono: string, template: WhatsAppTemplate, parametros: Record ): NotificationResult { console.log(`[WhatsApp Simulation] Sending ${template.nombre} to ${telefono}`); console.log(`[WhatsApp Simulation] Parameters:`, parametros); return { success: true, messageId: `sim_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, timestamp: new Date(), }; } /** * Send message via WhatsApp API (mock implementation) */ private async enviarMensajeAPI( telefono: string, payload: { template: string; language: string; components: any[] } ): Promise { // In production, this would call Meta's Cloud API // POST https://graph.facebook.com/v17.0/{phone-number-id}/messages if (!this.config) { throw new Error('WhatsApp API no configurado'); } // Mock implementation return { success: true, messageId: `wamid_${Date.now()}`, timestamp: new Date(), }; } /** * Delay helper for rate limiting */ private delay(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } }