diff --git a/README.md b/README.md index 87fed4f..ef2a3a3 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,36 @@ -# Paystack Enrolment Plugin # +

Payment Forms for Paystack

+ + +# Paystack Enrolment Plugin Enrolment in Moodle using the Paystack gateway for paid courses -This plugin helps admins and webmasters use Paystack as the payment gateway. This plugin has all the settings for development as well as for production usage. Its easy to install, set up and effective. +This plugin helps admins and webmasters use Paystack as the payment gateway. This plugin has all the settings for development as well as for production usage. Its easy to install, set up and effective. + +## Installation + +Login to your moodle site as an “admin user” and follow the steps. + +1) Upload the zip package from Site administration > Plugins > Install plugins. Choose Plugin type 'Enrolment method (enrol)'. Upload the ZIP package, check the acknowledgement and install. + +2) Go to Enrolments > Manage enrol plugins > Enable 'Paystack' from list + +3) Click 'Settings' which will lead to the settings page of the plugin + +4) Provide merchant credentials for Paystack. Note that, you will get all the details from your merchant account. Now select the checkbox as per requirement. +Choose the paystack connection mode, for test mode it uses the test api keys and for live mode uses the live api keys. Save the settings. + +5) Select any course from course listing page. + +6) Go to Course administration > Users > Enrolment methods > Add method 'Paystack' from the dropdown. Set 'Custom instance name', 'Enrol cost' etc and add the method. + +This completes all the steps from the administrator end. Now registered users can login to the Moodle site and view the course after a successful payment. + + + +## Contribution -TODO Provide more detailed description here. +Here you can browse the source, look at open issues and keep track of development. ## License ## diff --git a/classes/Paystack.php b/classes/paystack.php similarity index 82% rename from classes/Paystack.php rename to classes/paystack.php index c10f820..61bbd3d 100644 --- a/classes/Paystack.php +++ b/classes/paystack.php @@ -22,21 +22,21 @@ defined('MOODLE_INTERNAL') || die(); -class paystack { +class paystack +{ public $plugin_name; public $public_key; public $secret_key; - public function __construct($plugin, $pk, $sk){ - //configure plugin name - //configure public key + public function __construct($plugin, $pk, $sk) + { $this->base_url = "https://api.paystack.co/"; $this->plugin_name = $plugin; $this->public_key = $pk; $this->secret_key = $sk; } - /** + /** * Verify Payment Transaction * * @param string $reference @@ -61,10 +61,13 @@ public function initialize_transaction($data) ]); $request = curl_exec($curl); + $res = json_decode($request, true); + curl_close($curl); + if (curl_errno($curl)) { - throw new moodle_exception( + throw new \moodle_exception( 'errpaystackconnect', 'enrol_paystack', '', @@ -73,12 +76,10 @@ public function initialize_transaction($data) ); } - curl_close($curl); - return $res; } - - /** + + /** * Verify Payment Transaction * * @param string $reference @@ -89,6 +90,7 @@ public function verify_transaction($reference) $paystackUrl = $this->base_url . "transaction/verify/" . $reference; $curl = curl_init(); + curl_setopt_array($curl, [ CURLOPT_URL => $paystackUrl, CURLOPT_RETURNTRANSFER => true, @@ -101,11 +103,13 @@ public function verify_transaction($reference) ]); $request = curl_exec($curl); - $res = json_decode($request, true); // $code = curl_getinfo($curl, CURLINFO_HTTP_CODE); + $res = json_decode($request, true); + + curl_close($curl); if (curl_errno($curl)) { - throw new moodle_exception( + throw new \moodle_exception( 'errpaystackconnect', 'enrol_paystack', '', @@ -114,8 +118,6 @@ public function verify_transaction($reference) ); } - curl_close($curl); - return $res; } @@ -129,19 +131,31 @@ public function log_transaction_success($reference) { //send reference to logger along with plugin name and public key $url = "https://plugin-tracker.paystackintegrations.com/log/charge_success"; + $params = [ + 'public_key' => $this->public_key, 'plugin_name' => $this->plugin_name, 'transaction_reference' => $reference, - 'public_key' => $this->public_key ]; - $params_string = http_build_query($params); - $ch = curl_init(); - curl_setopt($ch,CURLOPT_URL, $url); - curl_setopt($ch,CURLOPT_POST, true); - curl_setopt($ch,CURLOPT_POSTFIELDS, $params_string); - curl_setopt($ch,CURLOPT_RETURNTRANSFER, true); - //execute post - curl_exec($ch); + + $curl = curl_init(); + + curl_setopt_array($curl, [ + CURLOPT_URL => $url, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => http_build_query($params), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => [ + "content-type: application/json", + "cache-control: no-cache" + ], + ]); + + // execute post + curl_exec($curl); + + // close conection + curl_close($curl); } /** @@ -152,6 +166,6 @@ public function log_transaction_success($reference) */ public function validate_webhook($input) { - return $_SERVER['HTTP_X_PAYSTACK_SIGNATURE'] !== hash_hmac('sha512', $input, $this->secret_key); + return $_SERVER['HTTP_X_PAYSTACK_SIGNATURE'] !== hash_hmac('sha512', $input, $this->secret_key); } } diff --git a/classes/util.php b/classes/util.php new file mode 100644 index 0000000..bfd2113 --- /dev/null +++ b/classes/util.php @@ -0,0 +1,89 @@ +. + +/** + * PayPal enrolment plugin utility class. + * + * @package enrol_paystack + * @copyright 2019 Paystack + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace enrol_paystack; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Paystack enrolment plugin utility class. + * + * @package enrol_paystack + * @copyright 2019 Paystack + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class util { + + /** + * Send payment error message to the admin. + * + * @param string $subject + * @param stdClass $data + */ + public static function message_paystack_error_to_admin($subject, $data) + { + $admin = get_admin(); + $site = get_site(); + $message = "$site->fullname: Transaction failed.\n\n$subject\n\n"; + foreach ($data as $key => $value) { + $message .= s($key) . " => " . s($value) . "\n"; + } + $eventdata = new \core\message\message(); + $eventdata->modulename = 'moodle'; + $eventdata->component = 'enrol_paystack'; + $eventdata->name = 'paystack_enrolment'; + $eventdata->userfrom = $admin; + $eventdata->userto = $admin; + $eventdata->subject = "PAYSTACK PAYMENT ERROR: " . $subject; + $eventdata->fullmessage = $message; + $eventdata->fullmessageformat = FORMAT_PLAIN; + $eventdata->fullmessagehtml = ''; + $eventdata->smallmessage = ''; + message_send($eventdata); + } + + /** + * Silent exception handler. + * + * @return callable exception handler + */ + public static function get_exception_handler() { + return function($ex) { + $info = get_exception_info($ex); + + $logerrmsg = "enrol_paystack exception handler: ".$info->message; + if (debugging('', DEBUG_NORMAL)) { + $logerrmsg .= ' Debug: '.$info->debuginfo."\n".format_backtrace($info->backtrace, true); + } + error_log($logerrmsg); + + if (http_response_code() == 200) { + http_response_code(500); + } + + exit(0); + }; + } + +} diff --git a/edit.php b/edit.php index 4dc167e..2b6af1e 100755 --- a/edit.php +++ b/edit.php @@ -86,8 +86,16 @@ } } else { - $fields = array('status' => $data->status, 'name' => $data->name, 'cost' => unformat_float($data->cost), - 'currency' => $data->currency, 'roleid' => $data->roleid, 'enrolperiod' => $data->enrolperiod, 'customint3' => $data->customint3, 'enrolstartdate' => $data->enrolstartdate, 'enrolenddate' => $data->enrolenddate); + $fields = array('status' => $data->status, + 'name' => $data->name, + 'cost' => unformat_float($data->cost), + 'currency' => $data->currency, + 'roleid' => $data->roleid, + 'enrolperiod' => $data->enrolperiod, + 'customint3' => $data->customint3, + 'enrolstartdate' => $data->enrolstartdate, + 'enrolenddate' => $data->enrolenddate + ); $plugin->add_instance($course, $fields); } diff --git a/enrol.html b/enrol.html index 31ee7e2..ce16922 100644 --- a/enrol.html +++ b/enrol.html @@ -24,17 +24,28 @@

"last_name" => s($userlastname), "email" => s($useremail), "reference" => $reference, + "custom_fields" => $customfields ]; ?>
wwwroot/enrol/paystack/verify.php"?>"> - + $value) { + echo ''; + } + ?> id}-{$course->id}-{$instance->id}" ?>" /> -
diff --git a/lang/en/enrol_paystack.php b/lang/en/enrol_paystack.php index a41b474..2d494ec 100644 --- a/lang/en/enrol_paystack.php +++ b/lang/en/enrol_paystack.php @@ -35,6 +35,8 @@ $string['costerror'] = 'The enrolment cost is not numeric'; $string['costorkey'] = 'Please choose one of the following methods of enrolment.'; $string['currency'] = 'Currency'; +$string['customfields'] = 'Profile fields to be used as custom fields'; +$string['customfields_desc'] = 'Which user profile fields can be used during enrolment'; $string['customwelcomemessage'] = 'Custom welcome message'; $string['customwelcomemessage_help'] = 'If you enter some text here, it will be shown instead of the standard text "This course requires a payment for entry." on the Enrolment options page that students see when they attempt to access a course they are not enrolled in. If you leave this blank, the standard text will be used.'; $string['defaultrole'] = 'Default role assignment'; diff --git a/lib.php b/lib.php index 3b6acac..0b67c90 100644 --- a/lib.php +++ b/lib.php @@ -399,6 +399,7 @@ public function enrol_page_hook(stdClass $instance) $userlastname = $USER->lastname; $useremail = $USER->email; $instancename = $this->get_instance_name($instance); + $customfields = $this->get_custom_fields(); $publickey = $this->get_publickey(); $reference = $this->getHashedToken(); @@ -409,6 +410,35 @@ public function enrol_page_hook(stdClass $instance) return $OUTPUT->box(ob_get_clean()); } + /** + * Get all custom fields available for plugin. + * + * @return $customfields. + */ + public function get_custom_fields() + { + global $USER, $DB; + + $customfieldrecords = $DB->get_records('user_info_field'); + $configured_customfields = explode(',', get_config('enrol_paystack', 'customfields')); + + $customfields = []; + + foreach ($customfieldrecords as $cus) { + foreach($configured_customfields as $con) { + if($con == $cus->shortname){ + $customfields[] = [ + 'display_name' => $cus->name , + 'variable_name' => $cus->shortname, + 'value' => $USER->profile[$con] + ]; + } + } + } + + return $customfields; + } + /** * Lists all currencies available for plugin. * @@ -416,7 +446,8 @@ public function enrol_page_hook(stdClass $instance) */ public function get_currencies() { - $codes = array('NGN', 'USD', 'GHS'); + $codes = array('NGN', 'USD', 'GHS', 'KES', 'XOF', 'ZAR','EGP'); + $currencies = array(); foreach ($codes as $c) { $currencies[$c] = new lang_string($c, 'core_currencies'); diff --git a/pix/banner.png b/pix/banner.png new file mode 100644 index 0000000..0c2c154 Binary files /dev/null and b/pix/banner.png differ diff --git a/settings.php b/settings.php index 1964b67..172d9f8 100644 --- a/settings.php +++ b/settings.php @@ -123,6 +123,19 @@ $options )); + // Profile fields to use in the selector + $customfieldrecords = $DB->get_records('user_info_field'); + if ($customfieldrecords) { + $customfields = []; + foreach ($customfieldrecords as $customfieldrecord) { + $customfields[$customfieldrecord->shortname] = $customfieldrecord->name; + } + asort($customfields); + $settings->add(new admin_setting_configmultiselect('enrol_paystack/customfields', + get_string('customfields', 'enrol_paystack'), get_string('customfields_desc', 'enrol_paystack'), + [], $customfields)); + } + // --- enrol instance defaults ---------------------------------------------------------------------------- $settings->add(new admin_setting_heading( 'enrol_paystack_defaults', diff --git a/verify.php b/verify.php index b6b07f6..1ca5ffc 100644 --- a/verify.php +++ b/verify.php @@ -11,65 +11,250 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** - * Verify Payment Callback from Paystack + * Listens for Instant Payment Notification from Paystack + * + * This script waits for Payment notification from Paystack, + * then double checks that data by sending it back to Paystack. + * If Paystack verifies this then it sets up the enrolment for that + * user. * * @package enrol_paystack * @copyright 2019 Paystack * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -require("../../config.php"); -require_once("$CFG->dirroot/enrol/paystack/lib.php"); +// Disable moodle specific debug messages and any errors in output, +// comment out when debugging or better look into error log! +define('NO_DEBUG_DISPLAY', true); + +require('../../config.php'); +require_once('lib.php'); +if ($CFG->version < 2018101900) { + require_once($CFG->libdir . '/eventslib.php'); +} +require_once($CFG->libdir . '/enrollib.php'); +require_once($CFG->libdir . '/filelib.php'); + +require_login(); + +// Paystack does not like when we return error messages here, +// the custom handler just logs exceptions and stops. +//set_exception_handler('enrol_paystack_charge_exception_handler'); -$custom = explode('-', optional_param('custom', array(), PARAM_RAW)); -$userid = (int)$custom[0]; -$courseid = (int)$custom[1]; -$instanceid = (int)$custom[2]; +set_exception_handler(array('enrol_paystack\util', 'get_exception_handler')); -if (!$course = $DB->get_record("course", array("id" => $courseid))) { - redirect($CFG->wwwroot); + +// Make sure we are enabled in the first place. +if (!enrol_is_enabled('paystack')) { + http_response_code(503); + throw new moodle_exception('errdisabled', 'enrol_paystack'); } -$context = context_course::instance($course->id, MUST_EXIST); +// Keep out casual intruders. +if (empty($_POST) or !empty($_GET)) { + http_response_code(400); + throw new moodle_exception('invalidrequest', 'core_error'); +} +if (empty(required_param('paystack-trxref', PARAM_RAW))) { + print_error(get_string('paystack_sorry', 'enrol_paystack')); +} +$data = new stdClass(); + +foreach ($_POST as $key => $value) { + if ($key !== clean_param($key, PARAM_ALPHANUMEXT)) { + throw new moodle_exception('invalidrequest', 'core_error', '', null, $key); + } + if (is_array($value)) { + throw new moodle_exception('invalidrequest', 'core_error', '', null, 'Unexpected array param: ' . $key); + } + $data->$key = fix_utf8($value); +} + +if (empty($data->custom)) { + throw new moodle_exception('invalidrequest', 'core_error', '', null, 'Missing request param: custom'); +} +$custom = explode('-', $data->custom); +unset($data->custom); +if (empty($custom) || count($custom) < 3) { + throw new moodle_exception('invalidrequest', 'core_error', '', null, 'Invalid value of the request param: custom'); +} + +$data->userid = (int) $custom[0]; +$data->courseid = (int) $custom[1]; +$data->instanceid = (int) $custom[2]; +$data->payment_gross = $data->amount; +$data->payment_currency = $data->currency_code; +$data->timeupdated = time(); + +// Get the user and course records. +$user = $DB->get_record("user", array("id" => $data->userid), "*", MUST_EXIST); +$course = $DB->get_record("course", array("id" => $data->courseid), "*", MUST_EXIST); +$context = context_course::instance($course->id, MUST_EXIST); $PAGE->set_context($context); -require_login(); +// Use the queried course's full name for the item_name field. +$data->item_name = $course->fullname; + +$plugin_instance = $DB->get_record("enrol", array("id" => $data->instanceid, "enrol" => "paystack", "status" => 0), "*", MUST_EXIST); +$plugin = enrol_get_plugin('paystack'); +$paystack = new \enrol_paystack\paystack('moodle-enrol', $plugin->get_publickey(), $plugin->get_secretkey()); + +// Set Course and Paystack Url +$courseUrl = "$CFG->wwwroot/course/view.php?id=$course->id"; + +// Verify Transaction +$res = $paystack->verify_transaction($data->reference); + +if (!$res['status']) { + notice($res['message'], $courseUrl); +} + +// Send the file, this line will be reached if no error was thrown above. +$data->tax = $res['data']['amount'] / 100; +$data->memo = $res['data']['gateway_response']; +$data->payment_status = $res['data']['status']; +$data->reason_code = $code; +// If currency is incorrectly set then someone maybe trying to cheat the system +if ($data->currency_code != $plugin_instance->currency) { + $message = "Currency does not match course settings, received: " . $data->currency_code; + \enrol_paystack\util::message_paystack_error_to_admin( + $message, + $data + ); + notice($message, $courseUrl); +} -if (!empty($SESSION->wantsurl)) { - $destination = $SESSION->wantsurl; - unset($SESSION->wantsurl); +// Check that amount paid is the correct amount +if ((float) $plugin_instance->cost <= 0) { + $cost = (float) $plugin->get_config('cost'); } else { - $destination = "$CFG->wwwroot/course/view.php?id=$course->id"; + $cost = (float) $plugin_instance->cost; } -if (empty(required_param('paystack-trxref', PARAM_RAW))) { - notice(get_string('paystack_sorry', 'enrol_paystack'), $destination); +// Use the same rounding of floats as on the enrol form. +$cost = format_float($cost, 2, false); + +// If cost is greater than payment_gross, then someone maybe trying to cheat the system +if ($data->payment_gross < $cost) { + $message = "Amount paid is not enough ($data->payment_gross < $cost))"; + \enrol_paystack\util::message_paystack_error_to_admin( + $message, + $data + ); + notice($message, $courseUrl); } -$ref = required_param('paystack-trxref', PARAM_RAW); $fullname = format_string($course->fullname, true, array('context' => $context)); -if (is_enrolled($context, NULL, '', true)) { - // use real paystack check - $plugin = enrol_get_plugin('paystack'); - $plugin_instance = $DB->get_record("enrol", array("id" => $instanceid, "enrol" => "paystack", "status" => 0), "*", MUST_EXIST); - $paystack = new \enrol_paystack\Paystack('moodle-enrol', $plugin->get_publickey(), $plugin->secretkey()); - $res = $paystack->verify_transaction($ref); - if ($res['data']['status'] != "success") { - $plugin->unenrol_user($plugin_instance, $userid); - message_paystack_error_to_admin( - "Status not successful. User unenrolled from course", - $res - ); - redirect($CFG->wwwroot); +if (is_enrolled($context, null, '', true)) { + redirect($courseUrl, get_string('paymentthanks', '', $fullname)); +} + +if ($data->payment_status == 'success') { + // ALL CLEAR ! + $paystack->log_transaction_success($data->reference); + $DB->insert_record("enrol_paystack", $data); + if ($plugin_instance->enrolperiod) { + $timestart = time(); + $timeend = $timestart + $plugin_instance->enrolperiod; + } else { + $timestart = 0; + $timeend = 0; + } + // Enrol user. + $plugin->enrol_user($plugin_instance, $user->id, $plugin_instance->roleid, $timestart, $timeend); + // Pass $view=true to filter hidden caps if the user cannot see them. + if ($users = get_users_by_capability( + $context, + 'moodle/course:update', + 'u.*', + 'u.id ASC', + '', + '', + '', + '', + false, + true + )) { + $users = sort_by_roleassignment_authority($users, $context); + $teacher = array_shift($users); + } else { + $teacher = false; + } + $mailstudents = $plugin->get_config('mailstudents'); + $mailteachers = $plugin->get_config('mailteachers'); + $mailadmins = $plugin->get_config('mailadmins'); + $shortname = format_string($course->shortname, true, array('context' => $context)); + if (!empty($mailstudents)) { + $a = new stdClass(); + $a->course = format_string($course->fullname, true, array('context' => $coursecontext)); + $a->profileurl = "$CFG->wwwroot/user/view.php?id=$user->id"; + $eventdata = new \core\message\message(); + $eventdata->modulename = 'moodle'; + $eventdata->component = 'enrol_paystack'; + $eventdata->name = 'paystack_enrolment'; + $eventdata->userfrom = empty($teacher) ? core_user::get_support_user() : $teacher; + $eventdata->userto = $user; + $eventdata->subject = get_string("enrolmentnew", 'enrol', $shortname); + $eventdata->fullmessage = get_string('welcometocoursetext', '', $a); + $eventdata->fullmessageformat = FORMAT_PLAIN; + $eventdata->fullmessagehtml = ''; + $eventdata->smallmessage = ''; + message_send($eventdata); + } + if (!empty($mailteachers) && !empty($teacher)) { + $a = new stdClass(); + $a->course = format_string($course->fullname, true, array('context' => $coursecontext)); + $a->user = fullname($user); + $eventdata = new \core\message\message(); + $eventdata->modulename = 'moodle'; + $eventdata->component = 'enrol_paystack'; + $eventdata->name = 'paystack_enrolment'; + $eventdata->userfrom = $user; + $eventdata->userto = $teacher; + $eventdata->subject = get_string("enrolmentnew", 'enrol', $shortname); + $eventdata->fullmessage = get_string('enrolmentnewuser', 'enrol', $a); + $eventdata->fullmessageformat = FORMAT_PLAIN; + $eventdata->fullmessagehtml = ''; + $eventdata->smallmessage = ''; + message_send($eventdata); } - redirect($destination, get_string('paymentthanks', '', $fullname)); -} else { - // Somehow they aren't enrolled yet! :-( - $PAGE->set_url($destination); + if (!empty($mailadmins)) { + $a = new stdClass(); + $a->course = format_string($course->fullname, true, array('context' => $coursecontext)); + $a->user = fullname($user); + $admins = get_admins(); + foreach ($admins as $admin) { + $eventdata = new \core\message\message(); + $eventdata->modulename = 'moodle'; + $eventdata->component = 'enrol_paystack'; + $eventdata->name = 'paystack_enrolment'; + $eventdata->userfrom = $user; + $eventdata->userto = $admin; + $eventdata->subject = get_string("enrolmentnew", 'enrol', $shortname); + $eventdata->fullmessage = get_string('enrolmentnewuser', 'enrol', $a); + $eventdata->fullmessageformat = FORMAT_PLAIN; + $eventdata->fullmessagehtml = ''; + $eventdata->smallmessage = ''; + message_send($eventdata); + } + } +} else { + $message = "Payment status not successful" . $data->memo; + \enrol_paystack\util::message_paystack_error_to_admin( + $message, + $data + ); + notice($message, $courseUrl); +} + +if (is_enrolled($context, null, '', true)) { + redirect($courseUrl, get_string('paymentthanks', '', $fullname)); +} else { // Somehow they aren't enrolled yet! + $PAGE->set_url($courseUrl); echo $OUTPUT->header(); $a = new stdClass(); $a->teacher = get_string('defaultcourseteacher'); $a->fullname = $fullname; - redirect($destination, get_string('paymentsorry', '', $a), 5); -} + notice(get_string('paymentsorry', '', $a), $courseUrl); +} \ No newline at end of file diff --git a/version.php b/version.php index ba007f4..9454095 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'enrol_paystack'; -$plugin->release = '1.2.1'; -$plugin->version = 2019082822; +$plugin->release = '1.4.1'; +$plugin->version = 2021011421; $plugin->requires = 2018120300; $plugin->maturity = MATURITY_STABLE; diff --git a/webhook.php b/webhook.php index 3f045dc..3ca9692 100644 --- a/webhook.php +++ b/webhook.php @@ -26,18 +26,15 @@ // comment out when debugging or better look into error log! define('NO_DEBUG_DISPLAY', true); -require('../../config.php'); -require_once("$CFG->dirroot/enrol/paystack/lib.php"); - -if ($CFG->version < 2018101900) { - require_once($CFG->libdir . '/eventslib.php'); -} -require_once($CFG->libdir . '/enrollib.php'); +// @codingStandardsIgnoreLine This script does not require login. +require("../../config.php"); +require_once("lib.php"); +require_once($CFG->libdir.'/enrollib.php'); require_once($CFG->libdir . '/filelib.php'); // Paystack does not like when we return error messages here, // the custom handler just logs exceptions and stops. -// set_exception_handler('enrol_paystack_charge_exception_handler'); +set_exception_handler(\enrol_paystack\util::get_exception_handler()); // Make sure we are enabled in the first place. if (!enrol_is_enabled('paystack')) { @@ -52,11 +49,12 @@ } $input = @file_get_contents("php://input"); -$res = (array) json_decode($input, true); +$values = (array) json_decode($input, true); +$metadata = $values['data']['metadata']; $data = new stdClass(); -foreach ($res['data']['metadata'] as $key => $value) { +foreach ($metadata as $key => $value) { if ($key !== clean_param($key, PARAM_ALPHANUMEXT)) { throw new moodle_exception('invalidrequest', 'core_error', '', null, $key); } @@ -80,6 +78,15 @@ $data->payment_currency = $data->currency_code; $data->timeupdated = time(); +$plugin = enrol_get_plugin('paystack'); +$paystack = new \enrol_paystack\paystack('moodle-enrol', $plugin->get_publickey(), $plugin->get_secretkey()); + +// validate event do all at once to avoid timing attack +if($paystack->validate_webhook($input)){ + http_response_code(400); + throw new moodle_exception('invalidrequest', 'core_error'); +} + // Get the user and course records. $user = $DB->get_record("user", array("id" => $data->userid), "*", MUST_EXIST); $course = $DB->get_record("course", array("id" => $data->courseid), "*", MUST_EXIST); @@ -91,18 +98,15 @@ $data->item_name = $course->fullname; $plugin_instance = $DB->get_record("enrol", array("id" => $data->instanceid, "enrol" => "paystack", "status" => 0), "*", MUST_EXIST); -$plugin = enrol_get_plugin('paystack'); -$paystack = new \enrol_paystack\paystack('moodle-enrol', $plugin->get_publickey(), $plugin->get_secretkey()); -// validate event do all at once to avoid timing attack -if($paystack->validate_webhook($input)){ - http_response_code(400); - throw new moodle_exception('invalidrequest', 'core_error'); +if (is_enrolled($context, $data->userid, '', true)) { + \enrol_paystack\util::message_paystack_error_to_admin( + "Webhook Stopped: User already enrolled", + $data + ); + die; } -// Set Course Url -$courseUrl = "$CFG->wwwroot/course/view.php?id=$course->id"; - // Verify Transaction $res = $paystack->verify_transaction($data->reference); @@ -114,7 +118,7 @@ // If currency is incorrectly set then someone maybe trying to cheat the system if ($data->currency_code != $plugin_instance->currency) { - message_paystack_error_to_admin( + \enrol_paystack\util::message_paystack_error_to_admin( "Currency does not match course settings, received: " . $data->currency_code, $data ); @@ -133,7 +137,7 @@ // If cost is greater than payment_gross, then someone maybe trying to cheat the system if ($data->payment_gross < $cost) { - message_paystack_error_to_admin( + \enrol_paystack\util::message_paystack_error_to_admin( "Amount paid is not enough ($data->payment_gross < $cost))", $data ); @@ -230,39 +234,10 @@ } } } else { - message_paystack_error_to_admin( + \enrol_paystack\util::message_paystack_error_to_admin( "Payment status not successful" . $data->memo, $data ); die; } } - -// --- HELPER FUNCTIONS --------------------------------------------------------------------------------------! -/** - * Send payment error message to the admin. - * - * @param string $subject - * @param stdClass $data - */ -function message_paystack_error_to_admin($subject, $data) -{ - $admin = get_admin(); - $site = get_site(); - $message = "$site->fullname: Transaction failed.\n\n$subject\n\n"; - foreach ($data as $key => $value) { - $message .= s($key) . " => " . s($value) . "\n"; - } - $eventdata = new \core\message\message(); - $eventdata->modulename = 'moodle'; - $eventdata->component = 'enrol_paystack'; - $eventdata->name = 'paystack_enrolment'; - $eventdata->userfrom = $admin; - $eventdata->userto = $admin; - $eventdata->subject = "PAYSTACK PAYMENT ERROR: " . $subject; - $eventdata->fullmessage = $message; - $eventdata->fullmessageformat = FORMAT_PLAIN; - $eventdata->fullmessagehtml = ''; - $eventdata->smallmessage = ''; - message_send($eventdata); -}