Update: 2026-06-11 21:53:27
This commit is contained in:
128
backend/ride/driverWallet/transfer.php
Normal file
128
backend/ride/driverWallet/transfer.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
// backend/ride/driverWallet/transfer.php
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// 1. Authenticate user
|
||||
$decodedToken = authenticateJWT();
|
||||
if (!$decodedToken) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$senderID = filterRequest('driverID');
|
||||
$receiverPhone = filterRequest('receiverPhone');
|
||||
$amount = filterRequest('amount');
|
||||
$country = filterRequest('country');
|
||||
|
||||
if (empty($senderID) || empty($receiverPhone) || empty($amount) || empty($country)) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Missing required fields']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Ensure the sender matches the token
|
||||
if ($decodedToken->id != $senderID) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Unauthorized driver ID']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2. Fetch Receiver details
|
||||
$stmt = $con->prepare("SELECT d.id as driver_id, dt.token as fcm_token, d.name_arabic
|
||||
FROM driver d
|
||||
LEFT JOIN driverToken dt ON d.id = dt.captain_id
|
||||
WHERE d.phone = :phone LIMIT 1");
|
||||
$stmt->execute([':phone' => $receiverPhone]);
|
||||
$receiver = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$receiver) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Receiver not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$receiverID = $receiver['driver_id'];
|
||||
|
||||
if ($receiverID == $senderID) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Cannot transfer to yourself']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 3. Determine Payment Server URL based on Country
|
||||
$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://walletintaleq.intaleq.xyz";
|
||||
} elseif (strtolower($country) === 'syria') {
|
||||
$walletServer = getenv('WALLET_SERVER_SYRIA') ?: "https://walletintaleq.intaleq.xyz";
|
||||
}
|
||||
|
||||
$paymentServerUrl = "$walletServer/v2/main/ride/driverWallet/transfer.php";
|
||||
|
||||
$postData = [
|
||||
'senderID' => $senderID,
|
||||
'receiverID' => $receiverID,
|
||||
'amount' => $amount,
|
||||
'country' => $country
|
||||
];
|
||||
|
||||
// Generate Headers for Payment Server (Use internal payment key)
|
||||
$headers = [];
|
||||
$paymentKey = getenv('PAYMENT_KEY') ;
|
||||
|
||||
if (!empty($paymentKey)) {
|
||||
$headers[] = "payment-key: $paymentKey";
|
||||
} else {
|
||||
// Fallback just in case
|
||||
$headers[] = "payment-key: default_internal_secret_123";
|
||||
}
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $paymentServerUrl);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
|
||||
$paymentResponseRaw = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$paymentResponse = json_decode($paymentResponseRaw, true);
|
||||
|
||||
// 4. Handle Payment Server Response
|
||||
if ($httpCode === 200 && isset($paymentResponse['status']) && $paymentResponse['status'] === 'success') {
|
||||
// Transaction successful, send Push Notification
|
||||
if (!empty($receiver['fcm_token'])) {
|
||||
$senderName = $decodedToken->name ?? 'A driver'; // Optional: Fetch sender name
|
||||
|
||||
$fcmBody = "You have received a transfer of " . $amount . " from " . $senderName;
|
||||
// Arabic fallback if name available
|
||||
$fcmBodyAr = "لقد تلقيت حوالة بقيمة " . $amount . " من " . $senderName;
|
||||
|
||||
sendFCM_Internal(
|
||||
$receiver['fcm_token'],
|
||||
"Transfer Received",
|
||||
$fcmBodyAr,
|
||||
['type' => 'transfer', 'amount' => $amount],
|
||||
'Transfer',
|
||||
false,
|
||||
'ding'
|
||||
);
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => 'Transfer completed successfully',
|
||||
'receiver' => $receiver['name_arabic']
|
||||
]);
|
||||
} else {
|
||||
// Payment failed or server error
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => $paymentResponse['message'] ?? 'Payment server error',
|
||||
'debug' => $paymentResponseRaw
|
||||
]);
|
||||
}
|
||||
?>
|
||||
@@ -88,6 +88,15 @@ static String get payWithSyriatelConfirm =>
|
||||
"$paymentServer/ride/syriatel/driver/confirm_payment.php";
|
||||
static String get payWithSyriatelStart =>
|
||||
"$paymentServer/ride/syriatel/driver/start_payment.php";
|
||||
|
||||
static String get createMtnInvoice => "$paymentServer/ride/mtn_new/create_mtn_invoice.php";
|
||||
static String get uploadMtnProof => "$paymentServer/ride/mtn_new/verify_payment_ai.php";
|
||||
static String get checkMtnStatus => "$paymentServer/ride/mtn_new/check_status.php";
|
||||
|
||||
static String get createCliqInvoice => "$paymentServer/ride/cliq/create_cliq_invoice.php";
|
||||
static String get uploadCliqProof => "$paymentServer/ride/cliq/verify_payment_ai.php";
|
||||
static String get checkCliqStatus => "$paymentServer/ride/cliq/check_status.php";
|
||||
|
||||
static String get payWithEcashDriver =>
|
||||
"$paymentServer/ride/ecash/driver/payWithEcash.php";
|
||||
static String get payWithEcashPassenger =>
|
||||
@@ -102,6 +111,8 @@ static String get deletePassengersWallet => "$wallet/delete.php";
|
||||
static String get updatePassengersWallet => "$wallet/update.php";
|
||||
|
||||
static String get getWalletByDriver => "$walletDriver/getWalletByDriver.php";
|
||||
static String get transferWalletDriver => "$endPoint/ride/driverWallet/transfer.php";
|
||||
static String get convertBudgetToPoints => "$walletDriver/convertBudgetToPoints.php";
|
||||
static String get driverStatistic =>
|
||||
"$endPoint/ride/driverWallet/driverStatistic.php";
|
||||
static String get getDriverDetails =>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/controller/firebase/firbase_messge.dart';
|
||||
import 'package:siro_driver/controller/firebase/local_notification.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
@@ -12,9 +10,10 @@ import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/widgets/mycircular.dart';
|
||||
|
||||
import '../../../views/widgets/mydialoug.dart';
|
||||
import '../../firebase/notification_service.dart';
|
||||
import '../../../views/home/my_wallet/payment_screen_mtn.dart';
|
||||
import '../../../views/home/my_wallet/payment_screen_cliq.dart';
|
||||
|
||||
class CaptainWalletController extends GetxController {
|
||||
bool isLoading = false;
|
||||
@@ -34,27 +33,42 @@ class CaptainWalletController extends GetxController {
|
||||
final cardBank = TextEditingController();
|
||||
final bankCode = TextEditingController();
|
||||
|
||||
double get transferFee {
|
||||
String country = box.read(BoxName.countryCode) ?? 'Jordan';
|
||||
if (country == 'Egypt') return 5.0;
|
||||
if (country == 'Syria') return 10.0;
|
||||
if (country == 'Jordan') return 0.25;
|
||||
return 5.0;
|
||||
}
|
||||
|
||||
double get minTransferAmount {
|
||||
String country = box.read(BoxName.countryCode) ?? 'Jordan';
|
||||
if (country == 'Egypt') return 10.0;
|
||||
if (country == 'Syria') return 100.0;
|
||||
if (country == 'Jordan') return 1.0;
|
||||
return 10.0;
|
||||
}
|
||||
payFromBudget() async {
|
||||
if (formKey.currentState!.validate()) {
|
||||
var pointFromBudget = int.parse((amountFromBudgetController.text));
|
||||
|
||||
// await getPaymentId('fromBudgetToPoints',
|
||||
// int.parse((amountFromBudgetController.text)) * -1);
|
||||
var paymentToken3 =
|
||||
await generateToken((pointFromBudget * -1).toString());
|
||||
var paymentID = await getPaymentId(
|
||||
'fromBudgetToPoints', (pointFromBudget * -1).toString());
|
||||
await CRUD().postWallet(link: AppLink.addDrivePayment, payload: {
|
||||
'amount': (pointFromBudget * -1).toString(),
|
||||
'rideId': paymentID.toString(),
|
||||
'payment_method': 'myBudget',
|
||||
'passengerID': 'myBudgetToPoint',
|
||||
'token': paymentToken3,
|
||||
Get.dialog(const Center(child: MyCircularProgressIndicator()),
|
||||
barrierDismissible: false);
|
||||
var res = await CRUD()
|
||||
.postWallet(link: AppLink.convertBudgetToPoints, payload: {
|
||||
'amount': pointFromBudget.toString(),
|
||||
'driverID': box.read(BoxName.driverID).toString(),
|
||||
});
|
||||
Future.delayed(const Duration(seconds: 1));
|
||||
await addDriverWallet(
|
||||
'fromBudget', pointFromBudget.toString(), pointFromBudget.toString());
|
||||
Get.back(); // close loading
|
||||
|
||||
if (res != 'failure') {
|
||||
late Map<String, dynamic> mapRes;
|
||||
if (res is String) {
|
||||
mapRes = json.decode(res);
|
||||
} else {
|
||||
mapRes = res;
|
||||
}
|
||||
|
||||
if (mapRes['status'] == 'success') {
|
||||
update();
|
||||
Get.back();
|
||||
await refreshCaptainWallet();
|
||||
@@ -64,6 +78,12 @@ class CaptainWalletController extends GetxController {
|
||||
'tone1',
|
||||
'',
|
||||
);
|
||||
} else {
|
||||
mySnackeBarError(mapRes['message']?.toString() ?? 'Error');
|
||||
}
|
||||
} else {
|
||||
mySnackeBarError('Error processing request'.tr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,55 +282,26 @@ class CaptainWalletController extends GetxController {
|
||||
}
|
||||
|
||||
Future addTransferDriversWallet(String paymentMethod1, paymentMethod2) async {
|
||||
var paymentID =
|
||||
await getPaymentId(paymentMethod1, amountFromBudgetController.text);
|
||||
paymentToken = await generateToken(
|
||||
(int.parse(amountFromBudgetController.text) * -1).toString());
|
||||
|
||||
await CRUD().postWallet(link: AppLink.addDrivePayment, payload: {
|
||||
'amount': (int.parse(amountFromBudgetController.text) * -1).toString(),
|
||||
'rideId': paymentID.toString(),
|
||||
'payment_method': paymentMethod1,
|
||||
'passengerID': 'To ${amountToNewDriverMap[0]['id']}',
|
||||
'token': paymentToken,
|
||||
Get.dialog(const Center(child: CircularProgressIndicator()),
|
||||
barrierDismissible: false);
|
||||
var res =
|
||||
await CRUD().postWallet(link: AppLink.transferWalletDriver, payload: {
|
||||
'amount': amountFromBudgetController.text,
|
||||
'receiverPhone': amountToNewDriverMap[0]['phone'].toString(),
|
||||
'country': box.read(BoxName.countryCode) ?? 'Egypt',
|
||||
'driverID': box.read(BoxName.driverID).toString(),
|
||||
});
|
||||
Get.back();
|
||||
|
||||
paymentID = await getPaymentId(paymentMethod2,
|
||||
(int.parse(amountFromBudgetController.text) - 5).toString());
|
||||
paymentToken = await generateToken(amountFromBudgetController.text);
|
||||
var res1 =
|
||||
await CRUD().postWallet(link: AppLink.addDriversWalletPoints, payload: {
|
||||
'driverID': amountToNewDriverMap[0]['id'].toString(),
|
||||
'paymentID': paymentID.toString(),
|
||||
'amount': ((int.parse(amountFromBudgetController.text) - 5))
|
||||
// kazan) // double.parse(kazan) .08 for egypt
|
||||
.toStringAsFixed(
|
||||
0), // this will convert buddget to poitns by kazan .08
|
||||
|
||||
'token': paymentToken,
|
||||
'paymentMethod': paymentMethod2.toString(),
|
||||
});
|
||||
if (res1 != 'failure') {
|
||||
// Get.find<FirebaseMessagesController>().sendNotificationToDriverMAP(
|
||||
// 'Transfer',
|
||||
// '${'You have transfer to your wallet from'.tr}'
|
||||
// '${box.read(BoxName.nameDriver)}',
|
||||
// amountToNewDriverMap[0]['token'].toString(),
|
||||
// [],
|
||||
// 'order1.wav');
|
||||
NotificationService.sendNotification(
|
||||
target: amountToNewDriverMap[0]['token'].toString(),
|
||||
title: 'Transfer'.tr,
|
||||
body: '${'You have transfer to your wallet from'.tr}'
|
||||
'${box.read(BoxName.nameDriver)}',
|
||||
|
||||
isTopic: false, // Important: this is a token
|
||||
tone: 'ding',
|
||||
driverList: [], category: 'Transfer',
|
||||
);
|
||||
await addSeferWallet('payout fee', '5');
|
||||
if (res != 'failure') {
|
||||
late Map<String, dynamic> mapRes;
|
||||
if (res is String) {
|
||||
mapRes = json.decode(res);
|
||||
} else {
|
||||
mapRes = res;
|
||||
}
|
||||
|
||||
if (mapRes['status'] == 'success') {
|
||||
Get.defaultDialog(
|
||||
title: 'transfer Successful'.tr,
|
||||
middleText: '',
|
||||
@@ -320,13 +311,16 @@ class CaptainWalletController extends GetxController {
|
||||
onPressed: () async {
|
||||
Get.back();
|
||||
Get.back();
|
||||
|
||||
await refreshCaptainWallet();
|
||||
}));
|
||||
} else {
|
||||
mySnackeBarError(mapRes['message']?.toString() ?? 'Error');
|
||||
}
|
||||
} else {
|
||||
mySnackeBarError('Error processing request'.tr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
getKazanPercent() async {
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getKazanPercent,
|
||||
@@ -354,4 +348,110 @@ class CaptainWalletController extends GetxController {
|
||||
await refreshCaptainWallet();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
Future<void> payWithMTNWallet(
|
||||
BuildContext context, String amount, String currency) async {
|
||||
try {
|
||||
final phone = phoneWallet.text.trim();
|
||||
if (phone.isEmpty) {
|
||||
Get.defaultDialog(
|
||||
title: 'Error'.tr, content: Text('Please enter phone number'.tr));
|
||||
return;
|
||||
}
|
||||
|
||||
Get.dialog(const Center(child: CircularProgressIndicator()),
|
||||
barrierDismissible: false);
|
||||
|
||||
var res = await CRUD().postWalletMtn(
|
||||
link: AppLink.createMtnInvoice,
|
||||
payload: {
|
||||
"amount": amount,
|
||||
"user_id": box.read(BoxName.driverID).toString(),
|
||||
"user_type": "driver",
|
||||
"mtn_phone": phone,
|
||||
},
|
||||
);
|
||||
|
||||
Get.back(); // close loading
|
||||
|
||||
late final Map<String, dynamic> resMap;
|
||||
if (res is Map<String, dynamic>) {
|
||||
resMap = res;
|
||||
} else if (res is String) {
|
||||
resMap = json.decode(res) as Map<String, dynamic>;
|
||||
} else {
|
||||
throw Exception("Unexpected response type");
|
||||
}
|
||||
|
||||
if (resMap['status'] == 'success') {
|
||||
Get.to(() => PaymentScreenMtn(
|
||||
invoiceNumber: resMap['invoice_number'],
|
||||
mtnNumber: resMap['mtn_payment_number'] ?? '---',
|
||||
amount: double.parse(amount),
|
||||
));
|
||||
} else {
|
||||
Get.defaultDialog(
|
||||
title: 'Error'.tr,
|
||||
content: Text(
|
||||
resMap['message']?.toString() ?? 'Failed to create invoice'.tr),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
Get.defaultDialog(title: 'Error'.tr, content: Text(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> payWithClickWallet(
|
||||
BuildContext context, String amount, String currency) async {
|
||||
try {
|
||||
final phone = phoneWallet.text.trim();
|
||||
if (phone.isEmpty) {
|
||||
Get.defaultDialog(
|
||||
title: 'Error'.tr, content: Text('Please enter phone number'.tr));
|
||||
return;
|
||||
}
|
||||
|
||||
Get.dialog(const Center(child: CircularProgressIndicator()),
|
||||
barrierDismissible: false);
|
||||
|
||||
var res = await CRUD().postWalletMtn(
|
||||
link: AppLink.createCliqInvoice,
|
||||
payload: {
|
||||
"amount": amount,
|
||||
"user_id": box.read(BoxName.driverID).toString(),
|
||||
"user_type": "driver",
|
||||
"click_phone": phone,
|
||||
},
|
||||
);
|
||||
|
||||
Get.back(); // close loading
|
||||
|
||||
late final Map<String, dynamic> resMap;
|
||||
if (res is Map<String, dynamic>) {
|
||||
resMap = res;
|
||||
} else if (res is String) {
|
||||
resMap = json.decode(res) as Map<String, dynamic>;
|
||||
} else {
|
||||
throw Exception("Unexpected response type");
|
||||
}
|
||||
|
||||
if (resMap['status'] == 'success') {
|
||||
Get.to(() => PaymentScreenCliq(
|
||||
invoiceNumber: resMap['invoice_number'],
|
||||
cliqAlias: resMap['cliq_alias'] ?? '---',
|
||||
amount: double.parse(amount),
|
||||
));
|
||||
} else {
|
||||
Get.defaultDialog(
|
||||
title: 'Error'.tr,
|
||||
content: Text(
|
||||
resMap['message']?.toString() ?? 'Failed to create invoice'.tr),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
Get.defaultDialog(title: 'Error'.tr, content: Text(e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
208
siro_driver/lib/views/home/my_wallet/payment_screen_cliq.dart
Normal file
208
siro_driver/lib/views/home/my_wallet/payment_screen_cliq.dart
Normal file
@@ -0,0 +1,208 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../controller/functions/crud.dart';
|
||||
|
||||
class PaymentScreenCliq extends StatefulWidget {
|
||||
final double amount;
|
||||
final String invoiceNumber;
|
||||
final String cliqAlias;
|
||||
|
||||
const PaymentScreenCliq({
|
||||
Key? key,
|
||||
required this.amount,
|
||||
required this.invoiceNumber,
|
||||
required this.cliqAlias,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<PaymentScreenCliq> createState() => _PaymentScreenCliqState();
|
||||
}
|
||||
|
||||
class _PaymentScreenCliqState extends State<PaymentScreenCliq> with SingleTickerProviderStateMixin {
|
||||
Timer? _pollingTimer;
|
||||
String _status = 'waiting'; // waiting, uploading, verifying, success, error
|
||||
final TextEditingController _proofController = TextEditingController();
|
||||
|
||||
late AnimationController _blinkController;
|
||||
late Animation<Color?> _colorAnimation;
|
||||
late Animation<double> _shadowAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_blinkController = AnimationController(duration: const Duration(milliseconds: 800), vsync: this)..repeat(reverse: true);
|
||||
_colorAnimation = ColorTween(begin: Colors.red.shade700, end: Colors.red.shade100).animate(_blinkController);
|
||||
_shadowAnimation = Tween<double>(begin: 2.0, end: 15.0).animate(CurvedAnimation(parent: _blinkController, curve: Curves.easeInOut));
|
||||
|
||||
_startPolling();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pollingTimer?.cancel();
|
||||
_blinkController.dispose();
|
||||
_proofController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startPolling() {
|
||||
_pollingTimer = Timer.periodic(const Duration(seconds: 5), (timer) async {
|
||||
if (_status == 'success' || _status == 'verifying') return;
|
||||
try {
|
||||
final res = await CRUD().postWallet(link: AppLink.checkCliqStatus, payload: {'invoice_number': widget.invoiceNumber});
|
||||
if (res != 'failure' && res['status'] == 'success' && res['invoice_status'] == 'completed') {
|
||||
timer.cancel();
|
||||
if (mounted) setState(() => _status = 'success');
|
||||
}
|
||||
} catch (_) {}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _submitProof() async {
|
||||
if (_proofController.text.trim().isEmpty) {
|
||||
Get.snackbar('Error'.tr, 'Please paste the transfer message'.tr, backgroundColor: Colors.red);
|
||||
return;
|
||||
}
|
||||
setState(() => _status = 'verifying');
|
||||
|
||||
try {
|
||||
final res = await CRUD().postWallet(link: AppLink.uploadCliqProof, payload: {
|
||||
'invoice_number': widget.invoiceNumber,
|
||||
'proof_text': _proofController.text.trim(),
|
||||
});
|
||||
|
||||
if (res != 'failure' && res['status'] == 'success') {
|
||||
if (mounted) setState(() => _status = 'success');
|
||||
} else {
|
||||
if (mounted) setState(() => _status = 'error');
|
||||
Get.defaultDialog(title: 'Failed'.tr, content: Text(res['message']?.toString() ?? 'Verification failed'));
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) setState(() => _status = 'error');
|
||||
Get.defaultDialog(title: 'Error'.tr, content: Text('Error uploading proof'.tr));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
appBar: AppBar(title: const Text("Cliq Payment"), centerTitle: true, backgroundColor: Colors.white, foregroundColor: Colors.black),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: _status == 'success' ? _buildSuccessUI() : _buildWaitingUI(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWaitingUI() {
|
||||
final currencyFormat = NumberFormat.decimalPattern('ar_SY');
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 15),
|
||||
decoration: BoxDecoration(gradient: LinearGradient(colors: [Colors.blue.shade800, Colors.blue.shade600]), borderRadius: BorderRadius.circular(16)),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text("المبلغ المطلوب", style: TextStyle(color: Colors.white70, fontSize: 14)),
|
||||
const SizedBox(height: 5),
|
||||
Text("${currencyFormat.format(widget.amount)} ل.س", style: const TextStyle(color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
|
||||
AnimatedBuilder(
|
||||
animation: _blinkController,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: _colorAnimation.value ?? Colors.red, width: 3.0), boxShadow: [BoxShadow(color: (_colorAnimation.value ?? Colors.red).withOpacity(0.4), blurRadius: _shadowAnimation.value, spreadRadius: 2)]),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text("يرجى تحويل المبلغ إلى الاسم المستعار التالي (Alias):", textAlign: TextAlign.center, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 15),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Clipboard.setData(ClipboardData(text: widget.cliqAlias));
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: const Text("تم نسخ الاسم ✅", textAlign: TextAlign.center), backgroundColor: Colors.green.shade600));
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(color: Colors.blue.shade50, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.blue.shade200)),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(widget.cliqAlias, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2.0)),
|
||||
const Icon(Icons.copy, color: Colors.blue),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
const Text("بعد إتمام التحويل، يرجى نسخ رسالة البنك النصية ولصقها هنا للتحقق التلقائي:", textAlign: TextAlign.center, style: TextStyle(fontSize: 14)),
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
controller: _proofController,
|
||||
maxLines: 4,
|
||||
decoration: InputDecoration(
|
||||
hintText: "قم بلصق نص رسالة التحويل هنا...",
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.blue.shade800, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))),
|
||||
onPressed: _status == 'verifying' ? null : _submitProof,
|
||||
child: _status == 'verifying'
|
||||
? const CircularProgressIndicator(color: Colors.white)
|
||||
: const Text("تحقق من الدفع", style: TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text("جاري فحص الفاتورة تلقائياً كل 5 ثوانٍ...", style: TextStyle(color: Colors.grey, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSuccessUI() {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.verified_rounded, color: Colors.green, size: 100),
|
||||
const SizedBox(height: 20),
|
||||
const Text("تم الدفع بنجاح!", style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 40),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.green, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16)),
|
||||
onPressed: () { Get.back(); Get.back(); },
|
||||
child: const Text("متابعة", style: TextStyle(fontSize: 18)),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
208
siro_driver/lib/views/home/my_wallet/payment_screen_mtn.dart
Normal file
208
siro_driver/lib/views/home/my_wallet/payment_screen_mtn.dart
Normal file
@@ -0,0 +1,208 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../controller/functions/crud.dart';
|
||||
|
||||
class PaymentScreenMtn extends StatefulWidget {
|
||||
final double amount;
|
||||
final String invoiceNumber;
|
||||
final String mtnNumber;
|
||||
|
||||
const PaymentScreenMtn({
|
||||
Key? key,
|
||||
required this.amount,
|
||||
required this.invoiceNumber,
|
||||
required this.mtnNumber,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<PaymentScreenMtn> createState() => _PaymentScreenMtnState();
|
||||
}
|
||||
|
||||
class _PaymentScreenMtnState extends State<PaymentScreenMtn> with SingleTickerProviderStateMixin {
|
||||
Timer? _pollingTimer;
|
||||
String _status = 'waiting'; // waiting, uploading, verifying, success, error
|
||||
final TextEditingController _proofController = TextEditingController();
|
||||
|
||||
late AnimationController _blinkController;
|
||||
late Animation<Color?> _colorAnimation;
|
||||
late Animation<double> _shadowAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_blinkController = AnimationController(duration: const Duration(milliseconds: 800), vsync: this)..repeat(reverse: true);
|
||||
_colorAnimation = ColorTween(begin: Colors.red.shade700, end: Colors.red.shade100).animate(_blinkController);
|
||||
_shadowAnimation = Tween<double>(begin: 2.0, end: 15.0).animate(CurvedAnimation(parent: _blinkController, curve: Curves.easeInOut));
|
||||
|
||||
_startPolling();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pollingTimer?.cancel();
|
||||
_blinkController.dispose();
|
||||
_proofController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startPolling() {
|
||||
_pollingTimer = Timer.periodic(const Duration(seconds: 5), (timer) async {
|
||||
if (_status == 'success' || _status == 'verifying') return;
|
||||
try {
|
||||
final res = await CRUD().postWallet(link: AppLink.checkMtnStatus, payload: {'invoice_number': widget.invoiceNumber});
|
||||
if (res != 'failure' && res['status'] == 'success' && res['invoice_status'] == 'completed') {
|
||||
timer.cancel();
|
||||
if (mounted) setState(() => _status = 'success');
|
||||
}
|
||||
} catch (_) {}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _submitProof() async {
|
||||
if (_proofController.text.trim().isEmpty) {
|
||||
Get.snackbar('Error'.tr, 'Please paste the transfer message'.tr, backgroundColor: Colors.red);
|
||||
return;
|
||||
}
|
||||
setState(() => _status = 'verifying');
|
||||
|
||||
try {
|
||||
final res = await CRUD().postWallet(link: AppLink.uploadMtnProof, payload: {
|
||||
'invoice_number': widget.invoiceNumber,
|
||||
'proof_text': _proofController.text.trim(),
|
||||
});
|
||||
|
||||
if (res != 'failure' && res['status'] == 'success') {
|
||||
if (mounted) setState(() => _status = 'success');
|
||||
} else {
|
||||
if (mounted) setState(() => _status = 'error');
|
||||
Get.defaultDialog(title: 'Failed'.tr, content: Text(res['message']?.toString() ?? 'Verification failed'));
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) setState(() => _status = 'error');
|
||||
Get.defaultDialog(title: 'Error'.tr, content: Text('Error uploading proof'.tr));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
appBar: AppBar(title: const Text("MTN Payment"), centerTitle: true, backgroundColor: Colors.white, foregroundColor: Colors.black),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: _status == 'success' ? _buildSuccessUI() : _buildWaitingUI(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWaitingUI() {
|
||||
final currencyFormat = NumberFormat.decimalPattern('ar_SY');
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 15),
|
||||
decoration: BoxDecoration(gradient: LinearGradient(colors: [Colors.blue.shade800, Colors.blue.shade600]), borderRadius: BorderRadius.circular(16)),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text("المبلغ المطلوب", style: TextStyle(color: Colors.white70, fontSize: 14)),
|
||||
const SizedBox(height: 5),
|
||||
Text("${currencyFormat.format(widget.amount)} ل.س", style: const TextStyle(color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
|
||||
AnimatedBuilder(
|
||||
animation: _blinkController,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: _colorAnimation.value ?? Colors.red, width: 3.0), boxShadow: [BoxShadow(color: (_colorAnimation.value ?? Colors.red).withOpacity(0.4), blurRadius: _shadowAnimation.value, spreadRadius: 2)]),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text("يرجى تحويل المبلغ إلى الرقم التالي:", textAlign: TextAlign.center, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 15),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Clipboard.setData(ClipboardData(text: widget.mtnNumber));
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: const Text("تم نسخ الرقم ✅", textAlign: TextAlign.center), backgroundColor: Colors.green.shade600));
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(color: Colors.blue.shade50, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.blue.shade200)),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(widget.mtnNumber, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2.0)),
|
||||
const Icon(Icons.copy, color: Colors.blue),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
const Text("بعد إتمام التحويل، يرجى نسخ رسالة البنك النصية ولصقها هنا للتحقق التلقائي:", textAlign: TextAlign.center, style: TextStyle(fontSize: 14)),
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
controller: _proofController,
|
||||
maxLines: 4,
|
||||
decoration: InputDecoration(
|
||||
hintText: "قم بلصق نص رسالة التحويل هنا...",
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.blue.shade800, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))),
|
||||
onPressed: _status == 'verifying' ? null : _submitProof,
|
||||
child: _status == 'verifying'
|
||||
? const CircularProgressIndicator(color: Colors.white)
|
||||
: const Text("تحقق من الدفع", style: TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text("جاري فحص الفاتورة تلقائياً كل 5 ثوانٍ...", style: TextStyle(color: Colors.grey, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSuccessUI() {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.verified_rounded, color: Colors.green, size: 100),
|
||||
const SizedBox(height: 20),
|
||||
const Text("تم الدفع بنجاح!", style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 40),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.green, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16)),
|
||||
onPressed: () { Get.back(); Get.back(); },
|
||||
child: const Text("متابعة", style: TextStyle(fontSize: 18)),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'package:siro_driver/views/widgets/mycircular.dart';
|
||||
import 'package:siro_driver/views/widgets/mydialoug.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/currency.dart';
|
||||
|
||||
import '../../../controller/home/payment/captain_wallet_controller.dart';
|
||||
|
||||
@@ -93,7 +94,19 @@ class TransferBudgetPage extends StatelessWidget {
|
||||
width: double.maxFinite,
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
child: Text(
|
||||
"${"amount".tr} ${captainWalletController.amountFromBudgetController.text} ${'LE'.tr}",
|
||||
"${"amount".tr} ${captainWalletController.amountFromBudgetController.text} ${CurrencyHelper.currency}",
|
||||
style: AppStyle.title,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
Container(
|
||||
width: double.maxFinite,
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
child: Text(
|
||||
"${"Transfer Fee".tr}: ${captainWalletController.transferFee} ${CurrencyHelper.currency}",
|
||||
style: AppStyle.title,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -106,27 +119,29 @@ class TransferBudgetPage extends StatelessWidget {
|
||||
? MyElevatedButton(
|
||||
title: 'Transfer'.tr,
|
||||
onPressed: () async {
|
||||
if (double.parse(
|
||||
captainWalletController
|
||||
.amountFromBudgetController
|
||||
.text) <
|
||||
double.parse(
|
||||
captainWalletController
|
||||
.totalAmountVisa) -
|
||||
5) {
|
||||
double amount = double.tryParse(captainWalletController.amountFromBudgetController.text) ?? 0.0;
|
||||
double totalAmount = double.tryParse(captainWalletController.totalAmountVisa) ?? 0.0;
|
||||
double fee = captainWalletController.transferFee;
|
||||
double minTransfer = captainWalletController.minTransferAmount;
|
||||
|
||||
if (amount < minTransfer) {
|
||||
MyDialog().getDialog(
|
||||
"Error".tr,
|
||||
"${"Minimum transfer amount is".tr} $minTransfer ${CurrencyHelper.currency}", () {
|
||||
Get.back();
|
||||
});
|
||||
} else if (amount > (totalAmount - fee)) {
|
||||
MyDialog().getDialog(
|
||||
"Insufficient Balance".tr,
|
||||
"${"You must leave at least".tr} $fee ${CurrencyHelper.currency} ${"for transfer fees".tr}", () {
|
||||
Get.back();
|
||||
});
|
||||
} else {
|
||||
await captainWalletController
|
||||
.addTransferDriversWallet(
|
||||
'TransferFrom',
|
||||
'TransferTo',
|
||||
);
|
||||
} else {
|
||||
MyDialog().getDialog(
|
||||
"You dont have money in your Wallet"
|
||||
.tr,
|
||||
"You dont have money in your Wallet or you should less transfer 5 LE to activate"
|
||||
.tr, () {
|
||||
Get.back();
|
||||
});
|
||||
}
|
||||
})
|
||||
: const SizedBox()
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
include '../../connect.php';
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 0);
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$driverID = filterRequest('driverID');
|
||||
$amount = floatval(filterRequest('amount'));
|
||||
|
||||
if (empty($driverID) || empty($amount) || $amount <= 0) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Missing required fields or invalid amount']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$con->beginTransaction();
|
||||
|
||||
// 1. Fetch current budget
|
||||
$stmt = $con->prepare("SELECT SUM(amount) as diff FROM payments WHERE captain_id = :driverID FOR UPDATE");
|
||||
$stmt->execute([':driverID' => $driverID]);
|
||||
$sumRow = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$totalBudget = floatval($sumRow['diff']);
|
||||
|
||||
if ($totalBudget < $amount) {
|
||||
$con->rollBack();
|
||||
echo json_encode(['status' => 'error', 'message' => 'Insufficient budget']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2. Generate unique tokens
|
||||
$paymentID1 = "budget2pt_" . time() . rand(1000, 9999);
|
||||
$paymentID2 = "pt2budget_" . time() . rand(1000, 9999);
|
||||
$token1 = md5(uniqid("b1", true));
|
||||
$token2 = md5(uniqid("b2", true));
|
||||
|
||||
// 3. Deduct from budget (payments)
|
||||
$deductAmount = -$amount;
|
||||
$stmt = $con->prepare("INSERT INTO payments (captain_id, amount, rideId, payment_method, passengerID, token)
|
||||
VALUES (:driverID, :amount, :rideId, 'myBudget', 'myBudgetToPoint', :token)");
|
||||
$stmt->execute([
|
||||
':driverID' => $driverID,
|
||||
':amount' => $deductAmount,
|
||||
':rideId' => $paymentID1,
|
||||
':token' => $token1
|
||||
]);
|
||||
|
||||
// 4. Add to points (paymentsDriverPoints)
|
||||
$stmt = $con->prepare("INSERT INTO paymentsDriverPoints (captain_id, paymentID, amount, token, paymentMethod)
|
||||
VALUES (:driverID, :paymentID, :amount, :token, 'fromBudget')");
|
||||
$stmt->execute([
|
||||
':driverID' => $driverID,
|
||||
':paymentID' => $paymentID2,
|
||||
':amount' => $amount,
|
||||
':token' => $token2
|
||||
]);
|
||||
|
||||
// Commit Transaction
|
||||
$con->commit();
|
||||
|
||||
echo json_encode(['status' => 'success', 'message' => 'Budget converted to points successfully']);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$con->rollBack();
|
||||
echo json_encode(['status' => 'error', 'message' => 'Database transaction failed: ' . $e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
129
walletintaleq.intaleq.xyz/v2/main/ride/driverWallet/transfer.php
Normal file
129
walletintaleq.intaleq.xyz/v2/main/ride/driverWallet/transfer.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
include '../../jwtconnect.php';
|
||||
|
||||
// Disable error reporting output for production API
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 0);
|
||||
|
||||
// Set header
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$senderID = filterRequest('senderID');
|
||||
$receiverID = filterRequest('receiverID'); // Now receiving the ID directly from Main Server
|
||||
$amount = floatval(filterRequest('amount'));
|
||||
$country = filterRequest('country'); // e.g. Egypt, Syria, Jordan
|
||||
|
||||
if (empty($senderID) || empty($receiverID) || empty($amount) || empty($country)) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Missing required fields']);
|
||||
exit;
|
||||
}
|
||||
// --- Payment Key Authentication ---
|
||||
$expectedKey = getenv('PAYMENT_KEY');
|
||||
$providedKey = $_SERVER['HTTP_PAYMENT_KEY'] ?? '';
|
||||
|
||||
if (empty($expectedKey) || $providedKey !== $expectedKey) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Unauthorized Payment Server Access (Invalid Key)']);
|
||||
exit;
|
||||
}
|
||||
// 1. Determine Fee based on Country
|
||||
$fee = 0;
|
||||
if (strtolower($country) === 'egypt') {
|
||||
$fee = 5;
|
||||
if ($amount < 10) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Minimum transfer amount in Egypt is 10']);
|
||||
exit;
|
||||
}
|
||||
} elseif (strtolower($country) === 'syria') {
|
||||
$fee = 10;
|
||||
if ($amount < 100) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Minimum transfer amount in Syria is 100']);
|
||||
exit;
|
||||
}
|
||||
} elseif (strtolower($country) === 'jordan') {
|
||||
$fee = 0.25;
|
||||
if ($amount < 1) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Minimum transfer amount in Jordan is 1']);
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
// Default fee if unknown
|
||||
$fee = 5;
|
||||
}
|
||||
|
||||
try {
|
||||
$con->beginTransaction();
|
||||
|
||||
if ($receiverID == $senderID) {
|
||||
$con->rollBack();
|
||||
echo json_encode(['status' => 'error', 'message' => 'Cannot transfer to yourself']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2. Fetch Sender Budget (with FOR UPDATE to lock rows)
|
||||
$stmt = $con->prepare("SELECT SUM(amount) as diff FROM payments WHERE captain_id = :senderID FOR UPDATE");
|
||||
$stmt->execute([':senderID' => $senderID]);
|
||||
$sumRow = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$totalBudget = floatval($sumRow['diff']);
|
||||
|
||||
if ($totalBudget < $amount) {
|
||||
$con->rollBack();
|
||||
echo json_encode(['status' => 'error', 'message' => 'Insufficient budget']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$amountForReceiver = $amount - $fee;
|
||||
if ($amountForReceiver <= 0) {
|
||||
$con->rollBack();
|
||||
echo json_encode(['status' => 'error', 'message' => 'Transfer amount must be greater than the fee']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 3. Generate unique Tokens and paymentIDs
|
||||
$paymentID1 = "transfer_" . time() . rand(1000, 9999);
|
||||
$paymentID2 = "transfer_recv_" . time() . rand(1000, 9999);
|
||||
$token1 = md5(uniqid("tk1", true));
|
||||
$token2 = md5(uniqid("tk2", true));
|
||||
$seferToken = md5(uniqid("sfr", true));
|
||||
|
||||
// 4. Deduct from Sender (payments table)
|
||||
$deductAmount = -$amount;
|
||||
$stmt = $con->prepare("INSERT INTO payments (captain_id, amount, rideId, payment_method, passengerID, token)
|
||||
VALUES (:senderID, :amount, :rideId, 'cash_transfer', :receiverRef, :token)");
|
||||
$stmt->execute([
|
||||
':senderID' => $senderID,
|
||||
':amount' => $deductAmount,
|
||||
':rideId' => $paymentID1,
|
||||
':receiverRef' => 'To ' . $receiverID,
|
||||
':token' => $token1
|
||||
]);
|
||||
|
||||
// 5. Add to Receiver Points (paymentsDriverPoints table)
|
||||
$stmt = $con->prepare("INSERT INTO paymentsDriverPoints (captain_id, paymentID, amount, token, paymentMethod)
|
||||
VALUES (:receiverID, :paymentID, :amount, :token, 'Transfer')");
|
||||
$stmt->execute([
|
||||
':receiverID' => $receiverID,
|
||||
':paymentID' => $paymentID2,
|
||||
':amount' => $amountForReceiver,
|
||||
':token' => $token2
|
||||
]);
|
||||
|
||||
// 6. Add Fee to Sefer Wallet
|
||||
$stmt = $con->prepare("INSERT INTO seferWallet (amount, paymentMethod, passengerId, token, driverId)
|
||||
VALUES (:fee, 'payout fee', 'driver', :token, :senderID)");
|
||||
$stmt->execute([
|
||||
':fee' => $fee,
|
||||
':token' => $seferToken,
|
||||
':senderID' => $senderID
|
||||
]);
|
||||
|
||||
// Commit Transaction
|
||||
$con->commit();
|
||||
|
||||
echo json_encode(['status' => 'success', 'message' => 'Transfer completed successfully on payment server']);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$con->rollBack();
|
||||
echo json_encode(['status' => 'error', 'message' => 'Database transaction failed: ' . $e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
Reference in New Issue
Block a user