diff --git a/backend/app/Controllers/WhatsAppController.php b/backend/app/Controllers/WhatsAppController.php index 12619a4..c84d9d8 100644 --- a/backend/app/Controllers/WhatsAppController.php +++ b/backend/app/Controllers/WhatsAppController.php @@ -299,6 +299,11 @@ class WhatsAppController extends BaseController private function triggerAutoReply(array $session, array $msgData) { try { + // Hook the Conversation Flow Engine to intercept messages for active or new flows + if (\App\Core\Flows\ConversationFlowEngine::processMessage($session, $msgData)) { + return; + } + $rule = \App\Models\ChatbotRule::findActiveForRule($session['company_id'], $session['id']); if (!$rule || !$rule['is_active']) { return; diff --git a/backend/app/Core/Flows/BaseFlow.php b/backend/app/Core/Flows/BaseFlow.php new file mode 100644 index 0000000..7b72895 --- /dev/null +++ b/backend/app/Core/Flows/BaseFlow.php @@ -0,0 +1,20 @@ + 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 + ]); + } +} diff --git a/backend/app/Core/Flows/FlowResult.php b/backend/app/Core/Flows/FlowResult.php new file mode 100644 index 0000000..c7fd61c --- /dev/null +++ b/backend/app/Core/Flows/FlowResult.php @@ -0,0 +1,55 @@ +replyText = $replyText; + $this->nextStep = $nextStep; + $this->finished = $finished; + $this->mediaUrl = $mediaUrl; + } + + /** + * Get the reply message text to send to the contact + */ + public function getReplyText(): string + { + return $this->replyText; + } + + /** + * Get the next step key + */ + public function getNextStep(): string + { + return $this->nextStep; + } + + /** + * Check if the flow is finished (to be destroyed) + */ + public function isFinished(): bool + { + return $this->finished; + } + + /** + * Get target media URL (if any) + */ + public function getMediaUrl(): ?string + { + return $this->mediaUrl; + } +} diff --git a/backend/app/Core/Flows/TestFlow.php b/backend/app/Core/Flows/TestFlow.php new file mode 100644 index 0000000..5eab433 --- /dev/null +++ b/backend/app/Core/Flows/TestFlow.php @@ -0,0 +1,38 @@ + NOW() LIMIT 1", + [$companyId, $hash] + ); + return self::decryptState($state); + } + + /** + * Save or update conversation state securely + */ + public static function saveState(array $data): string + { + self::ensureTableExists(); + + if (!empty($data['contact_phone'])) { + $data['contact_phone_hash'] = Security::blindIndex($data['contact_phone']); + $data['contact_phone'] = Security::encrypt($data['contact_phone']); + } + + $existing = Database::selectOne( + "SELECT id FROM " . static::$table . " WHERE company_id = ? AND contact_phone_hash = ? LIMIT 1", + [$data['company_id'], $data['contact_phone_hash']] + ); + + if ($existing) { + self::update($existing['id'], $data); + return $existing['id']; + } else { + return self::create($data); + } + } + + /** + * Delete conversation state by ID + */ + public static function deleteState(int $id): int + { + self::ensureTableExists(); + return self::delete($id); + } + + /** + * Clean up all expired states + */ + public static function cleanExpired(): void + { + self::ensureTableExists(); + try { + Database::execute("DELETE FROM " . static::$table . " WHERE expires_at < NOW()"); + } catch (\Exception $e) { + error_log("Failed to clean expired conversation states: " . $e->getMessage()); + } + } + + /** + * Helper to decrypt sensitive fields + */ + private static function decryptState(?array $state): ?array + { + if ($state) { + $state['contact_phone'] = !empty($state['contact_phone']) ? Security::decrypt($state['contact_phone']) : null; + } + return $state; + } + + /** + * Ensure the conversation_states table exists in the database + */ + public static function ensureTableExists(): void + { + static $checked = false; + if ($checked) return; + + try { + Database::execute(" + CREATE TABLE IF NOT EXISTS `conversation_states` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `company_id` INT NOT NULL, + `contact_phone` VARCHAR(512) NOT NULL, + `contact_phone_hash` VARCHAR(64) NOT NULL, + `flow_name` VARCHAR(100) NOT NULL, + `current_step` VARCHAR(100) NOT NULL, + `context_data` JSON DEFAULT NULL, + `expires_at` TIMESTAMP NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY `unique_company_contact` (`company_id`, `contact_phone_hash`), + FOREIGN KEY (`company_id`) REFERENCES `companies` (`id`) ON DELETE CASCADE, + INDEX `idx_conv_expires` (`expires_at`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + "); + $checked = true; + } catch (\Exception $e) { + error_log("Failed to ensure conversation_states table: " . $e->getMessage()); + } + } +} diff --git a/backend/create_conversation_states_table.sql b/backend/create_conversation_states_table.sql new file mode 100644 index 0000000..9ef5830 --- /dev/null +++ b/backend/create_conversation_states_table.sql @@ -0,0 +1,20 @@ +-- ============================================================================== +-- 🗄️ Conversation States Table Schema +-- Persists multi-stage flow sessions for WhatsApp contacts. +-- ============================================================================== + +CREATE TABLE IF NOT EXISTS `conversation_states` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `company_id` INT NOT NULL, + `contact_phone` VARCHAR(512) NOT NULL COMMENT 'Encrypted using AES-256-GCM', + `contact_phone_hash` VARCHAR(64) NOT NULL COMMENT 'HMAC-SHA256 Blind Index', + `flow_name` VARCHAR(100) NOT NULL COMMENT 'e.g. test_flow, registration', + `current_step` VARCHAR(100) NOT NULL, + `context_data` JSON DEFAULT NULL COMMENT 'Stores transient state data', + `expires_at` TIMESTAMP NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY `unique_company_contact` (`company_id`, `contact_phone_hash`), + FOREIGN KEY (`company_id`) REFERENCES `companies` (`id`) ON DELETE CASCADE, + INDEX `idx_conv_expires` (`expires_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/backend/database_schema.sql b/backend/database_schema.sql index ace99c5..1700882 100644 --- a/backend/database_schema.sql +++ b/backend/database_schema.sql @@ -162,3 +162,20 @@ CREATE TABLE IF NOT EXISTS `chatbot_rules` ( INDEX `idx_bot_company` (`company_id`), INDEX `idx_bot_active` (`is_active`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- 11. Multi-Stage Conversation States Table +CREATE TABLE IF NOT EXISTS `conversation_states` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `company_id` INT NOT NULL, + `contact_phone` VARCHAR(512) NOT NULL COMMENT 'Encrypted using AES-256-GCM', + `contact_phone_hash` VARCHAR(64) NOT NULL COMMENT 'HMAC-SHA256 Blind Index', + `flow_name` VARCHAR(100) NOT NULL COMMENT 'e.g. test_flow, registration', + `current_step` VARCHAR(100) NOT NULL, + `context_data` JSON DEFAULT NULL COMMENT 'Stores transient state data', + `expires_at` TIMESTAMP NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY `unique_company_contact` (`company_id`, `contact_phone_hash`), + FOREIGN KEY (`company_id`) REFERENCES `companies` (`id`) ON DELETE CASCADE, + INDEX `idx_conv_expires` (`expires_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/backend/public/test_flow.php b/backend/public/test_flow.php new file mode 100644 index 0000000..30aac23 --- /dev/null +++ b/backend/public/test_flow.php @@ -0,0 +1,101 @@ + $phone, + 'body' => $body, + 'id' => 'msg_' . uniqid() + ]; + + // Temporarily point gateway to a local dummy or mock so curl doesn't fail + $oldGateway = getenv('WHATSAPP_GATEWAY_URL'); + putenv('WHATSAPP_GATEWAY_URL=http://localhost:9999'); // Invalid/mock port to prevent actual gateway call or we can let it fail gracefully + + $handled = ConversationFlowEngine::processMessage($session, $msgData); + echo "Handled by Engine: " . ($handled ? "YES" : "NO") . "\n"; + + // Restore gateway + if ($oldGateway) { + putenv("WHATSAPP_GATEWAY_URL={$oldGateway}"); + } + + // Print current state in DB + $state = ConversationState::findActive($session['company_id'], $phone); + if ($state) { + echo "Current DB State: Step = '{$state['current_step']}', Context = '{$state['context_data']}'\n"; + } else { + echo "Current DB State: [No Active Flow / Finished]\n"; + } +} + +// 3. Run test cases +// Case A: Message not triggering anything +simulateIncoming($session, $testPhone, "مرحبا"); + +// Case B: Trigger keyword "اختبار" +simulateIncoming($session, $testPhone, "اختبار"); + +// Case C: Provide name "احمد" +simulateIncoming($session, $testPhone, "احمد"); + +// Case D: Provide invalid rating "10" +simulateIncoming($session, $testPhone, "10"); + +// Case E: Provide valid rating "5" +simulateIncoming($session, $testPhone, "5"); + +// Case F: Start again and test cancellation +simulateIncoming($session, $testPhone, "اختبار"); +simulateIncoming($session, $testPhone, "إلغاء"); + +echo "\n=== Mock Test Complete ===\n"; diff --git a/backend/test_flow.php b/backend/test_flow.php new file mode 100644 index 0000000..151f7e0 --- /dev/null +++ b/backend/test_flow.php @@ -0,0 +1,88 @@ + $phone, + 'body' => $body, + 'id' => 'msg_' . uniqid() + ]; + + $handled = ConversationFlowEngine::processMessage($session, $msgData); + echo "Handled by Engine: " . ($handled ? "YES" : "NO") . "\n"; + + // Print current state in DB + $state = ConversationState::findActive($session['company_id'], $phone); + if ($state) { + echo "Current DB State: Step = '{$state['current_step']}', Context = '{$state['context_data']}'\n"; + } else { + echo "Current DB State: [No Active Flow / Finished]\n"; + } +} + +// 3. Run test cases +// Case A: Message not triggering anything +simulateIncoming($session, $testPhone, "مرحبا"); + +// Case B: Trigger keyword "اختبار" +simulateIncoming($session, $testPhone, "اختبار"); + +// Case C: Provide name "احمد" +simulateIncoming($session, $testPhone, "احمد"); + +// Case D: Provide invalid rating "10" +simulateIncoming($session, $testPhone, "10"); + +// Case E: Provide valid rating "5" +simulateIncoming($session, $testPhone, "5"); + +// Case F: Start again and test cancellation +simulateIncoming($session, $testPhone, "اختبار"); +simulateIncoming($session, $testPhone, "إلغاء"); + +echo "\n=== Mock Test Complete ===\n"; diff --git a/docs/strategy_analysis.md b/docs/strategy_analysis.md new file mode 100644 index 0000000..8630026 --- /dev/null +++ b/docs/strategy_analysis.md @@ -0,0 +1,97 @@ +
+ +# 🧠 تحليل استراتيجي شامل — منصة نبيه (Nabeh) +### رؤية المنتج، المجالات التسويقية، والأفكار الإبداعية + +--- + +## 📊 أولاً: ماذا نملك الآن؟ (تحليل القدرات الحالية) + +| القدرة | الحالة | التفاصيل | +|--------|--------|----------| +| بوابة واتساب (Baileys Gateway) | ✅ جاهز | إرسال واستقبال رسائل نصية، صوتية، وصور | +| ذكاء اصطناعي (Gemini AI) | ✅ جاهز | رد تلقائي نصي + فهم صوتي + تحليل صور | +| نظام Multi-Tenant | ✅ جاهز | دعم شركات متعددة على نفس المنصة | +| نظام CRM أساسي | ✅ جاهز | جهات اتصال + مجموعات + حملات بث | +| ربط APIs خارجية | ✅ جاهز | Endpoints قابلة للتخصيص لكل شركة | +| تكامل سلة | ⏸️ مؤجل | الكود جاهز، بس بحاجة توثيق سعودي | +| التحقق من وصولات الدفع | ✅ جاهز | تحليل صور الوصولات + تحقق تلقائي | +| نظام أمني متقدم | ✅ جاهز | تشفير AES-256، JWT، Rate Limiting | + +--- + +## 🎯 ثانياً: المجالات التسويقية والقطاعات المستهدفة + +### القطاع 1: 🏪 التجارة الإلكترونية (E-Commerce) — أولوية عالية + +#### المنصات المستهدفة بعد سلة: + +| المنصة | حجم السوق | سهولة التكامل | الأولوية | +|--------|-----------|---------------|----------| +| **WooCommerce** | 🌍 عالمي — ملايين المتاجر | ⭐⭐⭐ سهل (REST API مفتوح) | 🔴 عالية جداً | +| **Shopify** | 🌍 عالمي — 4.8M+ متجر | ⭐⭐ متوسط (OAuth App) | 🔴 عالية | +| **Zid (زد)** | 🇸🇦 سعودي | ⭐⭐⭐ سهل (API مشابه لسلة) | 🟡 متوسطة | +| **OpenCart** | 🌍 عالمي | ⭐⭐ متوسط | 🟢 منخفضة | + +**شو نقدر نوفر لمتاجر WooCommerce/Shopify:** +- الرد الآلي على استفسارات العملاء (حالة الطلب، الشحن، الأسعار) +- إشعارات تلقائية بتعنوان الطلبات عبر واتساب +- استرجاع سلة المشتريات المتروكة (Abandoned Cart Recovery) + +--- + +### القطاع 2: 🚌 النقل والمواصلات (Entaleq / انطلق) — أولوية عالية جداً + +#### 💡 فكرة 1: التسجيل الذكي عبر الواتساب (Smart Registration Bot) +يقوم العميل بإرسال المستندات وتأكيد البيانات خطوة بخطوة عبر محادثة ذكية على واتساب. + +#### 💡 فكرة 2: الرسائل الصوتية بالذكاء الاصطناعي (Voice AI) +استقبال مقاطع صوتية وفهمها بالذكاء الاصطناعي، والرد الصوتي المولد بالذكاء الاصطناعي (Text-to-Speech). + +#### 💡 فكرة 3: طلب رحلة عبر الواتساب (Trip Request Bot) +تقديم طلبات الرحلات ومطابقتها مع نظام انطلق وتأكيدها. + +#### 💡 فكرة 4: تتبع الرحلة عبر الواتساب (Trip Tracking Bot) +تمكين أولياء الأمور من الاستعلام عن مسار وموقع الرحلة وإرسال تحديثات حالة الطلاب تلقائياً. + +--- + +## 🔥 ثالثاً: الأفكار الإبداعية المميزة (Killer Features) + +### 1. 🧠 نظام المحادثة الذكية متعدد المراحل (Conversation Flow Engine) +القدرة على إدارة حالة العميل وحفظ سياق المحادثة وتوجيهه عبر خطوات محددة (مثل نماذج إدخال البيانات الذكية). + +### 2. 🔌 نظام التكامل العالمي (Universal Integration Hub) +لوحة تحكم لربط الأنظمة الخارجية بسهولة عبر Webhooks و Custom APIs وتخصيص المدخلات والمخرجات. + +### 3. 📊 لوحة تحليلات ذكية (AI Analytics Dashboard) +تحليلات شاملة للمحادثات، وتصنيف للأسئلة الشائعة وتحديد رضا العملاء تلقائياً. + +### 4. 🎭 شخصيات البوت المخصصة (Bot Personas) +اختيار نبرة البوت ولهجته ولغته بما يتناسب مع طبيعة عمل الشركة وعملائها. + +--- + +## 🏆 رابعاً: المقارنة مع المنافسين + +يتفوق نبيه بقدرته العالية على معالجة الصور والملفات الصوتية بالذكاء الاصطناعي، ودعم الربط والتكامل المباشر مع الأنظمة المحلية مثل "انطلق" ومتاجر WooCommerce دون قيود. + +--- + +## 💰 خامساً: النموذج التجاري المقترح (Business Model) + +- باقة أساسية (محادثات نصية) +- باقة متقدمة (صوت وصور وتكاملات) +- باقة مؤسسات (تسجيل ذكي وتتبع وتحليلات متقدمة) + +--- + +## 📋 سادساً: خارطة الطريق المعتمدة (Roadmap) + +1. **المرحلة 1: المحرك الأساسي** (Conversation Flow Engine) - 🛠️ *نحن هنا* +2. **المرحلة 2: نظام التسجيل الذكي** لتطبيق انطلق (Smart Registration) +3. **المرحلة 3: التجارة الإلكترونية ولوحة الإدارة** (WooCommerce + Super Admin & Tenant Dashboards + Analytics) +4. **المرحلة 4: الاشتراكات ونظام الدفع** +5. **المرحلة 5: تطبيق الموبايل** (Flutter App) + +