diff --git a/system/plugin/.DS_Store b/system/plugin/.DS_Store
new file mode 100644
index 0000000..6662656
Binary files /dev/null and b/system/plugin/.DS_Store differ
diff --git a/system/plugin/.gitattributes b/system/plugin/.gitattributes
new file mode 100644
index 0000000..dfe0770
--- /dev/null
+++ b/system/plugin/.gitattributes
@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto
diff --git a/system/plugin/CreateHotspotUser.php b/system/plugin/CreateHotspotUser.php
new file mode 100644
index 0000000..37828c0
--- /dev/null
+++ b/system/plugin/CreateHotspotUser.php
@@ -0,0 +1,420 @@
+ 'error', 'code' => 400, 'message' => 'The parameter is not present in the URL.']);
+ }
+ }
+ }
+}
+
+function ReconnectVoucher() {
+ header('Content-Type: application/json');
+
+ $rawData = file_get_contents('php://input');
+ $postData = json_decode($rawData, true);
+
+ if (!isset($postData['voucher_code'], $postData['account_id'])) {
+ echo json_encode(['status' => 'error', 'code' => 400, 'message' => 'Missing accountId or voucherCode field']);
+ return;
+ }
+
+ $accountId = $postData['account_id'];
+ $voucherCode = $postData['voucher_code'];
+
+ $voucher = ORM::for_table('tbl_voucher')
+ ->where('code', $voucherCode)
+ ->where('status', '0')
+ ->find_one();
+
+ if (!$voucher) {
+ echo json_encode([
+ 'status' => 'error',
+ 'Resultcode' => '1',
+ 'voucher' => 'Not Found',
+ 'message' => 'Invalid Voucher code'
+ ]);
+ exit();
+ }
+
+ if ($voucher['status'] == '1') {
+ echo json_encode([
+ 'status' => 'error',
+ 'Resultcode' => '3',
+ 'voucher' => 'Used',
+ 'message' => 'Voucher code is already used'
+ ]);
+ exit();
+ }
+
+ $planId = $voucher['id_plan'];
+ $routername = $voucher['routers'];
+
+ $router = ORM::for_table('tbl_routers')
+ ->where('name', $routername)
+ ->find_one();
+
+ if (!$router) {
+ echo json_encode([
+ 'status' => 'error',
+ 'message' => 'Router not found'
+ ]);
+ exit();
+ }
+
+ $routerId = $router['id'];
+
+ if (!ORM::for_table('tbl_plans')->where('id', $planId)->count() || !ORM::for_table('tbl_routers')->where('id', $routerId)->count()) {
+ echo json_encode([
+ 'status' => 'error',
+ 'message' => 'Unable to process your request, please refresh the page'
+ ]);
+ exit();
+ }
+
+ $user = ORM::for_table('tbl_customers')->where('username', $accountId)->find_one();
+ if (!$user) {
+ // Create a new user if not exists
+ $user = ORM::for_table('tbl_customers')->create();
+ $user->username = $accountId;
+ $user->password = '1234';
+ $user->fullname = $accountId;
+ $user->email = $accountId . '@gmail.com';
+ $user->phonenumber = $accountId;
+ $user->pppoe_password = '1234';
+ $user->address = '';
+ $user->service_type = 'Hotspot';
+ }
+
+ $user->router_id = $routerId;
+ $user->save();
+
+ // Update the voucher with the user ID
+ $voucher->user = $user->id;
+ $voucher->status = '1'; // Mark as used
+ $voucher->save();
+
+ if (Package::rechargeUser($user->id, $routername, $planId, 'Voucher', $voucherCode)) {
+ echo json_encode([
+ 'status' => 'success',
+ 'Resultcode' => '2',
+ 'voucher' => 'activated',
+ 'message' => 'Voucher code has been activated',
+ 'username' => $user->username
+ ]);
+ } else {
+ echo json_encode([
+ 'status' => 'error',
+ 'message' => 'Failed to recharge user package'
+ ]);
+ }
+}
+
+function ReconnectUser()
+{
+ header('Content-Type: application/json');
+ $rawData = file_get_contents('php://input');
+ $postData = json_decode($rawData, true);
+ if (!$postData) {
+ echo json_encode(['status' => 'error', 'code' => 400, 'message' => 'Invalid JSON DATA']);
+ exit();
+ }
+
+ if (!isset($postData['mpesa_code'])) {
+ echo json_encode(['status' => 'error', 'code' => 400, 'message' => 'missing required fields']);
+ exit();
+ }
+
+ $mpesaCode = $postData['mpesa_code'];
+
+ // Query the payment gateway table
+ $payment = ORM::for_table('tbl_payment_gateway')
+ ->where('gateway_trx_id', $mpesaCode)
+ ->find_one();
+
+ if (!$payment) {
+ $data = array(['status' => 'error', "Resultcode" => "1", 'user' => "Not Found", 'message' => 'Invalid Mpesa Transaction code']);
+ echo json_encode($data);
+ exit();
+ }
+
+ $username = $payment['username'];
+
+ // Query the user recharges table
+ $recharge = ORM::for_table('tbl_user_recharges')
+ ->where('username', $username)
+ ->order_by_desc('id')
+ ->find_one();
+
+ if ($recharge) {
+ $status = $recharge['status'];
+ if ($status == 'on') {
+ $data = array(
+ "Resultcode" => "2",
+ "user" => "Active User",
+ "username" => $username,
+ "tyhK" => "1234", // Replace with the actual password or token
+ "Message" => "We have verified your transaction under the Mpesa Transaction $mpesaCode. Please don't leave this page as we are redirecting you.",
+ "Status" => "success"
+ );
+ } elseif ($status == "off") {
+ $data = array(
+ "Resultcode" => "3",
+ "user" => "Expired User",
+ "Message" => "We have verified your transaction under the Mpesa Transaction $mpesaCode. But your Package is already Expired. Please buy a new Package.",
+ "Status" => "danger"
+ );
+ } else {
+ $data = array(
+ "Message" => "Unexpected status value",
+ "Status" => "error"
+ );
+ }
+ } else {
+ $data = array(
+ "Message" => "Recharge information not found",
+ "Status" => "error"
+ );
+ }
+
+ echo json_encode($data);
+ exit();
+}
+
+
+function VerifyHotspot() {
+ header('Content-Type: application/json');
+ $rawData = file_get_contents('php://input');
+ $postData = json_decode($rawData, true);
+
+ if (!$postData) {
+ echo json_encode(['Resultcode' => 'error', 'Message' => 'Invalid JSON data']);
+ return;
+ }
+
+ if (!isset($postData['account_id'])) {
+ echo json_encode(['Resultcode' => 'error', 'Message' => 'Missing required fields']);
+ return;
+ }
+
+ $accountId = $postData['account_id'];
+ $user = ORM::for_table('tbl_payment_gateway')
+ ->where('username', $accountId)
+ ->order_by_desc('id')
+ ->find_one();
+
+ if ($user) {
+ $status = $user->status;
+ $mpesacode = $user->gateway_trx_id;
+ $res = $user->pg_paid_response;
+
+ if ($status == 2 && !empty($mpesacode)) {
+ echo json_encode([
+ "Resultcode" => "3",
+ "Message" => "We have received your transaction under the Mpesa Transaction $mpesacode. Please do not leave this page as we are redirecting you.",
+ "Status" => "success"
+ ]);
+ } elseif ($res == "Not enough balance") {
+ echo json_encode([
+ "Resultcode" => "2",
+ "Message" => "Insufficient Balance for the transaction",
+ "Status" => "danger"
+ ]);
+ } elseif ($res == "Wrong Mpesa pin") {
+ echo json_encode([
+ "Resultcode" => "2",
+ "Message" => "You entered Wrong Mpesa pin, please resubmit",
+ "Status" => "danger"
+ ]);
+ } elseif ($status == 4) {
+ echo json_encode([
+ "Resultcode" => "2",
+ "Message" => "You cancelled the transaction, you can enter phone number again to activate",
+ "Status" => "info"
+ ]);
+ } elseif (empty($mpesacode)) {
+ echo json_encode([
+ "Resultcode" => "1",
+ "Message" => "A payment pop up has been sent to your phone. Please enter PIN to continue (Please do not leave or reload the page until redirected).",
+ "Status" => "primary"
+ ]);
+ }
+ } else {
+ echo json_encode([
+ "Resultcode" => "error",
+ "Message" => "User not found"
+ ]);
+ }
+}
+
+
+
+function CreateHostspotUser()
+{
+ header('Content-Type: application/json');
+ $rawData = file_get_contents('php://input');
+ $postData = json_decode($rawData, true);
+ if (!$postData) {
+ echo json_encode(['status' => 'error', 'code' => 400, 'message' => 'Invalid JSON DATA' . $postData . ' n tes ']);
+ } else {
+ $phone = $postData['phone_number'];
+ $planId = $postData['plan_id'];
+ $routerId = $postData['router_id'];
+ $accountId = $postData['account_id'];
+
+
+
+ if (!isset( $postData['phone_number'], $postData['plan_id'], $postData['router_id'], $postData['account_id'])) {
+ echo json_encode(['status' => 'error', 'code' => 400, 'message' => 'missing required fields' . $postData, 'phone' => $phone, 'planId' => $planId, 'routerId' => $routerId, 'accountId' => $accountId]);
+ } else {
+ $phone = (substr($phone, 0, 1) == '+') ? str_replace('+', '', $phone) : $phone;
+ $phone = (substr($phone, 0, 1) == '0') ? preg_replace('/^0/', '254', $phone) : $phone;
+ $phone = (substr($phone, 0, 1) == '7') ? preg_replace('/^7/', '2547', $phone) : $phone; //cater for phone number prefix 2547XXXX
+ $phone = (substr($phone, 0, 1) == '1') ? preg_replace('/^1/', '2541', $phone) : $phone; //cater for phone number prefix 2541XXXX
+ $phone = (substr($phone, 0, 1) == '0') ? preg_replace('/^01/', '2541', $phone) : $phone;
+ $phone = (substr($phone, 0, 1) == '0') ? preg_replace('/^07/', '2547', $phone) : $phone;
+ if (strlen($phone) !== 12) {
+ echo json_encode(['status' => 'error', 'code' => 1, 'message' => 'Phone number ' . $phone . ' is invalid. Please confirm.']);
+ }
+ if (strlen($phone) == 12 && !empty($planId) && !empty($routerId)) {
+ $PlanExist = ORM::for_table('tbl_plans')->where('id', $planId)->count() > 0;
+ $RouterExist = ORM::for_table('tbl_routers')->where('id', $routerId)->count() > 0;
+ if (!$PlanExist || !$RouterExist)
+ echo json_encode(["status" => "error", "message" => "Unable to process your request, please refresh the page."]);
+ }
+ $Userexist = ORM::for_table('tbl_customers')->where('username', $accountId)->find_one();
+ if ($Userexist) {
+ $Userexist->router_id = $routerId;
+ $Userexist->save();
+ InitiateStkpush($phone, $planId, $accountId, $routerId);
+ } else {
+ try {
+ $defpass = '1234';
+ $defaddr = 'netXtreme';
+ $defmail = $phone . '@gmail.com';
+ $createUser = ORM::for_table('tbl_customers')->create();
+ $createUser->username = $accountId;
+ $createUser->password = $defpass;
+ $createUser->fullname = $phone;
+ $createUser->router_id = $routerId;
+ $createUser->phonenumber = $phone;
+ $createUser->pppoe_password = $defpass;
+ $createUser->address = $defaddr;
+ $createUser->email = $defmail;
+ $createUser->service_type = 'Hotspot';
+ if ($createUser->save()) {
+ InitiateStkpush($phone, $planId, $accountId, $routerId);
+ } else {
+ echo json_encode(["status" => "error", "message" => "There was a system error when registering user, please contact support."]);
+ }
+ } catch (Exception $e) {
+ echo json_encode(["status" => "error", "message" => "Error creating user: " . $e->getMessage()]);
+ }
+ }
+ }
+ }
+}
+
+
+function InitiateStkpush($phone, $planId, $accountId, $routerId)
+{
+ $gateway = ORM::for_table('tbl_appconfig')
+ ->where('setting', 'payment_gateway')
+ ->find_one();
+ $gateway = ($gateway) ? $gateway->value : null;
+ if ($gateway == "MpesatillStk") {
+ $url = U . "plugin/initiatetillstk";
+ } elseif ($gateway == "BankStkPush") {
+ $url = U . "plugin/initiatebankstk";
+ } elseif ($gateway == "mpesa") {
+ $url = U . "plugin/initiatempesa";
+ } else {
+ $url = null; // or handle the default case appropriately
+ }
+ $Planname = ORM::for_table('tbl_plans')
+ ->where('id', $planId)
+ ->order_by_desc('id')
+ ->find_one();
+ $Findrouter = ORM::for_table('tbl_routers')
+ ->where('id', $routerId)
+ ->order_by_desc('id')
+ ->find_one();
+ $rname = $Findrouter->name;
+ $price = $Planname->price;
+ $Planname = $Planname->name_plan;
+ $Checkorders = ORM::for_table('tbl_payment_gateway')
+ ->where('username', $accountId)
+ ->where('status', 1)
+ ->order_by_desc('id')
+ ->find_many();
+ if ($Checkorders) {
+ foreach ($Checkorders as $Dorder) {
+ $Dorder->delete();
+ }
+ }
+ try {
+ $d = ORM::for_table('tbl_payment_gateway')->create();
+ $d->username = $accountId;
+ $d->gateway = $gateway;
+ $d->plan_id = $planId;
+ $d->plan_name = $Planname;
+ $d->routers_id = $routerId;
+ $d->routers = $rname;
+ $d->price = $price;
+ $d->payment_method = $gateway;
+ $d->payment_channel = $gateway;
+ $d->created_date = date('Y-m-d H:i:s');
+ $d->paid_date = date('Y-m-d H:i:s');
+ $d->expired_date = date('Y-m-d H:i:s');
+ $d->pg_url_payment = $url;
+ $d->status = 1;
+ $d->save();
+ } catch (Exception $e) {
+ error_log('Error saving payment gateway record: ' . $e->getMessage());
+ throw $e;
+ }
+ SendSTKcred($phone, $url, $accountId);
+}
+
+function SendSTKcred($phone, $url, $accountId )
+{
+ $link = $url;
+ $fields = array(
+ 'username' => $accountId,
+ 'phone' => $phone,
+ 'channel' => 'Yes',
+ );
+ $postvars = http_build_query($fields);
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $link);
+ curl_setopt($ch, CURLOPT_POST, count($fields));
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $postvars);
+ $result = curl_exec($ch);
+}
+
+Alloworigins();
diff --git a/system/plugin/c2b.php b/system/plugin/c2b.php
new file mode 100644
index 0000000..1bcaf62
--- /dev/null
+++ b/system/plugin/c2b.php
@@ -0,0 +1,636 @@
+exec($tableCheckQuery);
+} catch (PDOException $e) {
+ echo "Error creating the table: " . $e->getMessage();
+} catch (Exception $e) {
+ echo "An unexpected error occurred: " . $e->getMessage();
+}
+
+function c2b_overview()
+{
+ global $ui, $config;
+ _admin();
+ $ui->assign('_title', 'Mpesa C2B Payment Overview');
+ $ui->assign('_system_menu', '');
+ $admin = Admin::_info();
+ $ui->assign('_admin', $admin);
+
+ // Check user type for access
+ if (!in_array($admin['user_type'], ['SuperAdmin', 'Admin', 'Sales'])) {
+ _alert(Lang::T('You do not have permission to access this page'), 'danger', "dashboard");
+ exit;
+ }
+
+ $query = ORM::for_table('tbl_mpesa_transactions')->order_by_desc('TransTime');
+ $payments = $query->find_many();
+
+ if (
+ (empty($config['mpesa_c2b_consumer_key']) || empty($config['mpesa_c2b_consumer_secret']) || empty($config['mpesa_c2b_business_code']))
+ && !$config['c2b_registered']
+ ) {
+ $ui->assign('message', '' . Lang::T("You haven't registered your validation and verification URLs. Please register URLs by clicking ") . ' Register URL ' . '');
+ }
+ $ui->assign('payments', $payments);
+ $ui->assign('xheader', '');
+ $ui->display('c2b_overview.tpl');
+}
+
+function c2b_settings()
+{
+ global $ui, $admin, $config;
+ $ui->assign('_title', Lang::T("Mpesa C2B Settings [Offline Payment]"));
+ $ui->assign('_system_menu', 'settings');
+ $admin = Admin::_info();
+ $ui->assign('_admin', $admin);
+
+ if (!in_array($admin['user_type'], ['SuperAdmin', 'Admin'])) {
+ _alert(Lang::T('You do not have permission to access this page'), 'danger', "dashboard");
+ }
+
+ if (_post('save') == 'save') {
+ $mpesa_c2b_consumer_key = _post('mpesa_c2b_consumer_key');
+ $mpesa_c2b_consumer_secret = _post('mpesa_c2b_consumer_secret');
+ $mpesa_c2b_business_code = _post('mpesa_c2b_business_code');
+ $mpesa_c2b_env = _post('mpesa_c2b_env');
+ $mpesa_c2b_api = _post('mpesa_c2b_api');
+ $mpesa_c2b_low_fee = _post('mpesa_c2b_low_fee') ? 1 : 0;
+ $mpesa_c2b_bill_ref = _post('mpesa_c2b_bill_ref');
+
+ $errors = [];
+ if (empty($mpesa_c2b_consumer_key)) {
+ $errors[] = Lang::T('Mpesa C2B Consumer Key is required.');
+ }
+ if (empty($mpesa_c2b_consumer_secret)) {
+ $errors[] = Lang::T('Mpesa C2B Consumer Secret is required.');
+ }
+ if (empty($mpesa_c2b_business_code)) {
+ $errors[] = Lang::T('Mpesa C2B Business Code is required.');
+ }
+ if (empty($mpesa_c2b_env)) {
+ $errors[] = Lang::T('Mpesa C2B Environment is required.');
+ }
+ if (empty($mpesa_c2b_api)) {
+ $errors[] = Lang::T('Mpesa C2B API URL is required.');
+ }
+
+ if (empty($mpesa_c2b_bill_ref)) {
+ $errors[] = Lang::T('Mpesa Bill Ref Number Type is required.');
+ }
+
+ if (!empty($errors)) {
+ $ui->assign('message', implode('
', $errors));
+ $ui->display('c2b_settings.tpl');
+ return;
+ }
+
+ $settings = [
+ 'mpesa_c2b_consumer_key' => $mpesa_c2b_consumer_key,
+ 'mpesa_c2b_consumer_secret' => $mpesa_c2b_consumer_secret,
+ 'mpesa_c2b_business_code' => $mpesa_c2b_business_code,
+ 'mpesa_c2b_env' => $mpesa_c2b_env,
+ 'mpesa_c2b_api' => $mpesa_c2b_api,
+ 'mpesa_c2b_low_fee' => $mpesa_c2b_low_fee,
+ 'mpesa_c2b_bill_ref' => $mpesa_c2b_bill_ref,
+ ];
+
+ // Update or insert settings in the database
+ foreach ($settings as $key => $value) {
+ $d = ORM::for_table('tbl_appconfig')->where('setting', $key)->find_one();
+ if ($d) {
+ $d->value = $value;
+ $d->save();
+ } else {
+ $d = ORM::for_table('tbl_appconfig')->create();
+ $d->setting = $key;
+ $d->value = $value;
+ $d->save();
+ }
+ }
+
+ if ($admin) {
+ _log('[' . $admin['username'] . ']: ' . Lang::T('Settings Saved Successfully'));
+ }
+ r2(U . 'plugin/c2b_settings', 's', Lang::T('Settings Saved Successfully'));
+ }
+
+ if (!empty($config['mpesa_c2b_consumer_key'] && $config['mpesa_c2b_consumer_secret'] && $config['mpesa_c2b_business_code']) && !$config['c2b_registered']) {
+ $ui->assign('message', '' . Lang::T("You haven't registered your validation and verification URLs, Please register URLs by clicking ") . ' Register URL ' . '');
+ }
+ $ui->assign('_c', $config);
+ $ui->assign('companyName', $config['CompanyName']);
+ $ui->display('c2b_settings.tpl');
+}
+
+function c2b_generateAccessToken()
+{
+ global $config;
+ $mpesa_c2b_env = $config['mpesa_c2b_env'] ?? null;
+ $mpesa_c2b_consumer_key = $config['mpesa_c2b_consumer_key'] ?? null;
+ $mpesa_c2b_consumer_secret = $config['mpesa_c2b_consumer_secret'] ?? null;
+ $access_token_url = match ($mpesa_c2b_env) {
+ "live" => 'https://api.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials',
+ "sandbox" => 'https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials',
+ };
+ $headers = ['Content-Type:application/json; charset=utf8'];
+ $curl = curl_init($access_token_url);
+ curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
+ curl_setopt($curl, CURLOPT_HEADER, FALSE);
+ curl_setopt($curl, CURLOPT_USERPWD, "$mpesa_c2b_consumer_key:$mpesa_c2b_consumer_secret");
+ $result = curl_exec($curl);
+ $result = json_decode($result);
+ if (isset($result->access_token)) {
+ return $result->access_token;
+ } else {
+ return null;
+ }
+}
+
+function c2b_registerUrl()
+{
+ global $config;
+ if (
+ (empty($config['mpesa_c2b_consumer_key']) || empty($config['mpesa_c2b_consumer_secret']) || empty($config['mpesa_c2b_business_code']))
+ && !$config['c2b_registered']
+ ) {
+ r2(U . 'plugin/c2b_settings', 'e', Lang::T('Please setup your M-Pesa C2B settings first'));
+ exit;
+ }
+ $access_token = c2b_generateAccessToken();
+ switch ($access_token) {
+ case null:
+ r2(U . 'plugin/c2b_settings', 'e', Lang::T('Failed to generate access token'));
+ exit;
+ default:
+ $BusinessShortCode = $config['mpesa_c2b_business_code'] ?? null;
+ $mpesa_c2b_env = $config['mpesa_c2b_env'] ?? null;
+ $confirmationUrl = U . 'plugin/c2b_confirmation';
+ $validationUrl = U . 'plugin/c2b_validation';
+ $mpesa_c2b_api = $config['mpesa_c2b_api'] ?? null;
+ $registerurl = match ($mpesa_c2b_env) {
+ "live" => match ($mpesa_c2b_api) {
+ "v1" => 'https://api.safaricom.co.ke/mpesa/c2b/v1/registerurl',
+ "v2" => 'https://api.safaricom.co.ke/mpesa/c2b/v2/registerurl',
+ },
+ "sandbox" => 'https://sandbox.safaricom.co.ke/mpesa/c2b/v1/registerurl',
+ };
+ $curl = curl_init();
+ curl_setopt($curl, CURLOPT_URL, $registerurl);
+ curl_setopt($curl, CURLOPT_HTTPHEADER, [
+ 'Content-Type:application/json',
+ "Authorization:Bearer $access_token"
+ ]);
+ $data = [
+ 'ShortCode' => $BusinessShortCode,
+ 'ResponseType' => 'Completed',
+ 'ConfirmationURL' => $confirmationUrl,
+ 'ValidationURL' => $validationUrl
+ ];
+ $data_string = json_encode($data);
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($curl, CURLOPT_POST, true);
+ curl_setopt($curl, CURLOPT_POSTFIELDS, $data_string);
+ $curl_response = curl_exec($curl);
+ $data = json_decode($curl_response);
+ if (isset($data->ResponseCode) && $data->ResponseCode == 0) {
+ try {
+ $d = ORM::for_table('tbl_appconfig')->create();
+ $d->setting = 'c2b_registered';
+ $d->value = '1';
+ $d->save();
+ } catch (Exception $e) {
+ _log("Failed to save M-Pesa C2B URL to database.\n\n" . $e->getMessage());
+ sendTelegram("Failed to save M-Pesa C2B URL to database.\n\n" . $e->getMessage());
+ }
+ sendTelegram("M-Pesa C2B URL registered successfully");
+ r2(U . 'plugin/c2b_settings', 's', "M-Pesa C2B URL registered successfully");
+ } else {
+ $errorMessage = $data->errorMessage;
+ sendTelegram("Resister M-Pesa C2B URL Failed\n\n" . json_encode($curl_response, JSON_PRETTY_PRINT));
+ r2(U . 'plugin/c2b_settings', 'e', "Failed to register M-Pesa C2B URL Error $errorMessage");
+ }
+ break;
+ }
+}
+
+
+function c2b_webhook_log($data)
+{
+ $logFile = 'pages/mpesa-webhook.html';
+ $logEntry = date('Y-m-d H:i:s') . "
" . htmlspecialchars($data, ENT_QUOTES, 'UTF-8') . "\n"; + + if (file_put_contents($logFile, $logEntry, FILE_APPEND) === false) { + sendTelegram("Failed to write to log file: $logFile"); + } +} + +function c2b_isValidSafaricomIP($ip) +{ + $config = c2b_config(); + $safaricomIPs = [ + '196.201.214.0/24', + '196.201.213.0/24', + '196.201.212.0/24', + '172.69.79.0/24', + '172.69.0.0/24', + '0.0.0.0/0', + ]; + if ($config['mpesa_c2b_env'] == 'sandbox') { + $safaricomIPs[] = '::1'; + } + + foreach ($safaricomIPs as $range) { + if (c2b_ipInRange($ip, $range)) { + return true; + } + } + + return false; +} + +function c2b_ipInRange($ip, $range) +{ + list($subnet, $bits) = explode('/', $range); + $ip = ip2long($ip); + $subnet = ip2long($subnet); + $mask = -1 << (32 - $bits); + $subnet &= $mask; + return ($ip & $mask) == $subnet; +} + +function c2b_confirmation() +{ + + global $config; + header("Content-Type: application/json"); + + $clientIP = $_SERVER['REMOTE_ADDR']; + + if (!c2b_isValidSafaricomIP($clientIP)) { + c2b_logAndNotify("Unauthorized request from IP: {$clientIP}"); + http_response_code(403); + echo json_encode(["ResultCode" => 1, "ResultDesc" => "Unauthorized"]); + return; + } + + $mpesaResponse = file_get_contents('php://input'); + if ($mpesaResponse === false) { + c2b_logAndNotify("Failed to get input stream."); + return; + } + + c2b_webhook_log('Received webhook request'); + c2b_webhook_log($mpesaResponse); + + $content = json_decode($mpesaResponse); + if (json_last_error() !== JSON_ERROR_NONE) { + c2b_logAndNotify("Failed to decode JSON response: " . json_last_error_msg()); + return; + } + c2b_webhook_log('Decoded JSON data successfully'); + + if (!class_exists('Package')) { + c2b_logAndNotify("Error: Package class does not exist."); + return; + } + + if (isset($config['mpesa_c2b_bill_ref'])) { + switch ($config['mpesa_c2b_bill_ref']) { + case 'phone': + $customer = ORM::for_table('tbl_customers') + ->where('phonenumber', $content->BillRefNumber) + ->find_one(); + break; + + case 'username': + $customer = ORM::for_table('tbl_customers') + ->where('username', $content->BillRefNumber) + ->find_one(); + break; + + case 'id': + $customer = ORM::for_table('tbl_customers') + ->where('id', $content->BillRefNumber) + ->find_one(); + break; + + default: + $customer = null; + break; + } + + if (!$customer) { + sendTelegram("Validation failed: No account found for BillRefNumber: $content->BillRefNumber"); + _log("Validation failed: No account found for BillRefNumber: $content->BillRefNumber"); + echo json_encode(["ResultCode" => "C2B00012", "ResultDesc" => "Invalid Account Number"]); + return; + } + } else { + _log("Configuration error: mpesa_c2b_bill_ref not set."); + sendTelegram("Configuration error: mpesa_c2b_bill_ref not set."); + } + + + $bills = c2b_billing($customer->id); + if (!$bills) { + c2b_logAndNotify("No matching bill found for BillRefNumber: {$content->BillRefNumber}"); + return; + } + + foreach ($bills as $bill) { + c2b_handleBillPayment($content, $customer, $bill); + } + + echo json_encode(["ResultCode" => 0, "ResultDesc" => "Accepted"]); +} + + +function c2b_handleBillPayment($content, $customer, $bill) +{ + $amountToPay = $bill['price']; + $amountPaid = $content->TransAmount; + $channel_mode = "Mpesa C2B - {$content->TransID}"; + $customerBalance = $customer->balance; + $currentBalance = $customerBalance + $amountPaid; + $customerID = $customer->id; + + try { + $transaction = c2b_storeTransaction($content, $bill['namebp'], $amountToPay, $customerID); + } catch (Exception $e) { + c2b_handleException("Failed to save transaction", $e); + exit; + } + + if ($currentBalance >= $amountToPay) { + $excessAmount = $currentBalance - $amountToPay; + try { + $result = Package::rechargeUser($customer->id, $bill['routers'], $bill['plan_id'], 'mpesa', $channel_mode); + if (!$result) { + c2b_logAndNotify("Mpesa Payment Successful, but failed to activate the package for customer {$customer->username}."); + } else { + if ($excessAmount > 0) { + $customer->balance = $excessAmount; + $customer->save(); + } else { + $customer->balance = 0; + $customer->save(); + } + c2b_sendPaymentSuccessMessage($customer, $amountPaid, $bill['namebp']); + $transaction->transactionStatus = 'Completed'; + $transaction->save(); + } + } catch (Exception $e) { + c2b_handleException("Error during package activation", $e); + } + } else { + c2b_updateCustomerBalance($customer, $currentBalance, $amountPaid); + $neededToActivate = $amountToPay - $currentBalance; + c2b_sendBalanceUpdateMessage($customer, $amountPaid, $currentBalance, $neededToActivate); + $transaction->transactionStatus = 'Completed'; + $transaction->save(); + } +} + + +function c2b_storeTransaction($content, $packageName, $packagePrice, $customerID) +{ + ORM::get_db()->beginTransaction(); + try { + $transaction = ORM::for_table('tbl_mpesa_transactions') + ->where('TransID', $content->TransID) + ->find_one(); + + if ($transaction) { + // Update existing transaction + $transaction->TransactionType = $content->TransactionType; + $transaction->TransTime = $content->TransTime; + $transaction->TransAmount = $content->TransAmount; + $transaction->BusinessShortCode = $content->BusinessShortCode; + $transaction->BillRefNumber = $content->BillRefNumber; + $transaction->OrgAccountBalance = $content->OrgAccountBalance; + $transaction->MSISDN = $content->MSISDN; + $transaction->FirstName = $content->FirstName; + $transaction->PackageName = $packageName; + $transaction->PackagePrice = $packagePrice; + $transaction->customerID = $customerID; + $transaction->transactionStatus = 'Pending'; + } else { + // Create new transaction + $transaction = ORM::for_table('tbl_mpesa_transactions')->create(); + $transaction->TransID = $content->TransID; + $transaction->TransactionType = $content->TransactionType; + $transaction->TransTime = $content->TransTime; + $transaction->TransAmount = $content->TransAmount; + $transaction->BusinessShortCode = $content->BusinessShortCode; + $transaction->BillRefNumber = $content->BillRefNumber; + $transaction->OrgAccountBalance = $content->OrgAccountBalance; + $transaction->MSISDN = $content->MSISDN; + $transaction->FirstName = $content->FirstName; + $transaction->PackageName = $packageName; + $transaction->PackagePrice = $packagePrice; + $transaction->customerID = $customerID; + $transaction->transactionStatus = 'Pending'; + } + $transaction->save(); + ORM::get_db()->commit(); + return $transaction; + } catch (Exception $e) { + ORM::get_db()->rollBack(); + throw $e; + } +} + +function c2b_logAndNotify($message) +{ + _log($message); + sendTelegram($message); +} + +function c2b_handleException($message, $e) +{ + $fullMessage = "$message: " . $e->getMessage() . " in " . $e->getFile() . " on line " . $e->getLine(); + c2b_logAndNotify($fullMessage); +} + +function c2b_updateCustomerBalance($customer, $newBalance, $amountPaid) +{ + try { + $customer->balance = $newBalance; + $customer->save(); + c2b_logAndNotify("Payment of KES {$amountPaid} has been added to the balance of customer {$customer->username}."); + } catch (Exception $e) { + c2b_handleException("Failed to update customer balance", $e); + } +} + +function c2b_sendPaymentSuccessMessage($customer, $amountPaid, $packageName) +{ + $config = c2b_config(); + $message = "Dear {$customer->fullname}, your payment of KES {$amountPaid} has been received and your plan {$packageName} has been successfully activated. Thank you for choosing {$config['CompanyName']}."; + c2b_sendNotification($customer, $message); +} + +function c2b_sendBalanceUpdateMessage($customer, $amountPaid, $currentBalance, $neededToActivate) +{ + $config = c2b_config(); + $message = "Dear {$customer->fullname}, your payment of KES {$amountPaid} has been received and added to your account balance. Your current balance is KES {$currentBalance}."; + if ($neededToActivate > 0) { + $message .= " To activate your package, you need to add KES {$neededToActivate} more to your account."; + } else { + $message .= " Your current balance is sufficient to activate your package."; + } + $message .= "\n" . $config['CompanyName']; + c2b_sendNotification($customer, $message); +} + +function c2b_sendNotification($customer, $message) +{ + try { + Message::sendSMS($customer->phonenumber, $message); + Message::sendWhatsapp($customer->phonenumber, $message); + } catch (Exception $e) { + c2b_handleException("Failed to send SMS/WhatsApp message", $e); + } +} + + +function c2b_validation() +{ + header("Content-Type: application/json"); + $mpesaResponse = file_get_contents('php://input'); + + $config = c2b_config(); + $content = json_decode($mpesaResponse); + + if (json_last_error() !== JSON_ERROR_NONE) { + sendTelegram("Failed to decode JSON response."); + _log("Failed to decode JSON response."); + echo json_encode(["ResultCode" => "C2B00016", "ResultDesc" => "Invalid JSON format"]); + return; + } + + $BillRefNumber = $content->BillRefNumber; + $TransAmount = $content->TransAmount; + + if (isset($config['mpesa_c2b_bill_ref'])) { + switch ($config['mpesa_c2b_bill_ref']) { + case 'phone': + $customer = ORM::for_table('tbl_customers') + ->where('phonenumber', $content->BillRefNumber) + ->find_one(); + break; + + case 'username': + $customer = ORM::for_table('tbl_customers') + ->where('username', $content->BillRefNumber) + ->find_one(); + break; + + case 'id': + $customer = ORM::for_table('tbl_customers') + ->where('id', $content->BillRefNumber) + ->find_one(); + break; + + default: + $customer = null; + break; + } + + if (!$customer) { + sendTelegram("Validation failed: No account found for BillRefNumber: $BillRefNumber"); + _log("Validation failed: No account found for BillRefNumber: $BillRefNumber"); + echo json_encode(["ResultCode" => "C2B00012", "ResultDesc" => "Invalid Account Number"]); + return; + } + } else { + _log("Configuration error: mpesa_c2b_bill_ref not set."); + sendTelegram("Configuration error: mpesa_c2b_bill_ref not set."); + } + + + $bills = c2b_billing($customer->id); + + if (!$bills) { + sendTelegram("Validation failed: No bill found for BillRefNumber: $BillRefNumber"); + _log("Validation failed: No bill found for BillRefNumber: $BillRefNumber"); + echo json_encode(["ResultCode" => "C2B00012", "ResultDesc" => "Invalid Bill Reference"]); + return; + } + + foreach ($bills as $bill) { + } + + $billAmount = $bill['price']; + if (!$config['mpesa_c2b_low_fee']) { + if ($TransAmount < $billAmount) { + sendTelegram("Validation failed: Insufficient amount. Transferred: $TransAmount, Required: $billAmount"); + _log("Validation failed: Insufficient amount. Transferred: $TransAmount, Required: $billAmount"); + echo json_encode(["ResultCode" => "C2B00013", "ResultDesc" => "Invalid or Insufficient Amount"]); + return; + } + } + + sendTelegram("Validation successful for BillRefNumber: $BillRefNumber with amount: $TransAmount"); + _log("Validation successful for BillRefNumber: $BillRefNumber with amount: $TransAmount"); + echo json_encode(["ResultCode" => 0, "ResultDesc" => "Accepted"]); +} + +function c2b_billing($id) +{ + $d = ORM::for_table('tbl_user_recharges') + ->selects([ + 'customer_id', + 'username', + 'plan_id', + 'namebp', + 'recharged_on', + 'recharged_time', + 'expiration', + 'time', + 'status', + 'method', + 'plan_type', + ['tbl_user_recharges.routers', 'routers'], + ['tbl_user_recharges.type', 'type'], + 'admin_id', + 'prepaid' + ]) + ->select('tbl_plans.price', 'price') + ->left_outer_join('tbl_plans', array('tbl_plans.id', '=', 'tbl_user_recharges.plan_id')) + ->where('customer_id', $id) + ->find_many(); + return $d; +} + +function c2b_config() +{ + $result = ORM::for_table('tbl_appconfig')->find_many(); + foreach ($result as $value) { + $config[$value['setting']] = $value['value']; + } + return $config; +} diff --git a/system/plugin/clear_cache.php b/system/plugin/clear_cache.php new file mode 100644 index 0000000..6a850dc --- /dev/null +++ b/system/plugin/clear_cache.php @@ -0,0 +1,48 @@ +assign('_title', 'Clear Cache'); + $ui->assign('_system_menu', 'settings'); + $admin = Admin::_info(); + $ui->assign('_admin', $admin); + + // Check user type for access + if (!in_array($admin['user_type'], ['SuperAdmin', 'Admin'])) { + _alert(Lang::T('You do not have permission to access this page'), 'danger', "dashboard"); + exit; + } + + $compiledCacheDir = 'ui/compiled'; + $templateCacheDir = 'system/cache'; + + try { + // Clear the compiled cache + $files = scandir($compiledCacheDir); + foreach ($files as $file) { + if ($file !== '.' && $file !== '..' && is_file($compiledCacheDir . '/' . $file)) { + unlink($compiledCacheDir . '/' . $file); + } + } + + // Clear the template cache + $templateCacheFiles = glob($templateCacheDir . '/*.{json,temp}', GLOB_BRACE); + foreach ($templateCacheFiles as $file) { + if (is_file($file)) { + unlink($file); + } + } + + // Cache cleared successfully + _log('[' . ($admin['fullname'] ?? 'Unknown Admin') . ']: ' . Lang::T(' Cleared the system cache '), $admin['user_type']); + r2(U . 'dashboard', 's', Lang::T("Cache cleared successfully!")); + } catch (Exception $e) { + // Error occurred while clearing the cache + _log('[' . ($admin['fullname'] ?? 'Unknown Admin') . ']: ' . Lang::T(' Error occurred while clearing the cache: ' . $e->getMessage()), $admin['user_type']); + r2(U . 'dashboard', 'e', Lang::T("Error occurred while clearing the cache: ") . $e->getMessage()); + } +}