prepare("SELECT * FROM complaint WHERE id = ? LIMIT 1"); $stmt->execute([$complaintId]); $complaint = $stmt->fetch(PDO::FETCH_ASSOC); if (!$complaint) { jsonError("Complaint not found", 404); } $rideId = $complaint['ride_id'] ?? null; // ── سياق الرحلة ───────────────────────────────────────── // ‏بدونه النموذج يخمّن. مع السعر والمسافة والحالة يصير الاقتراح مبنياً // ‏على وقائع. $ride = []; $currency = 'SYP'; $hardCap = 0.0; if ($rideId) { $stmtRide = $con->prepare("SELECT * FROM ride WHERE id = ? LIMIT 1"); $stmtRide->execute([$rideId]); $ride = $stmtRide->fetch(PDO::FETCH_ASSOC) ?: []; $kazan = getKazanForRide($con, $rideId); $currency = !empty($kazan['currency']) ? (string) $kazan['currency'] : getCurrencyByCountry((string) ($kazan['country'] ?? 'Syria')); } $hardCap = getMoneyHardCap($currency); // ── سوابق الطرفين ─────────────────────────────────────── // ‏شكوى أولى من راكب مختلفة عن الخامسة. والسائق الذي تكررت شكاواه // ‏مختلف عمّن يُشتكى عليه أول مرة. $history = ['passenger_complaints' => 0, 'driver_complaints' => 0]; try { if (!empty($complaint['passenger_id'])) { $st = $con->prepare("SELECT COUNT(*) FROM complaint WHERE passenger_id = ? AND id <> ?"); $st->execute([$complaint['passenger_id'], $complaintId]); $history['passenger_complaints'] = (int) $st->fetchColumn(); } if (!empty($complaint['driver_id'])) { $st = $con->prepare("SELECT COUNT(*) FROM complaint WHERE driver_id = ? AND id <> ?"); $st->execute([$complaint['driver_id'], $complaintId]); $history['driver_complaints'] = (int) $st->fetchColumn(); } } catch (PDOException $e) { error_log("[complaint/ai] تعذّر جلب السوابق: " . $e->getMessage()); } // ── بناء المُوجِّه ────────────────────────────────────── // ‏لا نمرّر أسماء ولا هواتف ولا بريداً: النموذج خدمة خارجية، وما يخرج // ‏من الخادم لا يعود. المعرّفات وحدها تكفي للتحليل. // ‏الملفان الكاملان: تقييم كل طرف، وتعليقات من تعاملوا معه سابقاً. // ‏هذا ما يجعل النموذج يعرف "من هؤلاء" لا الشكوى وحدها — راكب تكررت // ‏شكاواه الكيدية مختلف عن راكب أول شكوى له، والسائق كذلك. $driverProfile = !empty($complaint['driver_id']) ? getDriverFullProfile($con, $encryptionHelper ?? null, $complaint['driver_id']) : null; $passengerProfile = !empty($complaint['passenger_id']) ? getPassengerFullProfile($con, $encryptionHelper ?? null, $complaint['passenger_id']) : null; $driverBehavior = ($rideId && !empty($complaint['driver_id'])) ? getDriverBehavior($con, $rideId, $complaint['driver_id']) : null; // ‏نزع الأسماء: الملفات تحمل first_name/last_name، والنموذج خدمة // ‏خارجية. التقييمات والتعليقات تكفي للتحليل بلا هوية. foreach ([&$driverProfile, &$passengerProfile] as &$pr) { if (is_array($pr) && isset($pr['info']) && is_array($pr['info'])) { unset($pr['info']['first_name'], $pr['info']['last_name']); } } unset($pr); $context = [ 'complaint' => [ 'type' => $complaint['complaint_type'] ?? '', 'description' => mb_substr((string) ($complaint['description'] ?? ''), 0, 1500), 'passenger_report' => mb_substr((string) ($complaint['passenger_report'] ?? ''), 0, 800), 'driver_report' => mb_substr((string) ($complaint['driver_report'] ?? ''), 0, 800), 'prior_ai_solutions' => mb_substr((string) ($complaint['cs_solutions'] ?? ''), 0, 800), 'nature' => $complaint['complaint_nature'] ?? null, 'filed_at' => $complaint['date_filed'] ?? '', ], 'ride' => [ 'status' => $ride['status'] ?? null, 'price' => $ride['price'] ?? null, 'distance_km' => $ride['distance'] ?? null, 'car_type' => $ride['carType'] ?? $ride['car_type'] ?? null, ], 'driver_profile' => $driverProfile, 'passenger_profile' => $passengerProfile, 'driver_behavior' => $driverBehavior, 'complaint_counts' => $history, 'currency' => $currency, ]; // ‏التحويل قبل الـheredoc: استدعاء دالة داخله غير مسموح، و$this // ‏لا وجود له في سكربت إجرائي (خطأ قاتل لا يلتقطه php -l). $contextJson = json_encode($context, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); $reasons = implode(', ', [ 'driver_behavior', 'passenger_behavior', 'overcharge', 'route_issue', 'no_show', 'vehicle_condition', 'app_issue', 'payment_issue', 'safety', 'other', ]); $prompt = <<callGemini($prompt, $model); $text = $raw['candidates'][0]['content']['parts'][0]['text'] ?? null; if (!$text) { // ‏غياب الاقتراح لا يعطّل الموظف — الشاشة تعمل بدونه. error_log("[complaint/ai] لا رد من النموذج للشكوى #$complaintId"); jsonSuccess(['available' => false], "AI suggestion unavailable"); } // ‏النموذج يغلّف JSON بأسوار ```json أحياناً رغم التعليمات. $clean = trim(preg_replace('/^```(?:json)?|```$/mu', '', $text)); $parsed = json_decode($clean, true); if (!is_array($parsed)) { error_log("[complaint/ai] رد غير قابل للتحليل للشكوى #$complaintId: " . mb_substr($clean, 0, 200)); jsonSuccess(['available' => false], "AI suggestion unparsable"); } // ── تحصين المخرجات ───────────────────────────────────── // ‏النموذج قد يتجاهل القواعد. لا نمرّر رقماً منه إلى واجهة تصرف مالاً // ‏دون قصّه هنا أولاً. $amount = (float) ($parsed['compensation']['amount'] ?? 0); $amount = max(0.0, min($amount, $hardCap)); $kind = $parsed['compensation']['kind'] ?? 'goodwill'; if (!in_array($kind, ['refund', 'goodwill'], true)) $kind = 'goodwill'; $beneficiary = $parsed['compensation']['beneficiary'] ?? 'passenger'; if (!in_array($beneficiary, ['passenger', 'driver'], true)) $beneficiary = 'passenger'; jsonSuccess([ 'available' => true, 'reason_code' => $parsed['reason_code'] ?? null, 'fault_determination' => $parsed['fault_determination'] ?? 'unclear', 'suggested_status' => $parsed['suggested_status'] ?? 'In Progress', 'compensation' => [ 'amount' => $amount, 'kind' => $kind, 'beneficiary' => $beneficiary, 'currency' => $currency, 'hard_cap' => $hardCap, ], 'customer_reply' => $parsed['customer_reply'] ?? '', 'internal_note' => $parsed['internal_note'] ?? '', 'confidence' => (float) ($parsed['confidence'] ?? 0), ], "ok"); } catch (PDOException $e) { error_log("[complaint/ai] " . $e->getMessage()); jsonError("DB Error", 500); } catch (Throwable $e) { error_log("[complaint/ai] " . $e->getMessage()); jsonSuccess(['available' => false], "AI suggestion unavailable"); }