Update: 2026-07-02 15:35:12

This commit is contained in:
Hamza-Ayed
2026-07-02 15:35:12 +03:00
parent 2adf195b36
commit 75e6fc42d7
17 changed files with 389 additions and 19 deletions
+2
View File
@@ -129,6 +129,8 @@ APP_ENV=production
APP_DEBUG=false APP_DEBUG=false
APP_NAME=Siro APP_NAME=Siro
APP_DOMAIN=api-syria.siromove.com APP_DOMAIN=api-syria.siromove.com
APP_COUNTRY=Jordan
APP_CURRENCY=JOD
# ============================================================================= # =============================================================================
# Nabeh Integration (server-to-server API key) # Nabeh Integration (server-to-server API key)
+17
View File
@@ -539,6 +539,23 @@ function sendFCM_Internal(
return $fcm->send($target, $title, $body, is_array($customData) ? $customData : [], $category, $tone); return $fcm->send($target, $title, $body, is_array($customData) ? $customData : [], $category, $tone);
} }
// 🆕 دالة ذكية لإرسال وإرساء إشعارات السائقين في قاعدة البيانات (لضمان بقائها حتى لو لم يقرأها)
function sendAndSaveDriverNotification($con, $driver_id, $token, $title, $body, $customData = [], $category = 'System', $tone = 'ding') {
// 1. حفظ في قاعدة البيانات (لتظهر في صفحة الإشعارات داخل التطبيق)
try {
$stmt = $con->prepare("INSERT INTO notificationCaptain (driverID, title, body) VALUES (?, ?, ?)");
$stmt->execute([$driver_id, $title, $body]);
} catch (Exception $e) {
error_log("[sendAndSaveDriverNotification] DB Save Error: " . $e->getMessage());
}
// 2. إرسال Push Notification
if (!empty($token) && $token !== 'all') {
return sendFCM_Internal($token, $title, $body, $customData, $category, false, $tone);
}
return true;
}
function logAudit($con, $adminId, $action, $tableName = null, $recordId = null, $details = null) { function logAudit($con, $adminId, $action, $tableName = null, $recordId = null, $details = null) {
@@ -8,10 +8,30 @@ if (!$driver_id) {
} }
try { try {
// 1. Get Country and Currency Info global $redis;
$stmtKazan = $con->prepare("SELECT country, currency FROM kazan LIMIT 1"); $redisKey = "gamification_dashboard:{$driver_id}";
$stmtKazan->execute();
$kazan = $stmtKazan->fetch(PDO::FETCH_ASSOC) ?: ["country" => "Jordan", "currency" => "JOD"]; // 1. Try to read from Redis first
if (isset($redis)) {
try {
$cached = $redis->get($redisKey);
if ($cached) {
$data = json_decode($cached, true);
if ($data) {
jsonSuccess($data);
exit;
}
}
} catch (Exception $e) {
error_log("Redis read error in getGamificationDashboard: " . $e->getMessage());
}
}
// 2. Fallback to Database & External API
// Get Country and Currency Info from Environment Variables (to avoid querying kazan table)
$country = getenv('APP_COUNTRY') ?: getenv('COUNTRY') ?: "Jordan";
$currency = getenv('APP_CURRENCY') ?: getenv('CURRENCY') ?: "JOD";
$kazan = ["country" => $country, "currency" => $currency];
// 2. Get Total Completed Trips // 2. Get Total Completed Trips
$stmtTrips = $con->prepare("SELECT COUNT(*) as count FROM `ride` WHERE driver_id = :driver_id AND status = 'Finished'"); $stmtTrips = $con->prepare("SELECT COUNT(*) as count FROM `ride` WHERE driver_id = :driver_id AND status = 'Finished'");
@@ -94,10 +114,33 @@ try {
} }
// 8. Calculate Normalized Points // 8. Calculate Normalized Points
// 10 pts per finished trip, 100 pts per referral invite, 2 pts per behavior score point + claimed challenge points
$normalizedPoints = ($totalTrips * 10) + ($totalReferrals * 100) + ((int)$behavior['avg_score'] * 2) + $challengePoints; $normalizedPoints = ($totalTrips * 10) + ($totalReferrals * 100) + ((int)$behavior['avg_score'] * 2) + $challengePoints;
jsonSuccess([ // 9. Fetch Available Challenges
$stmtCh = $con->prepare("SELECT * FROM gamification_challenges WHERE is_active = 1 AND country = :country ORDER BY target_count ASC");
$stmtCh->execute([':country' => $kazan['country']]);
$challengesRaw = $stmtCh->fetchAll(PDO::FETCH_ASSOC);
$challenges = [];
$tier = 'Bronze'; // Placeholder logic
foreach ($challengesRaw as $chRaw) {
$isCompleted = false;
$isClaimed = false;
$challenges[] = [
'id' => (int)$chRaw['id'],
'title' => $chRaw['title'],
'description' => $chRaw['description'],
'type' => $chRaw['challenge_type'],
'target' => (int)$chRaw['target_count'],
'reward_amount' => (float)$chRaw['reward_amount'],
'reward_type' => $chRaw['reward_type'],
'is_completed' => $isCompleted,
'is_claimed' => $isClaimed
];
}
$responseData = [
"country" => $kazan["country"], "country" => $kazan["country"],
"currency" => $kazan["currency"], "currency" => $kazan["currency"],
"totalTrips" => $totalTrips, "totalTrips" => $totalTrips,
@@ -110,10 +153,23 @@ try {
"maxSpeed" => (float)$behavior["max_speed"], "maxSpeed" => (float)$behavior["max_speed"],
"todayTrips" => $todayTrips, "todayTrips" => $todayTrips,
"todayEarnings" => $todayEarnings, "todayEarnings" => $todayEarnings,
"totalPoints" => $normalizedPoints "totalPoints" => $normalizedPoints,
]); "tier" => $tier,
"challenges" => $challenges
];
} catch (PDOException $e) { // 10. Save the compiled data into Redis
if (isset($redis)) {
try {
$redis->setex($redisKey, 900, json_encode($responseData));
} catch (Exception $e) {
error_log("Redis write error in getGamificationDashboard: " . $e->getMessage());
}
}
jsonSuccess($responseData);
} catch (Exception $e) {
error_log("getGamificationDashboard Error: " . $e->getMessage()); error_log("getGamificationDashboard Error: " . $e->getMessage());
jsonError("An internal error occurred. Please try again later."); jsonError("An internal error occurred. Please try again later.");
} }
+34 -2
View File
@@ -4,6 +4,26 @@ require_once __DIR__ . '/../../connect.php';
$type = isset($_POST['type']) ? filterRequest("type") : 'trips'; $type = isset($_POST['type']) ? filterRequest("type") : 'trips';
try { try {
global $redis;
$redisKey = "gamification_leaderboard:{$type}";
// 1. Try to read from Redis first
if (isset($redis)) {
try {
$cached = $redis->get($redisKey);
if ($cached) {
$data = json_decode($cached, true);
if ($data !== null) {
jsonSuccess($data);
exit;
}
}
} catch (Exception $e) {
error_log("Redis read error in getLeaderboard: " . $e->getMessage());
}
}
// 2. Fallback to Database
if ($type === 'earnings') { if ($type === 'earnings') {
$sql = " $sql = "
SELECT SELECT
@@ -53,8 +73,20 @@ if ($stmt->rowCount() > 0) {
$row['rank'] = $rank++; $row['rank'] = $rank++;
} }
jsonSuccess($rows); $responseData = $rows;
} else { } else {
jsonSuccess([]); $responseData = [];
} }
// 3. Save the DB result into Redis for future reads
if (isset($redis)) {
try {
// Cache leaderboard for 1 hour (3600 seconds)
$redis->setex($redisKey, 3600, json_encode($responseData));
} catch (Exception $e) {
error_log("Redis write error in getLeaderboard: " . $e->getMessage());
}
}
jsonSuccess($responseData);
?> ?>
+69
View File
@@ -0,0 +1,69 @@
<?php
require_once __DIR__ . '/../../connect.php';
$driver_id = filterRequest("driver_id");
if (empty($driver_id)) {
jsonError("Missing driver_id");
exit;
}
global $redis;
$redisKey = "driver_streak:{$driver_id}";
// 1. Try to read from Redis first (Cache-Aside pattern)
if (isset($redis)) {
try {
$cached = $redis->get($redisKey);
if ($cached) {
$data = json_decode($cached, true);
if ($data) {
// Return immediately if found in cache!
jsonSuccess($data);
exit;
}
}
} catch (Exception $e) {
error_log("Redis read error in get_streak: " . $e->getMessage());
}
}
// 2. Fallback to Database if not found in Redis
$stmt = $con->prepare("SELECT current_streak, zero_commission_until FROM driver_streaks WHERE driver_id = ?");
$stmt->execute([$driver_id]);
$streak = $stmt->fetch(PDO::FETCH_ASSOC);
if ($streak) {
$now = new DateTime();
$until = new DateTime($streak['zero_commission_until']);
$isActive = false;
if ($streak['zero_commission_until'] !== '2000-01-01 00:00:00' && $now < $until) {
$isActive = true;
}
$responseData = [
'streak_count' => intval($streak['current_streak']),
'is_zero_commission_active' => $isActive,
'zero_commission_until' => $streak['zero_commission_until']
];
} else {
// Default if no record yet
$responseData = [
'streak_count' => 0,
'is_zero_commission_active' => false,
'zero_commission_until' => '2000-01-01 00:00:00'
];
}
// 3. Save the DB result into Redis for future reads
if (isset($redis)) {
try {
// Cache it for 2 hours (7200 seconds)
$redis->setex($redisKey, 7200, json_encode($responseData));
} catch (Exception $e) {
error_log("Redis write error in get_streak: " . $e->getMessage());
}
}
jsonSuccess($responseData);
?>
+3 -3
View File
@@ -4,10 +4,10 @@ require_once __DIR__ . '/../../connect.php';
$driverID = filterRequest("driverID"); $driverID = filterRequest("driverID");
$sql = "SELECT * FROM `notificationCaptain` $sql = "SELECT * FROM `notificationCaptain`
WHERE `driverID` = :driverID WHERE (`driverID` = :driverID OR `driverID` = 'all')
AND `dateCreated` > DATE_SUB(NOW(), INTERVAL 2 DAY) AND `dateCreated` > DATE_SUB(NOW(), INTERVAL 7 DAY)
ORDER BY `dateCreated` DESC ORDER BY `dateCreated` DESC
LIMIT 10"; LIMIT 50";
$stmt = $con->prepare($sql); $stmt = $con->prepare($sql);
$stmt->bindParam(':driverID', $driverID, PDO::PARAM_STR); $stmt->bindParam(':driverID', $driverID, PDO::PARAM_STR);
@@ -3,6 +3,7 @@
// تأكد أن هذا الملف يحتوي على دوال الإشعارات (notifyPassengerOnRideServer) // تأكد أن هذا الملف يحتوي على دوال الإشعارات (notifyPassengerOnRideServer)
require_once __DIR__ . '/../../connect.php'; require_once __DIR__ . '/../../connect.php';
require_once __DIR__ . '/streak_helper.php';
// 🚀 تسجيل بداية العملية // 🚀 تسجيل بداية العملية
error_log("🚀 [cancelRide.php] Request Started to Cancel Ride From Driver."); error_log("🚀 [cancelRide.php] Request Started to Cancel Ride From Driver.");
@@ -56,6 +57,14 @@ try {
$stmtDriverOrder = $con->prepare("UPDATE driver_orders SET status = ? WHERE order_id = ?"); $stmtDriverOrder = $con->prepare("UPDATE driver_orders SET status = ? WHERE order_id = ?");
$stmtDriverOrder->execute([$newStatus, $id]); $stmtDriverOrder->execute([$newStatus, $id]);
// 🆕 تصفير التتابع لأن السائق ألغى الرحلة
$stmtGetDriver = $con->prepare("SELECT driver_id FROM ride WHERE id = ?");
$stmtGetDriver->execute([$id]);
$driver_id_to_reset = $stmtGetDriver->fetchColumn() ?: $user_id;
if ($driver_id_to_reset) {
handleDriverStreak($con, $driver_id_to_reset, 'reset');
}
$con->commit(); $con->commit();
} catch (Exception $eLocal) { } catch (Exception $eLocal) {
$con->rollBack(); $con->rollBack();
@@ -8,6 +8,8 @@ try {
error_log("[cancel_ride_by_driver] Failed to connect to Ride Database: " . $e->getMessage()); error_log("[cancel_ride_by_driver] Failed to connect to Ride Database: " . $e->getMessage());
} }
require_once __DIR__ . '/streak_helper.php';
$rideId = filterRequest("ride_id"); $rideId = filterRequest("ride_id");
$driverId = filterRequest("driver_id"); $driverId = filterRequest("driver_id");
$reason = filterRequest("reason"); $reason = filterRequest("reason");
@@ -75,6 +77,9 @@ try {
$con_ride->prepare($sqlRide)->execute([$statusText, $rideId]); $con_ride->prepare($sqlRide)->execute([$statusText, $rideId]);
} }
// 🆕 تصفير التتابع لأن السائق ألغى الرحلة
handleDriverStreak($con, $driverId, 'reset');
// --------------------------------------------------------- // ---------------------------------------------------------
// 4. إشعار الراكب // 4. إشعار الراكب
// --------------------------------------------------------- // ---------------------------------------------------------
+36 -1
View File
@@ -7,6 +7,8 @@ try {
error_log("[finish_ride_updates] Failed to connect to Ride Database: " . $e->getMessage()); error_log("[finish_ride_updates] Failed to connect to Ride Database: " . $e->getMessage());
} }
require_once __DIR__ . '/streak_helper.php';
// ============================================================ // ============================================================
// finish_ride_updates.php — Atomic Server-to-Server // finish_ride_updates.php — Atomic Server-to-Server
// ============================================================ // ============================================================
@@ -133,6 +135,16 @@ try {
$durationMin = intval(preg_replace('/[^0-9]/', '', $actualDuration)); $durationMin = intval(preg_replace('/[^0-9]/', '', $actualDuration));
$calculated = ($distanceKm * $perKmRate) + ($durationMin * $perMinRate); $calculated = ($distanceKm * $perKmRate) + ($durationMin * $perMinRate);
// 🆕 تطبيق خصم التتابع (إعفاء من العمولة) - نحدد المتغير فقط لكن لا نغير السعر على الراكب
$is_zero_commission = false;
if (hasZeroCommission($con, $driver_id)) {
$is_zero_commission = true;
error_log("[finish_ride_updates] Driver $driver_id has active 0% commission streak!");
}
// السعر النهائي يجب أن يتضمن العمولة دائماً لكي يدفعها الراكب،
// لكن إذا كانت العمولة صفر للسائق، يتم إعطاؤها للسائق بدلاً من الشركة عبر السيرفر المالي.
$calculated *= (1 + ($kazanPercent / 100)); $calculated *= (1 + ($kazanPercent / 100));
$finalPrice = max($quotedPrice, round($calculated, 2)); $finalPrice = max($quotedPrice, round($calculated, 2));
@@ -198,6 +210,8 @@ try {
'authToken' => $driver_token, 'authToken' => $driver_token,
'currency' => $currency, // 🆕 إرسال العملة لمخدم الدفع 'currency' => $currency, // 🆕 إرسال العملة لمخدم الدفع
'country_code' => $countryCode, // 🆕 إرسال الدولة لمخدم الدفع 'country_code' => $countryCode, // 🆕 إرسال الدولة لمخدم الدفع
'is_zero_commission' => isset($is_zero_commission) && $is_zero_commission ? 'true' : 'false', // 🆕 إرسال إعفاء العمولة
'kazanPercent' => $kazanPercent, // 🆕 إرسال نسبة العمولة متغيرة حسب الدولة
]; ];
$ch = curl_init(WALLET_PAYMENT_URL); $ch = curl_init(WALLET_PAYMENT_URL);
@@ -245,6 +259,9 @@ try {
// ✅ Payment succeeded — COMMIT // ✅ Payment succeeded — COMMIT
$con->commit(); $con->commit();
// 🆕 Update Driver Streak (Increment because ride is finished successfully)
handleDriverStreak($con, $driver_id, 'increment');
// ============================================================ // ============================================================
// 5. Notifications (After successful commit) // 5. Notifications (After successful commit)
// ============================================================ // ============================================================
@@ -287,11 +304,29 @@ try {
"المبلغ المطلوب: " . $finalPrice . " " . $currency, "المبلغ المطلوب: " . $finalPrice . " " . $currency,
$fcmData, $fcmData,
'Driver Finish Trip', 'Driver Finish Trip',
false $passengerId
); );
} }
} }
// 🆕 c) Driver notification for Zero Commission
if (isset($is_zero_commission) && $is_zero_commission && !empty($driver_token)) {
$fcmDriverData = [
'ride_id' => (string)$rideId,
'status' => 'streak_reward'
];
sendAndSaveDriverNotification(
$con,
$driver_id,
$driver_token,
"🔥 مكافأة التتابع",
"أنت بطل! لم يتم خصم عمولة لهذه الرحلة لأنك حافظت على تتابع قبول الرحلات.",
$fcmDriverData,
'Zero Commission Reward'
);
}
// ============================================================ // ============================================================
// 6. Return Success with server-calculated price + currency // 6. Return Success with server-calculated price + currency
// ============================================================ // ============================================================
+76
View File
@@ -0,0 +1,76 @@
<?php
// backend/ride/rides/streak_helper.php
function handleDriverStreak($con, $driver_id, $action) {
try {
if ($action === 'reset') {
$stmt = $con->prepare("
INSERT INTO driver_streaks (driver_id, current_streak, zero_commission_until)
VALUES (?, 0, NULL)
ON DUPLICATE KEY UPDATE current_streak = 0
");
$stmt->execute([$driver_id]);
} elseif ($action === 'increment') {
$stmt = $con->prepare("SELECT current_streak FROM driver_streaks WHERE driver_id = ?");
$stmt->execute([$driver_id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$current = $row ? intval($row['current_streak']) + 1 : 1;
$zero_until = NULL;
if ($current >= 5) {
// Reward unlocked! 0% commission until end of today
$zero_until = date('Y-m-d 23:59:59');
$current = 0; // reset for next streak
}
$stmt2 = $con->prepare("
INSERT INTO driver_streaks (driver_id, current_streak, best_streak, zero_commission_until)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
current_streak = ?,
best_streak = GREATEST(best_streak, ?),
zero_commission_until = COALESCE(?, zero_commission_until)
");
$stmt2->execute([
$driver_id,
$current,
$current,
$zero_until,
$current,
$current,
$zero_until
]);
}
// 🆕 Invalidate Redis Cache (Cache-Aside pattern)
global $redis;
if (isset($redis)) {
try {
$redis->del("driver_streak:{$driver_id}");
$redis->del("gamification_dashboard:{$driver_id}");
} catch (Exception $e) {
error_log("[streak_helper] Redis Cache Invalidation Error: " . $e->getMessage());
}
}
} catch (Exception $e) {
error_log("[streak_helper] Error: " . $e->getMessage());
}
}
function hasZeroCommission($con, $driver_id) {
try {
$stmt = $con->prepare("SELECT zero_commission_until FROM driver_streaks WHERE driver_id = ?");
$stmt->execute([$driver_id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row && !empty($row['zero_commission_until'])) {
return (strtotime($row['zero_commission_until']) > time());
}
} catch (Exception $e) {
error_log("[streak_helper] Error checking commission: " . $e->getMessage());
}
return false;
}
?>
+18
View File
@@ -1450,6 +1450,7 @@ CREATE TABLE `ride` (
`time` time NOT NULL, `time` time NOT NULL,
`endtime` time NOT NULL, `endtime` time NOT NULL,
`price` decimal(10,2) NOT NULL DEFAULT '0.00', `price` decimal(10,2) NOT NULL DEFAULT '0.00',
`ai_negotiated_bonus` double DEFAULT '0',
`passenger_id` varchar(111) NOT NULL, `passenger_id` varchar(111) NOT NULL,
`driver_id` varchar(111) NOT NULL, `driver_id` varchar(111) NOT NULL,
`status` varchar(200) NOT NULL DEFAULT 'nothing', `status` varchar(200) NOT NULL DEFAULT 'nothing',
@@ -2051,3 +2052,20 @@ VALUES
('Mall of Egypt', 29.9705, 30.9856, 2000, 9, 'EG'), ('Mall of Egypt', 29.9705, 30.9856, 2000, 9, 'EG'),
('Cairo Opera House', 30.0427, 31.2238, 1000, 7, 'EG'), ('Cairo Opera House', 30.0427, 31.2238, 1000, 7, 'EG'),
('Al-Azhar Mosque', 30.0457, 31.2627, 1000, 6, 'EG'); ('Al-Azhar Mosque', 30.0457, 31.2627, 1000, 6, 'EG');
--
-- Table structure for table `driver_streaks`
--
DROP TABLE IF EXISTS `driver_streaks`;
CREATE TABLE `driver_streaks` (
`id` int NOT NULL AUTO_INCREMENT,
`driver_id` varchar(255) NOT NULL,
`current_streak` int DEFAULT 0,
`best_streak` int DEFAULT 0,
`zero_commission_until` datetime DEFAULT NULL,
`last_updated` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `driver_id` (`driver_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+1
View File
@@ -1353,6 +1353,7 @@ CREATE TABLE `ride` (
`time` time NOT NULL, `time` time NOT NULL,
`endtime` time NOT NULL, `endtime` time NOT NULL,
`price` decimal(10,2) NOT NULL DEFAULT '0.00', `price` decimal(10,2) NOT NULL DEFAULT '0.00',
`ai_negotiated_bonus` double DEFAULT '0',
`passenger_id` varchar(111) NOT NULL, `passenger_id` varchar(111) NOT NULL,
`driver_id` varchar(111) NOT NULL, `driver_id` varchar(111) NOT NULL,
`status` varchar(200) NOT NULL DEFAULT 'nothing', `status` varchar(200) NOT NULL DEFAULT 'nothing',
+6
View File
@@ -249,6 +249,12 @@ class AppLink {
"$endPoint/ride/gamification/getGamificationDashboard.php"; "$endPoint/ride/gamification/getGamificationDashboard.php";
static String get getLeaderboard => static String get getLeaderboard =>
"$endPoint/ride/gamification/getLeaderboard.php"; "$endPoint/ride/gamification/getLeaderboard.php";
static String get getSubAdminForDriver =>
"$serverName/Admin/gamification/getSubAdminForDriver.php";
// 🆕 مسار جلب حالة تتابع الرحلات (Gamification Streak)
static String get getDriverStreak =>
"$serverName/ride/gamification/get_streak.php";
static String get claimChallengeReward => static String get claimChallengeReward =>
"$endPoint/ride/gamification/claimChallengeReward.php"; "$endPoint/ride/gamification/claimChallengeReward.php";
static String get getReferralStats => static String get getReferralStats =>
@@ -802,6 +802,9 @@ class HomeCaptainController extends GetxController {
calculateConsumptionFuel() { calculateConsumptionFuel() {
mpg = fuelPrice / 12; //todo in register car add mpg in box mpg = fuelPrice / 12; //todo in register car add mpg in box
} }
// 🆕 Gamification Streak State
int streakCount = 0;
bool isZeroCommissionActive = false;
getCountRideToday() async { getCountRideToday() async {
var res = await CRUD().get( var res = await CRUD().get(
@@ -811,6 +814,24 @@ class HomeCaptainController extends GetxController {
countRideToday = data['message'][0]['count'].toString(); countRideToday = data['message'][0]['count'].toString();
update(); update();
// Fetch Gamification Streak as well
getDriverStreak();
}
// 🆕 Fetch Driver Streak
getDriverStreak() async {
var res = await CRUD().get(
link: AppLink.getDriverStreak,
payload: {'driver_id': box.read(BoxName.driverID).toString()});
if (res != 'failure') {
var decode = jsonDecode(res);
if (decode['status'] == 'success') {
streakCount = decode['message']['streak_count'] ?? 0;
isZeroCommissionActive = decode['message']['is_zero_commission_active'] ?? false;
update();
}
}
} }
getDriverRate() async { getDriverRate() async {
@@ -222,6 +222,15 @@ class _HomeAppBar extends StatelessWidget implements PreferredSizeWidget {
), ),
), ),
actions: [ actions: [
// ── Gamification Streak Badge ────────
GetBuilder<HomeCaptainController>(
builder: (c) => _PillBadge(
icon: Icons.local_fire_department_rounded,
label: c.isZeroCommissionActive ? 'Free!' : '${c.streakCount}/5',
color: c.isZeroCommissionActive ? _Token.success : Colors.orange,
),
),
const SizedBox(width: 6),
// ── Refuse Counter ─────────────────── // ── Refuse Counter ───────────────────
GetBuilder<HomeCaptainController>( GetBuilder<HomeCaptainController>(
builder: (c) => _PillBadge( builder: (c) => _PillBadge(
@@ -310,7 +319,7 @@ class _AppBarControls extends StatelessWidget {
// Heatmap // Heatmap
GetBuilder<HomeCaptainController>( GetBuilder<HomeCaptainController>(
builder: (c) => _IconBtn( builder: (c) => _IconBtn(
icon: Icons.local_fire_department_rounded, icon: Icons.layers_rounded,
color: color:
c.isHeatmapVisible ? Colors.orange : Colors.grey.shade600, c.isHeatmapVisible ? Colors.orange : Colors.grey.shade600,
tooltip: 'Heatmap'.tr, tooltip: 'Heatmap'.tr,
@@ -20,6 +20,7 @@ class CompetitorPriceBadge extends StatelessWidget {
this.savingsPercent, this.savingsPercent,
this.savingsLabel, this.savingsLabel,
this.topCompetitor, this.topCompetitor,
String? badgeText,
}); });
@override @override
@@ -79,7 +80,9 @@ class CompetitorPriceBadge extends StatelessWidget {
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: Icon( child: Icon(
isSignificant ? Icons.trending_down_rounded : Icons.price_check_rounded, isSignificant
? Icons.trending_down_rounded
: Icons.price_check_rounded,
color: textColor, color: textColor,
size: 18, size: 18,
), ),
@@ -42,6 +42,8 @@ $paymentAmount = filterRequest("paymentAmount");
$paymentMethod = filterRequest("paymentMethod"); $paymentMethod = filterRequest("paymentMethod");
$walletChecked = filterRequest("walletChecked"); // 'true' or 'false' $walletChecked = filterRequest("walletChecked"); // 'true' or 'false'
$authToken = filterRequest("authToken"); // kept for logging/audit, not used for auth $authToken = filterRequest("authToken"); // kept for logging/audit, not used for auth
$is_zero_commission = filterRequest("is_zero_commission"); // 'true' or 'false'
$kazanPercent = filterRequest("kazanPercent"); // dynamic commission rate
// --- Validate required fields --- // --- Validate required fields ---
if (empty($rideId) || empty($driverId) || empty($passengerId) || if (empty($rideId) || empty($driverId) || empty($passengerId) ||
@@ -134,8 +136,17 @@ try {
} }
} }
// 3d. Deduct driver points (8% of payment amount) // 3d. Deduct driver points (dynamic commission rate)
$pointsSubtraction = floatval($paymentAmount) * (-0.08); $commissionRate = 0.08; // default fallback 8%
if (isset($kazanPercent) && is_numeric($kazanPercent)) {
$commissionRate = floatval($kazanPercent) / 100;
}
$pointsSubtraction = floatval($paymentAmount) * (-$commissionRate);
if ($is_zero_commission === 'true') {
$pointsSubtraction = 0; // 🆕 لا نخصم عمولة أبداً لأن السائق لديه تتابع نشط
}
$stmtDriverPoints = $con->prepare( $stmtDriverPoints = $con->prepare(
"INSERT INTO `driverWallet` (`driverID`, `paymentID`, `amount`, `paymentMethod`) "INSERT INTO `driverWallet` (`driverID`, `paymentID`, `amount`, `paymentMethod`)
VALUES (:driverID, :paymentID, :amount, :paymentMethod)" VALUES (:driverID, :paymentID, :amount, :paymentMethod)"