Update Siro integration: NABEH_API_KEY, nabeh/ folder paths, /api/siro/ routes

This commit is contained in:
Hamza-Ayed
2026-06-17 18:23:29 +03:00
parent e339cf466d
commit 926d8bc4a5
3 changed files with 816 additions and 142 deletions

View File

@@ -3,166 +3,72 @@
namespace App\Core\Flows; namespace App\Core\Flows;
use App\Services\GeminiService; use App\Services\GeminiService;
use App\Services\SiroService;
use App\Models\DriverOcrData; use App\Models\DriverOcrData;
use App\Models\ChatbotRule; use App\Models\ChatbotRule;
use App\Models\DriverReminder; use App\Models\DriverReminder;
use App\Core\Database;
/** /**
* DriverRegistrationFlow * DriverRegistrationFlow
* Handles step-by-step driver and vehicle registration using Gemini OCR. * 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 class DriverRegistrationFlow extends BaseFlow
{ {
private array $prompts = [ private array $prompts = [];
"id_front" => <<<EOT private string $country = 'syria';
You are an OCR expert for Syrian national ID cards (green card).
### TASK private array $stepToDocType = [
Analyse the **front side** of the ID and return **raw JSON only** with exactly these keys: 'id_front' => 'id_front',
'id_back' => 'id_back',
{ 'driving_license_front' => 'driver_license_front',
"full_name": "", // الاسم الثلاثي أو الرباعي 'driving_license_back' => 'driver_license_back',
"national_number": "", // الرقم الوطني (LATIN digits only) 'vehicle_license_front' => 'car_license_front',
"dob": "YYYY-MM-DD", // تاريخ الميلاد 'vehicle_license_back' => 'car_license_back',
"address": "" // العنوان 'criminal_record' => 'criminal_record',
}
### 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
]; ];
public function __construct()
{
$this->prompts = SiroService::getDocumentPrompts($this->country);
}
public function handleStep(string $step, array $messageData, array &$context): FlowResult public function handleStep(string $step, array $messageData, array &$context): FlowResult
{ {
$text = isset($messageData['body']) ? trim($messageData['body']) : ''; $text = isset($messageData['body']) ? trim($messageData['body']) : '';
$phone = $messageData['phone']; $phone = $messageData['phone'];
$companyId = $context['company_id'] ?? 1; $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 currently postponed and user sends a message, resume the flow
if ($step === 'postponed') { if ($step === 'postponed') {
// Cancel active reminder
$activeReminder = DriverReminder::findActive($companyId, $phone); $activeReminder = DriverReminder::findActive($companyId, $phone);
if ($activeReminder) { if ($activeReminder) {
DriverReminder::update($activeReminder['id'], ['status' => 'cancelled']); DriverReminder::update($activeReminder['id'], ['status' => 'cancelled']);
} }
// Restore previous step
$step = $context['previous_step'] ?? 'ask_name'; $step = $context['previous_step'] ?? 'ask_name';
} }
@@ -188,7 +94,6 @@ EOT
$context['postpone_count'] = $postponeCount; $context['postpone_count'] = $postponeCount;
$context['previous_step'] = $step; $context['previous_step'] = $step;
// Schedule reminder
$scheduledAt = date('Y-m-d H:i:s', strtotime("+{$hours} hours")); $scheduledAt = date('Y-m-d H:i:s', strtotime("+{$hours} hours"));
DriverReminder::saveReminder([ DriverReminder::saveReminder([
'company_id' => $companyId, 'company_id' => $companyId,
@@ -209,7 +114,7 @@ EOT
switch ($step) { switch ($step) {
case 'start': case 'start':
return new FlowResult( return new FlowResult(
"أهلاً بك كابتن في خدمة تسجيل كباتن تطبيق انطلق 🚖.\nيرجى إرسال اسمك الثلاثي الكامل للبدء:", "أهلاً بك كابتن في خدمة تسجيل كباتن تطبيق {$appName} في {$countryName} 🚖.\nيرجى إرسال اسمك الثلاثي الكامل للبدء:",
"ask_name" "ask_name"
); );
@@ -306,6 +211,17 @@ EOT
return new FlowResult("عذراً، فشل حفظ الصورة. الرجاء إعادة إرسال صورة الوثيقة:", "criminal_record"); 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 // Securely save registration data to local database
try { try {
DriverOcrData::saveSecure([ DriverOcrData::saveSecure([
@@ -332,11 +248,83 @@ EOT
return new FlowResult("عذراً، حدث خطأ أثناء حفظ طلبك في قاعدة البيانات. يرجى المحاولة مرة أخرى لاحقاً.", "criminal_record"); 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( return new FlowResult(
"شكراً لك كابتن، لقد تم استلام كافة المستندات والتحقق منها بنجاح. سيقوم فريق خدمة عملاء انطلق بمراجعة طلبك وتفعيل حسابك بأسرع وقت ممكن. يومك سعيد! 🚖", "شكراً لك كابتن، تم تسجيلك بنجاح في تطبيق {$appName} 🚖✅\nسيتم مراجعة طلبك من قبل فريق الخدمة وتفعيل حسابك قريباً. يومك سعيد!",
"finished", "finished",
true true
); );
} else {
$errMsg = $syroResult['message'] ?? 'خطأ غير معروف';
error_log("[Registration Flow] Siro registration failed: " . json_encode($syroResult));
return new FlowResult(
"تم حفظ مستنداتك بنجاح في نظامنا. لكن حدث تأخير في تسجيلك على تطبيق {$appName}. سيقوم فريقنا بمراجعة بياناتك وتفعيل حسابك في أقرب وقت. شكراً لصبرك! 🙏",
"finished",
true
);
}
default: default:
return new FlowResult("خطأ في تحديد خطوة المسار.", "finished", true); return new FlowResult("خطأ في تحديد خطوة المسار.", "finished", true);
@@ -365,6 +353,23 @@ EOT
return new FlowResult("عذراً، فشل حفظ الصورة. الرجاء إعادة المحاولة وإرسال الصورة:", $step); 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; $companyId = $context['company_id'] ?? 1;
// Check subscription limit for OCR // Check subscription limit for OCR

View File

@@ -0,0 +1,520 @@
<?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,
};
}
}

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']); $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) // Mock External API for Entaleq Driver Info (Used to fetch real-time driver data)
$router->post('/api/external/driver-info', function ($request, $response) { $router->post('/api/external/driver-info', function ($request, $response) {
$body = $request->getBody(); $body = $request->getBody();