erp-transportistas-backend-v2/src/modules/whatsapp/services/whatsapp-notification.service.ts

423 lines
9.8 KiB
TypeScript

/**
* 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<string, string>;
metadata?: Record<string, any>;
}
/**
* 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<NotificationResult> {
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<BatchNotificationResult> {
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<NotificationResult> {
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<NotificationResult> {
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<NotificationResult> {
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<NotificationResult> {
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<NotificationResult> {
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<NotificationResult> {
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<NotificationResult> {
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<string, string>
): 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<NotificationResult> {
// 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<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}