Security hardening: fixed 13 vulnerabilities, added AI-powered SupportController (Gemini), and stabilized Flutter Complaint logic
This commit is contained in:
217
app/Http/Controllers/SupportController.php
Normal file
217
app/Http/Controllers/SupportController.php
Normal file
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use App\Services\LegacyEncryption;
|
||||
|
||||
class SupportController extends Controller
|
||||
{
|
||||
protected LegacyEncryption $encryption;
|
||||
|
||||
public function __construct(LegacyEncryption $encryption)
|
||||
{
|
||||
$this->encryption = $encryption;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v2/support/complaints
|
||||
* يحلل الشكوى باستخدام الذكاء الاصطناعي (Gemini) ويحفظ النتيجة
|
||||
*/
|
||||
public function storeComplaint(Request $request): JsonResponse
|
||||
{
|
||||
$userId = $request->attributes->get('_jwt_user_id');
|
||||
|
||||
$request->validate([
|
||||
'ride_id' => 'required|string',
|
||||
'complaint_text' => 'required|string',
|
||||
'audio_link' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$rideId = $request->input('ride_id');
|
||||
$complaintText = $request->input('complaint_text');
|
||||
$audioLink = $request->input('audio_link');
|
||||
|
||||
// 1. جلب بيانات الرحلة
|
||||
$ride = DB::connection('ride')->table('ride')
|
||||
->where('id', $rideId)
|
||||
->first();
|
||||
|
||||
if (!$ride) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Ride not found'], 404);
|
||||
}
|
||||
|
||||
$passengerId = $ride->passenger_id;
|
||||
$driverId = $ride->driver_id;
|
||||
|
||||
// 2. جلب الملفات التعريفية والسلوك
|
||||
$passengerProfile = $this->getPassengerFullProfile($passengerId);
|
||||
$driverProfile = $this->getDriverFullProfile($driverId);
|
||||
$driverBehavior = DB::connection('tracking')->table('driver_behavior')
|
||||
->where('trip_id', $rideId)
|
||||
->where('driver_id', $driverId)
|
||||
->first();
|
||||
|
||||
// 3. بناء الـ Prompt لـ Gemini
|
||||
$prompt = $this->buildGeminiPrompt($ride, $passengerProfile, $driverProfile, $driverBehavior, $complaintText, $audioLink);
|
||||
|
||||
// 4. استدعاء Gemini API
|
||||
$apiKey = config('services.gemini.key') ?? env('GEMINI_API_KEY');
|
||||
if (!$apiKey) {
|
||||
Log::error('Gemini API Key missing in SupportController');
|
||||
// Fallback: save without AI if key is missing?
|
||||
// Better to fail if AI is expected.
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::timeout(60)->post("https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key={$apiKey}", [
|
||||
'contents' => [
|
||||
['parts' => [['text' => $prompt]]]
|
||||
]
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
throw new \Exception('Gemini API failed: ' . $response->body());
|
||||
}
|
||||
|
||||
$aiData = $response->json();
|
||||
$analysisRaw = $aiData['candidates'][0]['content']['parts'][0]['text'] ?? '';
|
||||
$analysisJson = trim(preg_replace('/```json|```/', '', $analysisRaw));
|
||||
$analysis = json_decode($analysisJson, true);
|
||||
|
||||
if (!$analysis || !isset($analysis['passengerReport'])) {
|
||||
throw new \Exception('Failed to parse AI response: ' . $analysisRaw);
|
||||
}
|
||||
|
||||
// 5. حفظ الشكوى والتحليل
|
||||
$fullDescription = $complaintText . ($audioLink ? "\n\n[Audio Attached: {$audioLink}]" : "");
|
||||
|
||||
DB::connection('primary')->table('complaint')->insert([
|
||||
'ride_id' => $rideId,
|
||||
'passenger_id' => $passengerId,
|
||||
'driver_id' => $driverId,
|
||||
'complaint_type' => $analysis['complaint_type'] ?? 'General',
|
||||
'description' => $fullDescription,
|
||||
'statusComplaint' => 'Resolved',
|
||||
'resolution' => $analysisJson,
|
||||
'passenger_report' => json_encode($analysis['passengerReport'], JSON_UNESCAPED_UNICODE),
|
||||
'driver_report' => json_encode($analysis['driverReport'], JSON_UNESCAPED_UNICODE),
|
||||
'cs_solutions' => json_encode($analysis['customerServiceSolutions'], JSON_UNESCAPED_UNICODE),
|
||||
'fault_determination' => $analysis['fault_determination'] ?? 'N/A',
|
||||
'complaint_nature' => $analysis['complaint_nature'] ?? 'N/A',
|
||||
'date_filed' => now(),
|
||||
'date_resolved' => now(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Complaint processed successfully.',
|
||||
'passenger_response' => $analysis['passengerReport'],
|
||||
'driver_response' => $analysis['driverReport']
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('SupportController AI Error: ' . $e->getMessage());
|
||||
|
||||
// Fallback: Just save as pending if AI fails
|
||||
DB::connection('primary')->table('complaint')->insert([
|
||||
'ride_id' => $rideId,
|
||||
'passenger_id' => $passengerId,
|
||||
'driver_id' => $driverId,
|
||||
'complaint_type' => 'General',
|
||||
'description' => $complaintText . ($audioLink ? " [Audio: $audioLink]" : ""),
|
||||
'statusComplaint' => 'Open',
|
||||
'date_filed' => now(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Complaint received (Manual Review required).',
|
||||
'passenger_response' => ['title' => 'تم استلام شكواك', 'body' => 'جاري مراجعة طلبك من قبل فريق الدعم.'],
|
||||
'driver_response' => ['title' => 'بلاغ جديد', 'body' => 'تم تسجيل بلاغ بخصوص رحلتك الأخيرة.']
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function getPassengerFullProfile($id)
|
||||
{
|
||||
$p = DB::connection('primary')->table('passengers')->where('id', $id)->first();
|
||||
if (!$p) return null;
|
||||
|
||||
return [
|
||||
'info' => [
|
||||
'id' => $p->id,
|
||||
'full_name' => trim($this->encryption->decrypt($p->first_name) . ' ' . $this->encryption->decrypt($p->last_name)),
|
||||
'created_at' => $p->created_at
|
||||
],
|
||||
'ratings' => DB::connection('primary')->table('ratingPassenger')->where('passenger_id', $id)
|
||||
->selectRaw('AVG(rating) as avg_rating, COUNT(id) as total_ratings')->first(),
|
||||
'comments' => DB::connection('primary')->table('ratingPassenger')->where('passenger_id', $id)
|
||||
->whereNotNull('comment')->orderBy('created_at', 'desc')->limit(5)->pluck('comment')
|
||||
];
|
||||
}
|
||||
|
||||
private function getDriverFullProfile($id)
|
||||
{
|
||||
$d = DB::connection('primary')->table('driver')->where('id', $id)->first();
|
||||
if (!$d) return null;
|
||||
|
||||
return [
|
||||
'info' => [
|
||||
'id' => $d->id,
|
||||
'full_name' => trim($this->encryption->decrypt($d->first_name) . ' ' . $this->encryption->decrypt($d->last_name)),
|
||||
'created_at' => $d->created_at
|
||||
],
|
||||
'ratings' => DB::connection('primary')->table('ratingDriver')->where('driver_id', $id)
|
||||
->selectRaw('AVG(rating) as avg_rating, COUNT(id) as total_ratings')->first(),
|
||||
'comments' => DB::connection('primary')->table('ratingDriver')->where('driver_id', $id)
|
||||
->whereNotNull('comment')->orderBy('created_at', 'desc')->limit(5)->pluck('comment')
|
||||
];
|
||||
}
|
||||
|
||||
private function buildGeminiPrompt($ride, $pProfile, $dProfile, $behavior, $text, $audio)
|
||||
{
|
||||
return "
|
||||
أنت خبير في حل النزاعات في خدمات نقل الركاب لتطبيق intaleqapp.com. قم بتحليل الشكوى التالية بين راكب وسائق بناءً على البيانات الشاملة التالية:
|
||||
|
||||
**1. تفاصيل الرحلة:**
|
||||
" . json_encode($ride, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "
|
||||
|
||||
**2. ملف الراكب:**
|
||||
" . json_encode($pProfile, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "
|
||||
|
||||
**3. ملف السائق:**
|
||||
" . json_encode($dProfile, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "
|
||||
|
||||
**4. بيانات سلوك السائق (في هذه الرحلة):**
|
||||
" . json_encode($behavior, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "
|
||||
|
||||
**5. الشكوى نفسها:**
|
||||
- نص الشكوى من الراكب: '" . $text . "'
|
||||
- رابط تسجيل صوتي للشكوى (إن وجد): " . $audio . "
|
||||
|
||||
**مهمتك هي:**
|
||||
1. تحليل جميع البيانات المتاحة لتحديد الطرف المخطئ على الأرجح.
|
||||
2. تحديد ما إذا كانت الشكوى كيدية أم حقيقية.
|
||||
3. **تصنيف الشكوى** (مثال: سلوك السائق، مشكلة في الأجرة، مسار الرحلة، حالة السيارة، أخرى).
|
||||
4. اقتراح حلين واضحين ومختلفين لفريق خدمة العملاء.
|
||||
5. كتابة تقرير موجز ومناسب للراكب.
|
||||
6. كتابة تقرير موجز ومناسب للسائق.
|
||||
|
||||
**الخرج المطلوب:**
|
||||
أعد الرد بصيغة JSON فقط، بدون أي نصوص إضافية، وباللغة العربية (لهجة مصرية)، بالهيكل التالي:
|
||||
{
|
||||
\"customerServiceSolutions\": [\"الحل المقترح الأول\", \"الحل المقترح الثاني\"],
|
||||
\"passengerReport\": { \"title\": \"بخصوص شكوتك في رحلة Intaleq\", \"body\": \"رسالة واضحة للراكب بنتيجة الشكوى\" },
|
||||
\"driverReport\": { \"title\": \"بخصوص بلاغ رحلتك الأخيرة في Intaleq\", \"body\": \"رسالة واضحة للسائق بنتيجة الشكوى\" },
|
||||
\"fault_determination\": \"الطرف المخطئ (الراكب/السائق/كلاهما/غير واضح)\",
|
||||
\"complaint_nature\": \"طبيعة الشكوى (حقيقية/كيدية/نزاع بسيط)\",
|
||||
\"complaint_type\": \"تصنيف الشكوى الذي حددته\"
|
||||
}
|
||||
";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user