<< << << << << << '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); $configuredGeminiKey = ($rule && !empty($rule['gemini_api_key'])) ? $rule['gemini_api_key'] : null; $apiKey = GeminiService::getGeminiApiKey($configuredGeminiKey); 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); $configuredGeminiKey = ($rule && !empty($rule['gemini_api_key'])) ? $rule['gemini_api_key'] : null; $apiKey = GeminiService::getGeminiApiKey($configuredGeminiKey); 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; } }