From a2adcc8360131c1a8207521a3cbba2907d7a1d35 Mon Sep 17 00:00:00 2001 From: sackey Date: Sun, 19 Jan 2025 12:16:32 +0000 Subject: [PATCH 1/2] Initial commit --- LICENSE | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 ++ 2 files changed, 75 insertions(+) create mode 100644 LICENSE create mode 100644 README.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..655b249 --- /dev/null +++ b/LICENSE @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright 2025 sackey + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..345e9b1 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# paystack + -- 2.47.1 From 6fe27ccc22bab8ff30832a591186529c04b8bdbd Mon Sep 17 00:00:00 2001 From: sackey Date: Sun, 19 Jan 2025 12:18:55 +0000 Subject: [PATCH 2/2] Initial commit --- .idea/.gitignore | 8 + .idea/modules.xml | 8 + .idea/paystack.iml | 8 + .idea/php.xml | 19 ++ .idea/vcs.xml | 6 + config/autoload.php | 4 + controllers/Admin.php | 240 +++++++++++++++++ controllers/Paystack.php | 337 ++++++++++++++++++++++++ helpers/paystack_security_helper.php | 75 ++++++ install.php | 71 +++++ language/english/paystack_lang.php | 127 +++++++++ libraries/Paystack_error_handler.php | 112 ++++++++ libraries/Paystack_gateway.php | 378 +++++++++++++++++++++++++++ models/Paystack_model.php | 210 +++++++++++++++ paystack.php | 84 ++++++ views/admin/dashboard.php | 178 +++++++++++++ views/admin/settings.php | 104 ++++++++ views/admin/test_mode.php | 228 ++++++++++++++++ views/admin/transactions.php | 169 ++++++++++++ views/payment.php | 101 +++++++ 20 files changed, 2467 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/modules.xml create mode 100644 .idea/paystack.iml create mode 100644 .idea/php.xml create mode 100644 .idea/vcs.xml create mode 100644 config/autoload.php create mode 100644 controllers/Admin.php create mode 100644 controllers/Paystack.php create mode 100644 helpers/paystack_security_helper.php create mode 100644 install.php create mode 100644 language/english/paystack_lang.php create mode 100644 libraries/Paystack_error_handler.php create mode 100644 libraries/Paystack_gateway.php create mode 100644 models/Paystack_model.php create mode 100644 paystack.php create mode 100644 views/admin/dashboard.php create mode 100644 views/admin/settings.php create mode 100644 views/admin/test_mode.php create mode 100644 views/admin/transactions.php create mode 100644 views/payment.php diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -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 diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..7b7c9cc --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/paystack.iml b/.idea/paystack.iml new file mode 100644 index 0000000..c956989 --- /dev/null +++ b/.idea/paystack.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml new file mode 100644 index 0000000..f324872 --- /dev/null +++ b/.idea/php.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/config/autoload.php b/config/autoload.php new file mode 100644 index 0000000..7aad0d6 --- /dev/null +++ b/config/autoload.php @@ -0,0 +1,4 @@ +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(); + } +} diff --git a/controllers/Paystack.php b/controllers/Paystack.php new file mode 100644 index 0000000..a7627f9 --- /dev/null +++ b/controllers/Paystack.php @@ -0,0 +1,337 @@ +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); + } +} \ No newline at end of file diff --git a/helpers/paystack_security_helper.php b/helpers/paystack_security_helper.php new file mode 100644 index 0000000..2558295 --- /dev/null +++ b/helpers/paystack_security_helper.php @@ -0,0 +1,75 @@ + 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); +} \ No newline at end of file diff --git a/install.php b/install.php new file mode 100644 index 0000000..1e41f15 --- /dev/null +++ b/install.php @@ -0,0 +1,71 @@ +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']); + } +} \ No newline at end of file diff --git a/language/english/paystack_lang.php b/language/english/paystack_lang.php new file mode 100644 index 0000000..5fbbb49 --- /dev/null +++ b/language/english/paystack_lang.php @@ -0,0 +1,127 @@ +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 = []; + } +} \ No newline at end of file diff --git a/libraries/Paystack_gateway.php b/libraries/Paystack_gateway.php new file mode 100644 index 0000000..e111697 --- /dev/null +++ b/libraries/Paystack_gateway.php @@ -0,0 +1,378 @@ +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 = ' +
+ + + +
'; + + 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 + ]); + } +} \ No newline at end of file diff --git a/models/Paystack_model.php b/models/Paystack_model.php new file mode 100644 index 0000000..f43fa18 --- /dev/null +++ b/models/Paystack_model.php @@ -0,0 +1,210 @@ +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'); + } +} \ No newline at end of file diff --git a/paystack.php b/paystack.php new file mode 100644 index 0000000..bc85194 --- /dev/null +++ b/paystack.php @@ -0,0 +1,84 @@ +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]); diff --git a/views/admin/dashboard.php b/views/admin/dashboard.php new file mode 100644 index 0000000..bd41197 --- /dev/null +++ b/views/admin/dashboard.php @@ -0,0 +1,178 @@ + + +
+
+
+
+
+
+

+
+ + +
+
+
+
+

+
+
+

+
+
+
+
+
+
+

+
+
+

+
+
+
+
+
+
+

+
+
+

+
+
+
+
+
+
+

+
+
+

+
+
+
+
+ +
+ +
+
+
+

+
+ +
+
+
+ + +
+
+
+

+
+ +
+
+
+
+ + +
+
+

+
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+
+
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/views/admin/settings.php b/views/admin/settings.php new file mode 100644 index 0000000..59ee1ee --- /dev/null +++ b/views/admin/settings.php @@ -0,0 +1,104 @@ + + +
+
+
+
+
+
+ +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/views/admin/test_mode.php b/views/admin/test_mode.php new file mode 100644 index 0000000..bac43ee --- /dev/null +++ b/views/admin/test_mode.php @@ -0,0 +1,228 @@ + + +
+
+
+
+
+
+

+ + + + +

+
+ + +
+

+ +
+ + +
+
+

+
+ +
+
+
+ + +
+
+ + +
+ +
+
+
+

+

:

+
Card Number: 4084 0840 8408 4081
+Expiry: 01/25
+CVV: 408
+

:

+
Card Number: 4084 0840 8408 4080
+Expiry: 01/25
+CVV: 408
+
+
+
+
+
+ + +
+
+

+
+ +
+
+

:

+
+ + + + +
+
+ +
+
+
+
+
+
+
+
+ + +
+
+

+
+ +
+
+ +
+ +
+
+
+
+
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/views/admin/transactions.php b/views/admin/transactions.php new file mode 100644 index 0000000..af3b6ff --- /dev/null +++ b/views/admin/transactions.php @@ -0,0 +1,169 @@ + + +
+
+
+
+
+
+

+ +

+
+ + +
+
+ 'GET']); ?> +
+ input->get('start_date')); ?> +
+
+ input->get('end_date')); ?> +
+
+
+ + +
+
+
+ + + + +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+
+
+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/views/payment.php b/views/payment.php new file mode 100644 index 0000000..9c1af84 --- /dev/null +++ b/views/payment.php @@ -0,0 +1,101 @@ + +id)); ?> + + +
+
+ +
+

+

id); ?>

+
+ + +
+
+
+
+

+
+
+
+
+

:
+ date); ?> +

+

:
+ duedate); ?> +

+
+
+

:
+ total, $invoice->currency_name); ?> +

+

:
+ +

+
+
+
+
+
+
+ + +
+
+
+
+

+
+
+
+
+ +
+ +
+ Secured by Paystack +
+
+
+
+
+
+
+ + + + + \ No newline at end of file -- 2.47.1