Initial commit

This commit is contained in:
Kwesi Banson Jnr
2026-04-08 05:53:02 +00:00
commit 592a161ee6
63 changed files with 4105 additions and 0 deletions

35
.gitignore vendored Normal file
View File

@@ -0,0 +1,35 @@
# IDE/Editor specific files
.idea/
.vscode/
*.suo
*.ntvs*
*.njsproj
*.sln
*.vssscc
.DS_Store
# Dependency Manager (Composer)
/vendor/
composer.phar
# Environment variables and sensitive files
.env
.env.local
.env.*.php
secrets.php
# Temporary files and logs
/tmp/
/log/
*.log
*.tmp
*.cache
*.session
# Built assets (if applicable)
/public/build/
/public/storage/
# PHP-specific ignores
/web/cache/
*.swp

11
api-testing.md Normal file
View File

@@ -0,0 +1,11 @@
https://payments.click-mobile.com/mse/api/public/disbursement
{
"transaction_id" : "MP1234567906",
"reference_id" : "INF99887755",
"mobile_number" : "888123456",
"amount" : "500"
}
API Key : bd8f75cc-0f63-4707-9a2e-361800a0d94c

10
app/Config/database.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
return [
'host' => $_ENV['DB_HOST'],
'db' => $_ENV['DB_NAME'],
'user' => $_ENV['DB_USER'],
'pass' => $_ENV['DB_PASS'],
];
?>

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Controllers;
use App\Models\User;
use App\Core\Auth;
use App\Core\Validator;
use App\Core\Controller;
class AuthController extends Controller {
public function login() {
$errors = Validator::validate($_POST, [
'email' => 'required|email',
'password' => 'required'
]);
if (!empty($errors)) {
return $this->render('auth/login', ['errors' => $errors]);
}
// Check database for user
$user = User::findByEmail($_POST['email']); // Custom method in User model
if ($user && password_verify($_POST['password'], $user['password'])) {
Auth::login($user);
header('Location: /dashboard');
} else {
return $this->render('auth/login', ['error' => 'Invalid credentials']);
}
}
}
?>

View File

@@ -0,0 +1,397 @@
<?php
namespace App\Controllers;
use App\Core\Controller;
use App\Models\User;
use App\Models\Disbursement;
use App\Models\AirtelBroker;
use App\Models\MpambaBroker;
use App\Core\Auth;
use App\Core\Logger;
class DisbursementController extends Controller {
public function index() {
#$users = User::all();
$token = Auth::getBearerToken();
$user = User::getByToken($token);
$logger = new Logger();
if ($user == false) {
// code...
$logger->error("API Authentication failed token : " . $token);
return $this->json([
"status" => "failed",
"message" => "Unauthorised Access",
]);
}
$log_data = json_encode($user);
$logger->info("User $log_data successfully authenticated from " . $_SERVER['REMOTE_ADDR']);
$payload = json_decode(file_get_contents("php://input") , true);
if (json_last_error() !== JSON_ERROR_NONE) {
// echo "JSON Error: " . json_last_error_msg();
http_response_code(400);
return $this->json([
"status" => "failed",
"message" => "Bad Request. Invalid JSON Object",
]);
}
$requested_keys = ['transaction_id', 'reference_id', 'mobile_number', 'amount'];
foreach ($requested_keys as $key) {
if (!isset($payload[$key]) || trim($payload[$key]) === '') {
http_response_code(400);
return $this->json([
"status" => "failed",
"message" => "Validation Error: Missing or empty field : {$key}",
]);
}
}
if (!preg_match('/^[a-zA-Z0-9]+$/', $payload['transaction_id'])) {
http_response_code(400);
return $this->json([
"status" => "failed",
"message" => "Validation Error: Malformed Transaction ID",
]);
}
if (!preg_match('/^[a-zA-Z0-9]+$/', $payload['reference_id'])) {
return $this->json([
"status" => "failed",
"message" => "Validation Error: Malformed Reference ID",
]);
}
if (!preg_match('/^[0-9]{9,12}$/', $payload['mobile_number'])) {
return $this->json([
"status" => "failed",
"message" => "Validation Error: Invalid Mobile Number format",
]);
}
$amount = filter_var($payload['amount'], FILTER_VALIDATE_FLOAT);
if ($amount === false || $amount <= 0) {
return $this->json([
"status" => "failed",
"message" => "Validation Error: Amount must be a positive number",
]);
}
$logger->info("Disbursement check request : " . json_encode($payload));
$transaction_details = Disbursement::varifyTransaction($payload['transaction_id'], $payload['reference_id']);
if ($transaction_details == false) {
return $this->json([
'status' => 'fail',
'data' => 'Transaction not found',
]);
}
$network = $this->getNetwork($payload['mobile_number']);
//pull this away into the ENV file
$pin = "IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=";
if ($network == 'airtel') {
$broker_details = AirtelBroker::find($transaction_details[0]['wallet_id']);
$authURL = $_ENV['AIRTEL_MONEY_AUTH_URL'];
$auth_result = $this->authenticateAirtel($authURL, $broker_details['client_id'], $broker_details['client_secret']);
if ($auth_result['success'] == false) {
$logger->info("Airtel Money authentication failed " . json_encode($auth_result));
return $this->json([
'status' => 'fail',
'data' => 'Your request could not be handled at this time. Try again',
]);
}
$airtel_array = [
"accessToken" => $auth_result['token'],
"msisdn" => $transaction_details[0]['msisdn'],
"amount" => $amount,
"reference" => $transaction_details[0]['infotech_transaction_id'],
"pin" => $pin,
"transactionId" => $transaction_details[0]['transaction_id'],
"country" => "MW",
"currency" => "MWK"
];
$retval = $this->airtelDisbursement($airtel_array);
var_dump($retval);
$disbursement_update_data = [
'response_message' => $disbursement_retval['response'],
'confirmed' => 1,
'confirmed_at' => date('Y-m-d H:i:s'),
'status' => 'successful'
];
$logger->info('Disbursement Update data : ' . json_encode($disbursement_update_data));
$update_result = Disbursement::updateById($transaction_details[0]['id'], $disbursement_update_data);
$logger->info('Airtel Disbursement Update Result : ' . $update_result);
}
elseif($network == 'tnm'){
//code
$broker_details = MpambaBroker::getBroker($transaction_details[0]['wallet_id']);
$auth_result = $this->authenticateMpamba($transaction_details[0]['wallet_id'], $broker_details[0]['password']);
$params = [
"msisdn" => $transaction_details[0]['msisdn'],
"amount" => $transaction_details[0]['amount'],
"transaction_id" => $transaction_details[0]['transaction_id'],
"narration" => "Testing", //$transaction_details[0]['description']
"token" => $auth_result['token']
];
$disbursement_retval = $this->mpambaDisbursement($params);
var_dump($disbursement_retval);
$disbursement_update_data = [
'response_message' => $disbursement_retval['raw'],
'confirmed' => 1,
'confirmed_at' => date('Y-m-d H:i:s'),
'status' => 'successful'
];
$logger->info('Disbursement Update data : ' . json_encode($disbursement_update_data));
$update_result = Disbursement::updateById($transaction_details[0]['id'], $disbursement_update_data);
$logger->info('Mpamba Disbursement Update Result : ' . $update_result);
}
else{
return $this->json([
'status' => 'fail',
'data' => 'Invalid phone number',
]);
}
}
public function show($id) {
$user = User::find($id);
if (!$user) {
return $this->json(['message' => 'User not found'], 404);
}
return $this->json($user);
}
public function testUpdate(){
$disbursement_update_data = [
'response_message' => 'in heere',
'confirmed' => 1,
'confirmed_at' => date('Y-m-d H:i:s'),
'status' => 'successful'
];
$update_result = Disbursement::updateById('3', $disbursement_update_data);
var_dump($update_result);
}
public function airtelDisbursement($params) {
$timeout = 30;
$logger = new Logger();
$url = $_ENV['AIRTEL_MONEY_DISBURSEMENT_URL'];
$payload = [
"payee" => [
"msisdn" => $params['msisdn']
],
"reference" => $params['reference'],
"pin" => $params['pin'],
"transaction" => [
"amount" => $params['amount'],
"id" => $params['transactionId']
]
];
$log_data = json_encode($payload);
$logger->info("Disbursement Requests to Airtel MW : " . $log_data );
$headers = [
'Content-Type: application/json',
'Accept: */*',
'X-Country: ' . $params['country'],
'X-Currency: ' . $params['currency'],
'Authorization: Bearer ' . $params['accessToken']
];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_SSL_VERIFYPEER => true
]);
$response = curl_exec($ch);
$error = curl_error($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($error) {
return [
'success' => false,
'error' => 'CURL_ERROR',
'message' => $error
];
}
$logger->info("Disbursement Response from Airtel MW : " . $response);
return [
'success' => ($code === 200),
'http_code' => $code,
'response' => json_decode($response, true),
'raw' => $response
];
}
public function authenticateAirtel($baseURL, $wallet, $password){
// JSON payload
$postData = json_encode([
"client_id" => $wallet,
"client_secret" => $password,
"grant_type" => "client_credentials"
]);
if (json_last_error() !== JSON_ERROR_NONE) {
return [
"status" => "failed",
"message" => "Bad Request. Invalid JSON Object" . json_last_error_msg(),
];
}
$ch = curl_init($baseURL);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . strlen($postData)
]);
$response = curl_exec($ch);
if (curl_errno($ch)) {
$curl_error = curl_error($ch);
curl_close($ch);
return [
'success' => false,
'error' => 'Curl error: ' . $curl_error
];
}
// Get HTTP status code
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Decode JSON response
$result = json_decode($response, true);
// Check if token is present
if ($httpCode === 200 && isset($result['access_token'])) {
return [
'success' => true,
'token' => $result['access_token']
];
} else {
return [
'success' => false,
'error' => $result['error_description'] ?? 'Unknown error',
'details' => $result['error'] ?? []
];
}
}
#Mpamba Scripts
public function authenticateMpamba($wallet, $password){
$logger = new Logger();
$url = rtrim($_ENV['MPAMBA_BASE_URL'], '/') . '/authenticate';
// JSON payload
$postData = json_encode([
'wallet' => $wallet,
'password' => $password
]);
$logger->info("Mpamba Authentication Requests : " . $postData );
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Return the response
curl_setopt($ch, CURLOPT_POST, true); // Use POST method
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);// Set the request body
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . strlen($postData)
]);
$response = curl_exec($ch);
if (curl_errno($ch)) {
$curl_error = curl_error($ch);
$logger->info("Mpamba Authentication Error : " . $curl_error );
curl_close($ch);
return [
'success' => false,
'error' => 'Error: ' . $curl_error
];
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Decode JSON response
$result = json_decode($response, true);
// Check if token is present
$logger->info("Mpamba Authentication Response : " . json_encode($result));
if ($httpCode === 200 && isset($result['data']['token'])) {
return [
'success' => true,
'token' => $result['data']['token'],
'expires_at' => $result['data']['expires_at']
];
} else {
return [
'success' => false,
'error' => $result['message'] ?? 'Unknown error',
'details' => $result['errors'] ?? []
];
}
}
public function mpambaDisbursement($params){
$logger = new Logger();
$url = $_ENV['MPAMBA_BASE_URL'] . "/payments";
$payload = array(
"msisdn" => $params['msisdn'],
"amount" => (int)$params['amount'],
"transaction_id" => $params['transaction_id'],
"narration" => $params['narration']
);
$logger->info("Mpamba Disbursement Requests : " . json_encode($payload));
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
"Content-Type: application/json",
"Authorization: Bearer " . $params['token']
));
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
if ($response === false) {
$error = curl_error($ch);
$logger->info("Mpamba Disbursement Response Error : " . $error);
curl_close($ch);
return array(
"status" => "ERROR",
"message" => "Error: " . $error
);
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$logger->info("Mpamba Disbursement Response : " . $response);
$responseData = json_decode($response, true);
return array(
"http_code" => $httpCode,
"response" => $responseData
);
}
}
?>

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Controllers;
use App\Core\Controller;
use App\Models\User;
class HomeController extends Controller {
public function index() {
echo json_encode(['code' => 1, 'msg' => 'heere at the wall']);
}
}
?>

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Controllers;
use App\Core\Controller;
use App\Models\User;
class UserController extends Controller {
public function index() {
// 1. Get data from a Model
$users = User::all();
// 2. Pass data to the view via the base render() method
return $this->render('users/index', [
'title' => 'All Users',
'users' => $users
]);
}
public function store() {
User::create([
'username' => $_POST['username'],
'email' => $_POST['email'],
'password' => password_hash($_POST['password'], PASSWORD_DEFAULT)
]);
header('Location: /users');
}
// Update a user
public function update() {
User::updateById($_POST['id'], [
'email' => $_POST['email']
]);
}
// Delete a user
public function destroy() {
User::deleteById($_GET['id']);
}
}
?>

37
app/Core/Auth.php Normal file
View File

@@ -0,0 +1,37 @@
<?php
namespace App\Core;
class Auth {
public static function login($user) {
session_start();
session_regenerate_id(true); // Prevents session hijacking
$_SESSION['user_id'] = $user['id'];
$_SESSION['user_name'] = $user['username'];
}
public static function check() {
if (session_status() === PHP_SESSION_NONE) session_start();
return isset($_SESSION['user_id']);
}
public static function user() {
return $_SESSION['user_name'] ?? null;
}
public static function logout() {
session_start();
session_destroy();
header('Location: /login');
exit;
}
public static function getBearerToken(): ?string {
$headers = $_SERVER['Authorization'] ?? $_SERVER['HTTP_AUTHORIZATION'] ?? null;
if (!$headers && function_exists('apache_request_headers')) {
$req = apache_request_headers();
$headers = $req['Authorization'] ?? $req['authorization'] ?? null;
}
return ($headers && preg_match('/Bearer\s(\S+)/', $headers, $matches)) ? $matches[1] : null;
}
}
?>

51
app/Core/Config.php Normal file
View File

@@ -0,0 +1,51 @@
<?php
namespace App\Core;
class Config {
private static $instance = null;
private array $settings = [];
private function __construct() {
// Map environment variables to internal keys with optional defaults
$this->settings = [
'db' => [
'host' => $_ENV['DB_HOST'] ?? 'localhost',
'name' => $_ENV['DB_NAME'] ?? 'my_app',
'user' => $_ENV['DB_USER'] ?? 'root',
'pass' => $_ENV['DB_PASS'] ?? '',
],
'app' => [
'name' => $_ENV['APP_NAME'] ?? 'Vanilla PHP App',
'env' => $_ENV['APP_ENV'] ?? 'production',
'debug' => ($_ENV['APP_DEBUG'] ?? 'false') === 'true',
'url' => $_ENV['APP_URL'] ?? 'http://localhost',
],
'mail' => [
'host' => $_ENV['MAIL_HOST'] ?? 'smtp.mailtrap.io',
'port' => $_ENV['MAIL_PORT'] ?? 2525,
]
];
}
public static function get(string $key, $default = null) {
if (self::$instance === null) {
self::$instance = new self();
}
// Support dot notation (e.g., 'db.host')
$keys = explode('.', $key);
$value = self::$instance->settings;
foreach ($keys as $k) {
if (!isset($value[$k])) {
return $default;
}
$value = $value[$k];
}
return $value;
}
}
?>

87
app/Core/Controller.php Normal file
View File

@@ -0,0 +1,87 @@
<?php
namespace App\Core;
abstract class Controller {
/**
* Helper to render a view file with data
*/
protected function renderOld(string $view, array $data = []) {
// Find the view file in the app/Views folder
$viewFile = __DIR__ . "/../Views/{$view}.phtml";
if (!file_exists($viewFile)) {
die("View file $view not found.");
}
// Extract data array into local variables for the view
extract($data);
// Start output buffering to capture the template content
ob_start();
include $viewFile;
echo ob_get_clean();
}
protected function render(string $view, array $data = []) {
extract($data);
// 1. Render the specific page view into a variable
ob_start();
include __DIR__ . "/../Views/{$view}.phtml";
$content = ob_get_clean();
// 2. Render the main layout and pass the $content into it
include __DIR__ . "/../Views/layouts/main.phtml";
}
protected function json(array $data, int $code = 200) {
// Set header to JSON
header('Content-Type: application/json; charset=utf-8');
// Set HTTP status code (200, 201, 400, 404, etc.)
http_response_code($code);
// Output the data
echo json_encode($data);
exit; // End execution to prevent accidental HTML output
}
protected function getNetwork($msisdn){
$phone = str_replace(' ', '', $msisdn);
$phone = str_replace('+', '', $msisdn);
$pattern = "/^\+?(265|0)?[89]\d{8}$/i";
$retval = preg_match($pattern, $phone, $matches, PREG_OFFSET_CAPTURE);
if (count($matches) < 1) {
return false;
}
elseif (strlen($phone) == 9) {
$msisdn = "265" . $phone;
}
elseif (strlen($phone) == 10) {
$phone = ltrim($phone, 0);
$msisdn = "265" . $phone;
}
elseif (strlen($phone) == 12) {
$msisdn = $phone;
}
if (!isset($msisdn)) {
return FALSE;
}
$prefix = substr($msisdn, 0, 4);
if ($prefix == '2658') {
$network = 'tnm';
}
elseif ($prefix == '2659' ) {
$network = 'airtel';
}
else{
return FALSE;
}
return $network;
}
}
?>

17
app/Core/Csrf.php Normal file
View File

@@ -0,0 +1,17 @@
<?php
namespace App\Core;
class Csrf {
public static function generate() {
Session::start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
public static function verify($token) {
Session::start();
return hash_equals($_SESSION['csrf_token'] ?? '', $token);
}
}

33
app/Core/Database.php Normal file
View File

@@ -0,0 +1,33 @@
<?php
namespace App\Core;
use PDO;
class Database {
private static $instance = null;
private $connection;
private function __construct() {
$config = require __DIR__ . '/../Config/database.php';
$dsn = "mysql:host={$config['host']};dbname={$config['db']};charset=utf8mb4";
$this->connection = new PDO($dsn, $config['user'], $config['pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]);
}
public static function getInstance() {
if (!self::$instance) {
self::$instance = new self();
}
return self::$instance->connection;
}
}
// Usage in a Model:
// $db = \App\Core\Database::getInstance();
// $stmt = $db->query("SELECT * FROM users");
?>

60
app/Core/Logger.php Normal file
View File

@@ -0,0 +1,60 @@
<?php
namespace App\Core;
class Logger {
private string $logFile;
private int $maxSize = 5242880;
private int $maxFiles = 5;
public function __construct(string $filename = 'app.log') {
// Place logs in storage/logs/ at the project root
$this->logFile = __DIR__ . "/../../storage/logs/" . $filename;
}
public function log(string $message, string $level = 'INFO'): void {
$this->checkRotation();
$timestamp = date('Y-m-d H:i:s');
$formattedMessage = "[$timestamp] [$level] $message" . PHP_EOL;
file_put_contents($this->logFile, $formattedMessage, FILE_APPEND | LOCK_EX);
}
private function checkRotation(): void {
if (file_exists($this->logFile) && filesize($this->logFile) >= $this->maxSize) {
$this->rotate();
}
}
private function rotate(): void {
// Delete the oldest file if it exists (e.g., app.log.5)
$oldestFile = $this->logFile . '.' . $this->maxFiles;
if (file_exists($oldestFile)) {
unlink($oldestFile);
}
// Shift existing files up (4 becomes 5, 3 becomes 4, etc.)
for ($i = $this->maxFiles - 1; $i >= 1; $i--) {
$currentFile = $this->logFile . '.' . $i;
$nextFile = $this->logFile . '.' . ($i + 1);
if (file_exists($currentFile)) {
rename($currentFile, $nextFile);
}
}
// Move the main log file to .1
rename($this->logFile, $this->logFile . '.1');
// Create a new empty log file
touch($this->logFile);
chmod($this->logFile, 0664);
}
// Shorthand helpers
public function info($msg) { $this->log($msg, 'INFO'); }
public function error($msg) { $this->log($msg, 'ERROR'); }
}
?>

33
app/Core/Model.php Normal file
View File

@@ -0,0 +1,33 @@
<?php
namespace App\Core;
abstract class Model {
protected static $table;
protected static function builder() {
$pdo = Database::getInstance();
return (new QueryBuilder($pdo))->table(static::$table);
}
public static function all() {
return self::builder()->get();
}
public static function find($id) {
return self::builder()->where('id', $id)->get()[0] ?? null;
}
public static function create(array $data) {
return static::builder()->insert($data);
}
public static function updateById($id, array $data) {
return static::builder()->where('id', $id)->update($data);
}
public static function deleteById($id) {
return static::builder()->where('id', $id)->delete();
}
}
?>

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Core;
use PDO;
class OldQueryBuilder {
protected $pdo;
protected $table;
protected $where = [];
protected $params = [];
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function table($table) {
$this->table = $table;
return $this;
}
public function where($column, $value) {
$this->where[] = "{$column} = :{$column}";
$this->params[$column] = $value;
return $this;
}
public function get() {
$sql = "SELECT * FROM {$this->table}";
if (!empty($this->where)) {
$sql .= " WHERE " . implode(' AND ', $this->where);
}
$stmt = $this->pdo->prepare($sql);
$stmt->execute($this->params);
return $stmt->fetchAll();
}
public function insert(array $data) {
$columns = implode(', ', array_keys($data));
$placeholders = ':' . implode(', :', array_keys($data));
$sql = "INSERT INTO {$this->table} ({$columns}) VALUES ({$placeholders})";
return $this->pdo->prepare($sql)->execute($data);
}
public function update(array $data) {
$fields = "";
foreach ($data as $key => $value) {
$fields .= "{$key} = :{$key}, ";
}
$fields = rtrim($fields, ', ');
$sql = "UPDATE {$this->table} SET {$fields}";
if (!empty($this->where)) {
$sql .= " WHERE " . implode(' AND ', $this->where);
}
// Merge update data with where parameters
return $this->pdo->prepare($sql)->execute(array_merge($data, $this->params));
}
public function delete() {
$sql = "DELETE FROM {$this->table}";
if (!empty($this->where)) {
$sql .= " WHERE " . implode(' AND ', $this->where);
}
return $this->pdo->prepare($sql)->execute($this->params);
}
}
?>

View File

@@ -0,0 +1,82 @@
<?php
namespace App\Core;
use PDO;
class QueryBuilder {
protected $pdo;
protected $table;
protected $where = [];
protected $params = [];
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function table($table) {
$this->table = $table;
return $this;
}
public function where($column, $operator, $value = null) {
if ($value === null) {
$value = $operator;
$operator = '=';
}
$paramName = 'where_' . str_replace('.', '_', $column) . '_' . count($this->params);
$this->where[] = "{$column} {$operator} :{$paramName}";
$this->params[$paramName] = $value;
return $this;
}
public function get() {
$sql = "SELECT * FROM {$this->table}";
if (!empty($this->where)) {
$sql .= " WHERE " . implode(' AND ', $this->where);
}
// echo "<b>SQL:</b> " . $sql . "<br><br>";
// echo "<b>Params:</b><pre>"; print_r($this->params); echo "</pre>";
// // die();
$stmt = $this->pdo->prepare($sql);
$stmt->execute($this->params);
return $stmt->fetchAll();
}
public function insert(array $data) {
$columns = implode(', ', array_keys($data));
$placeholders = ':' . implode(', :', array_keys($data));
$sql = "INSERT INTO {$this->table} ({$columns}) VALUES ({$placeholders})";
return $this->pdo->prepare($sql)->execute($data);
}
public function update(array $data) {
$fields = "";
foreach ($data as $key => $value) {
$fields .= "{$key} = :{$key}, ";
}
$fields = rtrim($fields, ', ');
$sql = "UPDATE {$this->table} SET {$fields}";
if (!empty($this->where)) {
$sql .= " WHERE " . implode(' AND ', $this->where);
}
return $this->pdo->prepare($sql)->execute(array_merge($data, $this->params));
}
public function delete() {
$sql = "DELETE FROM {$this->table}";
if (!empty($this->where)) {
$sql .= " WHERE " . implode(' AND ', $this->where);
}
return $this->pdo->prepare($sql)->execute($this->params);
}
}
?>

163
app/Core/QueryBuilder.php Normal file
View File

@@ -0,0 +1,163 @@
<?php
namespace App\Core;
use PDO;
class QueryBuilder {
protected $pdo;
protected $table;
protected $where = [];
protected $params = [];
protected $orderBy = [];
protected $limit = null;
protected $selects = '*'; // Defaults to everything
protected $joins = [];
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function table($table) {
$this->table = $table;
return $this;
}
// Notice the new $boolean parameter (defaults to 'AND')
public function where($column, $operator, $value = null, $boolean = 'AND') {
if ($value === null) {
$value = $operator;
$operator = '=';
}
$paramName = 'where_' . str_replace('.', '_', $column) . '_' . count($this->params);
// If this is the first where clause, we don't need 'AND' or 'OR'
$prefix = empty($this->where) ? '' : strtoupper($boolean) . ' ';
$this->where[] = "{$prefix}{$column} {$operator} :{$paramName}";
$this->params[$paramName] = $value;
return $this;
}
// orWhere simply calls where() but passes 'OR'
public function orWhere($column, $operator, $value = null) {
return $this->where($column, $operator, $value, 'OR');
}
// Sort the results
public function orderBy($column, $direction = 'ASC') {
$direction = strtoupper($direction) === 'DESC' ? 'DESC' : 'ASC';
$this->orderBy[] = "{$column} {$direction}";
return $this;
}
// Limit the number of results
public function limit(int $limit) {
$this->limit = $limit;
return $this;
}
// Fetch a single record instead of an array of arrays
public function first() {
$this->limit(1);
$result = $this->get();
return $result ? $result[0] : null;
}
public function getOld() {
$sql = "SELECT * FROM {$this->table}";
// Changed implode from ' AND ' to ' ' because the prefix handles it now
if (!empty($this->where)) {
$sql .= " WHERE " . implode(' ', $this->where);
}
if (!empty($this->orderBy)) {
$sql .= " ORDER BY " . implode(', ', $this->orderBy);
}
if ($this->limit !== null) {
$sql .= " LIMIT {$this->limit}";
}
$stmt = $this->pdo->prepare($sql);
$stmt->execute($this->params);
return $stmt->fetchAll();
}
public function get() {
// Use the new selects property instead of hardcoded '*'
$sql = "SELECT {$this->selects} FROM {$this->table}";
// Add joins immediately after the FROM clause
if (!empty($this->joins)) {
$sql .= " " . implode(' ', $this->joins);
}
if (!empty($this->where)) {
$sql .= " WHERE " . implode(' ', $this->where);
}
if (!empty($this->orderBy)) {
$sql .= " ORDER BY " . implode(', ', $this->orderBy);
}
if ($this->limit !== null) {
$sql .= " LIMIT {$this->limit}";
}
$stmt = $this->pdo->prepare($sql);
$stmt->execute($this->params);
return $stmt->fetchAll();
}
public function insert(array $data) {
$columns = implode(', ', array_keys($data));
$placeholders = ':' . implode(', :', array_keys($data));
$sql = "INSERT INTO {$this->table} ({$columns}) VALUES ({$placeholders})";
return $this->pdo->prepare($sql)->execute($data);
}
public function update(array $data) {
$fields = "";
foreach ($data as $key => $value) {
$fields .= "{$key} = :{$key}, ";
}
$fields = rtrim($fields, ', ');
$sql = "UPDATE {$this->table} SET {$fields}";
if (!empty($this->where)) {
$sql .= " WHERE " . implode(' ', $this->where);
}
return $this->pdo->prepare($sql)->execute(array_merge($data, $this->params));
}
public function delete() {
$sql = "DELETE FROM {$this->table}";
if (!empty($this->where)) {
$sql .= " WHERE " . implode(' ', $this->where);
}
return $this->pdo->prepare($sql)->execute($this->params);
}
// Specify exactly which columns to return
public function select($columns) {
// Allow passing an array ['id', 'name'] or a raw string 'id, name'
$this->selects = is_array($columns) ? implode(', ', $columns) : $columns;
return $this;
}
// The main join method
public function join($table, $firstColumn, $operator, $secondColumn, $type = 'INNER') {
$this->joins[] = "{$type} JOIN {$table} ON {$firstColumn} {$operator} {$secondColumn}";
return $this;
}
// A helpful shortcut for LEFT JOINS
public function leftJoin($table, $firstColumn, $operator, $secondColumn) {
return $this->join($table, $firstColumn, $operator, $secondColumn, 'LEFT');
}
}
?>

66
app/Core/Router.php Normal file
View File

@@ -0,0 +1,66 @@
<?php
namespace App\Core;
class Router {
protected $routes = [];
public function get($path, $callback) {
$this->routes['GET'][$path] = $callback;
}
// Registers POST routes
public function post($path, $callback) {
$this->routes['POST'][$path] = $callback;
}
public function resolve($uri, $method) {
// Strip query strings (e.g., /users?id=1 becomes /users)
// In public/index.php
$path = parse_url($uri, PHP_URL_PATH);
// var_dump($path);
$callback = $this->routes[$method][$path] ?? null;
if (!$callback) {
http_response_code(404);
// echo "404 Not Found";
$this->handleNotFound($path, $method);
return;
}
// Handle 'Controller@method' strings
if (is_string($callback)) {
[$controllerName, $methodName] = explode('@', $callback);
$controllerClass = "\\App\\Controllers\\" . $controllerName;
$controller = new $controllerClass();
$controller->$methodName();
}
}
private function handleNotFound($path, $method) {
$logger = new Logger();
$isDebug = ($_ENV['APP_DEBUG'] ?? 'false') === 'true';
// 1. Log the failure for the developer
$logMessage = "404 Not Found | Method: $method | URI: $path";
$logger->error($logMessage);
http_response_code(404);
// 2. If Debug is ON, show detailed info in the browser
if ($isDebug) {
// echo "<h1>404 Not Found (Debug Mode)</h1>";
// echo "<p><strong>Method:</strong> $method</p>";
// echo "<p><strong>Attempted URI:</strong> $path</p>";
// echo "<p><strong>Defined Routes:</strong></p><pre>";
// print_r($this->routes[$method] ?? []);
// echo "</pre>";
echo json_encode([ 'message' => 'Not Found', 'method' => $method, 'path' => $path]);
} else {
// Simple message for production
echo "404 Not Found";
}
exit;
}
}
?>

22
app/Core/Session.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
namespace App\Core;
class Session {
public static function start() {
if (session_status() === PHP_SESSION_NONE) session_start();
}
public static function setFlash($key, $message) {
self::start();
$_SESSION['flash'][$key] = $message;
}
public static function getFlash($key) {
self::start();
$message = $_SESSION['flash'][$key] ?? null;
unset($_SESSION['flash'][$key]); // Delete after retrieval
return $message;
}
}
?>

24
app/Core/Validator.php Normal file
View File

@@ -0,0 +1,24 @@
<?php
namespace App\Core;
class Validator {
public static function validate(array $data, array $rules) {
$errors = [];
foreach ($rules as $field => $rule) {
if (strpos($rule, 'required') !== false && empty($data[$field])) {
$errors[$field] = ucfirst($field) . " is required.";
}
if (strpos($rule, 'email') !== false && !filter_var($data[$field], FILTER_VALIDATE_EMAIL)) {
$errors[$field] = "Invalid email format.";
}
if (strpos($rule, 'min:8') !== false && strlen($data[$field] ?? '') < 8) {
$errors[$field] = ucfirst($field) . " must be at least 8 characters.";
}
}
return $errors;
}
}
?>

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use App\Core\Model;
class AirtelBroker extends Model {
protected static $table = 'airtel_money_wallets';
public static function findActive() {
return self::builder()->where('status', 'active')->get();
}
public static function getBroker($wallet_id){
$details = self::builder()->where('id', $wallet_id)->get();
if ($details == false) {
return false;
}
return $details;
}
}
?>

47
app/Models/Auth.php Normal file
View File

@@ -0,0 +1,47 @@
<?php
class Auth {
private $connection;
// private $bearer_token;
public function __construct($db) {
$this->connection = $db;
}
public function read_api_auth(){
// $bearer = $this->bearer_token;
if(!function_exists('getallheaders')){
return false;
}
$headers = [];
foreach (getallheaders() as $name => $value) {
// echo "$name: $value <br>" . PHP_EOL;
$headers[$name] = $value;
}
$check = array_key_exists('Authorization', $headers);
if ($check == false) {
return false;
}
list($type, $bearer_token) = explode(" ", $headers['Authorization'], 2);
$query = 'SELECT id, name FROM auth_users WHERE bearer_token = ? LIMIT 0,1';
$statement = $this->connection->prepare($query);
$statement->bindParam(1, $bearer_token);
$statement->execute();
$row = $statement->fetch(PDO::FETCH_ASSOC);
if ($row == false) {
return false;
}
else{
return true;
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Models;
use App\Core\Model;
class Disbursement extends Model {
protected static $table = 'disbursements';
public static function findActive() {
return self::builder()->where('status', 'active')->get();
}
public static function varifyTransaction($transaction_id, $reference_number){
$transaction = self::builder()->where('transaction_id', $transaction_id)
->where('infotech_transaction_id', $reference_number)
->get();
if ($transaction == false) {
// code...
return false;
}
return $transaction;
}
public static function updateTransaction($id, $params){
$transaction = self::builder()->where('transaction_id', $transaction_id)
->where('infotech_transaction_id', $reference_number)
->get();
if ($transaction == false) {
// code...
return false;
}
return $transaction;
}
}
?>

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Models;
use App\Core\Model;
class MpambaBroker extends Model {
protected static $table = 'mpamba_tnm_wallets';
public static function findActive() {
return self::builder()->where('status', 'active')->get();
}
public static function getBroker($wallet_id){
$details = self::builder()->where('wallet_id', $wallet_id)->get();
if ($details == false) {
return false;
}
return $details;
}
}
?>

20
app/Models/User.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
namespace App\Models;
use App\Core\Model;
class User extends Model {
// Tell the model which database table to use
protected static $table = 'auth_users';
// You can add custom business logic here
public static function findActive() {
return self::builder()->where('status', 'active')->get();
}
public static function getByToken($token) {
return self::builder()->where('bearer_token', $token)->get();
}
}
?>

View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title><?= $title ?? 'My PHP App' ?></title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<nav>
<a href="/">Home</a>
<?php if (\App\Core\Auth::check()): ?>
<span>Welcome, <?= htmlspecialchars(\App\Core\Auth::user()) ?></span>
<a href="/logout">Logout</a>
<?php else: ?>
<a href="/login">Login</a>
<?php endif; ?>
</nav>
<main>
<!-- This is where the page content goes -->
<?= $content ?>
</main>
<footer>&copy; <?= date('Y') ?> My Vanilla App</footer>
<script src="/js/app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,8 @@
<!-- The $title and $users variables were passed from the controller -->
<h1><?= htmlspecialchars($title) ?></h1>
<ul>
<?php foreach ($users as $user): ?>
<li><?= htmlspecialchars($user['name']) ?> (<?= htmlspecialchars($user['email']) ?>)</li>
<?php endforeach; ?>
</ul>

19
app/auth_user.php Normal file
View File

@@ -0,0 +1,19 @@
<?php
// Headers
header('Access-Control-Allow-Origin: *');
header('Content-Type: application/json');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Access-Control-Allow-Headers,Content-Type,Access-Control-Allow-Methods, Authorization, X-Requested-With');
include_once '../models/Auth.php';
include_once '../config/Database.php';
//require_once("../getrequestheaders.php");
$database = new Database();
$db = $database->connect();
$auth = new Auth($db);
$retval = $auth->read_api_auth();
var_dump($retval);

69
app/disbursement.php Normal file
View File

@@ -0,0 +1,69 @@
<?php
// Headersa
header('Access-Control-Allow-Origin: *');
header('Content-Type: application/json');
header('Access-Control-Allow-Methods: POST');
// header('Access-Control-Allow-Headers: Access-Control-Allow-Headers, Content-Type,Access-Control-Allow-Methods, Authorization, X-Requested-With');
include_once 'models/Auth.php';
include_once 'config/Database.php';
include_once 'models/Disbursement.php';
$database = new Database();
$db = $database->connect();
$auth = new Auth($db);
$retval = $auth->read_api_auth();
$data = json_decode(file_get_contents("php://input") , true);
@file_put_contents("logs/" . date("Y_m_d_") . "contact_centre_electricity_purchase_requests.txt", json_encode($data) . PHP_EOL, FILE_APPEND);
if ($retval == false) {
http_response_code(401);
echo json_encode(["status" => "fail", "message" => "Unauthorised Access"]);
exit();
}
$requested_keys = ['transaction_id', 'reference_id', 'mobile_number', 'amount'];
$missing = [];
$missing = array_diff_key(array_flip($requested_keys), $data);
if (count($missing) > 0) {
$missing_string = implode(", ", array_flip($missing));
http_response_code(400);
echo json_encode(["status" => "fail", "message" => "Required parameter(s) missing : $missing_string"]);
exit();
}
// var_dump($missing);
// var_dump($data); die;
// TODO: check if transaction ID matches our record in DB
$result = varifyTransaction($data['transaction_id'], $data['mobile_number']);
// var_dump($result);
if ($result == false) {
http_response_code(200);
echo json_encode(["status" => "fail", "message" => "Transaction ID not found"]);
exit();
}
if($result !== false) {
// TODO: send request to ultima to purchase electricty
$result = processDisbursement($data['transaction_id'], $data['mobile_number'], $data['amount'], $data['reference_id']);
if ($electricity_token !== false) {
http_response_code(200);
echo json_encode(["status" => "success", "reference_id" => $data['reference_id']]);
exit();
}
http_response_code(200);
echo json_encode(["status" => "fail", "reference_id" => $data['reference_id'], "message" => "Disbursement could not be processed at this time"]);
exit();
}
else {
http_response_code(200);
echo json_encode(["status" => "fail", "reference_id" => $data['reference_id'], "message" => "Disbursement failed"]);
exit();
}

View File

@@ -0,0 +1,88 @@
<?php
defined('SMS_API_KEY') OR define("SMS_API_KEY", "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI1NDciLCJvaWQiOjU0NywidWlkIjoiNmFmMmMyZDktZTIwZS00YmYwLTg4ZTgtOGEwMzY0YmU5YTAyIiwiYXBpZCI6MzM1LCJpYXQiOjE3MTIyMjcyNDcsImV4cCI6MjA1MjIyNzI0N30.CG5VW2FU18yx-1OUMtPqFMce06LFUZwai-ecdmb79Ls67T7k4L7RuinSluZMWn1epP883ongI-E5fklSBVaEvQ");
function httpPostNew($params){
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => 'http://206.225.81.36:8989/api/messaging/sendsms',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => $params,
CURLOPT_HTTPHEADER => array(
'Content-Type: application/json',
'Authorization: Bearer ' . SMS_API_KEY
),
));
$response = curl_exec($curl);
curl_close($curl);
return $response;
}
function slackCurl($message){
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => "https://hooks.slack.com/services/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",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => $message,
CURLOPT_HTTPHEADER => array(
"cache-control: no-cache",
"content-type: application/json"
),
));
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
return "cURL Error #:" . $err;
} else {
return $response;
}
}
function sendSlack($message){
$retval = array('text' => $message);
$response = slackCurl(json_encode($retval));
return $response;
}
function sendNtfy($data){
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_HTTPHEADER => array(
'Content-Type: application/json'
),
CURLOPT_URL => 'https://ntfy.sh/bpc_rest_api',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => $data
));
$response = curl_exec($curl);
return $response;
}
?>

17
check.php Normal file
View File

@@ -0,0 +1,17 @@
<?php
if(strcasecmp($_SERVER['REQUEST_METHOD'], 'POST') != 0){
http_response_code(400);
echo json_encode(["status" => "fail", "message" => "Request method must be POST"]);
exit();
}
$contentType = isset($_SERVER["CONTENT_TYPE"]) ? trim($_SERVER["CONTENT_TYPE"]) : '';
if(strcasecmp($contentType, 'application/json') != 0){
http_response_code(400);
echo json_encode(["status" => "fail", "message" => "Content type must be: application/json"]);
exit();
}
?>

14
composer.json Normal file
View File

@@ -0,0 +1,14 @@
{
"name": "click-mobile/mse-api",
"description": "Api service for the main application",
"type": "project",
"require": {
"php": ">=7.4",
"vlucas/phpdotenv": "^5.5"
},
"autoload": {
"psr-4": {
"App\\": "app/"
}
}
}

494
composer.lock generated Normal file
View File

@@ -0,0 +1,494 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "3bc14ffd1efccc6146ffadfb721bd3c1",
"packages": [
{
"name": "graham-campbell/result-type",
"version": "v1.1.4",
"source": {
"type": "git",
"url": "https://github.com/GrahamCampbell/Result-Type.git",
"reference": "e01f4a821471308ba86aa202fed6698b6b695e3b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b",
"reference": "e01f4a821471308ba86aa202fed6698b6b695e3b",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.5"
},
"require-dev": {
"phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7"
},
"type": "library",
"autoload": {
"psr-4": {
"GrahamCampbell\\ResultType\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "An Implementation Of The Result Type",
"keywords": [
"Graham Campbell",
"GrahamCampbell",
"Result Type",
"Result-Type",
"result"
],
"support": {
"issues": "https://github.com/GrahamCampbell/Result-Type/issues",
"source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type",
"type": "tidelift"
}
],
"time": "2025-12-27T19:43:20+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.5",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/php-option.git",
"reference": "75365b91986c2405cf5e1e012c5595cd487a98be"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be",
"reference": "75365b91986c2405cf5e1e012c5595cd487a98be",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "1.9-dev"
}
},
"autoload": {
"psr-4": {
"PhpOption\\": "src/PhpOption/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Johannes M. Schmitt",
"email": "schmittjoh@gmail.com",
"homepage": "https://github.com/schmittjoh"
},
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "Option Type for PHP",
"keywords": [
"language",
"option",
"php",
"type"
],
"support": {
"issues": "https://github.com/schmittjoh/php-option/issues",
"source": "https://github.com/schmittjoh/php-option/tree/1.9.5"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption",
"type": "tidelift"
}
],
"time": "2025-12-27T19:41:33+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": ">=7.2"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-12-23T08:48:59+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
"reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-01-02T08:10:11+00:00"
},
{
"name": "vlucas/phpdotenv",
"version": "v5.6.3",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
"reference": "955e7815d677a3eaa7075231212f2110983adecc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc",
"reference": "955e7815d677a3eaa7075231212f2110983adecc",
"shasum": ""
},
"require": {
"ext-pcre": "*",
"graham-campbell/result-type": "^1.1.4",
"php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.5",
"symfony/polyfill-ctype": "^1.26",
"symfony/polyfill-mbstring": "^1.26",
"symfony/polyfill-php80": "^1.26"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"ext-filter": "*",
"phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2"
},
"suggest": {
"ext-filter": "Required to use the boolean validator."
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "5.6-dev"
}
},
"autoload": {
"psr-4": {
"Dotenv\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Vance Lucas",
"email": "vance@vancelucas.com",
"homepage": "https://github.com/vlucas"
}
],
"description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
"keywords": [
"dotenv",
"env",
"environment"
],
"support": {
"issues": "https://github.com/vlucas/phpdotenv/issues",
"source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv",
"type": "tidelift"
}
],
"time": "2025-12-27T19:49:13+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=8.0"
},
"platform-dev": {},
"plugin-api-version": "2.9.0"
}

14
example.htaccess Normal file
View File

@@ -0,0 +1,14 @@
RewriteEngine On
#RewriteBase /mse_api/
# 1. Prevent directory browsing
Options -Indexes
# 2. If the request is NOT a real directory...
RewriteCond %{REQUEST_FILENAME} !-d
# 3. ...and is NOT a real file...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
# 4. ...send the request to index.php
RewriteRule ^ public/index.php [L]

22
getrequestheaders.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
if(!function_exists('getallheaders')){
http_response_code(403);
echo "Access Denied 900";
exit();
}
$headers = [];
foreach (getallheaders() as $name => $value) {
// echo "$name: $value <br>" . PHP_EOL;
$headers[$name] = $value;
}
list($type, $bearer_data) = explode(" ", $headers['Authorization'], 2);
$retval = $auth->read_api_auth($bearer_data);
if ($retval == null) {
echo json_encode(['Access Denied']);
}
?>

23
info.md Normal file
View File

@@ -0,0 +1,23 @@
Hi Kwesi,
This is what we agreed on the mse works:
You will work on disbursement confirmation endpoint
1. To sit on the mobile money server
2. Db is on .201 and called malawi_stock_exchange
3. The endpoint to accept our transid,their unique reference id, mobile number, amount, among others in the payload
4. The endpoint to respond back with status of the transaction
5. You should have another endpoint for transaction enquiry
6. This will just refer to the transaction enquiry service for disbursements
7. To accept the transID and unique reference in the payload
8. To return TS of TF to mean transaction successful or transaction failed respectively
9. All MSE projects are found on the mobile money server in this path:
10. Airtel Money: /var/www/payments.click-mobile.com/html/mse/airtelmoney
11. Mpamba: /var/www/payments.click-mobile.com/html/mse/mpamba
13. API - /var/www/payments.click-mobile.com/html/mse/api
14. http://216.55.185.131/
15. www.payments.click-mobile.com/api
Would you like to see how to define a global constant for this base URL so you can easily link to images and CSS from your templates?

View File

@@ -0,0 +1,50 @@
<?php
/*
Developer: David Kumwenda
Contact: 0881161942 or 0996139030
App: Mpamba 4 MSE
Date: 30 August 2025
Duration: 1 day dev work*/
// Read raw POST data
$rawData = file_get_contents("php://input");
// Decode JSON
$data = json_decode($rawData, true);
// Basic logging (optional, useful for debugging)
file_put_contents("logs/callback_log_".date('Ymd').'.txt', date("Y-m-d H:i:s") . " - " . $rawData . PHP_EOL, FILE_APPEND); exit();
// Validate required fields
if (isset($data["receipt_number"], $data["result_code"], $data["transaction_id"])) {
$receiptNumber = $data["receipt_number"];
$resultCode = $data["result_code"];
$resultDescription = $data["result_description"] ?? "";
$resultTime = $data["result_time"] ?? date("Y-m-d H:i:s");
$transactionId = $data["transaction_id"];
$success = $data["success"] ?? false;
// Example: Save to database
// (Replace this with your actual DB insert/update code)
/*
$conn = mysqli_connect("localhost", "user", "password", "dbname");
$stmt = mysqli_prepare($conn, "INSERT INTO payments (transaction_id, receipt_number, result_code, result_description, result_time, success) VALUES (?, ?, ?, ?, ?, ?)");
mysqli_stmt_bind_param($stmt, "ssissi", $transactionId, $receiptNumber, $resultCode, $resultDescription, $resultTime, $success);
mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
mysqli_close($conn);
*/
// Respond with 200 OK to acknowledge receipt
http_response_code(200);
echo json_encode(["status" => "callback received"]);
} else {
// Missing required fields
http_response_code(400);
echo json_encode(["error" => "Invalid callback payload"]);
}
?>

View File

@@ -0,0 +1,170 @@
<?php
$clientID="94351d4d-4909-4056-ad9d-8052a332d6b9";
$clientSecret="bf665590-2519-49af-8d1f-7cd0dce1dc7a";
//CONTINENTAL CAPITAL
$clientID="9ff18a6d-331e-4ec5-9ecc-4e512e13747c";
$clientSecret="40f44254-10e7-4eb8-b161-38125117f4ba";
$authURL="https://openapiuat.airtel.africa/auth/oauth2/token";
$res=authenticate($authURL, $clientID, $clientSecret);
if($res['success']){
$bearerToken=$res['token'];
$country = "MW";
$currency = "MWK";
//enquire trans status
$res=getAirtelBalance($country, $currency, $bearerToken);
if ($res["status"] === "SUCCESS") {
echo "Balance: {$res['balance']} {$res['currency']}\n";
echo "Account Status: {$res['account_status']}";
} else {
echo "Error: " . $res["message"];
}
}else{
echo(print_r($res,true));
exit();
}
function getAirtelBalance($country, $currency, $token) {
$url = "https://openapiuat.airtel.africa/standard/v1/users/balance";
// Initialize cURL
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Accept: application/json",
"X-Country: $country",
"X-Currency: $currency",
"Authorization: Bearer $token"
],
]);
$response = curl_exec($curl);
if (curl_errno($curl)) {
$error = curl_error($curl);
curl_close($curl);
return [
"status" => "ERROR",
"message" => "cURL Error: $error"
];
}
curl_close($curl);
// Decode response
$result = json_decode($response, true);
// Handle invalid JSON
if (!$result) {
return [
"status" => "ERROR",
"message" => "Invalid JSON response from Airtel API"
];
}
// Check for API structure
if (!isset($result["status"])) {
return [
"status" => "ERROR",
"message" => "Unexpected API response format".print_r($result,true)
];
}
$statusCode = $result["status"]["code"] ?? null;
$message = $result["status"]["message"] ?? "Unknown error";
// ✅ SUCCESS case
if ($statusCode == "200" && isset($result["data"])) {
return [
"status" => "SUCCESS",
"balance" => $result["data"]["balance"] ?? "0",
"currency" => $result["data"]["currency"] ?? $currency,
"account_status" => $result["data"]["account_status"] ?? "Unknown"
];
}
// ❌ ERROR case (e.g. "User not found", "Invalid token", etc.)
return [
"status" => "ERROR",
"message" => $message,
"code" => $statusCode,
"responseCode" => $result["status"]["response_code"] ?? null,
"resultCode" => $result["status"]["result_code"] ?? null
];
}
function authenticate($baseURL, $wallet, $password)
{
// JSON payload
$postData = json_encode([
'client_id' => $wallet,
'client_secret' => $password,
'grant_type' => "client_credentials"
]);
// Initialize cURL
$ch = curl_init($baseURL);
// Set cURL options
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Return the response
curl_setopt($ch, CURLOPT_POST, true); // Use POST method
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);// Set the request body
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . strlen($postData)
]);
// Execute the request
$response = curl_exec($ch);
// Check for cURL errors
if (curl_errno($ch)) {
curl_close($ch);
return [
'success' => false,
'error' => 'Curl error: ' . curl_error($ch)
];
}
// Get HTTP status code
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Decode JSON response
$result = json_decode($response, true);
// Check if token is present
if ($httpCode === 200 && isset($result['access_token'])) {
return [
'success' => true,
'token' => $result['access_token']
];
} else {
return [
'success' => false,
'error' => $result['error_description'] ?? 'Unknown error',
'details' => $result['error'] ?? []
];
}
}
?>

View File

@@ -0,0 +1,323 @@
<?php
$incoming=file_get_contents("php://input");
$data=json_decode($incoming,true);
$subscriber_country = $data['country'];
$subscriber_currency = $data['currency'];
$subscriber_msisdn = $data['msisdn'];
$transaction_amount = $data['amount'];
$transaction_country = $data['country'];
$transaction_currency = $data['currency'];
$transaction_id = $data['transactionID'];
$authURL="https://openapiuat.airtel.mw/auth/oauth2/token";
/*CEDAR CAPITAL
$clientID="94351d4d-4909-4056-ad9d-8052a332d6b9";
$clientSecret="bf665590-2519-49af-8d1f-7cd0dce1dc7a";*/
//CONTINENTAL CAPITAL DISBURSEMENTS
$clientID="7f96ba01-2a74-4342-a50d-f0a36874f985";
$clientSecret="d31861d6-125e-4c0f-a0b3-2bca7af3cad0";
//CEDAR DISBURSEMENTS
//$clientID="0ffd5cef-40f6-4673-8620-9ace5c798364";
//$clientSecret="15fdc659-1431-4536-8c95-4307857464ce";
//STOCK BROKERS DISBURSEMENTS [not approved]
//$clientID="9ff18a6d-331e-4ec5-9ecc-4e512e13747c";
//$clientSecret="40f44254-10e7-4eb8-b161-38125117f4ba";
$res=authenticate($authURL, $clientID, $clientSecret);
if($res['success']){
$bearerToken=$res['token'];
echo $bearerToken;
//send a payment request
$payment = airtelDisbursement(
$bearerToken,
$subscriber_msisdn,
$transaction_amount,
'MSEMW',
'IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=',
uniqid('mse-')
);
echo (json_encode($payment));
//$data = json_decode($res, true);
/* Check if the response has a status and success flag
if (isset($data['status']['success']) && $data['status']['success'] === true) {
// Success case
$transactionId = $data['data']['transaction']['id'];
$transactionStatus = $data['data']['transaction']['status'];
$message = $data['status']['message'];
echo "✅ Transaction Successful!\n";
echo "Transaction ID: $transactionId\n";
echo "Status: $transactionStatus\n";
echo "Message: $message\n";
} else {
// Failure case
$errorCode = $data['status']['result_code'] ?? 'N/A';
$errorMessage = $data['status']['message'] ?? 'Unknown error';
echo "❌ Transaction Failed!\n";
echo "Error Code: $errorCode\n";
echo "Message: $errorMessage\n";
}
*/
}else{
echo(json_encode($res));
exit();
}
function airtelDisbursement(string $accessToken,string $msisdn,float $amount,string $reference,string $pin,string $transactionId,
string $country = 'MW',string $currency = 'MWK',int $timeout = 30) {
$url = 'https://openapiuat.airtel.mw/standard/v1/disbursements/';
$payload = [
"payee" => [
"msisdn" => $msisdn
],
"reference" => $reference,
"pin" => $pin,
"transaction" => [
"amount" => $amount,
"id" => $transactionId
]
];
file_put_contents('logs/requests.txt',json_encode($payload).PHP_EOL,FILE_APPEND);
$headers = [
'Content-Type: application/json',
'Accept: */*',
'X-Country: ' . $country,
'X-Currency: ' . $currency,
'Authorization: Bearer ' . $accessToken
];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_SSL_VERIFYPEER => true
]);
$response = curl_exec($ch);
$error = curl_error($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($error) {
return [
'success' => false,
'error' => 'CURL_ERROR',
'message' => $error
];
}
return [
'success' => ($code === 200),
'http_code' => $code,
'response' => json_decode($response, true),
'raw' => $response
];
}
function sendUSSDPush($token, $data) {
// Endpoint
$url = "https://openapiuat.airtel.africa/merchant/v1/payments/";
// Initialize cURL
$ch = curl_init($url);
// Set cURL options
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $token,
"Content-Type: application/json",
"X-Country: MW",
"X-Currency: MWK"
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
// Execute request
$response = curl_exec($ch);
// Check for cURL errors
if (curl_errno($ch)) {
echo "cURL Error: " . curl_error($ch);
curl_close($ch);
return false;
}
// Close connection
curl_close($ch);
// Decode and return response
return $response;
}
function changePassword($baseURL, $token,$newPassword, $newPasswordConfirmation) {
// Endpoint URL
$url = rtrim($baseURL, "/") . "/password";
// Prepare data
$data = [
"new_password" => $newPassword,
"new_password_confirmation" => $newPasswordConfirmation
];
// Initialize cURL
$ch = curl_init($url);
// Set cURL options
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PATCH"); // PATCH request
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $token,
"Content-Type: application/json"
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
// Execute and capture response
$response = curl_exec($ch);
// Check for errors
if (curl_errno($ch)) {
echo "cURL Error: " . curl_error($ch);
curl_close($ch);
return false;
}
// Close connection
curl_close($ch);
// Decode JSON response
return json_decode($response, true);
}
function validate_msisdn($baseURL, $msisdn, $bearerToken)
{
// Ensure proper endpoint format
$url = rtrim($baseURL, '/') . '/payments/validate/' . urlencode($msisdn);
// Initialize cURL
$ch = curl_init($url);
// Set cURL options
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPGET, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $bearerToken,
'Accept: application/json'
]);
// Execute the request
$response = curl_exec($ch);
// Handle cURL error
if (curl_errno($ch)) {
curl_close($ch);
return [
'success' => false,
'error' => 'Curl error: ' . curl_error($ch)
];
}
// Get HTTP status code
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Decode JSON response
$result = json_decode($response, true);
if ($httpCode === 200 && isset($result['data']['full_name'])) {
return [
'success' => true,
'full_name' => $result['data']['full_name']
];
} else {
return [
'success' => false,
'error' => $result['message'] ?? 'Unknown error',
'details' => $result['errors'] ?? []
];
}
}
function authenticate($baseURL, $wallet, $password)
{
// JSON payload
$postData = json_encode([
'client_id' => $wallet,
'client_secret' => $password,
'grant_type' => "client_credentials"
]);
// Initialize cURL
$ch = curl_init($baseURL);
// Set cURL options
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Return the response
curl_setopt($ch, CURLOPT_POST, true); // Use POST method
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);// Set the request body
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . strlen($postData)
]);
// Execute the request
$response = curl_exec($ch);
// Check for cURL errors
if (curl_errno($ch)) {
curl_close($ch);
return [
'success' => false,
'error' => 'Curl error: ' . curl_error($ch)
];
}
// Get HTTP status code
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Decode JSON response
$result = json_decode($response, true);
// Check if token is present
if ($httpCode === 200 && isset($result['access_token'])) {
return [
'success' => true,
'token' => $result['access_token']
];
} else {
return [
'success' => false,
'error' => $result['error_description'] ?? 'Unknown error',
'details' => $result['error'] ?? []
];
}
}
?>

View File

@@ -0,0 +1,8 @@
2025-10-24 04:47:56 - {
"transaction": {
"id": "BBZMiscxy",
"message": "Paid UGX 5,000 to TECHNOLOGIES LIMITED Charge UGX 140, Trans ID MP210603.1234.L06941.",
"status_code": "TS",
"airtel_money_id": "MP210603.1234.L06941"
}
}

View File

@@ -0,0 +1,5 @@
2025-11-07 13:14:45 -
2025-11-07 13:15:13 -
2025-11-07 13:15:31 -
2025-11-07 18:36:52 -
2025-11-07 19:00:47 -

View File

@@ -0,0 +1 @@
2026-02-12 08:45:17 - {"transaction":{"status_code":"TF","code":"DP00800001005","airtel_money_id":"null","id":"1234567919","message":"Transaction id is invalid"}}

View File

@@ -0,0 +1,44 @@
{"payee":{"msisdn":"996139030"},"reference":"MSE","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":1000,"id":"mse_69a54c4b7e054"}}
{"payee":{"msisdn":"996139030"},"reference":"MSE","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":1000,"id":"mse_69a54d2c75c62"}}
{"payee":{"msisdn":"996139030"},"reference":"MSE","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":1000,"id":"mse_69a54d3ceab6e"}}
{"payee":{"msisdn":"996139030"},"reference":"MSE","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":1000,"id":"mse_69a58de1834d8"}}
{"payee":{"msisdn":"996139030"},"reference":"MSE","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":1000,"id":"mse_69a58ed675c14"}}
{"payee":{"msisdn":"996139030"},"reference":"MSE","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":1000,"id":"mse_69a58fc2bb56d"}}
{"payee":{"msisdn":"996139030"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":1000,"id":"mse_69a5906ab20c0"}}
{"payee":{"msisdn":"996139030"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":1000,"id":"mse_69a590f316b25"}}
{"payee":{"msisdn":"996139030"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":1000,"id":"mse_69a5942d5434d"}}
{"payee":{"msisdn":"996139030"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":1000,"id":"mse_69a59b2c6a361"}}
{"payee":{"msisdn":"996139030"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":1000,"id":"mse_69a5a35dbf63e"}}
{"payee":{"msisdn":"996139030"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":1000,"id":"mse_69a9293a81d78"}}
{"payee":{"msisdn":"996139030"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":1000,"id":"mse_69a9310f9dd96"}}
{"payee":{"msisdn":"996139030"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":1000,"id":"mse_69a931d2eb29e"}}
{"payee":{"msisdn":"996139030"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":1000,"id":"mse-69aa7f7549067"}}
{"payee":{"msisdn":"996139030"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":1000,"id":"mse-69aa7ff5ea099"}}
{"payee":{"msisdn":"996139030"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":1000,"id":"mse-69aa80405eef1"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":100,"id":"mse-69aa88596095c"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":100,"id":"mse-69aa8960bca7f"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":100,"id":"mse-69aa89ab291dc"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":10,"id":"mse-69aa89ca9dd83"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":50,"id":"mse-69aa89fc03810"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":49,"id":"mse-69aa8a286793a"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":200000,"id":"mse-69aa8a5b59210"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":200.5,"id":"mse-69aa8ace57745"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":0,"id":"mse-69aa8b45944b1"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":0,"id":"mse-69aa8bd951d17"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":800000,"id":"mse-69aa8c56e961a"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":1800000,"id":"mse-69aa8d48d4725"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":1000000,"id":"mse-69aa8da5e5a46"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":1000001,"id":"mse-69aa8db2be4d7"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":100,"id":"mse-69aa8de74fa4d"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":100,"id":"mse-69aa8df1239ab"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":100,"id":"mse-69aa8effbeb72"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":50.5,"id":"mse-69aa8f3cc16fd"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":550.5,"id":"mse-69aa8fa266f85"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":50,"id":"mse-69aa90141ef14"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":50,"id":"mse-69aa90355ff2f"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":200,"id":"mse-69aa906ab50d9"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":200000,"id":"mse-69aa90d3ea868"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":1200000,"id":"mse-69aa910b28409"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":10,"id":"mse-69aa9386358f9"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":50,"id":"mse-69aa94788ea87"}}
{"payee":{"msisdn":"999959024"},"reference":"MSEMW","pin":"IGbCqXwRoiqsHTIIjxfo6vWyzPMKg6iF3+pNQK6gTXbOyJgOd1bbPuIstTcMwSAiRXOgQrkRC0+sQU5wHF33aha+AL0TevBntLzVyGl8002ZXy6Ux4Pu+zymRdlw7J6H\/PXRC2kXhbR2GIHLHlqHC49gu65OzpJ8fvpnscg1yjE=","transaction":{"amount":50,"id":"mse-69c636d71e959"}}

View File

@@ -0,0 +1,250 @@
<?php
$incoming=file_get_contents("php://input");
$data=json_decode($incoming,true);
$subscriber_country = $data['subscriber']['country'];
$subscriber_currency = $data['subscriber']['currency'];
$subscriber_msisdn = $data['subscriber']['msisdn'];
$transaction_amount = $data['transaction']['amount'];
$transaction_country = $data['transaction']['country'];
$transaction_currency = $data['transaction']['currency'];
$transaction_id = $data['transaction']['id'];
$authURL="https://openapi.airtel.mw/auth/oauth2/token";
/*CEDAR CAPITAL
$clientID="94351d4d-4909-4056-ad9d-8052a332d6b9";
$clientSecret="bf665590-2519-49af-8d1f-7cd0dce1dc7a";*/
//CONTINENTAL CAPITAL UAT
$clientID="9ff18a6d-331e-4ec5-9ecc-4e512e13747c";
$clientSecret="40f44254-10e7-4eb8-b161-38125117f4ba";
//CONTINENTAL CAPITAL PROD
$clientID="37063c29-d090-4133-8a6c-6f3295d7c2a9";
$clientSecret="6502ff2d-be8d-485f-8fa4-d03e2d8ed11d";
$res=authenticate($authURL, $clientID, $clientSecret);
if($res['success']){
$bearerToken=$res['token'];
//send a ussd push
$res=sendUSSDPush($bearerToken, $data);
$data = json_decode($res, true);
// Check if the response has a status and success flag
if (isset($data['status']['success']) && $data['status']['success'] === true) {
// Success case
$transactionId = $data['data']['transaction']['id'];
$transactionStatus = $data['data']['transaction']['status'];
$message = $data['status']['message'];
echo "✅ Transaction Successful!\n";
echo "Transaction ID: $transactionId\n";
echo "Status: $transactionStatus\n";
echo "Message: $message\n";
} else {
// Failure case
$errorCode = $data['status']['result_code'] ?? 'N/A';
$errorMessage = $data['status']['message'] ?? 'Unknown error';
echo "❌ Transaction Failed!\n";
echo "Error Code: $errorCode\n";
echo "Message: $errorMessage\n";
}
}else{
echo(print_r($res,true));
exit();
}
function sendUSSDPush($token, $data) {
// Endpoint
$url = "https://openapi.airtel.mw/merchant/v1/payments/";
// Initialize cURL
$ch = curl_init($url);
// Set cURL options
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $token,
"Content-Type: application/json",
"X-Country: MW",
"X-Currency: MWK"
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
// Execute request
$response = curl_exec($ch);
// Check for cURL errors
if (curl_errno($ch)) {
echo "cURL Error: " . curl_error($ch);
curl_close($ch);
return false;
}
// Close connection
curl_close($ch);
// Decode and return response
return $response;
}
function changePassword($baseURL, $token,$newPassword, $newPasswordConfirmation) {
// Endpoint URL
$url = rtrim($baseURL, "/") . "/password";
// Prepare data
$data = [
"new_password" => $newPassword,
"new_password_confirmation" => $newPasswordConfirmation
];
// Initialize cURL
$ch = curl_init($url);
// Set cURL options
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PATCH"); // PATCH request
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $token,
"Content-Type: application/json"
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
// Execute and capture response
$response = curl_exec($ch);
// Check for errors
if (curl_errno($ch)) {
echo "cURL Error: " . curl_error($ch);
curl_close($ch);
return false;
}
// Close connection
curl_close($ch);
// Decode JSON response
return json_decode($response, true);
}
function validate_msisdn($baseURL, $msisdn, $bearerToken)
{
// Ensure proper endpoint format
$url = rtrim($baseURL, '/') . '/payments/validate/' . urlencode($msisdn);
// Initialize cURL
$ch = curl_init($url);
// Set cURL options
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPGET, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $bearerToken,
'Accept: application/json'
]);
// Execute the request
$response = curl_exec($ch);
// Handle cURL error
if (curl_errno($ch)) {
curl_close($ch);
return [
'success' => false,
'error' => 'Curl error: ' . curl_error($ch)
];
}
// Get HTTP status code
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Decode JSON response
$result = json_decode($response, true);
if ($httpCode === 200 && isset($result['data']['full_name'])) {
return [
'success' => true,
'full_name' => $result['data']['full_name']
];
} else {
return [
'success' => false,
'error' => $result['message'] ?? 'Unknown error',
'details' => $result['errors'] ?? []
];
}
}
function authenticate($baseURL, $wallet, $password)
{
// JSON payload
$postData = json_encode([
'client_id' => $wallet,
'client_secret' => $password,
'grant_type' => "client_credentials"
]);
// Initialize cURL
$ch = curl_init($baseURL);
// Set cURL options
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Return the response
curl_setopt($ch, CURLOPT_POST, true); // Use POST method
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);// Set the request body
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . strlen($postData)
]);
// Execute the request
$response = curl_exec($ch);
// Check for cURL errors
if (curl_errno($ch)) {
curl_close($ch);
return [
'success' => false,
'error' => 'Curl error: ' . curl_error($ch)
];
}
// Get HTTP status code
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Decode JSON response
$result = json_decode($response, true);
// Check if token is present
if ($httpCode === 200 && isset($result['access_token'])) {
return [
'success' => true,
'token' => $result['access_token']
];
} else {
return [
'success' => false,
'error' => $result['error_description'] ?? 'Unknown error',
'details' => $result['error'] ?? []
];
}
}
?>

View File

@@ -0,0 +1,142 @@
<?php
//$requestUri = $_SERVER['REQUEST_URI'];
//$parts = explode('/', trim($requestUri, '/'));
$id = $_GET['id'];//end($parts);
//echo $id;
$enqURL="https://openapiuat.airtel.africa/standard/v1/payments/".$id;
$authURL="https://openapiuat.airtel.africa/auth/oauth2/token";
$clientID="94351d4d-4909-4056-ad9d-8052a332d6b9";
$clientSecret="bf665590-2519-49af-8d1f-7cd0dce1dc7a";
//CONTINENTAL CAPITAL
$clientID="9ff18a6d-331e-4ec5-9ecc-4e512e13747c";
$clientSecret="40f44254-10e7-4eb8-b161-38125117f4ba";
$res=authenticate($authURL, $clientID, $clientSecret);
if($res['success']){
$bearerToken=$res['token'];
//enquire trans status
$res=trans_enquiry($enqURL,$bearerToken);
echo $res;
exit();
$data = json_decode($res, true);
// Check if the response has a status and success flag
if (isset($data['status']['success']) && $data['status']['success'] === true) {
// Success case
$transactionId = $data['data']['transaction']['id'];
$transactionStatus = $data['data']['transaction']['status'];
$message = $data['status']['message'];
echo "✅ Transaction Successful!\n";
echo "Transaction ID: $transactionId\n";
echo "Status: $transactionStatus\n";
echo "Message: $message\n";
} else {
// Failure case
$errorCode = $data['status']['result_code'] ?? 'N/A';
$errorMessage = $data['status']['message'] ?? 'Unknown error';
echo "❌ Transaction Failed!\n";
echo "Error Code: $errorCode\n";
echo "Message: $errorMessage\n";
}
}else{
echo(print_r($res,true));
exit();
}
function trans_enquiry($enqURL,$token){
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => $enqURL,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => array(
'Authorization:'.$token,
'Accept: */* ',
'X-Country: MW',
'X-Currency: MWK'
),
));
$response = curl_exec($curl);
curl_close($curl);
return $response;
}
function authenticate($baseURL, $wallet, $password)
{
// JSON payload
$postData = json_encode([
'client_id' => $wallet,
'client_secret' => $password,
'grant_type' => "client_credentials"
]);
// Initialize cURL
$ch = curl_init($baseURL);
// Set cURL options
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Return the response
curl_setopt($ch, CURLOPT_POST, true); // Use POST method
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);// Set the request body
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . strlen($postData)
]);
// Execute the request
$response = curl_exec($ch);
// Check for cURL errors
if (curl_errno($ch)) {
curl_close($ch);
return [
'success' => false,
'error' => 'Curl error: ' . curl_error($ch)
];
}
// Get HTTP status code
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Decode JSON response
$result = json_decode($response, true);
// Check if token is present
if ($httpCode === 200 && isset($result['access_token'])) {
return [
'success' => true,
'token' => $result['access_token']
];
} else {
return [
'success' => false,
'error' => $result['error_description'] ?? 'Unknown error',
'details' => $result['error'] ?? []
];
}
}
?>

View File

@@ -0,0 +1,50 @@
<?php
/*
Developer: David Kumwenda
Contact: 0881161942 or 0996139030
App: Mpamba 4 MSE
Date: 30 August 2025
Duration: 1 day dev work*/
// Read raw POST data
$rawData = file_get_contents("php://input");
// Decode JSON
$data = json_decode($rawData, true);
// Basic logging (optional, useful for debugging)
file_put_contents("logs/callback_log_".date('Ymd').'.txt', date("Y-m-d H:i:s") . " - " . $rawData . PHP_EOL, FILE_APPEND);
// Validate required fields
if (isset($data["receipt_number"], $data["result_code"], $data["transaction_id"])) {
$receiptNumber = $data["receipt_number"];
$resultCode = $data["result_code"];
$resultDescription = $data["result_description"] ?? "";
$resultTime = $data["result_time"] ?? date("Y-m-d H:i:s");
$transactionId = $data["transaction_id"];
$success = $data["success"] ?? false;
// Example: Save to database
// (Replace this with your actual DB insert/update code)
/*
$conn = mysqli_connect("localhost", "user", "password", "dbname");
$stmt = mysqli_prepare($conn, "INSERT INTO payments (transaction_id, receipt_number, result_code, result_description, result_time, success) VALUES (?, ?, ?, ?, ?, ?)");
mysqli_stmt_bind_param($stmt, "ssissi", $transactionId, $receiptNumber, $resultCode, $resultDescription, $resultTime, $success);
mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
mysqli_close($conn);
*/
// Respond with 200 OK to acknowledge receipt
http_response_code(200);
echo json_encode(["status" => "callback received"]);
} else {
// Missing required fields
http_response_code(400);
echo json_encode(["error" => "Invalid callback payload"]);
}
?>

View File

@@ -0,0 +1,5 @@
<?php
$data=file_get_contents('php://input');
echo 'This is the correct path.';
?>

View File

@@ -0,0 +1 @@
{"Hi":"Hello"}

View File

@@ -0,0 +1,2 @@
2025-09-01 09:02:45 - {"receipt_number":"10887331688742698041","result_description":": Process service request successfully.","result_code":402,"result_time":"2025-09-01 11:02:44","transaction_id":"cedar6","success":false}
2025-09-01 09:03:42 - {"receipt_number":"10003173421101776517","result_description":": Process service request successfully.","result_code":402,"result_time":"2025-09-01 11:03:42","transaction_id":"cedar7","success":false}

View File

@@ -0,0 +1 @@
2025-09-08 08:05:34 - {"receipt_number":"10004158814706547506","result_description":": Process service request successfully.","result_code":402,"result_time":"2025-09-08 10:05:33","transaction_id":"cedar10","success":false}

View File

@@ -0,0 +1,15 @@
2025-09-17 09:10:09 - {"receipt_number":"CIH0000000","result_description":": Invalid Account Number","result_code":2002,"result_time":"2025-09-17 11:10:08","transaction_id":"cedar12","success":false}
2025-09-17 09:10:55 - {"receipt_number":"10216260777279973276","result_description":": Process service request successfully.","result_code":402,"result_time":"2025-09-17 11:10:55","transaction_id":"cedar11","success":false}
2025-09-17 09:21:36 - {"receipt_number":"CIH22CNO3O4","result_description":": Process service request successfully.","result_code":0,"result_time":"2025-09-17 11:21:35","transaction_id":"cedar14","success":true}
2025-09-17 09:26:28 - {"receipt_number":"CIH42CNO3O6","result_description":": Balance insufficient.","result_code":2006,"result_time":"2025-09-17 11:26:27","transaction_id":"cedar15","success":false}
2025-09-17 09:31:58 - {"receipt_number":"10395173983942501327","result_description":": Process service request successfully.","result_code":402,"result_time":"2025-09-17 11:31:57","transaction_id":"cedar16","success":false}
2025-09-17 09:35:40 - {"receipt_number":"CIH32CNO3OF","result_description":": Balance insufficient.","result_code":2006,"result_time":"2025-09-17 11:35:39","transaction_id":"cedar19","success":false}
2025-09-17 09:35:52 - {"receipt_number":"10839014874366890664","result_description":": Process service request successfully.","result_code":402,"result_time":"2025-09-17 11:35:51","transaction_id":"cedar17","success":false}
2025-09-17 09:36:25 - {"receipt_number":"10016670262346814213","result_description":": Process service request successfully.","result_code":402,"result_time":"2025-09-17 11:36:24","transaction_id":"cedar18","success":false}
2025-09-17 09:41:23 - {"receipt_number":"CIH92CNO3OL","result_description":": Balance insufficient.","result_code":2006,"result_time":"2025-09-17 11:41:22","transaction_id":"cedar22","success":false}
2025-09-17 09:41:41 - {"receipt_number":"10694042274274847334","result_description":": Process service request successfully.","result_code":402,"result_time":"2025-09-17 11:41:40","transaction_id":"cedar20","success":false}
2025-09-17 09:42:11 - {"receipt_number":"10310137165383093703","result_description":": Process service request successfully.","result_code":402,"result_time":"2025-09-17 11:42:10","transaction_id":"cedar21","success":false}
2025-09-17 09:46:38 - {"receipt_number":"CIH32CNO3OP","result_description":": Process service request successfully.","result_code":0,"result_time":"2025-09-17 11:46:37","transaction_id":"cedar23","success":true}
2025-09-17 09:49:16 - {"receipt_number":"CIH0000000","result_description":": Amount invalid.","result_code":2004,"result_time":"2025-09-17 11:49:15","transaction_id":"cedar24","success":false}
2025-09-17 09:52:11 - {"receipt_number":"10716865356500456126","result_description":": Process service request successfully.","result_code":402,"result_time":"2025-09-17 11:52:10","transaction_id":"cedar25","success":false}
2025-09-17 09:54:17 - {"receipt_number":"10988964508151890908","result_description":": Process service request successfully.","result_code":402,"result_time":"2025-09-17 11:54:17","transaction_id":"cedar26","success":false}

View File

@@ -0,0 +1,2 @@
2025-11-19 11:35:02 - {"receipt_number":"10478257471611622849","result_description":": Process service request successfully.","result_code":402,"result_time":"2025-11-19 13:34:56","transaction_id":"12345666","success":false}
2025-11-19 11:46:47 - {"receipt_number":"10847714824278927203","result_description":": Process service request successfully.","result_code":402,"result_time":"2025-11-19 13:46:41","transaction_id":"12345667","success":false}

View File

@@ -0,0 +1,2 @@
2025-11-26 11:22:08 - {"receipt_number":"10342708118343667220","result_description":": Process service request successfully.","result_code":402,"result_time":"2025-11-26 13:22:02","transaction_id":"kiddo2","success":false}
2025-11-26 11:25:22 - {"receipt_number":"10498981254538029140","result_description":": Process service request successfully.","result_code":402,"result_time":"2025-11-26 13:25:15","transaction_id":"kiddo3","success":false}

View File

@@ -0,0 +1,14 @@
2025-12-18 07:59:16 - {"receipt_number":"10225876409109341761","result_description":": Process service request successfully.","result_code":402,"result_time":"2025-12-18 09:59:15","transaction_id":"12345771","success":false}
2025-12-18 08:08:28 - {"receipt_number":"CLI0000000","result_description":": Invalid Account Number","result_code":2002,"result_time":"2025-12-18 10:08:28","transaction_id":"12345772","success":false}
2025-12-18 08:16:03 - {"receipt_number":"CLI72HP9KFT","result_description":": Process service request successfully.","result_code":0,"result_time":"2025-12-18 10:16:02","transaction_id":"12345773","success":true}
2025-12-18 08:41:39 - {"receipt_number":"CLI42HP9KG0","result_description":": Process service request successfully.","result_code":0,"result_time":"2025-12-18 10:41:38","transaction_id":"12345775","success":true}
2025-12-18 08:42:12 - {"receipt_number":"10615975978933751017","result_description":": Process service request successfully.","result_code":402,"result_time":"2025-12-18 10:42:11","transaction_id":"12345774","success":false}
2025-12-18 09:04:18 - {"receipt_number":"CLI02HP9KG6","result_description":": Process service request successfully.","result_code":0,"result_time":"2025-12-18 11:04:17","transaction_id":"12345778","success":true}
2025-12-18 09:05:26 - {"receipt_number":"10025084431939463372","result_description":": Process service request successfully.","result_code":402,"result_time":"2025-12-18 11:05:25","transaction_id":"12345777","success":false}
2025-12-18 09:08:56 - {"receipt_number":"CLI22HP9KG8","result_description":": Balance insufficient.","result_code":2006,"result_time":"2025-12-18 11:08:56","transaction_id":"12345779","success":false}
2025-12-18 09:11:53 - {"receipt_number":"CLI52HP9KGB","result_description":": Process service request successfully.","result_code":0,"result_time":"2025-12-18 11:11:52","transaction_id":"12345780","success":true}
2025-12-18 09:13:41 - {"receipt_number":"CLI72HP9KGD","result_description":": Rule limited.","result_code":2005,"result_time":"2025-12-18 11:13:40","transaction_id":"12345781","success":false}
2025-12-18 09:15:34 - {"receipt_number":"CLI92HP9KGF","result_description":": Balance insufficient.","result_code":2006,"result_time":"2025-12-18 11:15:34","transaction_id":"12345782","success":false}
2025-12-18 09:16:48 - {"receipt_number":"CLI22HP9KGI","result_description":": Balance insufficient.","result_code":2006,"result_time":"2025-12-18 11:16:48","transaction_id":"12345783","success":false}
2025-12-18 09:27:41 - {"receipt_number":"10280516611960183870","result_description":": Process service request successfully.","result_code":402,"result_time":"2025-12-18 11:27:40","transaction_id":"12345785","success":false}
2025-12-18 09:33:07 - {"receipt_number":"10729630476149503786","result_description":": Process service request successfully.","result_code":402,"result_time":"2025-12-18 11:33:06","transaction_id":"12345786","success":false}

View File

@@ -0,0 +1 @@
2026-01-29 23:40:42 - {"receipt_number":"10230529140546671414","result_description":": Process service request successfully.","result_code":402,"result_time":"2026-01-30 01:40:42","transaction_id":"12345789","success":false}

View File

@@ -0,0 +1,2 @@
2026-01-30 00:41:32 - {"receipt_number":"10369576091684466032","result_description":": Process service request successfully.","result_code":402,"result_time":"2026-01-30 02:41:31","transaction_id":"10388103","success":false}
2026-01-30 00:59:38 - {"receipt_number":"10345565245761348404","result_description":": Process service request successfully.","result_code":402,"result_time":"2026-01-30 02:59:37","transaction_id":"10389339","success":false}

View File

@@ -0,0 +1 @@
2026-02-03 00:00:53 - {"receipt_number":"10931861926510111848","result_description":": Process service request successfully.","result_code":402,"result_time":"2026-02-03 02:00:52","transaction_id":"3805907571","success":false}

View File

@@ -0,0 +1,311 @@
<?php
$incoming=file_get_contents("php://input");
$details=json_decode($incoming,true);
$transactionId=$details['transactionId'];
$amount=$details['amount'];
$msisdn=$details['msisdn'];
$description=$details['description'];
$baseURL="https://devpayouts.tnmmpamba.co.mw/api";
$wallet="505073";
$password="N8O7L0vpl5mflfwHzf4DSle9bToV*";//CEDAR PASSWORD & All other wallets
$res=authenticate($baseURL, $wallet, $password);
//echo print_r($res,true);
$bearerToken=$res['token'];//"102164|newoEd6QOFaTptUiGAp232jULtUmgMtaX1x2CRww4Ka2270dc49";
//echo $bearerToken;
//$res=validate_msisdn($baseURL, '265881161942', $bearerToken);
//$res=changePassword($baseURL, $bearerToken, "N8O7L0vpl5mflfwHzf4DSle9bToV*", "N8O7L0vpl5mflfwHzf4DSle9bToV*");
//$res=sendUSSDPush($baseURL, $bearerToken, $invoiceNumber, $amount, $msisdn, $description);
$result=makePayment($baseURL,$bearerToken,$msisdn, $amount, $transactionId, $description);
echo (print_r($result,true));
if (!isset($result['http_code'])) {
// Network or cURL failure
echo "SYSTEM ERROR: " . $result['message'];
exit;
}
switch ($result['http_code']) {
case 200:
// Completed successfully
$transID = $result['response']['data']['transaction_id'] ?? '';
$receipt = $result['response']['data']['receipt_number'] ?? '';
echo "SUCCESS: Payment completed. Transaction ID: $transID Receipt: " . $receipt;
//mark transaction as SUCCESSFUL in db later
break;
case 202:
// Accepted but pending
echo "PENDING: Transaction accepted and processing.";
//mark transaction as PENDING / SUSPENSE in the db
break;
case 400:
// Invalid request
echo "FAILED: Bad request. Check parameters.";
//mark transaction as FAILED in the db
break;
case 503:
// Service unavailable
echo "FAILED: Service unavailable. Try again later.";
//mark transaction as FAILED or RETRY later after setting a retry cron
break;
default:
echo "UNKNOWN RESPONSE: " . $result['http_code'];
//unkown response, do nothing
}
function makePayment($baseURL,$bearerToken,$msisdn, $amount, $transactionId, $description)
{
$url = $baseURL . "/payments";
// Request payload
$payload = array(
"msisdn" => $msisdn,
"amount" => (int)$amount,
"transaction_id" => $transactionId,
"narration" => $description
);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
"Content-Type: application/json",
"Authorization: Bearer " . $bearerToken
));
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
if ($response === false) {
$error = curl_error($ch);
curl_close($ch);
return array(
"status" => "ERROR",
"message" => "cURL error: " . $error
);
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$responseData = json_decode($response, true);
return array(
"http_code" => $httpCode,
"response" => $responseData
);
}
function sendUSSDPush($baseURL, $token, $invoiceNumber, $amount, $msisdn, $description) {
// Endpoint
$url = rtrim($baseURL, "/") . "/invoices";
// Prepare payload
$data = [
"invoice_number" => $invoiceNumber,
"amount" => (int)$amount,
"msisdn" => $msisdn,
"description" => $description
];
// Initialize cURL
$ch = curl_init($url);
// Set cURL options
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $token,
"Content-Type: application/json"
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
// Execute request
$response = curl_exec($ch);
// Check for cURL errors
if (curl_errno($ch)) {
echo "cURL Error: " . curl_error($ch);
curl_close($ch);
return false;
}
// Close connection
curl_close($ch);
// Decode and return response
return $response;//json_decode($response, true);
}
function changePassword($baseURL, $token,$newPassword, $newPasswordConfirmation) {
// Endpoint URL
$url = rtrim($baseURL, "/") . "/password";
// Prepare data
$data = [
"new_password" => $newPassword,
"new_password_confirmation" => $newPasswordConfirmation
];
// Initialize cURL
$ch = curl_init($url);
// Set cURL options
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PATCH"); // PATCH request
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $token,
"Content-Type: application/json"
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
// Execute and capture response
$response = curl_exec($ch);
// Check for errors
if (curl_errno($ch)) {
echo "cURL Error: " . curl_error($ch);
curl_close($ch);
return false;
}
// Close connection
curl_close($ch);
// Decode JSON response
return json_decode($response, true);
}
function validate_msisdn($baseURL, $msisdn, $bearerToken)
{
// Ensure proper endpoint format
$url = rtrim($baseURL, '/') . '/payments/validate/' . urlencode($msisdn);
// Initialize cURL
$ch = curl_init($url);
// Set cURL options
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPGET, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $bearerToken,
'Accept: application/json'
]);
// Execute the request
$response = curl_exec($ch);
// Handle cURL error
if (curl_errno($ch)) {
curl_close($ch);
return [
'success' => false,
'error' => 'Curl error: ' . curl_error($ch)
];
}
// Get HTTP status code
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Decode JSON response
$result = json_decode($response, true);
if ($httpCode === 200 && isset($result['data']['full_name'])) {
return [
'success' => true,
'full_name' => $result['data']['full_name']
];
} else {
return [
'success' => false,
'error' => $result['message'] ?? 'Unknown error',
'details' => $result['errors'] ?? []
];
}
}
function authenticate($baseURL, $wallet, $password)
{
// Endpoint URL
$url = rtrim($baseURL, '/') . '/authenticate';
// JSON payload
$postData = json_encode([
'wallet' => $wallet,
'password' => $password
]);
// Initialize cURL
$ch = curl_init($url);
// Set cURL options
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Return the response
curl_setopt($ch, CURLOPT_POST, true); // Use POST method
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);// Set the request body
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . strlen($postData)
]);
// Execute the request
$response = curl_exec($ch);
// Check for cURL errors
if (curl_errno($ch)) {
curl_close($ch);
return [
'success' => false,
'error' => 'Curl error: ' . curl_error($ch)
];
}
// Get HTTP status code
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Decode JSON response
$result = json_decode($response, true);
// Check if token is present
if ($httpCode === 200 && isset($result['data']['token'])) {
return [
'success' => true,
'token' => $result['data']['token'],
'expires_at' => $result['data']['expires_at']
];
} else {
return [
'success' => false,
'error' => $result['message'] ?? 'Unknown error',
'details' => $result['errors'] ?? []
];
}
}
?>

View File

@@ -0,0 +1,147 @@
<?php
$incoming=file_get_contents("php://input");
$details=json_decode($incoming,true);
$transactionId=$details['transactionId'];
//$token=$details['token'];
$baseURL="https://devpayouts.tnmmpamba.co.mw/api";
$wallet="505073";
$password="N8O7L0vpl5mflfwHzf4DSle9bToV*";//CEDAR PASSWORD & All other wallets
$res=authenticate($baseURL, $wallet, $password);
//echo print_r($res,true);
$bearerToken=$res['token'];
$res=checkPaymentStatus($transactionId, $bearerToken, $baseURL);
echo(print_r($res,true));
function checkPaymentStatus($transactionId, $bearerToken, $baseURL)
{
$url = rtrim($baseURL, '/') . "/payments/" . urlencode($transactionId);
$ch = curl_init();
curl_setopt_array($ch, array(
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => array(
"Authorization: Bearer " . $bearerToken,
"Accept: application/json"
),
CURLOPT_TIMEOUT => 30
));
$response = curl_exec($ch);
if ($response === false) {
$error = curl_error($ch);
curl_close($ch);
return array(
"status" => "ERROR",
"message" => "cURL error: " . $error
);
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$decoded = json_decode($response, true);
if ($httpCode !== 200 || !$decoded) {
return array(
"status" => "FAILED",
"http_code" => $httpCode,
"raw" => $response
);
}
// Safely extract data
$data = $decoded['data'] ?? [];
$success = $data['success'] ?? false;
$resultCode = $data['result_code'] ?? null;
$receipt = $data['receipt_number'] ?? null;
// Determine final status
if ($success === true && $resultCode === "0") {
$finalStatus = "SUCCESS";
} else {
$finalStatus = "PENDING"; // default fallback
}
return array(
"status" => $finalStatus,
"transaction_id" => $data['transaction_id'] ?? $transactionId,
"receipt_number" => $receipt,
"result_code" => $resultCode,
"result_description" => $data['result_description'] ?? null,
"created_at" => $data['created_at'] ?? null,
"message" => $decoded['message'] ?? null,
"raw_response" => $decoded
);
}
function authenticate($baseURL, $wallet, $password)
{
$url = rtrim($baseURL, '/') . '/authenticate';
$postData = json_encode([
'wallet' => $wallet,
'password' => $password
]);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Return the response
curl_setopt($ch, CURLOPT_POST, true); // Use POST method
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);// Set the request body
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . strlen($postData)
]);
// Execute the request
$response = curl_exec($ch);
// Check for cURL errors
if (curl_errno($ch)) {
curl_close($ch);
return [
'success' => false,
'error' => 'Curl error: ' . curl_error($ch)
];
}
// Get HTTP status code
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Decode JSON response
$result = json_decode($response, true);
// Check if token is present
if ($httpCode === 200 && isset($result['data']['token'])) {
return [
'success' => true,
'token' => $result['data']['token'],
'expires_at' => $result['data']['expires_at']
];
} else {
return [
'success' => false,
'error' => $result['message'] ?? 'Unknown error',
'details' => $result['errors'] ?? []
];
}
}
?>

View File

@@ -0,0 +1,216 @@
<?php
$incoming=file_get_contents("php://input");
$details=json_decode($incoming,true);
$invoiceNumber=$details['invoiceNumber'];
$amount=$details['amount'];
$msisdn=$details['msisdn'];
$description=$details['description'];
$baseURL="https://devpayouts.tnmmpamba.co.mw/api";
$wallet="505072";
$password="N8O7L0vpl5mflfwHzf4DSle9bToV*";//CEDAR PASSWORD & All other wallets
$res=authenticate($baseURL, $wallet, $password);
echo print_r($res,true);
$bearerToken=$res['token'];//"102164|newoEd6QOFaTptUiGAp232jULtUmgMtaX1x2CRww4Ka2270dc49";
echo $bearerToken;
//$res=validate_msisdn($baseURL, '265881161942', $bearerToken);
//$res=changePassword($baseURL, $bearerToken, "N8O7L0vpl5mflfwHzf4DSle9bToV*", "N8O7L0vpl5mflfwHzf4DSle9bToV*");
$res=sendUSSDPush($baseURL, $bearerToken, $invoiceNumber, $amount, $msisdn, $description);
echo (print_r($res,true));
function sendUSSDPush($baseURL, $token, $invoiceNumber, $amount, $msisdn, $description) {
// Endpoint
$url = rtrim($baseURL, "/") . "/invoices";
// Prepare payload
$data = [
"invoice_number" => $invoiceNumber,
"amount" => (int)$amount,
"msisdn" => $msisdn,
"description" => $description
];
// Initialize cURL
$ch = curl_init($url);
// Set cURL options
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $token,
"Content-Type: application/json"
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
// Execute request
$response = curl_exec($ch);
// Check for cURL errors
if (curl_errno($ch)) {
echo "cURL Error: " . curl_error($ch);
curl_close($ch);
return false;
}
// Close connection
curl_close($ch);
// Decode and return response
return $response;//json_decode($response, true);
}
function changePassword($baseURL, $token,$newPassword, $newPasswordConfirmation) {
// Endpoint URL
$url = rtrim($baseURL, "/") . "/password";
// Prepare data
$data = [
"new_password" => $newPassword,
"new_password_confirmation" => $newPasswordConfirmation
];
// Initialize cURL
$ch = curl_init($url);
// Set cURL options
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PATCH"); // PATCH request
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $token,
"Content-Type: application/json"
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
// Execute and capture response
$response = curl_exec($ch);
// Check for errors
if (curl_errno($ch)) {
echo "cURL Error: " . curl_error($ch);
curl_close($ch);
return false;
}
// Close connection
curl_close($ch);
// Decode JSON response
return json_decode($response, true);
}
function validate_msisdn($baseURL, $msisdn, $bearerToken)
{
// Ensure proper endpoint format
$url = rtrim($baseURL, '/') . '/payments/validate/' . urlencode($msisdn);
// Initialize cURL
$ch = curl_init($url);
// Set cURL options
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPGET, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $bearerToken,
'Accept: application/json'
]);
// Execute the request
$response = curl_exec($ch);
// Handle cURL error
if (curl_errno($ch)) {
curl_close($ch);
return [
'success' => false,
'error' => 'Curl error: ' . curl_error($ch)
];
}
// Get HTTP status code
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Decode JSON response
$result = json_decode($response, true);
if ($httpCode === 200 && isset($result['data']['full_name'])) {
return [
'success' => true,
'full_name' => $result['data']['full_name']
];
} else {
return [
'success' => false,
'error' => $result['message'] ?? 'Unknown error',
'details' => $result['errors'] ?? []
];
}
}
function authenticate($baseURL, $wallet, $password)
{
// Endpoint URL
$url = rtrim($baseURL, '/') . '/authenticate';
// JSON payload
$postData = json_encode([
'wallet' => $wallet,
'password' => $password
]);
// Initialize cURL
$ch = curl_init($url);
// Set cURL options
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Return the response
curl_setopt($ch, CURLOPT_POST, true); // Use POST method
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);// Set the request body
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . strlen($postData)
]);
// Execute the request
$response = curl_exec($ch);
// Check for cURL errors
if (curl_errno($ch)) {
curl_close($ch);
return [
'success' => false,
'error' => 'Curl error: ' . curl_error($ch)
];
}
// Get HTTP status code
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Decode JSON response
$result = json_decode($response, true);
// Check if token is present
if ($httpCode === 200 && isset($result['data']['token'])) {
return [
'success' => true,
'token' => $result['data']['token'],
'expires_at' => $result['data']['expires_at']
];
} else {
return [
'success' => false,
'error' => $result['message'] ?? 'Unknown error',
'details' => $result['errors'] ?? []
];
}
}
?>

View File

@@ -0,0 +1,60 @@
<?php
$incoming=file_get_contents("php://input");
$details=json_decode($incoming,true);
$invoiceNumber=$details['invoiceNumber'];
$token=$details['token'];
$baseURL="https://devpayouts.tnmmpamba.co.mw/api";
//$res=authenticate($baseURL, $wallet, $password);
$bearerToken=$token;//"102164|newoEd6QOFaTptUiGAp232jULtUmgMtaX1x2CRww4Ka2270dc49";
$res=checkInvoiceStatus($invoiceNumber, $bearerToken, $baseURL);
echo $res;//(print_r($res,true));
function checkInvoiceStatus($invoiceNumber, $bearerToken, $baseURL) {
// Build request URL
$url = rtrim($baseURL, '/') . "/invoices/" . urlencode($invoiceNumber);
// Initialize cURL
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . $bearerToken,
"Accept: application/json"
]
]);
// Execute the request
$response = curl_exec($ch);
// Handle errors
if (curl_errno($ch)) {
echo "cURL Error: " . curl_error($ch);
curl_close($ch);
return null;
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Check for successful response
if ($httpCode === 200 && $response) {
return $response;//json_decode($response, true);
} else {
echo "Request failed. HTTP Code: " . $httpCode . "\nResponse: " . $response;
return null;
}
}
?>

14
public/.htaccess Normal file
View File

@@ -0,0 +1,14 @@
RewriteEngine On
#RewriteBase /mse_api/public
# 1. Prevent directory browsing
Options -Indexes
# 2. If the request is NOT a real directory...
RewriteCond %{REQUEST_FILENAME} !-d
# 3. ...and is NOT a real file...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
# 4. ...send the request to index.php
RewriteRule ^ index.php [L]

30
public/index.php Normal file
View File

@@ -0,0 +1,30 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/../');
$dotenv->load();
$dotenv->required(['DB_HOST', 'DB_NAME', 'DB_USER', 'DB_PASS'])->notEmpty();
use App\Core\Router;
$router = new Router();
$router->get('/', 'HomeController@index');
$router->post('/disbursement', 'DisbursementController@index');
$router->get('/test', 'DisbursementController@testUpdate');
$request_uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$base_path = str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME']));
if ($base_path !== '/' && strpos($request_uri, $base_path) === 0) {
$request_uri = substr($request_uri, strlen($base_path));
}
$request_uri = '/' . ltrim($request_uri, '/');
$router->resolve($request_uri, $_SERVER['REQUEST_METHOD']);
?>