From 9a4d610bdd9c2256fd5db4cffafbe53bc0a8fcd0 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Thu, 18 Jun 2026 15:04:52 +0300 Subject: [PATCH] Deploy: 2026-06-18 15:04:52 --- .../app/Controllers/WhatsAppController.php | 148 ++++++++++-------- backend/app/Core/Flows/PaymentFlow.php | 136 ++++++++++------ 2 files changed, 173 insertions(+), 111 deletions(-) diff --git a/backend/app/Controllers/WhatsAppController.php b/backend/app/Controllers/WhatsAppController.php index bf0daaf..4b7bbaa 100644 --- a/backend/app/Controllers/WhatsAppController.php +++ b/backend/app/Controllers/WhatsAppController.php @@ -742,88 +742,114 @@ class WhatsAppController extends BaseController } /** - * Call external API to verify payment slip + * Verify payment — sends data to payment server for AI verification or status query. + * + * Two modes: + * 1. AI Verification (receiptImage given): sends phone + invoice_number + receipt_image + * to payment server, which runs GeminiAi::verifyPayment(). + * 2. Status Query (only jsonStr): parses {transaction_id, amount, method} from Gemini + * chatbot auto-detection, sends as status query to payment server. */ - public static function verifyPaymentSlipStatic(int $companyId, string $phone, string $jsonStr): ?string + public static function verifyPaymentSlipStatic( + int $companyId, + string $phone, + string $jsonStr, + string $userType = 'driver', + string $paymentMethod = '', + string $invoiceNumber = '', + string $receiptImage = '', + string $imageMimeType = 'image/jpeg' + ): ?string { try { - $data = json_decode($jsonStr, true); - if (!$data) { - return null; - } - - $transactionId = $data['transaction_id'] ?? ''; - $amount = $data['amount'] ?? ''; - $method = $data['method'] ?? ''; - - if (empty($transactionId) || empty($amount)) { - return null; - } - - // Find configured endpoint for verify_payment - $endpoint = \App\Models\CompanyEndpoint::findByAction($companyId, 'verify_payment'); - $apiUrl = $endpoint ? $endpoint['endpoint_url'] : null; - + $apiUrl = getenv('PAYMENT_API_URL') ?: getenv('ENTALEQ_PAYMENT_API_URL'); if (empty($apiUrl)) { - // Fallback to local default mock - $apiUrl = getenv('ENTALEQ_PAYMENT_API_URL'); - if (empty($apiUrl)) { - $appUrl = rtrim(getenv('APP_URL') ?: 'https://nabeh.intaleqapp.com', '/'); - $apiUrl = $appUrl . '/api/external/verify-payment'; - } + $appUrl = rtrim(getenv('APP_URL') ?: 'https://nabeh.intaleqapp.com', '/'); + $apiUrl = $appUrl . '/api/external/verify-payment'; } - $payload = json_encode([ - 'phone' => $phone, - 'transaction_id' => $transactionId, - 'amount' => $amount, - 'method' => $method - ]); + if (strpos($apiUrl, '/ride/nabeh/verify_payment.php') === false + && strpos($apiUrl, '/api/external/verify-payment') === false) { + $apiUrl = rtrim($apiUrl, '/') . '/ride/nabeh/verify_payment.php'; + } - $headers = ['Content-Type: application/json']; - if ($endpoint) { - if (!empty($endpoint['api_key'])) { - $headers[] = 'X-API-Key: ' . $endpoint['api_key']; - $headers[] = 'Authorization: Bearer ' . $endpoint['api_key']; - } - if (!empty($endpoint['headers'])) { - $customHeaders = json_decode($endpoint['headers'], true); - if (is_array($customHeaders)) { - foreach ($customHeaders as $key => $value) { - $headers[] = "$key: $value"; - } - } - } + $apiKey = getenv('NABEH_API_KEY') ?: ''; + $headers = [ + 'Content-Type: application/json', + 'X-API-Key: ' . $apiKey, + ]; + + // ── Mode 1: AI Verification (receipt image → auto-find invoice by phone) ── + if (!empty($receiptImage)) { + $payload = json_encode([ + 'phone' => $phone, + 'payment_method' => $paymentMethod, + 'receipt_image' => $receiptImage, + 'image_mime_type' => $imageMimeType, + ]); } else { - $headers[] = 'X-API-Key: ' . (getenv('ENTALEQ_API_KEY') ?: 'mock-key'); + // ── Mode 2: Status query from chatbot auto-detection ────────────── + $data = json_decode($jsonStr, true); + if (!$data) { + return null; + } + + $transactionId = $data['transaction_id'] ?? ''; + $amount = $data['amount'] ?? ''; + $method = $data['method'] ?? $paymentMethod; + + if (empty($transactionId) && empty($amount) && empty($invoiceNumber)) { + return null; + } + + $payload = json_encode([ + 'phone' => $phone, + 'payment_method' => $method, + 'transaction_id' => $transactionId, + 'amount' => is_numeric($amount) ? (float) $amount : 0, + 'invoice_number' => $invoiceNumber, + ]); } $ch = curl_init($apiUrl); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_TIMEOUT => 30, + ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode !== 200) { - return "⏳ تم استلام وصل الدفع ويجري التحقق منه حالياً من قبل المحاسب يدوياً. سنقوم بشحن رصيدك وتنبيهك فور انتهاء العملية."; + error_log("[PaymentVerify] HTTP $httpCode from $apiUrl: $response"); + return "⏳ تم استلام معلومات الدفع ويجري التحقق منها يدوياً. سنقوم بالرد فور الانتهاء."; } $resData = json_decode($response, true); - if (isset($resData['status']) && $resData['status'] === 'success') { - $amtStr = $resData['data']['amount'] ?? $amount; - return "✅ تم التحقق من وصل الدفع تلقائياً بنجاح!\n• رقم العملية: " . $transactionId . "\n• القيمة: " . $amtStr . " دينار\n• تم تحديث رصيد حسابك بنجاح."; - } else { - $reason = $resData['message'] ?? 'العملية مسجلة مسبقاً أو غير صالحة'; - return "⚠️ لم نتمكن من تأكيد العملية تلقائياً:\n• السبب: " . $reason . "\n\nيجري الآن تحويل المعاملة للمراجعة اليدوية من قبل الإدارة وسنقوم بالرد عليك قريباً."; + if (!$resData) { + return "⏳ تم استلام معلومات الدفع. يجري التحقق منها يدوياً."; } + + if (($resData['status'] ?? '') === 'success') { + $verified = $resData['verified'] ?? false; + if ($verified) { + return "✅ تم التحقق من عملية الدفع تلقائياً بنجاح!\n" + . "• تم تحديث رصيد حسابك."; + } + + $reason = $resData['message'] + ?? $resData['ai_reason'] + ?? 'لم يتم التأكيد بعد'; + return "⚠️ " . $reason . "\n\nسيتم مراجعة العملية من قبل الإدارة والرد عليك قريباً."; + } + + return "⏳ تم استلام معلومات الدفع. يجري التحقق منها يدوياً."; } catch (\Exception $e) { - error_log("[Payment Verification Exception] " . $e->getMessage()); - return "⏳ تم استلام وصل الدفع بنجاح. يجري الآن مراجعته وتدقيقه يدوياً من قبل الإدارة الفنية لتأكيد شحن رصيدك."; + error_log("[PaymentVerify Exception] " . $e->getMessage()); + return "⏳ تم استلام معلومات الدفع. يجري التحقق منها يدوياً."; } } diff --git a/backend/app/Core/Flows/PaymentFlow.php b/backend/app/Core/Flows/PaymentFlow.php index 8111e48..c94fa8e 100644 --- a/backend/app/Core/Flows/PaymentFlow.php +++ b/backend/app/Core/Flows/PaymentFlow.php @@ -2,75 +2,111 @@ namespace App\Core\Flows; -use App\Services\GeminiService; -use App\Models\ChatbotRule; +use App\Services\SiroService; /** - * PaymentFlow - * Handles payment receipt uploads and verification. + * PaymentFlow — Smart Payment Verification + * + * 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 */ class PaymentFlow extends BaseFlow { public function handleStep(string $step, array $messageData, array &$context): FlowResult { - $companyId = $context['company_id'] ?? 1; $phone = $messageData['phone'] ?? ''; + $text = $messageData['body'] ?? $messageData['text'] ?? ''; + $image = $messageData['image'] ?? ''; + $imageMimeType = $messageData['imageMimeType'] ?? 'image/jpeg'; switch ($step) { + // ───────────────────────────────────────────────── + // START: detect country, set method, ask for receipt + // ───────────────────────────────────────────────── 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)', + }; + return new FlowResult( - "أهلاً بك كابتن. يرجى إرسال صورة **إيصال التحويل المالي أو وصل الدفع** لكي نقوم بمراجعته وإضافته لحسابك:", - "awaiting_receipt" + "أهلاً بك. للتحقق من عملية الدفع:\n" + . "💰 طريقة الدفع المتوقعة: {$methodName}\n" + . "📍 الدولة: " . ($country === 'syria' ? 'سوريا' : ($country === 'jordan' ? 'الأردن' : $country)) + . "\n\n📸 يرجى إرسال صورة الإيصال أو وصل التحويل للتحقق منه.", + "await_receipt" ); - case 'awaiting_receipt': - if (empty($messageData['image']) || empty($messageData['imageMimeType'])) { - return new FlowResult("الرجاء إرسال صورة وصل الدفع بوضوح للاستمرار، أو اكتب 'إلغاء' للخروج:", "awaiting_receipt"); + // ───────────────────────────────────────────────── + // AWAIT_RECEIPT: collect receipt image + // ───────────────────────────────────────────────── + case 'await_receipt': + if (empty($image)) { + return new FlowResult( + "الرجاء إرسال صورة واضحة لوصل الدفع أو صورة الشاشة:", + "await_receipt" + ); } - if ($companyId !== 1) { - if (!\App\Models\CompanySubscriptionUsage::hasRemainingLimit($companyId, 'ocr')) { - return new FlowResult("⚠️ عذراً، تجاوز المتجر الحد المسموح لمعالجة الصور لهذا الشهر.", "finished", true); - } - } - - $rule = ChatbotRule::findActiveForRule($companyId); - $configuredGeminiKey = ($rule && !empty($rule['gemini_api_key'])) ? $rule['gemini_api_key'] : null; - $apiKey = GeminiService::getGeminiApiKey($configuredGeminiKey); - - if (empty($apiKey)) { - return new FlowResult("عذراً، عطل فني في خادم معالجة الصور بالذكاء الاصطناعي. يرجى المحاولة لاحقاً.", "finished", true); - } - - $imageSystemPrompt = "أنت خبير في مراجعة إيصالات الدفع. استخرج البيانات التالية بدقة بالغة واكتبها بصيغة JSON محاطة بـ [PAYMENT_RECEIPT: { ... }] كالتالي:\n[PAYMENT_RECEIPT: {\"transaction_id\": \"رقم المعاملة أو الحوالة هنا\", \"amount\": \"المبلغ المستخرج كأرقام فقط\", \"method\": \"طريقة الدفع مثل Syriatel Cash أو Bemo Bank\"}]\nفي حال عدم وضوح الإيصال، أرجع JSON فارغًا."; - - $mimeType = $messageData['imageMimeType']; - if (strpos($mimeType, ';') !== false) { - $mimeType = trim(explode(';', $mimeType)[0]); - } - - $replyText = GeminiService::generateResponseFromImage($apiKey, $imageSystemPrompt, $messageData['image'], $mimeType); - - if ($companyId !== 1) { - \App\Models\CompanySubscriptionUsage::incrementUsage($companyId, 'ocr'); - \App\Models\CompanySubscriptionUsage::incrementUsage($companyId, 'request'); - } - - if (!empty($replyText) && preg_match('/\[PAYMENT_RECEIPT:\s*(\{.*?\})\]/s', $replyText, $matches)) { - $jsonStr = $matches[1]; - $verificationResult = \App\Controllers\WhatsAppController::verifyPaymentSlipStatic($companyId, $phone, $jsonStr); - - if ($verificationResult) { - return new FlowResult("تم فحص الإيصال:\n" . $verificationResult, "finished", true); - } else { - return new FlowResult("لم نتمكن من التأكد من بيانات الإيصال. يرجى إرسال صورة أوضح أو التواصل مع الدعم الفني.", "finished", true); - } - } - - return new FlowResult("لم أتمكن من التعرف على بيانات الإيصال المرفق. يرجى التأكد من وضوح الصورة وإعادة المحاولة.", "awaiting_receipt"); + return $this->sendToVerification($phone, $context, $image, $imageMimeType); default: return new FlowResult("حدث خطأ في المسار.", "finished", true); } } + + /** + * 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). + */ + private function sendToVerification( + string $phone, + array &$context, + string $image, + string $imageMimeType + ): FlowResult { + $companyId = $context['company_id'] ?? 1; + + if (strpos($imageMimeType, ';') !== false) { + $imageMimeType = trim(explode(';', $imageMimeType)[0]); + } + + $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 + ); + } }