[240, 248, 255], 'text' => [15, 23, 42], 'line' => [148, 163, 184]], // Light Blue / Dark Navy ['bg' => [255, 250, 240], 'text' => [67, 20, 7], 'line' => [252, 211, 77]], // Floral White / Dark Brown ['bg' => [240, 253, 244], 'text' => [6, 78, 59], 'line' => [110, 231, 183]], // Mint Green / Dark Green ['bg' => [254, 242, 242], 'text' => [127, 29, 29], 'line' => [252, 165, 165]], // Light Red / Dark Red ['bg' => [250, 245, 255], 'text' => [88, 28, 135], 'line' => [216, 180, 254]], // Light Purple / Dark Purple ['bg' => [255, 247, 237], 'text' => [154, 52, 18], 'line' => [253, 186, 116]], // Light Orange / Dark Orange ]; $palette = $palettes[array_rand($palettes)]; $bg = imagecolorallocate($img, $palette['bg'][0], $palette['bg'][1], $palette['bg'][2]); $textColor = imagecolorallocate($img, $palette['text'][0], $palette['text'][1], $palette['text'][2]); $lineColor = imagecolorallocate($img, $palette['line'][0], $palette['line'][1], $palette['line'][2]); imagefill($img, 0, 0, $bg); // Add noise lines to make it look like a CAPTCHA and prevent automated scraping for ($i = 0; $i < 15; $i++) { imageline($img, rand(0, 400), rand(0, 200), rand(0, 400), rand(0, 200), $lineColor); } // Add random noise dots for ($i = 0; $i < 200; $i++) { imagesetpixel($img, rand(0, 400), rand(0, 200), $lineColor); } // Get or download a beautiful modern font (Roboto Bold) $fontDir = __DIR__ . '/../../public/fonts'; if (!is_dir($fontDir)) { mkdir($fontDir, 0755, true); } $fontPath = $fontDir . '/Roboto-Bold.ttf'; if (!file_exists($fontPath)) { $options = [ 'http' => [ 'method' => 'GET', 'header' => 'User-Agent: PHP' ] ]; $context = stream_context_create($options); $fontData = @file_get_contents('https://github.com/google/fonts/raw/main/apache/roboto/Roboto-Bold.ttf', false, $context); if ($fontData) { file_put_contents($fontPath, $fontData); } } $spacedCode = implode(' ', str_split($code)); if (file_exists($fontPath) && function_exists('imagettftext')) { // Calculate exact text dimensions to center it perfectly $fontSize = 48; // Large smooth font $bbox = imagettfbbox($fontSize, 0, $fontPath, $spacedCode); $textWidth = abs($bbox[2] - $bbox[0]); $textHeight = abs($bbox[1] - $bbox[7]); // If code is too long, shrink font if ($textWidth > 360) { $fontSize = 36; $bbox = imagettfbbox($fontSize, 0, $fontPath, $spacedCode); $textWidth = abs($bbox[2] - $bbox[0]); $textHeight = abs($bbox[1] - $bbox[7]); } // Center calculations $x = (400 - $textWidth) / 2; $y = ((200 - $textHeight) / 2) + $textHeight; // Y is the baseline of the text // Draw beautifully smooth true type text imagettftext($img, $fontSize, 0, (int)$x, (int)$y, $textColor, $fontPath, $spacedCode); } else { // Fallback for missing TTF $tmp = imagecreatetruecolor(200, 40); $tmpBg = imagecolorallocate($tmp, $palette['bg'][0], $palette['bg'][1], $palette['bg'][2]); $tmpText = imagecolorallocate($tmp, $palette['text'][0], $palette['text'][1], $palette['text'][2]); imagefill($tmp, 0, 0, $tmpBg); imagestring($tmp, 5, 5, 12, $spacedCode, $tmpText); // Scale up dynamically imagecopyresampled($img, $tmp, 0, 60, 0, 0, 400, 80, 200, 40); imagedestroy($tmp); } ob_start(); imagepng($img); $imageData = ob_get_clean(); imagedestroy($img); return base64_encode($imageData); } else { // Fallback to placehold.co if PHP GD extension is missing $url = "https://placehold.co/400x200/e0f2fe/0f172a/png?text=" . $code . "&font=oswald"; $content = file_get_contents($url); return base64_encode($content); } } /** * Send OTP verification code via WhatsApp (Text, Voice Note, or Image) * POST /api/otp/send */ public function send(Request $request, Response $response): void { $companyId = $request->company_id; $body = $request->getBody(); $phone = $body['phone'] ?? ''; $type = $body['type'] ?? 'text'; // 'text', 'voice', or 'image' $sessionId = $body['session_id'] ?? null; $customCode = $body['code'] ?? null; if (empty($phone)) { $response->status(400)->json(['error' => 'Missing required parameter: phone']); return; } // Clean phone number (remove non-digits including +) $phone = preg_replace('/\D/', '', $phone); // 1. Resolve WhatsApp Session $session = null; if ($sessionId) { $session = WhatsAppSession::findSecure((int)$sessionId); if (!$session || (int)$session['company_id'] !== (int)$companyId) { $response->status(404)->json(['error' => 'WhatsApp session not found']); return; } } else { // Grab the first connected session of the company $sessions = WhatsAppSession::findAllByCompany($companyId); foreach ($sessions as $s) { if ($s['status'] === 'connected') { $session = $s; break; } } if (!$session && !empty($sessions)) { $session = $sessions[0]; // fallback to first session if none is connected } } if (!$session) { $response->status(400)->json(['error' => 'No active WhatsApp sessions configured for this company.']); return; } if ($session['status'] !== 'connected') { $response->status(400)->json(['error' => 'WhatsApp session is not connected. Connect the session first.']); return; } // 2. Check SaaS subscription quotas $useElevenLabs = false; if ($companyId !== 1) { $activeSub = CompanySubscription::findActiveByCompany($companyId); if (!$activeSub) { $response->status(402)->json(['error' => 'Active subscription plan required.']); return; } if (!CompanySubscriptionUsage::hasRemainingLimit($companyId, 'request')) { $response->status(403)->json(['error' => 'Monthly request quota exceeded. Please upgrade your plan.']); return; } if ($type === 'voice') { $features = json_decode($activeSub['features'] ?: '{}', true); $voiceFeatureEnabled = isset($features['voice']) ? (bool)$features['voice'] : false; $hasVoiceLimit = CompanySubscriptionUsage::hasRemainingLimit($companyId, 'voice'); if ($voiceFeatureEnabled && $hasVoiceLimit) { $useElevenLabs = true; } } } else { if ($type === 'voice') { $useElevenLabs = true; } } // 3. Generate verification code $code = $customCode ? trim($customCode) : (string)rand(1000, 9999); // 4. Send Message try { $usedElevenLabs = false; if ($type === 'voice') { // Spacing the digits to force slow Arabic pronunciation: e.g. "1 2 3 4" $spacedCode = implode(' ', str_split($code)); $textToRead = "رمز التحقق الخاص بك هو: {$spacedCode}. أكرر، رمز التحقق هو: {$spacedCode}."; $audioBase64 = null; $mimeType = 'audio/mp3'; if ($useElevenLabs) { $rule = \App\Models\ChatbotRule::findActiveForRule($companyId, $session['id']); $configuredElKey = ($rule && !empty($rule['elevenlabs_api_key'])) ? $rule['elevenlabs_api_key'] : null; $elApiKey = \App\Services\GeminiService::getElevenLabsApiKey($configuredElKey); $configuredVoiceId = ($rule && !empty($rule['elevenlabs_voice_id'])) ? $rule['elevenlabs_voice_id'] : null; $elVoiceId = \App\Services\GeminiService::getElevenLabsVoiceId($configuredVoiceId); if (!empty($elApiKey)) { $elVoiceId = !empty($elVoiceId) ? $elVoiceId : 'pNInz6obpgDQGcFmaJgB'; // Default to Adam $audioData = \App\Services\GeminiService::generateAudioResponseWithElevenLabs($elApiKey, $textToRead, $elVoiceId); if ($audioData) { $audioBase64 = $audioData['audio']; $mimeType = $audioData['mimeType']; $usedElevenLabs = true; } } } if (!$audioBase64) { // Fallback to Google Translate TTS $audioBase64 = TTSService::textToSpeechArabic($textToRead); $mimeType = 'audio/mp3'; $usedElevenLabs = false; } if (!$audioBase64) { $response->status(500)->json(['error' => 'Failed to generate voice OTP audio.']); return; } // Send voice note ConversationFlowEngine::sendReply($session, $phone, '', null, $audioBase64, $mimeType); } else if ($type === 'image') { // Generate OTP Image as Base64 $imageBase64 = $this->generateOtpImage($code); ConversationFlowEngine::sendReply($session, $phone, '', null, null, null, $imageBase64); } else { // Send text $textMsg = "رمز التحقق الخاص بك لمتجر نابه هو: *{$code}* \n الرجاء عدم مشاركته مع أي شخص."; ConversationFlowEngine::sendReply($session, $phone, $textMsg); } // Increment usage stats if ($companyId !== 1) { CompanySubscriptionUsage::incrementUsage($companyId, 'request'); if ($type === 'voice' && $usedElevenLabs) { CompanySubscriptionUsage::incrementUsage($companyId, 'voice'); } } $response->json([ 'status' => 'success', 'message' => 'OTP sent successfully', 'code' => $code, 'type' => $type ]); } catch (\Exception $e) { error_log("[OTP Controller Error] " . $e->getMessage()); $response->status(500)->json(['error' => 'Failed to send OTP message: ' . $e->getMessage()]); } } }