feat: استيراد كود سيرو إلى تريبز (سيرو @ecfe7568) — بلا تعديل
قرار المالك 2026-07-27: باك إند سيرو PHP هو المعتمد، وتطبيقاته المجرّبة ميدانياً تحل محل إعادة البناء المؤرشفة. سيرو نفسه لم يُمسّ. الخريطة: backend · payment_server · loction_server · ride_server · passenger_server · docker · dashboard · stress_test → الجذر siro_rider → apps/rider siro_driver → apps/driver siro_admin → dashboards/admin siro_service → dashboards/service android_bot → apps/android_bot socialBot → apps/socialBot نُسخ المتعقَّب في git سيرو فقط عبر `git archive` (3,198 ملفاً / ~169 م.ب) لا `cp -r` — فاستُثنيت مخلفات البناء تلقائياً. بلا أي تعديل محتوى عمداً: كل ما يلي يصير فرقاً مقروءاً مقابل المصدر. لم يُستورد وسببه: siromove.com (الموقع التسويقي يبقى marketing/ في تريبز، سيرو فيه 8 ملفات) · docs و planning (تريبز له docs/ الخاص) · deploy.sh (ليس نشراً على سيرفر بل `git add . && git push origin --all` — فخّ في مستودع آخر) · transit_dashboard (بانتظار قرار مصير backend-transit و dashboards/transit-web). ⚠️ لا يبني بعد — ثلاثة نواقص متوقعة ومقصودة: 1. `.env` و `lib/env/env.g.dart` غير متعقَّبين في سيرو (أسرار لكل مستأجر): كل تطبيق فلاتر يحتاج .env خاصاً ثم توليد env.g.dart بـ build_runner. 2. إعدادات Firebase (9 ملفات google-services.json و GoogleService-Info.plist) يستبعدها .gitignore تريبز — ولكل مستأجر مشروع Firebase خاص أصلاً. 3. apps/driver في سيرو يشير إلى `../../Intaleq/packages/get` خارج المستودع → يجب ضمّ الحزم داخله أسوة بـ apps/rider. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9909d9b4c1
commit
4d8414c96b
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
/**
|
||||
* Nabeh Integration — Driver Status Check
|
||||
*
|
||||
* Called by Nabeh AI platform to check driver registration/activation status.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../core/bootstrap.php';
|
||||
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, X-API-Key');
|
||||
|
||||
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
|
||||
$expectedKey = getenv('NABEH_API_KEY') ?: '';
|
||||
|
||||
if (empty($apiKey) || $apiKey !== $expectedKey) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$phone = $_GET['phone'] ?? '';
|
||||
|
||||
if (empty($phone)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Phone parameter required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = Database::get('main');
|
||||
global $encryptionHelper;
|
||||
|
||||
$encryptedPhone = $encryptionHelper->encryptData($phone);
|
||||
global $blindIndex;
|
||||
$phoneBidx = $blindIndex ? $blindIndex->index('driver.phone', $phone) : null;
|
||||
|
||||
$stmt = $db->prepare("
|
||||
SELECT d.id, d.phone, d.first_name, d.last_name, d.status, d.created_at,
|
||||
cr.id as car_id, cr.make, cr.model, cr.year, cr.car_plate, cr.status as car_status
|
||||
FROM driver d
|
||||
LEFT JOIN CarRegistration cr ON cr.driverID = d.id
|
||||
WHERE (d.phone = :phone OR (:phone_bidx IS NOT NULL AND d.phone_bidx = :phone_bidx))
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([
|
||||
':phone' => $encryptedPhone,
|
||||
':phone_bidx' => $phoneBidx,
|
||||
]);
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$result) {
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'data' => null,
|
||||
'message' => 'Driver not found'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$decryptedPhone = $encryptionHelper->decryptData($result['phone']);
|
||||
$decryptedFirstName = $encryptionHelper->decryptData($result['first_name']);
|
||||
$decryptedLastName = $encryptionHelper->decryptData($result['last_name']);
|
||||
|
||||
$docStmt = $db->prepare("SELECT doc_type, link FROM driver_documents WHERE driverID = :driverID");
|
||||
$docStmt->execute([':driverID' => $result['id']]);
|
||||
$documents = $docStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'driver_id' => $result['id'],
|
||||
'phone' => $decryptedPhone,
|
||||
'name' => trim($decryptedFirstName . ' ' . $decryptedLastName),
|
||||
'status' => $result['status'],
|
||||
'registered_at' => $result['created_at'],
|
||||
'car' => [
|
||||
'id' => $result['car_id'],
|
||||
'make' => $result['make'],
|
||||
'model' => $result['model'],
|
||||
'year' => $result['year'],
|
||||
'plate' => $result['car_plate'],
|
||||
'status' => $result['car_status'],
|
||||
],
|
||||
'documents' => $documents,
|
||||
]
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
error_log("[Nabeh Status Error] " . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Internal server error']);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
/**
|
||||
* Nabeh Integration — Get User Recent Rides
|
||||
*
|
||||
* Returns the most recent rides for a user (driver or passenger)
|
||||
* identified by phone number. Used by the complaint workflow to
|
||||
* let the user pick which trip they're complaining about.
|
||||
*
|
||||
* Auth: X-API-Key header → NABEH_API_KEY
|
||||
*
|
||||
* Input:
|
||||
* phone (required) — User's phone number
|
||||
* limit (opt) — Max rides to return (default 5, max 20)
|
||||
*
|
||||
* Output:
|
||||
* List of rides with id, date, time, price, locations, status, etc.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../core/bootstrap.php';
|
||||
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, X-API-Key');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit;
|
||||
}
|
||||
|
||||
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
|
||||
$expectedKey = getenv('NABEH_API_KEY') ?: '';
|
||||
if (empty($apiKey) || $apiKey !== $expectedKey) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$raw = file_get_contents('php://input');
|
||||
$input = json_decode($raw, true) ?: ($_SERVER['REQUEST_METHOD'] === 'GET' ? $_GET : []);
|
||||
$phone = preg_replace('/\D+/', '', $input['phone'] ?? '');
|
||||
$limit = min(max((int)($input['limit'] ?? 5), 1), 20);
|
||||
|
||||
if (empty($phone)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'phone is required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$mainDb = Database::get('main');
|
||||
$rideDb = Database::get('ride');
|
||||
global $encryptionHelper;
|
||||
|
||||
// Resolve user
|
||||
$encryptedPhone = $encryptionHelper->encryptData($phone);
|
||||
global $blindIndex;
|
||||
$dBidx = $blindIndex ? $blindIndex->index('driver.phone', $phone) : null;
|
||||
$pBidx = $blindIndex ? $blindIndex->index('passengers.phone', $phone) : null;
|
||||
$driver = $mainDb->prepare("SELECT id, 'driver' AS type FROM driver WHERE phone = :p OR (:bidx IS NOT NULL AND phone_bidx = :bidx) LIMIT 1");
|
||||
$driver->execute([':p' => $encryptedPhone, ':bidx' => $dBidx]);
|
||||
$user = $driver->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$user) {
|
||||
$passenger = $mainDb->prepare("SELECT id, 'passenger' AS type FROM passengers WHERE phone = :p OR (:bidx IS NOT NULL AND phone_bidx = :bidx) LIMIT 1");
|
||||
$passenger->execute([':p' => $encryptedPhone, ':bidx' => $pBidx]);
|
||||
$user = $passenger->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
if (!$user) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'User not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$col = $user['type'] === 'driver' ? 'driver_id' : 'passenger_id';
|
||||
$stmt = $rideDb->prepare("
|
||||
SELECT id, start_location, end_location, date, time, endtime,
|
||||
price, price_for_driver, price_for_passenger,
|
||||
status, paymentMethod, carType, distance, created_at
|
||||
FROM ride
|
||||
WHERE $col = :uid
|
||||
ORDER BY created_at DESC
|
||||
LIMIT :lim
|
||||
");
|
||||
$stmt->bindValue(':uid', $user['id'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':lim', $limit, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$rides = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'user' => [
|
||||
'id' => $user['id'],
|
||||
'type' => $user['type'],
|
||||
],
|
||||
'rides' => $rides,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
/**
|
||||
* Nabeh Integration — Unified Query API
|
||||
*
|
||||
* Called by Nabeh AI platform to query driver info, trips, stats, and trip details.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../core/bootstrap.php';
|
||||
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, X-API-Key');
|
||||
|
||||
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
|
||||
$expectedKey = getenv('NABEH_API_KEY') ?: '';
|
||||
|
||||
if (empty($apiKey) || $apiKey !== $expectedKey) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized: invalid API key']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$input = [];
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$raw = file_get_contents('php://input');
|
||||
$input = json_decode($raw, true) ?: [];
|
||||
} else {
|
||||
$input = $_GET;
|
||||
}
|
||||
|
||||
$queryType = $input['query_type'] ?? '';
|
||||
$phone = preg_replace('/[^0-9]/', '', $input['phone'] ?? '');
|
||||
$driverId = $input['driver_id'] ?? '';
|
||||
$tripId = $input['trip_id'] ?? '';
|
||||
$limit = min((int)($input['limit'] ?? 10), 50);
|
||||
|
||||
if (empty($queryType)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'query_type is required. Options: driver_info, driver_trips, driver_stats, trip_detail']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$validTypes = ['driver_info', 'driver_trips', 'driver_stats', 'trip_detail'];
|
||||
if (!in_array($queryType, $validTypes, true)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Invalid query_type. Options: ' . implode(', ', $validTypes)]);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
global $encryptionHelper;
|
||||
$mainDb = Database::get('main');
|
||||
$rideDb = Database::get('ride');
|
||||
|
||||
// ========================================================================
|
||||
if ($queryType === 'driver_info') {
|
||||
if (empty($phone)) {
|
||||
jsonError('phone parameter is required');
|
||||
}
|
||||
|
||||
$encryptedPhone = $encryptionHelper->encryptData($phone);
|
||||
|
||||
$stmt = $mainDb->prepare("
|
||||
SELECT d.id, d.phone, d.first_name, d.last_name, d.name_arabic,
|
||||
d.status, d.created_at, d.birthdate, d.gender, d.site,
|
||||
cr.id as car_id, cr.make, cr.model, cr.year, cr.car_plate,
|
||||
cr.color, cr.color_hex, cr.fuel, cr.vin,
|
||||
cr.status as car_status, cr.expiration_date
|
||||
FROM driver d
|
||||
LEFT JOIN CarRegistration cr ON cr.driverID = d.id
|
||||
WHERE d.phone = :phone OR d.email LIKE :phoneLike
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([
|
||||
':phone' => $encryptedPhone,
|
||||
':phoneLike' => $phone . '%',
|
||||
]);
|
||||
$driver = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$driver) {
|
||||
echo json_encode(['status' => 'success', 'data' => null, 'message' => 'Driver not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$decrypt = function($val) use ($encryptionHelper) {
|
||||
return $val ? $encryptionHelper->decryptData($val) : $val;
|
||||
};
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'driver_id' => $driver['id'],
|
||||
'phone' => $decrypt($driver['phone']),
|
||||
'first_name' => $decrypt($driver['first_name']),
|
||||
'last_name' => $decrypt($driver['last_name']),
|
||||
'name_arabic' => $decrypt($driver['name_arabic']),
|
||||
'gender' => $decrypt($driver['gender']),
|
||||
'birthdate' => $driver['birthdate'],
|
||||
'status' => $driver['status'],
|
||||
'site' => $decrypt($driver['site']),
|
||||
'registered_at' => $driver['created_at'],
|
||||
'car' => $driver['car_id'] ? [
|
||||
'id' => $driver['car_id'],
|
||||
'make' => $driver['make'],
|
||||
'model' => $driver['model'],
|
||||
'year' => $driver['year'],
|
||||
'plate' => $driver['car_plate'],
|
||||
'color' => $driver['color'],
|
||||
'color_hex' => $driver['color_hex'],
|
||||
'fuel' => $driver['fuel'],
|
||||
'vin' => $decrypt($driver['vin']),
|
||||
'status' => $driver['car_status'],
|
||||
'expiration_date' => $driver['expiration_date'],
|
||||
] : null,
|
||||
],
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
if ($queryType === 'driver_trips') {
|
||||
if (empty($driverId) && empty($phone)) {
|
||||
jsonError('driver_id or phone is required');
|
||||
}
|
||||
|
||||
if (empty($driverId) && !empty($phone)) {
|
||||
$encryptedPhone = $encryptionHelper->encryptData($phone);
|
||||
$stmt = $mainDb->prepare("SELECT id FROM driver WHERE phone = :phone LIMIT 1");
|
||||
$stmt->execute([':phone' => $encryptedPhone]);
|
||||
$driverRow = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$driverRow) {
|
||||
echo json_encode(['status' => 'success', 'data' => [], 'message' => 'Driver not found']);
|
||||
exit;
|
||||
}
|
||||
$driverId = $driverRow['id'];
|
||||
}
|
||||
|
||||
$stmt = $rideDb->prepare("
|
||||
SELECT id, start_location, end_location, date, time, endtime,
|
||||
price, price_for_driver, price_for_passenger,
|
||||
status, paymentMethod, carType, distance, created_at
|
||||
FROM ride
|
||||
WHERE driver_id = :driver_id
|
||||
ORDER BY created_at DESC
|
||||
LIMIT :lim
|
||||
");
|
||||
$stmt->bindValue(':driver_id', $driverId, PDO::PARAM_STR);
|
||||
$stmt->bindValue(':lim', $limit, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$trips = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'data' => $trips,
|
||||
'count' => count($trips),
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
if ($queryType === 'driver_stats') {
|
||||
if (empty($driverId) && empty($phone)) {
|
||||
jsonError('driver_id or phone is required');
|
||||
}
|
||||
|
||||
if (empty($driverId) && !empty($phone)) {
|
||||
$encryptedPhone = $encryptionHelper->encryptData($phone);
|
||||
$stmt = $mainDb->prepare("SELECT id FROM driver WHERE phone = :phone LIMIT 1");
|
||||
$stmt->execute([':phone' => $encryptedPhone]);
|
||||
$driverRow = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$driverRow) {
|
||||
echo json_encode(['status' => 'success', 'data' => null, 'message' => 'Driver not found']);
|
||||
exit;
|
||||
}
|
||||
$driverId = $driverRow['id'];
|
||||
}
|
||||
|
||||
$stmt = $rideDb->prepare("
|
||||
SELECT
|
||||
COUNT(*) as total_trips,
|
||||
COALESCE(SUM(price_for_driver), 0) as total_earnings,
|
||||
COALESCE(SUM(price_for_passenger), 0) as total_collected,
|
||||
COALESCE(AVG(price_for_driver), 0) as avg_earning_per_trip,
|
||||
COALESCE(SUM(distance), 0) as total_distance,
|
||||
COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed_trips,
|
||||
COUNT(CASE WHEN status = 'cancelled' THEN 1 END) as cancelled_trips
|
||||
FROM ride
|
||||
WHERE driver_id = :driver_id
|
||||
");
|
||||
$stmt->execute([':driver_id' => $driverId]);
|
||||
$stats = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
$driverStmt = $mainDb->prepare("SELECT status, created_at FROM driver WHERE id = :id LIMIT 1");
|
||||
$driverStmt->execute([':id' => $driverId]);
|
||||
$driverStatus = $driverStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'driver_id' => $driverId,
|
||||
'status' => $driverStatus['status'] ?? 'unknown',
|
||||
'registered_at' => $driverStatus['created_at'] ?? null,
|
||||
'stats' => [
|
||||
'total_trips' => (int)$stats['total_trips'],
|
||||
'completed_trips' => (int)$stats['completed_trips'],
|
||||
'cancelled_trips' => (int)$stats['cancelled_trips'],
|
||||
'total_earnings' => (float)$stats['total_earnings'],
|
||||
'total_collected' => (float)$stats['total_collected'],
|
||||
'avg_earning_per_trip' => (float)$stats['avg_earning_per_trip'],
|
||||
'total_distance_km' => (float)$stats['total_distance'],
|
||||
],
|
||||
],
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
if ($queryType === 'trip_detail') {
|
||||
if (empty($tripId)) {
|
||||
jsonError('trip_id is required');
|
||||
}
|
||||
|
||||
$stmt = $rideDb->prepare("
|
||||
SELECT r.*,
|
||||
p.first_name as passenger_first_name,
|
||||
p.last_name as passenger_last_name,
|
||||
p.phone as passenger_phone
|
||||
FROM ride r
|
||||
LEFT JOIN driver p ON p.id = r.passenger_id
|
||||
WHERE r.id = :id
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([':id' => $tripId]);
|
||||
$trip = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$trip) {
|
||||
echo json_encode(['status' => 'success', 'data' => null, 'message' => 'Trip not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'data' => $trip,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
error_log("[Nabeh Query Error] " . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Internal server error']);
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
/**
|
||||
* Nabeh Integration — Driver Registration
|
||||
*
|
||||
* Called by Nabeh AI platform to register drivers in Siro.
|
||||
* Authenticated via NABEH_API_KEY (shared between servers).
|
||||
* Handles all 3 countries: Syria, Jordan, Egypt.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../core/bootstrap.php';
|
||||
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, X-API-Key');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
|
||||
$expectedKey = getenv('NABEH_API_KEY') ?: '';
|
||||
|
||||
if (empty($apiKey) || $apiKey !== $expectedKey) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized: invalid API key']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
$rateLimitFile = __DIR__ . '/../logs/nabeh_rate_' . md5($_SERVER['REMOTE_ADDR'] ?? 'unknown') . '.lock';
|
||||
$rateLimitWindow = 60;
|
||||
$rateLimitMax = 5;
|
||||
|
||||
$now = time();
|
||||
$attempts = [];
|
||||
if (file_exists($rateLimitFile)) {
|
||||
$attempts = json_decode(file_get_contents($rateLimitFile), true) ?: [];
|
||||
$attempts = array_filter($attempts, fn($t) => $t > ($now - $rateLimitWindow));
|
||||
}
|
||||
if (count($attempts) >= $rateLimitMax) {
|
||||
http_response_code(429);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Too many requests. Try again later.']);
|
||||
exit;
|
||||
}
|
||||
$attempts[] = $now;
|
||||
file_put_contents($rateLimitFile, json_encode($attempts), LOCK_EX);
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
if (!$input) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Invalid JSON body']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$phone = preg_replace('/[^0-9]/', '', $input['phone'] ?? '');
|
||||
$password = $input['password'] ?? substr(md5($phone . time()), 0, 12);
|
||||
$firstName = $input['first_name'] ?? $input['name_arabic'] ?? '';
|
||||
$lastName = $input['last_name'] ?? '-';
|
||||
$nameArabic = $input['name_arabic'] ?? $firstName;
|
||||
$nationalNumber = $input['national_number'] ?? '';
|
||||
$birthdate = $input['birthdate'] ?? '';
|
||||
$address = $input['address'] ?? '';
|
||||
$gender = $input['gender'] ?? 'Male';
|
||||
$site = $input['site'] ?? '';
|
||||
|
||||
$vin = $input['vin'] ?? '';
|
||||
$carPlate = $input['car_plate'] ?? '';
|
||||
$make = $input['make'] ?? '';
|
||||
$model = $input['model'] ?? '';
|
||||
$year = $input['year'] ?? '';
|
||||
$color = $input['color'] ?? '';
|
||||
$colorHex = $input['color_hex'] ?? '#000000';
|
||||
$owner = $input['owner'] ?? '';
|
||||
$fuel = $input['fuel'] ?? 'Petrol';
|
||||
$expirationDate = $input['expiration_date'] ?? '';
|
||||
|
||||
$idFront = $input['id_front'] ?? $input['id_front_url'] ?? '';
|
||||
$idBack = $input['id_back'] ?? $input['id_back_url'] ?? '';
|
||||
$driverLicense = $input['driver_license'] ?? $input['driver_license_front_url'] ?? '';
|
||||
$driverLicenseBack = $input['driver_license_back'] ?? $input['driver_license_back_url'] ?? '';
|
||||
$carLicenseFront = $input['car_license_front'] ?? $input['vehicle_license_front_url'] ?? '';
|
||||
$carLicenseBack = $input['car_license_back'] ?? $input['vehicle_license_back_url'] ?? '';
|
||||
$criminalRecord = $input['criminal_record'] ?? $input['criminal_record_url'] ?? '';
|
||||
$profilePicture = $input['profile_picture'] ?? '';
|
||||
|
||||
if (empty($phone)) {
|
||||
jsonError('Phone number is required');
|
||||
}
|
||||
if (empty($firstName)) {
|
||||
jsonError('First name is required');
|
||||
}
|
||||
if (empty($vin) || empty($carPlate) || empty($make) || empty($model) || empty($year)) {
|
||||
jsonError('Car details (vin, plate, make, model, year) are required');
|
||||
}
|
||||
|
||||
try {
|
||||
$db = Database::get('main');
|
||||
$driverId = 'DRV' . date('YmdHis') . rand(100, 999);
|
||||
$hashedPassword = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
|
||||
|
||||
global $encryptionHelper;
|
||||
$encryptedPhone = $encryptionHelper->encryptData($phone);
|
||||
$encryptedFirstName = $encryptionHelper->encryptData($firstName);
|
||||
$encryptedLastName = $encryptionHelper->encryptData($lastName);
|
||||
$encryptedNameArabic = $encryptionHelper->encryptData($nameArabic);
|
||||
$encryptedNationalNumber = !empty($nationalNumber) ? $encryptionHelper->encryptData($nationalNumber) : '';
|
||||
$encryptedAddress = !empty($address) ? $encryptionHelper->encryptData($address) : '';
|
||||
$encryptedGender = $encryptionHelper->encryptData($gender);
|
||||
$encryptedVin = $encryptionHelper->encryptData($vin);
|
||||
$encryptedCarPlate = $encryptionHelper->encryptData($carPlate);
|
||||
$encryptedOwner = $encryptionHelper->encryptData($owner);
|
||||
$encryptedSite = !empty($site) ? $encryptionHelper->encryptData($site) : $encryptedAddress;
|
||||
|
||||
$nowDate = date('Y-m-d H:i:s');
|
||||
|
||||
$driverStmt = $db->prepare("
|
||||
INSERT INTO driver (
|
||||
id, phone, email, password, gender, first_name, last_name,
|
||||
name_arabic, national_number, address, site, birthdate,
|
||||
status, created_at, updated_at
|
||||
) VALUES (
|
||||
:id, :phone, :email, :password, :gender, :first_name, :last_name,
|
||||
:name_arabic, :national_number, :address, :site, :birthdate,
|
||||
'yet', :created_at, :updated_at
|
||||
)
|
||||
");
|
||||
$driverStmt->execute([
|
||||
':id' => $driverId,
|
||||
':phone' => $encryptedPhone,
|
||||
':email' => $phone . '@intaleqapp.com',
|
||||
':password' => $hashedPassword,
|
||||
':gender' => $encryptedGender,
|
||||
':first_name' => $encryptedFirstName,
|
||||
':last_name' => $encryptedLastName,
|
||||
':name_arabic' => $encryptedNameArabic,
|
||||
':national_number' => $encryptedNationalNumber,
|
||||
':address' => $encryptedAddress,
|
||||
':site' => $encryptedSite,
|
||||
':birthdate' => $birthdate,
|
||||
':created_at' => $nowDate,
|
||||
':updated_at' => $nowDate,
|
||||
]);
|
||||
|
||||
$carStmt = $db->prepare("
|
||||
INSERT INTO CarRegistration (
|
||||
driverID, vin, car_plate, make, model, year,
|
||||
expiration_date, color, owner, color_hex, fuel,
|
||||
isDefault, status, created_at, vehicle_category_id
|
||||
) VALUES (
|
||||
:driverID, :vin, :car_plate, :make, :model, :year,
|
||||
:expiration_date, :color, :owner, :color_hex, :fuel,
|
||||
1, 'yet', :created_at, 1
|
||||
)
|
||||
");
|
||||
$carStmt->execute([
|
||||
':driverID' => $driverId,
|
||||
':vin' => $encryptedVin,
|
||||
':car_plate' => $encryptedCarPlate,
|
||||
':make' => $make,
|
||||
':model' => $model,
|
||||
':year' => $year,
|
||||
':expiration_date' => $expirationDate ?: $nowDate,
|
||||
':color' => $color,
|
||||
':owner' => $encryptedOwner,
|
||||
':color_hex' => $colorHex,
|
||||
':fuel' => $fuel,
|
||||
':created_at' => $nowDate,
|
||||
]);
|
||||
$carRegId = $db->lastInsertId();
|
||||
|
||||
$docTypes = [
|
||||
'id_front' => $idFront,
|
||||
'id_back' => $idBack,
|
||||
'driver_license' => $driverLicense,
|
||||
'driver_license_back' => $driverLicenseBack,
|
||||
'car_license_front' => $carLicenseFront,
|
||||
'car_license_back' => $carLicenseBack,
|
||||
'criminal_record' => $criminalRecord,
|
||||
'profile_picture' => $profilePicture,
|
||||
];
|
||||
|
||||
$docStmt = $db->prepare("
|
||||
INSERT INTO driver_documents (driverID, doc_type, image_name, link, upload_date)
|
||||
VALUES (:driverID, :doc_type, :image_name, :link, :upload_date)
|
||||
");
|
||||
foreach ($docTypes as $docType => $link) {
|
||||
if (!empty($link)) {
|
||||
$docStmt->execute([
|
||||
':driverID' => $driverId,
|
||||
':doc_type' => $docType,
|
||||
':image_name' => $driverId . '_' . $docType,
|
||||
':link' => $link,
|
||||
':upload_date' => $nowDate,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
error_log("[Nabeh Registration] New driver registered: {$driverId}, Phone: {$phone}");
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => [
|
||||
'status' => 'success',
|
||||
'driverID' => $driverId,
|
||||
'carRegID' => $carRegId,
|
||||
]
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
error_log("[Nabeh Registration Error] " . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'status' => 'failure',
|
||||
'message' => 'Internal server error: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
/**
|
||||
* Nabeh Integration — Resolve Phone → User ID
|
||||
*
|
||||
* Called by the payment server (server-to-server) to resolve
|
||||
* a phone number to a driverID or passengerID.
|
||||
*
|
||||
* Why: The wallet's invoice tables (invoices_shamcash, cliq_invoices, etc.)
|
||||
* store driverID/passengerID, NOT phone numbers. Only the Siro main DB
|
||||
* has the phone→userID mapping (with encryption).
|
||||
*
|
||||
* This endpoint bridges that gap:
|
||||
* Payment Server (phone) → Siro Backend (resolve_user.php) → driverID
|
||||
* Payment Server (driverID) → Wallet DB → pending invoices → AI verify
|
||||
*
|
||||
* Auth: X-API-Key header → NABEH_API_KEY
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../core/bootstrap.php';
|
||||
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, X-API-Key');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
|
||||
$expectedKey = getenv('NABEH_API_KEY') ?: '';
|
||||
|
||||
if (empty($apiKey) || $apiKey !== $expectedKey) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$rawPhone = preg_replace('/\D+/', '', $input['phone'] ?? '');
|
||||
|
||||
if (empty($rawPhone)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Phone number is required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// التطبيع عبر normalizePhone() الموحّدة في core/helpers.php (نفس المنطق سابقاً)
|
||||
$phone = normalizePhone($rawPhone);
|
||||
|
||||
try {
|
||||
$db = Database::get('main');
|
||||
global $encryptionHelper;
|
||||
|
||||
$encryptedPhone = $encryptionHelper->encryptData($phone);
|
||||
global $blindIndex;
|
||||
$dBidx = $blindIndex ? $blindIndex->index('driver.phone', $phone) : null;
|
||||
$pBidx = $blindIndex ? $blindIndex->index('passengers.phone', $phone) : null;
|
||||
|
||||
// Look for driver first
|
||||
$stmt = $db->prepare(
|
||||
"SELECT id, phone, first_name, last_name FROM driver WHERE phone = :phone OR (:bidx IS NOT NULL AND phone_bidx = :bidx) LIMIT 1"
|
||||
);
|
||||
$stmt->execute([':phone' => $encryptedPhone, ':bidx' => $dBidx]);
|
||||
$driver = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($driver) {
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'user_id' => $driver['id'],
|
||||
'phone' => $encryptionHelper->decryptData($driver['phone']),
|
||||
'name' => trim(
|
||||
$encryptionHelper->decryptData($driver['first_name'])
|
||||
. ' ' .
|
||||
$encryptionHelper->decryptData($driver['last_name'])
|
||||
),
|
||||
'type' => 'driver',
|
||||
],
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Fallback: look for passenger
|
||||
$stmt = $db->prepare(
|
||||
"SELECT id, phone, first_name, last_name FROM passengers WHERE phone = :phone OR (:bidx IS NOT NULL AND phone_bidx = :bidx) LIMIT 1"
|
||||
);
|
||||
$stmt->execute([':phone' => $encryptedPhone, ':bidx' => $pBidx]);
|
||||
$passenger = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($passenger) {
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'user_id' => $passenger['id'],
|
||||
'phone' => $encryptionHelper->decryptData($passenger['phone']),
|
||||
'name' => trim(
|
||||
$encryptionHelper->decryptData($passenger['first_name'])
|
||||
. ' ' .
|
||||
$encryptionHelper->decryptData($passenger['last_name'])
|
||||
),
|
||||
'type' => 'passenger',
|
||||
],
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'data' => null,
|
||||
'message' => 'User not found',
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
error_log("[ResolveUser Error] " . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Internal server error']);
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
<?php
|
||||
/**
|
||||
* Nabeh Integration — Submit Complaint with AI Analysis
|
||||
*
|
||||
* Called by Nabeh WhatsApp bot. Accepts a complaint from driver or passenger,
|
||||
* auto-resolves user from phone, fetches full trip context (ride, ratings,
|
||||
* driver/passenger profiles, behavior data), analyzes via Gemini AI,
|
||||
* and stores in the complaint table.
|
||||
*
|
||||
* Auth: X-API-Key header → NABEH_API_KEY
|
||||
*
|
||||
* Input:
|
||||
* phone (required) — User's phone number (resolve via resolve_user.php)
|
||||
* ride_id (required) — The trip ID this complaint is about
|
||||
* complaint_text (req) — Description of the issue
|
||||
* audio_link (opt) — Voice note link (if user recorded one)
|
||||
* user_type (opt) — 'driver' or 'passenger' (auto-detected if possible)
|
||||
*
|
||||
* Output:
|
||||
* status, message, complaint_id, passenger_report, driver_report
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../core/bootstrap.php';
|
||||
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, X-API-Key');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
|
||||
$expectedKey = getenv('NABEH_API_KEY') ?: '';
|
||||
if (empty($apiKey) || $apiKey !== $expectedKey) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$phone = preg_replace('/\D+/', '', $input['phone'] ?? '');
|
||||
$rideId = trim($input['ride_id'] ?? '');
|
||||
$complaintText = trim($input['complaint_text'] ?? '');
|
||||
$audioLink = trim($input['audio_link'] ?? '');
|
||||
$userType = trim($input['user_type'] ?? '');
|
||||
|
||||
if (empty($phone) || empty($rideId) || empty($complaintText)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'phone, ride_id, and complaint_text are required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$mainDb = Database::get('main');
|
||||
$rideDb = Database::get('ride');
|
||||
global $encryptionHelper;
|
||||
|
||||
// ── Resolve user by phone ────────────────────────────────────
|
||||
$encryptedPhone = $encryptionHelper->encryptData($phone);
|
||||
global $blindIndex;
|
||||
$dBidx = $blindIndex ? $blindIndex->index('driver.phone', $phone) : null;
|
||||
$pBidx = $blindIndex ? $blindIndex->index('passengers.phone', $phone) : null;
|
||||
$driverRow = $mainDb->prepare("SELECT id, first_name, last_name FROM driver WHERE phone = :p OR (:bidx IS NOT NULL AND phone_bidx = :bidx) LIMIT 1");
|
||||
$driverRow->execute([':p' => $encryptedPhone, ':bidx' => $dBidx]);
|
||||
$driver = $driverRow->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
$passengerRow = null;
|
||||
if (!$driver) {
|
||||
$passengerRow = $mainDb->prepare("SELECT id, first_name, last_name FROM passengers WHERE phone = :p OR (:bidx IS NOT NULL AND phone_bidx = :bidx) LIMIT 1");
|
||||
$passengerRow->execute([':p' => $encryptedPhone, ':bidx' => $pBidx]);
|
||||
$passenger = $passengerRow->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
if (!$driver && !$passenger) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'User not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$userId = $driver ? $driver['id'] : $passenger['id'];
|
||||
$detectedType = $driver ? 'driver' : 'passenger';
|
||||
if (empty($userType)) $userType = $detectedType;
|
||||
|
||||
// ── Validate ride exists ─────────────────────────────────────
|
||||
$stmt = $rideDb->prepare("SELECT * FROM ride WHERE id = :id");
|
||||
$stmt->execute([':id' => $rideId]);
|
||||
$ride = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$ride) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Ride not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Fetch full context ──────────────────────────────────────
|
||||
$passengerId = $ride['passenger_id'];
|
||||
$driverId = $ride['driver_id'];
|
||||
|
||||
/**
|
||||
* Fetch user profile + full rating history (received + given)
|
||||
*/
|
||||
function getEnhancedProfile($db, $table, $id, $enc, $ratingReceivedTable, $ratingReceivedCol, $ratingGivenTable, $ratingGivenCol, $ratingsDb) {
|
||||
$profile = ['info' => null, 'ratings_received' => [], 'ratings_given' => [], 'stats' => []];
|
||||
|
||||
// Profile info
|
||||
$stmt = $db->prepare("SELECT id, first_name, last_name, created_at FROM $table WHERE id = :id LIMIT 1");
|
||||
$stmt->execute([':id' => $id]);
|
||||
$info = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($info) {
|
||||
$fn = $enc->decryptData($info['first_name']);
|
||||
$ln = $enc->decryptData($info['last_name']);
|
||||
$info['full_name'] = trim("$fn $ln");
|
||||
$info['account_age_days'] = $info['created_at'] ? round((time() - strtotime($info['created_at'])) / 86400) : 0;
|
||||
unset($info['first_name'], $info['last_name']);
|
||||
$profile['info'] = $info;
|
||||
}
|
||||
|
||||
// Ratings received (others rated this user)
|
||||
$stmt = $ratingsDb->prepare("
|
||||
SELECT rating, comment, created_at
|
||||
FROM $ratingReceivedTable
|
||||
WHERE $ratingReceivedCol = :id
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 10
|
||||
");
|
||||
$stmt->execute([':id' => $id]);
|
||||
$profile['ratings_received'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Ratings given (this user rated others)
|
||||
$stmt = $ratingsDb->prepare("
|
||||
SELECT rating, comment, created_at
|
||||
FROM $ratingGivenTable
|
||||
WHERE $ratingGivenCol = :id
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 10
|
||||
");
|
||||
$stmt->execute([':id' => $id]);
|
||||
$profile['ratings_given'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Aggregate stats for received ratings
|
||||
$stmt = $ratingsDb->prepare("
|
||||
SELECT
|
||||
COUNT(id) AS total,
|
||||
AVG(rating) AS avg_rating,
|
||||
SUM(CASE WHEN rating <= 2 THEN 1 ELSE 0 END) AS low_count,
|
||||
SUM(CASE WHEN rating = 3 THEN 1 ELSE 0 END) AS mid_count,
|
||||
SUM(CASE WHEN rating >= 4 THEN 1 ELSE 0 END) AS high_count
|
||||
FROM $ratingReceivedTable
|
||||
WHERE $ratingReceivedCol = :id
|
||||
");
|
||||
$stmt->execute([':id' => $id]);
|
||||
$profile['stats'] = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
return $profile;
|
||||
}
|
||||
|
||||
// Driver profile: received ratings from ratingDriver (by driver_id), given ratings in ratingPassenger (by driverID)
|
||||
$driverProfile = getEnhancedProfile(
|
||||
$mainDb, 'driver', $driverId, $encryptionHelper,
|
||||
'ratingDriver', 'driver_id', // received: passengers rate driver
|
||||
'ratingPassenger', 'driverID', // given: driver rates passenger
|
||||
$mainDb
|
||||
);
|
||||
|
||||
// Passenger profile: received ratings from ratingPassenger (by passenger_id), given ratings in ratingDriver (by passenger_id)
|
||||
$passengerProfile = getEnhancedProfile(
|
||||
$mainDb, 'passengers', $passengerId, $encryptionHelper,
|
||||
'ratingPassenger', 'passenger_id', // received: drivers rate passenger
|
||||
'ratingDriver', 'passenger_id', // given: passenger rates driver
|
||||
$mainDb
|
||||
);
|
||||
|
||||
// Driver behavior data
|
||||
$behavior = null;
|
||||
$bStmt = $rideDb->prepare("SELECT max_speed, avg_speed, hard_brakes, behavior_score FROM driver_behavior WHERE trip_id = :trip AND driver_id = :did LIMIT 1");
|
||||
$bStmt->execute([':trip' => $rideId, ':did' => $driverId]);
|
||||
$behavior = $bStmt->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||
|
||||
// ── Gemini AI Analysis ──────────────────────────────────────
|
||||
$geminiKey = getenv('GEMINI_API_KEY');
|
||||
if (!$geminiKey) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'AI service not configured']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check existing complaints for the same ride
|
||||
$existingStmt = $mainDb->prepare("SELECT id, statusComplaint FROM complaint WHERE ride_id = :rid ORDER BY id DESC LIMIT 1");
|
||||
$existingStmt->execute([':rid' => $rideId]);
|
||||
$existingComplaint = $existingStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
$prompt = "
|
||||
أنت خبير في حل النزاعات في خدمات نقل الركاب لتطبيق Siro. قم بتحليل الشكوى التالية بناءً على البيانات الشاملة:
|
||||
|
||||
**1. تفاصيل الرحلة:**
|
||||
" . json_encode($ride, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "
|
||||
|
||||
**2. ملف الراكب (بيانات الحساب + سجل التقييمات):**
|
||||
" . json_encode($passengerProfile, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "
|
||||
|
||||
**3. ملف السائق (بيانات الحساب + سجل التقييمات + سلوك القيادة):**
|
||||
" . json_encode([
|
||||
'info' => $driverProfile['info'],
|
||||
'ratings_received' => $driverProfile['ratings_received'],
|
||||
'ratings_given' => $driverProfile['ratings_given'],
|
||||
'stats' => $driverProfile['stats'],
|
||||
'behavior' => $behavior,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "
|
||||
|
||||
**4. الشكوى:**
|
||||
- نص الشكوى: '" . $complaintText . "'
|
||||
- رابط تسجيل صوتي: " . ($audioLink ?: 'لا يوجد') . "
|
||||
- مقدم الشكوى: " . $userType . "
|
||||
" . ($existingComplaint ? "- شكوى سابقة موجودة للرحلة: ID={$existingComplaint['id']}, status={$existingComplaint['statusComplaint']}" : '') . "
|
||||
|
||||
**تعليمات التحليل الذكي (التقييمات):**
|
||||
- حلل سجل تقييمات السائق: هل يتكرر حصوله على تقييمات منخفضة (1-2)؟ ماذا تقول تعليقات الركاب السابقين عنه؟
|
||||
- حلل سجل تقييمات الراكب: هل يميل لإعطاء تقييمات منخفضة للسائقين؟
|
||||
- ادرس توزيع التقييمات: average + low/mid/high counts يعطي صورة عن سلوك كل طرف
|
||||
- اربط التعليقات السابقة بمضمون الشكوى الحالية: هل هناك نمط متكرر؟
|
||||
- استخدم عمر الحساب (account_age_days) لتقييم مصداقية المستخدم
|
||||
|
||||
**المطلوب:**
|
||||
1. تحديد الطرف المخطئ على الأرجح بناءً على: تفاصيل الرحلة + تاريخ التقييمات + سلوك القيادة.
|
||||
2. تحديد ما إذا كانت الشكوى حقيقية أم كيدية.
|
||||
3. تصنيف الشكوى (سلوك السائق، مشكلة أجرة، مسار، حالة السيارة، غير ذلك).
|
||||
4. اقتراح حلين واضحين ومحددين لخدمة العملاء.
|
||||
5. كتابة تقرير مناسب لمقدم الشكوى (دون إحراج).
|
||||
6. كتابة تقرير مناسب للطرف الآخر (مهذب ومحترم).
|
||||
|
||||
**الخرج المطلوب (JSON فقط، بالعربية):**
|
||||
{
|
||||
\"customerServiceSolutions\": [\"حل 1\", \"حل 2\"],
|
||||
\"passengerReport\": {\"title\": \"...\", \"body\": \"...\"},
|
||||
\"driverReport\": {\"title\": \"...\", \"body\": \"...\"},
|
||||
\"fault_determination\": \"الراكب/السائق/كلاهما/غير واضح\",
|
||||
\"complaint_nature\": \"حقيقية/كيدية/نزاع بسيط\",
|
||||
\"complaint_type\": \"تصنيف الشكوى\"
|
||||
}
|
||||
";
|
||||
|
||||
$apiURL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-lite-latest:generateContent?key=$geminiKey";
|
||||
$ch = curl_init($apiURL);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||||
CURLOPT_POSTFIELDS => json_encode(['contents' => [['parts' => [['text' => $prompt]]]]]),
|
||||
CURLOPT_TIMEOUT => 60,
|
||||
]);
|
||||
$response = curl_exec($ch);
|
||||
$curlErr = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($curlErr) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'AI service error: ' . $curlErr]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
$rawText = $data['candidates'][0]['content']['parts'][0]['text'] ?? '';
|
||||
$cleanJson = trim(preg_replace('/```json|```/', '', $rawText));
|
||||
$analysis = json_decode($cleanJson, true);
|
||||
|
||||
if (!$analysis || !isset($analysis['passengerReport']) || !isset($analysis['driverReport'])) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Failed to parse AI response']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Save to complaint table ──────────────────────────────────
|
||||
$fullDesc = $complaintText;
|
||||
if ($audioLink) $fullDesc .= "\n\n[audio: $audioLink]";
|
||||
|
||||
$stmt = $mainDb->prepare("
|
||||
INSERT INTO complaint
|
||||
(ride_id, passenger_id, driver_id, complaint_type, description,
|
||||
date_filed, statusComplaint, resolution,
|
||||
passenger_report, driver_report, cs_solutions,
|
||||
fault_determination, complaint_nature, date_resolved)
|
||||
VALUES
|
||||
(:rid, :pid, :did, :ctype, :desc,
|
||||
NOW(), 'Resolved', :res,
|
||||
:preport, :dreport, :cssol,
|
||||
:fault, :nature, NOW())
|
||||
");
|
||||
$stmt->execute([
|
||||
':rid' => $rideId,
|
||||
':pid' => $passengerId,
|
||||
':did' => $driverId,
|
||||
':ctype' => $analysis['complaint_type'] ?? 'General',
|
||||
':desc' => $fullDesc,
|
||||
':res' => $cleanJson,
|
||||
':preport'=> json_encode($analysis['passengerReport'] ?? null, JSON_UNESCAPED_UNICODE),
|
||||
':dreport'=> json_encode($analysis['driverReport'] ?? null, JSON_UNESCAPED_UNICODE),
|
||||
':cssol' => json_encode($analysis['customerServiceSolutions'] ?? null, JSON_UNESCAPED_UNICODE),
|
||||
':fault' => $analysis['fault_determination'] ?? 'N/A',
|
||||
':nature' => $analysis['complaint_nature'] ?? 'N/A',
|
||||
]);
|
||||
$complaintId = $mainDb->lastInsertId();
|
||||
|
||||
// ── Notify customer service ──────────────────────────────────
|
||||
$csPhone = getenv('SERVICE_PHONE1');
|
||||
$sendFn = getenv('SEND_WHATSAPP_FN_PATH');
|
||||
if (!empty($csPhone) && $sendFn && file_exists($sendFn)) {
|
||||
require_once $sendFn;
|
||||
if (function_exists('sendWhatsAppFromServer')) {
|
||||
$csMsg = "*شكوى جديدة (#$complaintId)*\n"
|
||||
. "*- الرحلة:* $rideId\n"
|
||||
. "*- مقدمها:* $userType\n"
|
||||
. "*- تصنيف:* {$analysis['complaint_type']}\n"
|
||||
. "*- المخطئ:* {$analysis['fault_determination']}\n"
|
||||
. "*- الحلول:* {$analysis['customerServiceSolutions'][0]} / {$analysis['customerServiceSolutions'][1]}";
|
||||
sendWhatsAppFromServer($csPhone, $csMsg);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Response ─────────────────────────────────────────────────
|
||||
$report = $userType === 'driver' ? $analysis['driverReport'] : $analysis['passengerReport'];
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => 'Complaint submitted and analyzed.',
|
||||
'complaint_id'=> $complaintId,
|
||||
'report' => $report,
|
||||
'ai_result' => [
|
||||
'fault_determination' => $analysis['fault_determination'],
|
||||
'complaint_nature' => $analysis['complaint_nature'],
|
||||
'complaint_type' => $analysis['complaint_type'],
|
||||
],
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
/**
|
||||
* Nabeh Integration — Document Upload
|
||||
*
|
||||
* Called by Nabeh AI platform to upload driver documents to Siro's private storage.
|
||||
* Returns a signed URL valid for 48 hours.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../core/bootstrap.php';
|
||||
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, X-API-Key');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
|
||||
$expectedKey = getenv('NABEH_API_KEY') ?: '';
|
||||
|
||||
if (empty($apiKey) || $apiKey !== $expectedKey) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized: invalid API key']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
$rateLimitFile = __DIR__ . '/../logs/nabeh_upload_rate_' . md5($_SERVER['REMOTE_ADDR'] ?? 'unknown') . '.lock';
|
||||
$rateLimitWindow = 60;
|
||||
$rateLimitMax = 10;
|
||||
|
||||
$nowTime = time();
|
||||
$attempts = [];
|
||||
if (file_exists($rateLimitFile)) {
|
||||
$attempts = json_decode(file_get_contents($rateLimitFile), true) ?: [];
|
||||
$attempts = array_filter($attempts, fn($t) => $t > ($nowTime - $rateLimitWindow));
|
||||
}
|
||||
if (count($attempts) >= $rateLimitMax) {
|
||||
http_response_code(429);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Too many requests. Try again later.']);
|
||||
exit;
|
||||
}
|
||||
$attempts[] = $nowTime;
|
||||
file_put_contents($rateLimitFile, json_encode($attempts), LOCK_EX);
|
||||
|
||||
const MAX_FILE_MB = 5;
|
||||
const ALLOWED_MIMES = ['image/jpeg', 'image/png', 'image/webp'];
|
||||
const UPLOAD_ROOT = __DIR__ . '/../private_uploads';
|
||||
const SIGNED_TTL_SEC = 172800;
|
||||
|
||||
$signSecret = getenv('SECRET_KEY_HMAC') ?: '';
|
||||
if (empty($signSecret)) {
|
||||
uploadLog('[Nabeh Upload] SECRET_KEY_HMAC not configured', 'ERROR');
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Server configuration error']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$host = getenv('APP_DOMAIN') ?: 'api-syria.siromove.com';
|
||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||
define('PUBLIC_BASE', "$protocol://$host/siro");
|
||||
|
||||
if (!is_dir(UPLOAD_ROOT)) {
|
||||
@mkdir(UPLOAD_ROOT, 0700, true);
|
||||
}
|
||||
|
||||
uploadLog("[Nabeh Upload] Document upload started");
|
||||
|
||||
$allowedDocTypes = [
|
||||
'id_front', 'id_back',
|
||||
'driver_license_front', 'driver_license_back',
|
||||
'car_license_front', 'car_license_back',
|
||||
'criminal_record', 'profile_picture',
|
||||
];
|
||||
|
||||
$driverId = $_POST['driver_id'] ?? '';
|
||||
$docType = $_POST['doc_type'] ?? '';
|
||||
|
||||
if (empty($driverId) || empty($docType)) {
|
||||
jsonError('driver_id and doc_type are required.');
|
||||
}
|
||||
|
||||
$driverIdSafe = preg_replace('/[^A-Za-z0-9_\-]/', '_', $driverId);
|
||||
|
||||
if (!in_array($docType, $allowedDocTypes, true)) {
|
||||
jsonError("Invalid doc_type. Allowed: " . implode(', ', $allowedDocTypes));
|
||||
}
|
||||
|
||||
if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
|
||||
$errCode = $_FILES['file']['error'] ?? 'missing_file';
|
||||
uploadLog("[Nabeh Upload] File upload error. Code: $errCode", 'ERROR');
|
||||
jsonError('No file uploaded or upload error.');
|
||||
}
|
||||
|
||||
$tmpPath = $_FILES['file']['tmp_name'];
|
||||
$size = filesize($tmpPath);
|
||||
if ($size === false || $size <= 0) {
|
||||
jsonError('Invalid file size.');
|
||||
}
|
||||
if ($size > MAX_FILE_MB * 1024 * 1024) {
|
||||
jsonError('File too large. Max ' . MAX_FILE_MB . ' MB.');
|
||||
}
|
||||
|
||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||
$mime = $finfo->file($tmpPath) ?: 'application/octet-stream';
|
||||
if (!in_array($mime, ALLOWED_MIMES, true)) {
|
||||
jsonError("Unsupported file type: $mime");
|
||||
}
|
||||
|
||||
$extMap = [
|
||||
'image/jpeg' => '.jpg',
|
||||
'image/png' => '.png',
|
||||
'image/webp' => '.webp',
|
||||
];
|
||||
$ext = $extMap[$mime];
|
||||
|
||||
$h = hash('sha1', $driverIdSafe);
|
||||
$subdir = substr($h, 0, 2) . '/' . substr($h, 2, 2);
|
||||
$destDir = UPLOAD_ROOT . '/' . $subdir;
|
||||
if (!is_dir($destDir)) { @mkdir($destDir, 0700, true); }
|
||||
|
||||
$serverName = "{$driverIdSafe}__{$docType}{$ext}";
|
||||
$destPath = $destDir . '/' . $serverName;
|
||||
|
||||
$resolvedDest = realpath($destPath) ?: $destPath;
|
||||
$resolvedRoot = realpath(UPLOAD_ROOT) ?: UPLOAD_ROOT;
|
||||
|
||||
if (is_file($destPath) && str_starts_with($resolvedDest, $resolvedRoot)) {
|
||||
@unlink($destPath);
|
||||
}
|
||||
|
||||
if (!move_uploaded_file($tmpPath, $destPath)) {
|
||||
jsonError('Failed to save the uploaded file.');
|
||||
}
|
||||
@chmod($destPath, 0600);
|
||||
|
||||
$extShort = ltrim($ext, '.');
|
||||
$expires = time() + SIGNED_TTL_SEC;
|
||||
$message = $driverIdSafe . ':' . $docType . ':' . $extShort . ':' . $expires;
|
||||
$signature = hash_hmac('sha256', $message, $signSecret);
|
||||
|
||||
$fileUrl = PUBLIC_BASE . '/secure_image.php'
|
||||
. '?driver_id=' . urlencode($driverIdSafe)
|
||||
. '&doc_type=' . urlencode($docType)
|
||||
. '&ext=' . urlencode($extShort)
|
||||
. '&expires=' . $expires
|
||||
. '&signature=' . urlencode($signature);
|
||||
|
||||
uploadLog("[Nabeh Upload] Document uploaded successfully. Type: $docType, Size: $size");
|
||||
|
||||
printSuccess([
|
||||
'status' => 'success',
|
||||
'success_file' => true,
|
||||
'file_url' => $fileUrl,
|
||||
'file_name' => $serverName,
|
||||
'driver_id' => $driverIdSafe,
|
||||
'doc_type' => $docType,
|
||||
'mime_type' => $mime,
|
||||
'size_bytes' => $size,
|
||||
'expires_at' => date('c', $expires),
|
||||
]);
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
/**
|
||||
* Nabeh Integration — Verify Payment (Proxy to Wallet Server)
|
||||
*
|
||||
* This script forwards requests to the actual Wallet Server endpoint
|
||||
* where the Gemini AI and invoice processing logic resides.
|
||||
*/
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, X-API-Key');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../core/bootstrap.php';
|
||||
|
||||
$walletUrl = getenv('WALLET_SERVER_URL') ?: 'https://walletintaleq.intaleq.xyz';
|
||||
$endpoint = rtrim($walletUrl, '/') . '/v2/main/ride/nabeh/verify_payment.php';
|
||||
|
||||
$headers = [
|
||||
'Content-Type: application/json'
|
||||
];
|
||||
|
||||
if (isset($_SERVER['HTTP_X_API_KEY'])) {
|
||||
$headers[] = 'X-API-Key: ' . $_SERVER['HTTP_X_API_KEY'];
|
||||
}
|
||||
|
||||
$rawInput = file_get_contents('php://input');
|
||||
|
||||
$ch = curl_init($endpoint);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $rawInput);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
// curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Enable in local dev if needed
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode) {
|
||||
http_response_code($httpCode);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
$response = json_encode(['status' => 'error', 'message' => 'Failed to connect to Wallet Server']);
|
||||
}
|
||||
|
||||
echo $response;
|
||||
Reference in New Issue
Block a user