Compare commits

..

3 Commits

Author SHA1 Message Date
Hamza-Ayed
e0c7f39ff6 Deploy: 2026-06-18 16:46:51 2026-06-18 16:46:51 +03:00
Hamza-Ayed
9a4d610bdd Deploy: 2026-06-18 15:04:52 2026-06-18 15:04:52 +03:00
Hamza-Ayed
61e8996ba8 Deploy: 2026-06-17 18:37:57 2026-06-17 18:37:57 +03:00
6 changed files with 591 additions and 103 deletions

View File

@@ -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 "⏳ تم استلام معلومات الدفع. يجري التحقق منها يدوياً.";
}
}

View 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);
}
}
}

View File

@@ -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,

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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,