diff --git a/backend/app/Core/Flows/ComplaintFlow.php b/backend/app/Core/Flows/ComplaintFlow.php new file mode 100644 index 0000000..595c12b --- /dev/null +++ b/backend/app/Core/Flows/ComplaintFlow.php @@ -0,0 +1,240 @@ + $r) { + $num = $i + 1; + $date = $r['date'] ?? ''; + $time = $r['time'] ?? ''; + $from = $r['start_location'] ?? '---'; + $to = $r['end_location'] ?? '---'; + $price = $r['price'] ?? '0'; + $status = $r['status'] ?? ''; + $rideList .= "{$num}. رحلة #{$r['id']} | {$date} {$time}\n" + . " من: {$from} → إلى: {$to}\n" + . " السعر: {$price} | الحالة: {$status}\n\n"; + } + $rideList .= "الرجاء إرسال رقم الرحلة من القائمة (1-{$num})\n" + . "أو اكتب رقم الرحلة كاملاً (مثال: 831):"; + + return new FlowResult($rideList, "await_ride"); + + // ───────────────────────────────────────────────── + // AWAIT_RIDE: let user pick a ride + // ───────────────────────────────────────────────── + case 'await_ride': + $selectedRide = null; + $rides = $context['rides'] ?? []; + + // Check if user entered a list number (1, 2, 3...) + $cleanNum = preg_replace('/[^0-9]/', '', $text); + if (!empty($cleanNum) && is_numeric($cleanNum)) { + $index = (int)$cleanNum - 1; + if (isset($rides[$index])) { + $selectedRide = $rides[$index]; + } + // If not found in list, try as ride_id directly + if (!$selectedRide) { + foreach ($rides as $r) { + if ((string)$r['id'] === $cleanNum) { + $selectedRide = $r; + break; + } + } + } + // If still not found, try the number as ride_id + if (!$selectedRide) { + $selectedRide = ['id' => $cleanNum]; + } + } + + if (!$selectedRide) { + return new FlowResult( + "لم نتعرف على الرقم. الرجاء إرسال رقم الرحلة من القائمة:", + "await_ride" + ); + } + + $context['ride_id'] = $selectedRide['id']; + $context['ride_details'] = $selectedRide; + + $locFrom = $selectedRide['start_location'] ?? '---'; + $locTo = $selectedRide['end_location'] ?? '---'; + $date = $selectedRide['date'] ?? '---'; + $time = $selectedRide['time'] ?? '---'; + $price = $selectedRide['price'] ?? '---'; + $status = $selectedRide['status'] ?? '---'; + + return new FlowResult( + "🚖 تفاصيل الرحلة المحددة:\n" + . "• رقم الرحلة: {$selectedRide['id']}\n" + . "• التاريخ: {$date} {$time}\n" + . "• من: {$locFrom}\n" + . "• إلى: {$locTo}\n" + . "• السعر: {$price}\n" + . "• الحالة: {$status}\n\n" + . "📋 وصف المشكلة:\n" + . "{$context['complaint_text']}\n\n" + . "هل تريد تأكيد إرسال الشكوى؟\n" + . "✅ أرسل: تأكيد\n" + . "🔄 أرسل: تعديل (لإعادة كتابة الوصف)\n" + . "🟡 أرسل: إلغاء", + "await_confirmation" + ); + + // ───────────────────────────────────────────────── + // AWAIT_CONFIRMATION: confirm and submit + // ───────────────────────────────────────────────── + case 'await_confirmation': + $clean = trim(mb_strtolower($text)); + + if (in_array($clean, ['تعديل', 'edit', 'تعديل الوصف'])) { + return new FlowResult( + "الرجاء إرسال وصف المشكلة الجديد:", + "await_description" + ); + } + + if (!in_array($clean, ['تأكيد', 'نعم', 'اكيد', 'ok', 'yes', 'confirm', 'تاكيد', 'okay'])) { + return new FlowResult( + "❌ لم يتم التأكيد.\n" + . "✅ للتأكيد أرسل: تأكيد\n" + . "🔄 للتعديل أرسل: تعديل\n" + . "🟡 للإلغاء أرسل: إلغاء", + "await_confirmation" + ); + } + + // Submit complaint via Siro + try { + $result = SiroService::submitComplaint( + $country, + $phone, + $context['ride_id'], + $context['complaint_text'] + ); + + if ($result && ($result['status'] ?? '') === 'success') { + $aiResult = $result['ai_result'] ?? []; + $report = $result['report'] ?? []; + $reportTitle = $report['title'] ?? ''; + $reportBody = $report['body'] ?? ''; + + $reply = "✅ تم إرسال شكواك بنجاح!\n\n" + . "📋 نتيجة التحليل:\n" + . "• تصنيف الشكوى: " . ($aiResult['complaint_type'] ?? '---') . "\n" + . "• الطرف المخطئ: " . ($aiResult['fault_determination'] ?? '---') . "\n" + . "• طبيعة الشكوى: " . ($aiResult['complaint_nature'] ?? '---') . "\n\n"; + + if ($reportBody) { + $reply .= "📄 {$reportTitle}\n{$reportBody}\n\n"; + } + + $reply .= "سيتم التواصل معك من قبل فريق الدعم إذا لزم الأمر."; + + return new FlowResult($reply, "finished", true); + } + + return new FlowResult( + "⚠️ حدث خطأ في إرسال الشكوى. يرجى المحاولة مرة أخرى أو التواصل مع الدعم الفني.", + "finished", + true + ); + } catch (\Exception $e) { + error_log("[ComplaintFlow] Submit error: " . $e->getMessage()); + return new FlowResult( + "⚠️ تعذر إرسال الشكوى حالياً. يرجى المحاولة لاحقاً.", + "finished", + true + ); + } + + default: + return new FlowResult("حدث خطأ في المسار.", "finished", true); + } + } +} diff --git a/backend/app/Core/Flows/ConversationFlowEngine.php b/backend/app/Core/Flows/ConversationFlowEngine.php index ed88915..4e40108 100644 --- a/backend/app/Core/Flows/ConversationFlowEngine.php +++ b/backend/app/Core/Flows/ConversationFlowEngine.php @@ -18,6 +18,7 @@ class ConversationFlowEngine 'test_flow' => TestFlow::class, 'driver_registration_flow' => DriverRegistrationFlow::class, 'payment_flow' => PaymentFlow::class, + 'complaint_flow' => ComplaintFlow::class, ]; /** @@ -33,6 +34,12 @@ class ConversationFlowEngine 'وصل' => 'payment_flow', 'تسديد' => 'payment_flow', 'رصيد' => 'payment_flow', + 'شكوى' => 'complaint_flow', + 'مشكلة' => 'complaint_flow', + 'بلاغ' => 'complaint_flow', + 'تظلم' => 'complaint_flow', + 'شكوي' => 'complaint_flow', + 'complaint' => 'complaint_flow', ]; /** diff --git a/backend/app/Core/Flows/PaymentFlow.php b/backend/app/Core/Flows/PaymentFlow.php index c94fa8e..fd88633 100644 --- a/backend/app/Core/Flows/PaymentFlow.php +++ b/backend/app/Core/Flows/PaymentFlow.php @@ -9,62 +9,160 @@ use App\Services\SiroService; * * Flow: start → await_receipt → finished * - * Automatically: - * • Detects country from phone prefix (963→Syria, 962→Jordan) - * • Sets payment method (Syria→shamcash, Jordan→cliq) - * • Forwards raw receipt image to payment server for AI verification - * • Payment server auto-finds the latest pending invoice by phone - * → no need for the user to type an invoice number + * Smart features: + * • Auto-detects country + payment method (shamcash/cliq) + * • Auto-finds pending invoice by phone (no invoice number needed) + * • AI receipt verification via payment server Gemini + * • Postponement detection (keyword-based) + * • Image validation + retry (max 3 attempts) + * • Currency-aware messages (SYP/JOD) */ class PaymentFlow extends BaseFlow { + private const MAX_RETRIES = 3; + public function handleStep(string $step, array $messageData, array &$context): FlowResult { $phone = $messageData['phone'] ?? ''; - $text = $messageData['body'] ?? $messageData['text'] ?? ''; + $text = isset($messageData['body']) ? trim($messageData['body']) : ''; $image = $messageData['image'] ?? ''; $imageMimeType = $messageData['imageMimeType'] ?? 'image/jpeg'; + // ── Postponement check (only if flow is active, not on start/finished) ── + if ($step !== 'start' && $step !== 'finished' && !empty($text)) { + $postpone = $this->detectPostponement($text); + if ($postpone !== null) { + $context['previous_step'] = $step; + $hours = $postpone; + return new FlowResult( + "حاضر كابتن، تم تأجيل طلب الدفع. سأذكرك بعد {$hours} ساعات.\n" + . "للمتابعة لاحقاً، أرسل 'دفع' مرة أخرى.", + "postponed" + ); + } + } + + // ── Country + method detection ── + $country = $context['country'] ?? SiroService::detectCountry($phone); + $context['country'] = $country; + + $paymentMethod = $context['payment_method'] ?? match ($country) { + 'jordan' => 'cliq', + default => 'shamcash', + }; + $context['payment_method'] = $paymentMethod; + + $methodName = $paymentMethod === 'cliq' ? 'كليك (Cliq)' : 'شام كاش (ShamCash)'; + $currency = $paymentMethod === 'cliq' ? 'دينار أردني' : 'ل.س'; + $countryName = match ($country) { + 'jordan' => 'الأردن', + 'egypt' => 'مصر', + default => 'سوريا', + }; + switch ($step) { // ───────────────────────────────────────────────── - // START: detect country, set method, ask for receipt + // START // ───────────────────────────────────────────────── case 'start': - $country = SiroService::detectCountry($phone); - $paymentMethod = match ($country) { - 'jordan' => 'cliq', - default => 'shamcash', - }; - - $context['payment_method'] = $paymentMethod; - $context['country'] = $country; - $context['user_type'] = 'driver'; - - $methodName = match ($paymentMethod) { - 'cliq' => 'كليك (Cliq)', - default => 'شام كاش (ShamCash)', - }; + $context['retry_count'] = 0; return new FlowResult( - "أهلاً بك. للتحقق من عملية الدفع:\n" - . "💰 طريقة الدفع المتوقعة: {$methodName}\n" - . "📍 الدولة: " . ($country === 'syria' ? 'سوريا' : ($country === 'jordan' ? 'الأردن' : $country)) - . "\n\n📸 يرجى إرسال صورة الإيصال أو وصل التحويل للتحقق منه.", + "أهلاً بك في خدمة التحقق من الدفع.\n\n" + . "📍 الدولة: {$countryName}\n" + . "💰 طريقة الدفع: {$methodName}\n" + . "💵 العملة: {$currency}\n\n" + . "📸 يرجى إرسال صورة واضحة لإيصال الدفع أو صورة الشاشة.\n" + . "سيتم التحقق من الفاتورة المعلقة تلقائياً.\n\n" + . "🟡 للخروج اكتب: إلغاء\n" + . "⏰ للتأجيل اكتب: بعدين", "await_receipt" ); // ───────────────────────────────────────────────── - // AWAIT_RECEIPT: collect receipt image + // AWAIT_RECEIPT // ───────────────────────────────────────────────── case 'await_receipt': + // ── No image sent ── if (empty($image)) { return new FlowResult( - "الرجاء إرسال صورة واضحة لوصل الدفع أو صورة الشاشة:", + "📸 يرجى إرسال صورة الإيصال أو وصل التحويل.\n" + . "تأكد من أن الصورة واضحة وتظهر المبلغ وتفاصيل التحويل.", "await_receipt" ); } - return $this->sendToVerification($phone, $context, $image, $imageMimeType); + // ── Validate image size ── + $decoded = base64_decode($image, true); + if ($decoded === false || strlen($decoded) < 1024) { + $retry = ($context['retry_count'] ?? 0) + 1; + $context['retry_count'] = $retry; + + if ($retry >= self::MAX_RETRIES) { + return new FlowResult( + "عذراً، لم نتمكن من قراءة الصورة بعد {$retry} محاولات.\n" + . "يرجى التواصل مع خدمة العملاء لإتمام عملية الدفع يدوياً.", + "finished", + true + ); + } + + return new FlowResult( + "⚠️ الصورة غير واضحة أو صغيرة جداً.\n" + . "يرجى إرسال صورة واضحة وحجم أكبر (محاولة {$retry} من " . self::MAX_RETRIES . "):", + "await_receipt" + ); + } + + // ── Normalize MIME type ── + if (strpos($imageMimeType, ';') !== false) { + $imageMimeType = trim(explode(';', $imageMimeType)[0]); + } + + // ── Send to payment server ── + $companyId = $context['company_id'] ?? 1; + + $result = \App\Controllers\WhatsAppController::verifyPaymentSlipStatic( + companyId: $companyId, + phone: $phone, + jsonStr: '', + userType: 'driver', + paymentMethod: $paymentMethod, + invoiceNumber: '', + receiptImage: $image, + imageMimeType: $imageMimeType, + ); + + if ($result) { + return new FlowResult($result, "finished", true); + } + + $retry = ($context['retry_count'] ?? 0) + 1; + $context['retry_count'] = $retry; + + if ($retry >= self::MAX_RETRIES) { + return new FlowResult( + "عذراً، تعذر التحقق من الدفع بعد {$retry} محاولات.\n" + . "سيتم مراجعة العملية من قبل الإدارة.", + "finished", + true + ); + } + + return new FlowResult( + "لم نتمكن من التحقق من الدفع حالياً (محاولة {$retry} من " . self::MAX_RETRIES . ").\n" + . "يرجى إرسال صورة أوضح والإضاءة جيدة:", + "await_receipt" + ); + + case 'postponed': + // Resume from postponed + $step = $context['previous_step'] ?? 'await_receipt'; + return new FlowResult( + "مرحباً بك مرة أخرى! 👋\n" + . "📸 أرسل صورة إيصال الدفع للمتابعة:", + $step + ); default: return new FlowResult("حدث خطأ في المسار.", "finished", true); @@ -72,41 +170,25 @@ class PaymentFlow extends BaseFlow } /** - * Forward receipt image + phone to payment server for AI verification. - * Payment server internally resolves phone→driverID via Siro backend. - * Nabeh only makes ONE API call (to payment server). + * Detect if user wants to postpone (keyword-based, no AI call) */ - private function sendToVerification( - string $phone, - array &$context, - string $image, - string $imageMimeType - ): FlowResult { - $companyId = $context['company_id'] ?? 1; + private function detectPostponement(string $text): ?int + { + $keywords = [ + 'بعدين' => 2, 'بكرا' => 12, 'بكرة' => 12, 'بعد' => 3, + 'شوي' => 1, 'مشغول' => 4, 'تأجيل' => 6, 'لاحقاً' => 6, + 'لاحقا' => 6, 'الحق' => 6, 'وقت ثاني' => 8, 'تعبان' => 6, + 'بعدين برسل' => 3, 'بعدين ببعت' => 3, 'بعدين بكمل' => 4, + 'ببعثها' => 3, 'ببعت' => 3, 'برسل' => 2, + ]; - if (strpos($imageMimeType, ';') !== false) { - $imageMimeType = trim(explode(';', $imageMimeType)[0]); + $normalized = trim(mb_strtolower($text)); + foreach ($keywords as $kw => $hours) { + if (mb_strpos($normalized, $kw) !== false) { + return $hours; + } } - $result = \App\Controllers\WhatsAppController::verifyPaymentSlipStatic( - companyId: $companyId, - phone: $phone, - jsonStr: '', - userType: $context['user_type'] ?? 'driver', - paymentMethod: $context['payment_method'] ?? 'shamcash', - invoiceNumber: '', - receiptImage: $image, - imageMimeType: $imageMimeType, - ); - - if ($result) { - return new FlowResult($result, "finished", true); - } - - return new FlowResult( - "لم نتمكن من التحقق من الدفع حالياً. يرجى المحاولة مرة أخرى أو التواصل مع الدعم الفني.", - "finished", - true - ); + return null; } } diff --git a/backend/app/Services/SiroService.php b/backend/app/Services/SiroService.php index 7bf183f..750a667 100644 --- a/backend/app/Services/SiroService.php +++ b/backend/app/Services/SiroService.php @@ -517,4 +517,78 @@ EOT, default => $syriaPrompts, }; } + + /** + * Fetch recent rides for a user from Siro + */ + public static function getUserRides(string $country, string $phone, int $limit = 5): ?array + { + $apiUrl = self::getApiUrl($country); + $apiKey = getenv('NABEH_API_KEY') ?: ''; + $ridesUrl = $apiUrl . '/nabeh/get_user_rides.php'; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $ridesUrl); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ + 'phone' => $phone, + 'limit' => $limit, + ])); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'X-API-Key: ' . $apiKey, + 'Content-Type: application/json', + ]); + curl_setopt($ch, CURLOPT_TIMEOUT, 15); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + + $res = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200) { + error_log("[SiroService] getUserRides failed: HTTP {$httpCode} - {$res}"); + return null; + } + + $data = json_decode($res, true); + return $data['rides'] ?? $data['data'] ?? null; + } + + /** + * Submit a complaint with AI analysis via Siro + */ + public static function submitComplaint(string $country, string $phone, string $rideId, string $complaintText): ?array + { + $apiUrl = self::getApiUrl($country); + $apiKey = getenv('NABEH_API_KEY') ?: ''; + $complaintUrl = $apiUrl . '/nabeh/submit_complaint.php'; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $complaintUrl); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ + 'phone' => $phone, + 'ride_id' => $rideId, + 'complaint_text' => $complaintText, + ])); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'X-API-Key: ' . $apiKey, + 'Content-Type: application/json', + ]); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + + $res = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200) { + error_log("[SiroService] submitComplaint failed: HTTP {$httpCode} - {$res}"); + return null; + } + + return json_decode($res, true); + } }