From 085b180bdbcd9515f8ec10991c7ef674c5579ead Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Sun, 19 Jul 2026 01:51:39 +0300 Subject: [PATCH] feat: implement secure OTP-based payout workflow with dynamic fee calculation and improved authentication checks --- .../loginUsingCredentialsWithoutGoogle.php | 72 +------------- backend/auth/otp/request.php | 6 +- backend/auth/otp/verify.php | 44 ++++----- .../loginUsingCredentialsWithoutGoogle.php | 50 +--------- backend/auth/passenger/register.php | 5 +- backend/core/Security/EncryptionHelper.php | 10 ++ .../v2/main/sms_webhook/finalize_payout.php | 35 +++++-- .../v2/main/sms_webhook/request_payout.php | 98 +++++++++++++++---- .../smsPaymnet/pay_out_syria_controller.dart | 24 ++++- .../views/home/my_wallet/pay_out_screen.dart | 83 +++++++++++++--- stress_test/generate_mock_data.php | 18 +++- 11 files changed, 252 insertions(+), 193 deletions(-) diff --git a/backend/auth/driver/loginUsingCredentialsWithoutGoogle.php b/backend/auth/driver/loginUsingCredentialsWithoutGoogle.php index 612cdd58..871030b5 100644 --- a/backend/auth/driver/loginUsingCredentialsWithoutGoogle.php +++ b/backend/auth/driver/loginUsingCredentialsWithoutGoogle.php @@ -42,75 +42,7 @@ $encryptedEmail = $encryptionHelper->encryptData($email); try { $con = Database::get('main'); - // Auto-seed/create tester driver if it doesn't exist - if ($cleanEmail === 'driver_tester@siromove.com') { - $stmtCheck = $con->prepare("SELECT id FROM driver WHERE email = :email LIMIT 1"); - $stmtCheck->bindParam(':email', $encryptedEmail); - $stmtCheck->execute(); - if (!$stmtCheck->fetch()) { - $driverId = 'tester_driver_id_2026'; - $phone = '+962790000002'; - $hashedPassword = password_hash('SiroDriver2026!', PASSWORD_DEFAULT); - - $encryptedPhone = $encryptionHelper->encryptData($phone); - $encryptedFirstName = $encryptionHelper->encryptData('Driver'); - $encryptedLastName = $encryptionHelper->encryptData('Tester'); - $encryptedGender = $encryptionHelper->encryptData('Male'); - $encryptedBirthdate = $encryptionHelper->encryptData('1990-01-01'); - $encryptedSite = $encryptionHelper->encryptData('Jordan'); - - // Insert driver - $insert = $con->prepare("INSERT INTO driver (id, phone, email, password, gender, birthdate, site, first_name, last_name) - VALUES (:id, :phone, :email, :password, :gender, :birthdate, :site, :first_name, :last_name)"); - $insert->execute([ - ':id' => $driverId, - ':phone' => $encryptedPhone, - ':email' => $encryptedEmail, - ':password' => $hashedPassword, - ':gender' => $encryptedGender, - ':birthdate' => $encryptedBirthdate, - ':site' => $encryptedSite, - ':first_name' => $encryptedFirstName, - ':last_name' => $encryptedLastName - ]); - - // Ensure phone_verification row exists - $stmtPhone = $con->prepare("SELECT * FROM phone_verification WHERE phone_number = :phone LIMIT 1"); - $stmtPhone->bindParam(':phone', $encryptedPhone); - $stmtPhone->execute(); - if (!$stmtPhone->fetch()) { - $insertPhone = $con->prepare("INSERT INTO phone_verification (phone_number, is_verified) VALUES (:phone, 1)"); - $insertPhone->bindParam(':phone', $encryptedPhone); - $insertPhone->execute(); - } else { - $updatePhone = $con->prepare("UPDATE phone_verification SET is_verified = 1 WHERE phone_number = :phone"); - $updatePhone->bindParam(':phone', $encryptedPhone); - $updatePhone->execute(); - } - - // Ensure CarRegistration row exists - $stmtCar = $con->prepare("SELECT * FROM CarRegistration WHERE driverID = :driverID LIMIT 1"); - $stmtCar->bindParam(':driverID', $driverId); - $stmtCar->execute(); - if (!$stmtCar->fetch()) { - $insertCar = $con->prepare("INSERT INTO CarRegistration (driverID, vin, car_plate, make, model, year, expiration_date, color, owner, color_hex, fuel) - VALUES (:driverID, :vin, :car_plate, 'Toyota', 'Prius', 2020, '2030-01-01', 'White', :owner, '#FFFFFF', 'Petrol')"); - $encryptedVin = $encryptionHelper->encryptData('TESTVIN1234567890'); - $encryptedPlate = $encryptionHelper->encryptData('155186'); - $encryptedOwner = $encryptionHelper->encryptData('Driver Tester'); - $insertCar->execute([ - ':driverID' => $driverId, - ':vin' => $encryptedVin, - ':car_plate' => $encryptedPlate, - ':owner' => $encryptedOwner - ]); - } else { - $updateCar = $con->prepare("UPDATE CarRegistration SET make = 'Toyota', model = 'Prius', year = 2020 WHERE driverID = :driverID"); - $updateCar->bindParam(':driverID', $driverId); - $updateCar->execute(); - } - } - } + // Auto-seed/create tester driver logic removed for security // SQL لاسترجاع المستخدم بناءً على البريد الإلكتروني المشفر $sql = "SELECT @@ -141,7 +73,7 @@ try { } // فحص الباسورد (في نظامنا، يمكن أن يكون الباسورد هو HMAC أو نص عادي للفاحصين) // لنفترض أن الفاحص له باسورد عادي أو مشفر بـ bcrypt - if (password_verify($password, $data['password']) || $password === $data['password']) { + if (password_verify($password, $data['password'])) { unset($data['password']); // فك تشفير الحقول الحساسة diff --git a/backend/auth/otp/request.php b/backend/auth/otp/request.php index 17da5878..58676f80 100644 --- a/backend/auth/otp/request.php +++ b/backend/auth/otp/request.php @@ -119,8 +119,8 @@ switch (strtolower($country)) { // 6. DB Storage on Success if ($sentSuccessfully) { - $encryptedPhone = $encryptionHelper->encryptData($receiver); - $encryptedOtp = $encryptionHelper->encryptData($otp); + $encryptedPhone = $encryptionHelper->encryptData($receiver); // Deterministic CBC + $encryptedOtp = $encryptionHelper->encryptDataGCM($otp); // Random GCM $encryptedEmail = !empty($email) ? $encryptionHelper->encryptData($email) : ''; try { @@ -143,7 +143,7 @@ if ($sentSuccessfully) { $encryptedOtp ]); } elseif ($user_type === 'driver') { - if ($context === 'token_change') { + if ($context === 'token_change' || $context === 'payout') { // Delete old verification attempts $stmtDel = $con->prepare("DELETE FROM `token_verification_driver` WHERE `phone_number` = ?"); $stmtDel->execute([$encryptedPhone]); diff --git a/backend/auth/otp/verify.php b/backend/auth/otp/verify.php index 6d19b01b..4bb10854 100644 --- a/backend/auth/otp/verify.php +++ b/backend/auth/otp/verify.php @@ -59,18 +59,19 @@ try { // 3. Encrypt data to query // 4. Verify based on user type try { + $encryptedPhoneSearch = $encryptionHelper->encryptData($phone_number); + if ($user_type === 'admin') { $sql = "SELECT * FROM token_verification_admin - WHERE expiration_time >= NOW() AND verified = 0"; + WHERE expiration_time >= NOW() AND verified = 0 AND phone_number = ?"; $stmt = $con->prepare($sql); - $stmt->execute(); + $stmt->execute([$encryptedPhoneSearch]); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $matchedRow = null; foreach ($rows as $row) { - $decryptedPhone = $encryptionHelper->decryptData($row['phone_number']); $decryptedToken = $encryptionHelper->decryptData($row['token']); - if ($decryptedPhone === $phone_number && $decryptedToken === $token_code) { + if ($decryptedToken === $token_code) { $matchedRow = $row; break; } @@ -101,16 +102,15 @@ try { } } elseif ($user_type === 'service') { $sql = "SELECT `id`, `phone_number`, `token_code` FROM `phone_verification_service` - WHERE `expiration_time` > NOW() AND `is_verified` = 0"; + WHERE `expiration_time` > NOW() AND `is_verified` = 0 AND `phone_number` = ?"; $stmt = $con->prepare($sql); - $stmt->execute(); + $stmt->execute([$encryptedPhoneSearch]); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $matchedRowId = null; foreach ($rows as $row) { - $decryptedPhone = $encryptionHelper->decryptData($row['phone_number']); $decryptedToken = $encryptionHelper->decryptData($row['token_code']); - if ($decryptedPhone === $phone_number && $decryptedToken === $token_code) { + if ($decryptedToken === $token_code) { $matchedRowId = $row['id']; break; } @@ -128,16 +128,15 @@ try { } elseif ($user_type === 'driver') { if ($context === 'token_change') { $sql = "SELECT `id`, `phone_number`, `token` FROM `token_verification_driver` - WHERE `expiration_time` > NOW() AND `verified` = 0"; + WHERE `expiration_time` > NOW() AND `verified` = 0 AND `phone_number` = ?"; $stmt = $con->prepare($sql); - $stmt->execute(); + $stmt->execute([$encryptedPhoneSearch]); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $matchedRowId = null; foreach ($rows as $row) { - $decryptedPhone = $encryptionHelper->decryptData($row['phone_number']); $decryptedToken = $encryptionHelper->decryptData($row['token']); - if ($decryptedPhone === $phone_number && $decryptedToken === $token_code) { + if ($decryptedToken === $token_code) { $matchedRowId = $row['id']; break; } @@ -154,16 +153,15 @@ try { } } else { $sql = "SELECT `id`, `phone_number`, `token_code` FROM `phone_verification` - WHERE `expiration_time` > NOW() AND `is_verified` = 0"; + WHERE `expiration_time` > NOW() AND `is_verified` = 0 AND `phone_number` = ?"; $stmt = $con->prepare($sql); - $stmt->execute(); + $stmt->execute([$encryptedPhoneSearch]); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $matchedRowId = null; foreach ($rows as $row) { - $decryptedPhone = $encryptionHelper->decryptData($row['phone_number']); $decryptedToken = $encryptionHelper->decryptData($row['token_code']); - if ($decryptedPhone === $phone_number && $decryptedToken === $token_code) { + if ($decryptedToken === $token_code) { $matchedRowId = $row['id']; break; } @@ -210,16 +208,15 @@ try { } else { if ($context === 'token_change') { $sql = "SELECT `id`, `phone_number`, `token` FROM `token_verification` - WHERE `expiration_time` > NOW() AND `verified` = 0"; + WHERE `expiration_time` > NOW() AND `verified` = 0 AND `phone_number` = ?"; $stmt = $con->prepare($sql); - $stmt->execute(); + $stmt->execute([$encryptedPhoneSearch]); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $matchedRowId = null; foreach ($rows as $row) { - $decryptedPhone = $encryptionHelper->decryptData($row['phone_number']); $decryptedToken = $encryptionHelper->decryptData($row['token']); - if ($decryptedPhone === $phone_number && $decryptedToken === $token_code) { + if ($decryptedToken === $token_code) { $matchedRowId = $row['id']; break; } @@ -236,16 +233,15 @@ try { } } else { $sql = "SELECT `id`, `phone_number`, `token` FROM `phone_verification_passenger` - WHERE `expiration_time` > NOW() AND `verified` = 0"; + WHERE `expiration_time` > NOW() AND `verified` = 0 AND `phone_number` = ?"; $stmt = $con->prepare($sql); - $stmt->execute(); + $stmt->execute([$encryptedPhoneSearch]); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $matchedRowId = null; foreach ($rows as $row) { - $decryptedPhone = $encryptionHelper->decryptData($row['phone_number']); $decryptedToken = $encryptionHelper->decryptData($row['token']); - if ($decryptedPhone === $phone_number && $decryptedToken === $token_code) { + if ($decryptedToken === $token_code) { $matchedRowId = $row['id']; break; } diff --git a/backend/auth/passenger/loginUsingCredentialsWithoutGoogle.php b/backend/auth/passenger/loginUsingCredentialsWithoutGoogle.php index 536aaad3..a9dde3b1 100644 --- a/backend/auth/passenger/loginUsingCredentialsWithoutGoogle.php +++ b/backend/auth/passenger/loginUsingCredentialsWithoutGoogle.php @@ -41,53 +41,7 @@ try { // تشفير الإيميل للبحث في قاعدة البيانات $encryptedEmail = $encryptionHelper->encryptData($email); - // Auto-seed/create tester passenger if it doesn't exist - if ($cleanEmail === 'passenger_tester@siromove.com') { - $stmtCheck = $con->prepare("SELECT id FROM passengers WHERE email = :email LIMIT 1"); - $stmtCheck->bindParam(':email', $encryptedEmail); - $stmtCheck->execute(); - if (!$stmtCheck->fetch()) { - $passengerId = 'tester_passenger_id_2026'; - $phone = '+962790000003'; - $hashedPassword = password_hash('SiroPassenger2026!', PASSWORD_DEFAULT); - - $encryptedPhone = $encryptionHelper->encryptData($phone); - $encryptedFirstName = $encryptionHelper->encryptData('Passenger'); - $encryptedLastName = $encryptionHelper->encryptData('Tester'); - $encryptedGender = $encryptionHelper->encryptData('Male'); - $encryptedBirthdate = $encryptionHelper->encryptData('1990-01-01'); - $encryptedSite = $encryptionHelper->encryptData('Jordan'); - - // Insert passenger with verified = 1 so app doesn't reject - $insert = $con->prepare("INSERT INTO passengers (id, phone, email, password, gender, birthdate, site, first_name, last_name, is_test, verified) - VALUES (:id, :phone, :email, :password, :gender, :birthdate, :site, :first_name, :last_name, 1, 1)"); - $insert->execute([ - ':id' => $passengerId, - ':phone' => $encryptedPhone, - ':email' => $encryptedEmail, - ':password' => $hashedPassword, - ':gender' => $encryptedGender, - ':birthdate' => $encryptedBirthdate, - ':site' => $encryptedSite, - ':first_name' => $encryptedFirstName, - ':last_name' => $encryptedLastName - ]); - - // Ensure phone_verification_passenger row exists - $stmtPhone = $con->prepare("SELECT * FROM phone_verification_passenger WHERE phone_number = :phone LIMIT 1"); - $stmtPhone->bindParam(':phone', $encryptedPhone); - $stmtPhone->execute(); - if (!$stmtPhone->fetch()) { - $insertPhone = $con->prepare("INSERT INTO phone_verification_passenger (phone_number, verified) VALUES (:phone, 1)"); - $insertPhone->bindParam(':phone', $encryptedPhone); - $insertPhone->execute(); - } else { - $updatePhone = $con->prepare("UPDATE phone_verification_passenger SET verified = 1 WHERE phone_number = :phone"); - $updatePhone->bindParam(':phone', $encryptedPhone); - $updatePhone->execute(); - } - } - } + // Auto-seed/create tester passenger logic removed for security $sql = "SELECT p.*, @@ -111,7 +65,7 @@ try { if ($data) { // فحص الباسورد - if (password_verify($password, $data['password']) || $password === $data['password']) { + if (password_verify($password, $data['password'])) { // التحقق من أن الحساب معلم كحساب فحص في قاعدة البيانات أو البيئة $isTestInDb = (isset($data['is_test']) && $data['is_test'] == 1) || (isset($data['isTest']) && $data['isTest'] == 1); if (!$isTestInDb && !$isTester) { diff --git a/backend/auth/passenger/register.php b/backend/auth/passenger/register.php index 7fc48534..6c423c8b 100644 --- a/backend/auth/passenger/register.php +++ b/backend/auth/passenger/register.php @@ -63,7 +63,8 @@ try { $firstName_encrypted = $encryptionHelper->encryptData($firstName); $lastName_encrypted = $encryptionHelper->encryptData($lastName); $email_encrypted = $encryptionHelper->encryptData($email); - $password_hashed = password_hash($email, PASSWORD_DEFAULT); + $uniqueId = substr(md5($phoneNumber), 0, 20); + $password_hashed = password_hash($email . $uniqueId, PASSWORD_DEFAULT); $unknown_encrypted = $encryptionHelper->encryptData("unknown yet"); // ====================================================== @@ -94,7 +95,7 @@ try { $step = 5; // $uniqueId = substr(md5(uniqid(mt_rand(), true)), 0, 20); - $uniqueId = substr(md5($phoneNumber_encrypted), 0, 20); + // $uniqueId is now generated earlier error_log("$logTag Step 5: Generated Unique ID: $uniqueId"); diff --git a/backend/core/Security/EncryptionHelper.php b/backend/core/Security/EncryptionHelper.php index 86e73e31..d410f8f1 100644 --- a/backend/core/Security/EncryptionHelper.php +++ b/backend/core/Security/EncryptionHelper.php @@ -33,6 +33,16 @@ class EncryptionHelper return base64_encode($encrypted); } + // ─── تشفير نص باستخدام AES-256-GCM العشوائي (عالي الأمان) ── + public function encryptDataGCM(string $plainText): string + { + $plainText = mb_convert_encoding($plainText, 'UTF-8'); + $iv = random_bytes(self::IV_LEN_GCM); + $tag = ''; + $encrypted = openssl_encrypt($plainText, self::ALGO_GCM, $this->key, OPENSSL_RAW_DATA, $iv, $tag, "", self::TAG_LEN); + return self::PREFIX_GCM . base64_encode($iv . $tag . $encrypted); + } + // ─── فك تشفير نص (يدعم CBC والـ GCM المستقبلي) ─────────── public function decryptData(string $cipherText): string|false { diff --git a/payment_server/v2/main/sms_webhook/finalize_payout.php b/payment_server/v2/main/sms_webhook/finalize_payout.php index 10fbbb7a..f7a704ee 100755 --- a/payment_server/v2/main/sms_webhook/finalize_payout.php +++ b/payment_server/v2/main/sms_webhook/finalize_payout.php @@ -67,9 +67,25 @@ if (!function_exists('finalizePayout')) { } $driverId = $payout['driver_id']; - $payoutFee = 3500; - $netAmount = (float)$payout['amount']-$payoutFee; // المبلغ الصافي الذي طلبه السائق - $totalDeducted = $netAmount; // المبلغ الإجمالي الذي سيتم خصمه + // Fetch driver site to calculate dynamic fee + $stmtDriver = $con->prepare("SELECT site FROM driver WHERE id = :id"); + $stmtDriver->execute([':id' => $driverId]); + $driverInfo = $stmtDriver->fetch(PDO::FETCH_ASSOC); + $site = $encryptionHelper->decryptData($driverInfo['site'] ?? ''); + + $payoutFee = 0.0; + if (strtolower($site) === 'syria') { + $payoutFee = 35.00; + } elseif (strtolower($site) === 'egypt') { + $payoutFee = 10.00; + } elseif (strtolower($site) === 'jordan') { + $payoutFee = 0.00; + } else { + $payoutFee = 35.00; + } + + $netAmount = (float)$payout['amount']; // The amount requested + $totalDeducted = $netAmount + $payoutFee; // Total deducted // 2. إنشاء معرف دفع رئيسي لهذه المعاملة // نسجل المبلغ الإجمالي المخصوم بالسالب @@ -83,17 +99,16 @@ if (!function_exists('finalizePayout')) { if (!$tokenDriver || !$tokenSiro) throw new Exception('Failed to generate required tokens'); logPayoutError("GEN_TOKENS", "Driver and Siro tokens generated successfully."); - // 4. تسجيل معاملة الخصم في محفظة السائق (driverWallet) - $insertDriver = $con->prepare("INSERT INTO driverWallet (driverID, paymentID, amount, paymentMethod) VALUES (:driverID, :paymentID, :amount, :paymentMethod)"); - $insertDriver->execute([ + // 4. Update the reserved deduction in driverWallet with the real paymentID + $updateDriver = $con->prepare("UPDATE driverWallet SET paymentID = :paymentID, paymentMethod = 'payout' WHERE driverID = :driverID AND paymentMethod = 'payout_reserved' AND amount = :amount LIMIT 1"); + $updateDriver->execute([ ':driverID' => $driverId, ':paymentID' => $paymentID, - ':amount' => -$totalDeducted, // تسجيل المبلغ بالسالب - ':paymentMethod' => 'payout' + ':amount' => -$totalDeducted ]); - if ($insertDriver->rowCount() === 0) throw new Exception('Insert to driverWallet failed'); + $con->prepare("UPDATE payment_tokens SET isUsed = TRUE WHERE token = :token")->execute([':token' => $tokenDriver]); - logPayoutError("DRIVER_WALLET", "Negative transaction of {$totalDeducted} recorded in driverWallet."); + logPayoutError("DRIVER_WALLET", "Updated reserved transaction of {$totalDeducted} in driverWallet."); // 5. تسجيل معاملة الربح (العمولة) في محفظة الشركة (siroWallet) $insertSiro = $con->prepare("INSERT INTO siroWallet (driverId, passengerId, amount, paymentMethod, token) VALUES (:driverId, :passengerId, :amount, :paymentMethod, :token)"); diff --git a/payment_server/v2/main/sms_webhook/request_payout.php b/payment_server/v2/main/sms_webhook/request_payout.php index 48bd2c81..ddcec7c5 100755 --- a/payment_server/v2/main/sms_webhook/request_payout.php +++ b/payment_server/v2/main/sms_webhook/request_payout.php @@ -1,16 +1,22 @@ sub)) { + $driverId = $decodedToken->sub; +} else { + printFailure("Unauthorized. Valid token required."); + exit; +} + $amount_raw = filterRequest("amount"); -$wallet_type = filterRequest("wallet_type") ?? 'Sham Cash'; // قيمة افتراضية -$phone = filterRequest("phone"); +$wallet_type = filterRequest("wallet_type") ?? 'Sham Cash'; +$otp = filterRequest("otp"); -// تأكيد المبلغ كرقم $amount = is_numeric($amount_raw) ? (float)$amount_raw : 0.0; // تحقق أساسي @@ -19,28 +25,80 @@ if (empty($driverId) || $amount <= 0) { exit; } +if (empty($otp)) { + printFailure("OTP code is required for payout."); + exit; +} + try { - // --- Atomic balance check + insert with transaction --- + // --- 2. Fetch Driver info & Dynamic Fee (S3) --- + $stmtDriver = $con->prepare("SELECT phone, site FROM driver WHERE id = :id"); + $stmtDriver->execute([':id' => $driverId]); + $driverInfo = $stmtDriver->fetch(PDO::FETCH_ASSOC); + + if (!$driverInfo) { + printFailure("Driver not found."); + exit; + } + + $phone = $encryptionHelper->decryptData($driverInfo['phone']); + $site = $encryptionHelper->decryptData($driverInfo['site']); + + $payout_fee = 0.0; + if (strtolower($site) === 'syria') { + $payout_fee = 35.00; + } elseif (strtolower($site) === 'egypt') { + $payout_fee = 10.00; + } elseif (strtolower($site) === 'jordan') { + $payout_fee = 0.00; + } else { + $payout_fee = 35.00; // Default fallback + } + + $total_deduction = $amount + $payout_fee; + + // --- 3. OTP Verification (S6) --- + $encryptedPhone = $encryptionHelper->encryptData($phone); + $encryptedOtp = $encryptionHelper->encryptData($otp); + + $stmtOtp = $con->prepare("SELECT id FROM token_verification_driver + WHERE phone_number = ? AND token = ? + AND expiration_time > NOW() AND verified = 0 + LIMIT 1"); + $stmtOtp->execute([$encryptedPhone, $encryptedOtp]); + $otpRow = $stmtOtp->fetch(PDO::FETCH_ASSOC); + + if (!$otpRow) { + printFailure("Invalid or expired OTP. Please request a new one."); + exit; + } + + // --- 4. Atomic balance check + deduction + insert (S2, S4) --- $con->beginTransaction(); - $stmt_driver = $con->prepare(" + $stmt_balance = $con->prepare(" SELECT COALESCE(SUM(amount), 0) AS balance FROM driverWallet WHERE driverID = :id FOR UPDATE "); - $stmt_driver->execute([':id' => $driverId]); - $driver = $stmt_driver->fetch(PDO::FETCH_ASSOC); + $stmt_balance->execute([':id' => $driverId]); + $wallet = $stmt_balance->fetch(PDO::FETCH_ASSOC); - $payout_fee = 3500.00; - $total_deduction = $amount + $payout_fee; - - if ($driver['balance'] < $total_deduction) { + if ($wallet['balance'] < $total_deduction) { $con->rollBack(); printFailure("Insufficient balance. Required: $total_deduction"); exit; } + // S4 Fix: Deduct the balance IMMEDIATELY + $insertDriver = $con->prepare("INSERT INTO driverWallet (driverID, paymentID, amount, paymentMethod) VALUES (:driverID, NULL, :amount, :paymentMethod)"); + $insertDriver->execute([ + ':driverID' => $driverId, + ':amount' => -$total_deduction, + ':paymentMethod' => 'payout_reserved' + ]); + $sql = " INSERT INTO payout_requests (driver_id, driver_phone, amount, wallet_type) VALUES (:did, :phone, :amount, :wallet) @@ -53,21 +111,24 @@ try { ':wallet'=> $wallet_type ]); + // Mark OTP as verified + $con->prepare("UPDATE token_verification_driver SET verified = 1 WHERE id = ?")->execute([$otpRow['id']]); + $con->commit(); if ($stmt->rowCount() > 0) { - // --- 4. إرسال إشعار لخدمة العملاء --- $customerServicePhone = getenv('CUSTOMER_SERVICE_PHONE'); $message = "⚠️ طلب دفع جديد:\n" . "ID السائق: {$driverId}\n" . "هاتف السائق: {$phone}\n" . + "البلد: {$site}\n" . "نوع المحفظة: {$wallet_type}\n" . - "المبلغ: {$amount} SYP\n\n" . + "المبلغ المطلوب: {$amount}\n" . + "الرسوم المخصومة: {$payout_fee}\n\n" . "الرجاء من فريق خدمة العملاء تنفيذ عملية الدفع الآن."; - // الإرسال (الفنكشن مُضمّن لديك مسبقًا) sendWhatsAppFromServer($customerServicePhone, $message); error_log("[RequestPayout] Successfully created payout request for driver ID: $driverId"); @@ -77,6 +138,9 @@ try { } } catch (PDOException $e) { + if ($con->inTransaction()) { + $con->rollBack(); + } error_log("[RequestPayout] PDOException: " . $e->getMessage()); printFailure("A database error occurred."); } diff --git a/siro_driver/lib/controller/payment/smsPaymnet/pay_out_syria_controller.dart b/siro_driver/lib/controller/payment/smsPaymnet/pay_out_syria_controller.dart index 0cdbae33..3bb60051 100644 --- a/siro_driver/lib/controller/payment/smsPaymnet/pay_out_syria_controller.dart +++ b/siro_driver/lib/controller/payment/smsPaymnet/pay_out_syria_controller.dart @@ -4,13 +4,14 @@ import 'package:siro_driver/controller/functions/crud.dart'; class PayoutService { final String _baseUrl = "https://walletintaleq.intaleq.xyz/v1/main/sms_webhook"; - static const double payoutFee = 5000.0; // عمولة السحب الثابتة + static const double payoutFee = 50.0; // عمولة السحب الثابتة /// دالة لإنشاء طلب سحب جديد على السيرفر /// /// تعيد رسالة النجاح من السيرفر، أو رسالة خطأ في حال الفشل. Future requestPayout({ required String driverId, + required String otp, walletType, payoutPhoneNumber, required double amount, @@ -23,6 +24,7 @@ class PayoutService { 'amount': amount.toString(), 'phone': payoutPhoneNumber.toString(), 'wallet_type': walletType.toString(), + 'otp': otp, }).timeout(const Duration(seconds: 20)); if (response != 'failure') { @@ -42,4 +44,24 @@ class PayoutService { return "حدث خطأ غير متوقع. يرجى المحاولة مرة أخرى."; } } + + Future requestOtp({required String phone}) async { + final url = "https://walletintaleq.intaleq.xyz/v1/auth/otp/request.php"; + try { + final response = await CRUD().postWallet(link: url, payload: { + 'phone_number': phone, + 'user_type': 'driver', + 'context': 'payout', + }).timeout(const Duration(seconds: 20)); + + if (response != 'failure') { + final data = (response); + return data['status'] == 'success'; + } + return false; + } catch (e) { + debugPrint("Exception during OTP request: $e"); + return false; + } + } } diff --git a/siro_driver/lib/views/home/my_wallet/pay_out_screen.dart b/siro_driver/lib/views/home/my_wallet/pay_out_screen.dart index d19e76b9..131257c7 100644 --- a/siro_driver/lib/views/home/my_wallet/pay_out_screen.dart +++ b/siro_driver/lib/views/home/my_wallet/pay_out_screen.dart @@ -41,22 +41,15 @@ class _PayoutScreenState extends State { if (didAuthenticate && mounted) { setState(() => _isLoading = true); - // 2. إرسال الطلب إلى السيرفر بالبيانات الجاهزة - final result = await _payoutService.requestPayout( - driverId: - box.read(BoxName.driverID).toString(), // استبدله بـ box.read - amount: widget.amountToWithdraw, - payoutPhoneNumber: widget.payoutPhoneNumber, - walletType: widget.walletType, - ); - + // 2. طلب رمز التحقق OTP + bool otpSent = await _payoutService.requestOtp(phone: widget.payoutPhoneNumber); setState(() => _isLoading = false); - if (result != null && result.contains("successfully")) { - // 3. عرض رسالة النجاح النهائية - _showSuccessDialog(); + if (otpSent) { + // 3. عرض حوار إدخال الرمز + _showOtpDialog(); } else { - _showErrorDialog(result ?? "حدث خطأ غير معروف."); + _showErrorDialog("فشل في إرسال رمز التحقق. يرجى المحاولة لاحقاً."); } } } catch (e) { @@ -66,6 +59,70 @@ class _PayoutScreenState extends State { } } + void _showOtpDialog() { + final TextEditingController otpController = TextEditingController(); + showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => AlertDialog( + title: const Text('أدخل رمز التحقق (OTP)'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('تم إرسال رمز تحقق إلى رقم هاتفك.'), + const SizedBox(height: 16), + TextField( + controller: otpController, + keyboardType: TextInputType.number, + maxLength: 4, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'رمز التحقق', + ), + ), + ], + ), + actions: [ + TextButton( + child: const Text('إلغاء'), + onPressed: () => Navigator.of(ctx).pop(), + ), + ElevatedButton( + child: const Text('تأكيد'), + onPressed: () { + final otp = otpController.text.trim(); + if (otp.length >= 3) { + Navigator.of(ctx).pop(); + _submitPayoutWithOtp(otp); + } + }, + ), + ], + ), + ); + } + + Future _submitPayoutWithOtp(String otp) async { + setState(() => _isLoading = true); + + final result = await _payoutService.requestPayout( + driverId: box.read(BoxName.driverID).toString(), + otp: otp, + amount: widget.amountToWithdraw, + payoutPhoneNumber: widget.payoutPhoneNumber, + walletType: widget.walletType, + ); + + setState(() => _isLoading = false); + + if (result != null && result.contains("successfully")) { + _showSuccessDialog(); + } else { + _showErrorDialog(result ?? "حدث خطأ غير معروف."); + } + } + + @override Widget build(BuildContext context) { // حساب المبلغ الإجمالي المخصوم diff --git a/stress_test/generate_mock_data.php b/stress_test/generate_mock_data.php index e27ddf0f..6a434797 100644 --- a/stress_test/generate_mock_data.php +++ b/stress_test/generate_mock_data.php @@ -10,13 +10,20 @@ error_reporting(E_ALL); ini_set('display_errors', '1'); // ── Load .env ── +$loadedFiles = []; function loadEnvFile(string $path): void { - if (!file_exists($path)) return; + global $loadedFiles; + if (!file_exists($path)) { + $loadedFiles[] = "NOT_FOUND: $path"; + return; + } + $loadedFiles[] = "LOADED: $path"; foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { $line = trim($line); if (empty($line) || $line[0] === '#' || strpos($line, '=') === false) continue; [$name, $value] = explode('=', $line, 2); putenv(trim($name) . '=' . trim($value, "\"'")); + $_ENV[trim($name)] = trim($value, "\"'"); } } @@ -32,10 +39,10 @@ function getJwtSecret(): string { return trim(file_get_contents($keyPath)); } // Priority 2: JWT_SECRET_KEY env var - $key = getenv('JWT_SECRET_KEY'); + $key = getenv('JWT_SECRET_KEY') ?: ($_ENV['JWT_SECRET_KEY'] ?? ''); if ($key) return $key; // Priority 3: JWT_SECRET env var (backend .env uses this) - $key = getenv('JWT_SECRET'); + $key = getenv('JWT_SECRET') ?: ($_ENV['JWT_SECRET'] ?? ''); if ($key) return $key; return ''; } @@ -55,8 +62,9 @@ function generate_jwt(string $secret, array $payload): string { // ── Main ── $secret = getJwtSecret(); if (empty($secret)) { - echo json_encode(['error' => 'JWT secret not found in any .env', 'jwt' => '']); - exit(1); + echo json_encode(['error' => 'JWT secret not found in any .env', 'files_checked' => $loadedFiles, 'jwt' => '']); + // Do not exit with 1 so Node.js can print the JSON error + exit(0); } $driverId = $argv[1] ?? '999999';