Update: 2026-08-07 04:35:28
This commit is contained in:
+21
-2
@@ -47,10 +47,29 @@ $role = $decoded->role ?? 'passenger';
|
|||||||
//
|
//
|
||||||
// نقاط تسجيل الدخول (Admin/auth/*, jwtService.php) لا تستدعي connect.php
|
// نقاط تسجيل الدخول (Admin/auth/*, jwtService.php) لا تستدعي connect.php
|
||||||
// أصلاً، فلا تتأثر بهذه البوّابة ولا يمكن أن تُقفل على نفسها.
|
// أصلاً، فلا تتأثر بهذه البوّابة ولا يمكن أن تُقفل على نفسها.
|
||||||
$adminDir = realpath(__DIR__ . '/Admin');
|
//
|
||||||
|
// 2026-08-07: وُسِّعت البوّابة لتشمل serviceapp/ أيضاً. كان 34 من 39 ملفاً
|
||||||
|
// هناك بلا أي فحص دور — بينها getDriverByPhone و getPassengersByPhone و
|
||||||
|
// update_complaint — أي أن أي حامل JWT صالح (راكب عادي) يقرأ بيانات
|
||||||
|
// السائقين والركّاب ويغلق الشكاوى. تحققنا أن siro_rider و siro_driver لا
|
||||||
|
// يستدعيان أي نقطة تحت serviceapp/، فالإغلاق لا يكسر تطبيقات الجوال.
|
||||||
|
$gatedDirs = array_filter([
|
||||||
|
realpath(__DIR__ . '/Admin'),
|
||||||
|
realpath(__DIR__ . '/serviceapp'),
|
||||||
|
]);
|
||||||
$script = realpath($_SERVER['SCRIPT_FILENAME'] ?? '');
|
$script = realpath($_SERVER['SCRIPT_FILENAME'] ?? '');
|
||||||
|
|
||||||
if ($adminDir && $script && str_starts_with($script, $adminDir . DIRECTORY_SEPARATOR)) {
|
$inGatedDir = false;
|
||||||
|
if ($script) {
|
||||||
|
foreach ($gatedDirs as $dir) {
|
||||||
|
if (str_starts_with($script, $dir . DIRECTORY_SEPARATOR)) {
|
||||||
|
$inGatedDir = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($inGatedDir) {
|
||||||
// نقاط تحت Admin/ تستدعيها تطبيقات الجوال بأدوار غير إدارية.
|
// نقاط تحت Admin/ تستدعيها تطبيقات الجوال بأدوار غير إدارية.
|
||||||
// تم التحقق من مواضع الاستدعاء الفعلية في siro_rider و siro_driver:
|
// تم التحقق من مواضع الاستدعاء الفعلية في siro_rider و siro_driver:
|
||||||
// errorApp.php → التطبيقان يبلّغان عن الأخطاء
|
// errorApp.php → التطبيقان يبلّغان عن الأخطاء
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
-- 2026_08_07_complaint_resolution.sql
|
||||||
|
--
|
||||||
|
-- دورة الشكوى وأداة التعويض.
|
||||||
|
--
|
||||||
|
-- الوضع اليوم: serviceapp/update_complaint.php يقلب الحالة ويكتب نصّ
|
||||||
|
-- قرار. لا تعويض، لا تصنيف سبب، ولا قياس زمن. والموظف الذي يريد
|
||||||
|
-- تعويض راكب متضرر لا يملك أي أداة — فيَعِد شفهياً أو لا يفعل شيئاً.
|
||||||
|
--
|
||||||
|
-- كلمة "refund" لا ترد إطلاقاً في مسار الرحلات، فقط في وحدة الطعام.
|
||||||
|
--
|
||||||
|
-- هذا الترحيل يحوّل الدعم من "استقبال شكاوى" إلى "إغلاق قضايا".
|
||||||
|
--
|
||||||
|
-- ⚠️ يُنفَّذ على قاعدة primary (jorSiroDB) وحدها.
|
||||||
|
|
||||||
|
-- ══════════════════════════════════════════════════════════════
|
||||||
|
-- ١) قياس الزمن (SLA)
|
||||||
|
--
|
||||||
|
-- date_resolved موجود أصلاً، لكن زمن **أول رد** هو المؤشر الذي يشعر به
|
||||||
|
-- العميل فعلاً — شكوى تُغلق بعد يومين بردٍّ خلال دقائق أفضل بكثير من
|
||||||
|
-- شكوى تُغلق بعد ساعة بلا أن يعلم صاحبها شيئاً.
|
||||||
|
-- ══════════════════════════════════════════════════════════════
|
||||||
|
ALTER TABLE `complaint`
|
||||||
|
ADD COLUMN `first_response_at` DATETIME NULL DEFAULT NULL
|
||||||
|
COMMENT 'لحظة أول رد من الدعم — يُختم مرة واحدة ولا يتغيّر',
|
||||||
|
ADD COLUMN `assigned_to` VARCHAR(100) NULL DEFAULT NULL
|
||||||
|
COMMENT 'معرّف الموظف المسؤول عن القضية',
|
||||||
|
ADD COLUMN `reason_code` VARCHAR(40) NULL DEFAULT NULL
|
||||||
|
COMMENT 'تصنيف موحّد للسبب — قائمة مغلقة في resolve_complaint.php',
|
||||||
|
ADD INDEX `idx_status_filed` (`statusComplaint`, `date_filed`),
|
||||||
|
ADD INDEX `idx_ride` (`ride_id`);
|
||||||
|
|
||||||
|
-- ══════════════════════════════════════════════════════════════
|
||||||
|
-- ٢) سجل التعويضات
|
||||||
|
--
|
||||||
|
-- جدول مستقل لا أعمدة في complaint: الشكوى الواحدة قد تُعوَّض أكثر من
|
||||||
|
-- مرة (استرجاع جزئي ثم رصيد اعتذار)، وكل حركة مالية تحتاج أثراً مستقلاً
|
||||||
|
-- يُراجَع ويُدقَّق.
|
||||||
|
-- ══════════════════════════════════════════════════════════════
|
||||||
|
CREATE TABLE IF NOT EXISTS `complaint_compensations` (
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`complaint_id` INT NOT NULL,
|
||||||
|
`ride_id` VARCHAR(20) NULL DEFAULT NULL,
|
||||||
|
|
||||||
|
`beneficiary_type` VARCHAR(20) NOT NULL
|
||||||
|
COMMENT 'passenger | driver — لمن ذهب التعويض',
|
||||||
|
`beneficiary_id` VARCHAR(100) NOT NULL,
|
||||||
|
|
||||||
|
`kind` VARCHAR(20) NOT NULL
|
||||||
|
COMMENT 'refund (استرجاع من قيمة الرحلة) | goodwill (رصيد اعتذار)',
|
||||||
|
`amount` DECIMAL(12,3) NOT NULL,
|
||||||
|
`currency` VARCHAR(5) NOT NULL DEFAULT 'SYP',
|
||||||
|
|
||||||
|
`issued_by` VARCHAR(100) NOT NULL COMMENT 'الموظف الذي صرفه',
|
||||||
|
`note` VARCHAR(500) NULL DEFAULT NULL,
|
||||||
|
|
||||||
|
-- نتيجة نداء المحفظة. الفشل يبقى مسجَّلاً للتسوية اليدوية بدل أن
|
||||||
|
-- يختفي في اللوجز — العميل وُعد بمبلغ ويجب أن يصله.
|
||||||
|
`transfer_status` VARCHAR(20) NOT NULL DEFAULT 'pending'
|
||||||
|
COMMENT 'pending | success | failed',
|
||||||
|
`transfer_code` SMALLINT NULL DEFAULT NULL,
|
||||||
|
|
||||||
|
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_complaint` (`complaint_id`),
|
||||||
|
KEY `idx_beneficiary` (`beneficiary_type`, `beneficiary_id`),
|
||||||
|
KEY `idx_failed` (`transfer_status`, `created_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
|
||||||
|
COMMENT='تعويضات الشكاوى — كل حركة مالية بأثر مستقل قابل للتدقيق';
|
||||||
|
|
||||||
|
-- للتراجع:
|
||||||
|
-- ALTER TABLE `complaint` DROP COLUMN `first_response_at`,
|
||||||
|
-- DROP COLUMN `assigned_to`, DROP COLUMN `reason_code`,
|
||||||
|
-- DROP INDEX `idx_status_filed`, DROP INDEX `idx_ride`;
|
||||||
|
-- DROP TABLE `complaint_compensations`;
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
// ============================================================
|
||||||
|
// serviceapp/complaint_sla_stats.php — قياس دورة الشكوى
|
||||||
|
//
|
||||||
|
// دورة بلا قياس تبقى "استقبال شكاوى" لا "إغلاق قضايا". هذه النقطة
|
||||||
|
// تعطي الأرقام الثلاثة التي تدير بها الفريق:
|
||||||
|
// • زمن أول رد ← ما يشعر به العميل فعلاً
|
||||||
|
// • زمن الإغلاق ← كفاءة الفريق
|
||||||
|
// • المفتوح الآن ← عبء اللحظة
|
||||||
|
//
|
||||||
|
// محمية بالبوّابة المسارية في connect.php (كل ما تحت serviceapp/).
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../connect.php';
|
||||||
|
|
||||||
|
$days = (int) (filterRequest("days", 'int') ?: 30);
|
||||||
|
// يُقصّ إلى عدد صحيح ضمن مدى آمن قبل أي إقحام في SQL. INTERVAL لا يقبل
|
||||||
|
// متغيراً مربوطاً، فالقصّ هنا هو الضمانة الوحيدة ضد الحقن.
|
||||||
|
$days = max(1, min(365, $days));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// ── الأزمنة ─────────────────────────────────────────────
|
||||||
|
// المتوسط وحده يخدع: شكوى واحدة أُهملت أسبوعاً ترفعه بينما الأغلبية
|
||||||
|
// تُعالَج بسرعة. الوسيط التقريبي عبر أقصى قيمة يكمّل الصورة.
|
||||||
|
$stmt = $con->prepare("
|
||||||
|
SELECT
|
||||||
|
COUNT(*) AS total,
|
||||||
|
SUM(first_response_at IS NOT NULL) AS answered,
|
||||||
|
ROUND(AVG(TIMESTAMPDIFF(MINUTE, date_filed, first_response_at)), 1) AS avg_first_response_min,
|
||||||
|
MAX(TIMESTAMPDIFF(MINUTE, date_filed, first_response_at)) AS worst_first_response_min,
|
||||||
|
SUM(statusComplaint = 'Resolved') AS resolved,
|
||||||
|
ROUND(AVG(CASE WHEN statusComplaint = 'Resolved'
|
||||||
|
THEN TIMESTAMPDIFF(MINUTE, date_filed, date_resolved) END), 1) AS avg_resolution_min
|
||||||
|
FROM complaint
|
||||||
|
WHERE date_filed >= NOW() - INTERVAL $days DAY
|
||||||
|
");
|
||||||
|
$stmt->execute();
|
||||||
|
$timing = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||||
|
|
||||||
|
// المفتوح الآن بلا نافذة زمنية: شكوى من الشهر الماضي ما زالت مفتوحة
|
||||||
|
// هي المشكلة، وحصرها في النافذة كان سيخفيها.
|
||||||
|
$openNow = (int) $con->query(
|
||||||
|
"SELECT COUNT(*) FROM complaint WHERE statusComplaint <> 'Resolved'"
|
||||||
|
)->fetchColumn();
|
||||||
|
|
||||||
|
$neverAnswered = (int) $con->query(
|
||||||
|
"SELECT COUNT(*) FROM complaint
|
||||||
|
WHERE statusComplaint <> 'Resolved' AND first_response_at IS NULL"
|
||||||
|
)->fetchColumn();
|
||||||
|
|
||||||
|
// ── التصنيف ─────────────────────────────────────────────
|
||||||
|
$stmtReasons = $con->prepare("
|
||||||
|
SELECT COALESCE(reason_code, 'unclassified') AS reason, COUNT(*) AS cnt
|
||||||
|
FROM complaint
|
||||||
|
WHERE date_filed >= NOW() - INTERVAL $days DAY
|
||||||
|
GROUP BY reason ORDER BY cnt DESC
|
||||||
|
");
|
||||||
|
$stmtReasons->execute();
|
||||||
|
|
||||||
|
// ── التعويضات ───────────────────────────────────────────
|
||||||
|
$compensation = ['total' => 0, 'count' => 0, 'failed' => 0];
|
||||||
|
try {
|
||||||
|
$stmtComp = $con->prepare("
|
||||||
|
SELECT
|
||||||
|
COUNT(*) AS cnt,
|
||||||
|
COALESCE(SUM(CASE WHEN transfer_status = 'success' THEN amount END), 0) AS total,
|
||||||
|
SUM(transfer_status = 'failed') AS failed
|
||||||
|
FROM complaint_compensations
|
||||||
|
WHERE created_at >= NOW() - INTERVAL $days DAY
|
||||||
|
");
|
||||||
|
$stmtComp->execute();
|
||||||
|
$row = $stmtComp->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||||
|
$compensation = [
|
||||||
|
'count' => (int) ($row['cnt'] ?? 0),
|
||||||
|
'total' => (float) ($row['total'] ?? 0),
|
||||||
|
// التعويضات الفاشلة أهم رقم هنا: عميل وُعد بمبلغ ولم يصله.
|
||||||
|
'failed' => (int) ($row['failed'] ?? 0),
|
||||||
|
];
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
error_log("[complaint/sla] جدول التعويضات غير مُرحَّل بعد: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonSuccess([
|
||||||
|
'window_days' => $days,
|
||||||
|
'timing' => $timing,
|
||||||
|
'open_now' => $openNow,
|
||||||
|
'never_answered' => $neverAnswered,
|
||||||
|
'by_reason' => $stmtReasons->fetchAll(PDO::FETCH_ASSOC),
|
||||||
|
'compensation' => $compensation,
|
||||||
|
], "ok");
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
error_log("[complaint/sla] " . $e->getMessage());
|
||||||
|
jsonError("DB Error", 500);
|
||||||
|
}
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
<?php
|
||||||
|
// ============================================================
|
||||||
|
// serviceapp/resolve_complaint.php — أداة التسوية
|
||||||
|
//
|
||||||
|
// نقطة واحدة يغلق بها الموظف القضية: يصنّف السبب، يحدد الحالة، ويصرف
|
||||||
|
// تعويضاً إن استحق — بدل أن يقلب حالة ويكتب نصاً ثم يَعِد شفهياً بمبلغ
|
||||||
|
// لا يملك أداة لصرفه.
|
||||||
|
//
|
||||||
|
// تعمل مع siro_service و siro_admin معاً: نفس الصلاحية ونفس المنطق.
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../connect.php';
|
||||||
|
require_once __DIR__ . '/../ride/pricing/pricing_helper.php';
|
||||||
|
|
||||||
|
// فحص الدور صراحةً — مجلد serviceapp ليس تحت بوّابة Admin/ المسارية في
|
||||||
|
// connect.php، وكل ملف هنا مسؤول عن حماية نفسه. (update_complaint.php
|
||||||
|
// الأصلي كان بلا فحص، فكان أي حامل JWT يغلق شكوى ويكتب قرارها.)
|
||||||
|
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||||
|
http_response_code(403);
|
||||||
|
exit(json_encode([
|
||||||
|
'status' => 'failure',
|
||||||
|
'message' => 'Forbidden. Support access required.',
|
||||||
|
], JSON_UNESCAPED_UNICODE));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── قوائم مغلقة ─────────────────────────────────────────────
|
||||||
|
// نص حر في التصنيف يجعل التقارير عديمة المعنى بعد شهر.
|
||||||
|
const COMPLAINT_STATUSES = ['Open', 'In Progress', 'Resolved'];
|
||||||
|
const COMPLAINT_REASONS = [
|
||||||
|
'driver_behavior', // سلوك السائق
|
||||||
|
'passenger_behavior', // سلوك الراكب
|
||||||
|
'overcharge', // سعر أعلى من المتوقع
|
||||||
|
'route_issue', // مسار خاطئ أو أطول
|
||||||
|
'no_show', // عدم حضور
|
||||||
|
'vehicle_condition', // حالة المركبة
|
||||||
|
'app_issue', // عطل تقني
|
||||||
|
'payment_issue', // مشكلة دفع
|
||||||
|
'safety', // سلامة
|
||||||
|
'other',
|
||||||
|
];
|
||||||
|
const COMPENSATION_KINDS = ['refund', 'goodwill'];
|
||||||
|
const COMPENSATION_BENEFICIARIES = ['passenger', 'driver'];
|
||||||
|
|
||||||
|
// سقف صلب على التعويض الواحد. صلاحية صرف المال بيد موظف دعم تحتاج حداً
|
||||||
|
// أعلى لا يتجاوزه خطأ مطبعي (صفر زائد) ولا حساب مخترَق.
|
||||||
|
const COMPENSATION_ABSOLUTE_CAP = 50000.0;
|
||||||
|
|
||||||
|
$complaintId = filterRequest("complaint_id", 'int');
|
||||||
|
$newStatus = filterRequest("status");
|
||||||
|
$reasonCode = filterRequest("reason_code");
|
||||||
|
$resolution = filterRequest("resolution");
|
||||||
|
$faultOn = filterRequest("fault_determination");
|
||||||
|
|
||||||
|
// التعويض اختياري: قد تُغلق القضية بلا مال.
|
||||||
|
$compAmount = filterRequest("compensation_amount", 'float');
|
||||||
|
$compKind = filterRequest("compensation_kind");
|
||||||
|
$compBeneficiary = filterRequest("compensation_beneficiary");
|
||||||
|
$compNote = filterRequest("compensation_note");
|
||||||
|
|
||||||
|
if (!$complaintId) {
|
||||||
|
jsonError("Missing complaint_id");
|
||||||
|
}
|
||||||
|
if ($newStatus && !in_array($newStatus, COMPLAINT_STATUSES, true)) {
|
||||||
|
jsonError("Invalid status");
|
||||||
|
}
|
||||||
|
if ($reasonCode && !in_array($reasonCode, COMPLAINT_REASONS, true)) {
|
||||||
|
jsonError("Invalid reason_code");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $con->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;
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
// ١) تحديث القضية
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
$sets = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
// أول رد يُختم مرة واحدة ولا يتغيّر — هو مؤشر الـSLA الذي يشعر به
|
||||||
|
// العميل. لمسة ثانية من الموظف لا تعيد ضبطه.
|
||||||
|
if (empty($complaint['first_response_at'])) {
|
||||||
|
$sets[] = "first_response_at = NOW()";
|
||||||
|
}
|
||||||
|
|
||||||
|
// من يلمس القضية يملكها. يمنع "الكل مسؤول فلا أحد مسؤول".
|
||||||
|
if (empty($complaint['assigned_to'])) {
|
||||||
|
$sets[] = "assigned_to = ?";
|
||||||
|
$params[] = $user_id ?? 'support';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($newStatus) {
|
||||||
|
$sets[] = "statusComplaint = ?";
|
||||||
|
$params[] = $newStatus;
|
||||||
|
if ($newStatus === 'Resolved' && empty($complaint['date_resolved'])) {
|
||||||
|
$sets[] = "date_resolved = NOW()";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($reasonCode) { $sets[] = "reason_code = ?"; $params[] = $reasonCode; }
|
||||||
|
if ($resolution) { $sets[] = "resolution = ?"; $params[] = mb_substr($resolution, 0, 2000); }
|
||||||
|
if ($faultOn) { $sets[] = "fault_determination = ?"; $params[] = mb_substr($faultOn, 0, 255); }
|
||||||
|
|
||||||
|
if (!empty($sets)) {
|
||||||
|
$params[] = $complaintId;
|
||||||
|
$con->prepare("UPDATE complaint SET " . implode(', ', $sets) . " WHERE id = ?")
|
||||||
|
->execute($params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
// ٢) التعويض
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
$compensation = null;
|
||||||
|
|
||||||
|
if ($compAmount !== null && $compAmount > 0) {
|
||||||
|
if (!in_array($compKind, COMPENSATION_KINDS, true)) {
|
||||||
|
jsonError("Invalid compensation_kind");
|
||||||
|
}
|
||||||
|
if (!in_array($compBeneficiary, COMPENSATION_BENEFICIARIES, true)) {
|
||||||
|
jsonError("Invalid compensation_beneficiary");
|
||||||
|
}
|
||||||
|
if ($compAmount > COMPENSATION_ABSOLUTE_CAP) {
|
||||||
|
jsonError("Compensation exceeds the allowed limit");
|
||||||
|
}
|
||||||
|
|
||||||
|
$beneficiaryId = $compBeneficiary === 'passenger'
|
||||||
|
? ($complaint['passenger_id'] ?? '')
|
||||||
|
: ($complaint['driver_id'] ?? '');
|
||||||
|
|
||||||
|
if (empty($beneficiaryId)) {
|
||||||
|
jsonError("Complaint has no $compBeneficiary to compensate");
|
||||||
|
}
|
||||||
|
|
||||||
|
// استرجاع لا يتجاوز قيمة الرحلة: "refund" يعني إعادة ما دُفع، وما
|
||||||
|
// فوقه رصيد اعتذار (goodwill) يُصنَّف كذلك ويُراجَع كذلك.
|
||||||
|
$kazan = $rideId ? getKazanForRide($con, $rideId) : [];
|
||||||
|
$currency = !empty($kazan['currency'])
|
||||||
|
? (string) $kazan['currency']
|
||||||
|
: getCurrencyByCountry((string) ($kazan['country'] ?? 'Syria'));
|
||||||
|
|
||||||
|
if ($compKind === 'refund' && $rideId) {
|
||||||
|
$stmtRide = $con->prepare("SELECT price FROM ride WHERE id = ? LIMIT 1");
|
||||||
|
$stmtRide->execute([$rideId]);
|
||||||
|
$ridePrice = (float) ($stmtRide->fetchColumn() ?: 0);
|
||||||
|
|
||||||
|
if ($ridePrice > 0 && $compAmount > $ridePrice) {
|
||||||
|
jsonError("Refund cannot exceed the ride price ($ridePrice $currency)."
|
||||||
|
. " Use goodwill for anything above it.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// السجل أولاً بحالة pending، ثم التحويل. العكس كان يترك مالاً
|
||||||
|
// مصروفاً بلا أثر إن انقطع التنفيذ بينهما.
|
||||||
|
$insComp = $con->prepare("
|
||||||
|
INSERT INTO complaint_compensations
|
||||||
|
(complaint_id, ride_id, beneficiary_type, beneficiary_id,
|
||||||
|
kind, amount, currency, issued_by, note)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
");
|
||||||
|
$insComp->execute([
|
||||||
|
$complaintId, $rideId, $compBeneficiary, $beneficiaryId,
|
||||||
|
$compKind, $compAmount, $currency,
|
||||||
|
$user_id ?? 'support',
|
||||||
|
$compNote ? mb_substr($compNote, 0, 500) : null,
|
||||||
|
]);
|
||||||
|
$compId = (int) $con->lastInsertId();
|
||||||
|
|
||||||
|
$code = transferCompensation(
|
||||||
|
$con, $rideId, $compBeneficiary, $beneficiaryId, $compAmount, $compId
|
||||||
|
);
|
||||||
|
|
||||||
|
$con->prepare("
|
||||||
|
UPDATE complaint_compensations
|
||||||
|
SET transfer_status = ?, transfer_code = ?
|
||||||
|
WHERE id = ?
|
||||||
|
")->execute([$code === 200 ? 'success' : 'failed', $code, $compId]);
|
||||||
|
|
||||||
|
if ($code !== 200) {
|
||||||
|
error_log("[complaint] MONEY: تعويض #$compId بقيمة $compAmount $currency"
|
||||||
|
. " للشكوى #$complaintId لم يصل ($compBeneficiary=$beneficiaryId، رمز=$code)");
|
||||||
|
}
|
||||||
|
|
||||||
|
$compensation = [
|
||||||
|
'id' => $compId,
|
||||||
|
'amount' => $compAmount,
|
||||||
|
'currency' => $currency,
|
||||||
|
'kind' => $compKind,
|
||||||
|
'status' => $code === 200 ? 'success' : 'failed',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log("[complaint] #$complaintId → " . ($newStatus ?: 'بلا تغيير حالة')
|
||||||
|
. " بواسطة " . ($user_id ?? 'support')
|
||||||
|
. ($compensation ? " (+تعويض {$compensation['amount']})" : ""));
|
||||||
|
|
||||||
|
jsonSuccess([
|
||||||
|
'complaint_id' => $complaintId,
|
||||||
|
'status' => $newStatus ?: $complaint['statusComplaint'],
|
||||||
|
'compensation' => $compensation,
|
||||||
|
], "Complaint updated");
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
error_log("[complaint/resolve] " . $e->getMessage());
|
||||||
|
jsonError("DB Error", 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* يحوّل التعويض إلى محفظة المستفيد. يرجع رمز HTTP من خادم المحفظة.
|
||||||
|
*
|
||||||
|
* paymentID مشتق من رقم التعويض لا من الشكوى: الشكوى الواحدة قد تُعوَّض
|
||||||
|
* مرتين (استرجاع ثم اعتذار)، واشتقاقه من الشكوى كان سيجعل الثانية
|
||||||
|
* تُرفض كمكرّرة.
|
||||||
|
*/
|
||||||
|
function transferCompensation(
|
||||||
|
PDO $con, $rideId, string $beneficiaryType, string $beneficiaryId,
|
||||||
|
float $amount, int $compId
|
||||||
|
): int {
|
||||||
|
$kazan = $rideId ? getKazanForRide($con, $rideId) : [];
|
||||||
|
$country = strtolower((string) ($kazan['country'] ?? 'jordan'));
|
||||||
|
|
||||||
|
$walletServer = "https://walletintaleq.intaleq.xyz";
|
||||||
|
if ($country === 'jordan') {
|
||||||
|
$walletServer = getenv('WALLET_SERVER_JORDAN') ?: $walletServer;
|
||||||
|
} elseif ($country === 'egypt') {
|
||||||
|
$walletServer = getenv('WALLET_SERVER_EGYPT') ?: $walletServer;
|
||||||
|
} else {
|
||||||
|
$walletServer = getenv('WALLET_SERVER_SYRIA') ?: $walletServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($beneficiaryType === 'driver') {
|
||||||
|
$url = "$walletServer/v2/main/ride/driverWallet/add_s2s_reward.php";
|
||||||
|
$fields = [
|
||||||
|
'driverID' => $beneficiaryId,
|
||||||
|
'paymentID' => "complaint_comp_$compId",
|
||||||
|
'amount' => $amount,
|
||||||
|
'paymentMethod' => 'complaint_compensation',
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// محفظة الراكب تستقبل الأرصدة كقيمة موجبة على نفس نقطة الدين.
|
||||||
|
$url = "$walletServer/v2/main/ride/passengerWallet/add_s2s_debt.php";
|
||||||
|
$fields = ['passengerID' => $beneficiaryId, 'amount' => $amount];
|
||||||
|
}
|
||||||
|
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => http_build_query($fields),
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => 8,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Content-Type: application/x-www-form-urlencoded',
|
||||||
|
'X-S2S-Api-Key: ' . getenv('S2S_SHARED_KEY'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
curl_exec($ch);
|
||||||
|
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
return $code;
|
||||||
|
}
|
||||||
@@ -1,6 +1,17 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once __DIR__ . '/../connect.php';
|
require_once __DIR__ . '/../connect.php';
|
||||||
|
|
||||||
|
// فحص الدور — كان غائباً تماماً بينما شقيقه getComplaintAllData.php يفحصه.
|
||||||
|
// مجلد serviceapp خارج بوّابة Admin/ المسارية، فكل ملف يحمي نفسه، وهذا
|
||||||
|
// كان مكشوفاً: أي حامل JWT صالح يقدر يغلق أي شكوى ويكتب نص قرارها.
|
||||||
|
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||||
|
http_response_code(403);
|
||||||
|
exit(json_encode([
|
||||||
|
'status' => 'failure',
|
||||||
|
'message' => 'Forbidden. Support access required.',
|
||||||
|
], JSON_UNESCAPED_UNICODE));
|
||||||
|
}
|
||||||
|
|
||||||
$id = filterRequest("id");
|
$id = filterRequest("id");
|
||||||
$status = filterRequest("statusComplaint");
|
$status = filterRequest("statusComplaint");
|
||||||
$resolution = filterRequest("resolution");
|
$resolution = filterRequest("resolution");
|
||||||
|
|||||||
Reference in New Issue
Block a user