TestFlow::class, ]; /** * Map of keyword triggers to start a specific flow */ private static array $startTriggers = [ 'test' => 'test_flow', 'اختبار' => 'test_flow', ]; /** * Process incoming message. * Returns true if handled by the flow engine, false otherwise. */ public static function processMessage(array $session, array $msgData): bool { // 1. Housekeeping: remove expired sessions ConversationState::cleanExpired(); $phone = $msgData['phone']; $companyId = $session['company_id']; $text = isset($msgData['body']) ? trim($msgData['body']) : ''; // 2. Lookup existing active flow $state = ConversationState::findActive($companyId, $phone); $flowName = ''; $currentStep = ''; $context = []; if ($state) { $flowName = $state['flow_name']; $currentStep = $state['current_step']; $context = json_decode($state['context_data'] ?: '{}', true) ?: []; } else { // Check if message is a starting trigger for a new flow $normalizedText = strtolower(trim($text)); if (isset(self::$startTriggers[$normalizedText])) { $flowName = self::$startTriggers[$normalizedText]; $currentStep = 'start'; $context = []; } } // If no active flow and no trigger matches, pass control back to normal bot rules if (empty($flowName) || !isset(self::$flows[$flowName])) { return false; } // 3. User cancel flow option $normalizedCancel = strtolower(trim($text)); if (in_array($normalizedCancel, ['إلغاء', 'خروج', 'cancel', 'exit'])) { if ($state) { ConversationState::deleteState($state['id']); $cancelMsg = in_array($normalizedCancel, ['cancel', 'exit']) ? 'Interactive flow cancelled.' : 'تم إلغاء المحادثة التفاعلية.'; self::sendReply($session, $phone, $cancelMsg); return true; } } // 4. Instantiate and execute the flow $flowClass = self::$flows[$flowName]; /** @var BaseFlow $flowInstance */ $flowInstance = new $flowClass(); try { $result = $flowInstance->handleStep($currentStep, $msgData, $context); if ($result->isFinished()) { // Flow has reached terminal state: clean up DB record if ($state) { ConversationState::deleteState($state['id']); } } else { // Flow has next step: save or update state record (TTL: 1 hour) ConversationState::saveState([ 'company_id' => $companyId, 'contact_phone' => $phone, 'flow_name' => $flowName, 'current_step' => $result->getNextStep(), 'context_data' => json_encode($context, JSON_UNESCAPED_UNICODE), 'expires_at' => date('Y-m-d H:i:s', strtotime('+1 hour')) ]); } // 5. Send reply if one is provided if ($result->getReplyText() !== '') { self::sendReply($session, $phone, $result->getReplyText()); } return true; } catch (\Exception $e) { error_log("[ConversationFlowEngine Exception] Flow '{$flowName}' failed: " . $e->getMessage()); return false; } } /** * Send outbound message reply via the Baileys Gateway */ private static function sendReply(array $session, string $phone, string $message): void { $gatewayUrl = rtrim(getenv('WHATSAPP_GATEWAY_URL') ?: 'http://localhost:3722', '/'); if (substr($gatewayUrl, -4) === '/api') { $sendUrl = $gatewayUrl . '/messages/send'; } else { $sendUrl = $gatewayUrl . '/api/messages/send'; } $payload = json_encode([ 'session_key' => $session['session_key'], 'phone' => $phone, 'message' => $message ]); $ch = curl_init($sendUrl); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', 'X-Webhook-Secret: ' . getenv('WEBHOOK_SECRET') ]); curl_setopt($ch, CURLOPT_TIMEOUT, 10); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $status = 'failed'; $errorMsg = null; $waMsgId = null; if ($httpCode === 200) { $status = 'sent'; $resData = json_decode($response, true); $waMsgId = $resData['data']['key']['id'] ?? null; } else { $resData = json_decode($response, true); $errorMsg = $resData['error'] ?? 'HTTP Code ' . $httpCode; error_log("[Flow Engine Gateway Error] Failed to send: " . $errorMsg); } // Log the outbound auto-reply message MessageLog::logMessage([ 'company_id' => $session['company_id'], 'session_id' => $session['id'], 'contact_phone' => $phone, 'direction' => 'outbound', 'message_type' => 'text', 'message_body' => $message, 'whatsapp_message_id' => $waMsgId, 'status' => $status, 'error_message' => $errorMsg ]); } }