Files
tripz-llc/backend/nabeh/submit_complaint.php
Hamza-AyedandClaude Opus 5 4d8414c96b 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>
2026-07-27 05:14:13 +03:00

339 lines
15 KiB
PHP

<?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);