[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); } // Create a temporary small image for the text $tmp = imagecreatetruecolor(100, 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); $spacedCode = implode(' ', str_split($code)); // Slightly wider spacing // Draw text multiple times with 1px offsets to create a bold effect imagestring($tmp, 5, 8, 12, $spacedCode, $tmpText); imagestring($tmp, 5, 9, 12, $spacedCode, $tmpText); // Bold X imagestring($tmp, 5, 8, 13, $spacedCode, $tmpText); // Bold Y imagestring($tmp, 5, 9, 13, $spacedCode, $tmpText); // Bold XY // Scale up the text image to the main image using resampled for smooth (non-pixelated) edges imagecopyresampled($img, $tmp, 0, 0, 0, 0, 400, 200, 100, 40); imagepng($img, $filepath); imagedestroy($img); imagedestroy($tmp); } 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); file_put_contents($filepath, $content); } $appUrl = rtrim(getenv('APP_URL') ?: 'https://nabeh.intaleqapp.com', '/'); if (strpos($appUrl, 'localhost') !== false || strpos($appUrl, '127.0.0.1') !== false) { $appUrl = 'https://nabeh.intaleqapp.com'; } return $appUrl . '/otp/' . $filename; } /** * 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 $imageUrl = $this->generateOtpImage($code); ConversationFlowEngine::sendReply($session, $phone, '', $imageUrl); } 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()]); } } }