Update: 2026-08-07 00:36:27

This commit is contained in:
Hamza-Ayed
2026-08-07 00:36:27 +03:00
parent 4d64a205fa
commit 6e451e8b71
5 changed files with 816 additions and 1035 deletions
@@ -0,0 +1,22 @@
-- 2026_08_07_kazan_cancel_penalty_policy.sql
--
-- ‏سبب الترحيل: كان مبلغ غرامة الإلغاء في ride/rides/cancel_ride_by_driver.php
-- ‏يُقرأ من طلب العميل مباشرة (filterRequest("penalty_fee")) ويُخصم من محفظة
-- ‏الراكب بلا سقف ولا تحقق — أي أن طلباً مصاغاً يدوياً كان يقدر يخصم أي مبلغ
-- ‏من أي راكب. صار المبلغ يُقرأ من هنا فقط، ولا يقبل الخادم أي قيمة من العميل.
--
-- ‏القيمة الافتراضية '0' تحفظ السلوك الحالي حرفياً: لا عميل يرسل penalty_fee
-- ‏اليوم، فالغرامة لا تُطبَّق فعلياً. تفعيلها قرار إداري صريح بتحديث الصف.
--
-- ‏القاعدة المستهدفة: primary فقط — الكود يقرأ kazan عبر $con، وجدول kazan في
-- ‏قاعدتي ride/tracking بنية أقدم ومختلفة الأعمدة (لا يُستخدم في هذا المسار).
ALTER TABLE `kazan`
ADD COLUMN `cancelPenaltyFee` VARCHAR(10)
CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci
NOT NULL DEFAULT '0'
COMMENT 'غرامة إلغاء السائق بعد القبول، تُخصم من محفظة الراكب. 0 = معطّلة'
AFTER `startPrice`;
-- ‏للتراجع:
-- ALTER TABLE `kazan` DROP COLUMN `cancelPenaltyFee`;
+59 -8
View File
@@ -14,7 +14,18 @@ $rideId = filterRequest("ride_id");
$driverId = filterRequest("driver_id"); $driverId = filterRequest("driver_id");
$reason = filterRequest("reason"); $reason = filterRequest("reason");
$passengerToken = filterRequest("passenger_token"); $passengerToken = filterRequest("passenger_token");
$penaltyFee = (float) filterRequest("penalty_fee");
// ‏مبلغ الغرامة لا يُقرأ من الطلب إطلاقاً. كان سابقاً filterRequest("penalty_fee")
// ‏فيُخصم من محفظة الراكب بلا سقف ولا تحقق. صار يُحسم من سياسة الخادم أدناه.
// ‏إن وصل المفتاح من عميل فهو إما نسخة قديمة أو محاولة عبث — نسجّله ونتجاهله.
if (filterRequest("penalty_fee") !== null && filterRequest("penalty_fee") !== '') {
error_log("[cancel_ride_by_driver] SECURITY: تجاهل penalty_fee من العميل"
. " (ride=$rideId driver=$driverId) — السياسة تُقرأ من kazan فقط");
}
// ‏سقف صلب فوق أي قيمة في قاعدة البيانات — يمنع خطأ إدارياً أو صفاً تالفاً
// ‏من تحويل الغرامة إلى خصم كارثي.
const CANCEL_PENALTY_ABSOLUTE_CAP = 50000.0;
// تثبيت الحالة // تثبيت الحالة
$statusText = "CancelFromDriverAfterApply"; $statusText = "CancelFromDriverAfterApply";
@@ -27,6 +38,28 @@ if (!$rideId || !$driverId) {
try { try {
$con->beginTransaction(); $con->beginTransaction();
// ---------------------------------------------------------
// 0. التحقق من الملكية — الرحلة يجب أن تكون مُسندة لهذا السائق
// ‏بدون هذا الفحص كان أي driver_id يقدر يلغي أي رحلة ويغرّم راكبها.
// ---------------------------------------------------------
$stmtOwn = $con->prepare("SELECT driver_id FROM ride WHERE id = ? FOR UPDATE");
$stmtOwn->execute([$rideId]);
$assignedDriver = $stmtOwn->fetchColumn();
if ($assignedDriver === false) {
$con->rollBack();
jsonError("Ride not found");
exit;
}
if ((string) $assignedDriver !== (string) $driverId) {
$con->rollBack();
error_log("[cancel_ride_by_driver] SECURITY: رفض إلغاء رحلة غير مُسندة"
. " (ride=$rideId caller=$driverId assigned=$assignedDriver)");
jsonError("Ride is not assigned to this driver");
exit;
}
// --------------------------------------------------------- // ---------------------------------------------------------
// 1. معالجة driver_orders (Insert or Update) // 1. معالجة driver_orders (Insert or Update)
// --------------------------------------------------------- // ---------------------------------------------------------
@@ -99,17 +132,35 @@ try {
notifyPassengerOnRideServer($passenger_id, $socketPayload); notifyPassengerOnRideServer($passenger_id, $socketPayload);
} }
// 4.1. إضافة غرامة الإلغاء على الراكب (الدين) إذا كانت موجودة // 4.1. غرامة الإلغاء على الراكب — المبلغ من سياسة الخادم لا من العميل
// Resolve country + penalty policy (kazan هو مصدر الحقيقة الوحيد)
// ‏قراءة متسامحة: لو نزل الكود قبل ترحيل cancelPenaltyFee، لا نُسقط
// ‏الإلغاء كله — نرجع لقراءة الدولة وحدها ونعتبر الغرامة صفراً.
try {
$stmtKazan = $con->prepare("SELECT country, cancelPenaltyFee FROM kazan LIMIT 1");
$stmtKazan->execute();
$kazan = $stmtKazan->fetch(PDO::FETCH_ASSOC) ?: [];
} catch (PDOException $eKazan) {
error_log("[cancel_ride_by_driver] cancelPenaltyFee غير موجود بعد"
. " (شغّل migrations/2026_08_07_kazan_cancel_penalty_policy.sql): "
. $eKazan->getMessage());
$stmtKazan = $con->prepare("SELECT country FROM kazan LIMIT 1");
$stmtKazan->execute();
$kazan = $stmtKazan->fetch(PDO::FETCH_ASSOC) ?: [];
}
$country = $kazan['country'] ?? 'Jordan';
$penaltyFee = (float) ($kazan['cancelPenaltyFee'] ?? 0);
if ($penaltyFee > CANCEL_PENALTY_ABSOLUTE_CAP) {
error_log("[cancel_ride_by_driver] kazan.cancelPenaltyFee=$penaltyFee"
. " تجاوز السقف الصلب — قُصّ إلى " . CANCEL_PENALTY_ABSOLUTE_CAP);
$penaltyFee = CANCEL_PENALTY_ABSOLUTE_CAP;
}
if ($penaltyFee > 0) { if ($penaltyFee > 0) {
// إضافة القيمة كدين سالب في المحفظة // إضافة القيمة كدين سالب في المحفظة
$negativeDebt = -$penaltyFee; $negativeDebt = -$penaltyFee;
// Resolve country and wallet server
$stmtKazan = $con->prepare("SELECT country FROM kazan LIMIT 1");
$stmtKazan->execute();
$kazan = $stmtKazan->fetch(PDO::FETCH_ASSOC) ?: ["country" => "Jordan"];
$country = $kazan['country'] ?? 'Jordan';
$walletServer = "https://walletintaleq.intaleq.xyz"; $walletServer = "https://walletintaleq.intaleq.xyz";
if (strtolower($country) == 'jordan') { if (strtolower($country) == 'jordan') {
$walletServer = getenv('WALLET_SERVER_JORDAN') ?: "https://walletintaleq.intaleq.xyz"; $walletServer = getenv('WALLET_SERVER_JORDAN') ?: "https://walletintaleq.intaleq.xyz";
+1
View File
@@ -839,6 +839,7 @@ CREATE TABLE `kazan` (
`fuelPrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'سعر الوقود المرجعي', `fuelPrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'سعر الوقود المرجعي',
`currency` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'SYP' COMMENT 'رمز العملة: SYP, EGP, JOD, IQD, ...', `currency` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'SYP' COMMENT 'رمز العملة: SYP, EGP, JOD, IQD, ...',
`startPrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT 'فتحة العداد', `startPrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT 'فتحة العداد',
`cancelPenaltyFee` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT 'غرامة إلغاء السائق بعد القبول، تُخصم من محفظة الراكب. 0 = معطّلة',
-- ========== 🚗 أسعار الكيلومتر لكل نوع سيارة ========== -- ========== 🚗 أسعار الكيلومتر لكل نوع سيارة ==========
`speedPrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'سعر الكيلومتر - Speed', `speedPrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'سعر الكيلومتر - Speed',
+4 -1
View File
@@ -209,7 +209,10 @@ services:
PMA_PORT: 3306 PMA_PORT: 3306
UPLOAD_LIMIT: 300M UPLOAD_LIMIT: 300M
ports: ports:
- "8082:80" # ادخل عليه عبر http://your-domain:8082 أو افتح بورت 8082 في CloudPanel # ‏مقيَّد على loopback عمداً: الربط السابق "8082:80" كان يكشف phpMyAdmin
# ‏للإنترنت بلا TLS (‏docker-proxy يلتفّ على UFW). الوصول عبر نفق SSH فقط:
# ‏ssh -N -L 8082:127.0.0.1:8082 USER@SERVER
- "127.0.0.1:8082:80"
depends_on: depends_on:
- mysql - mysql
mem_limit: 256m mem_limit: 256m
+748 -1044
View File
File diff suppressed because it is too large Load Diff