+--------------------------------------------------------------------+
*/
+use Civi\Api4\Address;
use Civi\Api4\Campaign;
use Civi\Api4\Contact;
use Civi\Api4\CustomField;
use Civi\Api4\DedupeRule;
use Civi\Api4\DedupeRuleGroup;
+use Civi\Api4\Email;
use Civi\Api4\Event;
+use Civi\Api4\Phone;
use Civi\Api4\UserJob;
use Civi\UserJob\UserJobInterface;
* @return array[]
*/
protected function getContactFields(string $contactType): array {
- $contactFields = CRM_Contact_BAO_Contact::importableFields($contactType, NULL);
+ $contactFields = $this->getAllContactFields('');
$dedupeFields = $this->getDedupeFields($contactType);
$contactFieldsForContactLookup = [];
*
* @param array $params
* @param int|null $dedupeRuleID
+ * @param bool $isApiMetadata
+ * Is the import using api4 style metadata (in which case no conversion needed) - eventually
+ * only contact import will use a different style (as it supports multiple locations) and the
+ * handling will be in that class.
*
* @return array
*
* @throws \CRM_Core_Exception
*/
- protected function getPossibleMatchesByDedupeRule(array $params, $dedupeRuleID = NULL): array {
- foreach (['email', 'address', 'phone', 'im'] as $locationEntity) {
- if (array_key_exists($locationEntity, $params)) {
- // Prefer primary
- if (array_key_exists('Primary', $params[$locationEntity])) {
- $locationParams = $params[$locationEntity]['Primary'];
- }
- else {
- // Chose the first one - at least they can manipulate the order.
- $locationParams = reset($params[$locationEntity]);
- }
- foreach ($locationParams as $key => $locationParam) {
- // Even though we might not be using 'primary' we 'pretend' here
- // since the apiv4 code expects that...
- $params[$locationEntity . '_primary' . '.' . $key] = $locationParam;
+ protected function getPossibleMatchesByDedupeRule(array $params, $dedupeRuleID = NULL, $isApiMetadata = TRUE): array {
+ if ($isApiMetadata === FALSE) {
+ foreach (['email', 'address', 'phone', 'im'] as $locationEntity) {
+ if (array_key_exists($locationEntity, $params)) {
+ // Prefer primary
+ if (array_key_exists('Primary', $params[$locationEntity])) {
+ $locationParams = $params[$locationEntity]['Primary'];
+ }
+ else {
+ // Chose the first one - at least they can manipulate the order.
+ $locationParams = reset($params[$locationEntity]);
+ }
+ foreach ($locationParams as $key => $locationParam) {
+ // Even though we might not be using 'primary' we 'pretend' here
+ // since the apiv4 code expects that...
+ $params[$locationEntity . '_primary' . '.' . $key] = $locationParam;
+ }
+ unset($params[$locationEntity]);
}
- unset($params[$locationEntity]);
}
- }
- foreach ($params as $key => $value) {
- if (strpos($key, 'custom_') === 0) {
- $params[$this->getApi4Name($key)] = $value;
- unset($params[$key]);
+ foreach ($params as $key => $value) {
+ if (strpos($key, 'custom_') === 0) {
+ $params[$this->getApi4Name($key)] = $value;
+ unset($params[$key]);
+ }
}
}
$dedupeRule = $dedupeRuleID ? $this->getDedupeRuleName($dedupeRuleID) : $this->getDefaultRuleForContactType($params['contact_type']);
return $this->getDedupeRule($contactType)['fields'];
}
+ /**
+ * Get all contact import fields metadata.
+ *
+ * @param string $prefix
+ *
+ * @return array
+ *
+ * @noinspection PhpUnhandledExceptionInspection
+ */
+ protected function getAllContactFields(string $prefix = 'Contact.'): array {
+ $allContactFields = (array) Contact::getFields()
+ ->addWhere('readonly', '=', FALSE)
+ ->addWhere('type', 'IN', ['Field', 'Custom'])
+ ->addWhere('fk_entity', 'IS EMPTY')
+ ->addOrderBy('title')
+ ->execute()->indexBy('name');
+
+ $contactTypeFields['Individual'] = (array) Contact::getFields()
+ ->addWhere('readonly', '=', FALSE)
+ ->addWhere('type', 'IN', ['Field', 'Custom'])
+ ->addWhere('fk_entity', 'IS EMPTY')
+ ->setSelect(['name'])
+ ->addValue('contact_type', 'Individual')
+ ->addOrderBy('title')
+ ->execute()->indexBy('name');
+
+ $contactTypeFields['Organization'] = (array) Contact::getFields()
+ ->addWhere('readonly', '=', FALSE)
+ ->addWhere('type', 'IN', ['Field', 'Custom'])
+ ->addWhere('fk_entity', 'IS EMPTY')
+ ->setSelect(['name'])
+ ->addValue('contact_type', 'Organization')
+ ->addOrderBy('title')
+ ->execute()->indexBy('name');
+
+ $contactTypeFields['Household'] = (array) Contact::getFields()
+ ->addWhere('readonly', '=', FALSE)
+ ->addWhere('type', 'IN', ['Field', 'Custom'])
+ ->addWhere('fk_entity', 'IS EMPTY')
+ ->setSelect(['name'])
+ ->addOrderBy('title')
+ ->execute()->indexBy('name');
+
+ $prefixedFields = [];
+ foreach ($allContactFields as $fieldName => $field) {
+ $field['contact_type'] = [];
+ foreach ($contactTypeFields as $contactTypeName => $fields) {
+ if (array_key_exists($fieldName, $fields)) {
+ $field['contact_type'][$contactTypeName] = $contactTypeName;
+ }
+ }
+ $fieldName = $prefix . $fieldName;
+ if (!empty($field['custom_field_id'])) {
+ $this->customFieldNameMap['custom_' . $field['custom_field_id']] = $fieldName;
+ }
+ $prefixedFields[$fieldName] = $field;
+ }
+
+ $addressFields = (array) Address::getFields()
+ ->addWhere('readonly', '=', FALSE)
+ ->addWhere('type', 'IN', ['Field', 'Custom'])
+ ->addOrderBy('title')
+ // Exclude these fields to keep it simpler for now - we just map to primary
+ ->addWhere('name', 'NOT IN', ['id', 'location_type_id', 'master_id'])
+ ->execute()->indexBy('name');
+ foreach ($addressFields as $fieldName => $field) {
+ // Set entity to contact as primary fields used in Contact actions
+ $field['entity'] = 'Contact';
+ $field['name'] = 'address_primary.' . $fieldName;
+ $field['contact_type'] = ['Individual', 'Organization', 'Household'];
+ $prefixedFields[$prefix . 'address_primary.' . $fieldName] = $field;
+ }
+
+ $phoneFields = (array) Phone::getFields()
+ ->addWhere('readonly', '=', FALSE)
+ ->addWhere('type', 'IN', ['Field', 'Custom'])
+ // Exclude these fields to keep it simpler for now - we just map to primary
+ ->addWhere('name', 'NOT IN', ['id', 'location_type_id', 'phone_type_id'])
+ ->addOrderBy('title')
+ ->execute()->indexBy('name');
+ foreach ($phoneFields as $fieldName => $field) {
+ $field['entity'] = 'Contact';
+ $field['name'] = 'phone_primary.' . $fieldName;
+ $field['contact_type'] = ['Individual', 'Organization', 'Household'];
+ $prefixedFields[$prefix . 'phone_primary.' . $fieldName] = $field;
+ }
+
+ $emailFields = (array) Email::getFields()
+ ->addWhere('readonly', '=', FALSE)
+ ->addWhere('type', 'IN', ['Field', 'Custom'])
+ // Exclude these fields to keep it simpler for now - we just map to primary
+ ->addWhere('name', 'NOT IN', ['id', 'location_type_id'])
+ ->addOrderBy('title')
+ ->execute()->indexBy('name');
+
+ foreach ($emailFields as $fieldName => $field) {
+ $field['entity'] = 'Contact';
+ $field['name'] = 'email_primary.' . $fieldName;
+ $field['contact_type'] = ['Individual', 'Organization', 'Household'];
+ $prefixedFields[$prefix . 'email_primary.' . $fieldName] = $field;
+ }
+ return $prefixedFields;
+ }
+
}
$this->addTask('Increase field length of civicrm_dedupe_rule_group.name', 'alterDedupeRuleGroupName');
$this->addTask('Add index civicrm_dedupe_rule_group.UI_name', 'addIndex', 'civicrm_dedupe_rule_group', 'name', 'UI');
$this->addTask('Install Elavon Payment Processor Extension as needed', 'installElavonPaymentProcessorExtension');
+ $this->addTask('Convert field names for contribution import saved mappings', 'updateContributionMappings');
}
public static function addCreatedIDColumnToParticipant($ctx): bool {
return TRUE;
}
+ /**
+ * Update saved mappings for contribution imports to use apiv4 style field names.
+ *
+ * In time we will do this to the other imports.
+ *
+ * @return true
+ */
+ public static function updateContributionMappings(): bool {
+ $mappingTypeID = (int) CRM_Core_DAO::singleValueQuery("
+ SELECT option_value.value
+ FROM civicrm_option_value option_value
+ INNER JOIN civicrm_option_group option_group
+ ON option_group.id = option_value.option_group_id
+ AND option_group.name = 'mapping_type'
+ WHERE option_value.name = 'Import Contribution'");
+
+ $mappingFields = CRM_Core_DAO::executeQuery('
+ SELECT field.id, field.name FROM civicrm_mapping_field field
+ INNER JOIN civicrm_mapping mapping
+ ON field.mapping_id = mapping.id
+ AND mapping_type_id = ' . $mappingTypeID
+ );
+ // Only dedupe fields could be stored. Phone number, email, address fields & custom fields
+ // is a realistic set. The impact of missing something is pretty minor as saved field mappings
+ // are easy to update during import & people normally do a visual check - so hard coding a list
+ // feels more future-proof than doing it by code.
+ $fieldsToConvert = [
+ 'email' => 'email_primary.email',
+ 'phone' => 'phone_primary.phone',
+ 'street_address' => 'address_primary.street_address',
+ 'supplemental_address_1' => 'address_primary.supplemental_address_1',
+ 'supplemental_address_2' => 'address_primary.supplemental_address_2',
+ 'supplemental_address_3' => 'address_primary.supplemental_address_3',
+ 'city' => 'address_primary.city',
+ 'county_id' => 'address_primary.county_id',
+ 'state_province_id' => 'address_primary.state_province_id',
+ 'country_id' => 'address_primary.country_id',
+ ];
+ $customFields = CRM_Core_DAO::executeQuery('
+ SELECT custom_field.id, custom_field.name, custom_group.name as custom_group_name
+ FROM civicrm_custom_field custom_field INNER JOIN civicrm_custom_group custom_group
+ ON custom_field.custom_group_id = custom_group.id
+ WHERE extends IN ("Contact", "Individual", "Organization", "Household")
+ ');
+ while ($customFields->fetch()) {
+ $fieldsToConvert['custom_' . $customFields->id] = $customFields->custom_group_name . '.' . $customFields->name;
+ }
+ while ($mappingFields->fetch()) {
+ // Convert the field.
+ if (isset($fieldsToConvert[$mappingFields->name])) {
+ CRM_Core_DAO::executeQuery(' UPDATE civicrm_mapping_field SET name = %1 WHERE id = %2', [
+ 1 => [$fieldsToConvert[$mappingFields->name], 'String'],
+ 2 => [$mappingFields->id, 'Integer'],
+ ]);
+ }
+ }
+ return TRUE;
+ }
+
}