Compare commits

..

5 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
Hamza-Ayed
c4d0cc8c50 feat: add telegram session management schema, update driver OCR model with syro driver ID, and update landing page 2026-06-17 18:23:56 +03:00
Hamza-Ayed
926d8bc4a5 Update Siro integration: NABEH_API_KEY, nabeh/ folder paths, /api/siro/ routes 2026-06-17 18:23:29 +03:00
10 changed files with 1457 additions and 252 deletions

View File

@@ -742,11 +742,53 @@ 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 {
$apiUrl = getenv('PAYMENT_API_URL') ?: getenv('ENTALEQ_PAYMENT_API_URL');
if (empty($apiUrl)) {
$appUrl = rtrim(getenv('APP_URL') ?: 'https://nabeh.intaleqapp.com', '/');
$apiUrl = $appUrl . '/api/external/verify-payment';
}
if (strpos($apiUrl, '/ride/nabeh/verify_payment.php') === false
&& strpos($apiUrl, '/api/external/verify-payment') === false) {
$apiUrl = rtrim($apiUrl, '/') . '/ride/nabeh/verify_payment.php';
}
$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 {
// ── Mode 2: Status query from chatbot auto-detection ──────────────
$data = json_decode($jsonStr, true);
if (!$data) {
return null;
@@ -754,76 +796,60 @@ class WhatsAppController extends BaseController
$transactionId = $data['transaction_id'] ?? '';
$amount = $data['amount'] ?? '';
$method = $data['method'] ?? '';
$method = $data['method'] ?? $paymentMethod;
if (empty($transactionId) || empty($amount)) {
if (empty($transactionId) && empty($amount) && empty($invoiceNumber)) {
return null;
}
// Find configured endpoint for verify_payment
$endpoint = \App\Models\CompanyEndpoint::findByAction($companyId, 'verify_payment');
$apiUrl = $endpoint ? $endpoint['endpoint_url'] : null;
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';
}
}
$payload = json_encode([
'phone' => $phone,
'payment_method' => $method,
'transaction_id' => $transactionId,
'amount' => $amount,
'method' => $method
'amount' => is_numeric($amount) ? (float) $amount : 0,
'invoice_number' => $invoiceNumber,
]);
$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";
}
}
}
} else {
$headers[] = 'X-API-Key: ' . (getenv('ENTALEQ_API_KEY') ?: 'mock-key');
}
$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

@@ -3,166 +3,72 @@
namespace App\Core\Flows;
use App\Services\GeminiService;
use App\Services\SiroService;
use App\Models\DriverOcrData;
use App\Models\ChatbotRule;
use App\Models\DriverReminder;
use App\Core\Database;
/**
* DriverRegistrationFlow
* Handles step-by-step driver and vehicle registration using Gemini OCR.
* Integrates with Siro platform for driver registration across Syria, Jordan, and Egypt.
*/
class DriverRegistrationFlow extends BaseFlow
{
private array $prompts = [
"id_front" => <<<EOT
You are an OCR expert for Syrian national ID cards (green card).
private array $prompts = [];
private string $country = 'syria';
### TASK
Analyse the **front side** of the ID and return **raw JSON only** with exactly these keys:
{
"full_name": "", // الاسم الثلاثي أو الرباعي
"national_number": "", // الرقم الوطني (LATIN digits only)
"dob": "YYYY-MM-DD", // تاريخ الميلاد
"address": "" // العنوان
}
### RULES
* Read the red number on the bottom of the card.
* Convert any Eastern-Arabic digits (٠١٢٣٤٥٦٧٨٩) to Western-Arabic digits (0-9).
* `national_number` must contain **Latin digits only, no spaces or other characters**.
* If a field is missing, set it to **null**.
* Convert the birth date to ISO `YYYY-MM-DD`.
* Return valid JSON only — no extra keys, no markdown.
EOT,
"id_back" => <<<EOT
أنت خبير OCR مختص ببطاقات الهوية السورية (الوجه الخلفي).
### المطلوب
حلّل صورة الوجه الخلفي للهوية السورية وأعد **JSON صِرف** يحتوي المفاتيح التالية فقط:
{
"governorate": "", // المحافظة (مثال: دمشق)
"address": "", // العنوان التفصيلي (حيّ، بلدة …)
"gender": "", //Male or Female
"issue_date": "YYYY-MM-DD"// تاريخ الإصدار بصيغة ISO
}
### القواعد
1. حوّل أي أرقام عربية شرقية (٠١٢٣٤٥٦٧٨٩) إلى أرقام لاتينية (0-9).
2. أعدّ تاريخ الإصدار بالتقويم الميلادي بصيغة `YYYY-MM-DD`.
3. استخدم أحرف لاتينية كبيرة لزمرة الدم مع رمز `+` أو `-` فقط.
4. إذا كان أحد الحقول غير موجود مطلقًا، أعد قيمته **null**.
5. لا تُرجع أي مفاتيح إضافية أو شروح أو Markdown — JSON صالح فقط.
EOT,
"driving_license_front" => <<<EOT
You are an OCR expert for Syrian documents.
### TASK
Analyse the **front side of a Syrian driving licence** and return **clean JSON only** with the following keys (no extra keys, no markdown):
{
"name_arabic": "", // الاسم الثلاثي أو الرباعي بالعربية
"birth_place": "", // المحافظة أو المنطقة المكتوبة بعد كلمة الولادة
"birth_year": "", // سنة الميلاد فقط (أربعة أرقام)
"national_number": "",
"civil_registry": "", // سطر "القيد" (مثال: سهوة 3)
"blood_type": "" // زمرة الدم بالشكل: A+ , A- , B+ , B- , AB+ , AB- , O+ , O-
}
### RULES
* إذا كانت القيمة مفقودة تمامًا اكتب **null**.
* لا تُغيّر ترتيب المفاتيح.
* لا تُرسل أى شرح أو أسطر إضافية JSON خالص فقط.
EOT,
"driving_license_back" => <<<EOT
You are an OCR expert for Syrian driving licences.
### TASK
Analyse the **back side** of a Syrian driving licence and return **raw JSON only** with exactly these keys:
{
"issue_date": "YYYY-MM-DD", // تاريخ المنح
"expiry_date": "YYYY-MM-DD", // صالحة لغاية
"license_number": "", // رقم الإجازة
"license_category": "" // D1, D2, D3 … (as printed after "UNIVERSAL DRIVING LICENCE")
}
### RULES
* If a value is totally absent, set it to **null**.
* Convert all dates to ISO `YYYY-MM-DD` (Gregorian).
* Do **NOT** add extra keys, comments, or markdown — return valid JSON only.
EOT,
"vehicle_license_front" => <<<EOT
You are an OCR expert specialized in analyzing Syrian vehicle registration cards (الرخصة البرتقالية).
Your task is to extract structured data from the **front side** of the Syrian orange vehicle card and return **raw JSON only** with the following exact fields:
{
"car_plate": "", // رقم المركبة الكامل مع اسم المحافظة، مأخوذ من الجهة اليسرى في السطر الأول (مثال: "155186 درعا")
"owner": "", // اسم المالك الكامل
"vin": "", // رقم الهيكل
"color": "", // اللون بالعربية أو الإنجليزية (مثال: "أبيض" أو "White")
"color_hex": "", // كود اللون بصيغة Hex (مثال: "#FFFFFF") أو #27332F إن تعذّر
"issue_date": "YYYY-MM-DD", // تاريخ المنح بصيغة ISO
"inspection_date": "YYYY-MM-DD" // تاريخ الفحص القادم بصيغة ISO
}
### Instructions & Rules:
1. Do **not** extract the "رمز المركبة" (on the right side of the first line) — use only the **left side** of the first line for `car_plate`.
2. Convert any Arabic dates (like `2024/05/13`) into ISO format `YYYY-MM-DD`.
3. If any value is missing or unreadable, return `null` for it.
4. Maintain Arabic encoding (e.g., owner name, city name, color).
5. Never guess — extract only what's visually found on the card.
6. Never include any explanation or extra output — return the JSON only.
Example of valid `car_plate`:
- "155186 درعا"
- "45291 دمشق"
- "122334 حمص"
EOT,
"vehicle_license_back" => <<<EOT
You are an OCR expert for Syrian vehicle registration cards (orange card).
### TASK
Analyse the **back side** of the card and return **raw JSON only** with exactly these keys (no more, no less):
{
"make": "", // الصانع (Hyundai …)
"model": "", // الطراز (H1 …)
"year": "", // سنة الصنع بالأرقام اللاتينية (e.g. "2019")
"fuel": "", // نوع الوقود (بنزين، ديزل …) أو بالإنجليزية (Petrol, Diesel,electric)
"chassis": "" // رقم الهيكل (VIN)
}
### RULES
* Convert any Eastern-Arabic digits (٠١٢٣٤٥٦٧٨٩) to Western digits (0-9).
* Normalise color names to standard English if possible, then map to a common Hex code
• "أبيض / White" → **#FFFFFF**
• "أسود / Black" → **#000000**
• "أحمر / Red" → **#FF0000**
• "أزرق / Blue" → **#0000FF**
• … (use the closest basic colour); if no match, set **color_hex = null**.
* If any field is unreadable or absent, set its value to **null**.
* Do **NOT** include extra keys, comments, or markdown — output valid JSON only.
EOT
private array $stepToDocType = [
'id_front' => 'id_front',
'id_back' => 'id_back',
'driving_license_front' => 'driver_license_front',
'driving_license_back' => 'driver_license_back',
'vehicle_license_front' => 'car_license_front',
'vehicle_license_back' => 'car_license_back',
'criminal_record' => 'criminal_record',
];
public function __construct()
{
$this->prompts = SiroService::getDocumentPrompts($this->country);
}
public function handleStep(string $step, array $messageData, array &$context): FlowResult
{
$text = isset($messageData['body']) ? trim($messageData['body']) : '';
$phone = $messageData['phone'];
$companyId = $context['company_id'] ?? 1;
// Detect country from phone number
$this->country = SiroService::detectCountry($phone);
$context['country'] = $this->country;
// Set country-specific prompts
$this->prompts = SiroService::getDocumentPrompts($this->country);
// Country name in Arabic for messages
$countryNames = [
'syria' => 'سوريا',
'jordan' => 'الأردن',
'egypt' => 'مصر',
];
$countryName = $countryNames[$this->country] ?? 'سوريا';
// App name based on country
$appNames = [
'syria' => 'سيرو',
'jordan' => 'سيرو',
'egypt' => 'سيرو',
];
$appName = $appNames[$this->country] ?? 'سيرو';
// If currently postponed and user sends a message, resume the flow
if ($step === 'postponed') {
// Cancel active reminder
$activeReminder = DriverReminder::findActive($companyId, $phone);
if ($activeReminder) {
DriverReminder::update($activeReminder['id'], ['status' => 'cancelled']);
}
// Restore previous step
$step = $context['previous_step'] ?? 'ask_name';
}
@@ -188,7 +94,6 @@ EOT
$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,
@@ -209,7 +114,7 @@ EOT
switch ($step) {
case 'start':
return new FlowResult(
"أهلاً بك كابتن في خدمة تسجيل كباتن تطبيق انطلق 🚖.\nيرجى إرسال اسمك الثلاثي الكامل للبدء:",
"أهلاً بك كابتن في خدمة تسجيل كباتن تطبيق {$appName} في {$countryName} 🚖.\nيرجى إرسال اسمك الثلاثي الكامل للبدء:",
"ask_name"
);
@@ -306,6 +211,17 @@ EOT
return new FlowResult("عذراً، فشل حفظ الصورة. الرجاء إعادة إرسال صورة الوثيقة:", "criminal_record");
}
// Upload criminal record to Siro
$fullPath = __DIR__ . '/../../../../public' . $imageUrl;
$criminalSiroUrl = SiroService::uploadDocument(
$this->country,
SiroService::formatPhone($phone, $this->country),
'criminal_record',
$fullPath,
$messageData['imageMimeType']
);
$context['criminal_record_siro_url'] = $criminalSiroUrl;
// Securely save registration data to local database
try {
DriverOcrData::saveSecure([
@@ -332,11 +248,83 @@ EOT
return new FlowResult("عذراً، حدث خطأ أثناء حفظ طلبك في قاعدة البيانات. يرجى المحاولة مرة أخرى لاحقاً.", "criminal_record");
}
// Register driver in Siro with Siro-hosted URLs
$docUrls = [
'id_front' => $context['id_front_siro_url'] ?? '',
'id_back' => $context['id_back_siro_url'] ?? '',
'driving_license_front' => $context['driving_license_front_siro_url'] ?? '',
'driving_license_back' => $context['driving_license_back_siro_url'] ?? '',
'vehicle_license_front' => $context['vehicle_license_front_siro_url'] ?? '',
'vehicle_license_back' => $context['vehicle_license_back_siro_url'] ?? '',
'criminal_record' => $criminalSiroUrl ?? '',
];
$idOcr = $context['id_front_ocr'] ?? [];
$vlOcr = $context['vehicle_license_front_ocr'] ?? [];
$vlbOcr = $context['vehicle_license_back_ocr'] ?? [];
$dlOcr = $context['driving_license_front_ocr'] ?? [];
$formattedPhone = SiroService::formatPhone($phone, $this->country);
$driverId = 'DRV' . date('YmdHis') . rand(100, 999);
$driverData = [
'phone' => $formattedPhone,
'password' => substr(md5($formattedPhone . time()), 0, 12),
'first_name' => explode(' ', $context['name'] ?? '')[0] ?? $context['name'],
'last_name' => implode(' ', array_slice(explode(' ', $context['name'] ?? ''), 1)) ?: $context['name'],
'name_arabic' => $context['name'] ?? '',
'national_number' => $idOcr['national_number'] ?? '',
'birthdate' => $idOcr['dob'] ?? '',
'address' => $idOcr['address'] ?? '',
'id' => $driverId,
];
$carData = [
'vin' => $vlbOcr['chassis'] ?? $vlOcr['vin'] ?? '',
'car_plate' => $vlOcr['car_plate'] ?? '',
'make' => $vlbOcr['make'] ?? '',
'model' => $vlbOcr['model'] ?? '',
'year' => $vlbOcr['year'] ?? '',
'color' => $vlOcr['color'] ?? '',
'color_hex' => $vlOcr['color_hex'] ?? '#000000',
'owner' => $vlOcr['owner'] ?? '',
'fuel' => $vlbOcr['fuel'] ?? '',
'expiration_date' => $vlOcr['issue_date'] ?? '',
'vehicle_category_id' => 1,
];
$syroResult = SiroService::registerDriver($driverData, $carData, $docUrls, $this->country);
$appNames = [
'syria' => 'سيرو',
'jordan' => 'سيرو',
'egypt' => 'سيرو',
];
$appName = $appNames[$this->country] ?? 'سيرو';
if ($syroResult && ($syroResult['status'] ?? '') === 'success') {
DriverOcrData::saveSecure([
'company_id' => $companyId,
'phone' => $phone,
'name' => $context['name'],
'status' => 'registered'
]);
return new FlowResult(
"شكراً لك كابتن، لقد تم استلام كافة المستندات والتحقق منها بنجاح. سيقوم فريق خدمة عملاء انطلق بمراجعة طلبك وتفعيل حسابك بأسرع وقت ممكن. يومك سعيد! 🚖",
"شكراً لك كابتن، تم تسجيلك بنجاح في تطبيق {$appName} 🚖✅\nسيتم مراجعة طلبك من قبل فريق الخدمة وتفعيل حسابك قريباً. يومك سعيد!",
"finished",
true
);
} else {
$errMsg = $syroResult['message'] ?? 'خطأ غير معروف';
error_log("[Registration Flow] Siro registration failed: " . json_encode($syroResult));
return new FlowResult(
"تم حفظ مستنداتك بنجاح في نظامنا. لكن حدث تأخير في تسجيلك على تطبيق {$appName}. سيقوم فريقنا بمراجعة بياناتك وتفعيل حسابك في أقرب وقت. شكراً لصبرك! 🙏",
"finished",
true
);
}
default:
return new FlowResult("خطأ في تحديد خطوة المسار.", "finished", true);
@@ -365,6 +353,23 @@ EOT
return new FlowResult("عذراً، فشل حفظ الصورة. الرجاء إعادة المحاولة وإرسال الصورة:", $step);
}
// Upload to Siro and store the signed URL
$fullPath = __DIR__ . '/../../../../public' . $imageUrl;
$siroUrl = SiroService::uploadDocument(
$this->country,
SiroService::formatPhone($messageData['phone'], $this->country),
$this->stepToDocType[$step] ?? $step,
$fullPath,
$messageData['imageMimeType']
);
if ($siroUrl) {
$context[$step . '_siro_url'] = $siroUrl;
error_log("[DriverRegistrationFlow] Uploaded {$step} to Siro: {$siroUrl}");
} else {
$context[$step . '_siro_url'] = null;
error_log("[DriverRegistrationFlow] Warning: Failed to upload {$step} to Siro, using local URL");
}
$companyId = $context['company_id'] ?? 1;
// Check subscription limit for OCR

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
);
}
$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);
return new FlowResult(
"⚠️ الصورة غير واضحة أو صغيرة جداً.\n"
. "يرجى إرسال صورة واضحة وحجم أكبر (محاولة {$retry} من " . self::MAX_RETRIES . "):",
"await_receipt"
);
}
$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]);
// ── Normalize MIME type ──
if (strpos($imageMimeType, ';') !== false) {
$imageMimeType = trim(explode(';', $imageMimeType)[0]);
}
$replyText = GeminiService::generateResponseFromImage($apiKey, $imageSystemPrompt, $messageData['image'], $mimeType);
// ── Send to payment server ──
$companyId = $context['company_id'] ?? 1;
if ($companyId !== 1) {
\App\Models\CompanySubscriptionUsage::incrementUsage($companyId, 'ocr');
\App\Models\CompanySubscriptionUsage::incrementUsage($companyId, 'request');
$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);
}
if (!empty($replyText) && preg_match('/\[PAYMENT_RECEIPT:\s*(\{.*?\})\]/s', $replyText, $matches)) {
$jsonStr = $matches[1];
$verificationResult = \App\Controllers\WhatsAppController::verifyPaymentSlipStatic($companyId, $phone, $jsonStr);
$retry = ($context['retry_count'] ?? 0) + 1;
$context['retry_count'] = $retry;
if ($verificationResult) {
return new FlowResult("تم فحص الإيصال:\n" . $verificationResult, "finished", true);
} else {
return new FlowResult("لم نتمكن من التأكد من بيانات الإيصال. يرجى إرسال صورة أوضح أو التواصل مع الدعم الفني.", "finished", true);
}
if ($retry >= self::MAX_RETRIES) {
return new FlowResult(
"عذراً، تعذر التحقق من الدفع بعد {$retry} محاولات.\n"
. "سيتم مراجعة العملية من قبل الإدارة.",
"finished",
true
);
}
return new FlowResult("لم أتمكن من التعرف على بيانات الإيصال المرفق. يرجى التأكد من وضوح الصورة وإعادة المحاولة.", "awaiting_receipt");
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);
}
}
/**
* 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

@@ -130,6 +130,7 @@ class DriverOcrData extends BaseModel
`vehicle_license_back_ocr` TEXT DEFAULT NULL,
`criminal_record_url` VARCHAR(512) DEFAULT NULL,
`status` VARCHAR(50) NOT NULL DEFAULT 'pending',
`syro_driver_id` VARCHAR(100) DEFAULT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `unique_company_driver_phone` (`company_id`, `phone_hash`),

View File

@@ -0,0 +1,594 @@
<?php
namespace App\Services;
/**
* SiroService
* Handles communication between Nabeh and the Siro ride-hailing platform APIs.
* Supports Syria, Jordan, and Egypt with country detection from phone numbers.
*/
class SiroService
{
private const COUNTRY_SYRIA = 'syria';
private const COUNTRY_JORDAN = 'jordan';
private const COUNTRY_EGYPT = 'egypt';
private static array $countryPrefixes = [
'963' => self::COUNTRY_SYRIA,
'00963' => self::COUNTRY_SYRIA,
'962' => self::COUNTRY_JORDAN,
'00962' => self::COUNTRY_JORDAN,
'20' => self::COUNTRY_EGYPT,
'0020' => self::COUNTRY_EGYPT,
];
private static array $countryPhonePrefixes = [
self::COUNTRY_SYRIA => '963',
self::COUNTRY_JORDAN => '962',
self::COUNTRY_EGYPT => '20',
];
private static array $countryEnvKeys = [
self::COUNTRY_SYRIA => 'SIRO_API_URL_SYRIA',
self::COUNTRY_JORDAN => 'SIRO_API_URL_JORDAN',
self::COUNTRY_EGYPT => 'SIRO_API_URL_EGYPT',
];
/**
* Detect country from phone number
*/
public static function detectCountry(string $phone): string
{
$cleaned = preg_replace('/[^0-9]/', '', $phone);
foreach (self::$countryPrefixes as $prefix => $country) {
if (strpos($cleaned, $prefix) === 0) {
return $country;
}
}
return self::COUNTRY_SYRIA;
}
/**
* Get country-specific API base URL
*/
public static function getApiUrl(string $country): string
{
$envKey = self::$countryEnvKeys[$country] ?? self::$countryEnvKeys[self::COUNTRY_SYRIA];
return rtrim(getenv($envKey) ?: 'https://api-syria.siromove.com/siro', '/');
}
/**
* Format phone number to international format for the given country
*/
public static function formatPhone(string $phone, string $country): string
{
$cleaned = preg_replace('/[^0-9]/', '', $phone);
$prefix = self::$countryPhonePrefixes[$country] ?? '963';
if (strpos($cleaned, $prefix) === 0) {
return $cleaned;
}
if (strpos($cleaned, '00' . $prefix) === 0) {
return $prefix . substr($cleaned, strlen($prefix) + 2);
}
if (strpos($cleaned, '0') === 0) {
return $prefix . substr($cleaned, 1);
}
return $prefix . $cleaned;
}
/**
* Upload a document image to Siro and return the signed URL
*/
public static function uploadDocument(
string $country,
string $driverId,
string $docType,
string $imagePath,
string $imageMimeType
): ?string {
$apiUrl = self::getApiUrl($country);
$apiKey = getenv('NABEH_API_KEY') ?: '';
$uploadUrl = rtrim($apiUrl, '/') . '/nabeh/upload_document.php';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $uploadUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
$headers = [
'X-API-Key: ' . $apiKey,
];
$body = [
'driver_id' => $driverId,
'doc_type' => $docType,
];
if (file_exists($imagePath)) {
$body['file'] = new \CURLFile($imagePath, $imageMimeType);
} else {
error_log("[SiroService] Image not found: {$imagePath}");
return null;
}
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
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] Upload failed for {$docType}: HTTP {$httpCode} - {$res}");
return null;
}
$data = json_decode($res, true);
$fileUrl = $data['message']['file_url'] ?? null;
if (!$fileUrl) {
error_log("[SiroService] Upload response missing file_url: {$res}");
}
return $fileUrl;
}
/**
* Register a driver in Siro via the simplified Nabeh integration endpoint
*/
public static function registerDriver(array $driverData, array $carData, array $documentUrls, string $country): ?array
{
$apiUrl = self::getApiUrl($country);
$apiKey = getenv('NABEH_API_KEY') ?: '';
$registerUrl = $apiUrl . '/nabeh/register.php';
$payload = array_merge($driverData, $carData, [
'id_front_url' => $documentUrls['id_front'] ?? '',
'id_back_url' => $documentUrls['id_back'] ?? '',
'driver_license_front_url' => $documentUrls['driving_license_front'] ?? '',
'driver_license_back_url' => $documentUrls['driving_license_back'] ?? '',
'vehicle_license_front_url' => $documentUrls['vehicle_license_front'] ?? '',
'vehicle_license_back_url' => $documentUrls['vehicle_license_back'] ?? '',
'criminal_record_url' => $documentUrls['criminal_record'] ?? '',
'profile_picture' => $documentUrls['profile_picture'] ?? '',
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $registerUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-API-Key: ' . $apiKey,
'Content-Type: application/json',
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
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] Registration failed: HTTP {$httpCode} - {$res}");
return null;
}
return json_decode($res, true);
}
/**
* Check driver registration status in Siro
*/
public static function checkDriverStatus(string $phone, string $country): ?array
{
$apiUrl = self::getApiUrl($country);
$apiKey = getenv('NABEH_API_KEY') ?: '';
$statusUrl = $apiUrl . '/nabeh/driver_status.php';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $statusUrl . '?phone=' . urlencode($phone));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-API-Key: ' . $apiKey,
]);
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) {
return null;
}
return json_decode($res, true);
}
/**
* Query Siro platform for driver info, trips, stats, or trip details
*/
public static function querySiro(string $country, array $params): ?array
{
$apiUrl = self::getApiUrl($country);
$apiKey = getenv('NABEH_API_KEY') ?: '';
$queryUrl = $apiUrl . '/nabeh/query.php';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $queryUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params));
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] Query failed: HTTP {$httpCode} - {$res}");
return null;
}
return json_decode($res, true);
}
/**
* Get OCR prompts for a specific country
*/
public static function getDocumentPrompts(string $country): array
{
$syriaPrompts = [
"id_front" => <<<EOT
You are an OCR expert for Syrian national ID cards (green card).
### TASK
Analyse the **front side** of the ID and return **raw JSON only** with exactly these keys:
{
"full_name": "",
"national_number": "",
"dob": "YYYY-MM-DD",
"address": ""
}
### RULES
* Read the red number on the bottom of the card.
* Convert any Eastern-Arabic digits to Western-Arabic digits.
* Return valid JSON only no extra keys, no markdown.
EOT,
"id_back" => <<<EOT
أنت خبير OCR مختص ببطاقات الهوية السورية (الوجه الخلفي).
### المطلوب
حلّل صورة الوجه الخلفي للهوية السورية وأعد **JSON صِرف**:
{
"governorate": "",
"address": "",
"gender": "",
"issue_date": "YYYY-MM-DD"
}
### القواعد
1. حوّل أي أرقام عربية شرقية إلى أرقام لاتينية.
2. لا تُرجع أي مفاتيح إضافية أو شروح.
EOT,
"driving_license_front" => <<<EOT
You are an OCR expert for Syrian documents.
### TASK
Analyse the **front side of a Syrian driving licence** and return **clean JSON only**:
{
"name_arabic": "",
"birth_place": "",
"birth_year": "",
"national_number": "",
"civil_registry": "",
"blood_type": ""
}
EOT,
"driving_license_back" => <<<EOT
You are an OCR expert for Syrian driving licences.
### TASK
Analyse the **back side** of a Syrian driving licence:
{
"issue_date": "YYYY-MM-DD",
"expiry_date": "YYYY-MM-DD",
"license_number": "",
"license_category": ""
}
EOT,
"vehicle_license_front" => <<<EOT
You are an OCR expert for Syrian orange vehicle cards.
### TASK
Analyse the **front side**:
{
"car_plate": "",
"owner": "",
"vin": "",
"color": "",
"color_hex": "",
"issue_date": "YYYY-MM-DD",
"inspection_date": "YYYY-MM-DD"
}
EOT,
"vehicle_license_back" => <<<EOT
You are an OCR expert for Syrian orange vehicle cards.
### TASK
Analyse the **back side**:
{
"make": "",
"model": "",
"year": "",
"fuel": "",
"chassis": ""
}
EOT,
];
$jordanPrompts = [
"id_front" => <<<EOT
You are an OCR expert for Jordanian national ID cards.
### TASK
Analyse the **front side** of the Jordanian ID and return **raw JSON only**:
{
"full_name": "",
"national_number": "",
"dob": "YYYY-MM-DD",
"address": ""
}
Extract the national number from the ID. Return valid JSON only.
EOT,
"id_back" => <<<EOT
You are an OCR expert for Jordanian national ID cards.
### TASK
Analyse the **back side** of the Jordanian ID:
{
"governorate": "",
"gender": "",
"issue_date": "YYYY-MM-DD",
"expiry_date": "YYYY-MM-DD"
}
EOT,
"driving_license_front" => <<<EOT
You are an OCR expert for Jordanian driving licences.
### TASK
Analyse the **front side**:
{
"name_arabic": "",
"license_number": "",
"issue_date": "YYYY-MM-DD",
"expiry_date": "YYYY-MM-DD",
"license_category": ""
}
EOT,
"driving_license_back" => <<<EOT
You are an OCR expert for Jordanian driving licences.
### TASK
Analyse the **back side**:
{
"blood_type": "",
"birth_place": "",
"notes": ""
}
EOT,
"vehicle_license_front" => <<<EOT
You are an OCR expert for Jordanian vehicle registration cards.
### TASK
Analyse the **front side**:
{
"car_plate": "",
"owner": "",
"vin": "",
"color": "",
"make": "",
"model": "",
"year": ""
}
EOT,
"vehicle_license_back" => <<<EOT
You are an OCR expert for Jordanian vehicle cards.
### TASK
Analyse the **back side**:
{
"fuel": "",
"chassis": "",
"inspection_date": "YYYY-MM-DD"
}
EOT,
];
$egyptPrompts = [
"id_front" => <<<EOT
You are an OCR expert for Egyptian national ID cards.
### TASK
Analyse the **front side** of the Egyptian ID:
{
"full_name": "",
"national_number": "",
"dob": "YYYY-MM-DD",
"address": ""
}
EOT,
"id_back" => <<<EOT
You are an OCR expert for Egyptian national ID cards.
### TASK
Analyse the **back side**:
{
"governorate": "",
"gender": "",
"issue_date": "YYYY-MM-DD",
"expiry_date": "YYYY-MM-DD"
}
EOT,
"driving_license_front" => <<<EOT
You are an OCR expert for Egyptian driving licences.
### TASK
Analyse the **front side**:
{
"name_arabic": "",
"license_number": "",
"issue_date": "YYYY-MM-DD",
"expiry_date": "YYYY-MM-DD",
"license_category": ""
}
EOT,
"driving_license_back" => <<<EOT
You are an OCR expert for Egyptian driving licences.
### TASK
Analyse the **back side**:
{
"blood_type": "",
"address": "",
"birth_place": ""
}
EOT,
"vehicle_license_front" => <<<EOT
You are an OCR expert for Egyptian vehicle registration cards.
### TASK
Analyse the **front side**:
{
"car_plate": "",
"owner": "",
"vin": "",
"color": "",
"make": "",
"model": "",
"year": ""
}
EOT,
"vehicle_license_back" => <<<EOT
You are an OCR expert for Egyptian vehicle cards.
### TASK
Analyse the **back side**:
{
"fuel": "",
"chassis": "",
"registration_date": "YYYY-MM-DD"
}
EOT,
];
return match ($country) {
self::COUNTRY_JORDAN => $jordanPrompts,
self::COUNTRY_EGYPT => $egyptPrompts,
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

@@ -0,0 +1 @@
[{"/app/bootstrap.php":"use AppCoreDatabase;\n\ntry {\n $pdo = Database::getConnection();\n\n echo","Migrations":"Telegram Bot Integration ===","\n CREATE TABLE IF NOT EXISTS `telegram_sessions` (\n `id` INT AUTO_INCREMENT PRIMARY KEY,\n `company_id` INT NOT NULL,\n `name` VARCHAR(255) NOT NULL COMMENT 'e.g., Support Bot, Sales Bot',\n `bot_token` TEXT NOT NULL COMMENT 'Encrypted using AES-256-GCM',\n `bot_username` VARCHAR(255) DEFAULT NULL,\n `bot_id` BIGINT DEFAULT NULL COMMENT 'Telegram Bot ID from getMe',\n `webhook_url` TEXT DEFAULT NULL,\n `status` ENUM('connected', 'disconnected', 'error') DEFAULT 'disconnected',\n `allowed_updates` JSON DEFAULT NULL COMMENT 'Array of update types to receive',\n `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n FOREIGN KEY (`company_id`) REFERENCES `companies`(`id`) ON DELETE CASCADE,\n UNIQUE KEY `company_bot_unique` (`company_id`, `bot_username`)\n ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;":"Database::execute($createSessionsTableSql);\n echo","telegram_sessions":"erified/created.","Database":"select(","telegram_session_id":"if (empty($columns)) {\n Database::execute(","meta_session_id`":"Database::execute(","✅ Added 'telegram_session_id' column and foreign key constraint to 'messages_log'.":""},{" Column 'telegram_session_id' already exists in 'messages_log'. Skipping.":""},{"Database":"execute(","session_id`":"Database::execute(","✅ Added 'telegram_session_id' column to 'chatbot_rules'.":""},{" Column 'telegram_session_id' already exists in 'chatbot_rules'. Skipping.":""},{"Database":"execute(","company_id`":"Database::execute(","✅ Added 'telegram_session_id' column to 'conversation_states'.":""},{" Column 'telegram_session_id' already exists in 'conversation_states'. Skipping.":""},{"Database":"execute(","✅ Added 'telegram_requests' column to 'company_subscription_usage'.":""},{" Column 'telegram_requests' already exists in 'company_subscription_usage'. Skipping.":""},{"❌ Migration error: \" . $e->getMessage() . \"":""}]

View File

@@ -160,6 +160,155 @@ $router->post('/api/integrations/woocommerce/disconnect', [\App\Controllers\WooC
$router->post('/api/webhooks/woocommerce', [\App\Controllers\WooCommerceController::class, 'webhook']);
// ============================================
// Siro Integration API Endpoints
// ============================================
// Siro Driver Info - Returns real-time driver data to Siro
$router->post('/api/siro/driver-info', function ($request, $response) {
$apiKey = getenv('NABEH_API_KEY');
$incomingKey = $request->getHeader('x-api-key') ?? '';
if (empty($apiKey) || $incomingKey !== $apiKey) {
$response->status(401)->json([
'status' => 'error',
'message' => 'Unauthorized'
]);
return;
}
$body = $request->getBody();
$phone = $body['phone'] ?? '';
if (empty($phone)) {
$response->status(400)->json([
'status' => 'error',
'message' => 'Missing phone number'
]);
return;
}
// Find driver OCR data
$hash = \App\Core\Security::blindIndex($phone);
$record = \App\Core\Database::selectOne(
"SELECT * FROM driver_ocr_data WHERE phone_hash = ? LIMIT 1",
[$hash]
);
$response->json([
'status' => 'success',
'data' => $record ? \App\Models\DriverOcrData::decryptRecord($record) : null
]);
});
// Siro Registration Status Check
$router->get('/api/siro/registration-status', function ($request, $response) {
$apiKey = getenv('NABEH_API_KEY');
$incomingKey = $request->getHeader('x-api-key') ?? '';
if (empty($apiKey) || $incomingKey !== $apiKey) {
$response->status(401)->json([
'status' => 'error',
'message' => 'Unauthorized'
]);
return;
}
$phone = $request->get('phone') ?? '';
if (empty($phone)) {
$response->status(400)->json([
'status' => 'error',
'message' => 'Missing phone parameter'
]);
return;
}
$hash = \App\Core\Security::blindIndex($phone);
$record = \App\Core\Database::selectOne(
"SELECT id, name, status, created_at, updated_at FROM driver_ocr_data WHERE phone_hash = ? LIMIT 1",
[$hash]
);
if (!$record) {
$response->json([
'status' => 'success',
'data' => null,
'message' => 'No registration found for this phone'
]);
return;
}
$response->json([
'status' => 'success',
'data' => $record
]);
});
// Siro Webhook - Receives driver activation confirmations from Siro
$router->post('/api/siro/webhook', function ($request, $response) {
$apiKey = getenv('NABEH_API_KEY');
$incomingKey = $request->getHeader('x-api-key') ?? '';
if (empty($apiKey) || $incomingKey !== $apiKey) {
$response->status(401)->json([
'status' => 'error',
'message' => 'Unauthorized'
]);
return;
}
$body = $request->getBody();
$phone = $body['phone'] ?? '';
$syroDriverId = $body['driver_id'] ?? '';
$event = $body['event'] ?? '';
$status = $body['status'] ?? '';
if (empty($phone) || empty($event)) {
$response->status(400)->json([
'status' => 'error',
'message' => 'Missing required fields: phone, event'
]);
return;
}
error_log("[Siro Webhook] Event: {$event}, Phone: {$phone}, DriverID: {$syroDriverId}, Status: {$status}");
$hash = \App\Core\Security::blindIndex($phone);
if ($event === 'driver_activated' && $status === 'actives') {
\App\Core\Database::execute(
"UPDATE driver_ocr_data SET status = 'registered', syro_driver_id = ? WHERE phone_hash = ?",
[$syroDriverId, $hash]
);
$response->json([
'status' => 'success',
'message' => 'Driver status updated'
]);
return;
}
if ($event === 'driver_rejected') {
\App\Core\Database::execute(
"UPDATE driver_ocr_data SET status = 'rejected' WHERE phone_hash = ?",
[$hash]
);
$response->json([
'status' => 'success',
'message' => 'Driver rejected'
]);
return;
}
$response->json([
'status' => 'success',
'message' => 'Event received'
]);
});
// Mock External API for Entaleq Driver Info (Used to fetch real-time driver data)
$router->post('/api/external/driver-info', function ($request, $response) {
$body = $request->getBody();
@@ -415,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,

View File

@@ -23,6 +23,7 @@
--success: #34a853; /* Google Green */
--warning: #fbbc05; /* Google Yellow */
--danger: #ea4335; /* Google Red */
--telegram: #229ED9; /* Telegram Blue */
--text-main: #202124;
--text-muted: #5f6368;
@@ -200,6 +201,29 @@
display: block;
}
/* Channel Pills */
.channel-pills {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
margin-bottom: 1.5rem;
}
.channel-pill {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.35rem 0.85rem;
border-radius: 999px;
background: #f1f3f4;
color: var(--text-muted);
font-size: 0.8rem;
font-weight: 700;
}
.channel-pill.telegram { background: rgba(34, 158, 217, 0.1); color: var(--telegram); }
.channel-pill.whatsapp { background: rgba(52, 168, 83, 0.1); color: var(--success); }
.channel-pill.meta { background: rgba(26, 115, 232, 0.1); color: var(--primary); }
/* Features Section */
.features {
padding: 5rem 0;
@@ -253,8 +277,9 @@
.feature-card:nth-child(2) .feature-icon { background: rgba(52, 168, 83, 0.1); color: var(--success); }
.feature-card:nth-child(3) .feature-icon { background: rgba(251, 188, 5, 0.1); color: var(--warning); }
.feature-card:nth-child(4) .feature-icon { background: rgba(234, 67, 53, 0.1); color: var(--danger); }
.feature-card:nth-child(5) .feature-icon { background: rgba(26, 115, 232, 0.1); color: var(--primary); }
.feature-card:nth-child(6) .feature-icon { background: rgba(52, 168, 83, 0.1); color: var(--success); }
.feature-card:nth-child(5) .feature-icon { background: rgba(34, 158, 217, 0.1); color: var(--telegram); }
.feature-card:nth-child(6) .feature-icon { background: rgba(26, 115, 232, 0.1); color: var(--primary); }
.feature-card:nth-child(7) .feature-icon { background: rgba(52, 168, 83, 0.1); color: var(--success); }
.feature-card h3 {
font-size: 1.25rem;
@@ -597,7 +622,13 @@
<div class="container hero-grid">
<div class="hero-content">
<h1>أتمتة خدمة العملاء والردود الذكية المتكاملة</h1>
<p>منصة نبيه تمنحك إمكانية ربط قنوات الاتصال والواتساب وتشغيل مساعد ذكي مدعوم بـ AI يقوم بقراءة الرسائل والصور والإجابة على العملاء فورياً لزيادة مبيعات متجرك وتفعيل أتمتة الدعم والعمليات بمعدل 5 أضعاف.</p>
<p>منصة نبيه تمنحك إمكانية ربط قنوات الاتصال (واتساب، تيليجرام، ماسنجر، إنستغرام) وتشغيل مساعد ذكي مدعوم بـ AI يقوم بقراءة الرسائل والصور والإجابة على العملاء فورياً لزيادة مبيعات متجرك وتفعيل أتمتة الدعم والعمليات بمعدل 5 أضعاف.</p>
<div class="channel-pills">
<span class="channel-pill whatsapp">💚 واتساب</span>
<span class="channel-pill telegram">✈️ تيليجرام</span>
<span class="channel-pill meta">💬 ماسنجر</span>
<span class="channel-pill meta">📷 إنستغرام</span>
</div>
<div class="hero-actions">
<a href="/register" class="btn btn-primary">ابدأ تجربتك المجانية (14 يوم)</a>
<a href="#demo" class="btn btn-secondary">جرب المحاكاة التفاعلية</a>
@@ -638,6 +669,12 @@
<p>الربط المباشر مع سلة (Salla) و WooCommerce لإرسال تحديثات حالة الطلب تلقائياً وتذكير العملاء بالسلات المتروكة.</p>
</div>
<div class="feature-card">
<div class="feature-icon">✈️</div>
<h3>دعم بوتات تيليجرام (Telegram)</h3>
<p>ربط بوتات التيليجرام مع المنصة لإدارة المحادثات الخاصة والمجموعات والقنوات، وتشغيل الردود التلقائية وأتمتة الدعم عبر Telegram Bot API.</p>
</div>
<div class="feature-card">
<div class="feature-icon">💬</div>
<h3>ربط قنوات ماسنجر وإنستغرام</h3>
@@ -782,10 +819,10 @@
</div>
<div class="roadmap-grid">
<div class="roadmap-card">
<span class="badge-soon">قريباً</span>
<h3>بوتات تيليجرام (Telegram)</h3>
<p>أتمتة المحادثات والرد التلقائي وإدارة مجموعات وقنوات التيليجرام بمساعد الذكاء الاصطناعي مباشرة.</p>
<div class="roadmap-card" style="border-color: var(--telegram);">
<span style="position: absolute; top: 1rem; left: 1rem; background: rgba(34, 158, 217, 0.15); border: 1px solid rgba(34, 158, 217, 0.3); color: #229ED9; padding: 0.2rem 0.6rem; border-radius: 20px; font-size: 0.75rem; font-weight: 700;">متاح الآن ✓</span>
<h3 style="margin-top: 2.25rem;">بوتات تيليجرام (Telegram)</h3>
<p>أتمتة المحادثات والرد التلقائي وإدارة مجموعات وقنوات التيليجرام بمساعد الذكاء الاصطناعي مباشرة. متاح للربط في جميع الباقات المدفوعة.</p>
</div>
<div class="roadmap-card">
@@ -864,6 +901,10 @@
reply = 'يمكنك بدء التجربة المجانية لمدة 14 يوماً فوراً دون الحاجة لبطاقة ائتمان من خلال الضغط على زر "ابدأ مجاناً" أعلى الصفحة.';
} else if (msgLower.includes('سلة') || msgLower.includes('woocommerce') || msgLower.includes('متجر')) {
reply = 'نعم! تدعم باقاتنا المتقدمة والاحترافية التكامل الفوري والمباشر مع متاجر سلة و WooCommerce لمتابعة الطلبات وتذكير السلات المتروكة.';
} else if (msgLower.includes('تيليجرام') || msgLower.includes('telegram') || msgLower.includes('بوت')) {
reply = 'نعم! منصة نبيه تدعم بوتات تيليجرام بالكامل! يمكنك ربط بوت التيليجرام الخاص بك مع المنصة لإدارة المحادثات والمجموعات والقنوات وتشغيل الرد التلقائي بالذكاء الاصطناعي. الميزة متاحة في الباقات المدفوعة.';
} else if (msgLower.includes('قنوات') || msgLower.includes('channel') || msgLower.includes('واتساب') && msgLower.includes('تليجرام')) {
reply = 'منصة نبيه تدعم الربط مع واتساب، تيليجرام، ماسنجر، وإنستغرام من مكان واحد، مع تشغيل أتمتة موحدة بالذكاء الاصطناعي عبر جميع القنوات.';
}
replyDiv.innerText = reply;