441 lines
17 KiB
PHP
441 lines
17 KiB
PHP
<?php
|
|
require_once __DIR__ . '/../../connect.php';
|
|
|
|
try {
|
|
$con_ride = Database::get('ride');
|
|
} catch (Exception $e) {
|
|
error_log("[finish_ride_updates] Failed to connect to Ride Database: " . $e->getMessage());
|
|
}
|
|
|
|
// ============================================================
|
|
// finish_ride_updates.php — Atomic Server-to-Server
|
|
// ============================================================
|
|
// Driver App calls this ONCE with raw ride data (NOT the price).
|
|
// Server calculates price securely, processes payment via S2S,
|
|
// and atomically updates all databases within a transaction.
|
|
//
|
|
// Flow:
|
|
// 1. Receive raw params from driver app (including country_code)
|
|
// 2. Load country pricing from `kazan` table
|
|
// 3. Calculate price server-side (from DB + actual distance)
|
|
// 4. BEGIN TRANSACTION (local DB)
|
|
// 5. Update ride on local DB + remote DB (con_ride)
|
|
// 6. Update driver_orders
|
|
// 7. S2S cURL → Wallet Payment Server (process_ride_payments.php)
|
|
// 8. If payment OK → COMMIT, notify passenger (Socket + FCM)
|
|
// 9. If payment FAIL → ROLLBACK, ride stays 'Begin', safe retry
|
|
// ============================================================
|
|
|
|
// --- Secure S2S Configuration ---
|
|
define('S2S_SHARED_KEY', getenv('S2S_SHARED_KEY') );
|
|
define('WALLET_PAYMENT_URL', 'https://walletintaleq.intaleq.xyz/v1/main/ride/payment/process_ride_payments.php');
|
|
|
|
// ============================================================
|
|
// 1. Receive Raw Parameters (NO price from client)
|
|
// ============================================================
|
|
$rideId = filterRequest("rideId");
|
|
// Force driver_id from JWT — never trust user-supplied driver_id
|
|
$driver_id = $user_id;
|
|
$passengerId = filterRequest("passengerId");
|
|
$newStatus = filterRequest("status"); // Expected: "Finished"
|
|
$actualDistance = filterRequest("actualDistance");
|
|
$actualDuration = filterRequest("actualDuration");
|
|
$passengerToken = filterRequest("passengerToken");
|
|
$driver_token = filterRequest("driver_token");
|
|
$walletChecked = filterRequest("walletChecked");
|
|
$passengerWalletBurc = filterRequest("passengerWalletBurc");
|
|
$countryCode = filterRequest("country_code"); // 🆕 الدولة: Syria, Egypt, ...
|
|
|
|
if (empty($rideId) || empty($newStatus) || empty($driver_id) || empty($passengerId)) {
|
|
jsonError("Missing required parameters: rideId, driver_id, passengerId, status");
|
|
exit;
|
|
}
|
|
|
|
if ($newStatus !== 'Finished') {
|
|
jsonError("Invalid status. Expected: Finished");
|
|
exit;
|
|
}
|
|
|
|
// 🆕 إذا لم يتم إرسال country_code، نأخذه من قاعدة بيانات الرحلة
|
|
if (empty($countryCode)) {
|
|
try {
|
|
$stmtCountry = $con->prepare("SELECT r.id, d.site AS country_code
|
|
FROM ride r
|
|
LEFT JOIN driver d ON r.driver_id = d.id
|
|
WHERE r.id = ? LIMIT 1");
|
|
$stmtCountry->execute([$rideId]);
|
|
$rowCountry = $stmtCountry->fetch(PDO::FETCH_ASSOC);
|
|
$countryCode = $rowCountry['country_code'] ?? 'Syria';
|
|
} catch (Exception $e) {
|
|
$countryCode = 'Syria'; // fallback
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// 2. Load Country Pricing from `kazan` Table
|
|
// ============================================================
|
|
try {
|
|
$stmtKazan = $con->prepare("SELECT * FROM kazan WHERE country = ? LIMIT 1");
|
|
$stmtKazan->execute([$countryCode]);
|
|
$countryPricing = $stmtKazan->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$countryPricing) {
|
|
// Fallback: إذا لم نجد سعر للدولة، نستخدم Syria كافتراضي
|
|
error_log("[finish_ride_updates] No pricing found for country: $countryCode. Falling back to Syria.");
|
|
$stmtKazan->execute(['Syria']);
|
|
$countryPricing = $stmtKazan->fetch(PDO::FETCH_ASSOC);
|
|
$countryCode = 'Syria';
|
|
}
|
|
} catch (PDOException $e) {
|
|
error_log("[finish_ride_updates] Failed to load country pricing: " . $e->getMessage());
|
|
jsonError("Failed to load pricing configuration.");
|
|
exit;
|
|
}
|
|
|
|
// ============================================================
|
|
// 3. Server-Side Price Calculation (Secure — NOT from client)
|
|
// ============================================================
|
|
try {
|
|
// Fetch ride data from remote/local DB for server-side calculation
|
|
$stmtRideData = $con->prepare("
|
|
SELECT id, price AS quoted_price, car_type,
|
|
distance AS planned_distance, passenger_id, driver_id
|
|
FROM ride WHERE id = ? AND driver_id = ?
|
|
LIMIT 1
|
|
");
|
|
$stmtRideData->execute([$rideId, $driver_id]);
|
|
$rideData = $stmtRideData->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$rideData) {
|
|
jsonError("Ride not found or driver mismatch.");
|
|
exit;
|
|
}
|
|
|
|
$quotedPrice = floatval($rideData['quoted_price'] ?? 0);
|
|
$kazanPercent = floatval($countryPricing['kazanPercent'] ?? $countryPricing['kazan'] ?? 10); // 🆕 من جدول kazan (kazanPercent هو الاسم الجديد)
|
|
$carType = $rideData['car_type'] ?? 'Fixed Price';
|
|
|
|
// Fixed-price types, Speed & Awfar: use quoted price as-is
|
|
$fixedPriceTypes = ['Speed', 'Fixed Price', 'Awfar Car'];
|
|
if (in_array($carType, $fixedPriceTypes)) {
|
|
$finalPrice = $quotedPrice;
|
|
} else {
|
|
// Variable pricing: calculate from actual distance
|
|
$cleanDist = preg_replace('/[^0-9.]/', '', $actualDistance);
|
|
$distanceKm = floatval($cleanDist);
|
|
|
|
if ($distanceKm <= 0) {
|
|
$finalPrice = $quotedPrice; // fallback
|
|
} else {
|
|
// 🆕 استخدام الأسعار من جدول kazan حسب الدولة (كل نوع سيارة له عمود سعره الخاص)
|
|
$perKmRate = getPerKmRate($carType, $countryPricing);
|
|
$perMinRate = getPerMinRate($countryPricing);
|
|
$durationMin = intval(preg_replace('/[^0-9]/', '', $actualDuration));
|
|
|
|
$calculated = ($distanceKm * $perKmRate) + ($durationMin * $perMinRate);
|
|
$calculated *= (1 + ($kazanPercent / 100));
|
|
|
|
$finalPrice = max($quotedPrice, round($calculated, 2));
|
|
}
|
|
}
|
|
|
|
// 🆕 تحديد رمز العملة حسب الدولة
|
|
$currency = getCurrencyByCountry($countryCode);
|
|
|
|
} catch (PDOException $e) {
|
|
error_log("[finish_ride_updates] " . $e->getMessage());
|
|
jsonError("Error calculating price");
|
|
exit;
|
|
}
|
|
|
|
// ============================================================
|
|
// 4. Atomic Transaction: Update DBs + Process Payment
|
|
// ============================================================
|
|
try {
|
|
// --- Update Remote DB (con_ride) FIRST ---
|
|
if (isset($con_ride)) {
|
|
$stmtRemote = $con_ride->prepare(
|
|
"UPDATE ride SET status = ?, rideTimeFinish = NOW(), price = ? WHERE id = ? AND status = 'Begin'"
|
|
);
|
|
$stmtRemote->execute([$newStatus, $finalPrice, $rideId]);
|
|
}
|
|
|
|
// --- BEGIN Local DB Transaction ---
|
|
$con->beginTransaction();
|
|
|
|
// 4a. Update ride (local DB)
|
|
$stmtLocal = $con->prepare(
|
|
"UPDATE ride SET status = ?, rideTimeFinish = NOW(), price = ? WHERE id = ? AND status = 'Begin'"
|
|
);
|
|
$stmtLocal->execute([$newStatus, $finalPrice, $rideId]);
|
|
|
|
if ($stmtLocal->rowCount() == 0) {
|
|
throw new Exception("Ride already finished or not found in local DB.");
|
|
}
|
|
|
|
// 4b. Update driver_orders
|
|
$checkStmt = $con->prepare("SELECT order_id FROM driver_orders WHERE order_id = ?");
|
|
$checkStmt->execute([$rideId]);
|
|
|
|
if ($checkStmt->rowCount() > 0) {
|
|
$con->prepare("UPDATE driver_orders SET driver_id = ?, status = ?, created_at = NOW() WHERE order_id = ?")
|
|
->execute([$driver_id, $newStatus, $rideId]);
|
|
} else {
|
|
$con->prepare("INSERT INTO driver_orders (driver_id, order_id, created_at, status) VALUES (?, ?, NOW(), ?)")
|
|
->execute([$driver_id, $rideId, $newStatus]);
|
|
}
|
|
|
|
// ============================================================
|
|
// 4c. Server-to-Server Payment Processing (S2S)
|
|
// ============================================================
|
|
$paymentPayload = [
|
|
'rideId' => $rideId,
|
|
'driverId' => $driver_id,
|
|
'passengerId' => $passengerId,
|
|
'paymentAmount' => $finalPrice,
|
|
'paymentMethod' => ($walletChecked === 'true') ? 'wallet' : 'cash',
|
|
'walletChecked' => $walletChecked,
|
|
'passengerWalletBurc' => $passengerWalletBurc,
|
|
'authToken' => $driver_token,
|
|
'currency' => $currency, // 🆕 إرسال العملة لمخدم الدفع
|
|
'country_code' => $countryCode, // 🆕 إرسال الدولة لمخدم الدفع
|
|
];
|
|
|
|
$ch = curl_init(WALLET_PAYMENT_URL);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => http_build_query($paymentPayload),
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 15,
|
|
CURLOPT_HTTPHEADER => [
|
|
'Content-Type: application/x-www-form-urlencoded',
|
|
'X-S2S-Api-Key: ' . S2S_SHARED_KEY,
|
|
],
|
|
]);
|
|
|
|
$paymentResponse = curl_exec($ch);
|
|
$httpStatusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$curlError = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
// Validate payment response
|
|
$paymentSuccess = false;
|
|
$paymentError = '';
|
|
|
|
if ($curlError) {
|
|
$paymentError = "S2S connection error: " . $curlError;
|
|
} elseif ($httpStatusCode !== 200) {
|
|
$paymentError = "Payment server returned HTTP $httpStatusCode";
|
|
} else {
|
|
$paymentResult = json_decode($paymentResponse, true);
|
|
if ($paymentResult && isset($paymentResult['status']) && $paymentResult['status'] === 'success') {
|
|
$paymentSuccess = true;
|
|
} else {
|
|
$paymentError = $paymentResult['error'] ?? 'Payment server returned failure';
|
|
}
|
|
}
|
|
|
|
if (!$paymentSuccess) {
|
|
// ❌ Payment failed — ROLLBACK everything
|
|
$con->rollBack();
|
|
error_log("[finish_ride_updates] Payment FAILED for ride $rideId: $paymentError");
|
|
jsonError("Payment processing failed: $paymentError");
|
|
exit;
|
|
}
|
|
|
|
// ✅ Payment succeeded — COMMIT
|
|
$con->commit();
|
|
|
|
// ============================================================
|
|
// 5. Notifications (After successful commit)
|
|
// ============================================================
|
|
$passenger_id = $passengerId; // alias for legacy code
|
|
|
|
if (!empty($passenger_id)) {
|
|
// Legacy list for backward compatibility
|
|
$legacyList = [
|
|
(string)$driver_id,
|
|
(string)$rideId,
|
|
(string)$driver_token,
|
|
(string)$finalPrice
|
|
];
|
|
|
|
// a) Socket notification
|
|
$socketPayload = [
|
|
'ride_id' => $rideId,
|
|
'status' => 'finished',
|
|
'price' => $finalPrice,
|
|
'currency' => $currency, // 🆕
|
|
'DriverList' => $legacyList
|
|
];
|
|
|
|
if (function_exists('notifyPassengerOnRideServer')) {
|
|
notifyPassengerOnRideServer($passenger_id, $socketPayload);
|
|
}
|
|
|
|
// b) FCM notification
|
|
if (!empty($passengerToken)) {
|
|
$fcmData = [
|
|
'ride_id' => (string)$rideId,
|
|
'price' => (string)$finalPrice,
|
|
'currency' => $currency, // 🆕
|
|
'DriverList' => $legacyList
|
|
];
|
|
|
|
sendFCM_Internal(
|
|
$passengerToken,
|
|
"تم إنهاء الرحلة 🏁",
|
|
"المبلغ المطلوب: " . $finalPrice . " " . $currency,
|
|
$fcmData,
|
|
'Driver Finish Trip',
|
|
false
|
|
);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// 6. Return Success with server-calculated price + currency
|
|
// ============================================================
|
|
jsonSuccess([
|
|
'price' => $finalPrice,
|
|
'currency' => $currency, // 🆕 إرجاع العملة للتطبيق
|
|
'rideId' => $rideId
|
|
], "Ride finished and payment processed successfully.");
|
|
|
|
} catch (Exception $e) {
|
|
if (isset($con) && $con->inTransaction()) {
|
|
$con->rollBack();
|
|
}
|
|
error_log("[finish_ride_updates] Error for ride $rideId: " . $e->getMessage());
|
|
jsonError("Transaction failed");
|
|
}
|
|
|
|
// ============================================================
|
|
// Helper Functions — الآن تقرأ الأسعار من جدول kazan حسب الدولة
|
|
// ============================================================
|
|
|
|
/**
|
|
* الحصول على سعر الكيلومتر حسب نوع السيارة من جدول أسعار الدولة
|
|
*
|
|
* 🆕 كل نوع سيارة له عمود مستقل في جدول kazan:
|
|
* Speed → speedPrice | Comfort → comfortPrice | Lady → ladyPrice
|
|
* Electric → electricPrice | Van → vanPrice | Delivery → deliveryPrice
|
|
* Mishwar Vip → mishwarVipPrice | Fixed Price → fixedPrice | Awfar → awfarPrice
|
|
*
|
|
* @param string $carType نوع السيارة
|
|
* @param array $countryPricing صف من جدول kazan
|
|
* @return float
|
|
*/
|
|
function getPerKmRate(string $carType, array $countryPricing): float {
|
|
// 🆕 الخريطة المباشرة: كل نوع سيارة يقابله عمود بنفس الاسم + "Price"
|
|
$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';
|
|
|
|
// دعم التوافق مع الإصدارات القديمة (backward compatibility)
|
|
$rate = floatval($countryPricing[$column] ?? 0);
|
|
|
|
// إذا كان السعر صفر أو غير موجود، نبحث في الأسماء القديمة
|
|
if ($rate <= 0) {
|
|
$oldColumnMap = [
|
|
'Lady' => 'familyPrice',
|
|
'Mishwar Vip' => 'freePrice',
|
|
'Electric' => 'naturePrice',
|
|
'Van' => 'heavyPrice',
|
|
];
|
|
$oldColumn = $oldColumnMap[$carType] ?? null;
|
|
if ($oldColumn && isset($countryPricing[$oldColumn])) {
|
|
$rate = floatval($countryPricing[$oldColumn]);
|
|
}
|
|
}
|
|
|
|
// Fallback أخير
|
|
if ($rate <= 0) {
|
|
$rate = floatval($countryPricing['speedPrice'] ?? 36);
|
|
}
|
|
|
|
return $rate;
|
|
}
|
|
|
|
/**
|
|
* الحصول على سعر الدقيقة حسب وقت اليوم من جدول kazan
|
|
*
|
|
* 🆕 الآن يقرأ من أعمدة الدقيقة المنفصلة:
|
|
* normalMinPrice → Normal (9 ص - 2 م / 6 م - 9 م)
|
|
* peakMinPrice → Peak (2 م - 5 م)
|
|
* lateMinPrice → Late (9 م - 1 ص)
|
|
*
|
|
* @param array $countryPricing صف من جدول kazan
|
|
* @return float
|
|
*/
|
|
function getPerMinRate(array $countryPricing): float {
|
|
$hour = (int)date('H');
|
|
|
|
// 🆕 قراءة الأسعار من الأعمدة الجديدة للدقيقة
|
|
$normalMinPrice = floatval($countryPricing['normalMinPrice'] ?? 0);
|
|
$peakMinPrice = floatval($countryPricing['peakMinPrice'] ?? 0);
|
|
$lateMinPrice = floatval($countryPricing['lateMinPrice'] ?? 0);
|
|
|
|
// 🆕 دعم التوافق مع الإصدارات القديمة (latePrice, naturePrice)
|
|
if ($lateMinPrice <= 0) $lateMinPrice = floatval($countryPricing['latePrice'] ?? 0);
|
|
if ($normalMinPrice <= 0) $normalMinPrice = floatval($countryPricing['naturePrice'] ?? 0);
|
|
|
|
// Fallback: حساب سعر الدقيقة من speedPrice إذا كانت الأعمدة الجديدة فارغة
|
|
if ($normalMinPrice <= 0) {
|
|
$speedPrice = floatval($countryPricing['speedPrice'] ?? 36);
|
|
$normalMinPrice = $speedPrice / 4;
|
|
}
|
|
if ($peakMinPrice <= 0) $peakMinPrice = $normalMinPrice * 1.15; // 15% زيادة
|
|
if ($lateMinPrice <= 0) $lateMinPrice = $normalMinPrice * 1.25; // 25% زيادة
|
|
|
|
if ($hour >= 21 || $hour < 1) {
|
|
return round($lateMinPrice, 2); // Late Night
|
|
}
|
|
if ($hour >= 14 && $hour <= 17) {
|
|
return round($peakMinPrice, 2); // Peak
|
|
}
|
|
return round($normalMinPrice, 2); // Normal
|
|
}
|
|
|
|
/**
|
|
* تحديد رمز العملة حسب الدولة
|
|
*
|
|
* @param string $countryCode رمز الدولة (Syria, Egypt, Jordan, ...)
|
|
* @return string رمز العملة (SYP, EGP, JOD, ...)
|
|
*/
|
|
function getCurrencyByCountry(string $countryCode): string {
|
|
$currencies = [
|
|
'Syria' => 'SYP',
|
|
'Egypt' => 'EGP',
|
|
'Jordan' => 'JOD',
|
|
'Iraq' => 'IQD',
|
|
'UAE' => 'AED',
|
|
'Saudi Arabia' => 'SAR',
|
|
'Qatar' => 'QAR',
|
|
'Kuwait' => 'KWD',
|
|
'Bahrain' => 'BHD',
|
|
'Oman' => 'OMR',
|
|
'Turkey' => 'TRY',
|
|
'Lebanon' => 'LBP',
|
|
'Palestine' => 'ILS',
|
|
'Yemen' => 'YER',
|
|
'Libya' => 'LYD',
|
|
'Tunisia' => 'TND',
|
|
'Algeria' => 'DZD',
|
|
'Morocco' => 'MAD',
|
|
'Sudan' => 'SDG',
|
|
];
|
|
|
|
return $currencies[$countryCode] ?? 'SYP'; // افتراضي: ليرة سورية
|
|
}
|
|
?>
|