chore: استيراد أولي من سيرو (ecfe7568) — بلا أي تعديل

نسخة كاملة من مستودع سيرو عند ecfe7568 لتكون أساس تطبيق «انطلق».
نُسخ المتعقَّب في git فقط (12,509 ملفاً / 302 م.ب) بـ git archive، لا
`cp -r` — فاستُثنيت تلقائياً مخلفات البناء (build · node_modules ·
.dart_tool · .gradle · Pods ≈ 10.7 غ.ب) وكل ما يستثنيه .gitignore.

هذا الكوميت **بلا أي تعديل عمداً** حتى يكون كل ما يليه فرقاً مقروءاً
مقابل سيرو الأصلي. سيرو نفسه لم يُمسّ.

⚠️ لا يبني بعد: `.env` و`lib/env/env.g.dart` غير متعقَّبين في سيرو (وهذا
صحيح — أسرار لكل مستأجر). كل تطبيق فلاتر هنا يحتاج .env خاصاً بانطلق ثم
توليد env.g.dart عبر build_runner. لا تُنسخ أسرار سيرو.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-27 05:10:29 +03:00
co-authored by Claude Opus 5
commit 92dc6b3641
3654 changed files with 896478 additions and 0 deletions
+94
View File
@@ -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']);
}
+96
View File
@@ -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);
+252
View File
@@ -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']);
}
+218
View File
@@ -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()
]);
}
+124
View File
@@ -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']);
}
+338
View File
@@ -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);
+164
View File
@@ -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),
]);
+50
View File
@@ -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;