MDL-69166 core_payment: archiving accounts, triggering events, tests
authorMarina Glancy <marina@moodle.com>
Fri, 2 Oct 2020 06:01:56 +0000 (16:01 +1000)
committerShamim Rezaie <shamim@moodle.com>
Tue, 27 Oct 2020 03:40:49 +0000 (14:40 +1100)
18 files changed:
admin/settings/plugins.php
enrol/fee/classes/plugin.php
enrol/fee/tests/behat/fee.feature [new file with mode: 0644]
lang/en/payment.php
lib/db/install.xml
lib/db/upgrade.php
payment/accounts.php
payment/classes/account.php
payment/classes/event/account_created.php [new file with mode: 0644]
payment/classes/event/account_deleted.php [new file with mode: 0644]
payment/classes/event/account_updated.php [new file with mode: 0644]
payment/classes/helper.php
payment/manage_account.php
payment/tests/accounts_test.php [new file with mode: 0644]
payment/tests/behat/accounts.feature [new file with mode: 0644]
payment/tests/behat/behat_payment.php [new file with mode: 0644]
payment/tests/generator/behat_core_payment_generator.php [new file with mode: 0644]
payment/tests/generator/lib.php [new file with mode: 0644]

index 31bbcc7..5bd2e2f 100644 (file)
@@ -284,6 +284,12 @@ if ($hassiteconfig) {
     $ADMIN->add('modules', new admin_category('paymentgateways', new lang_string('type_pg_plural', 'plugin')));
     $temp = new admin_settingpage('managepaymentgateways', new lang_string('type_pgmanage', 'plugin'));
     $temp->add(new \core_admin\local\settings\manage_payment_gateway_plugins());
+    $temp->add(new admin_setting_description(
+        'managepaymentgatewayspostfix',
+        '',
+        new lang_string('gotopaymentaccounts', 'payment',
+            html_writer::link(new moodle_url('/payment/accounts.php'), get_string('paymentaccounts', 'payment')))
+    ));
     $ADMIN->add('paymentgateways', $temp);
 
     $plugins = core_plugin_manager::instance()->get_plugins_of_type('pg');
index 1408139..5dd1c05 100644 (file)
@@ -303,9 +303,16 @@ class enrol_fee_plugin extends enrol_plugin {
         $mform->addElement('select', 'status', get_string('status', 'enrol_fee'), $options);
         $mform->setDefault('status', $this->get_config('status'));
 
-        $mform->addElement('select', 'customint1', get_string('paymentaccount', 'payment'),
-            ['' => ''] + \core_payment\helper::get_payment_accounts_menu($context));
-        $mform->addRule('customint1', get_string('required'), 'required', null, 'client');
+        $accounts = \core_payment\helper::get_payment_accounts_menu($context);
+        if ($accounts) {
+            $accounts = ((count($accounts) > 1) ? ['' => ''] : []) + $accounts;
+            $mform->addElement('select', 'customint1', get_string('paymentaccount', 'payment'), $accounts);
+        } else {
+            $mform->addElement('static', 'customint1_text', get_string('paymentaccount', 'payment'),
+                html_writer::span(get_string('noaccountsavilable', 'payment'), 'alert alert-danger'));
+            $mform->addElement('hidden', 'customint1');
+            $mform->setType('customint1', PARAM_INT);
+        }
 
         $mform->addElement('text', 'cost', get_string('cost', 'enrol_fee'), array('size' => 4));
         $mform->setType('cost', PARAM_RAW);
@@ -379,6 +386,12 @@ class enrol_fee_plugin extends enrol_plugin {
         $typeerrors = $this->validate_param_types($data, $tovalidate);
         $errors = array_merge($errors, $typeerrors);
 
+        if ($data['status'] == ENROL_INSTANCE_ENABLED &&
+                (!$data['customint1']
+                    || !array_key_exists($data['customint1'], \core_payment\helper::get_payment_accounts_menu($context)))) {
+            $errors['status'] = 'Enrolments can not be enabled without specifying the payment account';
+        }
+
         return $errors;
     }
 
diff --git a/enrol/fee/tests/behat/fee.feature b/enrol/fee/tests/behat/fee.feature
new file mode 100644 (file)
index 0000000..7d98bff
--- /dev/null
@@ -0,0 +1,46 @@
+@enrol @enrol_fee
+Feature: Signing up for a course with a fee enrolment method
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email                |
+      | teacher1 | Teacher   | 1        | teacher1@example.com |
+      | student1 | Student   | 1        | student1@example.com |
+      | manager1 | Manager   | 1        | manager1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | format | summary |
+      | Course 1 | C1        | topics |         |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+      | manager1 | C1     | manager        |
+    And payment plugin "paypal" is enabled
+    And the following "core_payment > payment accounts" exist:
+      | name           | gateways |
+      | Account1       | paypal   |
+    And I log in as "admin"
+    And I navigate to "Plugins > Enrolments > Manage enrol plugins" in site administration
+    And I click on "Enable" "link" in the "Fee" "table_row"
+    And I log out
+
+  @javascript
+  Scenario: Student can see the payment prompt on the course enrolment page
+    When I log in as "manager1"
+    And I am on "Course 1" course homepage
+    And I navigate to "Users > Enrolment methods" in current page administration
+    And I select "Fee" from the "Add method" singleselect
+    And I set the following fields to these values:
+      | Payment account | Account1 |
+      | Enrol cost      | 10       |
+      | Currency        | Euro     |
+    And I press "Add method"
+    And I log out
+    And I log in as "student1"
+    And I am on course index
+    And I follow "Course 1"
+    And I should see "This course requires a payment for entry."
+    #And I should see "Cost: EUR 10.00" # TODO for some reason behat does not "see" this text.
+    And I press "Pay enrolment fee"
+    And I should see "PayPal" in the "Select Payment Type" "dialogue"
+    And I click on "Cancel" "button" in the "Select Payment Type" "dialogue"
+    And I log out
index 534f722..1d508d3 100644 (file)
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['accountarchived'] = 'Archived';
+$string['accountdeleteconfirm'] = 'If this account has previous payments, they will be archived, otherwise all other data will be permanently deleted. Are you sure you want to continue?';
 $string['accountname'] = 'Account name';
 $string['accountnotavailable'] = 'Not available';
 $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';
+$string['eventaccountdeleted'] = 'Payment account deleted';
+$string['eventaccountupdated'] = 'Payment account updated';
 $string['feeincludesurcharge'] = '{$a->fee} (includes {$a->surcharge}% surcharge for using this payment type)';
 $string['gatewaycannotbeenabled'] = 'The payment gateway cannot be enabled because the configuration is incomplete.';
 $string['gatewaydisabled'] = 'Disabled';
 $string['gatewayenabled'] = 'Enabled';
 $string['gatewaynotfound'] = 'Gateway not found';
+$string['gotomanageplugins'] = 'Enable and disable payment gateways and set surcharges via {$a}.';
+$string['gotopaymentaccounts'] = 'You can create multiple payment accounts using any of these gateways on the {$a} page';
+$string['hidearchived'] = 'Hide archived';
+$string['noaccountsavilable'] = 'No payment accounts are available.';
 $string['nocurrencysupported'] = 'No payment in any currency is supported. Please make sure that at least one payment gateway is enabled.';
 $string['nogateway'] = 'There is no payment gateway that can be used.';
 $string['nogatewayselected'] = 'You first need to select a payment gateway.';
 $string['payments'] = 'Payments';
 $string['paymentaccount'] = 'Payment account';
 $string['paymentaccounts'] = 'Payment accounts';
+$string['restoreaccount'] = 'Restore';
 $string['selectpaymenttype'] = 'Select payment type';
+$string['showarchived'] = 'Show archived';
 $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.';
index 2583608..187e8c7 100755 (executable)
         <FIELD NAME="idnumber" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
         <FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="enabled" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="archived" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
         <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
       </FIELDS>
index df3fef8..20dc6ae 100644 (file)
@@ -2959,5 +2959,20 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2021052500.33);
     }
 
+    if ($oldversion < 2021052500.26) {
+
+        // Define field archived to be added to payment_accounts.
+        $table = new xmldb_table('payment_accounts');
+        $field = new xmldb_field('archived', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'enabled');
+
+        // Conditionally launch add field archived.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2021052500.26);
+    }
+
     return true;
 }
index c6d6879..92f1a61 100644 (file)
@@ -25,6 +25,8 @@
 require_once(__DIR__ . '/../config.php');
 require_once($CFG->libdir . '/adminlib.php');
 
+$showarchived = optional_param('showarchived', false, PARAM_BOOL);
+
 admin_externalpage_setup('paymentaccounts');
 $PAGE->set_heading(get_string('paymentaccounts', 'payment'));
 
@@ -32,7 +34,7 @@ $enabledplugins = \core\plugininfo\pg::get_enabled_plugins();
 
 echo $OUTPUT->header();
 
-$accounts = \core_payment\helper::get_payment_accounts_to_manage(context_system::instance());
+$accounts = \core_payment\helper::get_payment_accounts_to_manage(context_system::instance(), $showarchived);
 $table = new html_table();
 $table->head = [get_string('accountname', 'payment'), get_string('type_pg', 'plugin'), ''];
 $table->colclasses = ['', '', 'mdl-right'];
@@ -50,15 +52,23 @@ foreach ($accounts as $account) {
     if (!$account->is_available()) {
         $name .= ' ' . html_writer::span(get_string('accountnotavailable', 'payment'), 'badge badge-warning');
     }
+    if ($account->get('archived')) {
+        $name .= ' ' . html_writer::span(get_string('accountarchived', 'payment'), 'badge badge-secondary');
+    }
 
     $menu = new action_menu();
     $menu->set_alignment(action_menu::TL, action_menu::BL);
     $menu->set_menu_trigger(get_string('edit'));
     if ($canmanage) {
         $menu->add(new action_menu_link_secondary($account->get_edit_url(), null, get_string('edit')));
-        $deleteurl = $account->get_edit_url(['delete' => 1, 'sesskey' => sesskey()]);
-        $deleteaction = new confirm_action(get_string('deleteconfirm', 'tool_recyclebin'));
-        $menu->add(new action_menu_link_secondary($deleteurl, null, get_string('delete')));
+        if (!$account->get('archived')) {
+            $deleteurl = $account->get_edit_url(['delete' => 1, 'sesskey' => sesskey()]);
+            $menu->add(new action_menu_link_secondary($deleteurl, null, get_string('deleteorarchive', 'payment'),
+                ['data-action' => 'delete']));
+        } else {
+            $restoreurl = $account->get_edit_url(['restore' => 1, 'sesskey' => sesskey()]);
+            $menu->add(new action_menu_link_secondary($restoreurl, null, get_string('restoreaccount', 'payment')));
+        }
     }
 
     $table->data[] = [$name, join(', ', $gateways), $OUTPUT->render($menu)];
@@ -66,6 +76,20 @@ foreach ($accounts as $account) {
 
 echo html_writer::table($table);
 
+$PAGE->requires->event_handler('[data-action=delete]', 'click', 'M.util.show_confirm_dialog',
+    array('message' => get_string('accountdeleteconfirm', 'payment')));
+
+echo html_writer::div(html_writer::link(new moodle_url($PAGE->url, ['showarchived' => !$showarchived]),
+    $showarchived ? get_string('hidearchived', 'payment') : get_string('showarchived', 'payment')), 'mdl-right');
+
 echo $OUTPUT->single_button(new moodle_url('/payment/manage_account.php'), get_string('createaccount', 'payment'), 'get');
 
+if (has_capability('moodle/site:config', context_system::instance())) {
+    // For administrators add a link to "Manage payment gateways" page.
+    $link = html_writer::link(new moodle_url('/admin/settings.php', ['section' => 'managepaymentgateways']),
+        get_string('type_pgmanage', 'plugin'));
+    $text = get_string('gotomanageplugins', 'payment', $link);
+    echo html_writer::div($text, 'pt-3');
+}
+
 echo $OUTPUT->footer();
index c938829..7a03b4b 100644 (file)
@@ -65,6 +65,10 @@ class account extends persistent {
                 'type' => PARAM_BOOL,
                 'default' => true
             ],
+            'archived' => [
+                'type' => PARAM_BOOL,
+                'default' => false
+            ],
         );
     }
 
diff --git a/payment/classes/event/account_created.php b/payment/classes/event/account_created.php
new file mode 100644 (file)
index 0000000..b5a8104
--- /dev/null
@@ -0,0 +1,94 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace core_payment\event;
+
+use core\event\base;
+use core_payment\account;
+
+/**
+ * Class account_created
+ *
+ * @package     core_payment
+ * @copyright   2020 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+/**
+ * Class account_created
+ *
+ * @package     core_payment
+ * @copyright   2020 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class account_created extends base {
+
+    /**
+     * Initialise event parameters.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'payment_accounts';
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Create an instance of the event and add a record snapshot
+     *
+     * @param account $account
+     * @return base
+     * @throws \coding_exception
+     */
+    public static function create_from_account(account $account) {
+        $eventparams = [
+            'objectid' => $account->get('id'),
+            'context'  => $account->get_context(),
+            'other'    => ['name' => $account->get('name')]
+        ];
+        $event = self::create($eventparams);
+        $event->add_record_snapshot($event->objecttable, $account->to_record());
+        return $event;
+    }
+
+    /**
+     * Returns localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventaccountcreated', 'payment');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        $name = s($this->other['name']);
+        return "The user with id '$this->userid' created payment account with id '$this->objectid' and the name '{$name}'.";
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/payment/accounts.php');
+    }
+}
diff --git a/payment/classes/event/account_deleted.php b/payment/classes/event/account_deleted.php
new file mode 100644 (file)
index 0000000..d913214
--- /dev/null
@@ -0,0 +1,94 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace core_payment\event;
+
+use core\event\base;
+use core_payment\account;
+
+/**
+ * Class account_deleted
+ *
+ * @package     core_payment
+ * @copyright   2020 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+/**
+ * Class account_deleted
+ *
+ * @package     core_payment
+ * @copyright   2020 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class account_deleted extends base {
+
+    /**
+     * Initialise event parameters.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'payment_accounts';
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Create an instance of the event and add a record snapshot
+     *
+     * @param account $account
+     * @return base
+     * @throws \coding_exception
+     */
+    public static function create_from_account(account $account) {
+        $eventparams = [
+            'objectid' => $account->get('id'),
+            'context'  => $account->get_context(),
+            'other'    => ['name' => $account->get('name')]
+        ];
+        $event = self::create($eventparams);
+        $event->add_record_snapshot($event->objecttable, $account->to_record());
+        return $event;
+    }
+
+    /**
+     * Returns localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventaccountdeleted', 'payment');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        $name = s($this->other['name']);
+        return "The user with id '$this->userid' deleted payment account with id '$this->objectid' and the name '{$name}'.";
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/payment/accounts.php');
+    }
+}
diff --git a/payment/classes/event/account_updated.php b/payment/classes/event/account_updated.php
new file mode 100644 (file)
index 0000000..f0d6f0c
--- /dev/null
@@ -0,0 +1,101 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace core_payment\event;
+
+use core\event\base;
+use core_payment\account;
+
+/**
+ * Class account_updated
+ *
+ * @package     core_payment
+ * @copyright   2020 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+/**
+ * Class account_updated
+ *
+ * @package     core_payment
+ * @copyright   2020 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class account_updated extends base {
+
+    /**
+     * Initialise event parameters.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'payment_accounts';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Create an instance of the event and add a record snapshot
+     *
+     * @param account $account
+     * @param array $other
+     * @return base
+     */
+    public static function create_from_account(account $account, array $other = []) {
+        $eventparams = [
+            'objectid' => $account->get('id'),
+            'context'  => $account->get_context(),
+            'other'    => ['name' => $account->get('name')] + $other
+        ];
+        $event = self::create($eventparams);
+        $event->add_record_snapshot($event->objecttable, $account->to_record());
+        return $event;
+    }
+
+    /**
+     * Returns localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventaccountupdated', 'payment');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        $name = s($this->other['name']);
+        if (!empty($this->other['archived'])) {
+            $verb = 'archived';
+        } else if (!empty($this->other['restored'])) {
+            $verb = 'restored';
+        } else {
+            $verb = 'updated';
+        }
+        return "The user with id '$this->userid' $verb payment account with id '$this->objectid' and the name '{$name}'.";
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/payment/accounts.php');
+    }
+}
index 9c4b0cb..13fbe46 100644 (file)
 
 namespace core_payment;
 
+use core_payment\event\account_created;
+use core_payment\event\account_deleted;
+use core_payment\event\account_updated;
+
 defined('MOODLE_INTERNAL') || die();
 
 /**
@@ -266,18 +270,22 @@ class helper {
      * Save a new or edited payment account (used in management interface)
      *
      * @param \stdClass $data
+     * @return account
      */
-    public static function save_payment_account(\stdClass $data) {
+    public static function save_payment_account(\stdClass $data): account {
 
         if (empty($data->id)) {
             $account = new account(0, $data);
+            $account->save();
+            account_created::create_from_account($account)->trigger();
         } else {
             $account = new account($data->id);
             $account->from_record($data);
+            $account->save();
+            account_updated::create_from_account($account)->trigger();
         }
 
-        $account->save();
-        // TODO trigger event.
+        return $account;
     }
 
     /**
@@ -285,32 +293,60 @@ class helper {
      *
      * @param account $account
      */
-    public static function delete_payment_account(account $account) {
+    public static function delete_payment_account(account $account): void {
+        global $DB;
+        if ($DB->record_exists('payments', ['accountid' => $account->get('id')])) {
+            $account->set('archived', 1);
+            $account->save();
+            account_updated::create_from_account($account, ['archived' => 1])->trigger();
+            return;
+        }
+
         foreach ($account->get_gateways(false) as $gateway) {
             if ($gateway->get('id')) {
                 $gateway->delete();
             }
         }
+        $event = account_deleted::create_from_account($account);
         $account->delete();
-        // TODO trigger event.
+        $event->trigger();
+    }
+
+    /**
+     * Restore archived payment account (used in management interface)
+     *
+     * @param account $account
+     */
+    public static function restore_payment_account(account $account): void {
+        $account->set('archived', 0);
+        $account->save();
+        account_updated::create_from_account($account, ['restored' => 1])->trigger();
     }
 
     /**
      * Save a payment gateway linked to an existing account (used in management interface)
      *
      * @param \stdClass $data
+     * @return account_gateway
      */
-    public static function save_payment_gateway(\stdClass $data) {
+    public static function save_payment_gateway(\stdClass $data): account_gateway {
         if (empty($data->id)) {
-            $gateway = new account_gateway(0, $data);
+            $records = account_gateway::get_records(['accountid' => $data->accountid, 'gateway' => $data->gateway]);
+            if ($records) {
+                $gateway = reset($records);
+            } else {
+                $gateway = new account_gateway(0, $data);
+            }
         } else {
             $gateway = new account_gateway($data->id);
-            unset($data->accountid, $data->gateway, $data->id);
-            $gateway->from_record($data);
         }
+        unset($data->accountid, $data->gateway, $data->id);
+        $gateway->from_record($data);
 
+        $account = $gateway->get_account();
         $gateway->save();
-        // TODO trigger event.
+        account_updated::create_from_account($account)->trigger();
+        return $gateway;
     }
 
     /**
@@ -319,8 +355,10 @@ class helper {
      * @param \context $context
      * @return account[]
      */
-    public static function get_payment_accounts_to_manage(\context $context): array {
-        return account::get_records(['contextid' => $context->id]);
+    public static function get_payment_accounts_to_manage(\context $context, bool $showarchived = false): array {
+        $records = account::get_records(['contextid' => $context->id] + ($showarchived ? [] : ['archived' => 0]));
+        \core_collator::asort_objects_by_method($records, 'get_formatted_name');
+        return $records;
     }
 
     /**
@@ -333,7 +371,7 @@ class helper {
         global $DB;
         [$sql, $params] = $DB->get_in_or_equal($context->get_parent_context_ids(true));
         $accounts = array_filter(account::get_records_select('contextid '.$sql, $params), function($account) {
-            return $account->is_available();
+            return $account->is_available() && !$account->get('archived');
         });
         return array_map(function($account) {
             return $account->get_formatted_name();
index 1a5a0b1..2d92e6c 100644 (file)
@@ -27,6 +27,7 @@ require_once($CFG->libdir . '/adminlib.php');
 
 $id = optional_param('id', 0, PARAM_INT);
 $delete = optional_param('delete', false, PARAM_BOOL);
+$restore = optional_param('restore', false, PARAM_BOOL);
 
 $pageurl = new moodle_url('/payment/manage_account.php');
 admin_externalpage_setup('paymentaccounts', '', [], $pageurl);
@@ -36,10 +37,14 @@ $enabledplugins = \core\plugininfo\pg::get_enabled_plugins();
 $account = new \core_payment\account($id);
 require_capability('moodle/payment:manageaccounts', $account->get_context());
 
-if ($delete && confirm_sesskey()) {
+if ($delete && !$account->get('archived') && confirm_sesskey()) {
     \core_payment\helper::delete_payment_account($account);
     redirect(new moodle_url('/payment/accounts.php'));
 }
+if ($restore && $account->get('archived') && confirm_sesskey()) {
+    \core_payment\helper::restore_payment_account($account);
+    redirect(new moodle_url('/payment/accounts.php'));
+}
 
 $PAGE->set_heading($id ? format_string($account->get('name')) : get_string('createaccount', 'payment'));
 
diff --git a/payment/tests/accounts_test.php b/payment/tests/accounts_test.php
new file mode 100644 (file)
index 0000000..318850c
--- /dev/null
@@ -0,0 +1,130 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Testing accounts management in payments API
+ *
+ * @package    core_payment
+ * @category   test
+ * @copyright  2020 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_payment;
+
+use advanced_testcase;
+use core\plugininfo\pg;
+
+/**
+ * Testing accounts management in payments API
+ *
+ * @package    core_payment
+ * @category   test
+ * @copyright  2020 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class accounts_testcase extends advanced_testcase {
+
+    protected function enable_paypal_gateway(): bool {
+        if (!array_key_exists('paypal', \core_component::get_plugin_list('pg'))) {
+            return false;
+        }
+        pg::set_enabled_plugins('paypal');
+        return true;
+    }
+
+    public function test_create_account() {
+        global $DB;
+        $this->resetAfterTest();
+
+        $account = helper::save_payment_account((object)['name' => 'Test 1', 'idnumber' => '']);
+        $this->assertNotEmpty($account->get('id'));
+        $this->assertEquals('Test 1', $DB->get_field('payment_accounts', 'name', ['id' => $account->get('id')]));
+    }
+
+    public function test_update_account_details() {
+        global $DB;
+        $this->resetAfterTest();
+
+        $account = helper::save_payment_account((object)['name' => 'Test 1', 'idnumber' => '']);
+        $record = $account->to_record();
+        $record->name = 'Edited name';
+        $editedaccount = helper::save_payment_account($record);
+        $this->assertEquals($account->get('id'), $editedaccount->get('id'));
+        $this->assertEquals('Edited name', $DB->get_field('payment_accounts', 'name', ['id' => $account->get('id')]));
+    }
+
+    public function test_update_account_gateways() {
+        global $DB;
+        if (!$this->enable_paypal_gateway()) {
+            $this->markTestSkipped('Paypal payment gateway plugin not found');
+        }
+
+        $this->resetAfterTest();
+
+        $account = helper::save_payment_account((object)['name' => 'Test 1', 'idnumber' => '']);
+        $gateway = helper::save_payment_gateway(
+            (object)['accountid' => $account->get('id'), 'gateway' => 'paypal', 'config' => 'T1']);
+        $this->assertNotEmpty($gateway->get('id'));
+        $this->assertEquals('T1', $DB->get_field('payment_gateways', 'config', ['id' => $gateway->get('id')]));
+
+        // Update by id.
+        $editedgateway = helper::save_payment_gateway(
+            (object)['id' => $gateway->get('id'), 'accountid' => $account->get('id'), 'gateway' => 'paypal', 'config' => 'T2']);
+        $this->assertEquals($gateway->get('id'), $editedgateway->get('id'));
+        $this->assertEquals('T2', $DB->get_field('payment_gateways', 'config', ['id' => $gateway->get('id')]));
+
+        // Update by account/gateway.
+        $editedgateway = helper::save_payment_gateway(
+            (object)['accountid' => $account->get('id'), 'gateway' => 'paypal', 'config' => 'T3']);
+        $this->assertEquals($gateway->get('id'), $editedgateway->get('id'));
+        $this->assertEquals('T3', $DB->get_field('payment_gateways', 'config', ['id' => $gateway->get('id')]));
+    }
+
+    public function test_delete_account() {
+        global $DB;
+        if (!$this->enable_paypal_gateway()) {
+            $this->markTestSkipped('Paypal payment gateway plugin not found');
+        }
+        $this->resetAfterTest();
+
+        // Delete account without payments, it will be deleted, gateways will also be deleted.
+        $account = helper::save_payment_account((object)['name' => 'Test 1', 'idnumber' => '']);
+        $gateway = helper::save_payment_gateway(
+            (object)['accountid' => $account->get('id'), 'gateway' => 'paypal', 'config' => 'T1']);
+
+        helper::delete_payment_account(account::get_record(['id' => $account->get('id')]));
+        $this->assertEmpty($DB->get_records('payment_accounts', ['id' => $account->get('id')]));
+        $this->assertEmpty($DB->get_records('payment_gateways', ['id' => $gateway->get('id')]));
+    }
+
+    public function test_archive_restore_account() {
+        global $DB, $USER;
+        $this->resetAfterTest();
+
+        // Delete account with payments - it will be archived.
+        $this->setAdminUser();
+        $account = helper::save_payment_account((object)['name' => 'Test 1', 'idnumber' => '']);
+        $DB->insert_record('payments', ['accountid' => $account->get('id'), 'component' => 'test', 'componentid' => 1,
+            'userid' => $USER->id]);
+        helper::delete_payment_account(account::get_record(['id' => $account->get('id')]));
+        $this->assertEquals(1, $DB->get_field('payment_accounts', 'archived', ['id' => $account->get('id')]));
+
+        // Restore account.
+        helper::restore_payment_account(account::get_record(['id' => $account->get('id')]));
+        $this->assertEquals(0, $DB->get_field('payment_accounts', 'archived', ['id' => $account->get('id')]));
+    }
+}
diff --git a/payment/tests/behat/accounts.feature b/payment/tests/behat/accounts.feature
new file mode 100644 (file)
index 0000000..9a621d9
--- /dev/null
@@ -0,0 +1,87 @@
+@core @core_payment
+Feature: Manage payment accounts
+
+  @javascript
+  Scenario: Creating and editing payment account
+    When I log in as "admin"
+    And I navigate to "Payments > Payment accounts" in site administration
+    And I follow "Manage payment gateways"
+    And I click on "Enable" "link" in the "PayPal" "table_row"
+    And I follow "Payment accounts"
+    And I press "Create payment account"
+    And I set the field "Account name" to "TestAccount"
+    And I press "Save changes"
+    And I should see "PayPal" in the "TestAccount" "table_row"
+    And I open the action menu in "TestAccount" "table_row"
+    And I choose "Edit" in the open action menu
+    And I set the field "Account name" to "NewName"
+    And I press "Save changes"
+    And I should see "PayPal" in the "NewName" "table_row"
+    And I should not see "TestAccount"
+    And I log out
+
+  @javascript
+  Scenario: Configuring gateways on payment accounts
+    Given payment plugin "paypal" is enabled
+    And the following "core_payment > payment accounts" exist:
+      | name           |
+      | Account1       |
+      | Account2       |
+    When I log in as "admin"
+    And I navigate to "Payments > Payment accounts" in site administration
+    Then I should see "Not available" in the "Account1" "table_row"
+    And I click on "PayPal" "link" in the "Account1" "table_row"
+    And I set the field "Brand name" to "Test paypal"
+    And I set the following fields to these values:
+      | Brand name | Test paypal |
+      | Client ID  | Test        |
+      | Secret     | Test        |
+      | Enable     | 1           |
+    And I press "Save changes"
+    And I should see "PayPal" in the "Account1" "table_row"
+    And I should not see "Not available" in the "Account1" "table_row"
+    And I log out
+
+  @javascript
+  Scenario: Deleting payment accounts
+    Given payment plugin "paypal" is enabled
+    And the following "core_payment > payment accounts" exist:
+      | name           |
+      | Account1       |
+      | Account2       |
+    When I log in as "admin"
+    And I navigate to "Payments > Payment accounts" in site administration
+    And I open the action menu in "Account1" "table_row"
+    And I choose "Delete or archive" in the open action menu
+    And I click on "Yes" "button" in the "Confirmation" "dialogue"
+    Then I should not see "Account1"
+    And I should see "Account2"
+    And I log out
+
+  @javascript
+  Scenario: Archiving and restoring accounts
+    Given payment plugin "paypal" is enabled
+    And the following "users" exist:
+      | username |
+      | user1    |
+    And the following "core_payment > payment accounts" exist:
+      | name           |
+      | Account1       |
+      | Account2       |
+    And the following "core_payment > payments" exist:
+      | account  | component | amount | user  |
+      | Account1 | test      | 10     | user1 |
+      | Account1 | test      | 15     | user1 |
+    When I log in as "admin"
+    And I navigate to "Payments > Payment accounts" in site administration
+    And I open the action menu in "Account1" "table_row"
+    And I choose "Delete or archive" in the open action menu
+    And I click on "Yes" "button" in the "Confirmation" "dialogue"
+    Then I should not see "Account1"
+    And I should see "Account2"
+    And I follow "Show archived"
+    And I should see "Archived" in the "Account1" "table_row"
+    And I open the action menu in "Account1" "table_row"
+    And I choose "Restore" in the open action menu
+    And I should not see "Archived" in the "Account1" "table_row"
+    And I log out
diff --git a/payment/tests/behat/behat_payment.php b/payment/tests/behat/behat_payment.php
new file mode 100644 (file)
index 0000000..69454cc
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Steps definitions related with the payment API
+ *
+ * @package    core_payment
+ * @category   test
+ * @copyright  2020 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
+
+use Behat\Gherkin\Node\TableNode as TableNode,
+    Behat\Mink\Exception\ExpectationException as ExpectationException,
+    Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
+
+/**
+ * Steps definitions related with the payment API
+ *
+ * @package    core_payment
+ * @category   test
+ * @copyright  2020 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_payment extends behat_question_base {
+
+    /**
+     * Enable payment plugin
+     *
+     * @Given /^payment plugin "(?P<plugin_name_string>(?:[^"]|\\")*)" is enabled$/
+     * @param string $pluginname
+     */
+    public function payment_plugin_is_enabled($pluginname) {
+        $plugins = \core\plugininfo\pg::get_enabled_plugins();
+        \core\plugininfo\pg::set_enabled_plugins(array_merge($plugins, [$pluginname]));
+    }
+}
diff --git a/payment/tests/generator/behat_core_payment_generator.php b/payment/tests/generator/behat_core_payment_generator.php
new file mode 100644 (file)
index 0000000..7e83835
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Behat data generator for core_payment.
+ *
+ * @package    core_payment
+ * @category   test
+ * @copyright  2020 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Behat data generator for core_payment.
+ *
+ * @package    core_payment
+ * @category   test
+ * @copyright  2020 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_core_payment_generator extends behat_generator_base {
+
+    protected function get_creatable_entities(): array {
+        return [
+            'payment accounts' => [
+                'singular' => 'payment account',
+                'datagenerator' => 'payment_account',
+                'required' => ['name'],
+            ],
+            'payments' => [
+                'singular' => 'payment',
+                'datagenerator' => 'payment',
+                'required' => ['account', 'amount', 'user'],
+                'switchids' => ['account' => 'accountid', 'user' => 'userid'],
+            ],
+        ];
+    }
+
+    /**
+     * Look up the id of a account from its name.
+     *
+     * @param string $accountname
+     * @return int corresponding id.
+     */
+    protected function get_account_id(string $accountname): int {
+        global $DB;
+
+        if (!$id = $DB->get_field('payment_accounts', 'id', ['name' => $accountname])) {
+            throw new Exception('There is no account with name "' . $accountname . '".');
+        }
+        return $id;
+    }
+}
diff --git a/payment/tests/generator/lib.php b/payment/tests/generator/lib.php
new file mode 100644 (file)
index 0000000..8048827
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Quiz module test data generator class
+ *
+ * @package    core_payment
+ * @category   test
+ * @copyright  2020 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_payment_generator extends component_generator_base {
+
+    protected $accountcounter = 0;
+
+    /**
+     * Create a payment account
+     *
+     * @param array $data account data (name, idnumber, enabled) and additionally field 'gateways' that can include
+     *    a list of gateways that should be mock-enabled for this account.
+     */
+    public function create_payment_account(array $data): \core_payment\account {
+        $this->accountcounter++;
+        $gateways = [];
+        if (!empty($data['gateways'])) {
+            $gateways = preg_split('/,/', $data['gateways']);
+        }
+        unset($data['gateways']);
+        $account = \core_payment\helper::save_payment_account(
+            (object)($data + ['name' => 'Test '.$this->accountcounter, 'idnumber' => '', 'enabled' => 1]));
+        foreach ($gateways as $gateway) {
+            \core_payment\helper::save_payment_gateway(
+                (object)['accountid' => $account->get('id'), 'gateway' => $gateway, 'enabled' => 1]);
+        }
+        return $account;
+    }
+
+    /**
+     * Create a payment account
+     *
+     * @param array $data
+     */
+    public function create_payment(array $data): int {
+        global $DB;
+        if (empty($data['accountid']) || !\core_payment\account::get_record(['id' => $data['accountid']])) {
+            throw new coding_exception('Account id is not specified or does not exist');
+        }
+
+        if (empty($data['amount'])) {
+            throw new coding_exception('Amount must be specified');
+        }
+
+        $gateways = \core\plugininfo\pg::get_enabled_plugins();
+        if (empty($data['gateway'])) {
+            $data['gateway'] = reset($gateways);
+        }
+
+        $id = $DB->insert_record('payments', $data +
+            ['component' => 'testcomponent',
+                'componentarea' => 'teatarea',
+                'componentid' => 0,
+                'currency' => 'AUD']);
+        return $id;
+    }
+
+}