Files
Siro/backend/serviceapp/resolve_complaint.php

263 lines
12 KiB
PHP
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 يغلق شكوى ويكتب قرارها.)
// ‏البوّابة المسارية في connect.php تفرض بالفعل service/admin/super_admin
// ‏على كل ما تحت serviceapp/. لا نكرّرها هنا: نسختان من نفس الفحص تتباعدان
// ‏بصمت، وقد كتبتُ هذه أولاً بـ admin وحده فمنعت كل موظفي الخدمة.
// ── قوائم مغلقة ─────────────────────────────────────────────
// ‏نص حر في التصنيف يجعل التقارير عديمة المعنى بعد شهر.
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'];
$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");
}
$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'));
// ‏السقف الصلب بعملة الدولة — يُفحص بعد معرفة العملة لا قبلها.
// ‏صلاحية صرف المال بيد موظف دعم تحتاج حداً لا يتجاوزه خطأ مطبعي.
$hardCap = getMoneyHardCap($currency);
if ($compAmount > $hardCap) {
jsonError("Compensation exceeds the allowed limit ($hardCap $currency)");
}
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;
}