423 lines
9.8 KiB
TypeScript
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));
|
|
}
|
|
}
|