diff --git a/CRM/Twingle/Form/Profile.php b/CRM/Twingle/Form/Profile.php index 25f871c..fae3b0a 100644 --- a/CRM/Twingle/Form/Profile.php +++ b/CRM/Twingle/Form/Profile.php @@ -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 %1', 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; } } diff --git a/CRM/Twingle/Profile.php b/CRM/Twingle/Profile.php index 8fc9ec4..fafe07e 100644 --- a/CRM/Twingle/Profile.php +++ b/CRM/Twingle/Profile.php @@ -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); } } diff --git a/CRM/Twingle/Submission.php b/CRM/Twingle/Submission.php index a13d624..50c7204 100644 --- a/CRM/Twingle/Submission.php +++ b/CRM/Twingle/Submission.php @@ -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']; } diff --git a/CRM/Twingle/Tools.php b/CRM/Twingle/Tools.php new file mode 100644 index 0000000..6837d2f --- /dev/null +++ b/CRM/Twingle/Tools.php @@ -0,0 +1,94 @@ +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); + } +} diff --git a/CRM/Twingle/Upgrader.php b/CRM/Twingle/Upgrader.php index ec78674..35c1283 100644 --- a/CRM/Twingle/Upgrader.php +++ b/CRM/Twingle/Upgrader.php @@ -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; + } + } diff --git a/README.md b/README.md index 0b17b50..aa169f9 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/api/v3/TwingleDonation/Cancel.php b/api/v3/TwingleDonation/Cancel.php index 7a8c5fc..4883217 100644 --- a/api/v3/TwingleDonation/Cancel.php +++ b/api/v3/TwingleDonation/Cancel.php @@ -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); diff --git a/api/v3/TwingleDonation/Endrecurring.php b/api/v3/TwingleDonation/Endrecurring.php index eced12e..aa5d967 100644 --- a/api/v3/TwingleDonation/Endrecurring.php +++ b/api/v3/TwingleDonation/Endrecurring.php @@ -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); diff --git a/api/v3/TwingleDonation/Submit.php b/api/v3/TwingleDonation/Submit.php index 3bfc41f..2bc3d3a 100644 --- a/api/v3/TwingleDonation/Submit.php +++ b/api/v3/TwingleDonation/Submit.php @@ -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); diff --git a/info.xml b/info.xml index 2ef8a60..9449764 100644 --- a/info.xml +++ b/info.xml @@ -19,6 +19,7 @@ dev 4.7 + 5.0 diff --git a/settings/twingle.setting.php b/settings/twingle.setting.php index 164c97e..d703845 100644 --- a/settings/twingle.setting.php +++ b/settings/twingle.setting.php @@ -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.', + ), ); diff --git a/templates/CRM/Twingle/Form/Profile.hlp b/templates/CRM/Twingle/Form/Profile.hlp index f2c80e7..3e82883 100644 --- a/templates/CRM/Twingle/Form/Profile.hlp +++ b/templates/CRM/Twingle/Form/Profile.hlp @@ -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"}

Map Twingle custom fields to CiviCRM custom fields using the following format (each assignment in a separate line):

+
twingle_field_1=custom_123
twingle_field_2=custom_789
+

Always use the custom_[id] notation for CiviCRM custom fields.

+

Only custom fields extending one of the following CiviCRM entities are allowed:

+ {/ts} +{/htxt} diff --git a/templates/CRM/Twingle/Form/Profile.tpl b/templates/CRM/Twingle/Form/Profile.tpl index c7ad9db..05cd32e 100644 --- a/templates/CRM/Twingle/Form/Profile.tpl +++ b/templates/CRM/Twingle/Form/Profile.tpl @@ -28,7 +28,23 @@ - {$form.selector.label} + {$form.selector.label} + + {$form.selector.html} @@ -151,8 +167,14 @@ {foreach key=pi_name item=pi_label from=$payment_instruments} + + + {capture assign="pi_contribution_status"}{$pi_name}_status{/capture} + + + {/foreach}
{$form.$pi_name.label} {$form.$pi_name.html}{$form.$pi_contribution_status.label}{$form.$pi_contribution_status.html}
@@ -161,7 +183,7 @@
- {ts domain="de.systopia.twingle"}Groups{/ts} + {ts domain="de.systopia.twingle"}Groups and Correlations{/ts} @@ -195,6 +217,28 @@ + + + + +
{$form.contribution_source.html}
+ {$form.custom_field_mapping.label} + + {$form.custom_field_mapping.html}
diff --git a/templates/CRM/Twingle/Form/Settings.hlp b/templates/CRM/Twingle/Form/Settings.hlp index 2f0a45a..8b5ef1b 100644 --- a/templates/CRM/Twingle/Form/Settings.hlp +++ b/templates/CRM/Twingle/Form/Settings.hlp @@ -15,3 +15,11 @@ {htxt id='id-twingle_use_sepa'} {ts domain="de.systopia.twingle" 1="CiviSEPA (org.project60.sepa) extension"}When the %1 is enabled and one of its payment instruments is assigned to a Twingle payment method (practically the debit_manual 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} diff --git a/templates/CRM/Twingle/Page/Profiles.tpl b/templates/CRM/Twingle/Page/Profiles.tpl index 3b935a7..30dbfce 100644 --- a/templates/CRM/Twingle/Page/Profiles.tpl +++ b/templates/CRM/Twingle/Page/Profiles.tpl @@ -39,6 +39,7 @@ {ts domain="de.systopia.twingle"}Edit{/ts} + {ts domain="de.systopia.twingle"}Copy{/ts} {if $profile_name == 'default'} {ts domain="de.systopia.twingle"}Reset{/ts} {else} diff --git a/twingle.php b/twingle.php index c2a064e..214d041 100644 --- a/twingle.php +++ b/twingle.php @@ -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(). *