diff --git a/lang/en/payment.php b/lang/en/payment.php index 632f3be2769..b2cb5167798 100644 --- a/lang/en/payment.php +++ b/lang/en/payment.php @@ -23,7 +23,10 @@ */ $string['callbacknotimplemented'] = 'The callback is not implemented for component {$a}.'; +$string['feeincludesurcharge'] = '{$a->fee} (includes {$a->surcharge}% surcharge for using this payment type)'; $string['nogateway'] = 'There is no payment gateway that can be used.'; $string['nogatewayselected'] = 'You first need to select a payment gateway.'; $string['selectpaymenttype'] = 'Select payment type'; $string['supportedcurrencies'] = 'Supported currencies'; +$string['surcharge'] = 'Surcharge (percentage)'; +$string['surcharge_desc'] = 'The surcharge is an additional percentage charged to users who choose to pay using this payment gateway.'; diff --git a/payment/amd/build/gateways_modal.min.js b/payment/amd/build/gateways_modal.min.js index aa2086b6e22..963edabb6ba 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,t,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 {getGatewaysSupportingCurrency} 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 addToastRegion(modal.getRoot()[0]);\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 root = modal.getRoot()[0];\n const gateway = (root.querySelector(Selectors.values.gateway) || {value: ''}).value;\n\n if (gateway) {\n processPayment(\n gateway,\n rootNode.dataset.amount,\n rootNode.dataset.currency,\n rootNode.dataset.component,\n rootNode.dataset.componentid,\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 const currency = rootNode.dataset.currency;\n const gateways = await getGatewaysSupportingCurrency(currency);\n const context = {\n gateways\n };\n\n const {html, js} = await Templates.renderForPromise('core_payment/gateways', context);\n const root = modal.getRoot()[0];\n Templates.replaceNodeContents(root.querySelector(Selectors.regions.gatewaysContainer), html, js);\n updateCostRegion(root, parseFloat(rootNode.dataset.amount), rootNode.dataset.currency);\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 {number} amount The amount part of cost\n * @param {string} currency The currency part of cost in the 3-letter ISO-4217 format\n * @returns {Promise}\n */\nconst updateCostRegion = async(root, amount, currency) => {\n const locale = await updateCostRegion.locale; // This only takes a bit the first time.\n const localisedCost = amount.toLocaleString(locale, {style: \"currency\", currency: currency});\n\n const {html, js} = await Templates.renderForPromise('core_payment/fee_breakdown', {fee: localisedCost});\n Templates.replaceNodeContents(root.querySelector(Selectors.regions.costContainer), html, js);\n};\nupdateCostRegion.locale = getString(\"localecldr\", \"langconfig\");\n\n/**\n * Process payment using the selected gateway.\n *\n * @param {string} gateway The gateway to be used for payment\n * @param {number} amount Amount of payment\n * @param {string} currency The currency in the three-character ISO-4217 format\n * @param {string} component Name of the component that the componentid belongs to\n * @param {number} componentid 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, amount, currency, component, componentid, description, callback) => {\n const paymentMethod = await import(`pg_${gateway}/gateways_modal`);\n\n paymentMethod.process(amount, currency, component, componentid, 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":["registerEventListenersBySelector","nodeSelector","document","querySelectorAll","forEach","element","registerEventListeners","rootNode","addEventListener","e","preventDefault","show","focusOnClose","target","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","parseFloat","dataset","amount","currency","surcharge","parseInt","component","componentid","description","success","message","hide","Notification","addNotification","location","reload","alert","then","matches","elements","gateways","updateCostRegion","context","renderForPromise","html","js","replaceNodeContents","regions","gatewaysContainer","root","locale","localisedCost","toLocaleString","style","fee","costContainer","callback","paymentMethod","process"],"mappings":"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","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 {getGatewaysSupportingCurrency} 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 {\n value: parseFloat(rootNode.dataset.amount),\n currency: rootNode.dataset.currency,\n surcharge: parseInt((rootElement.querySelector(Selectors.values.gateway) || {dataset: {surcharge: 0}})\n .dataset.surcharge),\n },\n rootNode.dataset.component,\n rootNode.dataset.componentid,\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, parseFloat(rootNode.dataset.amount), rootNode.dataset.currency);\n }\n });\n\n const currency = rootNode.dataset.currency;\n const gateways = await getGatewaysSupportingCurrency(currency);\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 await updateCostRegion(rootElement, parseFloat(rootNode.dataset.amount), rootNode.dataset.currency);\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 {number} amount The amount part of cost\n * @param {string} currency The currency part of cost in the 3-letter ISO-4217 format\n * @returns {Promise}\n */\nconst updateCostRegion = async(root, amount, currency) => {\n const locale = await updateCostRegion.locale; // This only takes a bit the first time.\n const surcharge = parseInt((root.querySelector(Selectors.values.gateway) || {dataset: {surcharge: 0}}).dataset.surcharge);\n amount += amount * surcharge / 100;\n const localisedCost = amount.toLocaleString(locale, {style: \"currency\", currency: currency});\n\n const {html, js} = await Templates.renderForPromise('core_payment/fee_breakdown', {fee: localisedCost, surcharge});\n Templates.replaceNodeContents(root.querySelector(Selectors.regions.costContainer), html, js);\n};\nupdateCostRegion.locale = getString(\"localecldr\", \"langconfig\");\n\n/**\n * Process payment using the selected gateway.\n *\n * @param {string} gateway The gateway to be used for payment\n * @param {Object} amount - Amount of payment\n * @param {number} amount.value The numerical part of the amount\n * @param {string} amount.currency The currency part of the amount in the three-character ISO-4217 format\n * @param {number} amount.surcharge The surcharge percentage that should be added to the amount\n * @param {string} component Name of the component that the componentid belongs to\n * @param {number} componentid 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, {value, currency, surcharge = 0}, component, componentid, description, callback) => {\n const paymentMethod = await import(`pg_${gateway}/gateways_modal`);\n\n value += value * surcharge / 100;\n paymentMethod.process(value, currency, component, componentid, 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 diff --git a/payment/amd/build/selectors.min.js b/payment/amd/build/selectors.min.js index ac36b9675a6..21da99c4c85 100644 --- a/payment/amd/build/selectors.min.js +++ b/payment/amd/build/selectors.min.js @@ -1,2 +1,2 @@ -define ("core_payment/selectors",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;a.default={regions:{gatewaysContainer:"[data-region=\"gateways-container\"]",costContainer:"[data-region=\"fee-breakdown-container\"]"},values:{gateway:"[data-region=\"gateways-container\"] input[type=\"radio\"]:checked"}};return a.default}); +define ("core_payment/selectors",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;a.default={elements:{gateways:"[data-region=\"gateways-container\"] input[type=\"radio\"]"},regions:{gatewaysContainer:"[data-region=\"gateways-container\"]",costContainer:"[data-region=\"fee-breakdown-container\"]"},values:{gateway:"[data-region=\"gateways-container\"] input[type=\"radio\"]:checked"}};return a.default}); //# sourceMappingURL=selectors.min.js.map diff --git a/payment/amd/build/selectors.min.js.map b/payment/amd/build/selectors.min.js.map index e6532f225b6..e8a7ee10683 100644 --- a/payment/amd/build/selectors.min.js.map +++ b/payment/amd/build/selectors.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/selectors.js"],"names":["regions","gatewaysContainer","costContainer","values","gateway"],"mappings":"kJAwBe,CACXA,OAAO,CAAE,CACLC,iBAAiB,CAAE,sCADd,CAELC,aAAa,CAAE,2CAFV,CADE,CAKXC,MAAM,CAAE,CACJC,OAAO,CAAE,oEADL,CALG,C","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 * Define all of the selectors we will be using on the payment interface.\n *\n * @module core_payment/selectors\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\nexport default {\n regions: {\n gatewaysContainer: '[data-region=\"gateways-container\"]',\n costContainer: '[data-region=\"fee-breakdown-container\"]',\n },\n values: {\n gateway: '[data-region=\"gateways-container\"] input[type=\"radio\"]:checked',\n },\n};\n"],"file":"selectors.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/selectors.js"],"names":["elements","gateways","regions","gatewaysContainer","costContainer","values","gateway"],"mappings":"kJAwBe,CACXA,QAAQ,CAAE,CACNC,QAAQ,CAAE,4DADJ,CADC,CAIXC,OAAO,CAAE,CACLC,iBAAiB,CAAE,sCADd,CAELC,aAAa,CAAE,2CAFV,CAJE,CAQXC,MAAM,CAAE,CACJC,OAAO,CAAE,oEADL,CARG,C","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 * Define all of the selectors we will be using on the payment interface.\n *\n * @module core_payment/selectors\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\nexport default {\n elements: {\n gateways: '[data-region=\"gateways-container\"] input[type=\"radio\"]',\n },\n regions: {\n gatewaysContainer: '[data-region=\"gateways-container\"]',\n costContainer: '[data-region=\"fee-breakdown-container\"]',\n },\n values: {\n gateway: '[data-region=\"gateways-container\"] input[type=\"radio\"]:checked',\n },\n};\n"],"file":"selectors.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 e1488d05278..a115e716d88 100644 --- a/payment/amd/src/gateways_modal.js +++ b/payment/amd/src/gateways_modal.js @@ -72,7 +72,8 @@ const show = async(rootNode, { body: await Templates.render('core_payment/gateways_modal', {}), }); - addToastRegion(modal.getRoot()[0]); + const rootElement = modal.getRoot()[0]; + addToastRegion(rootElement); modal.show(); @@ -87,14 +88,17 @@ const show = async(rootNode, { }); modal.getRoot().on(PaymentEvents.proceed, (e) => { - const root = modal.getRoot()[0]; - const gateway = (root.querySelector(Selectors.values.gateway) || {value: ''}).value; + const gateway = (rootElement.querySelector(Selectors.values.gateway) || {value: ''}).value; if (gateway) { processPayment( gateway, - rootNode.dataset.amount, - rootNode.dataset.currency, + { + value: parseFloat(rootNode.dataset.amount), + currency: rootNode.dataset.currency, + surcharge: parseInt((rootElement.querySelector(Selectors.values.gateway) || {dataset: {surcharge: 0}}) + .dataset.surcharge), + }, rootNode.dataset.component, rootNode.dataset.componentid, rootNode.dataset.description, @@ -121,6 +125,13 @@ const show = async(rootNode, { e.preventDefault(); }); + // Re-calculate the cost when gateway is changed. + rootElement.addEventListener('change', e => { + if (e.target.matches(Selectors.elements.gateways)) { + updateCostRegion(rootElement, parseFloat(rootNode.dataset.amount), rootNode.dataset.currency); + } + }); + const currency = rootNode.dataset.currency; const gateways = await getGatewaysSupportingCurrency(currency); const context = { @@ -128,9 +139,8 @@ const show = async(rootNode, { }; const {html, js} = await Templates.renderForPromise('core_payment/gateways', context); - const root = modal.getRoot()[0]; - Templates.replaceNodeContents(root.querySelector(Selectors.regions.gatewaysContainer), html, js); - updateCostRegion(root, parseFloat(rootNode.dataset.amount), rootNode.dataset.currency); + Templates.replaceNodeContents(rootElement.querySelector(Selectors.regions.gatewaysContainer), html, js); + await updateCostRegion(rootElement, parseFloat(rootNode.dataset.amount), rootNode.dataset.currency); }; /** @@ -143,9 +153,11 @@ const show = async(rootNode, { */ const updateCostRegion = async(root, amount, currency) => { const locale = await updateCostRegion.locale; // This only takes a bit the first time. + const surcharge = parseInt((root.querySelector(Selectors.values.gateway) || {dataset: {surcharge: 0}}).dataset.surcharge); + amount += amount * surcharge / 100; const localisedCost = amount.toLocaleString(locale, {style: "currency", currency: currency}); - const {html, js} = await Templates.renderForPromise('core_payment/fee_breakdown', {fee: localisedCost}); + const {html, js} = await Templates.renderForPromise('core_payment/fee_breakdown', {fee: localisedCost, surcharge}); Templates.replaceNodeContents(root.querySelector(Selectors.regions.costContainer), html, js); }; updateCostRegion.locale = getString("localecldr", "langconfig"); @@ -154,18 +166,21 @@ updateCostRegion.locale = getString("localecldr", "langconfig"); * Process payment using the selected gateway. * * @param {string} gateway The gateway to be used for payment - * @param {number} amount Amount of payment - * @param {string} currency The currency in the three-character ISO-4217 format + * @param {Object} amount - Amount of payment + * @param {number} amount.value The numerical part of the amount + * @param {string} amount.currency The currency part of the amount in the three-character ISO-4217 format + * @param {number} amount.surcharge The surcharge percentage that should be added to the amount * @param {string} component Name of the component that the componentid belongs to * @param {number} componentid An internal identifier that is used by the component * @param {string} description Description of the payment * @param {processPaymentCallback} callback The callback function to call when processing is finished * @returns {Promise} */ -const processPayment = async(gateway, amount, currency, component, componentid, description, callback) => { +const processPayment = async(gateway, {value, currency, surcharge = 0}, component, componentid, description, callback) => { const paymentMethod = await import(`pg_${gateway}/gateways_modal`); - paymentMethod.process(amount, currency, component, componentid, description, callback); + value += value * surcharge / 100; + paymentMethod.process(value, currency, component, componentid, description, callback); }; /** diff --git a/payment/amd/src/selectors.js b/payment/amd/src/selectors.js index 1a571e8b458..3fea06c9cc4 100644 --- a/payment/amd/src/selectors.js +++ b/payment/amd/src/selectors.js @@ -23,6 +23,9 @@ */ export default { + elements: { + gateways: '[data-region="gateways-container"] input[type="radio"]', + }, regions: { gatewaysContainer: '[data-region="gateways-container"]', costContainer: '[data-region="fee-breakdown-container"]', diff --git a/payment/classes/external/get_gateways_for_currency.php b/payment/classes/external/get_gateways_for_currency.php index 4028e638fe7..42fe3f02211 100644 --- a/payment/classes/external/get_gateways_for_currency.php +++ b/payment/classes/external/get_gateways_for_currency.php @@ -67,6 +67,7 @@ class get_gateways_for_currency extends external_api { 'shortname' => $gateway, 'name' => get_string('gatewayname', 'pg_' . $gateway), 'description' => get_string('gatewaydescription', 'pg_' . $gateway), + 'surcharge' => \core_payment\helper::get_gateway_surcharge($gateway), ]; } @@ -84,6 +85,7 @@ class get_gateways_for_currency extends external_api { 'shortname' => new external_value(PARAM_PLUGIN, 'Name of the plugin'), 'name' => new external_value(PARAM_TEXT, 'Human readable name of the gateway'), 'description' => new external_value(PARAM_TEXT, 'description of the gateway'), + 'surcharge' => new external_value(PARAM_INT, 'percentage of surcharge when using the gateway'), ]) ); } diff --git a/payment/classes/helper.php b/payment/classes/helper.php index 58c8925ed76..2adf0978570 100644 --- a/payment/classes/helper.php +++ b/payment/classes/helper.php @@ -76,6 +76,16 @@ class helper { return $gateways; } + /** + * Returns the percentage of surcharge that is applied when using a gateway + * + * @param string $gateway Name of the gateway + * @return int + */ + public static function get_gateway_surcharge(string $gateway): int { + return get_config('pg_' . $gateway, 'surcharge') ?: 0; + } + /** * Returns the attributes to place on a pay button. * @@ -164,4 +174,16 @@ class helper { return $id; } + + /** + * This functions adds the settings that are common for all payment gateways. + * + * @param \admin_settingpage $settings The settings object + * @param string $gateway The gateway name prefic with pg_ + */ + public static function add_common_gateway_settings(\admin_settingpage $settings, string $gateway): void { + $settings->add(new \admin_setting_configtext($gateway . '/surcharge', get_string('surcharge', 'core_payment'), + get_string('surcharge_desc', 'core_payment'), 0, PARAM_INT)); + + } } diff --git a/payment/gateway/paypal/classes/external/transaction_complete.php b/payment/gateway/paypal/classes/external/transaction_complete.php index 2b95d2ae72e..889f13a99c1 100644 --- a/payment/gateway/paypal/classes/external/transaction_complete.php +++ b/payment/gateway/paypal/classes/external/transaction_complete.php @@ -77,6 +77,11 @@ class transaction_complete extends external_api { 'currency' => $currency ] = payment_helper::get_cost($component, $componentid); + // Add surcharge if there is any. + if ($config->surcharge) { + $amount += $amount * $config->surcharge / 100; + } + $paypalhelper = new paypal_helper($config->clientid, $config->secret, $sandbox); $orderdetails = $paypalhelper->get_order_details($orderid); diff --git a/payment/gateway/paypal/settings.php b/payment/gateway/paypal/settings.php index cdd850af9ed..2463cfaf215 100644 --- a/payment/gateway/paypal/settings.php +++ b/payment/gateway/paypal/settings.php @@ -41,4 +41,6 @@ if ($ADMIN->fulltree) { ]; $settings->add(new admin_setting_configselect('pg_paypal/environment', get_string('environment', 'pg_paypal'), get_string('environment_desc', 'pg_paypal'), 'live', $options)); + + \core_payment\helper::add_common_gateway_settings($settings, 'pg_paypal'); } diff --git a/payment/templates/fee_breakdown.mustache b/payment/templates/fee_breakdown.mustache index 2e74fd031b6..1483461515f 100644 --- a/payment/templates/fee_breakdown.mustache +++ b/payment/templates/fee_breakdown.mustache @@ -31,8 +31,19 @@ }}
- {{# str }} labelvalue, core, { - "label": {{# quote }}{{# str }} cost {{/ str }}{{/ quote }}, - "value": "{{fee}}" - } {{/ str }} + {{#surcharge}} + {{# str }} labelvalue, core, { + "label": {{# quote }}{{# str }} cost {{/ str }}{{/ quote }}, + "value": {{# quote }}{{# str }} feeincludesurcharge, core_payment, { + "fee": "{{fee}}", + "surcharge": {{surcharge}} + } {{/ str }}{{/ quote }} + } {{/ str }} + {{/surcharge}} + {{^surcharge}} + {{# str }} labelvalue, core, { + "label": {{# quote }}{{# str }} cost {{/ str }}{{/ quote }}, + "value": "{{fee}}" + } {{/ str }} + {{/surcharge}}
diff --git a/payment/templates/gateway.mustache b/payment/templates/gateway.mustache index e93fbadc836..9e2c41aedfa 100644 --- a/payment/templates/gateway.mustache +++ b/payment/templates/gateway.mustache @@ -29,6 +29,7 @@ * shortname * name * description + * surcharge * image Example context (json): @@ -36,11 +37,12 @@ "shortname": "paypal", "name": "PayPal", "description": "A description for PayPal.", + "surcharge": "3" } }}
- +