Update: 2026-07-03 01:48:00

This commit is contained in:
Hamza-Ayed
2026-07-03 01:48:00 +03:00
parent 8f59189832
commit 5ce6c00905
13 changed files with 1119 additions and 12 deletions
+17
View File
@@ -96,6 +96,23 @@ PAYMENT_GATEWAY_URL=https://api.paymentprovider.com
PAYMENT_GATEWAY_KEY=<CHANGE_ME_PAYMENT_KEY>
PAYMENT_GATEWAY_SECRET=<CHANGE_ME_PAYMENT_SECRET>
PAYMENT_WEBHOOK_SECRET=<CHANGE_ME_WEBHOOK_SECRET>
# Internal key used for server-to-server calls (Siro Backend → Wallet Server)
PAYMENT_KEY=<CHANGE_ME_SHARED_PAYMENT_KEY>
# =============================================================================
# Wallet Servers — Multi-Country (انطلق / Wallet Intaliq)
# =============================================================================
# Jordan wallet server (walletintaleq.intaleq.xyz)
WALLET_SERVER_JORDAN=https://walletintaleq.intaleq.xyz
# Egypt wallet server
WALLET_SERVER_EGYPT=https://wallet-egypt.siromove.com
# Syria wallet server
WALLET_SERVER_SYRIA=https://wallet-syria.siromove.com
# Shared S2S secret key (must match wallet server's X-S2S-Api-Key config)
S2S_SHARED_KEY=<CHANGE_ME_S2S_SHARED_SECRET>
# =============================================================================
# Siro Commissions per Country
+66
View File
@@ -0,0 +1,66 @@
<?php
// ============================================================
// api/payments/get_prime_status.php
// PURPOSE : جلب حالة اشتراك Siro Prime للراكب
// AUTH : JWT (passenger)
// ============================================================
require_once __DIR__ . '/../../connect.php';
$passengerId = $user_id ?? null;
if (!$passengerId || $role !== 'passenger') {
jsonError("Unauthorized");
exit;
}
$isPrime = false;
$expireAt = null;
// 1. تحقق من Redis أولاً (الأسرع)
if (isset($redis) && $redis !== null) {
try {
$cached = $redis->get("prime:passenger:{$passengerId}");
if ($cached) {
$data = json_decode($cached, true);
if (isset($data['is_prime']) && $data['is_prime'] == 1) {
$expTime = strtotime($data['expire_at'] ?? '0');
if ($expTime > time()) {
$isPrime = true;
$expireAt = $data['expire_at'];
}
}
}
} catch (Exception $e) {}
}
// 2. إذا ما وُجد في Redis، ارجع للـ DB
if (!$isPrime) {
try {
$stmt = $con->prepare("SELECT is_prime, expire_at FROM passenger_prime_subscriptions WHERE passenger_id = :pid LIMIT 1");
$stmt->execute([':pid' => $passengerId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row && $row['is_prime'] == 1 && strtotime($row['expire_at']) > time()) {
$isPrime = true;
$expireAt = $row['expire_at'];
// تحديث Redis للمرات القادمة
if (isset($redis) && $redis !== null) {
try {
$redis->setex("prime:passenger:{$passengerId}", 3600, json_encode([
'is_prime' => 1,
'expire_at' => $expireAt
]));
} catch (Exception $e) {}
}
}
} catch (PDOException $e) {
error_log("[Prime Status] DB Error: " . $e->getMessage());
}
}
jsonSuccess([
'is_prime' => $isPrime,
'expire_at' => $expireAt,
], "Prime status fetched");
?>
+201
View File
@@ -0,0 +1,201 @@
<?php
// ============================================================
// api/payments/initiate_prime.php
// PURPOSE : شراء اشتراك Siro Prime عبر خصم رصيد المحفظة الداخلية
// AUTH : JWT (passenger)
// FLOW :
// 1. جلب هوية الراكب من JWT
// 2. تحديد السعر حسب الدولة
// 3. التحقق من رصيد الراكب في سيرفر المحفظة (S2S)
// 4. إذا الرصيد كافٍ → الخصم + تفعيل Prime
// 5. إذا الرصيد غير كافٍ → رسالة لإرشاد المستخدم للشحن
// ============================================================
require_once __DIR__ . '/../../connect.php';
// ── 1. هوية الراكب من JWT ─────────────────────────────────────
$passengerId = $user_id ?? null;
if (!$passengerId || $role !== 'passenger') {
jsonError("Unauthorized");
exit;
}
// ── 2. الدولة والسعر ──────────────────────────────────────────
$country = filterRequest("country") ?: 'Jordan';
$pricingMap = [
'Jordan' => ['amount' => 3.00, 'currency' => 'JOD'], // ~4 USD/month
'Egypt' => ['amount' => 200.00,'currency' => 'EGP'], // ~4 USD/month
'Syria' => ['amount' => 500.00,'currency' => 'SYP'], // ~4 USD/month (New Syrian Pound)
];
$amount = $pricingMap[$country]['amount'] ?? 3.00;
$currency = $pricingMap[$country]['currency'] ?? 'JOD';
// ── 3. سيرفر المحفظة حسب الدولة ──────────────────────────────
$walletServer = "https://walletintaleq.intaleq.xyz"; // Default
if (strtolower($country) === 'jordan') {
$walletServer = getenv('WALLET_SERVER_JORDAN') ?: "https://walletintaleq.intaleq.xyz";
} elseif (strtolower($country) === 'egypt') {
$walletServer = getenv('WALLET_SERVER_EGYPT') ?: "https://wallet-egypt.siromove.com";
} elseif (strtolower($country) === 'syria') {
$walletServer = getenv('WALLET_SERVER_SYRIA') ?: "https://wallet-syria.siromove.com";
}
$s2sKey = getenv('S2S_SHARED_KEY');
if (empty($s2sKey)) {
error_log("[Prime] CRITICAL: S2S_SHARED_KEY not set");
jsonError("Server configuration error");
exit;
}
// ── 4. التحقق من رصيد الراكب في سيرفر المحفظة ────────────────
$balanceUrl = "$walletServer/v2/main/ride/passengerWallet/getWalletByPassenger.php";
$chBalance = curl_init($balanceUrl);
curl_setopt_array($chBalance, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query(['passenger_id' => $passengerId]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
'X-S2S-Api-Key: ' . $s2sKey
]
]);
$balanceRaw = curl_exec($chBalance);
$balanceCode = curl_getinfo($chBalance, CURLINFO_HTTP_CODE);
$balanceErr = curl_error($chBalance);
curl_close($chBalance);
if ($balanceErr || $balanceCode !== 200) {
error_log("[Prime] Wallet balance fetch failed: HTTP $balanceCode | err: $balanceErr");
jsonError("Unable to verify wallet balance. Please try again.");
exit;
}
$balanceData = json_decode($balanceRaw, true);
$walletBalance = (float)($balanceData['message'][0]['total'] ?? $balanceData['total'] ?? -1);
if ($walletBalance < 0) {
error_log("[Prime] Unexpected wallet response: $balanceRaw");
jsonError("Unable to read wallet balance.");
exit;
}
// ── 5. هل الرصيد كافٍ؟ ────────────────────────────────────────
if ($walletBalance < $amount) {
// رصيد غير كافٍ — أخبر التطبيق ليوجّه المستخدم للشحن
echo json_encode([
'status' => 'insufficient_balance',
'current_balance' => $walletBalance,
'required_amount' => $amount,
'currency' => $currency,
'message' => 'Your wallet balance is insufficient. Please top up your wallet to subscribe to Siro Prime.'
]);
exit;
}
// ── 6. الرصيد كافٍ → بدء عملية الاشتراك ─────────────────────
try {
$con->beginTransaction();
// 6a. تسجيل الحركة في قاعدة بيانات سيرو (بادئها paid مباشرةً)
$transactionRef = "PRIME-" . time() . "-" . rand(1000, 9999);
$stmtTx = $con->prepare("
INSERT INTO prime_payment_transactions (transaction_ref, passenger_id, amount, currency, status)
VALUES (:ref, :pid, :amt, :curr, 'paid')
");
$stmtTx->execute([
':ref' => $transactionRef,
':pid' => $passengerId,
':amt' => $amount,
':curr' => $currency
]);
// 6b. تفعيل أو تجديد اشتراك Prime (30 يوماً)
$expireAt = date('Y-m-d H:i:s', strtotime('+30 days'));
$stmtPrime = $con->prepare("
INSERT INTO passenger_prime_subscriptions (passenger_id, is_prime, expire_at)
VALUES (:pid, 1, :exp)
ON DUPLICATE KEY UPDATE is_prime = 1, expire_at = :exp2, updated_at = NOW()
");
$stmtPrime->execute([
':pid' => $passengerId,
':exp' => $expireAt,
':exp2' => $expireAt
]);
// 6c. خصم المبلغ من المحفظة عبر S2S (نفس نمط tips/add.php)
$deductUrl = "$walletServer/v2/main/ride/payment/add.php";
$deductData = [
"user_id" => $passengerId,
"user_type" => "passenger",
"amount" => -1 * $amount, // سالب = خصم
"action" => "subtract",
"paymentID" => $transactionRef,
"paymentMethod" => "prime-subscription",
"reason" => "Siro Prime Subscription - 1 Month"
];
$chDeduct = curl_init($deductUrl);
curl_setopt_array($chDeduct, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($deductData),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
'X-S2S-Api-Key: ' . $s2sKey
]
]);
$deductRaw = curl_exec($chDeduct);
$deductCode = curl_getinfo($chDeduct, CURLINFO_HTTP_CODE);
$deductErr = curl_error($chDeduct);
curl_close($chDeduct);
$deductRes = json_decode($deductRaw, true);
if ($deductErr || $deductCode !== 200 || ($deductRes['status'] ?? '') !== 'success') {
// فشل الخصم → نرجع الكل
$con->rollBack();
error_log("[Prime] Wallet deduct FAILED: HTTP $deductCode | err: $deductErr | response: $deductRaw");
jsonError("Failed to deduct wallet balance. Please try again.");
exit;
}
$con->commit();
// 6d. تحديث Redis فوراً (التفعيل اللحظي بدون إعادة طلب من DB)
if (isset($redis) && $redis !== null) {
try {
$primeKey = "prime:passenger:{$passengerId}";
$redis->setex($primeKey, 3600, json_encode([
'is_prime' => 1,
'expire_at' => $expireAt
]));
} catch (Exception $e) {
// Redis failure is non-critical — DB is source of truth
error_log("[Prime] Redis update failed (non-critical): " . $e->getMessage());
}
}
// ── 7. ردّ النجاح للفلاتر ───────────────────────────────────
jsonSuccess([
'is_prime' => true,
'expire_at' => $expireAt,
'transaction_ref' => $transactionRef,
'amount_deducted' => $amount,
'currency' => $currency,
], "Welcome to Siro Prime! 👑");
} catch (PDOException $e) {
if ($con->inTransaction()) {
$con->rollBack();
}
error_log("[Prime] DB Error: " . $e->getMessage());
jsonError("Database error. Please try again.");
}
?>
+76 -9
View File
@@ -112,7 +112,7 @@ function getPerKmRate($carType, $kazanRow) {
return $rate;
}
function calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanRow, $startNameAddress, $endNameAddress, $destLat, $destLng, $passengerLat, $passengerLng, $carType = 'Speed') {
function calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanRow, $startNameAddress, $endNameAddress, $destLat, $destLng, $passengerLat, $passengerLng, $carType = 'Speed', $isPrime = false) {
global $redis, $redisLocation, $con;
$surgeMultiplier = 1.0;
@@ -250,11 +250,17 @@ function calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanR
$billableMinutes = ($billableMinutes > $minuteCapMedium) ? $minuteCapMedium : $billableMinutes;
}
$fare = $billableDistance * $perKmSpeed;
$fare += $billableMinutes * $effectivePerMin;
$baseFareForDistance = $billableDistance * $perKmSpeed;
// 1. حساب التسعيرة مع الذروة (للسائق)
$fareWithSurge = $baseFareForDistance + ($billableMinutes * $effectivePerMin);
$fareWithSurge *= $surgeMultiplier;
// 2. حساب التسعيرة الطبيعية بدون ذروة (لراكب Prime)
$fareNoSurge = $baseFareForDistance + ($billableMinutes * $naturePrice);
// Apply Redis Geohash Surge Multiplier
$fare *= $surgeMultiplier;
// نحدد التسعيرة الأساسية حسب حالة الراكب
$fare = $isPrime ? $fareNoSurge : $fareWithSurge;
if ($airportCtx) $fare += $airportAddon;
if ($damascusAirportBoundCtx || $isInDamascusAirportBoundCtx) {
$fare += $damascusAirportBoundAddon;
@@ -363,10 +369,21 @@ function calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanR
}
}
// Apply kazan (e.g. 11%)
// Apply kazan (e.g. 11%) on the passenger's price
$withCommission = ceil($price * (1 + $kazanPercent / 100));
$kazan = $withCommission - $price;
$price_for_driver = $price;
// Driver price is based on the surged fare ($fareWithSurge)
$driverPriceTarget = max($fareWithSurge, $minFare);
if ($airportCtx) $driverPriceTarget += $airportAddon;
if ($damascusAirportBoundCtx || $isInDamascusAirportBoundCtx) {
$driverPriceTarget += $damascusAirportBoundAddon;
}
// The driver always gets the non-commissioned part of the SURGE price
$price_for_driver = $driverPriceTarget;
// Our commission is the difference between what passenger pays and what driver gets
$kazan = $withCommission - $price_for_driver;
return [
'price' => $price,
@@ -529,9 +546,58 @@ try {
error_log("[Destination Matching] Error: " . $e->getMessage());
}
// ----------------------------------------------------------------------
// Siro Prime: Check if passenger is a Prime subscriber
// ----------------------------------------------------------------------
$isPrime = false;
if (!empty($passenger_id)) {
$primeKey = "prime:passenger:{$passenger_id}";
$cachedPrime = null;
if (isset($redis) && $redis !== null) {
try {
$cachedPrime = $redis->get($primeKey);
} catch (Exception $e) {}
}
if ($cachedPrime !== false && $cachedPrime !== null) {
$primeData = json_decode($cachedPrime, true);
if (isset($primeData['is_prime']) && $primeData['is_prime'] == 1) {
$expireAt = strtotime($primeData['expire_at'] ?? '0');
if ($expireAt > time()) {
$isPrime = true;
}
}
} else {
// Fallback to MySQL if not in Redis
try {
$stmtPrime = $con->prepare("SELECT is_prime, expire_at FROM passenger_prime_subscriptions WHERE passenger_id = :pid LIMIT 1");
$stmtPrime->execute([':pid' => $passenger_id]);
$primeRow = $stmtPrime->fetch(PDO::FETCH_ASSOC);
if ($primeRow) {
$expireTime = strtotime($primeRow['expire_at'] ?? '0');
if ($primeRow['is_prime'] == 1 && $expireTime > time()) {
$isPrime = true;
}
// Cache in Redis for 1 hour
if (isset($redis) && $redis !== null) {
$redis->setex($primeKey, 3600, json_encode([
'is_prime' => $primeRow['is_prime'],
'expire_at' => $primeRow['expire_at']
]));
}
}
} catch (PDOException $e) {
error_log("[Prime] DB error: " . $e->getMessage());
}
}
}
// Calculate prices for all categories
foreach ($categories as $key => $carType) {
$result = calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanRow, $startNameAddress, $endNameAddress, $destLat, $destLng, $passengerLat, $passengerLng, $carType);
$result = calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanRow, $startNameAddress, $endNameAddress, $destLat, $destLng, $passengerLat, $passengerLng, $carType, $isPrime);
$withCommission = $result['withCommission'];
$price_for_driver = $result['price_for_driver'];
@@ -579,6 +645,7 @@ if (isset($encryptionHelper)) {
'duration' => $duration,
'is_destination_match' => $isDestinationMatch ? 1 : 0,
'matched_driver_id' => $matchedDriverId,
'is_prime' => $isPrime ? 1 : 0,
'expires' => time() + 420, // Valid for 7 minutes
'prices' => $pricesRaw
];
+4
View File
@@ -164,6 +164,9 @@ $price_for_passenger = $price;
$is_destination_match = isset($tokenData['is_destination_match']) ? (int)$tokenData['is_destination_match'] : 0;
$matched_driver_id = isset($tokenData['matched_driver_id']) ? $tokenData['matched_driver_id'] : 0;
// 👑 Siro Prime Status
$is_prime = isset($tokenData['is_prime']) ? (int)$tokenData['is_prime'] : 0;
// ── 2. تنسيق التواريخ ─────────────────────────────────────────
$date_formatted = date("Y-m-d");
$time_formatted = date("H:i:s");
@@ -286,6 +289,7 @@ try {
(string) $carType,
number_format($kazan, 2, '.', ''),
(string) $passenger_rating,
(string) $is_prime, // 👑 Index 34: Prime Status
];
// Direct dispatch للسائقين القريبين
@@ -174,8 +174,23 @@ try {
$payloadTemplate[29] = (string)$startName;
$payloadTemplate[30] = (string)$endName;
$payloadTemplate[31] = (string)$carType;
// 👑 Check Prime Status from Redis
$isPrimeFlag = "0";
if (isset($redis)) {
try {
$cachedPrime = $redis->get("prime:passenger:{$passengerId}");
if ($cachedPrime) {
$primeData = json_decode($cachedPrime, true);
if (isset($primeData['is_prime']) && $primeData['is_prime'] == 1) {
$isPrimeFlag = "1";
}
}
} catch (Exception $e) {}
}
$payloadTemplate[32] = (string)number_format($kazan, 2, '.', ''); // ← Reduced kazan
$payloadTemplate[33] = (string)$passengerRating;
$payloadTemplate[34] = $isPrimeFlag; // 👑 Index 34: Prime Status
ksort($payloadTemplate);
$payloadTemplate = array_values($payloadTemplate);