From 8acca92bbae7ac5ed105954326327a6468760436 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Fri, 22 May 2026 21:52:51 +0300 Subject: [PATCH] Deploy: 2026-05-22 21:52:51 --- ai_document.php | 245 +++++++++ .../app/Core/Flows/ConversationFlowEngine.php | 60 ++- .../app/Core/Flows/DriverRegistrationFlow.php | 483 ++++++++++++++++++ backend/app/Models/DriverOcrData.php | 144 ++++++ backend/app/Models/DriverReminder.php | 74 +++ backend/app/Services/GeminiService.php | 111 ++++ backend/public/index.html | 18 +- backend/public/index.php | 232 +++++++++ backend/public/test_simulation.php | 139 +++++ implementation_plan.md | 330 ++---------- registerDriverAndCarService.php | 237 +++++++++ updateDriverToActive.php | 150 ++++++ 12 files changed, 1938 insertions(+), 285 deletions(-) create mode 100644 ai_document.php create mode 100644 backend/app/Core/Flows/DriverRegistrationFlow.php create mode 100644 backend/app/Models/DriverOcrData.php create mode 100644 backend/app/Models/DriverReminder.php create mode 100644 backend/public/test_simulation.php create mode 100644 registerDriverAndCarService.php create mode 100644 updateDriverToActive.php diff --git a/ai_document.php b/ai_document.php new file mode 100644 index 0000000..2917c03 --- /dev/null +++ b/ai_document.php @@ -0,0 +1,245 @@ + 'image/jpeg', + 'png' => 'image/png', + default => 'application/octet-stream', +}; + +$prompts = [ + "id_front_sy" => << << << << << << [ + ["role" => "user", "parts" => [["text" => $prompt]]], + ["role" => "user", "parts" => [["inlineData" => ["mimeType" => $mimeType, "data" => $imageBase64]]]] + ] +]; + +$ch = curl_init($apiURL); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_setopt($ch, CURLOPT_POST, true); +curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); +curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); + +$response = curl_exec($ch); + +if (curl_errno($ch)) { + $error_msg = curl_error($ch); + error_log("CURL error: $error_msg"); + jsonError("AI Error: $error_msg"); + curl_close($ch); + exit; +} + +curl_close($ch); +error_log("AI raw response: $response"); + +$data = json_decode($response, true); +if (json_last_error() !== JSON_ERROR_NONE) { + error_log("JSON decode error: " . json_last_error_msg()); + jsonError("Failed to parse AI response"); + exit; +} + +$textRaw = $data['candidates'][0]['content']['parts'][0]['text'] ?? ''; +$textRaw = trim(preg_replace('/```json|```/', '', $textRaw)); +$json = json_decode($textRaw, true); + +$requiredKey = match ($type) { + 'id_front_sy' => 'national_number', + 'id_back_sy' => 'gender', + 'driving_license_sy' => 'license_type', + 'vehicle_license_sy' => 'chassis', + default => null, +}; + +if (!$json || ($requiredKey && !isset($json[$requiredKey]))) { + error_log("AI response missing required key '$requiredKey': $textRaw"); + jsonError("AI failed to extract required information"); + exit; +} + +printSuccess([ + "image_url" => $imageUrl, + "data" => $json +]); \ No newline at end of file diff --git a/backend/app/Core/Flows/ConversationFlowEngine.php b/backend/app/Core/Flows/ConversationFlowEngine.php index 23e4773..92427a2 100644 --- a/backend/app/Core/Flows/ConversationFlowEngine.php +++ b/backend/app/Core/Flows/ConversationFlowEngine.php @@ -16,6 +16,7 @@ class ConversationFlowEngine */ private static array $flows = [ 'test_flow' => TestFlow::class, + 'driver_registration_flow' => DriverRegistrationFlow::class, ]; /** @@ -24,6 +25,9 @@ class ConversationFlowEngine private static array $startTriggers = [ 'test' => 'test_flow', 'اختبار' => 'test_flow', + 'سجل' => 'driver_registration_flow', + 'تسجيل' => 'driver_registration_flow', + 'register' => 'driver_registration_flow', ]; /** @@ -39,6 +43,20 @@ class ConversationFlowEngine $companyId = $session['company_id']; $text = isset($msgData['body']) ? trim($msgData['body']) : ''; + // If incoming message is audio, transcribe it via Gemini + $isAudio = !empty($msgData['audio']) && !empty($msgData['mimeType']); + if ($isAudio) { + $rule = \App\Models\ChatbotRule::findActiveForRule($companyId); + $apiKey = ($rule && !empty($rule['gemini_api_key'])) ? $rule['gemini_api_key'] : getenv('GEMINI_API_KEY'); + if (!empty($apiKey)) { + $transcription = \App\Services\GeminiService::transcribeAudio($apiKey, $msgData['audio'], $msgData['mimeType']); + if ($transcription) { + $text = $transcription; + $msgData['body'] = $transcription; + } + } + } + // 2. Lookup existing active flow $state = ConversationState::findActive($companyId, $phone); @@ -84,6 +102,7 @@ class ConversationFlowEngine $flowInstance = new $flowClass(); try { + $context['company_id'] = $companyId; $result = $flowInstance->handleStep($currentStep, $msgData, $context); if ($result->isFinished()) { @@ -104,8 +123,8 @@ class ConversationFlowEngine } // 5. Send reply if one is provided - if ($result->getReplyText() !== '') { - self::sendReply($session, $phone, $result->getReplyText()); + if ($result->getReplyText() !== '' || $result->getMediaUrl() !== null) { + self::sendReply($session, $phone, $result->getReplyText(), $result->getMediaUrl()); } return true; @@ -118,8 +137,14 @@ class ConversationFlowEngine /** * Send outbound message reply via the Baileys Gateway */ - private static function sendReply(array $session, string $phone, string $message): void - { + public static function sendReply( + array $session, + string $phone, + string $message, + ?string $mediaUrl = null, + ?string $audioBase64 = null, + ?string $mimetype = null + ): void { $gatewayUrl = rtrim(getenv('WHATSAPP_GATEWAY_URL') ?: 'http://localhost:3722', '/'); if (substr($gatewayUrl, -4) === '/api') { $sendUrl = $gatewayUrl . '/messages/send'; @@ -127,11 +152,25 @@ class ConversationFlowEngine $sendUrl = $gatewayUrl . '/api/messages/send'; } - $payload = json_encode([ + $payloadData = [ 'session_key' => $session['session_key'], - 'phone' => $phone, - 'message' => $message - ]); + 'phone' => $phone + ]; + + if ($message !== '') { + $payloadData['message'] = $message; + } + if ($mediaUrl !== null) { + $payloadData['media_url'] = $mediaUrl; + } + if ($audioBase64 !== null) { + $payloadData['audio'] = $audioBase64; + } + if ($mimetype !== null) { + $payloadData['mimetype'] = $mimetype; + } + + $payload = json_encode($payloadData); $ch = curl_init($sendUrl); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); @@ -161,14 +200,17 @@ class ConversationFlowEngine error_log("[Flow Engine Gateway Error] Failed to send: " . $errorMsg); } + $msgType = ($audioBase64 !== null || $mediaUrl !== null) ? 'audio' : 'text'; + // 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_type' => $msgType, 'message_body' => $message, + 'media_url' => $mediaUrl, 'whatsapp_message_id' => $waMsgId, 'status' => $status, 'error_message' => $errorMsg diff --git a/backend/app/Core/Flows/DriverRegistrationFlow.php b/backend/app/Core/Flows/DriverRegistrationFlow.php new file mode 100644 index 0000000..895be7f --- /dev/null +++ b/backend/app/Core/Flows/DriverRegistrationFlow.php @@ -0,0 +1,483 @@ + << << << << << << 'cancelled']); + } + // Restore previous step + $step = $context['previous_step'] ?? 'ask_name'; + } + + // Check if user requests postponement/delay (only if already started and not finished) + if ($step !== 'start' && $step !== 'finished' && !empty($text)) { + $rule = ChatbotRule::findActiveForRule($companyId); + $apiKey = ($rule && !empty($rule['gemini_api_key'])) ? $rule['gemini_api_key'] : getenv('GEMINI_API_KEY'); + if (!empty($apiKey)) { + $postponeData = $this->detectPostponement($text, $apiKey); + if ($postponeData !== null) { + $hours = $postponeData['hours']; + $postponeCount = ($context['postpone_count'] ?? 0) + 1; + + if ($postponeCount > 3) { + return new FlowResult( + "عذراً كابتن، لقد تجاوزت الحد الأقصى لمرات التأجيل (3 مرات). تم إلغاء طلب التسجيل الحالي. يمكنك البدء من جديد عندما تكون جاهزاً بكتابة 'تسجيل'.", + "finished", + true + ); + } + + $context['postpone_count'] = $postponeCount; + $context['previous_step'] = $step; + + // Schedule reminder + $scheduledAt = date('Y-m-d H:i:s', strtotime("+{$hours} hours")); + DriverReminder::saveReminder([ + 'company_id' => $companyId, + 'phone' => $phone, + 'scheduled_at' => $scheduledAt, + 'postpone_count' => $postponeCount, + 'status' => 'pending' + ]); + + return new FlowResult( + "حاضر كابتن، قمت بتأجيل التسجيل. سأقوم بتذكيرك بعد {$hours} ساعة لإكمال خطوات التسجيل. بالتوفيق!", + "postponed" + ); + } + } + } + + switch ($step) { + case 'start': + return new FlowResult( + "أهلاً بك كابتن في خدمة تسجيل كباتن تطبيق انطلق 🚖.\nيرجى إرسال اسمك الثلاثي الكامل للبدء:", + "ask_name" + ); + + case 'ask_name': + if (empty($text)) { + return new FlowResult("يرجى إدخال اسمك الثلاثي الكامل للاستمرار:", "ask_name"); + } + $context['name'] = $text; + return new FlowResult( + "شكراً كابتن {$text}.\nالآن يرجى إرسال صورة **الوجه الأمامي للهوية الشخصية** (تأكد من أن الصورة واضحة والإضاءة جيدة):", + "id_front" + ); + + case 'id_front': + return $this->processOcrStep( + $step, + $messageData, + $context, + "id_front_sy", + "national_number", + "عذراً كابتن، لم أتمكن من قراءة الرقم الوطني من الهوية بوضوح. يرجى إرسال صورة أخرى للوجه الأمامي للهوية الشخصية تكون أكثر وضوحاً:", + "تم استخراج الرقم الوطني بنجاح ✅.\nالآن، يرجى إرسال صورة **الوجه الخلفي للهوية الشخصية**:", + "id_back" + ); + + case 'id_back': + return $this->processOcrStep( + $step, + $messageData, + $context, + "id_back_sy", + "gender", + "عذراً كابتن، لم أتمكن من قراءة بيانات الوجه الخلفي للهوية بوضوح. يرجى إرسال صورة أخرى للوجه الخلفي للهوية الشخصية:", + "تم استخراج البيانات بنجاح ✅.\nيرجى إرسال صورة **الوجه الأمامي لرخصة القيادة**:", + "driving_license_front" + ); + + case 'driving_license_front': + return $this->processOcrStep( + $step, + $messageData, + $context, + "driving_license_sy_front", + "national_number", + "عذراً كابتن، لم أتمكن من قراءة رخصة القيادة بوضوح. يرجى إرسال صورة أخرى واضحة للوجه الأمامي لرخصة القيادة:", + "تم استخراج بيانات رخصة القيادة بنجاح ✅.\nيرجى إرسال صورة **الوجه الخلفي لرخصة القيادة**:", + "driving_license_back" + ); + + case 'driving_license_back': + return $this->processOcrStep( + $step, + $messageData, + $context, + "driving_license_sy_back", + "license_number", + "عذراً كابتن، لم أتمكن من قراءة الوجه الخلفي لرخصة القيادة بوضوح. يرجى إعادة إرسال الصورة بشكل أكثر وضوحاً:", + "تم استخراج البيانات بنجاح ✅.\nيرجى إرسال صورة **الوجه الأمامي لرخصة السيارة (الرخصة البرتقالية)**:", + "vehicle_license_front" + ); + + case 'vehicle_license_front': + return $this->processOcrStep( + $step, + $messageData, + $context, + "vehicle_license_sy_front", + "car_plate", + "عذراً كابتن، لم أتمكن من قراءة رقم لوحة السيارة بوضوح. يرجى إرسال صورة واضحة للوجه الأمامي لرخصة السيارة:", + "تم استخراج رقم اللوحة بنجاح ✅.\nيرجى إرسال صورة **الوجه الخلفي لرخصة السيارة (الرخصة البرتقالية)**:", + "vehicle_license_back" + ); + + case 'vehicle_license_back': + return $this->processOcrStep( + $step, + $messageData, + $context, + "vehicle_license_sy_back", + "chassis", + "عذراً كابتن، لم أتمكن من قراءة مواصفات السيارة بوضوح. يرجى إرسال صورة واضحة للوجه الخلفي لرخصة السيارة:", + "تم استخراج مواصفات السيارة بنجاح ✅.\nيرجى إرسال صورة **وثيقة غير محكوم (لا حكم عليه)**:", + "criminal_record" + ); + + case 'criminal_record': + if (empty($messageData['image']) || empty($messageData['imageMimeType'])) { + return new FlowResult("الرجاء إرسال صورة وثيقة غير محكوم (لا حكم عليه) للاستمرار:", "criminal_record"); + } + + // Save non-OCR criminal record image + $imageUrl = $this->saveIncomingImage($step, $phone, $messageData); + if (!$imageUrl) { + return new FlowResult("عذراً، فشل حفظ الصورة. الرجاء إعادة إرسال صورة الوثيقة:", "criminal_record"); + } + + // Securely save registration data to local database + try { + DriverOcrData::saveSecure([ + 'company_id' => $companyId, + 'phone' => $phone, + 'name' => $context['name'], + 'id_front_url' => $context['id_front_url'] ?? null, + 'id_front_ocr' => $context['id_front_ocr'] ?? null, + 'id_back_url' => $context['id_back_url'] ?? null, + 'id_back_ocr' => $context['id_back_ocr'] ?? null, + 'driving_license_front_url' => $context['driving_license_front_url'] ?? null, + 'driving_license_front_ocr' => $context['driving_license_front_ocr'] ?? null, + 'driving_license_back_url' => $context['driving_license_back_url'] ?? null, + 'driving_license_back_ocr' => $context['driving_license_back_ocr'] ?? null, + 'vehicle_license_front_url' => $context['vehicle_license_front_url'] ?? null, + 'vehicle_license_front_ocr' => $context['vehicle_license_front_ocr'] ?? null, + 'vehicle_license_back_url' => $context['vehicle_license_back_url'] ?? null, + 'vehicle_license_back_ocr' => $context['vehicle_license_back_ocr'] ?? null, + 'criminal_record_url' => $imageUrl, + 'status' => 'ocr_completed' + ]); + } catch (\Exception $dbEx) { + error_log("[Registration Flow Error] DB Write Failed: " . $dbEx->getMessage()); + return new FlowResult("عذراً، حدث خطأ أثناء حفظ طلبك في قاعدة البيانات. يرجى المحاولة مرة أخرى لاحقاً.", "criminal_record"); + } + + return new FlowResult( + "شكراً لك كابتن، لقد تم استلام كافة المستندات والتحقق منها بنجاح. سيقوم فريق خدمة عملاء انطلق بمراجعة طلبك وتفعيل حسابك بأسرع وقت ممكن. يومك سعيد! 🚖", + "finished", + true + ); + + default: + return new FlowResult("خطأ في تحديد خطوة المسار.", "finished", true); + } + } + + /** + * Process document upload and OCR extraction step + */ + private function processOcrStep( + string $step, + array $messageData, + array &$context, + string $promptKey, + string $requiredJsonKey, + string $failMessage, + string $successMessage, + string $nextStep + ): FlowResult { + if (empty($messageData['image']) || empty($messageData['imageMimeType'])) { + return new FlowResult("الرجاء إرسال الصورة المطلوبة للمتابعة:", $step); + } + + $imageUrl = $this->saveIncomingImage($step, $messageData['phone'], $messageData); + if (!$imageUrl) { + return new FlowResult("عذراً، فشل حفظ الصورة. الرجاء إعادة المحاولة وإرسال الصورة:", $step); + } + + $companyId = $context['company_id'] ?? 1; + $rule = ChatbotRule::findActiveForRule($companyId); + $apiKey = ($rule && !empty($rule['gemini_api_key'])) ? $rule['gemini_api_key'] : getenv('GEMINI_API_KEY'); + + if (empty($apiKey)) { + error_log("[DriverRegistrationFlow] Gemini API key not configured."); + return new FlowResult("عذراً، عطل فني في خادم معالجة الصور بالذكاء الاصطناعي. يرجى المحاولة لاحقاً.", $step); + } + + $prompt = $this->prompts[$step] ?? ''; + $rawOcr = GeminiService::generateOcrFromImage($apiKey, $prompt, $messageData['image'], $messageData['imageMimeType']); + + if (!$rawOcr) { + error_log("[DriverRegistrationFlow] OCR response empty or model request failed."); + return new FlowResult($failMessage, $step); + } + + $ocrData = json_decode($rawOcr, true); + if (!$ocrData || (empty($ocrData[$requiredJsonKey]) && !array_key_exists($requiredJsonKey, $ocrData))) { + error_log("[DriverRegistrationFlow] Missing required key '$requiredJsonKey' in OCR response: " . $rawOcr); + return new FlowResult($failMessage, $step); + } + + // Save URL and OCR JSON string in the conversation context + $context[$step . '_url'] = $imageUrl; + $context[$step . '_ocr'] = $ocrData; + + return new FlowResult($successMessage, $nextStep); + } + + /** + * Decode base64 image data and save it to the public directory + */ + private function saveIncomingImage(string $step, string $phone, array $messageData): ?string + { + try { + $extension = 'jpg'; + if (strpos($messageData['imageMimeType'], 'png') !== false) { + $extension = 'png'; + } + + $uniqueName = 'driver_' . $step . '_' . md5($phone . time()) . '.' . $extension; + $uploadDir = __DIR__ . '/../../../../public/uploads/documents/'; + + if (!is_dir($uploadDir)) { + mkdir($uploadDir, 0755, true); + } + + $uploadPath = $uploadDir . $uniqueName; + $imgData = base64_decode($messageData['image']); + + if (file_put_contents($uploadPath, $imgData) === false) { + error_log("[DriverRegistrationFlow] Failed to write image file: " . $uploadPath); + return null; + } + + return '/uploads/documents/' . $uniqueName; + } catch (\Exception $e) { + error_log("[DriverRegistrationFlow Exception] Failed to save image: " . $e->getMessage()); + return null; + } + } + + /** + * Detect if user wants to postpone, and return hours_delay if so. + */ + private function detectPostponement(string $text, string $apiKey): ?array + { + if (empty($text)) { + return null; + } + + // Quick heuristic check: if the message is too long, or clearly doesn't contain postponement keywords, skip to save API costs + $keywords = ['بعدين', 'بكرا', 'بكرة', 'بعد', 'شوي', 'ثانية', 'مشغول', 'المسا', 'الليل', 'تأجيل', 'وقت ثاني', 'تعبان', 'بعدين برسل', 'بعدين ببعت', 'ببعثهم بعدين', 'ببعتهم بعدين', 'بعدين بكمل']; + $hasKeyword = false; + foreach ($keywords as $kw) { + if (mb_strpos($text, $kw) !== false) { + $hasKeyword = true; + break; + } + } + + if (!$hasKeyword && mb_strlen($text) > 100) { + return null; + } + + $systemPrompt = "You are an assistant that detects if a user wants to postpone, delay, or complete a registration flow later. Analyze the Arabic message."; + $userMessage = << isset($data['hours_delay']) ? (int)$data['hours_delay'] : 12 + ]; + } + + return null; + } +} diff --git a/backend/app/Models/DriverOcrData.php b/backend/app/Models/DriverOcrData.php new file mode 100644 index 0000000..d129e5f --- /dev/null +++ b/backend/app/Models/DriverOcrData.php @@ -0,0 +1,144 @@ +getMessage()); + } + } +} diff --git a/backend/app/Models/DriverReminder.php b/backend/app/Models/DriverReminder.php new file mode 100644 index 0000000..dcfaca0 --- /dev/null +++ b/backend/app/Models/DriverReminder.php @@ -0,0 +1,74 @@ +getMessage()); + } + } +} diff --git a/backend/app/Services/GeminiService.php b/backend/app/Services/GeminiService.php index 8920db5..a644cb4 100644 --- a/backend/app/Services/GeminiService.php +++ b/backend/app/Services/GeminiService.php @@ -155,6 +155,59 @@ class GeminiService return $data['candidates'][0]['content']['parts'][0]['text'] ?? null; } + /** + * Transcribe incoming audio voice note to text using gemini-2.0-flash-lite + */ + public static function transcribeAudio(string $apiKey, string $audioBase64, string $mimeType): ?string + { + $url = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite:generateContent?key=' . $apiKey; + + // Clean mimeType if it contains codec info + if (strpos($mimeType, ';') !== false) { + $mimeType = trim(explode(';', $mimeType)[0]); + } + + $payload = json_encode([ + 'contents' => [ + [ + 'role' => 'user', + 'parts' => [ + [ + 'inlineData' => [ + 'mimeType' => $mimeType, + 'data' => $audioBase64 + ] + ], + [ + 'text' => "Transcribe the following audio message to Arabic text. Output only the transcription, no translation, no commentary, no markdown, and no code blocks." + ] + ] + ] + ] + ]); + + $ch = curl_init($url); + 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' + ]); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200) { + error_log("[Gemini Audio Transcription Error] HTTP " . $httpCode . " | Response: " . $response); + return null; + } + + $data = json_decode($response, true); + return trim($data['candidates'][0]['content']['parts'][0]['text'] ?? ''); + } + /** * Call Gemini API with image inline data and system instruction to generate a response text */ @@ -213,6 +266,64 @@ class GeminiService return $data['candidates'][0]['content']['parts'][0]['text'] ?? null; } + /** + * Call Gemini API with image inline data and custom prompt to extract structured OCR data + */ + public static function generateOcrFromImage(string $apiKey, string $prompt, string $imageBase64, string $mimeType): ?string + { + $url = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite:generateContent?key=' . $apiKey; + + // Clean mimeType if it contains codec info + if (strpos($mimeType, ';') !== false) { + $mimeType = trim(explode(';', $mimeType)[0]); + } + + $payload = json_encode([ + 'contents' => [ + [ + 'role' => 'user', + 'parts' => [ + [ + 'text' => $prompt + ], + [ + 'inlineData' => [ + 'mimeType' => $mimeType, + 'data' => $imageBase64 + ] + ] + ] + ] + ] + ]); + + $ch = curl_init($url); + 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' + ]); + curl_setopt($ch, CURLOPT_TIMEOUT, 35); // 35 seconds timeout for image analysis + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200) { + error_log("[Gemini OCR Image Response Error] HTTP " . $httpCode . " | Response: " . $response); + return null; + } + + $data = json_decode($response, true); + $textRaw = $data['candidates'][0]['content']['parts'][0]['text'] ?? null; + if ($textRaw) { + // Clean markdown block if present + $textRaw = trim(preg_replace('/```json|```/', '', $textRaw)); + } + return $textRaw; + } + /** * Call ElevenLabs API to generate a native audio response from text */ diff --git a/backend/public/index.html b/backend/public/index.html index 5022a1a..57817f8 100644 --- a/backend/public/index.html +++ b/backend/public/index.html @@ -1073,6 +1073,7 @@
+ @@ -2006,7 +2007,22 @@ }, loadPromptTemplate(type) { - if (type === 'nabeh') { + if (type === 'intaleq') { + this.chatbotSettings.ai_prompt = `أنت روبوت خدمة العملاء الخاص بشركة "انطلق" (Intaleq). مهمتك هي مساعدة المستخدمين والإجابة على استفساراتهم حول خدماتنا بلهجة سورية ودودة ومهنية للغاية. + +التزم بالقواعد والتفاصيل التالية بدقة عند الرد: +1. اسم الشركة: انطلق. +2. ساعات العمل: يومياً من الساعة 11 صباحاً حتى 5 مساءً. +3. طرق الدفع المتاحة: شام كاش، سيريتل، إم تي إن، وبطاقات البنك. +4. طريقة التسجيل: يمكن للمستخدمين تحميل التطبيق من متجر جوجل بلاي، آبل ستور، أو عبر رابط مباشر. عملية التسجيل سهلة وسريعة. +5. مميزات التطبيق: + - عمولة التطبيق هي 10% فقط (وهي الأقل في السوق). + - يوجد ذكاء اصطناعي متطور لتحليل المشاكل وحلها. + - ضمان حقوق السائق والركاب بشكل كامل. + - خدمة الطلب من أي مكان وبكل سهولة. +6. تحدث دائماً باللهجة السورية اللطيفة والترحيبية والودية عند التحدث باللغة العربية (مثال: "ياهلا بك"، "كيف بقدر أساعدك اليوم؟"). +7. كوني مختصرة ومباشرة ومفيدة في إجاباتكِ وتجنبي الإطالة غير الضرورية.`; + } else if (type === 'nabeh') { this.chatbotSettings.ai_prompt = `أنتِ "سارة"، موظفة خدمة العملاء الافتراضية الذكية والودودة لتطبيق "نبيه" (Nabeh). مهمتكِ هي مساعدة المستخدمين والإجابة على استفساراتهم بلطف وأدب بالمسائل التقنية والتجارية المتعلقة بالتطبيق. diff --git a/backend/public/index.php b/backend/public/index.php index eb027bc..7eca031 100644 --- a/backend/public/index.php +++ b/backend/public/index.php @@ -148,5 +148,237 @@ $router->post('/api/external/verify-payment', function ($request, $response) { }); +// REST API for Entaleq Driver OCR Details (GET) +$router->get('/api/external/driver-ocr', function ($request, $response) { + $apiKey = getenv('ENTALEQ_API_KEY'); + $incomingKey = $request->getHeader('x-api-key') ?? ''; + + if (empty($apiKey) || $incomingKey !== $apiKey) { + $response->status(401)->json([ + 'status' => 'error', + 'message' => 'Unauthorized' + ]); + return; + } + + $phone = $request->get('phone') ?? ''; + $companyId = $request->get('company_id') ?? ''; + + if (empty($phone)) { + $response->status(400)->json([ + 'status' => 'error', + 'message' => 'Missing phone parameter' + ]); + return; + } + + \App\Models\DriverOcrData::ensureTableExists(); + $hash = \App\Core\Security::blindIndex($phone); + + if ($companyId) { + $records = \App\Core\Database::select( + "SELECT * FROM driver_ocr_data WHERE company_id = ? AND phone_hash = ?", + [$companyId, $hash] + ); + } else { + $records = \App\Core\Database::select( + "SELECT * FROM driver_ocr_data WHERE phone_hash = ?", + [$hash] + ); + } + + if (empty($records)) { + $response->status(404)->json([ + 'status' => 'error', + 'message' => 'No driver records found for this phone number' + ]); + return; + } + + $decryptedRecords = []; + $host = ($request->getHeader('x-forwarded-proto') ?: 'http') . '://' . ($request->getHeader('host') ?: 'localhost'); + + foreach ($records as $record) { + $decrypted = \App\Models\DriverOcrData::decryptRecord($record); + if ($decrypted) { + // Include absolute paths for all document URLs + $urlFields = [ + 'id_front_url', 'id_back_url', + 'driving_license_front_url', 'driving_license_back_url', + 'vehicle_license_front_url', 'vehicle_license_back_url', + 'criminal_record_url' + ]; + foreach ($urlFields as $field) { + if (!empty($decrypted[$field])) { + $decrypted[$field . '_absolute'] = $host . $decrypted[$field]; + } else { + $decrypted[$field . '_absolute'] = null; + } + } + $decryptedRecords[] = $decrypted; + } + } + + $response->json([ + 'status' => 'success', + 'data' => $decryptedRecords + ]); +}); + +// REST API to Mark Driver Registered (POST) +$router->post('/api/external/register-driver', function ($request, $response) { + $apiKey = getenv('ENTALEQ_API_KEY'); + $incomingKey = $request->getHeader('x-api-key') ?? ''; + + if (empty($apiKey) || $incomingKey !== $apiKey) { + $response->status(401)->json([ + 'status' => 'error', + 'message' => 'Unauthorized' + ]); + return; + } + + $phone = $request->get('phone') ?? ''; + $companyId = $request->get('company_id') ?? ''; + + if (empty($phone)) { + $response->status(400)->json([ + 'status' => 'error', + 'message' => 'Missing phone parameter' + ]); + return; + } + + \App\Models\DriverOcrData::ensureTableExists(); + $hash = \App\Core\Security::blindIndex($phone); + + if ($companyId) { + $existing = \App\Core\Database::selectOne( + "SELECT id FROM driver_ocr_data WHERE company_id = ? AND phone_hash = ? LIMIT 1", + [$companyId, $hash] + ); + } else { + $existing = \App\Core\Database::selectOne( + "SELECT id FROM driver_ocr_data WHERE phone_hash = ? LIMIT 1", + [$hash] + ); + } + + if (!$existing) { + $response->status(404)->json([ + 'status' => 'error', + 'message' => 'Driver record not found' + ]); + return; + } + + \App\Core\Database::execute( + "UPDATE driver_ocr_data SET status = 'registered' WHERE id = ?", + [$existing['id']] + ); + + $response->json([ + 'status' => 'success', + 'message' => 'Driver status successfully updated to registered' + ]); +}); + + +// Cron job endpoint to send pending driver registration reminders +$router->get('/api/external/cron/send-reminders', function ($request, $response) { + $apiKey = getenv('ENTALEQ_API_KEY'); + $incomingKey = $request->getHeader('x-api-key') ?? ''; + + if (empty($apiKey) || $incomingKey !== $apiKey) { + $response->status(401)->json([ + 'status' => 'error', + 'message' => 'Unauthorized' + ]); + return; + } + + \App\Models\DriverReminder::ensureTableExists(); + $now = date('Y-m-d H:i:s'); + $reminders = \App\Core\Database::select( + "SELECT * FROM driver_registration_reminders WHERE status = 'pending' AND scheduled_at <= ?", + [$now] + ); + + $processed = 0; + foreach ($reminders as $reminder) { + $companyId = $reminder['company_id']; + $phone = $reminder['phone']; + + // Get company chatbot rule details + $rule = \App\Models\ChatbotRule::findActiveForRule($companyId); + $geminiKey = ($rule && !empty($rule['gemini_api_key'])) ? $rule['gemini_api_key'] : getenv('GEMINI_API_KEY'); + $elApiKey = ($rule && !empty($rule['elevenlabs_api_key'])) ? $rule['elevenlabs_api_key'] : getenv('ELEVENLABS_API_KEY'); + $elVoiceId = ($rule && !empty($rule['elevenlabs_voice_id'])) ? $rule['elevenlabs_voice_id'] : getenv('ELEVENLABS_VOICE_ID'); + + // Fetch company WhatsApp session + $session = \App\Models\WhatsAppSession::findByCompany($companyId); + if (!$session || $session['status'] !== 'connected') { + error_log("Cron Reminder: WhatsApp session not connected for company {$companyId}"); + continue; + } + + // Try to get driver name from active conversation flow state + $driverName = ''; + $state = \App\Models\ConversationState::findActive($companyId, $phone); + if ($state) { + $ctx = json_decode($state['context_data'] ?: '{}', true); + $driverName = $ctx['name'] ?? ''; + } + + $nameStr = $driverName ? " كابتن " . $driverName : " كابتن"; + $reminderMsg = "أهلاً بك{$nameStr}، حابين نذكرك تكمل خطوات تسجيلك لتنضم لعائلة انطلق 🚖. بقية الأوراق كتير مهمة لنفعل حسابك ونبدأ سوا. بانتظار إرسالها!"; + + // Generate Audio voice note if key is present + $audioData = null; + if (!empty($geminiKey)) { + $audioData = \App\Services\GeminiService::generateAudioResponse( + $geminiKey, + "أنت روبوت خدمة العملاء لشركة انطلق، تتحدث باللهجة السورية الودودة.", + $reminderMsg, + 'Puck', + $elApiKey ?: null, + $elVoiceId ?: null + ); + } + + try { + // 1. Send the text reminder + \App\Core\Flows\ConversationFlowEngine::sendReply($session, $phone, $reminderMsg); + + // 2. Send the voice note reminder if generated successfully + if ($audioData && !empty($audioData['audio'])) { + \App\Core\Flows\ConversationFlowEngine::sendReply( + $session, + $phone, + '', + null, + $audioData['audio'], + $audioData['mimeType'] ?? 'audio/mp4' + ); + } + + // 3. Mark the reminder as sent + \App\Models\DriverReminder::update($reminder['id'], [ + 'status' => 'sent' + ]); + $processed++; + } catch (\Exception $ex) { + error_log("Failed to process reminder ID {$reminder['id']}: " . $ex->getMessage()); + } + } + + $response->json([ + 'status' => 'success', + 'processed_count' => $processed, + 'total_pending_due' => count($reminders) + ]); +}); + + // 4. Dispatch the request $router->dispatch($request, $response); diff --git a/backend/public/test_simulation.php b/backend/public/test_simulation.php new file mode 100644 index 0000000..c862468 --- /dev/null +++ b/backend/public/test_simulation.php @@ -0,0 +1,139 @@ + $phone, + 'status' => 'connected' +]); +// Refresh session data +$session = WhatsAppSession::findByCompany($companyId); + +echo "1. Cleaning up existing test states/reminders...\n"; +Database::execute("DELETE FROM conversation_states WHERE contact_phone = ?", [$phone]); +Database::execute("DELETE FROM driver_registration_reminders WHERE phone = ?", [$phone]); +Database::execute("DELETE FROM driver_ocr_data WHERE phone = ?", [$phone]); + +echo "2. Initializing conversation to 'id_front' state...\n"; +ConversationState::saveState([ + 'company_id' => $companyId, + 'contact_phone' => $phone, + 'flow_name' => 'driver_registration_flow', + 'current_step' => 'id_front', + 'context_data' => json_encode(['name' => 'جميل الشام'], JSON_UNESCAPED_UNICODE), + 'expires_at' => date('Y-m-d H:i:s', strtotime('+1 hour')) +]); + +echo "3. Simulating user postponement request: 'بكرة المسا ببعتلك الهوية الشخصية'...\n"; +$msgData = [ + 'phone' => $phone, + 'body' => 'بكرة المسا ببعتلك الهوية الشخصية' +]; + +$handled = ConversationFlowEngine::processMessage($session, $msgData); +echo " Message processed by flow engine: " . ($handled ? "YES" : "NO") . "\n"; + +// Fetch the new state and scheduled reminders +$state = ConversationState::findActive($companyId, $phone); +echo " New Flow Step: " . ($state ? $state['current_step'] : 'None (finished)') . "\n"; +echo " Context: " . ($state ? $state['context_data'] : 'None') . "\n"; + +$reminders = Database::select("SELECT * FROM driver_registration_reminders WHERE phone = ?", [$phone]); +echo " Scheduled Reminders Count: " . count($reminders) . "\n"; +if (!empty($reminders)) { + foreach ($reminders as $r) { + echo " - ID: {$r['id']}, Scheduled At: {$r['scheduled_at']}, Postpone Count: {$r['postpone_count']}, Status: {$r['status']}\n"; + } +} + +echo "\n4. Testing Cron Scheduler (Simulate reminder execution by setting scheduled_at to the past)...\n"; +if (!empty($reminders)) { + Database::execute("UPDATE driver_registration_reminders SET scheduled_at = ? WHERE id = ?", [ + date('Y-m-d H:i:s', strtotime('-1 minute')), + $reminders[0]['id'] + ]); + + echo " Triggering cron process via internal endpoint request simulation...\n"; + // We call the main cron logic directly by querying and executing the reminder + $dueReminders = Database::select( + "SELECT * FROM driver_registration_reminders WHERE status = 'pending' AND scheduled_at <= ?", + [date('Y-m-d H:i:s')] + ); + + echo " Due Reminders found: " . count($dueReminders) . "\n"; + foreach ($dueReminders as $reminder) { + $rule = \App\Models\ChatbotRule::findActiveForRule($companyId); + $geminiKey = ($rule && !empty($rule['gemini_api_key'])) ? $rule['gemini_api_key'] : getenv('GEMINI_API_KEY'); + $elApiKey = ($rule && !empty($rule['elevenlabs_api_key'])) ? $rule['elevenlabs_api_key'] : getenv('ELEVENLABS_API_KEY'); + $elVoiceId = ($rule && !empty($rule['elevenlabs_voice_id'])) ? $rule['elevenlabs_voice_id'] : getenv('ELEVENLABS_VOICE_ID'); + + $driverName = 'جميل الشام'; + $nameStr = $driverName ? " كابتن " . $driverName : " كابتن"; + $reminderMsg = "أهلاً بك{$nameStr}، حابين نذكرك تكمل خطوات تسجيلك لتنضم لعائلة انطلق 🚖. بقية الأوراق كتير مهمة لنفعل حسابك ونبدأ سوا. بانتظار إرسالها!"; + + echo " Generating reminder Audio via TTS...\n"; + $audioData = null; + if (!empty($geminiKey)) { + $audioData = \App\Services\GeminiService::generateAudioResponse( + $geminiKey, + "أنت روبوت خدمة العملاء لشركة انطلق، تتحدث باللهجة السورية الودودة.", + $reminderMsg, + 'Puck', + $elApiKey ?: null, + $elVoiceId ?: null + ); + } + + echo " Sending outbound simulated message...\n"; + ConversationFlowEngine::sendReply($session, $phone, $reminderMsg); + + if ($audioData && !empty($audioData['audio'])) { + echo " Audio voice note generated successfully (Length: " . strlen($audioData['audio']) . " bytes). Sending...\n"; + ConversationFlowEngine::sendReply( + $session, + $phone, + '', + null, + $audioData['audio'], + $audioData['mimeType'] ?? 'audio/mp4' + ); + } else { + echo " Audio voice note generation skipped or failed (Gemini Key may be missing or empty).\n"; + } + + DriverReminder::update($reminder['id'], ['status' => 'sent']); + echo " Reminder ID {$reminder['id']} status set to 'sent'.\n"; + } +} + +echo "\n5. Simulating user resuming registration (sending message 'هلا يا كابتن')...\n"; +$msgData = [ + 'phone' => $phone, + 'body' => 'هلا يا كابتن' +]; + +$handled = ConversationFlowEngine::processMessage($session, $msgData); +echo " Resumed Message handled: " . ($handled ? "YES" : "NO") . "\n"; + +$state = ConversationState::findActive($companyId, $phone); +echo " Resumed Flow Step: " . ($state ? $state['current_step'] : 'None') . "\n"; + +echo "\n=== Simulation Finished ===\n"; diff --git a/implementation_plan.md b/implementation_plan.md index c1f6bc9..b835b48 100644 --- a/implementation_plan.md +++ b/implementation_plan.md @@ -1,298 +1,78 @@ -# خطة تنفيذ المعمارية الخلفية ونظام النشر المؤتمت — تطبيق نبيه (Nabeh) +# Implementation Plan - Intaleq Customer Service & Driver Registration Flow -
+This plan details the implementation of the Intaleq customer service prompt template, the multi-stage WhatsApp driver registration flow using Gemini OCR, and the authenticated REST API endpoints for Nabeh's backend. -توضح هذه الخطة التفاصيل الهيكلية والخطوات العملية لبناء النواة البرمجية للخلفية (Backend Base Architecture) باستخدام **Pure PHP OOP** وبناء نظام نشر تلقائي آمن عبر **Git/SSH** إلى السيرفر. - ---- - -## أولاً: تقسيم المشروع إلى مراحل (Milestones) - -من أجل ضمان الانتقال السلس وبناء أساسات متينة وقابلة للتطوير، سنقسم العمل على الخلفية والنشر إلى المراحل التالية: - -### المرحلة 1: بناء نواة الـ MVC وتهيئة الهيكل البرمجي -- بناء هيكل المجلدات المنظم وفصل الملفات العامة (Public) عن الكود البرمجي الأساسي. -- إعداد نظام التحميل التلقائي للملفات (Autoloader) المتوافق مع معيار PSR-4. -- برمجة موجه الطلبات (Router) المتوافق مع خادم Nginx. -- برمجة قارئ ملفات البيئة المتطور والآمن (`.env` Reader) مع تخزينه خارج المسار العام لضمان أعلى مستويات الحماية. - -### المرحلة 2: تصميم قاعدة البيانات ونواة الاتصال (Database & Core) -- تصميم ملف تهيئة الاتصال بقاعدة البيانات باستخدام PDO. -- بناء الموديل الرئيسي (Base Model) والمتحكم الرئيسي (Base Controller) لإدارة المدخلات والمخرجات (JSON Responses). -- بناء مخطط الجداول الأساسية لقاعدة البيانات (الهيكل متعدد المستأجرين - Multi-Tenant Prep). - -### المرحلة 3: نظام النشر التلقائي للسيرفر (`deploy.sh`) -- كتابة سكربت النشر الآمن لرفع التحديثات لـ Git وعمل Pull تلقائي وإعادة بناء التهيئات على السيرفر. -- إعداد البنية التحتية للمجلدات على السيرفر وربط الـ Subdomain. - ---- - -## ثانياً: معمارية مجلدات الخلفية المقترحة (Directory Structure) - -سنقوم بتنظيم كود الخلفية داخل مجلد `backend/` لحماية كود التطبيق وملف البيانات الحساسة (`.env`) من الوصول المباشر من المتصفح، حيث سيكون مجلد `public/` هو المجلد الوحيد المتاح للعامة والمربوط بالـ Document Root في Nginx. - -الهيكل المستهدف للمجلدات هو كالتالي: - -
- -``` -nabeh/ -├── backend/ -│ ├── app/ -│ │ ├── Controllers/ # متحكمات معالجة الطلبات -│ │ │ ├── BaseController.php -│ │ │ └── AuthController.php -│ │ ├── Models/ # موديلات قواعد البيانات -│ │ │ ├── BaseModel.php -│ │ │ └── Tenant.php -│ │ ├── Core/ # نواة التطبيق البرمجية -│ │ │ ├── Router.php -│ │ │ ├── Database.php -│ │ │ ├── Request.php -│ │ │ ├── Response.php -│ │ │ └── Env.php -│ │ └── Middlewares/ # فلاتر التحقق والوساطة -│ │ └── AuthMiddleware.php -│ ├── config/ # ملفات الإعدادات العامة -│ │ └── app.php -│ ├── public/ # المجلد الوحيد المتاح للمتصفح -│ │ └── index.php # نقطة الدخول الموحدة (Front Controller) -│ ├── .env # ملف البيئة الحساس (مخفي تماماً) -│ ├── .env.example -│ └── composer.json # لإدارة التحميل التلقائي PSR-4 وحزم التطبيق -``` - -
- ---- - -## ثالثاً: تصميم النواة البرمجية (Core Architecture) - -### 1. نقطة الدخول الموحدة `backend/public/index.php` -يقوم هذا الملف بتهيئة بيئة العمل، تحميل الملفات تلقائياً، قراءة متغيرات البيئة، ثم تمرير الطلب إلى الـ Router. - -### 2. قارئ البيئة الآمن `backend/app/Core/Env.php` -يقرأ ملف `.env` المتواجد خارج المجلد العام ويقوم بتحميل المتغيرات داخل `$_ENV` و `getenv()` بأمان تام. - -### 3. نظام توجيه الطلبات `backend/app/Core/Router.php` -نظام توجيه خفيف وقوي يدعم تعبيرات Regex، ويتيح تعريف مسارات من نوع GET و POST و PUT و DELETE وتمرير البارامترات للمتحكمات (Controllers) وتطبيق فلاتر الوساطة (Middlewares). - ---- - -## رابعاً: نظام النشر المؤتمت (`deploy.sh`) - -سنقوم بإنشاء سكربت النشر الذكي في الجذر الرئيسي للمشروع لتبسيط عملية الدفع والتحديث. سيقوم السكربت بالخطوات التالية بشكل تفاعلي وآمن: - -1. التأكد من كتابة تعليق الالتزام (Commit Message). -2. إضافة كافة التغييرات محلياً والدفع إلى الفرع الرئيسي على GitHub/GitLab. -3. الاتصال بالسيرفر عبر بروتوكول SSH بشكل آمن. -4. التوجه إلى مجلد المشروع على السيرفر وسحب الكود البرمجي المحدث (`git pull origin main`). -5. تطبيق أي أوامر تهيئة مثل تثبيت حزم الملحقات (`composer install`) أو تنظيف الكاش. - -محتوى سكربت `deploy.sh` المقترح: - -
- -```bash -#!/bin/bash - -# ========================================== -# سكريبت النشر الآلي وتحديث السيرفر - تطبيق نبيه -# ========================================== - -# 1. إعدادات السيرفر والمتغيرات الأساسية -SERVER_USER="hamza" # اسم مستخدم السيرفر -SERVER_IP="your-server-ip" # عنوان السيرفر -SERVER_PATH="/var/www/nabeh" # مسار المشروع على السيرفر -GIT_BRANCH="main" # الفرع الرئيسي للنشر - -echo "==========================================" -echo "🚀 بدء عملية النشر والتحديث لتطبيق (نبيه)..." -echo "==========================================" - -# 2. التأكد من حالة التغييرات المحلية -if [ -z "$(git status --porcelain)" ]; then - echo "ℹ️ لا توجد تغييرات برمجية غير محفوظة للنشر." -else - # طلب وصف للتحديثات (Commit Message) - echo "✍️ الرجاء إدخال رسالة الالتزام (Commit Message):" - read commit_msg - - if [ -z "$commit_msg" ]; then - commit_msg="Update: Automatic deployment via deploy.sh" - fi - - # الإضافة والالتزام البرمجي - git add . - git commit -m "$commit_msg" -fi - -# 3. الدفع إلى Git Remote -echo "📤 دفع التحديثات إلى المستودع البعيد (Git Push)..." -git push origin $GIT_BRANCH - -if [ $? -ne 0 ]; then - echo "❌ فشلت عملية الدفع البرمجي (Git Push). تم إلغاء النشر." - exit 1 -fi - -echo "✅ تم رفع الكود بنجاح إلى المستودع البرمجي." - -# 4. الاتصال بالسيرفر وتحديث الكود (Git Pull) -echo "🌐 الاتصال بالسيرفر وسحب الكود المحدث (Git Pull)..." -ssh ${SERVER_USER}@${SERVER_IP} << EOF - cd ${SERVER_PATH} - echo "📁 الانتقال إلى مجلد المشروع: ${SERVER_PATH}" - - echo "📥 سحب التحديثات من الفرع ${GIT_BRANCH}..." - git pull origin ${GIT_BRANCH} - - if [ -f "backend/composer.json" ]; then - echo "📦 تحديث الحزم البرمجية والتحميل التلقائي (Composer)..." - cd backend - composer install --no-dev --optimize-autoloader - cd .. - fi - - echo "🎉 تم تحديث السيرفر وتثبيت التحديثات بنجاح!" -EOF - -echo "==========================================" -echo "✨ تمت عملية النشر والتحديث بالكامل بنجاح!" -echo "==========================================" -``` - -
- ---- - -## خامساً: خطة إعداد الخادم والـ Subdomain - -لتشغيل نظام الـ Front Controller والمسارات الديناميكية بشكل متوافق مع **Nginx** وبدون الحاجة لملفات `.htaccess` الخاصة بـ Apache، يجب إعداد ملف تهيئة الـ Subdomain (مثال: `api.nabeh.sa` أو `app.nabeh.sa`) ليوجه الطلبات إلى المجلد العام `backend/public/`: - -إعداد Nginx المقترح للمخدم: - -
- -```nginx -server { - listen 80; - listen [::]:80; - - server_name api.nabeh.sa; # النطاق الفرعي للتطبيق - - # تحديد مسار المجلد العام فقط - root /var/www/nabeh/backend/public; - index index.php index.html; - - charset utf-8; - - # توجيه كافة الطلبات إلى index.php لدعم الـ Router - location / { - try_files $uri $uri/ /index.php?$query_string; - } - - location = /favicon.ico { access_log off; log_not_found off; } - location = /robots.txt { access_log off; log_not_found off; } - - error_page 404 /index.php; - - # تشغيل ملفات PHP وتكاملها مع PHP-FPM - location ~ \.php$ { - fastcgi_pass unix:/var/run/php/php8.2-fpm.sock; # حسب إصدار PHP المثبت - fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; - include fastcgi_params; - } - - # منع الوصول تماماً لأي ملفات حساسة أو مخفية - location ~ /\.(?!well-known).* { - deny all; - } -} -``` - -
- ---- - -## سادساً: خطة التحقق والتدقيق (Verification Plan) - -### التحقق الذاتي والمحلي: -1. **التحقق من المسارات (Router Testing)**: إنشاء طلبات محلية للتحقق من تشغيل المسارات المختلفة (GET/POST) وإرجاع بيانات JSON صحيحة. -2. **التحقق من حماية متغيرات البيئة (`.env` Security)**: محاولة استعراض ملف `.env` عبر المسار المباشر للتأكد من حظر الوصول العام إليه، وقراءته بنجاح من داخل تطبيق PHP. -3. **فحص التحميل التلقائي**: التحقق من استدعاء كلاسات الـ Controllers والموديلات دون الحاجة لكتابة `require_once` يدوياً لكل ملف. - ---- - -## المرحلة الرابعة: نظام التوثيق والمصادقة (Authentication Phase) - -بناءً على النواة الأمنية التي تم تجهيزها (JWT, AES-256-GCM, Bcrypt, HMAC Blind Index)، سنقوم ببناء نظام المصادقة ليكون جاهزاً لإدارة جلسات المستخدمين. - -### Proposed Changes - -#### [NEW] `backend/app/Models/User.php` -- كلاس يمتد من `BaseModel` لإدارة جدول `users`. -- يحتوي على دالة `findByEmail` التي تقوم بتوليد (Blind Index HMAC) للبريد الإلكتروني للبحث السريع في قاعدة البيانات دون فك التشفير. -- دالة `createSecure` لإنشاء مستخدم جديد مع تشفير بريده الإلكتروني وكلمة مروره. - -#### [NEW] `backend/app/Controllers/AuthController.php` -- دالة `login`: تستقبل البريد الإلكتروني وكلمة المرور، تتحقق منها عبر الموديل، وتولد JWT Token. -- دالة `register` (اختيارية كأداة تطوير مبدئية أو للأدمن): إنشاء حساب موظف جديد. -- دالة `me`: جلب بيانات المستخدم الحالي باستخدام الـ `AuthMiddleware` لاختبار سلامة الجلسة. - -#### [MODIFY] `backend/public/index.php` -- إضافة المسارات الخاصة بنظام التوثيق: - - `POST /api/auth/login` - - `GET /api/auth/me` (مرفق بـ `AuthMiddleware`) - -### User Review Required +## User Review Required > [!IMPORTANT] -> - هل ترغب بإنشاء مسار `POST /api/auth/register` مفتوح للجميع لإنشاء حسابات شركات جديدة؟ أم نكتفي حالياً بإنشاء مستخدم مدير (Admin) افتراضي يدوياً أو عبر سكربت ليكون النظام مغلقاً للشركات المعتمدة فقط؟ -> - في عملية تسجيل الدخول، هل نحتاج لإرجاع بيانات الشركة المرتبطة بالمستخدم ضمن نفس الـ Response، أم نكتفي بإرجاع التوكن (Token) وبيانات المستخدم الأساسية فقط؟ +> - **API Key Configuration**: We will authenticate the REST endpoints using a shared `ENTALEQ_API_KEY` defined in the `.env` file. Please ensure this environment variable is populated on your environment before calling these endpoints. +> - **Direct Upload Path**: Decoded WhatsApp images will be saved under the public directory `backend/public/uploads/documents/` to ensure they are accessible via public URL paths. +> - **Prompt Dialect**: The custom service prompt is written in a professional, warm Syrian dialect (اللهجة السورية) adhering to all business rules. --- -## المرحلة الخامسة: معالجة وإصلاح الثغرات الأمنية (Security Audit Remediation) +## Proposed Changes -بناءً على التقرير الأمني الصادر، سنقوم بتطبيق التعديلات البرمجية لرفع مستوى أمان الخادم وحماية البيانات. +### Database Schema Component -### Proposed Changes +#### [NEW] [DriverOcrData.php](file:///Users/hamzaaleghwairyeen/development/App/nabeh/backend/app/Models/DriverOcrData.php) +- Class extending `BaseModel` that manages the `driver_ocr_data` table. +- Dynamically creates the table via `ensureTableExists()`. +- Implements `saveSecure` to encrypt sensitive fields (`phone`, `name`, `password`, and all JSON OCR strings) using AES-256-GCM (`Security::encrypt()`) and generate search hashes (`Security::blindIndex()`). +- Implements `findByPhone` to retrieve and decrypt records. -#### [MODIFY] [Security.php](file:///Users/hamzaaleghwairyeen/development/App/nabeh/backend/app/Core/Security.php) -- إرجاع خطأ (Exception) فوراً في حال عدم وجود متغيرات البيئة (`ENCRYPTION_KEY`, `HMAC_SALT`, `JWT_SECRET`) لمنع تشفير البيانات بمفاتيح فارغة. +--- -#### [MODIFY] [BaseModel.php](file:///Users/hamzaaleghwairyeen/development/App/nabeh/backend/app/Models/BaseModel.php) -- تنظيف وفلترة أسماء الأعمدة ديناميكياً باستخدام مصفوفة بيضاء (Whitelist Regex) لمنع ثغرات الـ SQL Injection عبر أسماء الأعمدة في دالتي `create()` و `update()`. +### Gemini Service Component -#### [MODIFY] [SecurityMiddleware.php](file:///Users/hamzaaleghwairyeen/development/App/nabeh/backend/app/Middlewares/SecurityMiddleware.php) -- إلغاء تطبيق `htmlspecialchars` و `strip_tags` على المدخلات الخام القادمة للخلفية لمنع تلف كلمات المرور والرموز الخاصة، وتأجيل التنظيف ليكون حصراً عند الطباعة/العرض (Output Encoding). +#### [MODIFY] [GeminiService.php](file:///Users/hamzaaleghwairyeen/development/App/nabeh/backend/app/Services/GeminiService.php) +- Add the `generateOcrFromImage` static method to support custom structured document analysis. It forwards custom prompts (e.g. green card OCR instructions) and base64 inline images to the `gemini-2.0-flash-lite` model. -#### [MODIFY] [AuthController.php](file:///Users/hamzaaleghwairyeen/development/App/nabeh/backend/app/Controllers/AuthController.php) -- إزالة التنظيف المزدوج (Double Encoding) لأسماء المستخدمين والشركات. +--- -#### [MODIFY] [Router.php](file:///Users/hamzaaleghwairyeen/development/App/nabeh/backend/app/Core/Router.php) -- إخفاء مسار الملفات الحقيقي من رسائل الخطأ 404 و 500 وإظهار رسائل عامة لمنع تسريب بنية المجلدات (Information Disclosure). +### Dashboard Component -#### [MODIFY] [Response.php](file:///Users/hamzaaleghwairyeen/development/App/nabeh/backend/app/Core/Response.php) -- جعل رابط الـ CORS ديناميكياً يعتمد على متغير بيئة `ALLOWED_ORIGIN` بدلاً من فتح النطاق للجميع عبر `*`. +#### [MODIFY] [index.html](file:///Users/hamzaaleghwairyeen/development/App/nabeh/backend/public/index.html) +- Add the new prompt template button: ``. +- Add `intaleq` template case inside `loadPromptTemplate(type)` to load the full customer service system instruction prompt. -#### [MODIFY] [bootstrap.php](file:///Users/hamzaaleghwairyeen/development/App/nabeh/backend/app/bootstrap.php) -- تسجيل معالج أخطاء عام (`set_exception_handler` / `set_error_handler`) لتحويل الأخطاء الفادحة غير المعالجة إلى استجابات JSON نظيفة ومخفية في الإنتاج، مع تسجيل التفاصيل التقنية في الـ `error_log` فقط. +--- -#### [MODIFY] [Request.php](file:///Users/hamzaaleghwairyeen/development/App/nabeh/backend/app/Core/Request.php) -- تعريف الخصائص المعرفية (`$user_id`, `$company_id`, `$role`) بشكل صريح لتفادي مشاكل الخصائص الديناميكية (Dynamic Properties Deprecation) في إصدارات PHP 8.2+. +### Conversation Flow Component -#### [NEW] [RateLimitMiddleware.php](file:///Users/hamzaaleghwairyeen/development/App/nabeh/backend/app/Middlewares/RateLimitMiddleware.php) -- بناء فلتر مخصص للحد من معدل الطلبات (Rate Limiting) على مسارات تسجيل الدخول والتسجيل لحماية التطبيق من محاولات التخمين العنيف (Brute Force). +#### [NEW] [DriverRegistrationFlow.php](file:///Users/hamzaaleghwairyeen/development/App/nabeh/backend/app/Core/Flows/DriverRegistrationFlow.php) +- Implement a step-by-step driver registration state machine. +- Steps: + 1. `start`: Welcome message, request full name. + 2. `ask_name`: Save name, request password. + 3. `ask_password`: Validate password, request ID Front image. + 4. `id_front`: Run `id_front_sy` Gemini OCR prompt. Request ID Back. + 5. `id_back`: Run `id_back_sy` Gemini OCR prompt. Request Driving License Front. + 6. `driving_license_front`: Run `driving_license_sy_front` Gemini OCR prompt. Request Driving License Back. + 7. `driving_license_back`: Run `driving_license_sy_back` Gemini OCR prompt. Request Vehicle License Front. + 8. `vehicle_license_front`: Run `vehicle_license_sy_front` Gemini OCR prompt. Request Vehicle License Back. + 9. `vehicle_license_back`: Run `vehicle_license_sy_back` Gemini OCR prompt. Request Criminal Record (لا حكم عليه). + 10. `criminal_record`: Save document, update status to `ocr_completed`, save to `driver_ocr_data` table, and conclude. + +#### [MODIFY] [ConversationFlowEngine.php](file:///Users/hamzaaleghwairyeen/development/App/nabeh/backend/app/Core/Flows/ConversationFlowEngine.php) +- Add trigger mappings: `'تسجيل' => 'driver_registration_flow'`, `'سجل' => 'driver_registration_flow'`, and `'register' => 'driver_registration_flow'`. +- Register the flow class: `'driver_registration_flow' => DriverRegistrationFlow::class`. + +--- + +### REST API Component #### [MODIFY] [index.php](file:///Users/hamzaaleghwairyeen/development/App/nabeh/backend/public/index.php) -- تفعيل فلتر الحد من معدل الطلبات على مسارات الحماية. +- Define 2 REST API endpoints: + - `GET /api/external/driver-ocr`: Accepts a query parameter `phone`. Authenticates against `ENTALEQ_API_KEY`. Retrieves and decrypts driver documents, OCR logs, and password details. + - `POST /api/external/register-driver`: Accepts a `phone` body parameter. Authenticates against `ENTALEQ_API_KEY`. Marks the local record status as `registered` (confirming transfer to primary DB). -### User Review Required +--- -> [!IMPORTANT] -> هل نعتمد نظام تخزين المحاولات للحد من معدل الطلبات (Rate Limiting) في ملفات مؤقتة داخل مجلد `storage/` محلي (سهل الإعداد ومستقل)، أم نستخدم جدولاً مخصصاً في قاعدة البيانات؟ - -
+## Verification Plan +### Manual Verification +1. Open the dashboard, click **قالب خدمة عملاء انطلق (سوري)**, and verify the prompt loads. +2. Send a WhatsApp message to the bot saying `"تسجيل"` and follow the registration wizard. Send test images for each stage, verifying database storage and Gemini OCR parsing logs. +3. Query the `GET /api/external/driver-ocr` endpoint using `curl` with the header `X-API-Key: your_key` and confirm decrypted data is returned. +4. Execute `POST /api/external/register-driver` with the API key header and confirm the record status updates to `registered`. diff --git a/registerDriverAndCarService.php b/registerDriverAndCarService.php new file mode 100644 index 0000000..72a0c13 --- /dev/null +++ b/registerDriverAndCarService.php @@ -0,0 +1,237 @@ +beginTransaction(); + logStep(1, "Transaction started via beginTransaction()"); + + // --- 2. Recolección de Datos (Conductor + Coche) --- + $phone = filterRequest("phone"); + $password = filterRequest("password"); + $firstName = filterRequest("first_name"); + $lastName = filterRequest("last_name"); + + // تسجيل البيانات المبدئية (بدون كلمات المرور) للتأكد من وصولها + logStep(2, "Inputs received -> Phone: $phone, Name: $firstName $lastName"); + + // التحقق من الحقول الإجبارية + if (empty($phone) || empty($password) || empty($firstName) || empty($lastName)) { + throw new Exception("Required fields missing (phone, password, first_name, last_name)."); + } + + // --- 3. Generar ID de Conductor --- + $driverId = substr(md5($phone), 0, 20); + logStep(3, "Driver ID generated: $driverId"); + + // --- 4. Procesamiento de Datos del Conductor --- + $password_hashed = password_hash($password, PASSWORD_DEFAULT); + $email = filterRequest("email"); + + if (empty($email) || $email === 'Not specified') { + $email = $phone . '@intaleqapp.com'; + } + + $nameArabic = $firstName . ' ' . $lastName; + $site = filterRequest("site"); + $address = $site; + + // بيانات إضافية + $gender = filterRequest("gender"); + $license_type = filterRequest("license_type"); + $nationalNumber = filterRequest("national_number"); + $issue_date = filterRequest("issue_date"); + $expiry_date = filterRequest("expiry_date"); + $licenseCategories = filterRequest("license_categories"); + $licenseIssueDate = filterRequest("license_issue_date"); + $birthdate = filterRequest("birthdate"); + $maritalStatus = filterRequest("maritalStatus"); + + // --- 5. Recolección de Datos del Coche --- + $owner = filterRequest("owner"); + $color = filterRequest("color"); + $colorHex = filterRequest("color_hex"); + $model = filterRequest("model"); + $carPlate = filterRequest("car_plate"); + $make = filterRequest("make"); + $fuel = filterRequest("fuel"); + $year = filterRequest("year"); + $vin = filterRequest("vin"); + + if (empty($vin)) { + $vin = 'unknown'; + } + + $carExpirationDate = filterRequest("expiration_date"); + + logStep(4, "Data processing completed. Car Plate: $carPlate, VIN: $vin"); + + // --- 6. Cifrado de Datos --- + try { + $encryptedPhone = $encryptionHelper->encryptData($phone); + $encryptedEmail = $encryptionHelper->encryptData($email); + $encryptedFirstName = $encryptionHelper->encryptData($firstName); + $encryptedLastName = $encryptionHelper->encryptData($lastName); + $encryptedNameArabic = $encryptionHelper->encryptData($nameArabic); + $encryptedGender = $encryptionHelper->encryptData($gender); + $encryptedNationalNumber = $encryptionHelper->encryptData($nationalNumber); + $encryptedAddress = $encryptionHelper->encryptData($address); + $encryptedSite = $encryptionHelper->encryptData($site); + $encryptedBirthdate = $encryptionHelper->encryptData($birthdate); + $encryptedOwner = $encryptionHelper->encryptData($owner); + $encryptedCarPlate = $encryptionHelper->encryptData($carPlate); + + logStep(5, "Encryption successful for sensitive fields."); + } catch (Exception $encEx) { + throw new Exception("Encryption Error: " . $encEx->getMessage()); + } + + // --- 7. Comprobación de Duplicados --- + // ملاحظة: إذا كان التشفير عشوائياً، فلن يجد التكرار هنا. + $dup = $con->prepare("SELECT id FROM driver WHERE phone = :phone OR email = :email OR national_number = :national_number"); + $dup->execute([':phone' => $encryptedPhone, ':email' => $encryptedEmail, ':national_number' =>$encryptedNationalNumber]); + + if ($dup->rowCount() > 0) { + logStep(6, "Duplicate found! Phone or Email or encryptedNationalNumber already exists."); + throw new Exception("Phone or email already registered."); + } + logStep(6, "No duplicates found. Proceeding."); + + // --- 8. INSERCIÓN 1: Tabla 'driver' --- + $sqlDriver = " + INSERT INTO driver ( + id, phone, email, password, gender, license_type, national_number, + name_arabic, issue_date, expiry_date, license_categories, + address, licenseIssueDate, status, birthdate, site, + first_name, last_name, accountBank, bankCode, + employmentType, maritalStatus, fullNameMaritial, expirationDate, + created_at, updated_at + ) VALUES ( + :id, :phone, :email, :pwd, :gender, :license_type, :national_number, + :name_arabic, :issue_date, :expiry_date, :license_categories, + :address, :licenseIssueDate, :status, :birthdate, :site, + :first_name, :last_name, :accountBank, :bankCode, + :employmentType, :maritalStatus, :fullNameMaritial, :expirationDate, + NOW(), NOW() + ) + "; + + $stmtDriver = $con->prepare($sqlDriver); + + // تم توحيد المفاتيح لتشمل النقطتين (:) + $driverData = [ + ':id' => $driverId, + ':phone' => $encryptedPhone, + ':email' => $encryptedEmail, + ':pwd' => $password_hashed, + ':gender' => $encryptedGender, + ':license_type' => $license_type, + ':national_number' => $encryptedNationalNumber, + ':name_arabic' => $encryptedNameArabic, + ':issue_date' => $issue_date, + ':expiry_date' => $expiry_date, + ':license_categories' => $licenseCategories ?? 'B', + ':address' => $encryptedAddress, + ':licenseIssueDate' => $licenseIssueDate, + ':status' => 'actives', + ':birthdate' => $encryptedBirthdate, + ':site' => $encryptedSite, + ':first_name' => $encryptedFirstName, + ':last_name' => $encryptedLastName, + ':accountBank' => 'yet', + ':bankCode' => 'yet', + ':employmentType' => $maritalStatus ?? 'yet', + ':maritalStatus' => $maritalStatus ?? 'yet', + ':fullNameMaritial' => 'yet', + ':expirationDate' => 'yet', + ]; + + if (!$stmtDriver->execute($driverData)) { + // تسجيل خطأ SQL بالتفصيل + $errInfo = $stmtDriver->errorInfo(); + throw new Exception("Driver Insert Failed: " . $errInfo[2]); + } + logStep(7, "Driver table insert successful."); + + // --- 9. INSERCIÓN 2: Tabla 'CarRegistration' --- + $sqlCar = " + INSERT INTO CarRegistration ( + driverID, vin, owner, color, color_hex, model, car_plate, + make, fuel, `year`, expiration_date, created_at + ) VALUES ( + :driverId, :vin, :owner, :color, :color_hex, :model, :car_plate, + :make, :fuel, :year, :expiration_date, NOW() + ) + "; + + $stmtCar = $con->prepare($sqlCar); + $carData = [ + ':driverId' => $driverId, + ':vin' => $vin, + ':owner' => $encryptedOwner, + ':color' => $color, + ':color_hex' => $colorHex, + ':model' => $model, + ':car_plate' => $encryptedCarPlate, + ':make' => $make, + ':fuel' => $fuel, + ':year' => $year, + ':expiration_date' => $carExpirationDate + ]; + + if (!$stmtCar->execute($carData)) { + $errInfo = $stmtCar->errorInfo(); + throw new Exception("Car Insert Failed: " . $errInfo[2]); + } + logStep(8, "CarRegistration insert successful."); + + // --- 10. Confirmar Transacción --- + $con->commit(); + logStep(9, "COMMIT successful. Sending Success Response."); + + jsonSuccess(["driverId" => $driverId, "message" => "Driver and car registered successfully."]); + + // --- 11. Enviar Notificación (خارج المعاملة يفضل، ولكن هنا كما في الكود الأصلي) --- + try { + $supportPhones = ['0952475740', '0952475742']; + $randomIndex = array_rand($supportPhones); + $phoneToUse = $supportPhones[$randomIndex]; + $randomNumber = rand(1000, 999999); + + $messageBody = "أهلاً وسهلاً كابتن $firstName 👋\n" + . "تم تفعيل حسابك على تطبيق *انطلق*.\n" + . "يمكنك الآن تسجيل الدخول والبدء بالعمل مباشرة.\n" + . "للمساعدة تواصل معنا على الرقم: $phoneToUse\n" + . "نتمنى لك عمل موفق 🚖\n\n" + . "معرف الرسالة: $randomNumber"; + + sendWhatsAppFromServer($phone, $messageBody); + logStep(10, "WhatsApp notification sent."); + } catch (Exception $waError) { + // لا نوقف العملية إذا فشل الواتساب، فقط نسجل الخطأ + logStep(10, "WhatsApp Warning: " . $waError->getMessage()); + } + +} catch (PDOException $e) { + $con->rollBack(); + $errorMsg = "Database Error (PDO): " . $e->getMessage(); + logStep("ERROR-PDO", $errorMsg); + // إظهار رسالة عامة للمستخدم، وتسجيل التفاصيل في السيرفر + jsonError("System error during registration. Please contact support."); +} catch (Exception $e) { + // إذا كانت المعاملة مفتوحة، قم بإلغائها + if ($con->inTransaction()) { + $con->rollBack(); + } + $errorMsg = "General Error: " . $e->getMessage(); + logStep("ERROR-GEN", $errorMsg); + jsonError($e->getMessage()); +} +?> \ No newline at end of file diff --git a/updateDriverToActive.php b/updateDriverToActive.php new file mode 100644 index 0000000..23b5a25 --- /dev/null +++ b/updateDriverToActive.php @@ -0,0 +1,150 @@ +beginTransaction(); + +try { + // --- 1. معالجة وتشفير البيانات --- + $nameArabic = $firstName . ' ' . $lastName; + $address = $site; + + // تشفير الحقول الحساسة + $encryptedFirstName = $encryptionHelper->encryptData($firstName); + $encryptedLastName = $encryptionHelper->encryptData($lastName); + $encryptedSite = $encryptionHelper->encryptData($site); + $encryptedAddress = $encryptionHelper->encryptData($address); + $encryptedNameArabic = $encryptionHelper->encryptData($nameArabic); + $encryptedNationalNumber = $encryptionHelper->encryptData($nationalNumber); + $encryptedOwner = $encryptionHelper->encryptData($owner); + $encryptedCarPlate = $encryptionHelper->encryptData($carPlate); + $encryptedBirthdate = $encryptionHelper->encryptData($birthdate); + $encryptedGender = $encryptionHelper->encryptData($gender); + + // --- 2. تحديث جدول السائق --- + $sqlDriver = "UPDATE `driver` SET + `first_name` = :first_name, + `last_name` = :last_name, + `site` = :site, + `address` = :address, + `national_number` = :national_number, + `license_categories` = :license_categories, + `expiry_date` = :expiry_date, + `issue_date` = :issue_date, + `gender` = :gender, + `birthdate` = :birthdate, + `name_arabic` = :name_arabic, + `maritalStatus` = :maritalStatus, + `status` = 'actives' + WHERE `id` = :driverId"; + + $stmtDriver = $con->prepare($sqlDriver); + $stmtDriver->execute([ + ':first_name' => $encryptedFirstName, + ':last_name' => $encryptedLastName, + ':site' => $encryptedSite, + ':address' => $encryptedAddress, + ':national_number' => $encryptedNationalNumber, + ':license_categories' => $licenseCategories, + ':expiry_date' => $expiryDate, + ':issue_date' => $licenseIssueDate, + ':gender' => $encryptedGender, + ':birthdate' => $encryptedBirthdate, + ':name_arabic' => $encryptedNameArabic, + ':driverId' => $driverId, + ':maritalStatus' =>$maritalStatus + ]); + + // --- 3. تحديث جدول السيارة --- + $sqlCar = "UPDATE `CarRegistration` SET + `owner` = :owner, + `color` = :color, + `color_hex` = :color_hex, + `model` = :model, + `car_plate` = :car_plate, + `make` = :make, + `fuel` = :fuel, + `year` = :year, + `expiration_date` = :expiration_date + WHERE `driverID` = :driverId"; + + $stmtCar = $con->prepare($sqlCar); + $stmtCar->execute([ + ':owner' => $encryptedOwner, + ':color' => $color, + ':color_hex' => $colorHex, + ':model' => $model, + ':car_plate' => $encryptedCarPlate, + ':make' => $make, + ':fuel' => $fuel, + ':year' => $year, + ':expiration_date' => $carExpirationDate, + ':driverId' => $driverId + ]); + + // --- 4. تأكيد المعاملة --- + $con->commit(); + jsonSuccess(["message" => "Driver and car data updated successfully."]); + + // --- 5. إرسال رسالة واتساب مبسطة وآمنة (باختيار رقم عشوائي) --- + + // 5.1. تعريف الأرقام + $supportPhones = ['0952475740', '0952475742']; // يمكنك إضافة المزيد من الأرقام هنا + + // 5.2. اختيار رقم عشوائي من القائمة + $randomIndex = array_rand($supportPhones); // يختار "مفتاح" عشوائي (index) + $phoneToUse = $supportPhones[$randomIndex]; // يحصل على الرقم من المفتاح + + + // --- !!! التعديل: إضافة رقم عشوائي --- + // هذا يضيف رقم عشوائي (4-6 خانات) لجعل الرسالة فريدة + $randomNumber = rand(1000, 999999); + + // 5.5. إعداد نص الرسالة بالرقم المتغير + $messageBody = "أهلاً وسهلاً كابتن $firstName 👋\n" + . "تم تفعيل حسابك على تطبيق *انطلق*.\n" + . "يمكنك الآن تسجيل الدخول والبدء بالعمل مباشرة.\n" + . "للمساعدة تواصل معنا على الرقم: $phoneToUse\n" // <-- تم استخدام المتغير العشوائي هنا + . "نتمنى لك عمل موفق 🚖\n\n" + . "معرف الرسالة: $randomNumber"; // <-- إضافة الرقم العشوائي + + // 5.6. إرسال الرسالة + sendWhatsAppFromServer($phone, $messageBody); + +} catch (Exception $e) { + // --- 6. التراجع في حال الخطأ --- + $con->rollBack(); + jsonError("An error occurred: " . $e->getMessage()); +} + +?> +