From 3463da4514b07148c2d990069a5ba50769dd9f1c Mon Sep 17 00:00:00 2001 From: "B. Endres" Date: Thu, 13 Feb 2020 09:23:13 +0100 Subject: [PATCH] [#20] refactored settings and implemented activity creation --- CRM/Twingle/Config.php | 37 ++++ CRM/Twingle/Form/Settings.php | 261 +++++++++++------------- CRM/Twingle/Tools.php | 100 ++++++++- settings/twingle.setting.php | 62 ------ templates/CRM/Twingle/Form/Settings.tpl | 105 +++++++++- 5 files changed, 342 insertions(+), 223 deletions(-) create mode 100644 CRM/Twingle/Config.php delete mode 100644 settings/twingle.setting.php diff --git a/CRM/Twingle/Config.php b/CRM/Twingle/Config.php new file mode 100644 index 0000000..81f4647 --- /dev/null +++ b/CRM/Twingle/Config.php @@ -0,0 +1,37 @@ + E::ts("No"), + self::RCUR_PROTECTION_EXCEPTION => E::ts("Raise Exception"), + self::RCUR_PROTECTION_ACTIVITY => E::ts("Create Activity"), + ]; + } +} diff --git a/CRM/Twingle/Form/Settings.php b/CRM/Twingle/Form/Settings.php index a8460a2..b85fe5e 100644 --- a/CRM/Twingle/Form/Settings.php +++ b/CRM/Twingle/Form/Settings.php @@ -22,12 +22,18 @@ use CRM_Twingle_ExtensionUtil as E; */ class CRM_Twingle_Form_Settings extends CRM_Core_Form { - private $_settingFilter = array('group' => 'de.systopia.twingle'); - - //everything from this line down is generic & can be re-used for a setting form in another extension - //actually - I lied - I added a specific call in getFormSettings - private $_submittedValues = array(); - private $_settings = array(); + /** + * @var array list of all settings options + */ + public static $SETTINGS_LIST = [ + 'twingle_prefix', + 'twingle_use_sepa', + 'twingle_protect_recurring', + 'twingle_protect_recurring_activity_type', + 'twingle_protect_recurring_activity_subject', + 'twingle_protect_recurring_activity_status', + 'twingle_protect_recurring_activity_assignee', + ]; /** * @inheritdoc @@ -36,157 +42,132 @@ class CRM_Twingle_Form_Settings extends CRM_Core_Form { // Set redirect destination. $this->controller->_destination = CRM_Utils_System::url('civicrm/admin/settings/twingle', 'reset=1'); - $settings = $this->getFormSettings(); - $form_elements = array(); + $this->add( + 'text', + 'twingle_prefix', + E::ts("Twingle ID Prefix") + ); - foreach ($settings as $name => $setting) { - if (isset($setting['quick_form_type'])) { - $add = 'add' . $setting['quick_form_type']; - if ($add == 'addElement') { - $this->$add( - $setting['html_type'], - $name, - ts($setting['title']), - CRM_Utils_Array::value('html_attributes', $setting, array()) - ); - } - elseif ($setting['html_type'] == 'Select') { - $optionValues = array(); - if (!empty($setting['pseudoconstant']) && !empty($setting['pseudoconstant']['optionGroupName'])) { - $optionValues = CRM_Core_OptionGroup::values($setting['pseudoconstant']['optionGroupName'], FALSE, FALSE, FALSE, NULL, 'name'); - } - $this->add( - 'select', - $setting['name'], - $setting['title'], - $optionValues, - FALSE, - CRM_Utils_Array::value('html_attributes', $setting, array()) - ); - } - else { - $this->$add($name, ts($setting['title'])); - } - $form_elements[$setting['name']] = array('description' => ts($setting['description'])); + $this->add( + 'checkbox', + 'twingle_use_sepa', + E::ts("Use CiviSEPA") + ); - // Disable CiviSEPA setting if the extension is not installed. - if ($name == 'twingle_use_sepa') { - $sepa_extension = civicrm_api3('Extension', 'get', array( - 'full_name' => 'org.project60.sepa', - 'is_active' => 1, - )); - if ($sepa_extension['count'] == 0) { - $element = $this->getElement('twingle_use_sepa'); - $element->freeze(); - $form_elements['twingle_use_sepa']['description'] .= ' The CiviSEPA (org.project60.sepa) extension is not installed.'; - } + $this->add( + 'select', + 'twingle_protect_recurring', + E::ts("Protect Recurring Contributions"), + CRM_Twingle_Config::getRecurringProtectionOptions() + ); + + $this->add( + 'select', + 'twingle_protect_recurring_activity_type', + E::ts("Activity Type"), + $this->getOptionValueList('activity_type', [0]) + ); + + $this->add( + 'text', + 'twingle_protect_recurring_activity_subject', + E::ts("Subject"), + ['class' => 'huge'] + ); + + $this->add( + 'select', + 'twingle_protect_recurring_activity_status', + E::ts("Status"), + $this->getOptionValueList('activity_status') + ); + + $this->addEntityRef( + 'twingle_protect_recurring_activity_assignee', + E::ts('Assigned To'), + [ + 'contact_type' => ['IN' => ['Individual', 'Organization']], + 'check_permissions' => 0, + ] + ); + + $this->addButtons(array( + array ( + 'type' => 'submit', + 'name' => E::ts('Save'), + 'isDefault' => TRUE, + ) + )); + + // set defaults + foreach (self::$SETTINGS_LIST as $setting) { + $this->setDefaults([ + $setting => Civi::settings()->get($setting) + ]); + } + + parent::buildQuickForm(); + } + + /** + * Custom form validation, because the activity creation fields + * are only mandatory if activity creation is active + * @return bool + */ + public function validate() { + parent::validate(); + + // if activity creation is active, make sure the fields are set + $protection_mode = CRM_Utils_Array::value('twingle_protect_recurring', $this->_submitValues); + if ($protection_mode == CRM_Twingle_Config::RCUR_PROTECTION_ACTIVITY) { + foreach (['twingle_protect_recurring_activity_type', + 'twingle_protect_recurring_activity_subject', + 'twingle_protect_recurring_activity_status', + 'twingle_protect_recurring_activity_assignee',] as $activity_field) { + $current_value = CRM_Utils_Array::value($activity_field, $this->_submitValues); + if (empty($current_value)) { + $this->_errors[$activity_field] = E::ts("This is required for activity creation"); } } } - $this->assign('formElements', $form_elements); - - $this->addButtons(array( - array ( - 'type' => 'submit', - 'name' => ts('Save'), - 'isDefault' => TRUE, - ) - )); - - // Export form elements. - $this->assign('elementNames', $this->getRenderableElementNames()); - parent::buildQuickForm(); + return (0 == count($this->_errors)); } + /** * @inheritdoc */ function postProcess() { - $this->_submittedValues = $this->exportValues(); - $this->saveSettings(); + $values = $this->exportValues(); + + // store settings + foreach (self::$SETTINGS_LIST as $setting) { + Civi::settings()->set($setting, CRM_Utils_Array::value($setting, $values)); + } + parent::postProcess(); } - /** - * Get the fields/elements defined in this form. - * - * @return array (string) - */ - function getRenderableElementNames() { - // The _elements list includes some items which should not be - // auto-rendered in the loop -- such as "qfKey" and "buttons". These - // items don't have labels. We'll identify renderable by filtering on - // the 'label'. - $elementNames = array(); - foreach ($this->_elements as $element) { - /* @var \HTML_QuickForm_element $element */ - $label = $element->getLabel(); - if (!empty($label)) { - $elementNames[] = $element->getName(); - } - } - return $elementNames; - } - /** - * Get the settings we are going to allow to be set on this form. - * - * @return array - * - * @throws \CiviCRM_API3_Exception - */ - function getFormSettings() { - if (empty($this->_settings)) { - $settings = civicrm_api3('setting', 'getfields', array('filters' => $this->_settingFilter)); - $settings = $settings['values']; - } - else { - $settings = $this->_settings; - } - return $settings; - } - /** - * Save the settings set on this form. - */ - function saveSettings() { - $settings = $this->getFormSettings(); - $values = array_intersect_key($this->_submittedValues, $settings); - civicrm_api3('setting', 'create', $values); - } - /** - * @inheritdoc - */ - function setDefaultValues() { - $existing = civicrm_api3('setting', 'get', array('return' => array_keys($this->getFormSettings()))); - $defaults = array(); - $domainID = CRM_Core_Config::domainID(); - foreach ($existing['values'][$domainID] as $name => $value) { - $defaults[$name] = $value; - } - return $defaults; - } /** - * @inheritdoc + * Get a list of option group items + * @param $group_id string group ID or name + * @return array list of ID(value) => label + * @throws CiviCRM_API3_Exception */ - public function addRules() { - $this->addFormRule(array('CRM_Twingle_Form_Settings', 'validateSettingsForm')); + protected function getOptionValueList($group_id, $reserved = [0,1]) { + $list = ['' => E::ts("-select-")]; + $query = civicrm_api3('OptionValue', 'get', [ + 'option_group_id' => $group_id, + 'option.limit' => 0, + 'is_active' => 1, + 'is_reserved' => ['IN' => $reserved], + 'return' => 'value,label', + ]); + foreach ($query['values'] as $value) { + $list[$value['value']] = $value['label']; + } + return $list; } - - /** - * Validates the profile form. - * - * @param array $values - * The submitted form values, keyed by form element name. - * - * @return bool | array - * TRUE when the form was successfully validated, or an array of error - * messages, keyed by form element name. - */ - public static function validateSettingsForm($values) { - $errors = array(); - - return empty($errors) ? TRUE : $errors; - } - } diff --git a/CRM/Twingle/Tools.php b/CRM/Twingle/Tools.php index b1ff95d..bb3bbb5 100644 --- a/CRM/Twingle/Tools.php +++ b/CRM/Twingle/Tools.php @@ -41,29 +41,109 @@ class CRM_Twingle_Tools { // check if we're suspended if (self::$protection_suspended) return; - // currently only works with prefixes - $prefix = Civi::settings()->get('twingle_prefix'); - if (empty($prefix)) return; - // check if protection is turned on $protection_on = Civi::settings()->get('twingle_protect_recurring'); if (empty($protection_on)) return; // load the recurring contribution $recurring_contribution = civicrm_api3('ContributionRecur', 'getsingle', [ - 'return' => 'trxn_id,contribution_status_id,payment_instrument_id', + 'return' => 'trxn_id,contribution_status_id,payment_instrument_id,contact_id', 'id' => $recurring_contribution_id]); - // check if this is a SEPA transaction + // check if this is a SEPA transaction (doesn't concern us) if (self::isSDD($recurring_contribution['payment_instrument_id'])) return; + // see if this recurring contribution is from Twingle + if (!self::isTwingleRecurringContribution($recurring_contribution_id, $recurring_contribution)) { + return; + } + // check if it's really a termination (i.e. current status is 2 or 5) if (!in_array($recurring_contribution['contribution_status_id'], [2,5])) return; - // check if it's a Twingle contribution - if (substr($recurring_contribution['trxn_id'], 0, strlen($prefix)) == $prefix) { - // this is a Twingle contribution that is about to be terminated - throw new Exception(E::ts("This is a Twingle recurring contribution. It should be terminated through the Twingle interface, otherwise it will still be collected.")); + // this _IS_ on of the cases where we should step in: + CRM_Twingle_Tools::processRecurringContributionTermination($recurring_contribution_id, $recurring_contribution); + } + + /** + * @param $recurring_contribution_id int recurring contribution ID to check + * @param $recurring_contribution array recurring contribution data, optional + * @return bool|null true, false or null if can't be determined + * @throws CiviCRM_API3_Exception + */ + public static function isTwingleRecurringContribution($recurring_contribution_id, $recurring_contribution = NULL) { + // this currently only works with prefixes + $prefix = Civi::settings()->get('twingle_prefix'); + if (empty($prefix)) return null; + + // load recurring contribution if necessary + if (empty($recurring_contribution['trxn_id'])) { + $recurring_contribution = civicrm_api3('ContributionRecur', 'getsingle', ['id' => $recurring_contribution_id]); + } + + // check if it's a Twingle contribution by checking the prefix + // fixme: better ways (e.g. tags) should be used to mark twingle contributions + return (substr($recurring_contribution['trxn_id'], 0, strlen($prefix)) == $prefix); + } + + /** + * Execute the recurring contribution protection + * + * @param $recurring_contribution_id int recurring contribution ID + * @param $recurring_contribution array recurring contribution fields + * @throws Exception could be one of the measures + */ + public static function processRecurringContributionTermination($recurring_contribution_id, $recurring_contribution) { + // check if we're suspended + if (self::$protection_suspended) return; + + $protection_mode = Civi::settings()->get('twingle_protect_recurring'); + switch ($protection_mode) { + case CRM_Twingle_Config::RCUR_PROTECTION_OFF: + // do nothing + break; + + case CRM_Twingle_Config::RCUR_PROTECTION_EXCEPTION: + throw new Exception(E::ts("This is a Twingle recurring contribution. It should be terminated through the Twingle interface, otherwise it will still be collected.")); + + case CRM_Twingle_Config::RCUR_PROTECTION_ACTIVITY: + // create contact source activity + // first: get the contact ID + if (!empty($recurring_contribution['contact_id'])) { + $target_id = (int) $recurring_contribution['contact_id']; + } else { + $target_id = (int) civicrm_api3('ContributionRecur', 'getvalue', [ + 'id' => $recurring_contribution_id, + 'return' => 'contact_id']); + } + if (!empty($recurring_contribution['trxn_id'])) { + $trxn_id = $recurring_contribution['trxn_id']; + } else { + $trxn_id = civicrm_api3('ContributionRecur', 'getvalue', [ + 'id' => $recurring_contribution_id, + 'return' => 'trxn_id']); + } + + try { + civicrm_api3('Activity', 'create', [ + 'activity_type_id' => Civi::settings()->get('twingle_protect_recurring_activity_type'), + 'subject' => Civi::settings()->get('twingle_protect_recurring_activity_subject'), + 'activity_date_time' => date('YmdHis'), + 'target_id' => $target_id, + 'assignee_id' => Civi::settings()->get('twingle_protect_recurring_activity_assignee'), + 'status_id' => Civi::settings()->get('twingle_protect_recurring_activity_status'), + 'details' => E::ts("Recurring contribution [%1] (Transaction ID '%2') was terminated by a user. You need to end the corresponding record in Twingle as well, or it will still be collected.", + [1 => $recurring_contribution_id, 2 => $trxn_id]), + 'source_contact_id' => CRM_Core_Session::getLoggedInContactID(), + ]); + } catch (Exception $ex) { + Civi::log()->debug("TwingleAPI: Couldn't create recurring protection activity: " . $ex->getMessage()); + } + break; + + default: + Civi::log()->debug("TwingleAPI: Unknown recurring contribution protection mode: '{$protection_mode}'"); + break; } } diff --git a/settings/twingle.setting.php b/settings/twingle.setting.php deleted file mode 100644 index d703845..0000000 --- a/settings/twingle.setting.php +++ /dev/null @@ -1,62 +0,0 @@ - array( - 'group_name' => 'de.systopia.twingle', - 'group' => 'de.systopia.twingle', - 'name' => 'twingle_use_sepa', - 'type' => 'Boolean', - 'quick_form_type' => 'YesNo', - 'html_type' => 'radio', - 'title' => 'Use CiviSEPA', - 'default' => 0, - 'add' => '4.6', - 'is_domain' => 1, - 'is_contact' => 0, - 'description' => 'Whether to provide CiviSEPA functionality for manual debit payment method. This requires the CiviSEPA (org.project60.sepa) extension be installed.', - ), - 'twingle_protect_recurring' => array( - 'group_name' => 'de.systopia.twingle', - 'group' => 'de.systopia.twingle', - 'name' => 'twingle_protect_recurring', - 'type' => 'Boolean', - 'quick_form_type' => 'YesNo', - 'html_type' => 'radio', - 'title' => 'Protect Recurring Contributions', - 'default' => 0, - 'add' => '4.6', - 'is_domain' => 1, - 'is_contact' => 0, - 'description' => 'Will protect all recurring contributions created by Twingle from termination, since this does NOT terminate the Twingle collection process. Currently only works with a prefix', - ), - 'twingle_prefix' => array( - 'group_name' => 'de.systopia.twingle', - 'group' => 'de.systopia.twingle', - 'name' => 'twingle_prefix', - 'type' => CRM_Utils_Type::T_STRING, - 'quick_form_type' => 'Element', - 'html_type' => 'text', - 'title' => 'Twingle ID Prefix', - 'default' => '', - 'add' => '4.6', - 'is_domain' => 1, - 'is_contact' => 0, - 'description' => 'You can use this setting to add a prefix to the Twingle transaction ID, in order to avoid collisions with other transaction ids.', - ), -); diff --git a/templates/CRM/Twingle/Form/Settings.tpl b/templates/CRM/Twingle/Form/Settings.tpl index 6f57d37..49d068e 100644 --- a/templates/CRM/Twingle/Form/Settings.tpl +++ b/templates/CRM/Twingle/Form/Settings.tpl @@ -14,29 +14,112 @@
- {* HEADER *} -
- {include file="CRM/common/formButtons.tpl" location="top"} -
+

Twingle API - Generic Settings

- {foreach from=$elementNames item=elementName} - - + + - {/foreach} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{$form.$elementName.label}  
{$form.twingle_use_sepa.label}   - {$form.$elementName.html} + {$form.twingle_use_sepa.html}
- {$formElements.$elementName.description} + {$formElements.twingle_use_sepa.description}
{$form.twingle_prefix.label}   + {$form.twingle_prefix.html} +
+ + {$formElements.twingle_prefix.description} + +
{$form.twingle_protect_recurring.label}   + {$form.twingle_protect_recurring.html} +
+ + {$formElements.protect_recurring.description} + +
{$form.twingle_protect_recurring_activity_type.label} + {$form.twingle_protect_recurring_activity_type.html} +
+ + {$formElements.twingle_protect_recurring_activity_type.description} + +
{$form.twingle_protect_recurring_activity_subject.label} + {$form.twingle_protect_recurring_activity_subject.html} +
+ + {$formElements.twingle_protect_recurring_activity_subject.description} + +
{$form.twingle_protect_recurring_activity_status.label} + {$form.twingle_protect_recurring_activity_status.html} +
+ + {$formElements.twingle_protect_recurring_activity_status.description} + +
{$form.twingle_protect_recurring_activity_assignee.label} + {$form.twingle_protect_recurring_activity_assignee.html} +
+ + {$formElements.twingle_protect_recurring_activity_assignee.description} + +
- {* FOOTER *} +
{include file="CRM/common/formButtons.tpl" location="bottom"}
+ +{literal} + +{/literal} \ No newline at end of file