Merge branch 'master' into dev_5

This commit is contained in:
Jens Schuppe 2020-01-22 11:59:43 +01:00
commit cd7c7e7d12
16 changed files with 772 additions and 186 deletions

View file

@ -44,6 +44,76 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
*/
protected static $_paymentInstruments = NULL;
/**
* @var array
*
* A static cache of retrieved contribution statuses found within
* static::getContributionStatusOptions().
*/
protected static $_contributionStatusOptions = NULL;
/**
* @var array
*
* A static cache of retrieved groups found within static::getGroups().
*/
protected static $_groups = NULL;
/**
* @var array
*
* A static cache of retrieved newsletter groups found within
* static::getNewsletterGroups().
*/
protected static $_newsletterGroups = NULL;
/**
* @var array
*
* A static cache of retrieved campaigns found within static::getCampaigns().
*/
protected static $_campaigns = NULL;
/**
* @var array
*
* A static cache of retrieved financial types found within
* static::getFinancialTypes().
*/
protected static $_financialTypes = NULL;
/**
* @var array
*
* A static cache of retrieved genders found within
* static::getGenderOptions().
*/
protected static $_genderOptions = NULL;
/**
* @var array
*
* A static cache of retrieved location types found within
* static::getLocationTypes().
*/
protected static $_locationTypes = NULL;
/**
* @var array
*
* A static cache of retrieved membership types found within
* static::getMembershipTypes().
*/
protected static $_membershipTypes = NULL;
/**
* @var array
*
* A static cache of retrieved CiviSEPA creditors found within
* static::getSepaCreditors().
*/
protected static $_sepaCreditors = NULL;
/**
* Builds the form structure.
*/
@ -59,10 +129,6 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
$profile_name = NULL;
}
// Assign template variables.
$this->assign('op', $this->_op);
$this->assign('profile_name', $profile_name);
// Set redirect destination.
$this->controller->_destination = CRM_Utils_System::url('civicrm/admin/settings/twingle/profiles', 'reset=1');
@ -88,6 +154,21 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
}
CRM_Utils_System::setTitle(E::ts('Edit Twingle API profile <em>%1</em>', array(1 => $this->profile->getName())));
break;
case 'copy':
// This will be a 'create' actually.
$this->_op = 'create';
// When copying without a valid profile name, copy the default profile.
if (!$profile_name) {
$profile_name = 'default';
$this->profile = CRM_Twingle_Profile::getProfile($profile_name);
}
// Set a new name for this profile.
$profile_name = $profile_name . '_copy';
$this->profile->setName($profile_name);
CRM_Utils_System::setTitle(E::ts('New Twingle API profile'));
break;
case 'create':
// Load factory default profile values.
$this->profile = CRM_twingle_Profile::createDefaultProfile($profile_name);
@ -95,6 +176,10 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
break;
}
// Assign template variables.
$this->assign('op', $this->_op);
$this->assign('profile_name', $profile_name);
// Add form elements.
$is_default = $profile_name == 'default';
$this->add(
@ -109,7 +194,7 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
'text', // field type
'selector', // field name
E::ts('Project IDs'), // field label
array(),
['class' => 'huge'],
TRUE // is required
);
@ -117,7 +202,7 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
'select',
'location_type_id',
E::ts('Location type'),
$this->getLocationTypes(),
static::getLocationTypes(),
TRUE
);
@ -125,7 +210,7 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
'select',
'location_type_id_organisation',
E::ts('Location type for organisations'),
$this->getLocationTypes(),
static::getLocationTypes(),
TRUE
);
@ -133,14 +218,14 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
'select', // field type
'financial_type_id', // field name
E::ts('Financial type'), // field label
$this->getFinancialTypes(), // list of options
static::getFinancialTypes(), // list of options
TRUE // is required
);
$this->add(
'select', // field type
'financial_type_id_recur', // field name
E::ts('Financial type (recurring)'), // field label
$this->getFinancialTypes(), // list of options
static::getFinancialTypes(), // list of options
TRUE // is required
);
@ -148,21 +233,21 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
'select',
'gender_male',
E::ts('Gender option for submitted value "male"'),
$this->getGenderOptions(),
static::getGenderOptions(),
TRUE
);
$this->add(
'select',
'gender_female',
E::ts('Gender option for submitted value "female"'),
$this->getGenderOptions(),
static::getGenderOptions(),
TRUE
);
$this->add(
'select',
'gender_other',
E::ts('Gender option for submitted value "other"'),
$this->getGenderOptions(),
static::getGenderOptions(),
TRUE
);
@ -173,9 +258,17 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
'select', // field type
$pi_name, // field name
E::ts('Record %1 as', array(1 => $pi_label)), // field label
$this->getPaymentInstruments(), // list of options
static::getPaymentInstruments(), // list of options
TRUE // is required
);
$this->add(
'select',
$pi_name . '_status',
E::ts('Record %1 donations with contribution status', array(1 => $pi_label)),
static::getContributionStatusOptions(),
TRUE
);
}
if (CRM_Twingle_Submission::civiSepaEnabled()) {
@ -183,7 +276,7 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
'select',
'sepa_creditor_id',
E::ts('CiviSEPA creditor'),
$this->getSepaCreditors(),
static::getSepaCreditors(),
TRUE
);
}
@ -192,7 +285,7 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
'select', // field type
'newsletter_groups', // field name
E::ts('Sign up for newsletter groups'), // field label
$this->getNewsletterGroups(), // list of options
static::getNewsletterGroups(), // list of options
FALSE, // is not required
array('class' => 'crm-select2 huge', 'multiple' => 'multiple')
);
@ -201,7 +294,7 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
'select', // field type
'postinfo_groups', // field name
E::ts('Sign up for postal mail groups'), // field label
$this->getPostinfoGroups(), // list of options
static::getPostinfoGroups(), // list of options
FALSE, // is not required
array('class' => 'crm-select2 huge', 'multiple' => 'multiple')
);
@ -210,7 +303,7 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
'select', // field type
'donation_receipt_groups', // field name
E::ts('Sign up for Donation receipt groups'), // field label
$this->getDonationReceiptGroups(), // list of options
static::getDonationReceiptGroups(), // list of options
FALSE, // is not required
array('class' => 'crm-select2 huge', 'multiple' => 'multiple')
);
@ -219,7 +312,7 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
'select', // field type
'campaign', // field name
E::ts('Assign donation to campaign'), // field label
array('' => E::ts('- none -')) + $this->getCampaigns(), // list of options
array('' => E::ts('- none -')) + static::getCampaigns(), // list of options
FALSE, // is not required
array('class' => 'crm-select2 huge')
);
@ -228,7 +321,7 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
'select', // field type
'membership_type_id', // field name
E::ts('Create membership of type'), // field label
array('' => E::ts('- none -')) + $this->getMembershipTypes(), // list of options
array('' => E::ts('- none -')) + static::getMembershipTypes(), // list of options
FALSE, // is not required
array('class' => 'crm-select2 huge')
);
@ -240,6 +333,13 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
array()
);
$this->add(
'textarea', // field type
'custom_field_mapping', // field name
E::ts('Custom field mapping'), // field label
array()
);
$this->addButtons(array(
array(
'type' => 'submit',
@ -277,6 +377,69 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
$errors['name'] = E::ts('Only alphanumeric characters and the underscore (_) are allowed for profile names.');
}
// Validate custom field mapping.
try {
if (isset($values['custom_field_mapping'])) {
$custom_field_mapping = preg_split('/\r\n|\r|\n/', $values['custom_field_mapping'], -1, PREG_SPLIT_NO_EMPTY);
if (!is_array($custom_field_mapping)) {
throw new Exception(
E::ts('Could not parse custom field mapping.')
);
}
foreach ($custom_field_mapping as $custom_field_map) {
$custom_field_map = explode("=", $custom_field_map);
if (count($custom_field_map) !== 2) {
throw new Exception(
E::ts('Could not parse custom field mapping.')
);
}
list($twingle_field_name, $custom_field_name) = $custom_field_map;
$custom_field_id = substr($custom_field_name, strlen('custom_'));
// Check for custom field existence
try {
$custom_field = civicrm_api3('CustomField', 'getsingle', array(
'id' => $custom_field_id,
));
}
catch (CiviCRM_API3_Exception $exception) {
throw new Exception(
E::ts(
'Custom field custom_%1 does not exist.',
array(1 => $custom_field_id)
)
);
}
// Only allow custom fields on relevant entities.
try {
$custom_group = civicrm_api3('CustomGroup', 'getsingle', array(
'id' => $custom_field['custom_group_id'],
'extends' => array(
'IN' => array(
'Contact',
'Individual',
'Organization',
'Contribution',
'ContributionRecur',
),
),
));
} catch (CiviCRM_API3_Exception $exception) {
throw new Exception(
E::ts(
'Custom field custom_%1 is not in a CustomGroup that extends one of the supported CiviCRM entities.',
array(1 => $custom_field['id'])
)
);
}
}
}
}
catch (Exception $exception) {
$errors['custom_field_mapping'] = $exception->getMessage();
}
return empty($errors) ? TRUE : $errors;
}
@ -325,16 +488,18 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
*
* @throws \CiviCRM_API3_Exception
*/
public function getLocationTypes() {
$location_types = array();
$query = civicrm_api3('LocationType', 'get', array(
'is_active' => 1,
));
foreach ($query['values'] as $type) {
$location_types[$type['id']] = $type['name'];
public static function getLocationTypes() {
if (!isset(static::$_locationTypes)) {
static::$_locationTypes = array();
$query = civicrm_api3('LocationType', 'get', array(
'option.limit' => 0,
'is_active' => 1,
));
foreach ($query['values'] as $type) {
static::$_locationTypes[$type['id']] = $type['name'];
}
}
return $location_types;
return static::$_locationTypes;
}
/**
@ -345,17 +510,19 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
*
* @throws \CiviCRM_API3_Exception
*/
public function getFinancialTypes() {
$financial_types = array();
$query = civicrm_api3('FinancialType', 'get', array(
'is_active' => 1,
'option.limit' => 0,
'return' => 'id,name'
));
foreach ($query['values'] as $type) {
$financial_types[$type['id']] = $type['name'];
public static function getFinancialTypes() {
if (!isset(static::$_financialTypes)) {
static::$_financialTypes = array();
$query = civicrm_api3('FinancialType', 'get', array(
'option.limit' => 0,
'is_active' => 1,
'return' => 'id,name'
));
foreach ($query['values'] as $type) {
static::$_financialTypes[$type['id']] = $type['name'];
}
}
return $financial_types;
return static::$_financialTypes;
}
/**
@ -366,17 +533,19 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
*
* @throws \CiviCRM_API3_Exception
*/
public function getMembershipTypes() {
$membership_types = array();
$query = civicrm_api3('MembershipType', 'get', array(
'is_active' => 1,
'option.limit' => 0,
'return' => 'id,name'
));
foreach ($query['values'] as $type) {
$membership_types[$type['id']] = $type['name'];
public static function getMembershipTypes() {
if (!isset(static::$_membershipTypes)) {
static::$_membershipTypes = array();
$query = civicrm_api3('MembershipType', 'get', array(
'option.limit' => 0,
'is_active' => 1,
'return' => 'id,name'
));
foreach ($query['values'] as $type) {
static::$_membershipTypes[$type['id']] = $type['name'];
}
}
return $membership_types;
return static::$_membershipTypes;
}
/**
@ -387,21 +556,23 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
*
* @throws \CiviCRM_API3_Exception
*/
public function getGenderOptions() {
$genders = array();
$query = civicrm_api3('OptionValue', 'get', array(
'option_group_id' => 'gender',
'is_active' => 1,
'option.limit' => 0,
'return' => array(
'value',
'label',
),
));
foreach ($query['values'] as $gender) {
$genders[$gender['value']] = $gender['label'];
public static function getGenderOptions() {
if (!isset(static::$_genderOptions)) {
static::$_genderOptions = array();
$query = civicrm_api3('OptionValue', 'get', array(
'option.limit' => 0,
'option_group_id' => 'gender',
'is_active' => 1,
'return' => array(
'value',
'label',
),
));
foreach ($query['values'] as $gender) {
static::$_genderOptions[$gender['value']] = $gender['label'];
}
}
return $genders;
return static::$_genderOptions;
}
/**
@ -411,33 +582,37 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
*
* @throws \CiviCRM_API3_Exception
*/
public function getSepaCreditors() {
$creditors = array();
if (CRM_Twingle_Submission::civiSepaEnabled()) {
$result = civicrm_api3('SepaCreditor', 'get', array(
'option.limit' => 0,
));
foreach ($result['values'] as $sepa_creditor) {
$creditors[$sepa_creditor['id']] = $sepa_creditor['name'];
public static function getSepaCreditors() {
if (!isset(static::$_sepaCreditors)) {
static::$_sepaCreditors = array();
if (CRM_Twingle_Submission::civiSepaEnabled()) {
$result = civicrm_api3('SepaCreditor', 'get', array(
'option.limit' => 0,
));
foreach ($result['values'] as $sepa_creditor) {
static::$_sepaCreditors[$sepa_creditor['id']] = $sepa_creditor['name'];
}
}
}
return $creditors;
return static::$_sepaCreditors;
}
/**
* Retrieves payment instruments present within the system as options for
* select form elements.
*
* @return array
*
* @throws \CiviCRM_API3_Exception
*/
public function getPaymentInstruments() {
public static function getPaymentInstruments() {
if (!isset(self::$_paymentInstruments)) {
self::$_paymentInstruments = array();
$query = civicrm_api3('OptionValue', 'get', array(
'option.limit' => 0,
'option_group_id' => 'payment_instrument',
'is_active' => 1,
'option.limit' => 0,
'return' => 'value,label'
'return' => 'value,label'
));
foreach ($query['values'] as $payment_instrument) {
// Do not include CiviSEPA payment instruments, but add a SEPA option if
@ -472,81 +647,138 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
}
/**
* Retrieves active groups used as mailing lists within the system as options
* for select form elements.
* Retrieves contribution statuses as options for select form elements.
*
* @return array
*
* @throws \CiviCRM_API3_Exception
*/
public function getNewsletterGroups() {
$groups = array();
$group_types = civicrm_api3('OptionValue', 'get', array(
'option_group_id' => 'group_type',
'name' => CRM_Twingle_Submission::GROUP_TYPE_NEWSLETTER,
));
if ($group_types['count'] > 0) {
$group_type = reset($group_types['values']);
$query = civicrm_api3('Group', 'get', array(
'is_active' => 1,
'group_type' => array('LIKE' => '%' . CRM_Utils_Array::implodePadded($group_type['value']) . '%'),
'option.limit' => 0,
'return' => 'id,name'
));
foreach ($query['values'] as $group) {
$groups[$group['id']] = $group['name'];
public static function getContributionStatusOptions() {
if (!isset(self::$_contributionStatusOptions)) {
$query = civicrm_api3(
'OptionValue',
'get',
array(
'option.limit' => 0,
'option_group_id' => 'contribution_status',
'return' => array(
'value',
'label',
)
)
);
foreach ($query['values'] as $contribution_status) {
self::$_contributionStatusOptions[$contribution_status['value']] = $contribution_status['label'];
}
}
else {
$groups[''] = E::ts('No mailing lists available');
return self::$_contributionStatusOptions;
}
/**
* Retrieves active groups used as mailing lists within the system as options
* for select form elements.
*
* @return array
*
* @throws \CiviCRM_API3_Exception
*/
public static function getNewsletterGroups() {
if (!isset(static::$_newsletterGroups)) {
static::$_newsletterGroups = array();
$group_types = civicrm_api3('OptionValue', 'get', array(
'option.limit' => 0,
'option_group_id' => 'group_type',
'name' => CRM_Twingle_Submission::GROUP_TYPE_NEWSLETTER,
));
if ($group_types['count'] > 0) {
$group_type = reset($group_types['values']);
$query = civicrm_api3('Group', 'get', array(
'is_active' => 1,
'group_type' => array('LIKE' => '%' . CRM_Utils_Array::implodePadded($group_type['value']) . '%'),
'option.limit' => 0,
'return' => 'id,name'
));
foreach ($query['values'] as $group) {
static::$_newsletterGroups[$group['id']] = $group['name'];
}
}
else {
static::$_newsletterGroups[''] = E::ts('No mailing lists available');
}
}
return $groups;
return static::$_newsletterGroups;
}
/**
* Retrieves active groups as options for select form elements.
*
* @return array
*
* @throws \CiviCRM_API3_Exception
*/
public function getGroups() {
$groups = array();
$query = civicrm_api3('Group', 'get', array(
'is_active' => 1,
'option.limit' => 0,
'return' => 'id,name'
));
foreach ($query['values'] as $group) {
$groups[$group['id']] = $group['name'];
public static function getGroups() {
if (!isset(static::$_groups)) {
static::$_groups = array();
$query = civicrm_api3('Group', 'get', array(
'option.limit' => 0,
'is_active' => 1,
'return' => 'id,name'
));
foreach ($query['values'] as $group) {
static::$_groups[$group['id']] = $group['name'];
}
}
return $groups;
return static::$_groups;
}
/**
* Retrieves active groups used as postal mailing lists within the system as
* options for select form elements.
*
* @return array
*
* @throws \CiviCRM_API3_Exception
*/
public function getPostinfoGroups() {
return $this->getGroups();
public static function getPostinfoGroups() {
return static::getGroups();
}
/**
* Retrieves active groups used as donation receipt requester lists within the
* system as options for select form elements.
*
* @return array
*
* @throws \CiviCRM_API3_Exception
*/
public function getDonationReceiptGroups() {
return $this->getGroups();
public static function getDonationReceiptGroups() {
return static::getGroups();
}
/**
* Retrieves campaigns as options for select elements.
*
* @return array
*
* @throws \CiviCRM_API3_Exception
*/
public function getCampaigns() {
$campaigns = array();
$query = civicrm_api3('Campaign', 'get', array(
'option.limit' => 0,
'return' => array(
'id',
'title',
)
));
foreach ($query['values'] as $campaign) {
$campaigns[$campaign['id']] = $campaign['title'];
public static function getCampaigns() {
if (!isset(static::$_campaigns)) {
static::$_campaigns = array();
$query = civicrm_api3('Campaign', 'get', array(
'option.limit' => 0,
'return' => array(
'id',
'title',
)
));
foreach ($query['values'] as $campaign) {
static::$_campaigns[$campaign['id']] = $campaign['title'];
}
}
return $campaigns;
return static::$_campaigns;
}
}

View file

@ -69,6 +69,21 @@ class CRM_Twingle_Profile {
return in_array($project_id, $project_ids);
}
/**
* @return array
* The profile's configured custom field mapping
*/
public function getCustomFieldMapping() {
$custom_field_mapping = array();
if (!empty($custom_field_definition = $this->getAttribute('custom_field_mapping'))) {
foreach (preg_split('/\r\n|\r|\n/', $custom_field_definition, -1, PREG_SPLIT_NO_EMPTY) as $custom_field_map) {
list($twingle_field_name, $custom_field_name) = explode("=", $custom_field_map);
$custom_field_mapping[$twingle_field_name] = $custom_field_name;
}
}
return $custom_field_mapping;
}
/**
* Retrieves all data attributes of the profile.
*
@ -100,15 +115,16 @@ class CRM_Twingle_Profile {
* Retrieves an attribute of the profile.
*
* @param string $attribute_name
* @param mixed $default
*
* @return mixed | NULL
*/
public function getAttribute($attribute_name) {
public function getAttribute($attribute_name, $default = NULL) {
if (isset($this->data[$attribute_name])) {
return $this->data[$attribute_name];
}
else {
return NULL;
return $default;
}
}
@ -129,6 +145,21 @@ class CRM_Twingle_Profile {
$this->data[$attribute_name] = $value;
}
/**
* Get the CiviCRM transaction ID (to be used in contributions and recurring contributions)
*
* @param $twingle_id string Twingle ID
* @return string CiviCRM transaction ID
*/
public function getTransactionID($twingle_id) {
$prefix = Civi::settings()->get('twingle_prefix');
if (empty($prefix)) {
return $twingle_id;
} else {
return $prefix . $twingle_id;
}
}
/**
* Verifies whether the profile is valid (i.e. consistent and not colliding
* with other profiles).
@ -165,33 +196,32 @@ class CRM_Twingle_Profile {
* @return array
*/
public static function allowedAttributes() {
return array(
'selector',
'location_type_id',
'location_type_id_organisation',
'financial_type_id',
'financial_type_id_recur',
'pi_banktransfer',
'pi_debit_manual',
'pi_debit_automatic',
'pi_creditcard',
'pi_mobilephone_germany',
'pi_paypal',
'pi_sofortueberweisung',
'pi_amazonpay',
'pi_paydirekt',
'pi_applepay',
'pi_googlepay',
'sepa_creditor_id',
'gender_male',
'gender_female',
'gender_other',
'newsletter_groups',
'postinfo_groups',
'donation_receipt_groups',
'campaign',
'contribution_source',
'membership_type_id',
return array_merge(
array(
'selector',
'location_type_id',
'location_type_id_organisation',
'financial_type_id',
'financial_type_id_recur',
'sepa_creditor_id',
'gender_male',
'gender_female',
'gender_other',
'newsletter_groups',
'postinfo_groups',
'donation_receipt_groups',
'campaign',
'contribution_source',
'custom_field_mapping',
'membership_type_id',
),
// Add payment methods.
array_keys(static::paymentInstruments()),
// Add contribution status for all payment methods.
array_map(function ($attribute) {
return $attribute . '_status';
}, array_keys(static::paymentInstruments()))
);
}
@ -251,8 +281,13 @@ class CRM_Twingle_Profile {
'donation_receipt_groups' => NULL,
'campaign' => NULL,
'contribution_source' => NULL,
'custom_field_mapping' => NULL,
'membership_type_id' => NULL,
));
)
// Add contribution status for all payment methods.
+ array_fill_keys(array_map(function($attribute) {
return $attribute . '_status';
}, array_keys(static::paymentInstruments())), CRM_Twingle_Submission::CONTRIBUTION_STATUS_COMPLETED));
}
/**
@ -267,16 +302,14 @@ class CRM_Twingle_Profile {
public static function getProfileForProject($project_id) {
$profiles = self::getProfiles();
// If none matches, use the default profile.
$profile = $profiles['default'];
foreach ($profiles as $profile) {
if ($profile->matches($project_id)) {
break;
return $profile;
}
}
return $profile;
// If none matches, use the default profile.
return $profiles['default'];
}
/**
@ -305,7 +338,7 @@ class CRM_Twingle_Profile {
public static function getProfiles() {
if (self::$_profiles === NULL) {
self::$_profiles = array();
if ($profiles_data = CRM_Core_BAO_Setting::getItem('de.systopia.twingle', 'twingle_profiles')) {
if ($profiles_data = Civi::settings()->get('twingle_profiles')) {
foreach ($profiles_data as $profile_name => $profile_data) {
self::$_profiles[$profile_name] = new CRM_Twingle_Profile($profile_name, $profile_data);
}
@ -330,6 +363,6 @@ class CRM_Twingle_Profile {
foreach (self::$_profiles as $profile_name => $profile) {
$profile_data[$profile_name] = $profile->data;
}
CRM_Core_BAO_Setting::setItem((object) $profile_data, 'de.systopia.twingle', 'twingle_profiles');
Civi::settings()->set('twingle_profiles', $profile_data);
}
}

View file

@ -27,6 +27,11 @@ class CRM_Twingle_Submission {
*/
const GROUP_TYPE_NEWSLETTER = 'Mailing List';
/**
* The option value for the contribution type for completed contributions.
*/
const CONTRIBUTION_STATUS_COMPLETED = 'Completed';
/**
* The default ID of the "Employer of" relationship type.
*/
@ -99,6 +104,16 @@ class CRM_Twingle_Submission {
}
$params['gender_id'] = $gender_id;
}
// Validate custom fields parameter, if given.
if (!empty($params['custom_fields'])) {
if (!is_array($custom_fields = json_decode($params['custom_fields'], TRUE))) {
throw new CiviCRM_API3_Exception(
E::ts('Invalid format for custom fields.'),
'invalid_format'
);
}
}
}
/**
@ -254,10 +269,7 @@ class CRM_Twingle_Submission {
'is_active' => 1,
));
return
CRM_Core_BAO_Setting::getItem(
'de.systopia.twingle',
'twingle_use_sepa'
)
Civi::settings()->get('twingle_use_sepa')
&& $sepa_extension['count'];
}

94
CRM/Twingle/Tools.php Normal file
View file

@ -0,0 +1,94 @@
<?php
/*------------------------------------------------------------+
| SYSTOPIA Twingle Integration |
| Copyright (C) 2019 SYSTOPIA |
| Author: B. Endres (endres@systopia.de) |
+-------------------------------------------------------------+
| This program is released as free software under the |
| Affero GPL license. You can redistribute it and/or |
| modify it under the terms of this license which you |
| can read by viewing the included agpl.txt or online |
| at www.gnu.org/licenses/agpl.html. Removal of this |
| copyright header is strictly prohibited without |
| written permission from the original author(s). |
+-------------------------------------------------------------*/
use CRM_Twingle_ExtensionUtil as E;
class CRM_Twingle_Tools {
/**
* This flag can be used to temporarily suspend twingle protection
* @var bool
*/
public static $protection_suspended = FALSE;
/**
* Check if the attempted modification of the recurring contribution is allowed.
* If not, an exception will be raised
*
* @param $recurring_contribution_id int
* @param $change array
* @throws Exception if the change is not allowed
*/
public static function checkRecurringContributionChange($recurring_contribution_id, $change) {
// check if a change to the status is planned
if (empty($change['contribution_status_id'])) return;
// check if the target status is not closed
if (in_array($change['contribution_status_id'], [2,5])) return;
// 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',
'id' => $recurring_contribution_id]);
// check if this is a SEPA transaction
//if (self::isSDD($recurring_contribution['payment_instrument_id'])) 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."));
}
}
/**
* Check if the given payment instrument is SEPA
*
* @param $payment_instrument_id string payment instrument
* @return boolean
*/
public static function isSDD($payment_instrument_id) {
static $sepa_payment_instruments = NULL;
if ($sepa_payment_instruments === NULL) {
// init with instrument names
$sepa_payment_instruments = ['FRST', 'RCUR', 'OOFF'];
// lookup and add instrument IDs
$lookup = civicrm_api3('OptionValue', 'get', [
'option_group_id' => 'payment_instrument',
'name' => ['IN' => $sepa_payment_instruments],
'return' => 'value'
]);
foreach ($lookup['values'] as $payment_instrument) {
$sepa_payment_instruments[] = $payment_instrument['value'];
}
}
return in_array($payment_instrument_id, $sepa_payment_instruments);
}
}

View file

@ -143,4 +143,24 @@ class CRM_Twingle_Upgrader extends CRM_Twingle_Upgrader_Base {
return TRUE;
}
/**
* Convert serialized settings from objects to arrays.
*
* @link https://civicrm.org/advisory/civi-sa-2019-21-poi-saved-search-and-report-instance-apis
*/
public function upgrade_5011() {
// Do not use CRM_Core_BAO::getItem() or Civi::settings()->get().
// Extract and unserialize directly from the database.
$twingle_profiles_query = CRM_Core_DAO::executeQuery("
SELECT `value`
FROM `civicrm_setting`
WHERE `name` = 'twingle_profiles';");
if ($twingle_profiles_query->fetch()) {
$profiles = unserialize($twingle_profiles_query->value);
Civi::settings()->set('twingle_profiles', (array) $profiles);
}
return TRUE;
}
}

View file

@ -13,6 +13,11 @@ The extension is licensed under
*This section is yet to be completed.*
### Configure Extended Contact Matcher (XCM)
Make sure you use an XCM profile with the option *Match contacts by contact ID*
enabled.
### Configure CiviCRM
- Go to the Administration console `/civicrm/admin`
@ -36,18 +41,22 @@ The *default* profile is used whenever the plugin cannot match the Twingle
project ID from any other profile. Therefore the default profile will be used
for all newly created Twingle projects.
| Label | Description |
|---------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Profile name | Internal name, used inside the extension. |
| Project IDs | Twingle project IDs. Separate multiple IDs with commas. |
| Location type | Specify how the address data sent by the form should be categorised in CiviCRM. The list is based on your CiviCRM configuration. |
| Location type for organisations | Specify how the address data sent by the form should be categorised in CiviCRM for organisational donations. The list is based on your CiviCRM configuration. |
| Financial type | Specify which financial type incoming one-time donations should be recorded with in CiviCRM. The list is based on your CiviCRM configuration. |
| Financial type (recurring) | Specify which financial type incoming recurring donations should be recorded with in CiviCRM. The list is based on your CiviCRM configuration. |
| Gender options | Specify which CiviCRM gender option the incoming Twingle gender value should be mapped to. The list is based on your CiviCRM configuration. |
| Record *Payment method* as | Specifiy the payment methods mapping for incoming donations for each Twingle payment method. |
| CiviSEPA creditor | When enabled to integrate with CiviSEPA, specify the CiviSEPA creditor to use. |
| Sign up for groups | Whenever the donor checked the newsletter/postal mailing/donation receipt checkbox on the Twingle form, the contact will be added to the groups listed here. |
| Label | Description |
|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Profile name | Internal name, used inside the extension. |
| Project IDs | Twingle project IDs. Separate multiple IDs with commas. |
| Location type | Specify how the address data sent by the form should be categorised in CiviCRM. The list is based on your CiviCRM configuration. |
| Location type for organisations | Specify how the address data sent by the form should be categorised in CiviCRM for organisational donations. The list is based on your CiviCRM configuration. |
| Financial type | Specify which financial type incoming one-time donations should be recorded with in CiviCRM. The list is based on your CiviCRM configuration. |
| Financial type (recurring) | Specify which financial type incoming recurring donations should be recorded with in CiviCRM. The list is based on your CiviCRM configuration. |
| CiviSEPA creditor | When enabled to integrate with CiviSEPA, specify the CiviSEPA creditor to use. |
| Gender options | Specify which CiviCRM gender option the incoming Twingle gender value should be mapped to. The list is based on your CiviCRM configuration. |
| Record *Payment method* as | Specifiy the payment methods mapping for incoming donations for each Twingle payment method. |
| Sign up for groups | Whenever the donor checked the newsletter/postal mailing/donation receipt checkbox on the Twingle form, the contact will be added to the groups listed here. |
| Assign donation to campaign | The donation will be assigned to the selected campaign. If a campaign ID is being submitted using the `campaign_id` parameter, this setting will be overridden with the submitted value. |
| Create membership of type | A membership of the selected type will be created for the Individual contact. If no membership type is selected, no membership will be created. |
| Contribution source | The configured value will be set as the "Source" field for the contribution. |
| Custom field mapping | Additional field values may be set to CiviCRM custom fields using a mapping. See the option's help text for the exact format. |
## API documentation

View file

@ -80,15 +80,16 @@ function civicrm_api3_twingle_donation_Cancel($params) {
}
// Retrieve (recurring) contribution.
$default_profile = CRM_Twingle_Profile::getProfile('default');
try {
$contribution = civicrm_api3('Contribution', 'getsingle', array(
'trxn_id' => $params['trx_id'],
'trxn_id' => $default_profile->getTransactionID($params['trx_id']),
));
$contribution_type = 'Contribution';
}
catch (CiviCRM_API3_Exception $exception) {
$contribution = civicrm_api3('ContributionRecur', 'getsingle', array(
'trxn_id' => $params['trx_id'],
'trxn_id' => $default_profile->getTransactionID($params['trx_id']),
));
$contribution_type = 'ContributionRecur';
}
@ -121,12 +122,14 @@ function civicrm_api3_twingle_donation_Cancel($params) {
));
}
else {
CRM_Twingle_Tools::$protection_suspended = TRUE;
$contribution = civicrm_api3($contribution_type, 'create', array(
'id' => $contribution['id'],
'cancel_date' => $params['cancelled_at'],
'contribution_status_id' => 'Cancelled',
'cancel_reason' => $params['cancel_reason'],
));
CRM_Twingle_Tools::$protection_suspended = FALSE;
}
$result = civicrm_api3_create_success($contribution);

View file

@ -72,8 +72,9 @@ function civicrm_api3_twingle_donation_endrecurring($params) {
);
}
$default_profile = CRM_Twingle_Profile::getProfile('default');
$contribution = civicrm_api3('ContributionRecur', 'getsingle', array(
'trxn_id' => $params['trx_id'],
'trxn_id' => $default_profile->getTransactionID($params['trx_id']),
));
// End SEPA mandate (which ends the associated recurring contribution) or
// recurring contributions.
@ -87,6 +88,16 @@ function civicrm_api3_twingle_donation_endrecurring($params) {
time(),
date_create_from_format('Ymd', $params['cancelled_at'])->getTimestamp()
));
// verify that the mandate has not been terminated in the past
$mandate_status = civicrm_api3('SepaMandate', 'getvalue', ['return' => 'status', 'id' => $mandate_id]);
if ($mandate_status != 'FRST' && $mandate_status != 'RCUR') {
throw new CiviCRM_API3_Exception(
E::ts("SEPA Mandate [%1] already terminated.", [1 => $mandate_id]),
'api_error'
);
}
if (!CRM_Sepa_BAO_SEPAMandate::terminateMandate(
$mandate_id,
$end_date,
@ -102,11 +113,13 @@ function civicrm_api3_twingle_donation_endrecurring($params) {
));
}
else {
CRM_Twingle_Tools::$protection_suspended = TRUE;
$contribution = civicrm_api3('ContributionRecur', 'create', array(
'id' => $contribution['id'],
'end_date' => $params['ended_at'],
'contribution_status_id' => 'Completed',
'contribution_status_id' => CRM_Twingle_Submission::CONTRIBUTION_STATUS_COMPLETED,
));
CRM_Twingle_Tools::$protection_suspended = FALSE;
}
$result = civicrm_api3_create_success($contribution);

View file

@ -238,6 +238,13 @@ function _civicrm_api3_twingle_donation_Submit_spec(&$params) {
'api.required' => 0,
'description' => E::ts('The CiviCRM ID of a campaign to assign the contribution.'),
);
$params['custom_fields'] = array(
'name' => 'custom_fields',
'title' => E::ts('Custom fields'),
'type' => CRM_Utils_Type::T_STRING,
'api.required' => 0,
'description' => E::ts('Additional information for either the contact or the (recurring) contribution.'),
);
}
/**
@ -271,10 +278,10 @@ function civicrm_api3_twingle_donation_Submit($params) {
// Do not process an already existing contribution with the given
// transaction ID.
$existing_contribution = civicrm_api3('Contribution', 'get', array(
'trxn_id' => $params['trx_id']
'trxn_id' => $profile->getTransactionID($params['trx_id'])
));
$existing_contribution_recur = civicrm_api3('ContributionRecur', 'get', array(
'trxn_id' => $params['trx_id']
'trxn_id' => $profile->getTransactionID($params['trx_id'])
));
if ($existing_contribution['count'] > 0 || $existing_contribution_recur['count'] > 0) {
throw new CiviCRM_API3_Exception(
@ -283,6 +290,27 @@ function civicrm_api3_twingle_donation_Submit($params) {
);
}
// Extract custom field values using the profile's mapping of Twingle fields
// to CiviCRM custom fields.
$custom_fields = array();
if (!empty($params['custom_fields'])) {
$custom_field_mapping = $profile->getCustomFieldMapping();
foreach (json_decode($params['custom_fields']) as $twingle_field => $value) {
if (isset($custom_field_mapping[$twingle_field])) {
// Get custom field definition to store values by entity the field
// extends.
$custom_field_id = substr($custom_field_mapping[$twingle_field], strlen('custom_'));
$custom_field = civicrm_api3('CustomField', 'getsingle', array(
'id' => $custom_field_id,
// Chain a CustomGroup.getsingle API call.
'api.CustomGroup.getsingle' => array(),
));
$custom_fields[$custom_field['api.CustomGroup.getsingle']['extends']][$custom_field_mapping[$twingle_field]] = $value;
}
}
}
// Create contact(s).
if ($params['is_anonymous']) {
// Retrieve the ID of the contact to use for anonymous donations defined
@ -349,11 +377,21 @@ function civicrm_api3_twingle_donation_Submit($params) {
'user_email' => 'email',
'user_telephone' => 'phone',
'user_title' => 'formal_title',
'debit_iban' => 'iban',
) as $contact_param => $contact_component) {
if (!empty($params[$contact_param])) {
$contact_data[$contact_component] = $params[$contact_param];
}
}
// Add custom field values.
if (!empty($custom_fields['Contact'])) {
$contact_data += $custom_fields['Contact'];
}
if (!empty($custom_fields['Individual'])) {
$contact_data += $custom_fields['Individual'];
}
if (!$contact_id = CRM_Twingle_Submission::getContact(
'Individual',
$contact_data
@ -378,6 +416,12 @@ function civicrm_api3_twingle_donation_Submit($params) {
$organisation_data = array(
'organization_name' => $params['organization_name'],
);
// Add custom field values.
if (!empty($custom_fields['Organization'])) {
$organisation_data += $custom_fields['Organization'];
}
if (!empty($submitted_address)) {
$organisation_data += $submitted_address;
// Use configured location type for organisation address.
@ -467,12 +511,17 @@ function civicrm_api3_twingle_donation_Submit($params) {
$contribution_data = array(
'contact_id' => (isset($organisation_id) ? $organisation_id : $contact_id),
'currency' => $params['currency'],
'trxn_id' => $params['trx_id'],
'trxn_id' => $profile->getTransactionID($params['trx_id']),
'payment_instrument_id' => $params['payment_instrument_id'],
'amount' => $params['amount'] / 100,
'total_amount' => $params['amount'] / 100,
);
// Add custom field values.
if (!empty($custom_fields['Contribution'])) {
$contribution_data += $custom_fields['Contribution'];
}
if (!empty($params['purpose'])) {
$contribution_data['note'] = $params['purpose'];
}
@ -526,6 +575,10 @@ function civicrm_api3_twingle_donation_Submit($params) {
)
// ... and frequency unit and interval from a static mapping.
+ CRM_Twingle_Submission::getFrequencyMapping($params['donation_rhythm']);
// Add custom field values.
if (!empty($custom_fields['ContributionRecur'])) {
$mandate_data += $custom_fields['ContributionRecur'];
}
// Add cycle day for recurring contributions.
if ($params['donation_rhythm'] != 'one_time') {
@ -557,6 +610,13 @@ function civicrm_api3_twingle_donation_Submit($params) {
'financial_type_id' => $profile->getAttribute('financial_type_id_recur'),
)
+ CRM_Twingle_Submission::getFrequencyMapping($params['donation_rhythm']);
// Add custom field values.
if (!empty($custom_fields['ContributionRecur'])) {
$contribution_recur_data += $custom_fields['ContributionRecur'];
$contribution_data += $custom_fields['ContributionRecur'];
}
$contribution_recur = civicrm_api3('contributionRecur', 'create', $contribution_recur_data);
if ($contribution_recur['is_error']) {
throw new CiviCRM_API3_Exception(
@ -573,7 +633,7 @@ function civicrm_api3_twingle_donation_Submit($params) {
// Create contribution.
$contribution_data += array(
'contribution_status_id' => 'Completed',
'contribution_status_id' => $profile->getAttribute("pi_{$params['payment_method']}_status", CRM_Twingle_Submission::CONTRIBUTION_STATUS_COMPLETED),
'receive_date' => $params['confirmed_at'],
);
$contribution = civicrm_api3('Contribution', 'create', $contribution_data);

View file

@ -19,6 +19,7 @@
<develStage>dev</develStage>
<compatibility>
<ver>4.7</ver>
<ver>5.0</ver>
</compatibility>
<comments></comments>
<requires>

View file

@ -31,4 +31,32 @@ return array(
'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.',
),
);

View file

@ -16,6 +16,11 @@
{ts domain="de.systopia.twingle"}Select which location type to use for addresses for individuals, either when no organisation name is specified, or an organisation address can not be shared with the individual contact.{/ts}
{/htxt}
{htxt id='id-project_ids'}
{ts domain="de.systopia.twingle"}Put your project's Twingle ID in here, to activate this profile for that project.{/ts}
{ts domain="de.systopia.twingle"}You can also provide multiple project IDs separated by a comma.{/ts}
{/htxt}
{htxt id='id-location_type_id_organisation'}
{ts domain="de.systopia.twingle"}Select which location type to use for addresses for organisations and shared organisation addresses for individual contacts.{/ts}
{/htxt}
@ -27,3 +32,17 @@
{htxt id='id-financial_type_id_recur'}
{ts domain="de.systopia.twingle"}Select which financial type to use for recurring contributions.{/ts}
{/htxt}
{htxt id='id-custom_field_mapping'}
{ts domain="de.systopia.twingle"}<p>Map Twingle custom fields to CiviCRM custom fields using the following format (each assignment in a separate line):</p>
<pre>twingle_field_1=custom_123<br />twingle_field_2=custom_789</pre>
<p>Always use the <code>custom_[id]</code> notation for CiviCRM custom fields.</p>
<p>Only custom fields extending one of the following CiviCRM entities are allowed:</p>
<ul>
<li><strong>Contact</strong> &ndash; Will be set on the Individual contact</li>
<li><strong>Individual</strong> &ndash; Will be set on the Individual contact</li>
<li><strong>Organization</strong> &ndash; Will be set on the Organization contact, if an organisation name was submitted</li>
<li><strong>Contribution</strong> &ndash; Will be set on the contribution</li>
<li><strong>ContributionRecur</strong> &ndash; Will be set on the recurring contribution and deriving single contributions</li>
</ul>{/ts}
{/htxt}

View file

@ -28,7 +28,23 @@
</tr>
<tr class="crm-section">
<td class="label">{$form.selector.label}</td>
<td class="label">{$form.selector.label}
<a
onclick='
CRM.help(
"{ts domain="de.systopia.twingle"}Project IDs{/ts}",
{literal}{
"id": "id-project_ids",
"file": "CRM\/Twingle\/Form\/Profile"
}{/literal}
);
return false;
'
href="#"
title="{ts domain="de.systopia.twingle"}Help{/ts}"
class="helpicon"
></a>
</td>
<td class="content">{$form.selector.html}</td>
</tr>
@ -151,8 +167,14 @@
<table class="form-layout-compressed">
{foreach key=pi_name item=pi_label from=$payment_instruments}
<tr class="crm-section {cycle values="odd,even"}">
<td class="label">{$form.$pi_name.label}</td>
<td class="content">{$form.$pi_name.html}</td>
{capture assign="pi_contribution_status"}{$pi_name}_status{/capture}
<td class="label">{$form.$pi_contribution_status.label}</td>
<td class="content">{$form.$pi_contribution_status.html}</td>
</tr>
{/foreach}
</table>
@ -161,7 +183,7 @@
<fieldset>
<legend>{ts domain="de.systopia.twingle"}Groups{/ts}</legend>
<legend>{ts domain="de.systopia.twingle"}Groups and Correlations{/ts}</legend>
<table class="form-layout-compressed">
@ -195,6 +217,28 @@
<td class="content">{$form.contribution_source.html}</td>
</tr>
<tr class="crm-section">
<td class="label">
{$form.custom_field_mapping.label}
<a
onclick='
CRM.help(
"{ts domain="de.systopia.twingle"}Custom field mapping{/ts}",
{literal}{
"id": "id-custom_field_mapping",
"file": "CRM\/Twingle\/Form\/Profile"
}{/literal}
);
return false;
'
href="#"
title="{ts domain="de.systopia.twingle"}Help{/ts}"
class="helpicon"
></a>
</td>
<td class="content">{$form.custom_field_mapping.html}</td>
</tr>
</table>
</fieldset>

View file

@ -15,3 +15,11 @@
{htxt id='id-twingle_use_sepa'}
{ts domain="de.systopia.twingle" 1="<a href=\"https://github.com/project60/org.project60.sepa\" target=\"_blank\">CiviSEPA (<kbd>org.project60.sepa</kbd>) extension</a>"}When the %1 is enabled and one of its payment instruments is assigned to a Twingle payment method (practically the <em>debit_manual</em> payment method), submitting a Twingle donation through the API will create a SEPA mandate with the given data.{/ts}
{/htxt}
{htxt id='id-twingle_protect_recurring'}
{ts domain="de.systopia.twingle"}Will protect all recurring contributions created by Twingle from termination, since this does NOT terminate the Twingle collection process{/ts}
{/htxt}
{htxt id='id-twingle_prefix'}
{ts domain="de.systopia.twingle"}You can use this setting to add a prefix to the Twingle transaction ID, in order to avoid collisions with other transaction ids.{/ts}
{/htxt}

View file

@ -39,6 +39,7 @@
</td>
<td>
<a href="{crmURL p="civicrm/admin/settings/twingle/profile" q="op=edit&name=$profile_name"}" title="{ts domain="de.systopia.twingle" 1=$profile.name}Edit profile %1{/ts}" class="action-item crm-hover-button">{ts domain="de.systopia.twingle"}Edit{/ts}</a>
<a href="{crmURL p="civicrm/admin/settings/twingle/profile" q="op=copy&name=$profile_name"}" title="{ts domain="de.systopia.twingle" 1=$profile.name}Copy profile %1{/ts}" class="action-item crm-hover-button">{ts domain="de.systopia.twingle"}Copy{/ts}</a>
{if $profile_name == 'default'}
<a href="{crmURL p="civicrm/admin/settings/twingle/profile" q="op=delete&name=$profile_name"}" title="{ts domain="de.systopia.twingle" 1=$profile.name}Reset profile %1{/ts}" class="action-item crm-hover-button">{ts domain="de.systopia.twingle"}Reset{/ts}</a>
{else}

View file

@ -3,6 +3,15 @@
require_once 'twingle.civix.php';
use CRM_Twingle_ExtensionUtil as E;
/**
* Implements hook_civicrm_pre().
*/
function twingle_civicrm_pre($op, $objectName, $id, &$params) {
if ($objectName == 'ContributionRecur' && $op == 'edit') {
CRM_Twingle_Tools::checkRecurringContributionChange($id, $params);
}
}
/**
* Implements hook_civicrm_config().
*