Files
Siro/backend/ride/pricing/get.php
2026-06-21 18:58:13 +03:00

458 lines
17 KiB
PHP

<?php
require_once __DIR__ . '/../../connect.php';
$distance = (float) filterRequest("distance");
$duration = (float) filterRequest("durationToRide");
$promo_code = filterRequest("promo_code");
$passenger_id = filterRequest("passenger_id");
$promo_code_id = filterRequest("promo_code_id") ? filterRequest("promo_code_id") : 0;
$country = filterRequest("country");
$startNameAddress = filterRequest("startNameAddress");
$endNameAddress = filterRequest("endNameAddress");
$destLat = (float) filterRequest("destLat");
$destLng = (float) filterRequest("destLng");
$passengerLat = (float) filterRequest("passengerLat");
$passengerLng = (float) filterRequest("passengerLng");
$activeMenuWaypointCount = (int) filterRequest("activeMenuWaypointCount");
if (!$country) {
echo json_encode(["status" => "failure", "message" => "Country parameter is required"]);
exit;
}
$prices = [];
$pricesRaw = [];
$categories = [
'totalPassengerSpeed' => 'Speed',
'totalPassengerBalash' => 'Awfar Car',
'totalPassengerComfort' => 'Comfort',
'totalPassengerElectric' => 'Electric',
'totalPassengerLady' => 'Lady',
'totalPassengerScooter' => 'Delivery',
'totalPassengerVan' => 'Van',
'totalPassengerRayehGai' => 'Speed',
'totalPassengerRayehGaiComfort' => 'Comfort',
'totalPassengerRayehGaiBalash' => 'Awfar Car',
];
// Common variables
date_default_timezone_set('Asia/Damascus');
$currentTime = new DateTime();
$hour = (int)$currentTime->format('H');
switch ($country) {
case 'Syria':
$minFare = 150.0;
break;
case 'Egypt':
$minFare = 20.0;
break;
case 'Jordan':
$minFare = 1.0;
break;
default:
$minFare = 0.0;
break;
}
// Fetch kazan from DB for the specified country
$sql = "SELECT * FROM `kazan` WHERE country = :country LIMIT 1";
$stmt = $con->prepare($sql);
$stmt->execute([':country' => $country]);
$kazanRow = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$kazanRow) {
echo json_encode(["status" => "failure", "message" => "No pricing available for this country"]);
exit;
}
// ----------------------------------------------------------------------
// Helper Functions for Countries
// ----------------------------------------------------------------------
function getPerKmRate($carType, $kazanRow) {
$rateColumns = [
'Comfort' => 'comfortPrice',
'Speed' => 'speedPrice',
'Lady' => 'ladyPrice',
'Electric' => 'electricPrice',
'Van' => 'vanPrice',
'Delivery' => 'deliveryPrice',
'Mishwar Vip' => 'mishwarVipPrice',
'Fixed Price' => 'fixedPrice',
'Awfar Car' => 'awfarPrice',
];
$column = $rateColumns[$carType] ?? 'speedPrice';
$rate = floatval($kazanRow[$column] ?? 0);
if ($rate <= 0) {
$oldColumnMap = [
'Lady' => 'familyPrice',
'Mishwar Vip' => 'freePrice',
'Electric' => 'naturePrice',
'Van' => 'heavyPrice',
];
$oldColumn = $oldColumnMap[$carType] ?? null;
if ($oldColumn && isset($kazanRow[$oldColumn])) {
$rate = floatval($kazanRow[$oldColumn]);
}
}
if ($rate <= 0) {
$rate = floatval($kazanRow['speedPrice'] ?? 36);
}
return $rate;
}
function calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanRow, $startNameAddress, $endNameAddress, $destLat, $destLng, $passengerLat, $passengerLng, $carType = 'Speed') {
global $redis, $redisLocation, $con;
$surgeMultiplier = 1.0;
if (isset($redis) && $redis !== null) {
try {
$refLat = 33.5;
$grid_size = 0.0135 * (cos(deg2rad($refLat)) / max(cos(deg2rad((float)$passengerLat)), 0.01));
$grid_lat = round((float)$passengerLat / $grid_size) * $grid_size;
$grid_lng = round((float)$passengerLng / $grid_size) * $grid_size;
$grid_id = $grid_lat . "_" . $grid_lng;
// Demand is handled by Main Redis (prefix automatically applied)
$demandCount = (int)$redis->get("demand:grid:" . $grid_id);
$availableDrivers = 0;
// Driver locations are handled by Location Redis (no prefix)
try {
if (isset($redisLocation) && $redisLocation !== null) {
$drivers = $redisLocation->georadius('geo:drivers:available', $grid_lng, $grid_lat, 0.75, 'km');
$availableDrivers = count($drivers);
}
} catch (Exception $e) {}
if ($demandCount > 0) {
$surgeRatio = ($availableDrivers > 0) ? ($demandCount / $availableDrivers) : $demandCount;
if ($surgeRatio > 1.2) {
$surgeMultiplier = 1.0 + ($surgeRatio - 1.2) * 0.5;
$surgeMultiplier = min(3.0, $surgeMultiplier); // Cap at 3.0
}
}
} catch (Exception $e) {}
}
$naturePrice = (float) ($kazanRow['naturePrice'] ?? 0);
$heavyPrice = (float) ($kazanRow['heavyPrice'] ?? 0);
$latePrice = (float) ($kazanRow['latePrice'] ?? 0);
$kazanPercent = (float) ($kazanRow['kazan'] ?? 10);
// === General Settings ===
$minBillableKm = 0.2;
$airportAddon = 0.0;
$damascusAirportBoundAddon = 0.0;
switch ($country) {
case 'Egypt':
$airportAddon = 35.0;
break;
case 'Jordan':
$airportAddon = 5.0;
break;
default: // Syria
$airportAddon = 200.0;
$damascusAirportBoundAddon = 1400.0;
break;
}
$longSpeedThresholdKm = 40.0;
$longSpeedPerKm = 26.0;
$mediumDistThresholdKm = 25.0;
$longDistThresholdKm = 35.0;
$longTripPerMin = 6.0;
$minuteCapMedium = 60;
$minuteCapLong = 80;
$freeMinutesLong = 10;
$extraReduction100 = 0.07;
$maxReductionCap = 0.35;
$totalMinutes = floor($duration / 60);
$airportCtx = (stripos($startNameAddress, 'airport') !== false || stripos($startNameAddress, 'مطار') !== false || stripos($endNameAddress, 'airport') !== false || stripos($endNameAddress, 'مطار') !== false);
$clubCtx = (stripos($startNameAddress, 'club') !== false || stripos($startNameAddress, 'ديسكو') !== false || stripos($endNameAddress, 'club') !== false || stripos($endNameAddress, 'ديسكو') !== false);
$northLat = 33.415313;
$southLat = 33.400265;
$eastLng = 36.531505;
$westLng = 36.499687;
$damascusAirportBoundCtx = ($destLat <= $northLat && $destLat >= $southLat && $destLng <= $eastLng && $destLng >= $westLng);
$isInDamascusAirportBoundCtx = ($passengerLat <= $northLat && $passengerLat >= $southLat && $passengerLng <= $eastLng && $passengerLng >= $westLng);
$billableDistance = ($distance < $minBillableKm) ? $minBillableKm : $distance;
$isLongSpeed = $billableDistance > $longSpeedThresholdKm;
$perKmSpeedBaseFromServer = getPerKmRate($carType, $kazanRow);
$perKmSpeed = $isLongSpeed ? $longSpeedPerKm : $perKmSpeedBaseFromServer;
$reductionPct40 = 0.0;
if ($perKmSpeedBaseFromServer > 0) {
$r = 1.0 - ($longSpeedPerKm / $perKmSpeedBaseFromServer);
$reductionPct40 = max(0.0, min($maxReductionCap, $r));
}
$reductionPct100 = max(0.0, min($maxReductionCap, $reductionPct40 + $extraReduction100));
$distanceReduction = 0.0;
if ($billableDistance > 100.0) {
$distanceReduction = $reductionPct100;
} else if ($billableDistance > 40.0) {
$distanceReduction = $reductionPct40;
}
date_default_timezone_set('Asia/Damascus');
$hour = (int)date('H');
$effectivePerMin = $naturePrice;
if ($hour >= 21 || $hour < 1) {
$effectivePerMin = $latePrice;
} else if ($hour >= 1 && $hour < 5) {
$effectivePerMin = $clubCtx ? ($latePrice * 2) : $latePrice;
} else if ($hour >= 14 && $hour <= 17) {
$effectivePerMin = $heavyPrice;
}
$billableMinutes = $totalMinutes;
if ($billableDistance > $longDistThresholdKm) {
$effectivePerMin = $longTripPerMin;
$capped = ($billableMinutes > $minuteCapLong) ? $minuteCapLong : $billableMinutes;
$billableMinutes = max(0, $capped - $freeMinutesLong);
} else if ($billableDistance > $mediumDistThresholdKm) {
$effectivePerMin = $longTripPerMin;
$billableMinutes = ($billableMinutes > $minuteCapMedium) ? $minuteCapMedium : $billableMinutes;
}
$fare = $billableDistance * $perKmSpeed;
$fare += $billableMinutes * $effectivePerMin;
// Apply Redis Geohash Surge Multiplier
$fare *= $surgeMultiplier;
if ($airportCtx) $fare += $airportAddon;
if ($damascusAirportBoundCtx || $isInDamascusAirportBoundCtx) {
$fare += $damascusAirportBoundAddon;
}
$price = max($fare, $minFare);
// Apply competitor-linked regional pricing overrides (undercut by 8%)
$competitorTarget = null;
if (isset($con) && $con !== null) {
try {
$countryCodeMap = [
'Syria' => 'SY',
'Jordan' => 'JO',
'Egypt' => 'EG',
'Iraq' => 'IQ'
];
$cc = $countryCodeMap[$country] ?? 'SY';
$latDelta = 0.02;
$lngDelta = 0.02;
$minFlat = $passengerLat - $latDelta;
$maxFlat = $passengerLat + $latDelta;
$minFlng = $passengerLng - $lngDelta;
$maxFlng = $passengerLng + $lngDelta;
$minTlat = $destLat - $latDelta;
$maxTlat = $destLat + $latDelta;
$minTlng = $destLng - $lngDelta;
$maxTlng = $destLng + $lngDelta;
// Layer 1: Start and End match within bounding box
$sqlComp = "SELECT total_price, distance_km
FROM competitor_prices
WHERE country_code = :country_code
AND (from_latitude + 0.0) BETWEEN :min_flat AND :max_flat
AND (from_longitude + 0.0) BETWEEN :min_flng AND :max_flng
AND (to_latitude + 0.0) BETWEEN :min_tlat AND :max_tlat
AND (to_longitude + 0.0) BETWEEN :min_tlng AND :max_tlng
AND created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
ORDER BY created_at DESC LIMIT 5";
$stmtComp = $con->prepare($sqlComp);
$stmtComp->execute([
':country_code' => $cc,
':min_flat' => $minFlat,
':max_flat' => $maxFlat,
':min_flng' => $minFlng,
':max_flng' => $maxFlng,
':min_tlat' => $minTlat,
':max_tlat' => $maxTlat,
':min_tlng' => $minTlng,
':max_tlng' => $maxTlng
]);
$matches = $stmtComp->fetchAll(PDO::FETCH_ASSOC);
if (empty($matches)) {
// Layer 2 Fallback: Start match only within bounding box
$sqlFallback = "SELECT total_price, distance_km
FROM competitor_prices
WHERE country_code = :country_code
AND (from_latitude + 0.0) BETWEEN :min_flat AND :max_flat
AND (from_longitude + 0.0) BETWEEN :min_flng AND :max_flng
AND created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
ORDER BY created_at DESC LIMIT 10";
$stmtFallback = $con->prepare($sqlFallback);
$stmtFallback->execute([
':country_code' => $cc,
':min_flat' => $minFlat,
':max_flat' => $maxFlat,
':min_flng' => $minFlng,
':max_flng' => $maxFlng
]);
$matches = $stmtFallback->fetchAll(PDO::FETCH_ASSOC);
}
if (!empty($matches)) {
$normalizedPrices = [];
foreach ($matches as $row) {
$compDist = (float)$row['distance_km'];
$compPrice = (float)$row['total_price'];
if ($compDist > 0) {
$normalizedPrices[] = ($compPrice / $compDist) * $distance;
}
}
if (!empty($normalizedPrices)) {
$competitorTarget = array_sum($normalizedPrices) / count($normalizedPrices);
}
}
} catch (Exception $e) {
error_log("[calculateDynamicPrice] Competitor pricing query failed: " . $e->getMessage());
}
}
if ($competitorTarget !== null) {
$undercutPrice = $competitorTarget * 0.92;
$speedBaseRate = getPerKmRate('Speed', $kazanRow);
$currentBaseRate = getPerKmRate($carType, $kazanRow);
$categoryMultiplier = $speedBaseRate > 0 ? ($currentBaseRate / $speedBaseRate) : 1.0;
$targetAdjustedPrice = $undercutPrice * $categoryMultiplier;
if ($price > $targetAdjustedPrice) {
$price = $targetAdjustedPrice;
}
}
// Apply kazan (e.g. 11%)
$withCommission = ceil($price * (1 + $kazanPercent / 100));
$kazan = $withCommission - $price;
$price_for_driver = $price;
return [
'price' => $price,
'price_for_driver' => $price_for_driver,
'withCommission' => $withCommission,
'kazan' => $kazan,
'isNightFare' => false
];
}
// 2. Validate Promo Code
$discount = 0;
if (!empty($promo_code)) {
$sqlPromo = "SELECT amount FROM `promos`
WHERE promo_code = :promo_code
AND (passengerID = :passenger_id OR passengerID LIKE '%all%')
AND validity_start_date <= CURDATE()
AND validity_end_date >= CURDATE()";
$stmtPromo = $con->prepare($sqlPromo);
$stmtPromo->execute([
':promo_code' => $promo_code,
':passenger_id' => $passenger_id
]);
if ($stmtPromo->rowCount() > 0) {
$promoData = $stmtPromo->fetch(PDO::FETCH_ASSOC);
$discount = (float) $promoData['amount'];
}
}
// 3. Fetch Passenger Wallet (Negative Balance / Debt)
$negativeBalance = 0;
if (!empty($passenger_id)) {
try {
$redisInstance = null;
if (isset($redis) && $redis !== null) {
$redisInstance = $redis;
} else if (extension_loaded('redis')) {
$localRedis = new Redis();
$redisHost = getenv('REDIS_MAIN_HOST') ?: getenv('REDIS_HOST') ?: '127.0.0.1';
$redisPort = (int)(getenv('REDIS_MAIN_PORT') ?: getenv('REDIS_PORT') ?: 6379);
$redisPass = getenv('REDIS_MAIN_PASSWORD') ?: getenv('REDIS_MAIN_AUTH') ?: getenv('REDIS_PASSWORD') ?: getenv('REDIS_AUTH');
if ($localRedis->connect($redisHost, $redisPort, 1.5)) {
if ($redisPass) $localRedis->auth($redisPass);
$localRedis->setOption(Redis::OPT_PREFIX, 'siro:');
$redisInstance = $localRedis;
}
}
if ($redisInstance !== null) {
$redisKey = "passenger_debt_" . $passenger_id;
$redisDebt = $redisInstance->get($redisKey);
if ($redisDebt !== false) {
$negativeBalance = (float) $redisDebt;
}
}
} catch (Exception $e) {
$negativeBalance = 0;
}
}
// Calculate prices for all categories
foreach ($categories as $key => $carType) {
$result = calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanRow, $startNameAddress, $endNameAddress, $destLat, $destLng, $passengerLat, $passengerLng, $carType);
$withCommission = $result['withCommission'];
$price_for_driver = $result['price_for_driver'];
// Apply discount
if ($discount > 0 && $discount <= 100) {
$finalPrice = max(0, $withCommission - ($withCommission * ($discount / 100)));
} else {
$finalPrice = max(0, $withCommission - $discount);
}
// Add negative balance
$finalPrice += $negativeBalance;
$prices[$key] = $finalPrice;
// For the token, we map the clean database carType to the final price and driver price
$pricesRaw[$carType] = [
'price' => $finalPrice,
'driver_price' => $price_for_driver
];
}
// 4. Generate Cryptographically Signed Token
$priceToken = "";
if (isset($encryptionHelper)) {
$tokenPayload = [
'passenger_id' => $passenger_id,
'start_location' => $passengerLat . ',' . $passengerLng,
'end_location' => $destLat . ',' . $destLng,
// ✅ FIX R6: تضمين distance و duration في الـ token لمنع التلاعب
'distance' => $distance,
'duration' => $duration,
'expires' => time() + 420, // Valid for 7 minutes
'prices' => $pricesRaw
];
$priceToken = $encryptionHelper->encryptData(json_encode($tokenPayload));
}
echo json_encode([
'status' => 'success',
'data' => $prices,
'price_token' => $priceToken,
'applied_discount' => $discount,
'added_negative_balance' => $negativeBalance
]);
?>