Initial commit

This commit is contained in:
2025-01-19 12:18:55 +00:00
commit 6fe27ccc22
20 changed files with 2467 additions and 0 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/paystack.iml" filepath="$PROJECT_DIR$/.idea/paystack.iml" />
</modules>
</component>
</project>

8
.idea/paystack.iml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

19
.idea/php.xml generated Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MessDetectorOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCSFixerOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCodeSnifferOptionsConfiguration">
<option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" />
</component>
<component name="PhpStanOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PsalmOptionsConfiguration">
<option name="transferred" value="true" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

4
config/autoload.php Normal file
View File

@ -0,0 +1,4 @@
<?php
defined('BASEPATH') or exit('No direct script access allowed');
$autoload['model'] = array('paystack_model');

240
controllers/Admin.php Normal file
View File

@ -0,0 +1,240 @@
<?php
defined('BASEPATH') or exit('No direct script access allowed');
class Admin extends AdminController
{
public function __construct()
{
parent::__construct();
$this->load->model('paystack_model');
}
/**
* Display settings page
*/
public function settings()
{
if (!has_permission('settings', '', 'view')) {
access_denied('settings');
}
if ($this->input->post()) {
if (!has_permission('settings', '', 'edit')) {
access_denied('settings');
}
$data = $this->input->post();
$success = $this->paystack_model->update_settings($data);
if ($success) {
set_alert('success', _l('settings_updated'));
}
}
$data['title'] = _l('paystack_settings');
$data['tab'] = 'settings';
$this->load->view('admin/settings', $data);
}
/**
* Display transaction logs
*/
public function transactions()
{
if (!has_permission('payments', '', 'view')) {
access_denied('payments');
}
$data['title'] = _l('paystack_transactions');
$data['tab'] = 'transactions';
// Get filters
$filter = [
'start_date' => $this->input->get('start_date'),
'end_date' => $this->input->get('end_date'),
'status' => $this->input->get('status')
];
$data['transactions'] = $this->paystack_model->get_transactions($filter);
$this->load->view('admin/transactions', $data);
}
/**
* Display test mode interface
*/
public function test_mode()
{
if (!has_permission('settings', '', 'view')) {
access_denied('settings');
}
$data['title'] = _l('paystack_test_mode');
$data['tab'] = 'test_mode';
$data['test_keys'] = $this->paystack_model->get_test_keys();
$this->load->view('admin/test_mode', $data);
}
/**
* Display payment status dashboard
*/
public function dashboard()
{
if (!has_permission('payments', '', 'view')) {
access_denied('payments');
}
$data['title'] = _l('paystack_dashboard');
$data['tab'] = 'dashboard';
// Get statistics
$data['stats'] = $this->paystack_model->get_payment_stats();
$data['recent_transactions'] = $this->paystack_model->get_recent_transactions();
$data['monthly_chart'] = $this->paystack_model->get_monthly_chart_data();
$this->load->view('admin/dashboard', $data);
}
/**
* Get transaction details (AJAX)
*/
public function get_transaction_details($reference)
{
if (!has_permission('payments', '', 'view')) {
ajax_access_denied();
}
$transaction = $this->paystack_model->get_transaction($reference);
echo json_encode($transaction);
}
/**
* Verify test webhook
*/
// public function test_webhook()
// {
// if (!has_permission('settings', '', 'view')) {
// ajax_access_denied();
// }
//
// $this->load->library('paystack_gateway');
// $result = $this->paystack_gateway->test_webhook();
//
// echo json_encode($result);
// }
/**
* Initiate test payment
*/
public function initiate_test_payment()
{
if (!has_permission('settings', '', 'view')) {
ajax_access_denied();
}
$amount = $this->input->post('amount');
$email = $this->input->post('email');
if (!$amount || !$email) {
echo json_encode([
'success' => false,
'message' => _l('invalid_input')
]);
return;
}
$reference = 'TEST_' . time() . '_' . mt_rand(1000, 9999);
echo json_encode([
'success' => true,
'reference' => $reference
]);
}
/**
* Test webhook connection
*/
public function test_webhook()
{
if (!has_permission('settings', '', 'view')) {
ajax_access_denied();
}
$this->load->library('paystack_gateway');
// Try to send a test webhook
$webhook_url = site_url('paystack/webhook');
$test_data = [
'event' => 'test',
'data' => [
'reference' => 'TEST_' . time(),
'status' => 'success'
]
];
$ch = curl_init($webhook_url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($test_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'X-Paystack-Signature: ' . hash_hmac('sha512', json_encode($test_data), $this->paystack_gateway->get_webhook_secret())
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code == 200) {
echo json_encode([
'success' => true,
'message' => _l('webhook_received_response')
]);
} else {
echo json_encode([
'success' => false,
'message' => _l('webhook_connection_failed') . ' (HTTP ' . $http_code . ')'
]);
}
}
/**
* Log debug message
*/
public function log_debug()
{
if (!has_permission('settings', '', 'view')) {
ajax_access_denied();
}
$message = $this->input->post('message');
if ($message) {
$this->paystack_model->add_debug_log($message);
}
}
/**
* Get debug log
*/
public function get_debug_log()
{
if (!has_permission('settings', '', 'view')) {
ajax_access_denied();
}
$log = $this->paystack_model->get_debug_log();
echo $log;
}
/**
* Clear debug log
*/
public function clear_debug_log()
{
if (!has_permission('settings', '', 'view')) {
ajax_access_denied();
}
$this->paystack_model->clear_debug_log();
}
}

337
controllers/Paystack.php Normal file
View File

@ -0,0 +1,337 @@
<?php
defined('BASEPATH') or exit('No direct script access allowed');
class Paystack extends App_Controller
{
public function __construct()
{
parent::__construct();
$this->load->model('invoices_model');
$this->load->model('clients_model');
$this->load->library('paystack_gateway');
$this->load->model('paystack/paystack_model');
}
/**
* Show payment form
*/
public function make_payment()
{
$payment_data = $this->session->userdata('paystack_payment_data');
if (!$payment_data) {
set_alert('danger', _l('invalid_payment'));
redirect(site_url('invoices'));
}
$invoice = $this->invoices_model->get($payment_data['invoice_id']);
if (!$invoice) {
set_alert('danger', _l('invoice_not_found'));
redirect(site_url('invoices'));
}
// Get client data
$client = $this->clients_model->get($invoice->clientid);
$data = [];
$data['invoice'] = $invoice;
$data['payment_data'] = $payment_data;
$data['client'] = $client;
$this->load->view('paystack/payment', $data);
}
/**
* Verify payment callback
*/
public function verify_payment($reference = null)
{
if (!$reference) {
redirect(site_url('clients/invoices'));
}
// Log verification attempt
$this->paystack_model->add_payment_log([
'message' => 'Verifying payment: ' . $reference,
'log_type' => 'info'
]);
$transaction = $this->paystack_gateway->verify_transaction($reference);
if ($transaction['success']) {
// Get the transaction from our database
$local_transaction = $this->paystack_model->get_transaction_by_reference($reference);
if ($local_transaction) {
// Update local transaction status
$this->db->where('id', $local_transaction['id']);
$this->db->update(db_prefix() . 'paystack_transactions', [
'status' => 'success',
'transaction_date' => date('Y-m-d H:i:s')
]);
// Record the payment in Perfex CRM
$payment_data = [
'amount' => $local_transaction['amount'],
'invoiceid' => $local_transaction['invoice_id'],
'paymentmode' => 'paystack',
'transactionid' => $reference
];
$this->load->model('payments_model');
$payment_id = $this->payments_model->add($payment_data);
if ($payment_id) {
set_alert('success', _l('payment_recorded_successfully'));
} else {
set_alert('danger', _l('payment_record_failed'));
}
}
} else {
set_alert('danger', _l('payment_failed'));
}
// Redirect to invoice
redirect(site_url('clients/invoices'));
}
/**
* Handle Paystack webhook
*/
// public function webhook()
// {
// if ((strtoupper($_SERVER['REQUEST_METHOD']) != 'POST') || !array_key_exists('HTTP_X_PAYSTACK_SIGNATURE', $_SERVER)) {
// exit();
// }
//
// $input = file_get_contents("php://input");
//
// // Verify webhook signature
// $secret_key = $this->paystack_gateway->decryptSetting('paystack_secret_key');
// if ($_SERVER['HTTP_X_PAYSTACK_SIGNATURE'] !== hash_hmac('sha512', $input, $secret_key)) {
// exit();
// }
//
// http_response_code(200);
//
// $event = json_decode($input);
//
// // Handle the event
// switch ($event->event) {
// case 'charge.success':
// $this->handle_successful_charge($event->data);
// break;
// case 'transfer.success':
// $this->handle_successful_transfer($event->data);
// break;
// case 'charge.failed':
// $this->handle_failed_charge($event->data);
// break;
// }
//
// exit();
// }
public function webhook()
{
$this->load->helper('paystack_security');
// Get payload and signature
$payload = file_get_contents('php://input');
$signature = isset($_SERVER['HTTP_X_PAYSTACK_SIGNATURE']) ? $_SERVER['HTTP_X_PAYSTACK_SIGNATURE'] : '';
// Log webhook request
$this->paystack_model->add_webhook_log([
'event' => 'webhook_received',
'payload' => $payload,
'signature' => $signature,
'status' => 'received'
]);
// Verify signature
if (!verify_paystack_webhook_signature($payload, $signature)) {
$this->paystack_model->add_webhook_log([
'event' => 'webhook_verification_failed',
'payload' => $payload,
'status' => 'failed',
'message' => 'Invalid signature'
]);
header('HTTP/1.1 401 Unauthorized');
exit();
}
// Parse payload
$event = json_decode($payload);
// Validate payload
if (!is_object($event) || !isset($event->event)) {
$this->paystack_model->add_webhook_log([
'event' => 'webhook_invalid_payload',
'payload' => $payload,
'status' => 'failed',
'message' => 'Invalid payload format'
]);
header('HTTP/1.1 400 Bad Request');
exit();
}
try {
// Process different event types
switch ($event->event) {
case 'charge.success':
$this->handle_successful_charge($event->data);
break;
case 'charge.failed':
$this->handle_failed_charge($event->data);
break;
case 'transfer.success':
$this->handle_successful_transfer($event->data);
break;
case 'transfer.failed':
$this->handle_failed_transfer($event->data);
break;
default:
// Log unknown event type
$this->paystack_model->add_webhook_log([
'event' => $event->event,
'payload' => $payload,
'status' => 'skipped',
'message' => 'Unknown event type'
]);
break;
}
header('HTTP/1.1 200 OK');
echo json_encode(['status' => 'success']);
} catch (Exception $e) {
// Log error
$this->paystack_model->add_webhook_log([
'event' => $event->event,
'payload' => $payload,
'status' => 'error',
'message' => $e->getMessage()
]);
header('HTTP/1.1 500 Internal Server Error');
echo json_encode(['status' => 'error', 'message' => 'Internal processing error']);
}
}
/**
* Handle successful charge
*/
private function handle_successful_charge($data)
{
// Validate the charge data
if (!isset($data->reference) || !isset($data->amount)) {
throw new Exception('Invalid charge data');
}
// Find the transaction
$transaction = $this->paystack_model->get_transaction_by_reference($data->reference);
if (!$transaction) {
throw new Exception('Transaction not found');
}
// Verify amount
$expected_amount = $transaction['amount'] * 100; // Convert to kobo
if ($data->amount !== $expected_amount) {
throw new Exception('Amount mismatch');
}
// Update transaction status
$this->paystack_model->update_transaction($transaction['id'], [
'status' => 'success',
'transaction_date' => date('Y-m-d H:i:s')
]);
// Add to payment logs
$this->paystack_model->add_payment_log([
'transaction_id' => $transaction['id'],
'invoice_id' => $transaction['invoice_id'],
'amount' => $transaction['amount'],
'message' => 'Payment successful',
'log_type' => 'success'
]);
// Record payment in Perfex CRM
$payment_data = [
'amount' => $transaction['amount'],
'invoiceid' => $transaction['invoice_id'],
'paymentmode' => 'paystack',
'transactionid' => $data->reference
];
$this->load->model('payments_model');
$payment_id = $this->payments_model->add($payment_data);
if (!$payment_id) {
throw new Exception('Failed to record payment');
}
// Send email notification
$this->send_payment_notification($transaction['invoice_id'], $payment_id);
}
/**
* Handle failed charge
*/
private function handle_failed_charge($data)
{
if (!isset($data->reference)) {
throw new Exception('Invalid charge data');
}
$transaction = $this->paystack_model->get_transaction_by_reference($data->reference);
if ($transaction) {
// Update transaction status
$this->paystack_model->update_transaction($transaction['id'], [
'status' => 'failed',
'transaction_date' => date('Y-m-d H:i:s')
]);
// Add to payment logs
$this->paystack_model->add_payment_log([
'transaction_id' => $transaction['id'],
'invoice_id' => $transaction['invoice_id'],
'amount' => $transaction['amount'],
'message' => 'Payment failed: ' . ($data->gateway_response ?? 'Unknown error'),
'log_type' => 'error'
]);
}
}
/**
* Handle successful transfer
*/
private function handle_successful_transfer($data)
{
log_activity('Paystack Webhook: Successful transfer - Reference: ' . $data->reference);
}
/**
* Send payment success email
*/
private function send_payment_success_email($invoice_id, $transaction)
{
$this->load->model('emails_model');
$invoice = $this->invoices_model->get($invoice_id);
$client = $this->clients_model->get($invoice->clientid);
$email_template = 'invoice-payment-recorded';
$merge_fields = [];
$merge_fields = array_merge($merge_fields, get_invoice_merge_fields($invoice_id));
$merge_fields = array_merge($merge_fields, get_client_merge_fields($client->userid));
$merge_fields['{payment_total}'] = app_format_money($transaction['data']->amount / 100, $invoice->currency_name);
$merge_fields['{payment_reference}'] = $transaction['data']->reference;
$this->emails_model->send_email_template($email_template, $client->email, $merge_fields);
}
}

View File

@ -0,0 +1,75 @@
<?php
defined('BASEPATH') or exit('No direct script access allowed');
/**
* Verify Paystack webhook signature
*/
function verify_paystack_webhook_signature($payload, $signature)
{
$secret_key = get_option('paystack_webhook_secret');
$calculated_signature = hash_hmac('sha512', $payload, $secret_key);
return hash_equals($calculated_signature, $signature);
}
/**
* Validate Paystack API response
*/
function validate_paystack_api_response($response)
{
if (!is_object($response)) {
return [
'valid' => false,
'message' => 'Invalid response format'
];
}
if (!isset($response->status) || $response->status !== true) {
return [
'valid' => false,
'message' => isset($response->message) ? $response->message : 'Invalid response status'
];
}
return [
'valid' => true,
'data' => $response->data
];
}
/**
* Sanitize API keys
*/
function sanitize_paystack_keys($key)
{
return preg_replace('/[^a-zA-Z0-9_]/', '', $key);
}
/**
* Validate amount
*/
function validate_paystack_amount($amount)
{
return is_numeric($amount) && $amount > 0;
}
/**
* Encrypt sensitive data
*/
function encrypt_paystack_data($data)
{
$CI = &get_instance();
$CI->load->library('encryption');
return $CI->encryption->encrypt($data);
}
/**
* Decrypt sensitive data
*/
function decrypt_paystack_data($data)
{
$CI = &get_instance();
$CI->load->library('encryption');
return $CI->encryption->decrypt($data);
}

71
install.php Normal file
View File

@ -0,0 +1,71 @@
<?php
defined('BASEPATH') or exit('No direct script access allowed');
// Paystack transactions table
if (!$CI->db->table_exists(db_prefix() . 'paystack_transactions')) {
$CI->db->query('CREATE TABLE `' . db_prefix() . 'paystack_transactions` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`invoice_id` int(11) NOT NULL,
`reference` varchar(100) NOT NULL,
`email` varchar(100) NOT NULL,
`amount` decimal(15,2) NOT NULL,
`status` varchar(20) DEFAULT NULL,
`transaction_date` datetime NOT NULL,
`date_created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `invoice_id` (`invoice_id`),
KEY `reference` (`reference`),
KEY `status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=' . $CI->db->char_set . ';');
}
// Paystack payment logs table
if (!$CI->db->table_exists(db_prefix() . 'paystack_payment_logs')) {
$CI->db->query('CREATE TABLE `' . db_prefix() . 'paystack_payment_logs` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`transaction_id` int(11) DEFAULT NULL,
`invoice_id` int(11) DEFAULT NULL,
`amount` decimal(15,2) DEFAULT NULL,
`message` text,
`log_type` varchar(50) DEFAULT NULL,
`ip_address` varchar(45) DEFAULT NULL,
`user_agent` text,
`date_created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `transaction_id` (`transaction_id`),
KEY `invoice_id` (`invoice_id`),
KEY `log_type` (`log_type`)
) ENGINE=InnoDB DEFAULT CHARSET=' . $CI->db->char_set . ';');
}
// Paystack webhook logs table
if (!$CI->db->table_exists(db_prefix() . 'paystack_webhook_logs')) {
$CI->db->query('CREATE TABLE `' . db_prefix() . 'paystack_webhook_logs` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`event` varchar(100) NOT NULL,
`payload` text,
`status` varchar(20) DEFAULT NULL,
`ip_address` varchar(45) DEFAULT NULL,
`date_created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `event` (`event`),
KEY `status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=' . $CI->db->char_set . ';');
}
// Add payment gateway options if they don't exist
$options = [
['name' => 'paystack_test_mode', 'value' => '1'],
['name' => 'paystack_live_secret_key', 'value' => ''],
['name' => 'paystack_live_public_key', 'value' => ''],
['name' => 'paystack_test_secret_key', 'value' => ''],
['name' => 'paystack_test_public_key', 'value' => ''],
['name' => 'paystack_webhook_secret', 'value' => '']
];
foreach ($options as $option) {
if (!get_option($option['name'])) {
add_option($option['name'], $option['value']);
}
}

View File

@ -0,0 +1,127 @@
<?php
# Gateway Name
$lang['paystack'] = 'Paystack';
$lang['paystack_gateway_name'] = 'Paystack Payment Gateway';
# Settings
$lang['settings_paystack_secret_key'] = 'Secret Key';
$lang['settings_paystack_public_key'] = 'Public Key';
$lang['settings_paystack_test_mode_enabled'] = 'Test Mode';
$lang['settings_group_paystack'] = 'Paystack Settings';
# Messages
$lang['payment_received'] = 'Payment received successfully';
$lang['payment_failed'] = 'Payment failed';
$lang['payment_cancelled'] = 'Payment cancelled';
# Errors
$lang['paystack_invalid_transaction'] = 'Invalid transaction';
$lang['paystack_transaction_not_found'] = 'Transaction not found';
$lang['paystack_invalid_reference'] = 'Invalid reference';
$lang['paystack_api_error'] = 'API Error: %s';
# Gateway Settings
$lang['paystack'] = 'Paystack';
$lang['paystack_gateway_name'] = 'Paystack Payment Gateway';
$lang['paystack_live_secret_key'] = 'Live Secret Key';
$lang['paystack_live_public_key'] = 'Live Public Key';
$lang['paystack_test_secret_key'] = 'Test Secret Key';
$lang['paystack_test_public_key'] = 'Test Public Key';
$lang['test_mode_enabled'] = 'Test Mode Enabled';
$lang['webhook_secret'] = 'Webhook Secret';
$lang['max_retry_attempts'] = 'Maximum Retry Attempts';
# Payment Status
$lang['payment_successful'] = 'Payment Successful';
$lang['payment_failed'] = 'Payment Failed';
$lang['payment_pending'] = 'Payment Pending';
$lang['payment_cancelled'] = 'Payment Cancelled';
$lang['payment_processing'] = 'Processing Payment';
$lang['payment_validation_failed'] = 'Payment Validation Failed';
# Error Messages
$lang['invalid_transaction'] = 'Invalid Transaction';
$lang['invalid_amount'] = 'Invalid Amount';
$lang['invalid_currency'] = 'Invalid Currency';
$lang['invalid_email'] = 'Invalid Email';
$lang['invalid_reference'] = 'Invalid Reference';
$lang['transaction_failed'] = 'Transaction Failed';
$lang['transaction_expired'] = 'Transaction Expired';
$lang['rate_limit_exceeded'] = 'Too many attempts. Please try again later.';
$lang['api_error'] = 'API Error: %s';
$lang['verification_failed'] = 'Transaction Verification Failed';
$lang['webhook_verification_failed'] = 'Webhook Verification Failed';
# Success Messages
$lang['payment_recorded'] = 'Payment Recorded Successfully';
$lang['webhook_received'] = 'Webhook Received Successfully';
$lang['verification_successful'] = 'Verification Successful';
# Transaction Details
$lang['transaction_reference'] = 'Transaction Reference';
$lang['total_transactions'] = 'Total Transaction';
$lang['transaction_amount'] = 'Amount';
$lang['transaction_date'] = 'Transaction Date';
$lang['transaction_status'] = 'Status';
$lang['transaction_currency'] = 'Currency';
$lang['transaction_email'] = 'Email';
$lang['recent_transactions'] = 'Recent transactions';
$lang['monthly_transactions'] = 'Monthly transactions';
$lang['successful_transactions'] = 'Successful Transactions';
$lang['failed_transactions'] = 'Failed Transactions';
# Admin Area
$lang['paystack_settings'] = 'Paystack Settings';
$lang['test_mode_warning'] = 'Gateway is in Test Mode. Real payments will not be processed.';
$lang['settings_updated'] = 'Settings Updated Successfully';
$lang['view_transaction'] = 'View Transaction';
$lang['retry_payment'] = 'Retry Payment';
$lang['gateway_mode'] = 'Gateway Mode';
$lang['live_mode'] = 'Live Mode';
$lang['test_mode'] = 'Test Mode';
# Logging
$lang['payment_attempt_logged'] = 'Payment Attempt Logged';
$lang['api_request_logged'] = 'API Request Logged';
$lang['error_logged'] = 'Error Logged';
$lang['view_logs'] = 'View Logs';
$lang['clear_logs'] = 'Clear Logs';
# Webhook
$lang['webhook_url'] = 'Webhook URL';
$lang['webhook_setup_guide'] = 'Webhook Setup Guide';
$lang['configure_webhook'] = 'Configure Webhook';
$lang['test_webhook'] = 'Test Webhook';
# Security
$lang['security_settings'] = 'Security Settings';
$lang['encryption_key'] = 'Encryption Key';
$lang['rate_limiting'] = 'Rate Limiting';
$lang['attempts_allowed'] = 'Attempts Allowed';
$lang['timeframe_minutes'] = 'Timeframe (Minutes)';
# Documentation
$lang['integration_guide'] = 'Integration Guide';
$lang['api_documentation'] = 'API Documentation';
$lang['troubleshooting_guide'] = 'Troubleshooting Guide';
# Buttons
$lang['pay_with_paystack'] = 'Pay with Paystack';
$lang['verify_payment'] = 'Verify Payment';
$lang['retry_verification'] = 'Retry Verification';
$lang['cancel_payment'] = 'Cancel Payment';
# Misc
$lang['paystack_not_configured'] = 'Paystack Gateway is not properly configured';
$lang['contact_support'] = 'Contact Support';
$lang['view_transaction_history'] = 'View Transaction History';
$lang['export_transactions'] = 'Export Transactions';
$lang['sync_transactions'] = 'Sync Transactions';
$lang['paystack_transactions'] = 'Paystack Transactions';
$lang['paystack_dashboard'] = 'Dashboard';
$lang['dashboard'] = 'Dashboard';
$lang['transactions'] = 'Transactions';
$lang['start_date'] = 'Start Date';
$lang['end_date'] = 'End Date';

View File

@ -0,0 +1,112 @@
<?php
defined('BASEPATH') or exit('No direct script access allowed');
class Paystack_error_handler
{
protected $CI;
protected $errors = [];
public function __construct()
{
$this->CI = &get_instance();
}
/**
* Handle API errors
*/
public function handle_api_error($response, $context = '')
{
$error_data = [
'type' => 'api_error',
'context' => $context,
'message' => isset($response->message) ? $response->message : 'Unknown API error',
'code' => isset($response->code) ? $response->code : null,
'timestamp' => date('Y-m-d H:i:s')
];
$this->log_error($error_data);
return $error_data;
}
/**
* Handle validation errors
*/
public function handle_validation_error($errors, $context = '')
{
$error_data = [
'type' => 'validation_error',
'context' => $context,
'message' => is_array($errors) ? implode(', ', $errors) : $errors,
'timestamp' => date('Y-m-d H:i:s')
];
$this->log_error($error_data);
return $error_data;
}
/**
* Handle system errors
*/
public function handle_system_error($exception, $context = '')
{
$error_data = [
'type' => 'system_error',
'context' => $context,
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $exception->getTraceAsString(),
'timestamp' => date('Y-m-d H:i:s')
];
$this->log_error($error_data);
return $error_data;
}
/**
* Log error
*/
protected function log_error($error_data)
{
// Add to errors array
$this->errors[] = $error_data;
// Log to database
$this->CI->load->model('paystack_model');
$log_data = [
'message' => json_encode($error_data),
'log_type' => 'error'
];
$this->CI->paystack_model->add_payment_log($log_data);
// Log to system log if serious error
if ($error_data['type'] === 'system_error') {
log_message('error', 'Paystack Error: ' . $error_data['message']);
}
}
/**
* Get last error
*/
public function get_last_error()
{
return end($this->errors);
}
/**
* Get all errors
*/
public function get_all_errors()
{
return $this->errors;
}
/**
* Clear errors
*/
public function clear_errors()
{
$this->errors = [];
}
}

View File

@ -0,0 +1,378 @@
<?php
defined('BASEPATH') or exit('No direct script access allowed');
class Paystack_gateway extends App_gateway
{
protected $test_mode;
protected $api_url = 'https://api.paystack.co/';
public function __construct()
{
parent::__construct();
$this->test_mode = get_option('test_mode_enabled');
// Load the model using the full module path
$this->ci->load->model('paystack/paystack_model');
$this->setId('paystack');
$this->setName('Paystack');
/**
* Enhanced settings with additional security options
*/
$this->setSettings([
[
'name' => 'paystack_secret_key',
'encrypted' => true,
'label' => 'Paystack Secret Key',
'type' => 'input'
],
[
'name' => 'paystack_public_key',
'label' => 'Paystack Public Key',
'type' => 'input'
],
[
'name' => 'test_mode_enabled',
'label' => 'Test Mode',
'type' => 'yes_no',
'default_value' => 1
],
[
'name' => 'currencies',
'label' => 'settings_paymentmethod_currencies',
'default_value' => 'NGN,USD,GHS,ZAR'
],
[
'name' => 'webhook_secret',
'encrypted' => true,
'label' => 'Webhook Secret',
'type' => 'input'
],
[
'name' => 'max_retry_attempts',
'label' => 'Maximum Retry Attempts',
'type' => 'input',
'default_value' => '3'
]
]);
}
/**
* Process the payment with validation and security checks
*/
public function process_payment($data)
{
try {
// Log the payment data for debugging
$this->ci->paystack_model->add_payment_log([
'invoice_id' => $data['invoiceid'],
'amount' => $data['amount'],
'message' => 'Payment process initiated',
'log_type' => 'debug'
]);
// Generate payment reference
$reference = 'INV_' . $data['invoiceid'] . '_' . time();
// Store transaction data
$transaction_data = [
'invoice_id' => $data['invoiceid'],
'reference' => $reference,
'email' => $data['client']->email,
'amount' => $data['amount'],
'status' => 'pending',
'transaction_date' => date('Y-m-d H:i:s')
];
$this->ci->db->insert(db_prefix() . 'paystack_transactions', $transaction_data);
// Return HTML for the payment form
return $this->generate_payment_form($data, $reference);
} catch (Exception $e) {
// Log any errors
$this->ci->paystack_model->add_payment_log([
'invoice_id' => $data['invoiceid'],
'message' => 'Error: ' . $e->getMessage(),
'log_type' => 'error'
]);
return false;
}
}
private function generate_payment_form($data, $reference)
{
$public_key = $this->getSetting('paystack_public_key');
$form = '
<div id="paystack-payment-form">
<script src="https://js.paystack.co/v1/inline.js"></script>
<button type="button" class="btn btn-success" onclick="payWithPaystack()">Pay with Paystack</button>
<script>
function payWithPaystack() {
let handler = PaystackPop.setup({
key: "' . $public_key . '",
email: "' . $data['client']->email . '",
amount: ' . ($data['amount'] * 100) . ', // Convert to kobo
currency: "' . $data['currency'] . '",
ref: "' . $reference . '",
callback: function(response) {
if(response.status == "success") {
window.location.href = "' . site_url('paystack/verify_payment/') . '" + response.reference;
}
},
onClose: function() {
// Handle popup closed
console.log("Payment window closed");
}
});
handler.openIframe();
}
</script>
</div>';
return $form;
}
/**
* Record payment with additional validation
*/
public function record_payment($invoice_id, $transaction)
{
// Validate transaction data
if (!$this->validate_transaction_data($transaction)) {
$this->log_error('Invalid transaction data for payment recording');
return false;
}
$payment_data = [
'amount' => $transaction['data']->amount / 100,
'invoiceid' => $invoice_id,
'paymentmode' => $this->getId(),
'transactionid' => $transaction['data']->reference,
'note' => 'Paystack Transaction Reference: ' . $transaction['data']->reference
];
// Record payment
$this->ci->load->model('payments_model');
$payment_id = $this->ci->payments_model->add($payment_data);
if ($payment_id) {
$this->update_invoice_status($invoice_id);
$this->log_success($transaction['data']->reference, $payment_data['amount']);
return true;
}
$this->log_error('Failed to record payment for invoice #' . $invoice_id);
return false;
}
/**
* Make secure API request
*/
protected function make_api_request($endpoint, $method = 'GET', $data = null)
{
$url = $this->api_url . $endpoint;
$secret_key = $this->get_secret_key();
$headers = [
'Authorization: Bearer ' . $secret_key,
'Cache-Control: no-cache'
];
if ($data && in_array($method, ['POST', 'PUT'])) {
$headers[] = 'Content-Type: application/json';
}
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers
]);
if ($data && in_array($method, ['POST', 'PUT'])) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
}
$response = curl_exec($ch);
$error = curl_error($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$this->log_api_request($endpoint, $method, $data, $http_code, $error);
if ($error) {
return [
'success' => false,
'message' => $error
];
}
$result = json_decode($response);
if (!$result || !$result->status) {
return [
'success' => false,
'message' => isset($result->message) ? $result->message : 'Invalid response'
];
}
return [
'success' => true,
'data' => $result
];
}
/**
* Validation methods
*/
protected function validate_payment_data($data)
{
$required_fields = ['amount', 'invoiceid', 'currency'];
foreach ($required_fields as $field) {
if (!isset($data[$field]) || empty($data[$field])) {
$this->log_error('Missing required field: ' . $field);
return false;
}
}
return true;
}
protected function validate_payment_amount($amount)
{
return is_numeric($amount) && $amount > 0;
}
protected function validate_payment_currency($currency)
{
$allowed_currencies = explode(',', get_option('currencies'));
return in_array(strtoupper($currency), $allowed_currencies);
}
public function verify_transaction($reference)
{
$secret_key = $this->decryptSetting('paystack_secret_key');
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => "https://api.paystack.co/transaction/verify/" . rawurlencode($reference),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . $secret_key,
"Cache-Control: no-cache",
],
]);
$response = curl_exec($ch);
$err = curl_error($ch);
curl_close($ch);
if ($err) {
$this->ci->paystack_model->add_payment_log([
'message' => 'Verification Error: ' . $err,
'log_type' => 'error'
]);
return [
'success' => false,
'message' => $err
];
}
$result = json_decode($response);
return [
'success' => true,
'data' => $result->data
];
}
/**
* Security methods
*/
protected function encrypt_payment_data($data)
{
$this->ci->load->library('encryption');
return $this->ci->encryption->encrypt(serialize($data));
}
protected function decrypt_payment_data($encrypted_data)
{
if (!$encrypted_data) return false;
$this->ci->load->library('encryption');
$decrypted = $this->ci->encryption->decrypt($encrypted_data);
return $decrypted ? unserialize($decrypted) : false;
}
protected function get_secret_key()
{
$key_option = $this->test_mode ? 'paystack_test_secret_key' : 'paystack_live_secret_key';
return $this->decryptSetting($key_option);
}
/**
* Rate limiting
*/
protected function check_rate_limit($invoice_id)
{
$max_attempts = get_option('max_retry_attempts');
$timeframe = 15; // minutes
$this->ci->load->model('paystack_model');
$attempts = $this->ci->paystack_model->get_recent_failed_attempts($invoice_id, $timeframe);
return $attempts < $max_attempts;
}
/**
* Logging methods
*/
protected function log_payment_attempt($invoice_id, $amount)
{
$this->ci->load->model('paystack_model');
$this->ci->paystack_model->add_payment_log([
'invoice_id' => $invoice_id,
'amount' => $amount,
'log_type' => 'attempt',
'message' => 'Payment attempt initiated'
]);
}
protected function log_api_request($endpoint, $method, $data, $http_code, $error = null)
{
$this->ci->load->model('paystack_model');
$this->ci->paystack_model->add_payment_log([
'log_type' => 'api_request',
'message' => json_encode([
'endpoint' => $endpoint,
'method' => $method,
'http_code' => $http_code,
'error' => $error
])
]);
}
protected function log_success($reference, $amount)
{
$this->ci->load->model('paystack_model');
$this->ci->paystack_model->add_payment_log([
'log_type' => 'success',
'message' => "Payment successful - Reference: $reference, Amount: $amount"
]);
}
protected function log_error($message)
{
$this->ci->load->model('paystack_model');
$this->ci->paystack_model->add_payment_log([
'log_type' => 'error',
'message' => $message
]);
}
}

210
models/Paystack_model.php Normal file
View File

@ -0,0 +1,210 @@
<?php
defined('BASEPATH') or exit('No direct script access allowed');
class Paystack_model extends App_Model
{
public function __construct()
{
parent::__construct();
}
/**
* Get all transactions
*/
public function get_transactions($filter = [])
{
$this->db->select('*');
$this->db->from(db_prefix() . 'paystack_transactions');
if (isset($filter['start_date']) && $filter['start_date']) {
$this->db->where('date_created >=', $filter['start_date'] . ' 00:00:00');
}
if (isset($filter['end_date']) && $filter['end_date']) {
$this->db->where('date_created <=', $filter['end_date'] . ' 23:59:59');
}
$this->db->order_by('date_created', 'desc');
return $this->db->get()->result_array();
}
/**
* Get payment statistics
*/
public function get_payment_stats()
{
$stats = [
'total_transactions' => 0,
'successful_transactions' => 0,
'failed_transactions' => 0,
'total_amount' => 0,
'success_rate' => 0
];
// Total transactions
$this->db->select('COUNT(*) as total');
$this->db->from(db_prefix() . 'paystack_transactions');
$stats['total_transactions'] = $this->db->get()->row()->total;
// Successful transactions
$this->db->select('COUNT(*) as total, SUM(amount) as amount');
$this->db->where('status', 'success');
$this->db->from(db_prefix() . 'paystack_transactions');
$result = $this->db->get()->row();
$stats['successful_transactions'] = $result->total;
$stats['total_amount'] = $result->amount;
// Failed transactions
$this->db->select('COUNT(*) as total');
$this->db->where('status', 'failed');
$this->db->from(db_prefix() . 'paystack_transactions');
$stats['failed_transactions'] = $this->db->get()->row()->total;
// Calculate success rate
if ($stats['total_transactions'] > 0) {
$stats['success_rate'] = ($stats['successful_transactions'] / $stats['total_transactions']) * 100;
}
return $stats;
}
/**
* Get monthly chart data
*/
public function get_monthly_chart_data()
{
$months = [];
for ($m = 11; $m >= 0; $m--) {
$months[date('Y-m', strtotime("-$m months"))] = [
'successful' => 0,
'failed' => 0,
'amount' => 0
];
}
$this->db->select('DATE_FORMAT(date_created, "%Y-%m") as month, status, COUNT(*) as total, SUM(amount) as amount');
$this->db->from(db_prefix() . 'paystack_transactions');
$this->db->where('date_created >= DATE_SUB(NOW(), INTERVAL 12 MONTH)');
$this->db->group_by('month, status');
$results = $this->db->get()->result_array();
foreach ($results as $result) {
if (isset($months[$result['month']])) {
if ($result['status'] == 'success') {
$months[$result['month']]['successful'] = $result['total'];
$months[$result['month']]['amount'] = $result['amount'];
} else {
$months[$result['month']]['failed'] = $result['total'];
}
}
}
return $months;
}
/**
* Get recent transactions
*/
public function get_recent_transactions($limit = 10)
{
$this->db->select('t.*, i.number as invoice_number');
$this->db->from(db_prefix() . 'paystack_transactions t');
$this->db->join(db_prefix() . 'invoices i', 'i.id = t.invoice_id', 'left');
$this->db->order_by('t.date_created', 'desc');
$this->db->limit($limit);
return $this->db->get()->result_array();
}
/**
* Add transaction log
*/
public function add_log($data)
{
$this->db->insert(db_prefix() . 'paystack_payment_logs', $data);
return $this->db->insert_id();
}
/**
* Get transaction by reference
*/
public function get_transaction_by_reference($reference)
{
$this->db->where('reference', $reference);
return $this->db->get(db_prefix() . 'paystack_transactions')->row_array();
}
/**
* Add webhook log
*/
public function add_webhook_log($event, $payload, $status)
{
return $this->db->insert(db_prefix() . 'paystack_webhook_logs', [
'event' => $event,
'payload' => json_encode($payload),
'status' => $status
]);
}
/**
* Update settings
*/
public function update_settings($data)
{
foreach ($data as $key => $value) {
update_option($key, $value);
}
return true;
}
/**
* Get recent failed attempts
*/
public function get_recent_failed_attempts($invoice_id, $timeframe)
{
$this->db->where('invoice_id', $invoice_id);
$this->db->where('log_type', 'failed_attempt');
$this->db->where('date_created >=', date('Y-m-d H:i:s', strtotime("-$timeframe minutes")));
return $this->db->count_all_results(db_prefix() . 'paystack_payment_logs');
}
/**
* Add payment log with enhanced details
*/
public function add_payment_log($data)
{
// Ensure required fields
$data['date_created'] = date('Y-m-d H:i:s');
// Add client IP if available
if (!isset($data['ip_address']) && isset($_SERVER['REMOTE_ADDR'])) {
$data['ip_address'] = $_SERVER['REMOTE_ADDR'];
}
// Add user agent if available
if (!isset($data['user_agent']) && isset($_SERVER['HTTP_USER_AGENT'])) {
$data['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
}
return $this->db->insert(db_prefix() . 'paystack_payment_logs', $data);
}
/**
* Get transaction logs
*/
public function get_transaction_logs($transaction_id)
{
$this->db->where('transaction_id', $transaction_id);
$this->db->order_by('date_created', 'desc');
return $this->db->get(db_prefix() . 'paystack_payment_logs')->result_array();
}
/**
* Clean old logs
*/
public function clean_old_logs($days = 90)
{
$this->db->where('date_created <', date('Y-m-d H:i:s', strtotime("-$days days")));
return $this->db->delete(db_prefix() . 'paystack_payment_logs');
}
}

84
paystack.php Normal file
View File

@ -0,0 +1,84 @@
<?php
defined('BASEPATH') or exit('No direct script access allowed');
/*
Module Name: Paystack Payment Gateway
Description: Payment gateway integration for processing payments through Paystack
Version: 1.0.0
Requires at least: 2.3.4
Author: [Your Name]
Author URI: [Your Website]
*/
define('PAYSTACK_MODULE_NAME', 'PAYSTACK');
/**
* Activation hook
*/
function paystack_activation_hook()
{
$CI = &get_instance();
require_once(__DIR__ . '/install.php');
}
/**
* Add menu items for Paystack
* @return null
*/
function paystack_init_menu_items()
{
$CI = &get_instance();
// Main Menu Item
if (has_permission('payments', '', 'view')) {
$CI->app_menu->add_sidebar_menu_item('paystack', [
'name' => 'Paystack',
'position' => 30,
'icon' => 'fa fa-credit-card',
]);
// Sub Menu Items
$CI->app_menu->add_sidebar_children_item('paystack', [
'slug' => 'paystack-dashboard',
'name' => _l('dashboard'),
'href' => admin_url('paystack/admin/dashboard'),
'position' => 1,
]);
$CI->app_menu->add_sidebar_children_item('paystack', [
'slug' => 'paystack-transactions',
'name' => _l('transactions'),
'href' => admin_url('paystack/admin/transactions'),
'position' => 5,
]);
}
// Settings Menu Item
if (has_permission('settings', '', 'view')) {
$CI->app_menu->add_setup_menu_item('paystack-settings', [
'name' => _l('paystack_settings'),
'href' => admin_url('paystack/admin/settings'),
'position' => 65,
'icon' => 'fa fa-credit-card',
]);
$CI->app_menu->add_setup_menu_item('paystack-test-mode', [
'parent' => 'paystack-settings',
'name' => _l('test_mode'),
'href' => admin_url('paystack/admin/test_mode'),
'position' => 5,
]);
}
}
// Register activation hook
register_activation_hook('paystack', 'paystack_activation_hook');
// Register payment gateway
register_payment_gateway('paystack_gateway', 'paystack');
// Add menu items
hooks()->add_action('admin_init', 'paystack_init_menu_items');
register_language_files(PAYSTACK_MODULE_NAME, [PAYSTACK_MODULE_NAME]);

178
views/admin/dashboard.php Normal file
View File

@ -0,0 +1,178 @@
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
<?php init_head(); ?>
<div id="wrapper">
<div class="content">
<div class="row">
<div class="col-md-12">
<div class="panel_s">
<div class="panel-body">
<h4 class="no-margin"><?php echo _l('paystack_dashboard'); ?></h4>
<hr class="hr-panel-heading" />
<!-- Stats Widgets -->
<div class="row">
<div class="col-md-3">
<div class="widget-box">
<div class="widget-header">
<h3><?php echo _l('total_transactions'); ?></h3>
</div>
<div class="widget-content">
<h3 class="bold"><?php echo $stats['total_transactions']; ?></h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="widget-box success-bg">
<div class="widget-header">
<h3><?php echo _l('successful_transactions'); ?></h3>
</div>
<div class="widget-content">
<h3 class="bold"><?php echo $stats['successful_transactions']; ?></h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="widget-box warning-bg">
<div class="widget-header">
<h3><?php echo _l('failed_transactions'); ?></h3>
</div>
<div class="widget-content">
<h3 class="bold"><?php echo $stats['failed_transactions']; ?></h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="widget-box info-bg">
<div class="widget-header">
<h3><?php echo _l('total_amount'); ?></h3>
</div>
<div class="widget-content">
<h3 class="bold"><?php echo app_format_money($stats['total_amount'], get_base_currency()); ?></h3>
</div>
</div>
</div>
</div>
<div class="row mtop20">
<!-- Monthly Chart -->
<div class="col-md-8">
<div class="panel_s">
<div class="panel-body">
<h4 class="no-margin"><?php echo _l('monthly_transactions'); ?></h4>
<hr class="hr-panel-heading" />
<canvas id="monthly-chart" height="300"></canvas>
</div>
</div>
</div>
<!-- Success Rate -->
<div class="col-md-4">
<div class="panel_s">
<div class="panel-body">
<h4 class="no-margin"><?php echo _l('success_rate'); ?></h4>
<hr class="hr-panel-heading" />
<canvas id="success-rate-chart" height="300"></canvas>
</div>
</div>
</div>
</div>
<!-- Recent Transactions -->
<div class="panel_s mtop20">
<div class="panel-body">
<h4 class="no-margin"><?php echo _l('recent_transactions'); ?></h4>
<hr class="hr-panel-heading" />
<table class="table dt-table table-recent-transactions">
<thead>
<tr>
<th><?php echo _l('date'); ?></th>
<th><?php echo _l('reference'); ?></th>
<th><?php echo _l('invoice_number'); ?></th>
<th><?php echo _l('amount'); ?></th>
<th><?php echo _l('status'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach($recent_transactions as $transaction) { ?>
<tr>
<td><?php echo _dt($transaction['date']); ?></td>
<td><?php echo $transaction['reference']; ?></td>
<td>
<a href="<?php echo admin_url('invoices/list_invoices/'.$transaction['invoice_id']); ?>">
<?php echo format_invoice_number($transaction['invoice_number']); ?>
</a>
</td>
<td><?php echo app_format_money($transaction['amount'], get_base_currency()); ?></td>
<td>
<span class="label label-<?php echo $transaction['status'] == 'success' ? 'success' : 'danger'; ?>">
<?php echo _l($transaction['status']); ?>
</span>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<?php init_tail(); ?>
<script>
// Monthly Chart
var ctx = document.getElementById('monthly-chart').getContext('2d');
var monthlyChart = new Chart(ctx, {
type: 'bar',
data: {
labels: <?php echo json_encode(array_keys($monthly_chart)); ?>,
datasets: [{
label: '<?php echo _l("successful_transactions"); ?>',
data: <?php echo json_encode(array_column($monthly_chart, 'successful')); ?>,
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1
},
{
label: '<?php echo _l("failed_transactions"); ?>',
data: <?php echo json_encode(array_column($monthly_chart, 'failed')); ?>,
backgroundColor: 'rgba(255, 99, 132, 0.2)',
borderColor: 'rgba(255, 99, 132, 1)',
borderWidth: 1
}]
},
options: {
scales: {
y: {
beginAtZero: true
}
}
}
});
// Success Rate Chart
var ctx2 = document.getElementById('success-rate-chart').getContext('2d');
var successRateChart = new Chart(ctx2, {
type: 'doughnut',
data: {
labels: ['<?php echo _l("successful"); ?>', '<?php echo _l("failed"); ?>'],
datasets: [{
data: [
<?php echo $stats['successful_transactions']; ?>,
<?php echo $stats['failed_transactions']; ?>
],
backgroundColor: [
'rgba(75, 192, 192, 0.2)',
'rgba(255, 99, 132, 0.2)'
],
borderColor: [
'rgba(75, 192, 192, 1)',
'rgba(255, 99, 132, 1)'
],
borderWidth: 1
}]
}
});
</script>

104
views/admin/settings.php Normal file
View File

@ -0,0 +1,104 @@
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
<?php init_head(); ?>
<div id="wrapper">
<div class="content">
<div class="row">
<div class="col-md-12">
<div class="panel_s">
<div class="panel-body">
<?php echo form_open(admin_url('paystack/admin/settings')); ?>
<div class="tab-content mtop15">
<!-- Settings Tab -->
<div class="tab-pane<?php if($tab == 'settings'){echo ' active';} ?>" id="settings">
<div class="form-group">
<label for="paystack_live_public_key"><?php echo _l('live_public_key'); ?></label>
<input type="text" class="form-control" id="paystack_live_public_key"
name="paystack_live_public_key"
value="<?php echo get_option('paystack_live_public_key'); ?>">
</div>
<div class="form-group">
<label for="paystack_live_secret_key"><?php echo _l('live_secret_key'); ?></label>
<input type="password" class="form-control" id="paystack_live_secret_key"
name="paystack_live_secret_key"
value="<?php echo get_option('paystack_live_secret_key'); ?>">
</div>
<div class="form-group">
<label for="paystack_webhook_secret"><?php echo _l('webhook_secret'); ?></label>
<input type="password" class="form-control" id="paystack_webhook_secret"
name="paystack_webhook_secret"
value="<?php echo get_option('paystack_webhook_secret'); ?>">
</div>
<div class="form-group">
<label for="paystack_default_description"><?php echo _l('default_description'); ?></label>
<textarea class="form-control" id="paystack_default_description"
name="paystack_default_description" rows="3"><?php echo get_option('paystack_default_description'); ?></textarea>
<small class="text-muted"><?php echo _l('default_description_help'); ?></small>
</div>
</div>
<!-- Test Mode Tab -->
<div class="tab-pane<?php if($tab == 'test_mode'){echo ' active';} ?>" id="test_mode">
<div class="form-group">
<label for="paystack_test_public_key"><?php echo _l('test_public_key'); ?></label>
<input type="text" class="form-control" id="paystack_test_public_key"
name="paystack_test_public_key"
value="<?php echo get_option('paystack_test_public_key'); ?>">
</div>
<div class="form-group">
<label for="paystack_test_secret_key"><?php echo _l('test_secret_key'); ?></label>
<input type="password" class="form-control" id="paystack_test_secret_key"
name="paystack_test_secret_key"
value="<?php echo get_option('paystack_test_secret_key'); ?>">
</div>
<div class="form-group">
<label class="control-label">
<div class="checkbox checkbox-primary">
<input type="checkbox" name="paystack_test_mode" id="paystack_test_mode"
<?php if(get_option('paystack_test_mode') == 1){echo 'checked';} ?>>
<label for="paystack_test_mode"><?php echo _l('enable_test_mode'); ?></label>
</div>
</label>
</div>
<div class="alert alert-info">
<?php echo _l('test_mode_notice'); ?>
</div>
</div>
</div>
<div class="btn-bottom-toolbar text-right">
<button type="submit" class="btn btn-primary"><?php echo _l('save'); ?></button>
</div>
<?php echo form_close(); ?>
</div>
</div>
</div>
</div>
</div>
</div>
<?php init_tail(); ?>
<script>
$(function() {
// Toggle test mode fields
$('#paystack_test_mode').on('change', function() {
if($(this).is(':checked')) {
$('#test_keys').removeClass('hide');
} else {
$('#test_keys').addClass('hide');
}
});
// Copy webhook URL to clipboard
$('#webhook_url').on('click', function() {
$(this).select();
document.execCommand('copy');
alert_float('success', '<?php echo _l("webhook_url_copied"); ?>');
});
});
</script>

228
views/admin/test_mode.php Normal file
View File

@ -0,0 +1,228 @@
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
<?php init_head(); ?>
<div id="wrapper">
<div class="content">
<div class="row">
<div class="col-md-12">
<div class="panel_s">
<div class="panel-body">
<h4 class="no-margin">
<?php echo _l('paystack_test_mode'); ?>
<?php if(get_option('paystack_test_mode')): ?>
<span class="label label-warning"><?php echo _l('test_mode_enabled'); ?></span>
<?php endif; ?>
</h4>
<hr class="hr-panel-heading" />
<!-- Test Mode Warning -->
<div class="alert alert-warning">
<h4><i class="fa fa-warning"></i> <?php echo _l('test_mode_warning'); ?></h4>
<?php echo _l('test_mode_description'); ?>
</div>
<!-- Test Transaction -->
<div class="panel_s">
<div class="panel-body">
<h4 class="no-margin"><?php echo _l('test_transaction'); ?></h4>
<hr class="hr-panel-heading" />
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="test_amount"><?php echo _l('amount'); ?></label>
<input type="number" class="form-control" id="test_amount" value="1000">
</div>
<div class="form-group">
<label for="test_email"><?php echo _l('email'); ?></label>
<input type="email" class="form-control" id="test_email" value="test@example.com">
</div>
<button type="button" class="btn btn-info" onclick="initiate_test_payment()">
<?php echo _l('initiate_test_payment'); ?>
</button>
</div>
<div class="col-md-6">
<div class="alert alert-info">
<h4><?php echo _l('test_cards'); ?></h4>
<p><strong><?php echo _l('successful_payment'); ?>:</strong></p>
<pre>Card Number: 4084 0840 8408 4081
Expiry: 01/25
CVV: 408</pre>
<p><strong><?php echo _l('failed_payment'); ?>:</strong></p>
<pre>Card Number: 4084 0840 8408 4080
Expiry: 01/25
CVV: 408</pre>
</div>
</div>
</div>
</div>
</div>
<!-- Test Webhook -->
<div class="panel_s">
<div class="panel-body">
<h4 class="no-margin"><?php echo _l('test_webhook'); ?></h4>
<hr class="hr-panel-heading" />
<div class="row">
<div class="col-md-6">
<p><?php echo _l('webhook_url'); ?>:</p>
<div class="input-group">
<input type="text" class="form-control" id="webhook_url" readonly
value="<?php echo site_url('paystack/webhook'); ?>">
<span class="input-group-btn">
<button class="btn btn-default" type="button" onclick="copy_webhook_url()">
<i class="fa fa-copy"></i>
</button>
</span>
</div>
<div class="mtop15">
<button type="button" class="btn btn-info" onclick="test_webhook()">
<?php echo _l('test_webhook_connection'); ?>
</button>
</div>
</div>
<div class="col-md-6">
<div id="webhook_test_result"></div>
</div>
</div>
</div>
</div>
<!-- Debug Log -->
<div class="panel_s">
<div class="panel-body">
<h4 class="no-margin"><?php echo _l('debug_log'); ?></h4>
<hr class="hr-panel-heading" />
<div class="row">
<div class="col-md-12">
<textarea class="form-control" id="debug_log" rows="10" readonly></textarea>
<div class="mtop15">
<button type="button" class="btn btn-danger" onclick="clear_debug_log()">
<?php echo _l('clear_log'); ?>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<?php init_tail(); ?>
<script>
// Initiate test payment
function initiate_test_payment() {
var amount = $('#test_amount').val();
var email = $('#test_email').val();
$.post(admin_url + 'paystack/admin/initiate_test_payment', {
amount: amount,
email: email
}).done(function(response) {
var data = JSON.parse(response);
if(data.success) {
// Open Paystack popup
let handler = PaystackPop.setup({
key: '<?php echo get_option("paystack_test_public_key"); ?>',
email: email,
amount: amount * 100,
ref: data.reference,
callback: function(response) {
log_debug('Test payment completed: ' + response.reference);
alert_float('success', '<?php echo _l("test_payment_successful"); ?>');
},
onClose: function() {
log_debug('Test payment cancelled');
alert_float('warning', '<?php echo _l("test_payment_cancelled"); ?>');
}
});
handler.openIframe();
} else {
alert_float('danger', data.message);
}
});
}
// Test webhook
function test_webhook() {
$('#webhook_test_result').html('<div class="alert alert-info"><?php echo _l("testing_webhook"); ?></div>');
$.post(admin_url + 'paystack/admin/test_webhook')
.done(function(response) {
var data = JSON.parse(response);
if(data.success) {
$('#webhook_test_result').html(
'<div class="alert alert-success">' +
'<h4><i class="fa fa-check"></i> <?php echo _l("webhook_test_successful"); ?></h4>' +
'<p>' + data.message + '</p>' +
'</div>'
);
log_debug('Webhook test successful: ' + data.message);
} else {
$('#webhook_test_result').html(
'<div class="alert alert-danger">' +
'<h4><i class="fa fa-times"></i> <?php echo _l("webhook_test_failed"); ?></h4>' +
'<p>' + data.message + '</p>' +
'</div>'
);
log_debug('Webhook test failed: ' + data.message);
}
})
.fail(function(xhr) {
$('#webhook_test_result').html(
'<div class="alert alert-danger">' +
'<h4><i class="fa fa-times"></i> <?php echo _l("webhook_test_failed"); ?></h4>' +
'<p><?php echo _l("connection_failed"); ?></p>' +
'</div>'
);
log_debug('Webhook test failed: Connection error');
});
}
// Copy webhook URL
function copy_webhook_url() {
var copyText = document.getElementById("webhook_url");
copyText.select();
document.execCommand("copy");
alert_float('success', '<?php echo _l("webhook_url_copied"); ?>');
}
// Debug logging
function log_debug(message) {
var now = new Date().toISOString();
var current = $('#debug_log').val();
$('#debug_log').val(current + now + ': ' + message + "\n");
// Save to server
$.post(admin_url + 'paystack/admin/log_debug', {
message: message
});
}
// Clear debug log
function clear_debug_log() {
if(confirm('<?php echo _l("confirm_clear_log"); ?>')) {
$('#debug_log').val('');
$.post(admin_url + 'paystack/admin/clear_debug_log');
alert_float('success', '<?php echo _l("log_cleared"); ?>');
}
}
// Load initial debug log
function load_debug_log() {
$.get(admin_url + 'paystack/admin/get_debug_log', function(response) {
$('#debug_log').val(response);
});
}
// Initialize
$(function() {
load_debug_log();
// Refresh debug log every 30 seconds
setInterval(load_debug_log, 30000);
});
</script>

View File

@ -0,0 +1,169 @@
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
<?php init_head(); ?>
<div id="wrapper">
<div class="content">
<div class="row">
<div class="col-md-12">
<div class="panel_s">
<div class="panel-body">
<h4 class="no-margin">
<?php echo _l('paystack_transactions'); ?>
</h4>
<hr class="hr-panel-heading" />
<!-- Filters -->
<div class="row">
<div class="col-md-12">
<?php echo form_open('', ['method' => 'GET']); ?>
<div class="col-md-3">
<?php echo render_date_input('start_date', 'start_date', $this->input->get('start_date')); ?>
</div>
<div class="col-md-3">
<?php echo render_date_input('end_date', 'end_date', $this->input->get('end_date')); ?>
</div>
<div class="col-md-3">
<div class="form-group">
<label for="status"><?php echo _l('status'); ?></label>
<select name="status" id="status" class="selectpicker" data-width="100%">
<option value=""><?php echo _l('all'); ?></option>
<option value="success" <?php if($this->input->get('status') == 'success'){echo 'selected';} ?>>
<?php echo _l('success'); ?>
</option>
<option value="failed" <?php if($this->input->get('status') == 'failed'){echo 'selected';} ?>>
<?php echo _l('failed'); ?>
</option>
</select>
</div>
</div>
<div class="col-md-3 mtop25">
<button type="submit" class="btn btn-primary"><?php echo _l('filter'); ?></button>
<a href="<?php echo admin_url('paystack/admin/transactions'); ?>" class="btn btn-default">
<?php echo _l('clear'); ?>
</a>
</div>
<?php echo form_close(); ?>
</div>
</div>
<div class="clearfix mtop20"></div>
<!-- Transactions Table -->
<table class="table dt-table table-transactions">
<thead>
<tr>
<th><?php echo _l('date'); ?></th>
<th><?php echo _l('reference'); ?></th>
<th><?php echo _l('invoice_dt_number'); ?></th>
<th><?php echo _l('client'); ?></th>
<th><?php echo _l('amount'); ?></th>
<th><?php echo _l('status'); ?></th>
<th><?php echo _l('options'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach($transactions as $transaction) { ?>
<tr>
<td><?php echo _dt($transaction['date']); ?></td>
<td><?php echo $transaction['reference']; ?></td>
<td>
<a href="<?php echo admin_url('invoices/list_invoices/' . $transaction['invoice_id']); ?>">
<?php echo format_invoice_number($transaction['invoice_id']); ?>
</a>
</td>
<td><?php echo $transaction['email']; ?></td>
<td><?php echo app_format_money($transaction['amount'], get_base_currency()); ?></td>
<td>
<?php
$status_class = 'default';
if($transaction['status'] == 'success') {
$status_class = 'success';
} else if($transaction['status'] == 'failed') {
$status_class = 'danger';
}
?>
<span class="label label-<?php echo $status_class; ?>">
<?php echo _l($transaction['status']); ?>
</span>
</td>
<td>
<a href="#" class="btn btn-default btn-icon" onclick="view_transaction(<?php echo $transaction['id']; ?>)">
<i class="fa fa-eye"></i>
</a>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Transaction Details Modal -->
<div class="modal fade" id="transaction_modal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><?php echo _l('transaction_details'); ?></h4>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-12">
<table class="table table-striped">
<tbody>
<tr>
<td><strong><?php echo _l('reference'); ?>:</strong></td>
<td id="td_reference"></td>
</tr>
<tr>
<td><strong><?php echo _l('amount'); ?>:</strong></td>
<td id="td_amount"></td>
</tr>
<tr>
<td><strong><?php echo _l('status'); ?>:</strong></td>
<td id="td_status"></td>
</tr>
<tr>
<td><strong><?php echo _l('date'); ?>:</strong></td>
<td id="td_date"></td>
</tr>
<tr>
<td><strong><?php echo _l('email'); ?>:</strong></td>
<td id="td_email"></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal"><?php echo _l('close'); ?></button>
</div>
</div>
</div>
</div>
<?php init_tail(); ?>
<script>
function view_transaction(id) {
$.get(admin_url + 'paystack/admin/get_transaction_details/' + id, function(response) {
var data = JSON.parse(response);
$('#td_reference').html(data.reference);
$('#td_amount').html(format_money(data.amount));
$('#td_status').html(data.status);
$('#td_date').html(data.date);
$('#td_email').html(data.email);
$('#transaction_modal').modal('show');
});
}
$(function() {
var TableTransactions = $('.table-transactions').DataTable({
"order": [[ 0, "desc" ]],
"pageLength": 25
});
});
</script>

101
views/payment.php Normal file
View File

@ -0,0 +1,101 @@
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
<?php echo payment_gateway_head(_l('payment_for_invoice') . ' ' . format_invoice_number($invoice->id)); ?>
<body class="gateway-payment">
<div class="container">
<div class="col-md-8 col-md-offset-2 mtop30">
<!-- Payment Header -->
<div class="mbot30 text-center">
<h2 class="bold"><?php echo _l('payment_for_invoice'); ?></h2>
<h3 class="bold"><?php echo format_invoice_number($invoice->id); ?></h3>
</div>
<!-- Invoice Details -->
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title"><?php echo _l('invoice_details'); ?></h4>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-6">
<p><strong><?php echo _l('invoice_date'); ?>:</strong><br>
<?php echo _d($invoice->date); ?>
</p>
<p><strong><?php echo _l('due_date'); ?>:</strong><br>
<?php echo _d($invoice->duedate); ?>
</p>
</div>
<div class="col-md-6">
<p><strong><?php echo _l('invoice_total'); ?>:</strong><br>
<?php echo app_format_money($invoice->total, $invoice->currency_name); ?>
</p>
<p><strong><?php echo _l('amount_due'); ?>:</strong><br>
<?php echo app_format_money($payment_data['amount'], $payment_data['currency']); ?>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Payment Button -->
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title"><?php echo _l('payment_details'); ?></h4>
</div>
<div class="panel-body">
<div id="payment-loading" class="text-center hide">
<i class="fa fa-spinner fa-spin fa-3x"></i><br />
<?php echo _l('processing_payment'); ?>
</div>
<button type="button" id="payWithPaystack" class="btn btn-success btn-block btn-lg">
<i class="fa fa-lock pad-right-sm"></i> <?php echo _l('pay_securely'); ?>
</button>
<div class="mtop15 text-center">
<img src="<?php echo module_dir_url('paystack', 'assets/img/secured-by-paystack.png'); ?>"
alt="Secured by Paystack" style="max-height: 40px;">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://js.paystack.co/v1/inline.js"></script>
<script>
document.getElementById('payWithPaystack').onclick = function() {
// Show loading
document.getElementById('payment-loading').classList.remove('hide');
this.disabled = true;
let handler = PaystackPop.setup({
key: '<?php echo $this->paystack_gateway->getSetting('paystack_public_key'); ?>',
email: '<?php echo $client->email; ?>',
amount: <?php echo $payment_data['amount'] * 100; ?>,
currency: '<?php echo $payment_data['currency']; ?>',
ref: '<?php echo 'INV_' . $invoice->id . '_' . time(); ?>',
metadata: {
invoice_id: '<?php echo $invoice->id; ?>',
client_id: '<?php echo $invoice->clientid; ?>'
},
callback: function(response) {
window.location.href = '<?php echo site_url('paystack/verify_payment/'); ?>' + response.reference;
},
onClose: function() {
// Hide loading and enable button
document.getElementById('payment-loading').classList.add('hide');
document.getElementById('payWithPaystack').disabled = false;
alert('<?php echo _l('payment_cancelled'); ?>');
}
});
handler.openIframe();
}
</script>
</body>
</html>