Update: 2026-06-18 16:46:30

This commit is contained in:
Hamza-Ayed
2026-06-18 16:46:30 +03:00
parent 8b52d2f115
commit f13faa8c31
12 changed files with 693 additions and 169 deletions

View File

@@ -0,0 +1,93 @@
<?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);
$driver = $mainDb->prepare("SELECT id, 'driver' AS type FROM driver WHERE phone = :p LIMIT 1");
$driver->execute([':p' => $encryptedPhone]);
$user = $driver->fetch(PDO::FETCH_ASSOC);
if (!$user) {
$passenger = $mainDb->prepare("SELECT id, 'passenger' AS type FROM passengers WHERE phone = :p LIMIT 1");
$passenger->execute([':p' => $encryptedPhone]);
$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);

View File

@@ -0,0 +1,335 @@
<?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);
$driverRow = $mainDb->prepare("SELECT id, first_name, last_name FROM driver WHERE phone = :p LIMIT 1");
$driverRow->execute([':p' => $encryptedPhone]);
$driver = $driverRow->fetch(PDO::FETCH_ASSOC);
$passengerRow = null;
if (!$driver) {
$passengerRow = $mainDb->prepare("SELECT id, first_name, last_name FROM passengers WHERE phone = :p LIMIT 1");
$passengerRow->execute([':p' => $encryptedPhone]);
$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);