feat: implement multi-line session failover with round-robin strategy and session readiness validation for OTP delivery.
This commit is contained in:
@@ -144,38 +144,33 @@ class OTPController extends BaseController
|
||||
// Clean phone number (remove non-digits including +)
|
||||
$phone = preg_replace('/\D/', '', $phone);
|
||||
|
||||
// 1. Resolve WhatsApp Session
|
||||
$session = null;
|
||||
// 1. Resolve WhatsApp Candidate Sessions with Round-Robin & Multi-Line Failover
|
||||
$candidateSessions = [];
|
||||
if ($sessionId) {
|
||||
$session = WhatsAppSession::findSecure((int)$sessionId);
|
||||
if (!$session || (int)$session['company_id'] !== (int)$companyId) {
|
||||
$singleSession = WhatsAppSession::findSecure((int)$sessionId);
|
||||
if (!$singleSession || (int)$singleSession['company_id'] !== (int)$companyId) {
|
||||
$response->status(404)->json(['error' => 'WhatsApp session not found']);
|
||||
return;
|
||||
}
|
||||
$candidateSessions = [$singleSession];
|
||||
} else {
|
||||
// Grab the first connected session of the company
|
||||
$sessions = WhatsAppSession::findAllByCompany($companyId);
|
||||
foreach ($sessions as $s) {
|
||||
if ($s['status'] === 'connected') {
|
||||
$session = $s;
|
||||
break;
|
||||
// Get all active connected sessions ordered by Round-Robin strategy
|
||||
$candidateSessions = WhatsAppSession::getRoundRobinSessionsList($companyId);
|
||||
|
||||
// Fallback: If no connected session found in cache/DB, check all company sessions
|
||||
if (empty($candidateSessions)) {
|
||||
$all = WhatsAppSession::findAllByCompany($companyId);
|
||||
if (!empty($all)) {
|
||||
$candidateSessions = $all;
|
||||
}
|
||||
}
|
||||
if (!$session && !empty($sessions)) {
|
||||
$session = $sessions[0]; // fallback to first session if none is connected
|
||||
}
|
||||
}
|
||||
|
||||
if (!$session) {
|
||||
if (empty($candidateSessions)) {
|
||||
$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) {
|
||||
@@ -208,19 +203,24 @@ class OTPController extends BaseController
|
||||
// 3. Generate verification code
|
||||
$code = $customCode ? trim($customCode) : (string)rand(1000, 9999);
|
||||
|
||||
// 4. Send Message
|
||||
// 4. Prepare Payload
|
||||
try {
|
||||
$usedElevenLabs = false;
|
||||
$usedElevenLabsFinal = false;
|
||||
$audioBase64 = null;
|
||||
$mimeType = null;
|
||||
$imageBase64 = null;
|
||||
$textMsg = null;
|
||||
|
||||
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']);
|
||||
// Try getting ElevenLabs key from first candidate session
|
||||
$firstSession = $candidateSessions[0];
|
||||
$rule = \App\Models\ChatbotRule::findActiveForRule($companyId, $firstSession['id']);
|
||||
$configuredElKey = ($rule && !empty($rule['elevenlabs_api_key'])) ? $rule['elevenlabs_api_key'] : null;
|
||||
$elApiKey = \App\Services\GeminiService::getElevenLabsApiKey($configuredElKey);
|
||||
|
||||
@@ -233,7 +233,7 @@ class OTPController extends BaseController
|
||||
if ($audioData) {
|
||||
$audioBase64 = $audioData['audio'];
|
||||
$mimeType = $audioData['mimeType'];
|
||||
$usedElevenLabs = true;
|
||||
$usedElevenLabsFinal = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -242,47 +242,75 @@ class OTPController extends BaseController
|
||||
// Fallback to Google Translate TTS
|
||||
$audioBase64 = TTSService::textToSpeechArabic($textToRead);
|
||||
$mimeType = 'audio/mp3';
|
||||
$usedElevenLabs = false;
|
||||
$usedElevenLabsFinal = false;
|
||||
}
|
||||
|
||||
if (!$audioBase64) {
|
||||
$response->status(500)->json(['error' => 'Failed to generate voice OTP audio.']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send voice note
|
||||
$success = ConversationFlowEngine::sendReply($session, $phone, '', null, $audioBase64, $mimeType);
|
||||
if (!$success) {
|
||||
$response->status(500)->json(['error' => 'Failed to send voice OTP via gateway.']);
|
||||
return;
|
||||
}
|
||||
} else if ($type === 'image') {
|
||||
// Generate OTP Image as Base64
|
||||
$imageBase64 = $this->generateOtpImage($code);
|
||||
$success = ConversationFlowEngine::sendReply($session, $phone, '', null, null, null, $imageBase64);
|
||||
if (!$success) {
|
||||
$response->status(500)->json(['error' => 'Failed to send image OTP via gateway.']);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Send text
|
||||
$customText = $body['message'] ?? null;
|
||||
if ($customText) {
|
||||
$textMsg = str_replace('{code}', $code, $customText);
|
||||
} else {
|
||||
$textMsg = "رمز التحقق الخاص بك لمتجر نابه هو: *{$code}* \n الرجاء عدم مشاركته مع أي شخص.";
|
||||
}
|
||||
$success = ConversationFlowEngine::sendReply($session, $phone, $textMsg);
|
||||
if (!$success) {
|
||||
$response->status(500)->json(['error' => 'Failed to send text OTP via gateway.']);
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. Send Message with Automatic Multi-Line Failover across Active Lines
|
||||
$sentSuccessfully = false;
|
||||
$usedSession = null;
|
||||
$attemptErrors = [];
|
||||
|
||||
foreach ($candidateSessions as $index => $session) {
|
||||
// If there are multiple sessions, skip non-connected ones
|
||||
if ($session['status'] !== 'connected' && count($candidateSessions) > 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sessionLabel = ($session['name'] ?? 'Line') . " (" . ($session['session_key'] ?? 'unknown') . ")";
|
||||
|
||||
try {
|
||||
$success = false;
|
||||
if ($type === 'voice') {
|
||||
$success = ConversationFlowEngine::sendReply($session, $phone, '', null, $audioBase64, $mimeType);
|
||||
} else if ($type === 'image') {
|
||||
$success = ConversationFlowEngine::sendReply($session, $phone, '', null, null, null, $imageBase64);
|
||||
} else {
|
||||
$success = ConversationFlowEngine::sendReply($session, $phone, $textMsg);
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
$sentSuccessfully = true;
|
||||
$usedSession = $session;
|
||||
break; // Sent successfully! Break out of failover loop
|
||||
} else {
|
||||
$errorDetail = "Failed on {$sessionLabel}";
|
||||
$attemptErrors[] = $errorDetail;
|
||||
error_log("[OTP Failover] " . $errorDetail . " → Trying next active line...");
|
||||
}
|
||||
} catch (\Exception $sendEx) {
|
||||
$errorDetail = "Exception on {$sessionLabel}: " . $sendEx->getMessage();
|
||||
$attemptErrors[] = $errorDetail;
|
||||
error_log("[OTP Failover Exception] " . $errorDetail);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$sentSuccessfully) {
|
||||
$errorSummary = !empty($attemptErrors) ? implode(' | ', $attemptErrors) : 'All candidate lines failed';
|
||||
$response->status(500)->json([
|
||||
'error' => "Failed to send {$type} OTP via gateway. Tried " . count($attemptErrors) . " active line(s): {$errorSummary}"
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Increment usage stats
|
||||
if ($companyId !== 1) {
|
||||
CompanySubscriptionUsage::incrementUsage($companyId, 'request');
|
||||
if ($type === 'voice' && $usedElevenLabs) {
|
||||
if ($type === 'voice' && $usedElevenLabsFinal) {
|
||||
CompanySubscriptionUsage::incrementUsage($companyId, 'voice');
|
||||
}
|
||||
}
|
||||
@@ -291,7 +319,12 @@ class OTPController extends BaseController
|
||||
'status' => 'success',
|
||||
'message' => 'OTP sent successfully',
|
||||
'code' => $code,
|
||||
'type' => $type
|
||||
'type' => $type,
|
||||
'session_used' => [
|
||||
'id' => $usedSession['id'] ?? null,
|
||||
'name' => $usedSession['name'] ?? null,
|
||||
'session_key' => $usedSession['session_key'] ?? null
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
|
||||
Reference in New Issue
Block a user