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"); $driver_id = filterRequest("driver_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) { jsonError("Error calculating price: " . $e->getMessage()); 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: " . $e->getMessage()); } // ============================================================ // 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'; // افتراضي: ليرة سورية } ?>