Compare commits
3 Commits
c4d0cc8c50
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0c7f39ff6 | ||
|
|
9a4d610bdd | ||
|
|
61e8996ba8 |
@@ -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 "⏳ تم استلام معلومات الدفع. يجري التحقق منها يدوياً.";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
240
backend/app/Core/Flows/ComplaintFlow.php
Normal file
240
backend/app/Core/Flows/ComplaintFlow.php
Normal file
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core\Flows;
|
||||
|
||||
use App\Services\SiroService;
|
||||
|
||||
/**
|
||||
* ComplaintFlow — Submit a trip complaint via WhatsApp
|
||||
*
|
||||
* Flow: start → await_description → await_ride → await_confirmation → finished
|
||||
*
|
||||
* Steps:
|
||||
* start → resolve user, ask for problem description
|
||||
* await_description → collect description text (or voice transcription)
|
||||
* await_ride → fetch recent rides, let user pick one
|
||||
* await_confirmation → show full details, ask to confirm
|
||||
* finished → show AI analysis result
|
||||
*/
|
||||
class ComplaintFlow extends BaseFlow
|
||||
{
|
||||
public function handleStep(string $step, array $messageData, array &$context): FlowResult
|
||||
{
|
||||
$phone = $messageData['phone'] ?? '';
|
||||
$text = $messageData['body'] ?? $messageData['text'] ?? '';
|
||||
$country = $context['country'] ?? SiroService::detectCountry($phone);
|
||||
|
||||
switch ($step) {
|
||||
// ─────────────────────────────────────────────────
|
||||
// START: resolve user, ask for description
|
||||
// ─────────────────────────────────────────────────
|
||||
case 'start':
|
||||
$context['country'] = $country;
|
||||
|
||||
// Resolve user type via Siro
|
||||
try {
|
||||
$driverData = SiroService::checkDriverStatus($phone, $country);
|
||||
if ($driverData && !empty($driverData['data']['driver_id'])) {
|
||||
$context['user_type'] = 'driver';
|
||||
$context['user_id'] = $driverData['data']['driver_id'];
|
||||
$context['user_name'] = $driverData['data']['name'] ?? '';
|
||||
} else {
|
||||
$context['user_type'] = 'passenger';
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$context['user_type'] = 'driver';
|
||||
}
|
||||
|
||||
return new FlowResult(
|
||||
"أهلاً بك في نظام الشكاوى.\n\n"
|
||||
. "📝 يرجى وصف المشكلة التي حدثت بالتفصيل.\n"
|
||||
. "مثال: \"السائق تأخر 20 دقيقة واتصلت فيه وما رد\"\n"
|
||||
. "يمكنك إرسال النص أو تسجيل مقطع صوتي.\n\n"
|
||||
. "🟡 للخروج اكتب: إلغاء",
|
||||
"await_description"
|
||||
);
|
||||
|
||||
// ─────────────────────────────────────────────────
|
||||
// AWAIT_DESCRIPTION: collect complaint text
|
||||
// ─────────────────────────────────────────────────
|
||||
case 'await_description':
|
||||
if (empty(trim($text))) {
|
||||
return new FlowResult(
|
||||
"الرجاء كتابة وصف المشكلة أو تسجيل مقطع صوتي:",
|
||||
"await_description"
|
||||
);
|
||||
}
|
||||
|
||||
$context['complaint_text'] = trim($text);
|
||||
|
||||
// Fetch recent rides from Siro
|
||||
try {
|
||||
$rides = SiroService::getUserRides($country, $phone, 5);
|
||||
$context['rides'] = $rides ?? [];
|
||||
} catch (\Exception $e) {
|
||||
$context['rides'] = [];
|
||||
}
|
||||
|
||||
if (empty($context['rides'])) {
|
||||
return new FlowResult(
|
||||
"تم حفظ وصف المشكلة. ✅\n\n"
|
||||
. "لم نتمكن من العثور على رحلات حديثة لحسابك.\n"
|
||||
. "الرجاء إرسال رقم الرحلة (مثال: 831):",
|
||||
"await_ride"
|
||||
);
|
||||
}
|
||||
|
||||
$rideList = "تم حفظ وصف المشكلة. ✅\n\n"
|
||||
. "🚖 آخر رحلاتك:\n\n";
|
||||
foreach ($context['rides'] as $i => $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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -181,7 +188,21 @@ class ConversationFlowEngine
|
||||
// Generate the audio voice note
|
||||
$audioData = \App\Services\GeminiService::generateAudioResponse(
|
||||
$apiKey,
|
||||
"أنت خدمة تسجيل كباتن تطبيق انطلق، تتحدث بلهجة سورية ودودة ومرحبة ومهنية جداً كأنك إنسان حقيقي.",
|
||||
"أنت سارة، مساعدة فريق خدمة العملاء في تطبيق سيرو (Siro) — أول منصة نقل ذكية وبنية تحتية رقمية للتنقل في سوريا والأردن ومصر. تتحدثين بلهجة سورية دافئة ومحببة ومهنية جداً كأنك إنسان حقيقي.
|
||||
|
||||
معلومات عن سيرو:
|
||||
- التطبيق: سيرو (Siro) — من تطوير سيرو لنقل الركاب
|
||||
- العمولة: 11% فقط (أقل عمولة في السوق السوري)
|
||||
- التحميل: أندروید https://play.google.com/store/apps/details?id=com.Siro.siro | آيفون https://apps.apple.com/app/id6748075179
|
||||
- طرق الشحن: سيريتيل كاش، شام كاش، المحفظة الداخلية
|
||||
- أنواع الرحلات: 10 أنواع (مريح، سريع، سائقات، سكوتر، فان، VIP، اقتصاد، سعر ثابت، رايح جاي، كهربائي)
|
||||
- المميزات: خرائط مملوكة SiroMaps، ترخيص حكومي NANS، 4 تطبيقات متكاملة، توثيق بالذكاء الاصطناعي، زر SOS، رحلات للسائقات
|
||||
- الأمان: JWT + بصمة جهاز، تشفير AES-256-GCM، حماية من الاحتيال
|
||||
- البلدان: سوريا (شغال)، مصر (جاهز)، الأردن (قريباً)
|
||||
- الدعم: support@intaleqapp.com | support@siromove.com
|
||||
|
||||
ملاحظة: إذا المستخدم كتب بالإنجليزية، ردي بالإنجليزية. إذا كتب بالعربية، ردي بالعربية (اللهجة السورية).",
|
||||
$result->getReplyText(),
|
||||
$result->getReplyText(),
|
||||
'Puck',
|
||||
$elApiKey,
|
||||
|
||||
@@ -2,75 +2,193 @@
|
||||
|
||||
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
|
||||
*
|
||||
* 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
|
||||
{
|
||||
$companyId = $context['company_id'] ?? 1;
|
||||
$phone = $messageData['phone'] ?? '';
|
||||
$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
|
||||
// ─────────────────────────────────────────────────
|
||||
case 'start':
|
||||
$context['retry_count'] = 0;
|
||||
|
||||
return new FlowResult(
|
||||
"أهلاً بك كابتن. يرجى إرسال صورة **إيصال التحويل المالي أو وصل الدفع** لكي نقوم بمراجعته وإضافته لحسابك:",
|
||||
"awaiting_receipt"
|
||||
"أهلاً بك في خدمة التحقق من الدفع.\n\n"
|
||||
. "📍 الدولة: {$countryName}\n"
|
||||
. "💰 طريقة الدفع: {$methodName}\n"
|
||||
. "💵 العملة: {$currency}\n\n"
|
||||
. "📸 يرجى إرسال صورة واضحة لإيصال الدفع أو صورة الشاشة.\n"
|
||||
. "سيتم التحقق من الفاتورة المعلقة تلقائياً.\n\n"
|
||||
. "🟡 للخروج اكتب: إلغاء\n"
|
||||
. "⏰ للتأجيل اكتب: بعدين",
|
||||
"await_receipt"
|
||||
);
|
||||
|
||||
case 'awaiting_receipt':
|
||||
if (empty($messageData['image']) || empty($messageData['imageMimeType'])) {
|
||||
return new FlowResult("الرجاء إرسال صورة وصل الدفع بوضوح للاستمرار، أو اكتب 'إلغاء' للخروج:", "awaiting_receipt");
|
||||
// ─────────────────────────────────────────────────
|
||||
// AWAIT_RECEIPT
|
||||
// ─────────────────────────────────────────────────
|
||||
case 'await_receipt':
|
||||
// ── No image sent ──
|
||||
if (empty($image)) {
|
||||
return new FlowResult(
|
||||
"📸 يرجى إرسال صورة الإيصال أو وصل التحويل.\n"
|
||||
. "تأكد من أن الصورة واضحة وتظهر المبلغ وتفاصيل التحويل.",
|
||||
"await_receipt"
|
||||
);
|
||||
}
|
||||
|
||||
if ($companyId !== 1) {
|
||||
if (!\App\Models\CompanySubscriptionUsage::hasRemainingLimit($companyId, 'ocr')) {
|
||||
return new FlowResult("⚠️ عذراً، تجاوز المتجر الحد المسموح لمعالجة الصور لهذا الشهر.", "finished", true);
|
||||
// ── 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"
|
||||
);
|
||||
}
|
||||
|
||||
$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);
|
||||
// ── Normalize MIME type ──
|
||||
if (strpos($imageMimeType, ';') !== false) {
|
||||
$imageMimeType = trim(explode(';', $imageMimeType)[0]);
|
||||
}
|
||||
|
||||
$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]);
|
||||
// ── 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);
|
||||
}
|
||||
|
||||
$replyText = GeminiService::generateResponseFromImage($apiKey, $imageSystemPrompt, $messageData['image'], $mimeType);
|
||||
$retry = ($context['retry_count'] ?? 0) + 1;
|
||||
$context['retry_count'] = $retry;
|
||||
|
||||
if ($companyId !== 1) {
|
||||
\App\Models\CompanySubscriptionUsage::incrementUsage($companyId, 'ocr');
|
||||
\App\Models\CompanySubscriptionUsage::incrementUsage($companyId, 'request');
|
||||
if ($retry >= self::MAX_RETRIES) {
|
||||
return new FlowResult(
|
||||
"عذراً، تعذر التحقق من الدفع بعد {$retry} محاولات.\n"
|
||||
. "سيتم مراجعة العملية من قبل الإدارة.",
|
||||
"finished",
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
"لم نتمكن من التحقق من الدفع حالياً (محاولة {$retry} من " . self::MAX_RETRIES . ").\n"
|
||||
. "يرجى إرسال صورة أوضح والإضاءة جيدة:",
|
||||
"await_receipt"
|
||||
);
|
||||
|
||||
return new FlowResult("لم أتمكن من التعرف على بيانات الإيصال المرفق. يرجى التأكد من وضوح الصورة وإعادة المحاولة.", "awaiting_receipt");
|
||||
case 'postponed':
|
||||
// Resume from postponed
|
||||
$step = $context['previous_step'] ?? 'await_receipt';
|
||||
return new FlowResult(
|
||||
"مرحباً بك مرة أخرى! 👋\n"
|
||||
. "📸 أرسل صورة إيصال الدفع للمتابعة:",
|
||||
$step
|
||||
);
|
||||
|
||||
default:
|
||||
return new FlowResult("حدث خطأ في المسار.", "finished", true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if user wants to postpone (keyword-based, no AI call)
|
||||
*/
|
||||
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,
|
||||
];
|
||||
|
||||
$normalized = trim(mb_strtolower($text));
|
||||
foreach ($keywords as $kw => $hours) {
|
||||
if (mb_strpos($normalized, $kw) !== false) {
|
||||
return $hours;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,14 +564,23 @@ $router->get('/api/external/cron/send-reminders', function ($request, $response)
|
||||
}
|
||||
|
||||
$nameStr = $driverName ? " كابتن " . $driverName : " كابتن";
|
||||
$reminderMsg = "أهلاً بك{$nameStr}، حابين نذكرك تكمل خطوات تسجيلك لتنضم لعائلة انطلق 🚖. بقية الأوراق كتير مهمة لنفعل حسابك ونبدأ سوا. بانتظار إرسالها!";
|
||||
$reminderMsg = "أهلاً بك{$nameStr}، حابين نذكرك تكمل خطوات تسجيلك لتنضم لعائلة سيرو 🚖. باقي الأوراق مهمة لنفعل حسابك ونبدأ سوا. بانتظار إرسالها!";
|
||||
|
||||
// Generate Audio voice note if key is present
|
||||
$audioData = null;
|
||||
if (!empty($geminiKey)) {
|
||||
$audioData = \App\Services\GeminiService::generateAudioResponse(
|
||||
$geminiKey,
|
||||
"أنت روبوت خدمة العملاء لشركة انطلق، تتحدث باللهجة السورية الودودة.",
|
||||
"أنت سارة، مساعدة فريق خدمة العملاء في تطبيق سيرو (Siro) لنقل الركاب، أوّل منصة نقل ذكية ورقمية في سوريا. تتحدثين باللهجة السورية الدافئة.
|
||||
|
||||
معلومات أساسية:
|
||||
- العمولة: 11% فقط (أقل عمولة بالسوق السوري)
|
||||
- طرق الشحن: سيريتيل كاش، شام كاش، المحفظة الداخلية
|
||||
- أنواع الرحلات: 10 أنواع (مريح، سريع، سائقات، سكوتر، فان، VIP، اقتصاد، سعر ثابت، رايح جاي)
|
||||
- المزايا: خرائط مملوكة SiroMaps، ترخيص حكومي NANS، توثيق بالذكاء الاصطناعي
|
||||
- الدعم: support@intaleqapp.com | support@siromove.com
|
||||
|
||||
ملاحظة: إذا المستخدم كتب بالإنجليزية، ردي بالإنجليزية. إذا كتب بالعربية، ردي بالعربية (اللهجة السورية).",
|
||||
$reminderMsg,
|
||||
'Puck',
|
||||
$elApiKey ?: null,
|
||||
|
||||
Reference in New Issue
Block a user