MDL-69166 pg_paypal: capture payment directly
authorShamim Rezaie <shamim@moodle.com>
Fri, 17 Jan 2020 16:20:41 +0000 (03:20 +1100)
committerShamim Rezaie <shamim@moodle.com>
Tue, 27 Oct 2020 04:44:59 +0000 (15:44 +1100)
payment/gateway/paypal/amd/build/gateways_modal.min.js
payment/gateway/paypal/amd/build/gateways_modal.min.js.map
payment/gateway/paypal/amd/src/gateways_modal.js
payment/gateway/paypal/classes/external/transaction_complete.php
payment/gateway/paypal/classes/paypal_helper.php
payment/gateway/paypal/db/install.xml
payment/gateway/paypal/lang/en/pg_paypal.php
payment/gateway/paypal/settings.php

index 6a780da..fa4872a 100644 (file)
Binary files a/payment/gateway/paypal/amd/build/gateways_modal.min.js and b/payment/gateway/paypal/amd/build/gateways_modal.min.js differ
index 99d5636..503b693 100644 (file)
Binary files a/payment/gateway/paypal/amd/build/gateways_modal.min.js.map and b/payment/gateway/paypal/amd/build/gateways_modal.min.js.map differ
index ba4416e..45fbfb1 100644 (file)
@@ -27,15 +27,15 @@ import Truncate from 'core/truncate';
 import Ajax from 'core/ajax';
 import ModalFactory from 'core/modal_factory';
 import ModalEvents from 'core/modal_events';
+import {get_string as getString} from 'core/str';
 
 /**
  * Creates and shows a modal that contains a placeholder.
  *
  * @returns {Promise<Modal>}
  */
-const showPlaceholder = async() => {
+const showModalWithPlaceholder = async() => {
     const modal = await ModalFactory.create({
-        type: ModalFactory.types.CANCEL,
         body: await Templates.render('pg_paypal/paypal_button_placeholder', {})
     });
     modal.show();
@@ -59,23 +59,28 @@ export const process = async(amount, currency, component, componentid, descripti
         modal,
         paypalConfig,
     ] = await Promise.all([
-        showPlaceholder(),
+        showModalWithPlaceholder(),
         Repository.getConfigForJs(),
     ]);
 
+    modal.getRoot().on(ModalEvents.outsideClick, (e) => {
+        // Prevent closing the modal when clicking outside of it.
+        e.preventDefault();
+    });
+
     modal.getRoot().on(ModalEvents.hidden, () => {
         // Destroy when hidden.
         modal.destroy();
     });
 
-    const paypalScript = `https://www.paypal.com/sdk/js?client-id=${paypalConfig.clientid}&currency=${currency}&intent=authorize`;
+    const paypalScript = `https://www.paypal.com/sdk/js?client-id=${paypalConfig.clientid}&currency=${currency}`;
 
     callExternalFunction(paypalScript, () => {
-        modal.setBody('<form></form>'); // This is a hack. Instead of emptying the body, we put an empty form there so the modal
-                                        // is not closed when user clicks outside of modal.
+        modal.setBody(''); // We have to clear the body. The render method in paypal.Buttons will render everything.
+
         paypal.Buttons({ // eslint-disable-line
+            // Set up the transaction.
             createOrder: function(data, actions) {
-                // This function sets up the details of the transaction, including the amount and line item details.
                 return actions.order.create({
                     purchase_units: [{ // eslint-disable-line
                         amount: {
@@ -90,26 +95,22 @@ export const process = async(amount, currency, component, componentid, descripti
                     },
                 });
             },
-            onApprove: function(data, actions) {
-                // Authorize the transaction.
-                actions.order.authorize().then(function(authorization) {
-                    // Get the authorization id.
-                    const authorizationID = authorization.purchase_units[0].payments.authorizations[0].id;
-
-                    // Call your server to validate and capture the transaction.
-                    return Ajax.call([{
-                        methodname: 'pg_paypal_transaction_complete',
-                        args: {
-                            component,
-                            componentid,
-                            orderid: data.orderID,
-                            authorizationid: authorizationID,
-                        },
-                    }])[0]
-                    .then(function(res) {
-                        modal.hide();
-                        return callback(res);
-                    });
+            // Finalise the transaction.
+            onApprove: function(data) {
+                modal.setBody(getString('authorising', 'pg_paypal'));
+
+                // Call server to validate and capture payment for order.
+                return Ajax.call([{
+                    methodname: 'pg_paypal_create_transaction_complete',
+                    args: {
+                        component,
+                        componentid,
+                        orderid: data.orderID,
+                    },
+                }])[0]
+                .then(function(res) {
+                    modal.hide();
+                    return callback(res);
                 });
             }
         }).render(modal.getBody()[0]);
index 46037a9..2b95d2a 100644 (file)
@@ -29,6 +29,8 @@ namespace pg_paypal\external;
 use external_api;
 use external_function_parameters;
 use external_value;
+use core_payment\helper as payment_helper;
+use pg_paypal\paypal_helper;
 
 defined('MOODLE_INTERNAL') || die();
 
@@ -46,7 +48,6 @@ class transaction_complete extends external_api {
             'component' => new external_value(PARAM_COMPONENT, 'The component name'),
             'componentid' => new external_value(PARAM_INT, 'The item id in the context of the component'),
             'orderid' => new external_value(PARAM_TEXT, 'The order id coming back from PayPal'),
-            'authorizationid' => new external_value(PARAM_TEXT, 'The authorization id coming back from PayPal'),
         ]);
     }
 
@@ -57,18 +58,15 @@ class transaction_complete extends external_api {
      * @param string $component Name of the component that the componentid belongs to
      * @param int $componentid An internal identifier that is used by the component
      * @param string $orderid PayPal order ID
-     * @param string $authorizationid The PayPal-generated ID for the authorized payment
      * @return array
      */
-    public static function execute(string $component, int $componentid, string $orderid,
-            string $authorizationid): array {
+    public static function execute(string $component, int $componentid, string $orderid): array {
         global $USER, $DB;
 
         self::validate_parameters(self::execute_parameters(), [
             'component' => $component,
             'componentid' => $componentid,
             'orderid' => $orderid,
-            'authorizationid' => $authorizationid,
         ]);
 
         $config = get_config('pg_paypal');
@@ -77,52 +75,56 @@ class transaction_complete extends external_api {
         [
             'amount' => $amount,
             'currency' => $currency
-        ] = \core_payment\helper::get_cost($component, $componentid);
+        ] = payment_helper::get_cost($component, $componentid);
 
-        $paypalhelper = new \pg_paypal\paypal_helper($config->clientid, $config->secret, $sandbox);
-        $authorization = $paypalhelper->capture_authorization($authorizationid, $amount, $currency);
+        $paypalhelper = new paypal_helper($config->clientid, $config->secret, $sandbox);
+        $orderdetails = $paypalhelper->get_order_details($orderid);
 
         $success = false;
         $message = '';
 
-        if ($authorization) {
-            switch ($authorization['status']) {
-                case 'COMPLETED':
-                    $success = true;
-                    // Everything is correct. Let's give them what they paid for.
-                    try {
-                        \core_payment\helper::deliver_order($component, $componentid);
-
-                        $paymentid = \core_payment\helper::save_payment($component, $componentid, (int)$USER->id, $amount, $currency,
-                                'paypal');
-
-                        // Store PayPal extra information.
-                        $record = new \stdClass();
-                        $record->paymentid = $paymentid;
-                        $record->pp_orderid = $orderid;
-                        $record->pp_authorizationid = $authorizationid;
-                        $record->pp_paymentid = $authorization->id; // The PayPal-generated ID for the captured payment.
-                        $record->pp_status = 'COMPLETED';
-
-                        $DB->insert_record('pg_paypal', $record);
-                    } catch (\Exception $e) {
-                        debugging('Exception while trying to process payment: ' . $e->getMessage(), DEBUG_DEVELOPER);
+        if ($orderdetails) {
+            if ($orderdetails['status'] == paypal_helper::ORDER_STATUS_APPROVED &&
+                    $orderdetails['intent'] == paypal_helper::ORDER_INTENT_CAPTURE) {
+                $item = $orderdetails['purchase_units'][0];
+                if ($item['amount']['value'] == $amount && $item['amount']['currency_code'] == $currency) {
+                    $capture = $paypalhelper->capture_order($orderid);
+                    if ($capture && $capture['status'] == paypal_helper::CAPTURE_STATUS_COMPLETED) {
+                        $success = true;
+                        // Everything is correct. Let's give them what they paid for.
+                        try {
+                            payment_helper::deliver_order($component, $componentid);
+
+                            $paymentid = payment_helper::save_payment($component, $componentid, (int) $USER->id, $amount,
+                                    $currency, 'paypal');
+
+                            // Store PayPal extra information.
+                            $record = new \stdClass();
+                            $record->paymentid = $paymentid;
+                            $record->pp_orderid = $orderid;
+
+                            $DB->insert_record('pg_paypal', $record);
+                        } catch (\Exception $e) {
+                            debugging('Exception while trying to process payment: ' . $e->getMessage(), DEBUG_DEVELOPER);
+                            $success = false;
+                            $message = get_string('internalerror', 'pg_paypal');
+                        }
+                    } else {
                         $success = false;
-                        $message = get_string('internalerror', 'pg_paypal');
+                        $message = get_string('paymentnotcleared', 'pg_paypal');
                     }
-                    break;
-                case 'PENDING':
+                } else {
                     $success = false;
-                    $message = get_string('echecknotsupported', 'pg_paypal');
-                    break;
-                default:
-                    $success = false;
-                    $message = get_string('paymentnotcleared', 'pg_paypal');
+                    $message = get_string('amountmismatch', 'pg_paypal');
+                }
+            } else {
+                $success = false;
+                $message = get_string('paymentnotcleared', 'pg_paypal');
             }
         } else {
             // Could not capture authorization!
             $success = false;
-            $message = get_string('captureauthorizationfailed', 'pg_paypal');
+            $message = get_string('cannotfetchorderdatails', 'pg_paypal');
         }
 
         return [
index 62bc637..f74984e 100644 (file)
@@ -32,6 +32,21 @@ require_once($CFG->libdir . '/filelib.php');
 
 class paypal_helper {
 
+    /**
+     * @var string The payment was authorized or the authorized payment was captured for the order.
+     */
+    public const CAPTURE_STATUS_COMPLETED = 'COMPLETED';
+
+    /**
+     * @var string The merchant intends to capture payment immediately after the customer makes a payment.
+     */
+    public const ORDER_INTENT_CAPTURE = 'CAPTURE';
+
+    /**
+     * @var string The customer approved the payment.
+     */
+    public const ORDER_STATUS_APPROVED = 'APPROVED';
+
     /**
      * @var string The base API URL
      */
index e936131..808f99e 100755 (executable)
@@ -9,9 +9,6 @@
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
         <FIELD NAME="paymentid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="pp_orderid" TYPE="char" LENGTH="255" NOTNULL="true" DEFAULT="The ID of the order in PayPal" SEQUENCE="false"/>
-        <FIELD NAME="pp_authorizationid" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
-        <FIELD NAME="pp_paymentid" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="The PayPal-generated ID for the captured payment."/>
-        <FIELD NAME="pp_status" TYPE="char" LENGTH="32" NOTNULL="false" SEQUENCE="false"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
index 81ed8bd..761e9ef 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['amountmismatch'] = 'The amount you attempted to pay does not match the required fee. Your account has not been debited.';
+$string['authorising'] = 'Authorising the payment. Please wait...';
 $string['brandname'] = 'Brand name';
 $string['brandname_desc'] = 'The optional label that overrides the business name in the PayPal account on the PayPal site.';
-$string['captureauthorizationfailed'] = 'Could not capture authorization.';
+$string['cannotfetchorderdatails'] = 'Could not fetch payment details from PayPal. Your account has not been debited.';
 $string['clientid'] = 'Client ID';
 $string['clientid_desc'] = 'The client ID that PayPal generated for your application.';
-$string['echecknotsupported'] = 'E-check is not supported.';
 $string['environment'] = 'Environment';
 $string['environment_desc'] = 'You can set this to Sandbox if you are using sandbox accounts (for testing purpose only).';
 $string['gatewaydescription'] = 'PayPal is an authorised payment gateway provider for processing credit card transactions.';
 $string['gatewayname'] = 'PayPal';
-$string['internalerror'] = 'Internal error.';
+$string['internalerror'] = 'An internal error has occurred. Please contact us.';
 $string['live'] = 'Live';
-$string['paymentauthorizationwarning'] = 'For PayPal payment authorizations, you must enable this feature on your PayPal account.';
 $string['paymentnotcleared'] = 'payment not cleared by PayPal.';
 $string['pluginname'] = 'PayPal';
 $string['pluginname_desc'] = 'The PayPal plugin allows you to receive payments via PayPal.';
index 4e57615..cdd850a 100644 (file)
@@ -28,9 +28,6 @@ if ($ADMIN->fulltree) {
 
     $settings->add(new admin_setting_heading('pg_paypal_settings', '', get_string('pluginname_desc', 'pg_paypal')));
 
-    $warning = $OUTPUT->notification(get_string('paymentauthorizationwarning', 'pg_paypal'), 'warning');
-    $settings->add(new admin_setting_heading('pg_paypal/warning', '', $warning));
-
     $settings->add(new admin_setting_configtext('pg_paypal/brandname', get_string('brandname', 'pg_paypal'),
             get_string('brandname', 'pg_paypal'), '', PARAM_TEXT));
     $settings->add(new admin_setting_configtext('pg_paypal/clientid', get_string('clientid', 'pg_paypal'),