renatoascencio / convochatsms
Laravel package for ConvoChat SMS and WhatsApp gateway integration
v1.1.1
2025-09-18 05:23 UTC
Requires
- php: ^8.0
- guzzlehttp/guzzle: ^7.0
- illuminate/support: ^8.0|^9.0|^10.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- mockery/mockery: ^1.4
- orchestra/testbench: ^6.23|^7.0|^8.0
- phpstan/extension-installer: ^1.3
- phpstan/phpstan: ^1.10
- phpstan/phpstan-mockery: ^1.1
- phpunit/phpunit: ^9.5|^10.0|^11.0
README
Un paquete Laravel moderno y robusto para integración completa con ConvoChat API para envío de SMS y WhatsApp con soporte para múltiples modos, configuración avanzada y monitoreo.
🚀 Instalación
composer require renatoascencio/convochatsms
Publicar configuración
php artisan vendor:publish --tag=convochat-config
⚙️ Configuración
Agrega tu API Key en el archivo .env
:
CONVOCHAT_API_KEY=tu_api_key_aqui CONVOCHAT_BASE_URL=https://sms.convo.chat/api # Configuración SMS opcional CONVOCHAT_SMS_MODE=devices CONVOCHAT_SMS_PRIORITY=2 CONVOCHAT_SMS_DEVICE=tu_device_id CONVOCHAT_SMS_SIM=1 # Configuración WhatsApp opcional CONVOCHAT_WA_ACCOUNT=tu_account_id CONVOCHAT_WA_PRIORITY=2 # Logging opcional CONVOCHAT_LOG_REQUESTS=false
Obtén tu API Key: https://sms.convo.chat/dashboard/tools/keys
📱 Uso - SMS
Enviar SMS básico
use ConvoChat\LaravelSmsGateway\Facades\ConvoChat; // Con dispositivo Android $result = ConvoChat::sms()->sendSmsWithDevice( phone: '+573001234567', message: '¡Hola desde ConvoChat!', deviceId: 'abc123' ); // Con créditos $result = ConvoChat::sms()->sendSmsWithCredits( phone: '+573001234567', message: '¡Hola desde ConvoChat!', gatewayId: 'gateway123' );
Opciones avanzadas SMS
$result = ConvoChat::sms()->sendSmsWithDevice( phone: '+573001234567', message: 'Mensaje importante', deviceId: 'abc123', options: [ 'sim' => 1, // SIM 1 o 2 'priority' => 1 // 1 = alta, 2 = normal ] );
Obtener información SMS
// Dispositivos conectados $devices = ConvoChat::sms()->getDevices(); // Créditos disponibles $credits = ConvoChat::sms()->getCredits(); // Gateways y tarifas $rates = ConvoChat::sms()->getGatewayRates(); // Información de suscripción $subscription = ConvoChat::sms()->getSubscription();
📞 Uso - WhatsApp
Enviar mensajes de texto
use ConvoChat\LaravelSmsGateway\Facades\ConvoChat; $result = ConvoChat::whatsapp()->sendText( account: 'wa_account_id', recipient: '+573001234567', message: '¡Hola por WhatsApp!' );
Enviar multimedia
// Imagen/Video/Audio $result = ConvoChat::whatsapp()->sendMedia( account: 'wa_account_id', recipient: '+573001234567', message: 'Mira esta imagen', mediaUrl: 'https://ejemplo.com/imagen.jpg', mediaType: 'image' ); // Documento $result = ConvoChat::whatsapp()->sendDocument( account: 'wa_account_id', recipient: '+573001234567', message: 'Te envío el reporte', documentUrl: 'https://ejemplo.com/reporte.pdf', documentName: 'Reporte-Mensual.pdf', documentType: 'pdf' );
Gestión de cuentas WhatsApp
// Obtener servidores disponibles $servers = ConvoChat::whatsapp()->getWhatsAppServers(); // Vincular nueva cuenta $linkData = ConvoChat::whatsapp()->linkWhatsAppAccount(serverId: 1); // Retorna QR code para escanear // Re-vincular cuenta existente $result = ConvoChat::whatsapp()->relinkWhatsAppAccount( serverId: 1, uniqueId: 'unique_account_id' ); // Validar número $validation = ConvoChat::whatsapp()->validateWhatsAppNumber( accountId: 'wa_account_id', phone: '+573001234567' ); // Obtener cuentas vinculadas $accounts = ConvoChat::whatsapp()->getWhatsAppAccounts();
Casos de uso avanzados de WhatsApp
Envío de mensajes con botones
// Mensaje con botones de respuesta rápida $result = ConvoChat::whatsapp()->sendButtonMessage( account: 'wa_account_id', recipient: '+573001234567', message: '¿Deseas confirmar tu cita para mañana?', buttons: [ ['id' => 'confirm', 'title' => 'Confirmar'], ['id' => 'reschedule', 'title' => 'Reprogramar'], ['id' => 'cancel', 'title' => 'Cancelar'] ] );
Envío masivo con plantillas
// Envío de mensajes promocionales usando plantillas aprobadas $contacts = [ '+573001234567' => ['name' => 'Juan', 'discount' => '20%'], '+573007654321' => ['name' => 'María', 'discount' => '15%'] ]; foreach ($contacts as $phone => $data) { $result = ConvoChat::whatsapp()->sendTemplate( account: 'wa_account_id', recipient: $phone, templateName: 'promocion_personalizada', templateParams: [ $data['name'], $data['discount'] ] ); }
Envío de ubicación
$result = ConvoChat::whatsapp()->sendLocation( account: 'wa_account_id', recipient: '+573001234567', latitude: 4.6097100, longitude: -74.0817500, address: 'Bogotá, Colombia', name: 'Nuestra oficina principal' );
Gestión de estado de mensajes
// Verificar estado de entrega $messageStatus = ConvoChat::whatsapp()->getMessageStatus( messageId: 'msg_123456789' ); // Marcar mensaje como leído $result = ConvoChat::whatsapp()->markAsRead( account: 'wa_account_id', messageId: 'msg_123456789' ); // Obtener historial de conversación $conversation = ConvoChat::whatsapp()->getConversationHistory( account: 'wa_account_id', recipient: '+573001234567', limit: 50 );
WhatsApp Business API Features
// Configurar perfil de negocio $result = ConvoChat::whatsapp()->updateBusinessProfile( account: 'wa_account_id', data: [ 'description' => 'Empresa líder en tecnología', 'email' => 'contacto@empresa.com', 'website' => 'https://empresa.com', 'address' => 'Calle 123 #45-67, Bogotá', 'category' => 'SOFTWARE' ] ); // Crear catálogo de productos $result = ConvoChat::whatsapp()->createProduct( account: 'wa_account_id', data: [ 'name' => 'Producto Premium', 'description' => 'Descripción detallada del producto', 'price' => 99900, 'currency' => 'COP', 'image_url' => 'https://ejemplo.com/producto.jpg', 'category' => 'ELECTRONICS' ] ); // Enviar catálogo a cliente $result = ConvoChat::whatsapp()->sendCatalog( account: 'wa_account_id', recipient: '+573001234567', message: 'Mira nuestro catálogo de productos' );
🔧 Uso sin Facade
Inyección de dependencias básica
use ConvoChat\LaravelSmsGateway\Services\ConvoChatSmsService; use ConvoChat\LaravelSmsGateway\Services\ConvoChatWhatsAppService; // SMS $smsService = app(ConvoChatSmsService::class); $result = $smsService->sendSmsWithDevice('+573001234567', 'Mensaje', 'device123'); // WhatsApp $waService = app(ConvoChatWhatsAppService::class); $result = $waService->sendText('account123', '+573001234567', 'Mensaje WA');
Inyección de dependencias avanzada
En Controladores
use ConvoChat\LaravelSmsGateway\Services\ConvoChatSmsService; use ConvoChat\LaravelSmsGateway\Services\ConvoChatWhatsAppService; class NotificationController extends Controller { public function __construct( private ConvoChatSmsService $smsService, private ConvoChatWhatsAppService $whatsappService ) {} public function sendMultiChannelNotification(Request $request) { $phone = $request->phone; $message = $request->message; // Intentar primero por WhatsApp try { $result = $this->whatsappService->sendText( config('convochat.whatsapp.default_account'), $phone, $message ); if ($result['status'] === 'success') { return response()->json(['channel' => 'whatsapp', 'result' => $result]); } } catch (Exception $e) { logger('WhatsApp failed, trying SMS: ' . $e->getMessage()); } // Fallback a SMS si WhatsApp falla try { $result = $this->smsService->sendSmsWithCredits($phone, $message); return response()->json(['channel' => 'sms', 'result' => $result]); } catch (Exception $e) { return response()->json(['error' => 'All channels failed'], 500); } } }
En Service Providers
use Illuminate\Support\ServiceProvider; use ConvoChat\LaravelSmsGateway\Services\ConvoChatSmsService; class AppServiceProvider extends ServiceProvider { public function register() { // Configuración personalizada del servicio $this->app->singleton('convochat.custom', function ($app) { return new ConvoChatSmsService( apiKey: config('convochat.api_key'), baseUrl: config('convochat.base_url'), timeout: 60, // timeout personalizado retries: 3 // reintentos personalizados ); }); } }
En Jobs/Queues
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; use ConvoChat\LaravelSmsGateway\Services\ConvoChatSmsService; class SendBulkSmsJob implements ShouldQueue { use InteractsWithQueue, SerializesModels; public function __construct( private array $recipients, private string $message ) {} public function handle(ConvoChatSmsService $smsService) { foreach ($this->recipients as $phone) { try { $result = $smsService->sendSmsWithCredits($phone, $this->message); if ($result['status'] !== 'success') { // Re-encolar mensaje fallido dispatch(new SendBulkSmsJob([$phone], $this->message)) ->delay(now()->addMinutes(5)); } } catch (Exception $e) { logger("Failed to send SMS to {$phone}: " . $e->getMessage()); $this->fail($e); } } } }
Patrón Repository
interface NotificationRepositoryInterface { public function sendSms(string $phone, string $message): array; public function sendWhatsApp(string $phone, string $message): array; } class ConvoChatNotificationRepository implements NotificationRepositoryInterface { public function __construct( private ConvoChatSmsService $smsService, private ConvoChatWhatsAppService $whatsappService ) {} public function sendSms(string $phone, string $message): array { return $this->smsService->sendSmsWithCredits($phone, $message); } public function sendWhatsApp(string $phone, string $message): array { return $this->whatsappService->sendText( config('convochat.whatsapp.default_account'), $phone, $message ); } } // En AppServiceProvider public function register() { $this->app->bind( NotificationRepositoryInterface::class, ConvoChatNotificationRepository::class ); }
📝 Notificaciones Laravel
Puedes crear notificaciones personalizadas:
use Illuminate\Notifications\Notification; use ConvoChat\LaravelSmsGateway\Facades\ConvoChat; class OrderNotification extends Notification { public function via($notifiable) { return ['convochat']; } public function toConvoChat($notifiable) { // Para SMS ConvoChat::sms()->sendSmsWithDevice( $notifiable->phone, 'Tu pedido #123 ha sido confirmado', config('convochat.sms.default_device') ); // Para WhatsApp ConvoChat::whatsapp()->sendText( config('convochat.whatsapp.default_account'), $notifiable->phone, 'Tu pedido #123 ha sido confirmado ✅' ); } }
⚡ Ejemplos de Controlador
use ConvoChat\LaravelSmsGateway\Facades\ConvoChat; class NotificationController extends Controller { public function sendWelcomeSms(Request $request) { try { $result = ConvoChat::sms()->sendSmsWithCredits( phone: $request->phone, message: "¡Bienvenido a nuestra plataforma! 🎉" ); return response()->json(['success' => true, 'data' => $result]); } catch (Exception $e) { return response()->json(['error' => $e->getMessage()], 500); } } public function sendWhatsAppPromo(Request $request) { try { $result = ConvoChat::whatsapp()->sendText( account: config('convochat.whatsapp.default_account'), recipient: $request->phone, message: "🔥 Oferta especial solo por hoy: 50% de descuento" ); return response()->json(['success' => true, 'data' => $result]); } catch (Exception $e) { return response()->json(['error' => $e->getMessage()], 500); } } }
🛠️ Manejo de Errores
Manejo básico de errores
try { $result = ConvoChat::sms()->sendSmsWithDevice('+573001234567', 'Test', 'device123'); if (isset($result['status']) && $result['status'] === 'success') { // Mensaje enviado exitosamente logger('SMS enviado: ' . json_encode($result)); } else { // Error en la respuesta de la API logger('Error API: ' . json_encode($result)); } } catch (Exception $e) { // Error de conexión o configuración logger('Error ConvoChat: ' . $e->getMessage()); }
Patrones avanzados de manejo de errores
Manejo granular de errores por tipo
use ConvoChat\LaravelSmsGateway\Exceptions\ConvoChatApiException; use ConvoChat\LaravelSmsGateway\Exceptions\ConvoChatConnectionException; use ConvoChat\LaravelSmsGateway\Exceptions\ConvoChatValidationException; try { $result = ConvoChat::sms()->sendSmsWithDevice($phone, $message, $deviceId); // Verificar respuesta exitosa if ($result['status'] === 'success') { return ['success' => true, 'data' => $result]; } // Manejar errores específicos de la API return $this->handleApiError($result); } catch (ConvoChatValidationException $e) { // Errores de validación (datos inválidos) return [ 'success' => false, 'error' => 'validation', 'message' => 'Datos inválidos: ' . $e->getMessage(), 'details' => $e->getValidationErrors() ]; } catch (ConvoChatApiException $e) { // Errores específicos de la API ConvoChat return [ 'success' => false, 'error' => 'api', 'message' => 'Error de API: ' . $e->getMessage(), 'code' => $e->getApiErrorCode() ]; } catch (ConvoChatConnectionException $e) { // Errores de conexión return [ 'success' => false, 'error' => 'connection', 'message' => 'Error de conexión: ' . $e->getMessage(), 'retry_after' => 60 ]; } catch (Exception $e) { // Otros errores no esperados logger()->error('ConvoChat unexpected error', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); return [ 'success' => false, 'error' => 'unexpected', 'message' => 'Error interno del servidor' ]; }
Sistema de reintentos automáticos
class ConvoChatRetryService { private const MAX_RETRIES = 3; private const RETRY_DELAYS = [1, 3, 5]; // segundos public function sendWithRetry(callable $sendFunction, array $params): array { $lastException = null; for ($attempt = 0; $attempt < self::MAX_RETRIES; $attempt++) { try { $result = call_user_func_array($sendFunction, $params); if ($result['status'] === 'success') { return $result; } // Si es error recuperable, intentar de nuevo if ($this->isRetryableError($result)) { $this->waitBeforeRetry($attempt); continue; } // Error no recuperable, no reintentar return $result; } catch (ConvoChatConnectionException $e) { $lastException = $e; if ($attempt < self::MAX_RETRIES - 1) { $this->waitBeforeRetry($attempt); continue; } } catch (Exception $e) { // Error no recuperable throw $e; } } throw $lastException ?? new Exception('Máximo de reintentos alcanzado'); } private function isRetryableError(array $result): bool { $retryableCodes = ['rate_limit', 'temporary_error', 'server_busy']; return in_array($result['error_code'] ?? '', $retryableCodes); } private function waitBeforeRetry(int $attempt): void { if (isset(self::RETRY_DELAYS[$attempt])) { sleep(self::RETRY_DELAYS[$attempt]); } } } // Uso del servicio de reintentos $retryService = new ConvoChatRetryService(); $result = $retryService->sendWithRetry( fn() => ConvoChat::sms()->sendSmsWithCredits($phone, $message), [] );
Circuit Breaker Pattern
class ConvoChatCircuitBreaker { private const FAILURE_THRESHOLD = 5; private const RECOVERY_TIMEOUT = 300; // 5 minutos private string $state = 'closed'; // closed, open, half-open private int $failureCount = 0; private ?int $lastFailureTime = null; public function call(callable $function, array $params) { if ($this->state === 'open') { if ($this->shouldAttemptReset()) { $this->state = 'half-open'; } else { throw new Exception('Circuit breaker is OPEN'); } } try { $result = call_user_func_array($function, $params); $this->onSuccess(); return $result; } catch (Exception $e) { $this->onFailure(); throw $e; } } private function onSuccess(): void { $this->failureCount = 0; $this->state = 'closed'; } private function onFailure(): void { $this->failureCount++; $this->lastFailureTime = time(); if ($this->failureCount >= self::FAILURE_THRESHOLD) { $this->state = 'open'; } } private function shouldAttemptReset(): bool { return $this->lastFailureTime !== null && (time() - $this->lastFailureTime) >= self::RECOVERY_TIMEOUT; } }
Logging estructurado de errores
class ConvoChatErrorLogger { public static function logError(Exception $e, array $context = []): void { $errorData = [ 'error_type' => get_class($e), 'error_message' => $e->getMessage(), 'error_code' => $e->getCode(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'timestamp' => now()->toISOString(), 'context' => $context ]; // Log específico por tipo de error if ($e instanceof ConvoChatApiException) { $errorData['api_error_code'] = $e->getApiErrorCode(); $errorData['api_response'] = $e->getApiResponse(); } if ($e instanceof ConvoChatConnectionException) { $errorData['connection_timeout'] = $e->getTimeout(); $errorData['endpoint'] = $e->getEndpoint(); } logger()->error('ConvoChat Error', $errorData); // Enviar a servicio de monitoreo si está configurado if (config('convochat.monitoring.enabled')) { self::sendToMonitoring($errorData); } } private static function sendToMonitoring(array $errorData): void { // Integración con Sentry, Bugsnag, etc. if (app()->bound('sentry')) { app('sentry')->captureException( new Exception($errorData['error_message']), $errorData ); } } } // Uso en try-catch try { $result = ConvoChat::sms()->sendSmsWithDevice($phone, $message, $deviceId); } catch (Exception $e) { ConvoChatErrorLogger::logError($e, [ 'phone' => $phone, 'device_id' => $deviceId, 'message_length' => strlen($message) ]); throw $e; }
📊 Logging
Para debug, habilita logs en .env
:
CONVOCHAT_LOG_REQUESTS=true
Los logs aparecerán en storage/logs/laravel.log
.
🎯 Casos de Uso Comunes
Sistema de OTP/Verificación
class OTPService { public function sendOTP(string $phone): string { $otp = rand(100000, 999999); ConvoChat::sms()->sendSmsWithCredits( phone: $phone, message: "Tu código de verificación es: {$otp}" ); // Guardar OTP en cache/base de datos Cache::put("otp_{$phone}", $otp, 300); // 5 min return $otp; } }
Notificaciones de Pedidos
class OrderService { public function notifyOrderStatus(Order $order, string $status) { $message = match($status) { 'confirmed' => "✅ Pedido #{$order->id} confirmado", 'shipped' => "🚚 Pedido #{$order->id} enviado", 'delivered' => "📦 Pedido #{$order->id} entregado", }; // SMS ConvoChat::sms()->sendSmsWithCredits($order->phone, $message); // WhatsApp con más detalle ConvoChat::whatsapp()->sendText( config('convochat.whatsapp.default_account'), $order->phone, $message . "\n\nRastrear: " . route('track.order', $order->id) ); } }
🔧 Configuración Avanzada
Configuración completa del archivo config/convochat.php
<?php return [ /* |-------------------------------------------------------------------------- | ConvoChat API Configuration |-------------------------------------------------------------------------- */ 'api_key' => env('CONVOCHAT_API_KEY'), 'base_url' => env('CONVOCHAT_BASE_URL', 'https://sms.convo.chat/api'), /* |-------------------------------------------------------------------------- | Connection Settings |-------------------------------------------------------------------------- */ 'timeout' => env('CONVOCHAT_TIMEOUT', 30), 'connect_timeout' => env('CONVOCHAT_CONNECT_TIMEOUT', 10), 'retries' => env('CONVOCHAT_RETRIES', 3), 'retry_delay' => env('CONVOCHAT_RETRY_DELAY', 1000), // milisegundos /* |-------------------------------------------------------------------------- | SMS Configuration |-------------------------------------------------------------------------- */ 'sms' => [ 'mode' => env('CONVOCHAT_SMS_MODE', 'devices'), // 'devices' o 'credits' 'priority' => env('CONVOCHAT_SMS_PRIORITY', 2), // 1=alta, 2=normal 'default_device' => env('CONVOCHAT_SMS_DEVICE'), 'default_sim' => env('CONVOCHAT_SMS_SIM', 1), 'fallback_enabled' => env('CONVOCHAT_SMS_FALLBACK', true), 'fallback_mode' => env('CONVOCHAT_SMS_FALLBACK_MODE', 'credits'), ], /* |-------------------------------------------------------------------------- | WhatsApp Configuration |-------------------------------------------------------------------------- */ 'whatsapp' => [ 'default_account' => env('CONVOCHAT_WA_ACCOUNT'), 'priority' => env('CONVOCHAT_WA_PRIORITY', 2), 'enable_receipts' => env('CONVOCHAT_WA_RECEIPTS', true), 'enable_typing' => env('CONVOCHAT_WA_TYPING', true), 'max_media_size' => env('CONVOCHAT_WA_MAX_MEDIA_SIZE', 16777216), // 16MB ], /* |-------------------------------------------------------------------------- | Logging Configuration |-------------------------------------------------------------------------- */ 'logging' => [ 'enabled' => env('CONVOCHAT_LOG_REQUESTS', false), 'level' => env('CONVOCHAT_LOG_LEVEL', 'info'), 'channel' => env('CONVOCHAT_LOG_CHANNEL', 'single'), 'log_responses' => env('CONVOCHAT_LOG_RESPONSES', false), 'log_sensitive_data' => env('CONVOCHAT_LOG_SENSITIVE', false), ], /* |-------------------------------------------------------------------------- | Rate Limiting |-------------------------------------------------------------------------- */ 'rate_limiting' => [ 'enabled' => env('CONVOCHAT_RATE_LIMITING', true), 'max_requests_per_minute' => env('CONVOCHAT_MAX_REQUESTS_PER_MINUTE', 60), 'burst_limit' => env('CONVOCHAT_BURST_LIMIT', 10), ], /* |-------------------------------------------------------------------------- | Circuit Breaker Configuration |-------------------------------------------------------------------------- */ 'circuit_breaker' => [ 'enabled' => env('CONVOCHAT_CIRCUIT_BREAKER', false), 'failure_threshold' => env('CONVOCHAT_FAILURE_THRESHOLD', 5), 'recovery_timeout' => env('CONVOCHAT_RECOVERY_TIMEOUT', 300), 'success_threshold' => env('CONVOCHAT_SUCCESS_THRESHOLD', 3), ], /* |-------------------------------------------------------------------------- | Monitoring and Health Checks |-------------------------------------------------------------------------- */ 'monitoring' => [ 'enabled' => env('CONVOCHAT_MONITORING', false), 'health_check_interval' => env('CONVOCHAT_HEALTH_CHECK_INTERVAL', 300), 'metrics_enabled' => env('CONVOCHAT_METRICS', false), 'alerts_enabled' => env('CONVOCHAT_ALERTS', false), ], /* |-------------------------------------------------------------------------- | Queue Configuration |-------------------------------------------------------------------------- */ 'queue' => [ 'enabled' => env('CONVOCHAT_QUEUE_ENABLED', false), 'connection' => env('CONVOCHAT_QUEUE_CONNECTION', 'default'), 'queue_name' => env('CONVOCHAT_QUEUE_NAME', 'convochat'), 'max_attempts' => env('CONVOCHAT_QUEUE_MAX_ATTEMPTS', 3), 'backoff' => env('CONVOCHAT_QUEUE_BACKOFF', 60), ], /* |-------------------------------------------------------------------------- | Environment-specific Configuration |-------------------------------------------------------------------------- */ 'environments' => [ 'testing' => [ 'mock_responses' => env('CONVOCHAT_MOCK_RESPONSES', true), 'log_all_requests' => true, ], 'staging' => [ 'rate_limiting' => ['max_requests_per_minute' => 30], 'logging' => ['level' => 'debug'], ], 'production' => [ 'logging' => ['log_sensitive_data' => false], 'monitoring' => ['enabled' => true], ], ], ];
Variables de entorno recomendadas
# API Configuration CONVOCHAT_API_KEY=your_api_key_here CONVOCHAT_BASE_URL=https://sms.convo.chat/api # Connection Settings CONVOCHAT_TIMEOUT=30 CONVOCHAT_CONNECT_TIMEOUT=10 CONVOCHAT_RETRIES=3 # SMS Settings CONVOCHAT_SMS_MODE=devices CONVOCHAT_SMS_PRIORITY=2 CONVOCHAT_SMS_DEVICE=your_device_id CONVOCHAT_SMS_SIM=1 CONVOCHAT_SMS_FALLBACK=true # WhatsApp Settings CONVOCHAT_WA_ACCOUNT=your_whatsapp_account_id CONVOCHAT_WA_PRIORITY=2 CONVOCHAT_WA_RECEIPTS=true # Logging CONVOCHAT_LOG_REQUESTS=false CONVOCHAT_LOG_LEVEL=info CONVOCHAT_LOG_RESPONSES=false # Performance CONVOCHAT_RATE_LIMITING=true CONVOCHAT_MAX_REQUESTS_PER_MINUTE=60 CONVOCHAT_CIRCUIT_BREAKER=true # Monitoring (Producción) CONVOCHAT_MONITORING=true CONVOCHAT_METRICS=true CONVOCHAT_ALERTS=true # Queue (Para envíos masivos) CONVOCHAT_QUEUE_ENABLED=true CONVOCHAT_QUEUE_CONNECTION=redis CONVOCHAT_QUEUE_NAME=convochat
Configuración por entorno
// En config/convochat.php - configuración dinámica por entorno $config = [ // ... configuración base ]; // Aplicar configuraciones específicas por entorno $environment = app()->environment(); if (isset($config['environments'][$environment])) { $config = array_merge_recursive($config, $config['environments'][$environment]); } return $config;
🧪 Testing y Debugging
Tests unitarios con PHPUnit
<?php use Tests\TestCase; use ConvoChat\LaravelSmsGateway\Facades\ConvoChat; use ConvoChat\LaravelSmsGateway\Services\ConvoChatSmsService; use Illuminate\Support\Facades\Http; class ConvoChatSmsTest extends TestCase { public function setUp(): void { parent::setUp(); // Configurar entorno de prueba config([ 'convochat.api_key' => 'test_key', 'convochat.base_url' => 'https://test.convo.chat/api', 'convochat.sms.default_device' => 'test_device' ]); } /** @test */ public function it_can_send_sms_with_device() { // Mock de respuesta exitosa Http::fake([ 'test.convo.chat/*' => Http::response([ 'status' => 'success', 'message_id' => 'msg_123456', 'credits_used' => 1 ], 200) ]); $result = ConvoChat::sms()->sendSmsWithDevice( '+573001234567', 'Test message', 'test_device' ); $this->assertEquals('success', $result['status']); $this->assertArrayHasKey('message_id', $result); } /** @test */ public function it_handles_api_errors_gracefully() { Http::fake([ 'test.convo.chat/*' => Http::response([ 'status' => 'error', 'error_code' => 'INVALID_DEVICE', 'message' => 'Device not found' ], 400) ]); $result = ConvoChat::sms()->sendSmsWithDevice( '+573001234567', 'Test message', 'invalid_device' ); $this->assertEquals('error', $result['status']); $this->assertEquals('INVALID_DEVICE', $result['error_code']); } /** @test */ public function it_retries_on_connection_failure() { Http::fake([ 'test.convo.chat/*' => Http::sequence() ->pushStatus(500) // Primera llamada falla ->pushStatus(500) // Segunda llamada falla ->push([ // Tercera llamada exitosa 'status' => 'success', 'message_id' => 'msg_retry_123' ]) ]); $retryService = new ConvoChatRetryService(); $result = $retryService->sendWithRetry( fn() => ConvoChat::sms()->sendSmsWithDevice('+573001234567', 'Test', 'device'), [] ); $this->assertEquals('success', $result['status']); } }
Test de integración con WhatsApp
class ConvoChatWhatsAppTest extends TestCase { /** @test */ public function it_can_send_whatsapp_message() { Http::fake([ 'test.convo.chat/*' => Http::response([ 'status' => 'success', 'message_id' => 'wa_msg_123', 'account_id' => 'wa_account_test' ], 200) ]); $result = ConvoChat::whatsapp()->sendText( 'wa_account_test', '+573001234567', 'Hello WhatsApp!' ); $this->assertEquals('success', $result['status']); $this->assertStringStartsWith('wa_msg_', $result['message_id']); } /** @test */ public function it_validates_media_size_for_whatsapp() { $this->expectException(InvalidArgumentException::class); ConvoChat::whatsapp()->sendMedia( 'wa_account_test', '+573001234567', 'Large file', 'https://example.com/large_file.mp4', // > 16MB 'video' ); } }
Debugging avanzado
// Habilitar debugging completo en .env CONVOCHAT_LOG_REQUESTS=true CONVOCHAT_LOG_RESPONSES=true CONVOCHAT_LOG_LEVEL=debug CONVOCHAT_LOG_SENSITIVE=true // ¡Solo en desarrollo! // Debug helper personalizado class ConvoChatDebugger { public static function enableVerboseLogging(): void { config([ 'convochat.logging.enabled' => true, 'convochat.logging.level' => 'debug', 'convochat.logging.log_responses' => true, ]); } public static function dumpLastRequest(): array { return cache()->get('convochat.last_request', []); } public static function dumpLastResponse(): array { return cache()->get('convochat.last_response', []); } public static function simulateApiError(string $errorCode = 'TEST_ERROR'): void { Http::fake([ '*convo.chat/*' => Http::response([ 'status' => 'error', 'error_code' => $errorCode, 'message' => 'Simulated error for testing' ], 400) ]); } } // Uso en desarrollo ConvoChatDebugger::enableVerboseLogging(); $result = ConvoChat::sms()->sendSmsWithDevice($phone, $message, $device); dd(ConvoChatDebugger::dumpLastRequest(), ConvoChatDebugger::dumpLastResponse());
Testing con Factory Pattern
// Factory para crear datos de prueba class ConvoChatTestFactory { public static function smsResponse(array $overrides = []): array { return array_merge([ 'status' => 'success', 'message_id' => 'sms_' . uniqid(), 'credits_used' => 1, 'device_id' => 'test_device', 'timestamp' => now()->toISOString() ], $overrides); } public static function whatsappResponse(array $overrides = []): array { return array_merge([ 'status' => 'success', 'message_id' => 'wa_' . uniqid(), 'account_id' => 'wa_test_account', 'timestamp' => now()->toISOString() ], $overrides); } public static function errorResponse(string $errorCode = 'GENERIC_ERROR'): array { return [ 'status' => 'error', 'error_code' => $errorCode, 'message' => 'Test error response', 'timestamp' => now()->toISOString() ]; } } // Uso en tests Http::fake([ '*' => Http::response(ConvoChatTestFactory::smsResponse()) ]);
🔍 Troubleshooting
Problemas comunes y soluciones
1. Error de autenticación
Problema: 401 Unauthorized
# Verificar API key php artisan tinker >>> config('convochat.api_key') >>> env('CONVOCHAT_API_KEY')
Soluciones:
- Verificar que la API key esté correctamente configurada en
.env
- Confirmar que la API key sea válida en el dashboard
- Limpiar cache de configuración:
php artisan config:clear
2. Timeout de conexión
Problema: Connection timeout after 30 seconds
Soluciones:
// Aumentar timeout en config/convochat.php 'timeout' => 60, 'connect_timeout' => 20, // O en .env CONVOCHAT_TIMEOUT=60 CONVOCHAT_CONNECT_TIMEOUT=20
3. Device no encontrado
Problema: Device not found or offline
Verificación:
// Listar dispositivos disponibles $devices = ConvoChat::sms()->getDevices(); foreach ($devices as $device) { echo "Device: {$device['id']} - Status: {$device['status']}\n"; }
Soluciones:
- Verificar que el dispositivo esté conectado
- Usar fallback a créditos si está configurado
- Verificar permisos del dispositivo
4. Problemas con WhatsApp
Problema: WhatsApp account not linked
Diagnóstico:
// Verificar cuentas vinculadas $accounts = ConvoChat::whatsapp()->getWhatsAppAccounts(); var_dump($accounts); // Verificar estado de la cuenta $status = ConvoChat::whatsapp()->getAccountStatus('your_account_id'); var_dump($status);
5. Rate limiting
Problema: Rate limit exceeded
Solución:
// Implementar rate limiting local class ConvoChatRateLimiter { public function attempt(callable $function, int $maxAttempts = 60): mixed { $key = 'convochat_rate_limit_' . auth()->id(); if (Cache::get($key, 0) >= $maxAttempts) { throw new Exception('Rate limit exceeded'); } Cache::increment($key, 1); Cache::expire($key, 60); // 1 minuto return $function(); } }
Herramientas de diagnóstico
// Comando Artisan para diagnóstico php artisan make:command ConvoChatDiagnose class ConvoChatDiagnose extends Command { protected $signature = 'convochat:diagnose'; protected $description = 'Diagnose ConvoChat configuration and connectivity'; public function handle() { $this->info('ConvoChat Diagnostic Tool'); $this->line('================================'); // Test 1: Configuration $this->checkConfiguration(); // Test 2: Connectivity $this->checkConnectivity(); // Test 3: API key validity $this->checkApiKey(); // Test 4: Device status $this->checkDevices(); // Test 5: WhatsApp accounts $this->checkWhatsAppAccounts(); } private function checkConfiguration(): void { $this->info('Checking configuration...'); $apiKey = config('convochat.api_key'); $baseUrl = config('convochat.base_url'); $this->line("API Key: " . ($apiKey ? 'Set' : 'Missing')); $this->line("Base URL: " . $baseUrl); if (!$apiKey) { $this->error('❌ API Key not configured'); return; } $this->info('✅ Configuration OK'); } private function checkConnectivity(): void { $this->info('Checking connectivity...'); try { $response = Http::timeout(10)->get(config('convochat.base_url') . '/health'); if ($response->successful()) { $this->info('✅ Connectivity OK'); } else { $this->error('❌ Connectivity failed: ' . $response->status()); } } catch (Exception $e) { $this->error('❌ Connectivity error: ' . $e->getMessage()); } } private function checkApiKey(): void { $this->info('Checking API key validity...'); try { $credits = ConvoChat::sms()->getCredits(); $this->info('✅ API Key valid - Credits: ' . $credits['balance']); } catch (Exception $e) { $this->error('❌ API Key invalid: ' . $e->getMessage()); } } private function checkDevices(): void { $this->info('Checking SMS devices...'); try { $devices = ConvoChat::sms()->getDevices(); $this->info('✅ Found ' . count($devices) . ' devices'); foreach ($devices as $device) { $status = $device['status'] === 'online' ? '✅' : '❌'; $this->line(" {$status} {$device['name']} ({$device['id']})"); } } catch (Exception $e) { $this->error('❌ Device check failed: ' . $e->getMessage()); } } private function checkWhatsAppAccounts(): void { $this->info('Checking WhatsApp accounts...'); try { $accounts = ConvoChat::whatsapp()->getWhatsAppAccounts(); $this->info('✅ Found ' . count($accounts) . ' WhatsApp accounts'); foreach ($accounts as $account) { $status = $account['status'] === 'connected' ? '✅' : '❌'; $this->line(" {$status} {$account['number']} ({$account['id']})"); } } catch (Exception $e) { $this->error('❌ WhatsApp check failed: ' . $e->getMessage()); } } }
🚀 Performance Optimization
Optimizaciones para envíos masivos
1. Queue-based bulk sending
// Job para envío masivo optimizado class BulkSmsJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public $tries = 3; public $backoff = [30, 60, 120]; // Progressive backoff public function __construct( private array $recipients, private string $message, private array $options = [] ) {} public function handle(ConvoChatSmsService $smsService): void { $batchSize = config('convochat.queue.batch_size', 10); $chunks = array_chunk($this->recipients, $batchSize); foreach ($chunks as $chunk) { $this->processBatch($smsService, $chunk); // Rate limiting entre batches usleep(config('convochat.queue.batch_delay', 100000)); // 100ms } } private function processBatch(ConvoChatSmsService $smsService, array $recipients): void { $promises = []; foreach ($recipients as $phone) { $promises[] = $this->sendAsync($smsService, $phone); } // Procesar todas las promesas en paralelo Promise::settle($promises)->wait(); } private function sendAsync(ConvoChatSmsService $smsService, string $phone): PromiseInterface { return $smsService->sendSmsWithCreditsAsync($phone, $this->message); } } // Dispatcher optimizado class BulkSmsDispatcher { public function sendBulkSms(array $recipients, string $message): void { $maxChunkSize = config('convochat.performance.max_chunk_size', 1000); $chunks = array_chunk($recipients, $maxChunkSize); foreach ($chunks as $index => $chunk) { dispatch(new BulkSmsJob($chunk, $message)) ->onQueue('convochat-bulk') ->delay(now()->addSeconds($index * 5)); // Spread load } } }
2. Connection pooling y reuso
class ConvoChatConnectionPool { private static array $pool = []; private const MAX_CONNECTIONS = 10; public static function getConnection(): GuzzleHttp\Client { if (count(self::$pool) < self::MAX_CONNECTIONS) { self::$pool[] = new GuzzleHttp\Client([ 'base_uri' => config('convochat.base_url'), 'timeout' => config('convochat.timeout'), 'headers' => [ 'Authorization' => 'Bearer ' . config('convochat.api_key'), 'Accept' => 'application/json', ], 'http_errors' => false, ]); } return self::$pool[array_rand(self::$pool)]; } public static function releaseConnection(GuzzleHttp\Client $client): void { // Implementar lógica de release si es necesario } }
3. Caché inteligente
class ConvoChatCache { private const CACHE_TTL = [ 'devices' => 300, // 5 minutos 'credits' => 60, // 1 minuto 'accounts' => 600, // 10 minutos 'rates' => 3600, // 1 hora ]; public static function remember(string $type, string $key, callable $callback): mixed { $cacheKey = "convochat.{$type}.{$key}"; $ttl = self::CACHE_TTL[$type] ?? 300; return Cache::remember($cacheKey, $ttl, $callback); } public static function invalidate(string $type, string $key = '*'): void { if ($key === '*') { Cache::flush(); // Solo en desarrollo } else { Cache::forget("convochat.{$type}.{$key}"); } } // Caché con warming estratégico public static function warmUp(): void { // Pre-cargar datos frecuentemente accedidos self::remember('devices', 'all', fn() => ConvoChat::sms()->getDevices()); self::remember('credits', 'balance', fn() => ConvoChat::sms()->getCredits()); self::remember('accounts', 'whatsapp', fn() => ConvoChat::whatsapp()->getWhatsAppAccounts()); } } // Comando para warming php artisan make:command ConvoChatWarmCache class ConvoChatWarmCache extends Command { protected $signature = 'convochat:warm-cache'; public function handle() { ConvoChatCache::warmUp(); $this->info('ConvoChat cache warmed successfully'); } }
4. Monitoring de performance
class ConvoChatPerformanceMonitor { public function measureRequest(callable $operation, array $context = []): array { $startTime = microtime(true); $startMemory = memory_get_usage(true); try { $result = $operation(); $success = true; } catch (Exception $e) { $result = null; $success = false; $error = $e->getMessage(); } $metrics = [ 'duration' => (microtime(true) - $startTime) * 1000, // ms 'memory' => memory_get_usage(true) - $startMemory, 'success' => $success, 'timestamp' => now()->toISOString(), 'context' => $context ]; if (isset($error)) { $metrics['error'] = $error; } $this->recordMetrics($metrics); return $result; } private function recordMetrics(array $metrics): void { // Log métricas logger()->info('ConvoChat Performance', $metrics); // Enviar a sistema de métricas (InfluxDB, CloudWatch, etc.) if (config('convochat.monitoring.metrics_enabled')) { $this->sendToMetricsBackend($metrics); } // Alertas por performance degradada if ($metrics['duration'] > config('convochat.performance.alert_threshold', 5000)) { $this->sendPerformanceAlert($metrics); } } }
🔒 Security Best Practices
1. Protección de API Keys
// Service Provider para cifrado de claves sensibles class ConvoChatSecurityProvider extends ServiceProvider { public function register(): void { $this->app->singleton('convochat.secure_config', function () { return new SecureConfigManager(); }); } } class SecureConfigManager { public function getSecureApiKey(): string { $encryptedKey = config('convochat.encrypted_api_key'); if ($encryptedKey) { return decrypt($encryptedKey); } return config('convochat.api_key'); } public function setSecureApiKey(string $apiKey): void { $encrypted = encrypt($apiKey); // Guardar de forma segura Cache::put('convochat.secure_api_key', $encrypted, now()->addHours(24)); } }
2. Validación y sanitización de datos
class ConvoChatValidator { public static function validatePhone(string $phone): string { // Eliminar caracteres no numéricos excepto + $phone = preg_replace('/[^\d+]/', '', $phone); // Validar formato internacional if (!preg_match('/^\+[1-9]\d{1,14}$/', $phone)) { throw new InvalidArgumentException('Invalid phone number format'); } return $phone; } public static function sanitizeMessage(string $message): string { // Eliminar caracteres peligrosos $message = strip_tags($message); $message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8'); // Limitar longitud if (strlen($message) > 1600) { throw new InvalidArgumentException('Message too long'); } return $message; } public static function validateMediaUrl(string $url): string { // Validar URL if (!filter_var($url, FILTER_VALIDATE_URL)) { throw new InvalidArgumentException('Invalid media URL'); } // Verificar esquema permitido $allowedSchemes = ['https']; $scheme = parse_url($url, PHP_URL_SCHEME); if (!in_array($scheme, $allowedSchemes)) { throw new InvalidArgumentException('Only HTTPS URLs are allowed'); } return $url; } } // Middleware para validación automática class ConvoChatValidationMiddleware { public function handle($request, Closure $next) { if ($request->has('phone')) { $request->merge([ 'phone' => ConvoChatValidator::validatePhone($request->phone) ]); } if ($request->has('message')) { $request->merge([ 'message' => ConvoChatValidator::sanitizeMessage($request->message) ]); } return $next($request); } }
3. Rate limiting por usuario
class ConvoChatUserRateLimiter { public function attempt(int $userId, string $action, int $maxAttempts = 60): bool { $key = "convochat_rate_limit_{$action}_{$userId}"; $attempts = Cache::get($key, 0); if ($attempts >= $maxAttempts) { return false; } Cache::put($key, $attempts + 1, now()->addMinutes(1)); return true; } public function getRemainingAttempts(int $userId, string $action, int $maxAttempts = 60): int { $key = "convochat_rate_limit_{$action}_{$userId}"; $attempts = Cache::get($key, 0); return max(0, $maxAttempts - $attempts); } } // Uso en controlador class SecureNotificationController extends Controller { public function sendSms(Request $request, ConvoChatUserRateLimiter $rateLimiter) { $userId = auth()->id(); if (!$rateLimiter->attempt($userId, 'sms', 10)) { return response()->json([ 'error' => 'Rate limit exceeded', 'retry_after' => 60 ], 429); } // Proceder con el envío... } }
4. Audit logging
class ConvoChatAuditLogger { public static function logOperation(string $operation, array $data = []): void { $auditData = [ 'user_id' => auth()->id(), 'operation' => $operation, 'ip_address' => request()->ip(), 'user_agent' => request()->userAgent(), 'timestamp' => now()->toISOString(), 'data' => $data ]; // Remover datos sensibles del log unset($auditData['data']['api_key']); unset($auditData['data']['password']); logger()->info('ConvoChat Audit', $auditData); // Enviar a sistema de auditoría externa if (config('convochat.security.external_audit')) { self::sendToAuditSystem($auditData); } } private static function sendToAuditSystem(array $data): void { // Integración con sistemas de auditoría Http::post(config('convochat.security.audit_endpoint'), $data); } } // Uso automático en servicios class SecureConvoChatSmsService extends ConvoChatSmsService { public function sendSmsWithDevice(string $phone, string $message, string $deviceId): array { ConvoChatAuditLogger::logOperation('sms_send', [ 'phone' => substr($phone, 0, -4) . '****', // Partially mask phone 'device_id' => $deviceId, 'message_length' => strlen($message) ]); return parent::sendSmsWithDevice($phone, $message, $deviceId); } }
5. Cifrado de mensajes sensibles
class ConvoChatEncryption { public static function encryptMessage(string $message, ?string $key = null): string { $key = $key ?: config('app.key'); return encrypt($message); } public static function decryptMessage(string $encryptedMessage, ?string $key = null): string { return decrypt($encryptedMessage); } public static function hashPhone(string $phone): string { return hash('sha256', $phone . config('app.key')); } } // Service para mensajes cifrados class EncryptedMessageService { public function sendEncryptedSms(string $phone, string $message, string $deviceId): array { // Cifrar mensaje antes del envío $encryptedMessage = ConvoChatEncryption::encryptMessage($message); // Guardar relación en BD para recuperar después EncryptedMessage::create([ 'phone_hash' => ConvoChatEncryption::hashPhone($phone), 'encrypted_content' => $encryptedMessage, 'sent_at' => now() ]); // Enviar mensaje sin contenido sensible return ConvoChat::sms()->sendSmsWithDevice( $phone, 'Mensaje cifrado enviado. Consulte su portal seguro.', $deviceId ); } }
📋 Requisitos
- PHP 8.0+
- Laravel 8.0+
- Guzzle HTTP 7.0+
- API Key válida de ConvoChat
📞 Soporte
- Documentación API: https://docs.convo.chat
- Dashboard: https://sms.convo.chat
- API Keys: https://sms.convo.chat/dashboard/tools/keys
📄 Licencia
MIT License