From f5d94d630529fc358ae60d7db1d77ad3c07d6ecd Mon Sep 17 00:00:00 2001 From: Shamim Rezaie Date: Sat, 17 Oct 2020 04:43:25 +1100 Subject: [PATCH] MDL-69166 core_payment: Addressing various integration points - Add help for 'payment account' field in the enrol instance form - Remove MOODLE_INTERNALs when not necessary - Add $userid to deliver_order - Check if provider classes implement the provider interface - Rename get_cost to get_payable - get_payable returns payable object - Improve registerEventListeners and added init - Rename payment\provider to payment\service_provider --- .../{provider.php => service_provider.php} | 22 +++--- enrol/fee/classes/plugin.php | 1 + enrol/fee/lang/en/enrol_fee.php | 2 + enrol/fee/templates/payment_region.mustache | 3 +- lang/en/payment.php | 1 - payment/amd/build/gateways_modal.min.js | 2 +- payment/amd/build/gateways_modal.min.js.map | 2 +- payment/amd/src/gateways_modal.js | 42 +++++++---- .../external/get_available_gateways.php | 7 +- payment/classes/helper.php | 73 ++++++++++--------- .../{provider.php => service_provider.php} | 16 ++-- payment/classes/local/entities/payable.php | 70 ++++++++++++++++++ .../classes/external/get_config_for_js.php | 6 +- .../classes/external/transaction_complete.php | 15 ++-- payment/gateway/paypal/classes/gateway.php | 2 - payment/tests/helper_test.php | 2 +- 16 files changed, 174 insertions(+), 92 deletions(-) rename enrol/fee/classes/payment/{provider.php => service_provider.php} (74%) rename payment/classes/local/callback/{provider.php => service_provider.php} (74%) create mode 100644 payment/classes/local/entities/payable.php diff --git a/enrol/fee/classes/payment/provider.php b/enrol/fee/classes/payment/service_provider.php similarity index 74% rename from enrol/fee/classes/payment/provider.php rename to enrol/fee/classes/payment/service_provider.php index 8edd405c80f..cab7b0f4275 100644 --- a/enrol/fee/classes/payment/provider.php +++ b/enrol/fee/classes/payment/service_provider.php @@ -31,25 +31,22 @@ namespace enrol_fee\payment; * @copyright 2020 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class provider implements \core_payment\local\callback\provider { +class service_provider implements \core_payment\local\callback\service_provider { /** - * Callback function that returns the enrolment cost for the course that $instanceid enrolment instance belongs to. + * Callback function that returns the enrolment cost and the accountid + * for the course that $instanceid enrolment instance belongs to. * * @param string $paymentarea * @param int $instanceid The enrolment instance id - * @return array['amount' => float, 'currency' => string, 'accountid' => int] + * @return \core_payment\local\entities\payable */ - public static function get_cost(string $paymentarea, int $instanceid): array { + public static function get_payable(string $paymentarea, int $instanceid): \core_payment\local\entities\payable { global $DB; $instance = $DB->get_record('enrol', ['enrol' => 'fee', 'id' => $instanceid], '*', MUST_EXIST); - return [ - 'amount' => (float) $instance->cost, - 'currency' => $instance->currency, - 'accountid' => $instance->customint1, - ]; + return new \core_payment\local\entities\payable($instance->cost, $instance->currency, $instance->customint1); } /** @@ -58,10 +55,11 @@ class provider implements \core_payment\local\callback\provider { * @param string $paymentarea * @param int $instanceid The enrolment instance id * @param int $paymentid payment id as inserted into the 'payments' table, if needed for reference + * @param int $userid The userid the order is going to deliver to * @return bool Whether successful or not */ - public static function deliver_order(string $paymentarea, int $instanceid, int $paymentid): bool { - global $DB, $USER; + public static function deliver_order(string $paymentarea, int $instanceid, int $paymentid, int $userid): bool { + global $DB; $instance = $DB->get_record('enrol', ['enrol' => 'fee', 'id' => $instanceid], '*', MUST_EXIST); @@ -75,7 +73,7 @@ class provider implements \core_payment\local\callback\provider { $timeend = 0; } - $plugin->enrol_user($instance, $USER->id, $instance->roleid, $timestart, $timeend); + $plugin->enrol_user($instance, $userid, $instance->roleid, $timestart, $timeend); return true; } diff --git a/enrol/fee/classes/plugin.php b/enrol/fee/classes/plugin.php index 5dd1c05fade..78cbbcd4546 100644 --- a/enrol/fee/classes/plugin.php +++ b/enrol/fee/classes/plugin.php @@ -313,6 +313,7 @@ class enrol_fee_plugin extends enrol_plugin { $mform->addElement('hidden', 'customint1'); $mform->setType('customint1', PARAM_INT); } + $mform->addHelpButton('customint1', 'paymentaccount', 'enrol_fee'); $mform->addElement('text', 'cost', get_string('cost', 'enrol_fee'), array('size' => 4)); $mform->setType('cost', PARAM_RAW); diff --git a/enrol/fee/lang/en/enrol_fee.php b/enrol/fee/lang/en/enrol_fee.php index dc97e9964a0..3a9b829fc6e 100644 --- a/enrol/fee/lang/en/enrol_fee.php +++ b/enrol/fee/lang/en/enrol_fee.php @@ -43,6 +43,8 @@ $string['fee:manage'] = 'Manage enrolled users'; $string['fee:unenrol'] = 'Unenrol users from course'; $string['fee:unenrolself'] = 'Unenrol self from course'; $string['nocost'] = 'There is no cost to enrol in this course!'; +$string['paymentaccount'] = 'Payment account'; +$string['paymentaccount_help'] = 'Enrolment fees will be paid to this account.'; $string['pluginname'] = 'Enrolment on payment'; $string['pluginname_desc'] = 'The enrolment on payment enrolment method allows you to set up courses requiring a payment. If the fee for any course is set to zero, then students are not asked to pay for entry. There is a site-wide fee that you set here as a default for the whole site and then a course setting that you can set for each course individually. The course fee overrides the site fee.'; $string['purchasedescription'] = 'Enrolment in course {$a}'; diff --git a/enrol/fee/templates/payment_region.mustache b/enrol/fee/templates/payment_region.mustache index 6fb27c3c383..b8947aaec90 100644 --- a/enrol/fee/templates/payment_region.mustache +++ b/enrol/fee/templates/payment_region.mustache @@ -58,6 +58,7 @@ class="btn btn-secondary" type="button" id="gateways-modal-trigger-{{ uniqid }}" + data-action="core_payment/triggerPayment" data-component="enrol_fee" data-paymentarea="fee" data-itemid="{{instanceid}}" @@ -70,6 +71,6 @@ {{#js}} require(['core_payment/gateways_modal'], function(modal) { - modal.registerEventListeners(document.querySelector('#gateways-modal-trigger-{{ uniqid }}')); + modal.init(); }); {{/js}} diff --git a/lang/en/payment.php b/lang/en/payment.php index c5261b91183..052dbda2dbb 100644 --- a/lang/en/payment.php +++ b/lang/en/payment.php @@ -31,7 +31,6 @@ $string['accountname'] = 'Account name'; $string['accountname_help'] = 'How this account will be identified for teachers or managers who set up payments (for example in the course enrolment plugin)'; $string['accountnotavailable'] = 'Not available'; $string['paymentaccountsexplained'] = 'Create one or multiple payment accounts for this site. Each account includes configuration for available payment gateways. The person who configures payments on the site (for example, payment for the course enrolment) will be able to chose from the available accounts.'; -$string['callbacknotimplemented'] = 'The callback is not implemented for component {$a}.'; $string['createaccount'] = 'Create payment account'; $string['deleteorarchive'] = 'Delete or archive'; $string['eventaccountcreated'] = 'Payment account created'; diff --git a/payment/amd/build/gateways_modal.min.js b/payment/amd/build/gateways_modal.min.js index fabf9e580c2..d177a3d55ed 100644 --- a/payment/amd/build/gateways_modal.min.js +++ b/payment/amd/build/gateways_modal.min.js @@ -1,2 +1,2 @@ -define ("core_payment/gateways_modal",["exports","core/modal_factory","core/templates","core/str","./repository","./selectors","core/modal_events","core_payment/events","core/toast","core/notification","./modal_gateways"],function(a,b,c,d,e,f,g,h,i,j,k){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.registerEventListeners=a.registerEventListenersBySelector=void 0;b=l(b);c=l(c);f=l(f);g=l(g);h=l(h);j=l(j);k=l(k);var o="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};function l(a){return a&&a.__esModule?a:{default:a}}function m(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function n(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var h=a.apply(b,c);function f(a){m(h,d,e,f,g,"next",a)}function g(a){m(h,d,e,f,g,"throw",a)}f(void 0)})}}a.registerEventListenersBySelector=function registerEventListenersBySelector(a){document.querySelectorAll(a).forEach(function(a){p(a)})};var p=function(a){a.addEventListener("click",function(b){b.preventDefault();q(a,{focusOnClose:b.target})})};a.registerEventListeners=p;var q=function(){var a=n(regeneratorRuntime.mark(function a(l){var m,n,o,p,q,u,v,w,x,y,z=arguments;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:m=1.\n\n/**\n * Contain the logic for the gateways modal.\n *\n * @module core_payment/gateways_modal\n * @package core_payment\n * @copyright 2019 Shamim Rezaie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ModalFactory from 'core/modal_factory';\nimport Templates from 'core/templates';\nimport {get_string as getString} from 'core/str';\nimport {getAvailableGateways} from './repository';\nimport Selectors from './selectors';\nimport ModalEvents from 'core/modal_events';\nimport PaymentEvents from 'core_payment/events';\nimport {add as addToast, addToastRegion} from 'core/toast';\nimport Notification from 'core/notification';\nimport ModalGateways from './modal_gateways';\n\n/**\n * Register event listeners for the module.\n *\n * @param {string} nodeSelector The root to listen to.\n */\nexport const registerEventListenersBySelector = (nodeSelector) => {\n document.querySelectorAll(nodeSelector).forEach((element) => {\n registerEventListeners(element);\n });\n};\n\n/**\n * Register event listeners for the module.\n *\n * @param {HTMLElement} rootNode The root to listen to.\n */\nexport const registerEventListeners = (rootNode) => {\n rootNode.addEventListener('click', (e) => {\n e.preventDefault();\n show(rootNode, {focusOnClose: e.target});\n });\n};\n\n/**\n * Shows the gateway selector modal.\n *\n * @param {HTMLElement} rootNode\n * @param {Object} options - Additional options\n * @param {HTMLElement} options.focusOnClose The element to focus on when the modal is closed.\n */\nconst show = async(rootNode, {\n focusOnClose = null,\n} = {}) => {\n const modal = await ModalFactory.create({\n type: ModalGateways.TYPE,\n title: await getString('selectpaymenttype', 'core_payment'),\n body: await Templates.render('core_payment/gateways_modal', {}),\n });\n\n const rootElement = modal.getRoot()[0];\n addToastRegion(rootElement);\n\n modal.show();\n\n modal.getRoot().on(ModalEvents.hidden, () => {\n // Destroy when hidden.\n modal.destroy();\n try {\n focusOnClose.focus();\n } catch (e) {\n // eslint-disable-line\n }\n });\n\n modal.getRoot().on(PaymentEvents.proceed, (e) => {\n const gateway = (rootElement.querySelector(Selectors.values.gateway) || {value: ''}).value;\n\n if (gateway) {\n processPayment(\n gateway,\n rootNode.dataset.component,\n rootNode.dataset.paymentarea,\n rootNode.dataset.itemid,\n rootNode.dataset.description,\n ({success, message = ''}) => {\n modal.hide();\n if (success) {\n Notification.addNotification({\n message: message,\n type: 'success',\n });\n location.reload();\n } else {\n Notification.alert('', message);\n }\n },\n );\n } else {\n // We cannot use await in the following line.\n // The reason is that we are preventing the default action of the save event being triggered,\n // therefore we cannot define the event handler function asynchronous.\n getString('nogatewayselected', 'core_payment').then(message => addToast(message));\n }\n\n e.preventDefault();\n });\n\n // Re-calculate the cost when gateway is changed.\n rootElement.addEventListener('change', e => {\n if (e.target.matches(Selectors.elements.gateways)) {\n updateCostRegion(rootElement, rootNode.dataset.cost);\n }\n });\n\n const gateways = await getAvailableGateways(rootNode.dataset.component, rootNode.dataset.paymentarea, rootNode.dataset.itemid);\n const context = {\n gateways\n };\n\n const {html, js} = await Templates.renderForPromise('core_payment/gateways', context);\n Templates.replaceNodeContents(rootElement.querySelector(Selectors.regions.gatewaysContainer), html, js);\n selectSingleGateway(rootElement);\n await updateCostRegion(rootElement, rootNode.dataset.cost);\n};\n\n/**\n * Auto-select the gateway if there is only one gateway.\n *\n * @param {HTMLElement} root An HTMLElement that contains the cost region\n */\nconst selectSingleGateway = root => {\n const gateways = root.querySelectorAll(Selectors.elements.gateways);\n\n if (gateways.length == 1) {\n gateways[0].checked = true;\n }\n};\n\n/**\n * Shows the cost of the item the user is purchasing in the cost region.\n *\n * @param {HTMLElement} root An HTMLElement that contains the cost region\n * @param {string} defaultCost The default cost that is going to be displayed if no gateway is selected\n * @returns {Promise}\n */\nconst updateCostRegion = async(root, defaultCost = '') => {\n const gatewayElement = root.querySelector(Selectors.values.gateway);\n const surcharge = parseInt((gatewayElement || {dataset: {surcharge: 0}}).dataset.surcharge);\n const cost = (gatewayElement || {dataset: {cost: defaultCost}}).dataset.cost;\n\n const {html, js} = await Templates.renderForPromise('core_payment/fee_breakdown', {fee: cost, surcharge});\n Templates.replaceNodeContents(root.querySelector(Selectors.regions.costContainer), html, js);\n};\n\n/**\n * Process payment using the selected gateway.\n *\n * @param {string} gateway The gateway to be used for payment\n * @param {string} component Name of the component that the itemId belongs to\n * @param {string} paymentArea Name of the area in the component that the itemId belongs to\n * @param {number} itemId An internal identifier that is used by the component\n * @param {string} description Description of the payment\n * @param {processPaymentCallback} callback The callback function to call when processing is finished\n * @returns {Promise}\n */\nconst processPayment = async(gateway, component, paymentArea, itemId, description, callback) => {\n const paymentMethod = await import(`paygw_${gateway}/gateways_modal`);\n paymentMethod.process(component, paymentArea, itemId, description, callback);\n};\n\n/**\n * The callback definition for processPayment.\n *\n * @callback processPaymentCallback\n * @param {bool} success\n * @param {string} message\n */\n"],"file":"gateways_modal.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/gateways_modal.js"],"names":["registerEventListeners","document","addEventListener","e","gatewayTrigger","target","closest","preventDefault","show","focusOnClose","rootNode","ModalFactory","ModalGateways","TYPE","Templates","render","type","title","body","create","modal","rootElement","getRoot","on","ModalEvents","hidden","destroy","focus","PaymentEvents","proceed","gateway","querySelector","Selectors","values","value","processPayment","dataset","component","paymentarea","itemid","description","success","message","hide","Notification","addNotification","location","reload","alert","then","matches","elements","gateways","updateCostRegion","cost","context","renderForPromise","html","js","replaceNodeContents","regions","gatewaysContainer","selectSingleGateway","root","querySelectorAll","length","checked","defaultCost","gatewayElement","surcharge","parseInt","fee","costContainer","paymentArea","itemId","callback","paymentMethod","process","init","initialised"],"mappings":"0UAwBA,OACA,OAGA,OACA,OACA,OAEA,OACA,O,geAKMA,CAAAA,CAAsB,CAAG,UAAM,CACjCC,QAAQ,CAACC,gBAAT,CAA0B,OAA1B,CAAmC,SAAAC,CAAC,CAAI,CACpC,GAAMC,CAAAA,CAAc,CAAGD,CAAC,CAACE,MAAF,CAASC,OAAT,CAAiB,+CAAjB,CAAvB,CACA,GAAIF,CAAJ,CAAoB,CAChBD,CAAC,CAACI,cAAF,GAEAC,CAAI,CAACJ,CAAD,CAAiB,CAACK,YAAY,CAAEN,CAAC,CAACE,MAAjB,CAAjB,CACP,CACJ,CAPD,CAQH,C,CASKG,CAAI,4CAAG,WAAME,CAAN,wJAET,EAFS,KACTD,YADS,CACTA,CADS,YACM,IADN,QAGWE,SAHX,MAICC,UAAcC,IAJf,gBAKQ,iBAAU,mBAAV,CAA+B,cAA/B,CALR,mCAMOC,WAAUC,MAAV,CAAiB,6BAAjB,CAAgD,EAAhD,CANP,0BAILC,IAJK,MAKLC,KALK,MAMLC,IANK,6BAGwBC,MAHxB,yBAGHC,CAHG,QASHC,CATG,CASWD,CAAK,CAACE,OAAN,GAAgB,CAAhB,CATX,CAUT,qBAAeD,CAAf,EAEAD,CAAK,CAACZ,IAAN,GAEAY,CAAK,CAACE,OAAN,GAAgBC,EAAhB,CAAmBC,UAAYC,MAA/B,CAAuC,UAAM,CAEzCL,CAAK,CAACM,OAAN,GACA,GAAI,CACAjB,CAAY,CAACkB,KAAb,EACH,CAAC,MAAOxB,CAAP,CAAU,CAEX,CACJ,CARD,EAUAiB,CAAK,CAACE,OAAN,GAAgBC,EAAhB,CAAmBK,UAAcC,OAAjC,CAA0C,SAAC1B,CAAD,CAAO,CAC7C,GAAM2B,CAAAA,CAAO,CAAG,CAACT,CAAW,CAACU,aAAZ,CAA0BC,UAAUC,MAAV,CAAiBH,OAA3C,GAAuD,CAACI,KAAK,CAAE,EAAR,CAAxD,EAAqEA,KAArF,CAEA,GAAIJ,CAAJ,CAAa,CACTK,CAAc,CACVL,CADU,CAEVpB,CAAQ,CAAC0B,OAAT,CAAiBC,SAFP,CAGV3B,CAAQ,CAAC0B,OAAT,CAAiBE,WAHP,CAIV5B,CAAQ,CAAC0B,OAAT,CAAiBG,MAJP,CAKV7B,CAAQ,CAAC0B,OAAT,CAAiBI,WALP,CAMV,WAA6B,IAA3BC,CAAAA,CAA2B,GAA3BA,OAA2B,KAAlBC,OAAkB,CAAlBA,CAAkB,YAAR,EAAQ,GACzBtB,CAAK,CAACuB,IAAN,GACA,GAAIF,CAAJ,CAAa,CACTG,UAAaC,eAAb,CAA6B,CACzBH,OAAO,CAAEA,CADgB,CAEzB1B,IAAI,CAAE,SAFmB,CAA7B,EAIA8B,QAAQ,CAACC,MAAT,EACH,CAND,IAMO,CACHH,UAAaI,KAAb,CAAmB,EAAnB,CAAuBN,CAAvB,CACH,CACJ,CAjBS,CAmBjB,CApBD,IAoBO,CAIH,iBAAU,mBAAV,CAA+B,cAA/B,EAA+CO,IAA/C,CAAoD,SAAAP,CAAO,QAAI,UAASA,CAAT,CAAJ,CAA3D,CACH,CAEDvC,CAAC,CAACI,cAAF,EACH,CA/BD,EAkCAc,CAAW,CAACnB,gBAAZ,CAA6B,QAA7B,CAAuC,SAAAC,CAAC,CAAI,CACxC,GAAIA,CAAC,CAACE,MAAF,CAAS6C,OAAT,CAAiBlB,UAAUmB,QAAV,CAAmBC,QAApC,CAAJ,CAAmD,CAC/CC,CAAgB,CAAChC,CAAD,CAAcX,CAAQ,CAAC0B,OAAT,CAAiBkB,IAA/B,CACnB,CACJ,CAJD,EA1DS,gBAgEc,2BAAqB5C,CAAQ,CAAC0B,OAAT,CAAiBC,SAAtC,CAAiD3B,CAAQ,CAAC0B,OAAT,CAAiBE,WAAlE,CAA+E5B,CAAQ,CAAC0B,OAAT,CAAiBG,MAAhG,CAhEd,SAgEHa,CAhEG,QAiEHG,CAjEG,CAiEO,CACZH,QAAQ,CAARA,CADY,CAjEP,iBAqEgBtC,WAAU0C,gBAAV,CAA2B,uBAA3B,CAAoDD,CAApD,CArEhB,kBAqEFE,CArEE,GAqEFA,IArEE,CAqEIC,CArEJ,GAqEIA,EArEJ,CAsET5C,UAAU6C,mBAAV,CAA8BtC,CAAW,CAACU,aAAZ,CAA0BC,UAAU4B,OAAV,CAAkBC,iBAA5C,CAA9B,CAA8FJ,CAA9F,CAAoGC,CAApG,EACAI,CAAmB,CAACzC,CAAD,CAAnB,CAvES,gBAwEHgC,CAAAA,CAAgB,CAAChC,CAAD,CAAcX,CAAQ,CAAC0B,OAAT,CAAiBkB,IAA/B,CAxEb,0CAAH,uD,CAgFJQ,CAAmB,CAAG,SAAAC,CAAI,CAAI,CAChC,GAAMX,CAAAA,CAAQ,CAAGW,CAAI,CAACC,gBAAL,CAAsBhC,UAAUmB,QAAV,CAAmBC,QAAzC,CAAjB,CAEA,GAAuB,CAAnB,EAAAA,CAAQ,CAACa,MAAb,CAA0B,CACtBb,CAAQ,CAAC,CAAD,CAAR,CAAYc,OAAZ,GACH,CACJ,C,CASKb,CAAgB,4CAAG,WAAMU,CAAN,iHAAYI,CAAZ,gCAA0B,EAA1B,CACfC,CADe,CACEL,CAAI,CAAChC,aAAL,CAAmBC,UAAUC,MAAV,CAAiBH,OAApC,CADF,CAEfuC,CAFe,CAEHC,QAAQ,CAAC,CAACF,CAAc,EAAI,CAAChC,OAAO,CAAE,CAACiC,SAAS,CAAE,CAAZ,CAAV,CAAnB,EAA8CjC,OAA9C,CAAsDiC,SAAvD,CAFL,CAGff,CAHe,CAGR,CAACc,CAAc,EAAI,CAAChC,OAAO,CAAE,CAACkB,IAAI,CAAEa,CAAP,CAAV,CAAnB,EAAmD/B,OAAnD,CAA2DkB,IAHnD,gBAKIxC,WAAU0C,gBAAV,CAA2B,4BAA3B,CAAyD,CAACe,GAAG,CAAEjB,CAAN,CAAYe,SAAS,CAATA,CAAZ,CAAzD,CALJ,iBAKdZ,CALc,GAKdA,IALc,CAKRC,CALQ,GAKRA,EALQ,CAMrB5C,UAAU6C,mBAAV,CAA8BI,CAAI,CAAChC,aAAL,CAAmBC,UAAU4B,OAAV,CAAkBY,aAArC,CAA9B,CAAmFf,CAAnF,CAAyFC,CAAzF,EANqB,yCAAH,uD,CAoBhBvB,CAAc,4CAAG,WAAML,CAAN,CAAeO,CAAf,CAA0BoC,CAA1B,CAAuCC,CAAvC,CAA+ClC,CAA/C,CAA4DmC,CAA5D,uMACyB7C,CADzB,sOACyBA,CADzB,yDACyBA,CADzB,6BACb8C,CADa,QAEnBA,CAAa,CAACC,OAAd,CAAsBxC,CAAtB,CAAiCoC,CAAjC,CAA8CC,CAA9C,CAAsDlC,CAAtD,CAAmEmC,CAAnE,EAFmB,wCAAH,uD,CAgBPG,CAAI,CAAG,UAAM,CACtB,GAAI,CAACA,CAAI,CAACC,WAAV,CAAuB,CAEnBD,CAAI,CAACC,WAAL,IACA/E,CAAsB,EACzB,CACJ,C,UAQD8E,CAAI,CAACC,WAAL,G","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Contain the logic for the gateways modal.\n *\n * @module core_payment/gateways_modal\n * @package core_payment\n * @copyright 2019 Shamim Rezaie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ModalFactory from 'core/modal_factory';\nimport Templates from 'core/templates';\nimport {get_string as getString} from 'core/str';\nimport {getAvailableGateways} from './repository';\nimport Selectors from './selectors';\nimport ModalEvents from 'core/modal_events';\nimport PaymentEvents from 'core_payment/events';\nimport {add as addToast, addToastRegion} from 'core/toast';\nimport Notification from 'core/notification';\nimport ModalGateways from './modal_gateways';\n\n/**\n * Register event listeners for the module.\n */\nconst registerEventListeners = () => {\n document.addEventListener('click', e => {\n const gatewayTrigger = e.target.closest('[data-action=\"core_payment/triggerPayment\"]');\n if (gatewayTrigger) {\n e.preventDefault();\n\n show(gatewayTrigger, {focusOnClose: e.target});\n }\n });\n};\n\n/**\n * Shows the gateway selector modal.\n *\n * @param {HTMLElement} rootNode\n * @param {Object} options - Additional options\n * @param {HTMLElement} options.focusOnClose The element to focus on when the modal is closed.\n */\nconst show = async(rootNode, {\n focusOnClose = null,\n} = {}) => {\n const modal = await ModalFactory.create({\n type: ModalGateways.TYPE,\n title: await getString('selectpaymenttype', 'core_payment'),\n body: await Templates.render('core_payment/gateways_modal', {}),\n });\n\n const rootElement = modal.getRoot()[0];\n addToastRegion(rootElement);\n\n modal.show();\n\n modal.getRoot().on(ModalEvents.hidden, () => {\n // Destroy when hidden.\n modal.destroy();\n try {\n focusOnClose.focus();\n } catch (e) {\n // eslint-disable-line\n }\n });\n\n modal.getRoot().on(PaymentEvents.proceed, (e) => {\n const gateway = (rootElement.querySelector(Selectors.values.gateway) || {value: ''}).value;\n\n if (gateway) {\n processPayment(\n gateway,\n rootNode.dataset.component,\n rootNode.dataset.paymentarea,\n rootNode.dataset.itemid,\n rootNode.dataset.description,\n ({success, message = ''}) => {\n modal.hide();\n if (success) {\n Notification.addNotification({\n message: message,\n type: 'success',\n });\n location.reload();\n } else {\n Notification.alert('', message);\n }\n },\n );\n } else {\n // We cannot use await in the following line.\n // The reason is that we are preventing the default action of the save event being triggered,\n // therefore we cannot define the event handler function asynchronous.\n getString('nogatewayselected', 'core_payment').then(message => addToast(message));\n }\n\n e.preventDefault();\n });\n\n // Re-calculate the cost when gateway is changed.\n rootElement.addEventListener('change', e => {\n if (e.target.matches(Selectors.elements.gateways)) {\n updateCostRegion(rootElement, rootNode.dataset.cost);\n }\n });\n\n const gateways = await getAvailableGateways(rootNode.dataset.component, rootNode.dataset.paymentarea, rootNode.dataset.itemid);\n const context = {\n gateways\n };\n\n const {html, js} = await Templates.renderForPromise('core_payment/gateways', context);\n Templates.replaceNodeContents(rootElement.querySelector(Selectors.regions.gatewaysContainer), html, js);\n selectSingleGateway(rootElement);\n await updateCostRegion(rootElement, rootNode.dataset.cost);\n};\n\n/**\n * Auto-select the gateway if there is only one gateway.\n *\n * @param {HTMLElement} root An HTMLElement that contains the cost region\n */\nconst selectSingleGateway = root => {\n const gateways = root.querySelectorAll(Selectors.elements.gateways);\n\n if (gateways.length == 1) {\n gateways[0].checked = true;\n }\n};\n\n/**\n * Shows the cost of the item the user is purchasing in the cost region.\n *\n * @param {HTMLElement} root An HTMLElement that contains the cost region\n * @param {string} defaultCost The default cost that is going to be displayed if no gateway is selected\n * @returns {Promise}\n */\nconst updateCostRegion = async(root, defaultCost = '') => {\n const gatewayElement = root.querySelector(Selectors.values.gateway);\n const surcharge = parseInt((gatewayElement || {dataset: {surcharge: 0}}).dataset.surcharge);\n const cost = (gatewayElement || {dataset: {cost: defaultCost}}).dataset.cost;\n\n const {html, js} = await Templates.renderForPromise('core_payment/fee_breakdown', {fee: cost, surcharge});\n Templates.replaceNodeContents(root.querySelector(Selectors.regions.costContainer), html, js);\n};\n\n/**\n * Process payment using the selected gateway.\n *\n * @param {string} gateway The gateway to be used for payment\n * @param {string} component Name of the component that the itemId belongs to\n * @param {string} paymentArea Name of the area in the component that the itemId belongs to\n * @param {number} itemId An internal identifier that is used by the component\n * @param {string} description Description of the payment\n * @param {processPaymentCallback} callback The callback function to call when processing is finished\n * @returns {Promise}\n */\nconst processPayment = async(gateway, component, paymentArea, itemId, description, callback) => {\n const paymentMethod = await import(`paygw_${gateway}/gateways_modal`);\n paymentMethod.process(component, paymentArea, itemId, description, callback);\n};\n\n/**\n * The callback definition for processPayment.\n *\n * @callback processPaymentCallback\n * @param {bool} success\n * @param {string} message\n */\n\n/**\n * Set up the payment actions.\n */\nexport const init = () => {\n if (!init.initialised) {\n // Event listeners should only be registered once.\n init.initialised = true;\n registerEventListeners();\n }\n};\n\n/**\n * Whether the init function was called before.\n *\n * @static\n * @type {boolean}\n */\ninit.initialised = false;\n"],"file":"gateways_modal.min.js"} \ No newline at end of file diff --git a/payment/amd/src/gateways_modal.js b/payment/amd/src/gateways_modal.js index afeeb1b9424..3ae9f66453b 100644 --- a/payment/amd/src/gateways_modal.js +++ b/payment/amd/src/gateways_modal.js @@ -35,24 +35,15 @@ import ModalGateways from './modal_gateways'; /** * Register event listeners for the module. - * - * @param {string} nodeSelector The root to listen to. */ -export const registerEventListenersBySelector = (nodeSelector) => { - document.querySelectorAll(nodeSelector).forEach((element) => { - registerEventListeners(element); - }); -}; +const registerEventListeners = () => { + document.addEventListener('click', e => { + const gatewayTrigger = e.target.closest('[data-action="core_payment/triggerPayment"]'); + if (gatewayTrigger) { + e.preventDefault(); -/** - * Register event listeners for the module. - * - * @param {HTMLElement} rootNode The root to listen to. - */ -export const registerEventListeners = (rootNode) => { - rootNode.addEventListener('click', (e) => { - e.preventDefault(); - show(rootNode, {focusOnClose: e.target}); + show(gatewayTrigger, {focusOnClose: e.target}); + } }); }; @@ -190,3 +181,22 @@ const processPayment = async(gateway, component, paymentArea, itemId, descriptio * @param {bool} success * @param {string} message */ + +/** + * Set up the payment actions. + */ +export const init = () => { + if (!init.initialised) { + // Event listeners should only be registered once. + init.initialised = true; + registerEventListeners(); + } +}; + +/** + * Whether the init function was called before. + * + * @static + * @type {boolean} + */ +init.initialised = false; diff --git a/payment/classes/external/get_available_gateways.php b/payment/classes/external/get_available_gateways.php index 09fa896f7ef..b8a1abf0f66 100644 --- a/payment/classes/external/get_available_gateways.php +++ b/payment/classes/external/get_available_gateways.php @@ -68,10 +68,9 @@ class get_available_gateways extends external_api { $list = []; $gateways = helper::get_available_gateways($params['component'], $params['paymentarea'], $params['itemid']); - [ - 'amount' => $amount, - 'currency' => $currency - ] = helper::get_cost($params['component'], $params['paymentarea'], $params['itemid']); + $payable = helper::get_payable($params['component'], $params['paymentarea'], $params['itemid']); + $amount = $payable->get_amount(); + $currency = $payable->get_currency(); foreach ($gateways as $gateway) { $surcharge = helper::get_gateway_surcharge($gateway); diff --git a/payment/classes/helper.php b/payment/classes/helper.php index 0148a5782c5..f0792b1c932 100644 --- a/payment/classes/helper.php +++ b/payment/classes/helper.php @@ -68,16 +68,14 @@ class helper { public static function get_available_gateways(string $component, string $paymentarea, int $itemid): array { $gateways = []; - [ - 'amount' => $amount, - 'currency' => $currency, - 'accountid' => $accountid, - ] = self::get_cost($component, $paymentarea, $itemid); - $account = new account($accountid); + $payable = static::get_payable($component, $paymentarea, $itemid); + $account = new account($payable->get_account_id()); + if (!$account->get('id') || !$account->get('enabled')) { return $gateways; } + $currency = $payable->get_currency(); foreach ($account->get_gateways() as $plugin => $gateway) { if (!$gateway->get('enabled')) { continue; @@ -149,41 +147,53 @@ class helper { * @param string $description Description of the payment * @return array */ - public static function gateways_modal_link_params(string $component, string $paymentarea, int $itemid, string $description): array { - [ - 'amount' => $amount, - 'currency' => $currency - ] = self::get_cost($component, $paymentarea, $itemid); + public static function gateways_modal_link_params(string $component, string $paymentarea, int $itemid, + string $description): array { + + $payable = static::get_payable($component, $paymentarea, $itemid); return [ 'id' => 'gateways-modal-trigger', 'role' => 'button', + 'data-action' => 'core_payment/triggerPayment', 'data-component' => $component, 'data-paymentarea' => $paymentarea, 'data-itemid' => $itemid, - 'data-cost' => self::get_cost_as_string($amount, $currency), + 'data-cost' => static::get_cost_as_string($payable->get_amount(), $payable->get_currency()), 'data-description' => $description, ]; } /** - * Asks the cost from the related component. + * @param string $component + * @return string + * @throws \coding_exception + */ + private static function get_service_provider_classname(string $component) { + $providerclass = "$component\\payment\\service_provider"; + + if (class_exists($providerclass)) { + $rc = new \ReflectionClass($providerclass); + if ($rc->implementsInterface(local\callback\service_provider::class)) { + return $providerclass; + } + } + + throw new \coding_exception("$component does not have an eligible implementation of payment service_provider."); + } + + /** + * Asks the payable from the related component. * * @param string $component Name of the component that the itemid belongs to * @param string $paymentarea * @param int $itemid An internal identifier that is used by the component - * @return array['amount' => float, 'currency' => string, 'accountid' => int] - * @throws \moodle_exception + * @return local\entities\payable */ - public static function get_cost(string $component, string $paymentarea, int $itemid): array { - $cost = component_class_callback("$component\\payment\\provider", 'get_cost', [$paymentarea, $itemid]); + public static function get_payable(string $component, string $paymentarea, int $itemid): local\entities\payable { + $providerclass = static::get_service_provider_classname($component); - if ($cost === null || !is_array($cost) || !array_key_exists('amount', $cost) - || !array_key_exists('currency', $cost) || !array_key_exists('accountid', $cost) ) { - throw new \moodle_exception('callbacknotimplemented', 'core_payment', '', $component); - } - - return $cost; + return component_class_callback($providerclass, 'get_payable', [$paymentarea, $itemid]); } /** @@ -198,9 +208,9 @@ class helper { */ public static function get_gateway_configuration(string $component, string $paymentarea, int $itemid, string $gatewayname): array { - $x = self::get_cost($component, $paymentarea, $itemid); + $payable = self::get_payable($component, $paymentarea, $itemid); $gateway = null; - $account = new account($x['accountid']); + $account = new account($payable->get_account_id()); if ($account && $account->get('enabled')) { $gateway = $account->get_gateways()[$gatewayname] ?? null; } @@ -213,21 +223,18 @@ class helper { /** * Delivers what the user paid for. * - * @uses \core_payment\local\callback\provider::deliver_order() + * @uses \core_payment\local\callback\service_provider::deliver_order() * * @param string $component Name of the component that the itemid belongs to * @param string $paymentarea * @param int $itemid An internal identifier that is used by the component * @param int $paymentid payment id as inserted into the 'payments' table, if needed for reference + * @param int $userid The userid the order is going to deliver to * @return bool Whether successful or not */ - public static function deliver_order(string $component, string $paymentarea, int $itemid, int $paymentid): bool { - $result = component_class_callback("$component\\payment\\provider", 'deliver_order', - [$paymentarea, $itemid, $paymentid]); - - if ($result === null) { - throw new \moodle_exception('callbacknotimplemented', 'core_payment', '', $component); - } + public static function deliver_order(string $component, string $paymentarea, int $itemid, int $paymentid, int $userid): bool { + $providerclass = static::get_service_provider_classname($component); + $result = component_class_callback($providerclass, 'deliver_order', [$paymentarea, $itemid, $paymentid, $userid]); return $result; } diff --git a/payment/classes/local/callback/provider.php b/payment/classes/local/callback/service_provider.php similarity index 74% rename from payment/classes/local/callback/provider.php rename to payment/classes/local/callback/service_provider.php index 8b6fa60464e..20fe0725e19 100644 --- a/payment/classes/local/callback/provider.php +++ b/payment/classes/local/callback/service_provider.php @@ -15,7 +15,7 @@ // along with Moodle. If not, see . /** - * This file contains the \core_payment\local\local\callback\provider interface. + * This file contains the \core_payment\local\local\callback\service_provider interface. * * Plugins should implement this if they use payment subsystem. * @@ -26,28 +26,28 @@ namespace core_payment\local\callback; -defined('MOODLE_INTERNAL') || die(); - /** - * The provider interface for plugins to provide callbacks which are needed by the payment subsystem. + * The service_provider interface for plugins to provide callbacks which are needed by the payment subsystem. * * @copyright 2020 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -interface provider { +interface service_provider { /** * @param string $paymentarea * @param int $itemid An identifier that is known to the plugin - * @return array['amount' => float, 'currency' => string, 'accountid' => int] + * @return \core_payment\local\entities\payable */ - public static function get_cost(string $paymentarea, int $itemid): array; + public static function get_payable(string $paymentarea, int $itemid): \core_payment\local\entities\payable; /** * @param string $paymentarea * @param int $itemid An identifier that is known to the plugin * @param int $paymentid payment id as inserted into the 'payments' table, if needed for reference + * @param int $userid The userid the order is going to deliver to + * * @return bool Whether successful or not */ - public static function deliver_order(string $paymentarea, int $itemid, int $paymentid): bool; + public static function deliver_order(string $paymentarea, int $itemid, int $paymentid, int $userid): bool; } diff --git a/payment/classes/local/entities/payable.php b/payment/classes/local/entities/payable.php new file mode 100644 index 00000000000..b8f4ed4a262 --- /dev/null +++ b/payment/classes/local/entities/payable.php @@ -0,0 +1,70 @@ +. + +/** + * The payable class. + * + * @package core_payment + * @copyright 2020 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_payment\local\entities; + +/** + * The payable class. + * + * @copyright 2020 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class payable { + private $amount; + private $currency; + private $accountid; + + public function __construct(float $amount, string $currency, int $accountid) { + $this->amount = $amount; + $this->currency = $currency; + $this->accountid = $accountid; + } + + /** + * Get the amount of the payable cost. + * + * @return float + */ + public function get_amount(): float { + return $this->amount; + } + + /** + * Get the currency of the payable cost. + * + * @return string + */ + public function get_currency(): string { + return $this->currency; + } + + /** + * Get the id of the payment account the cost is payable to. + * + * @return int + */ + public function get_account_id(): int { + return $this->accountid; + } +} diff --git a/payment/gateway/paypal/classes/external/get_config_for_js.php b/payment/gateway/paypal/classes/external/get_config_for_js.php index 5850dcb9c1c..342e483bee9 100644 --- a/payment/gateway/paypal/classes/external/get_config_for_js.php +++ b/payment/gateway/paypal/classes/external/get_config_for_js.php @@ -67,14 +67,14 @@ class get_config_for_js extends external_api { ]); $config = helper::get_gateway_configuration($component, $paymentarea, $itemid, 'paypal'); - $cost = helper::get_cost($component, $paymentarea, $itemid); + $payable = helper::get_payable($component, $paymentarea, $itemid); $surcharge = helper::get_gateway_surcharge('paypal'); return [ 'clientid' => $config['clientid'], 'brandname' => $config['brandname'], - 'cost' => helper::get_rounded_cost($cost['amount'], $cost['currency'], $surcharge), - 'currency' => $cost['currency'], + 'cost' => helper::get_rounded_cost($payable->get_amount(), $payable->get_currency(), $surcharge), + 'currency' => $payable->get_currency(), ]; } diff --git a/payment/gateway/paypal/classes/external/transaction_complete.php b/payment/gateway/paypal/classes/external/transaction_complete.php index dba305ea545..891028017f8 100644 --- a/payment/gateway/paypal/classes/external/transaction_complete.php +++ b/payment/gateway/paypal/classes/external/transaction_complete.php @@ -76,15 +76,12 @@ class transaction_complete extends external_api { $config = (object)helper::get_gateway_configuration($component, $paymentarea, $itemid, 'paypal'); $sandbox = $config->environment == 'sandbox'; - [ - 'amount' => $amount, - 'currency' => $currency, - 'accountid' => $accountid, - ] = payment_helper::get_cost($component, $paymentarea, $itemid); + $payable = payment_helper::get_payable($component, $paymentarea, $itemid); + $currency = $payable->get_currency(); // Add surcharge if there is any. $surcharge = helper::get_gateway_surcharge('paypal'); - $amount = helper::get_rounded_cost($amount, $currency, $surcharge); + $amount = helper::get_rounded_cost($payable->get_amount(), $currency, $surcharge); $paypalhelper = new paypal_helper($config->clientid, $config->secret, $sandbox); $orderdetails = $paypalhelper->get_order_details($orderid); @@ -102,8 +99,8 @@ class transaction_complete extends external_api { $success = true; // Everything is correct. Let's give them what they paid for. try { - $paymentid = payment_helper::save_payment((int) $accountid, $component, $paymentarea, $itemid, - (int) $USER->id, $amount, $currency, 'paypal'); + $paymentid = payment_helper::save_payment($payable->get_account_id(), $component, $paymentarea, + $itemid, (int) $USER->id, $amount, $currency, 'paypal'); // Store PayPal extra information. $record = new \stdClass(); @@ -112,7 +109,7 @@ class transaction_complete extends external_api { $DB->insert_record('paygw_paypal', $record); - payment_helper::deliver_order($component, $paymentarea, $itemid, $paymentid); + payment_helper::deliver_order($component, $paymentarea, $itemid, $paymentid, (int) $USER->id); } catch (\Exception $e) { debugging('Exception while trying to process payment: ' . $e->getMessage(), DEBUG_DEVELOPER); $success = false; diff --git a/payment/gateway/paypal/classes/gateway.php b/payment/gateway/paypal/classes/gateway.php index 42519dbb679..f900267b4ed 100644 --- a/payment/gateway/paypal/classes/gateway.php +++ b/payment/gateway/paypal/classes/gateway.php @@ -24,8 +24,6 @@ namespace paygw_paypal; -defined('MOODLE_INTERNAL') || die(); - /** * The gateway class for PayPal payment gateway. * diff --git a/payment/tests/helper_test.php b/payment/tests/helper_test.php index e1c5e296ecd..8d735ab3ec1 100644 --- a/payment/tests/helper_test.php +++ b/payment/tests/helper_test.php @@ -147,7 +147,7 @@ class accounts_testcase extends advanced_testcase { } /** - * Provier for test_get_cost_as_string + * Provider for test_get_cost_as_string * * @return array[] */