*/
class CRM_Contribute_Import_Form_MapField extends CRM_Import_Form_MapField {
- /**
- * Check if required fields are present.
- *
- * @param CRM_Contribute_Import_Form_MapField $self
- * @param string $contactORContributionId
- * @param array $importKeys
- * @param array $errors
- * @param int $weightSum
- * @param int $threshold
- * @param string $fieldMessage
- *
- * @return array
- */
- protected static function checkRequiredFields($self, string $contactORContributionId, array $importKeys, array $errors, int $weightSum, $threshold, string $fieldMessage): array {
- // FIXME: should use the schema titles, not redeclare them
- $requiredFields = [
- 'contribution_contact_id' => ts('Contact ID'),
- ];
-
- foreach ($requiredFields as $field => $title) {
- if (!in_array($field, $importKeys)) {
- if ($field == 'contribution_contact_id') {
- if (!($weightSum >= $threshold || in_array('external_identifier', $importKeys))
- ) {
- $errors['_qf_default'] .= ts('Missing required contact matching fields.') . " $fieldMessage " . ts('(Sum of all weights should be greater than or equal to threshold: %1).', [1 => $threshold]) . '<br />';
- }
- }
- }
- }
- return $errors;
- }
-
/**
* Set variables up before form is built.
*/
* list of errors to be posted back to the form
*/
public static function formRule($fields, $files, $self) {
- $errors = [];
- $fieldMessage = NULL;
- $contactORContributionId = $self->isUpdateExisting() ? 'contribution_id' : 'contribution_contact_id';
- if (!array_key_exists('savedMapping', $fields)) {
- $importKeys = [];
- foreach ($fields['mapper'] as $mapperPart) {
- $importKeys[] = $mapperPart[0];
- }
-
- $params = [
- 'used' => 'Unsupervised',
- 'contact_type' => $self->getContactType(),
- ];
- [$ruleFields, $threshold] = CRM_Dedupe_BAO_DedupeRuleGroup::dedupeRuleFieldsWeight($params);
- $weightSum = 0;
- foreach ($importKeys as $key => $val) {
- if (array_key_exists($val, $ruleFields)) {
- $weightSum += $ruleFields[$val];
- }
- }
- foreach ($ruleFields as $field => $weight) {
- $fieldMessage .= ' ' . $field . '(weight ' . $weight . ')';
- }
- try {
- $parser = $self->getParser();
- $parser->validateMapping($fields['mapper']);
- }
- catch (CRM_Core_Exception $e) {
- $errors['_qf_default'] = $e->getMessage();
- }
+ $mapperError = [];
+ try {
+ $parser = $self->getParser();
+ $rule = $parser->getDedupeRule($self->getContactType());
if (!$self->isUpdateExisting()) {
- $errors = self::checkRequiredFields($self, $contactORContributionId, $importKeys, $errors, $weightSum, $threshold, $fieldMessage);
+ $missingDedupeFields = $self->validateDedupeFieldsSufficientInMapping($rule, $fields['mapper']);
+ if ($missingDedupeFields) {
+ $mapperError[] = $missingDedupeFields;
+ }
}
+ $parser->validateMapping($fields['mapper']);
}
- return !empty($errors) ? $errors : TRUE;
+ catch (CRM_Core_Exception $e) {
+ $mapperError[] = $e->getMessage();
+ }
+ if (!empty($mapperError)) {
+ return ['_qf_default' => implode('<br/>', $mapperError)];
+ }
+ return TRUE;
}
/**
return $defaults;
}
+ /**
+ * Validate the the mapped fields contain enough to meet the dedupe rule lookup requirements.
+ *
+ * @param array $rule
+ * @param array $mapper
+ *
+ * @return string|false
+ * Error string if insufficient.
+ */
+ protected function validateDedupeFieldsSufficientInMapping(array $rule, array $mapper): ?string {
+ $threshold = $rule['threshold'];
+ $ruleFields = $rule['fields'];
+ $weightSum = 0;
+ foreach ($mapper as $mapping) {
+ if ($mapping[0] === 'external_identifier') {
+ // It is enough to have external identifier mapped.
+ $weightSum = $threshold;
+ break;
+ }
+ if (array_key_exists($mapping[0], $ruleFields)) {
+ $weightSum += $ruleFields[$mapping[0]];
+ }
+ }
+ if ($weightSum < $threshold) {
+ return $rule['rule_message'];
+ }
+ return NULL;
+ }
+
}
use Civi\Api4\Campaign;
use Civi\Api4\Contact;
use Civi\Api4\CustomField;
+use Civi\Api4\DedupeRule;
use Civi\Api4\DedupeRuleGroup;
use Civi\Api4\Event;
use Civi\Api4\UserJob;
*/
private $_fields;
+ private $dedupeRules = [];
+
/**
* Metadata for all available fields, keyed by unique name.
*
* @param string $contactType
*
* @return array[]
- * @throws \CRM_Core_Exception
*/
protected function getContactFields(string $contactType): array {
$contactFields = CRM_Contact_BAO_Contact::importableFields($contactType, NULL);
+ $dedupeFields = $this->getDedupeFields($contactType);
- // Using new Dedupe rule.
- $ruleParams = [
- 'contact_type' => $contactType,
- 'used' => 'Unsupervised',
- ];
- $fieldsArray = CRM_Dedupe_BAO_DedupeRule::dedupeRuleFields($ruleParams);
- $tmpContactField = [];
- if (is_array($fieldsArray)) {
- foreach ($fieldsArray as $value) {
- //skip if there is no dupe rule
- if ($value === 'none') {
- continue;
- }
- $customFieldId = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField',
- $value,
- 'id',
- 'column_name'
- );
- $value = trim($customFieldId ? 'custom_' . $customFieldId : $value);
- $tmpContactField[$value] = $contactFields[$value];
- $title = $tmpContactField[$value]['title'] . ' ' . ts('(match to contact)');
- $tmpContactField[$value]['title'] = $title;
- // When we switch to apiv4 getfields this will already be set for
- // all fields (including custom which it isn't yet)
- $tmpContactField[$value]['entity'] = 'Contact';
+ $contactFieldsForContactLookup = [];
+ foreach ($dedupeFields as $fieldName => $dedupeField) {
+ if (!isset($contactFields[$fieldName])) {
+ continue;
}
+ $contactFieldsForContactLookup[$fieldName] = $contactFields[$fieldName];
+ $contactFieldsForContactLookup[$fieldName]['title'] . ' ' . ts('(match to contact)');
+ $contactFieldsForContactLookup[$fieldName]['entity'] = 'Contact';
}
- $tmpContactField['external_identifier'] = $contactFields['external_identifier'];
- $tmpContactField['external_identifier']['title'] = $contactFields['external_identifier']['title'] . ' ' . ts('(match to contact)');
- return $tmpContactField;
+ $contactFieldsForContactLookup['external_identifier'] = $contactFields['external_identifier'];
+ $contactFieldsForContactLookup['external_identifier']['title'] = $contactFields['external_identifier']['title'] . ' ' . ts('(match to contact)');
+ return $contactFieldsForContactLookup;
}
/**
* @param string $contactType
*
* @return string
+ * @throws \CRM_Core_Exception
*/
protected function getDefaultRuleForContactType(string $contactType): string {
- return $contactType . '.Unsupervised';
+ return DedupeRuleGroup::get(FALSE)
+ ->addWhere('contact_type', '=', $contactType)
+ ->addWhere('used', '=', 'Unsupervised')
+ ->addSelect('id', 'name')->execute()->first()['name'];
}
/**
->execute()->first()['name'];
}
+ /**
+ * Get the dedupe rule, including an array of fields with weights.
+ *
+ * The fields are keyed according to the metadata.
+ *
+ * @param string $contactType
+ * @param string|null $name
+ *
+ * @return array
+ * @noinspection PhpUnhandledExceptionInspection
+ * @noinspection PhpDocMissingThrowsInspection
+ */
+ public function getDedupeRule(string $contactType, ?string $name = NULL): array {
+ if (!$name) {
+ $name = $this->getDefaultRuleForContactType($contactType);
+ }
+ if (empty($this->dedupeRules[$name])) {
+ $this->dedupeRules[$name] = (array) DedupeRuleGroup::get(FALSE)
+ ->addWhere('name', '=', $name)
+ ->addSelect('threshold', 'name', 'id', 'title', 'contact_type')
+ ->execute()->first();
+ $fields = [];
+ $this->dedupeRules[$name]['rule_message'] = $fieldMessage = '';
+ // Now we add the fields in a format like ['first_name' => 6, 'custom_8' => 9]
+ // The number is the weight and we add both api three & four style fields so the
+ // array can be used for converted & unconverted.
+ $ruleFields = DedupeRule::get(FALSE)
+ ->addWhere('dedupe_rule_group_id', '=', $this->dedupeRules[$name]['id'])
+ ->addSelect('id', 'rule_table', 'rule_field', 'rule_weight')->execute();
+ foreach ($ruleFields as $ruleField) {
+ $fieldMessage .= ' ' . $ruleField['rule_field'] . '(weight ' . $ruleField['rule_weight'] . ')';
+ if ($ruleField['rule_table'] === 'civicrm_contact') {
+ $fields[$ruleField['rule_field']] = $ruleField['rule_weight'];
+ }
+ // If not a contact field we add both api variants of fields.
+ elseif ($ruleField['rule_table'] === 'civicrm_phone') {
+ // Actually the dedupe rule for phone should always be phone_numeric. so checking 'phone' is probably unncessary
+ if (in_array($ruleField['rule_field'], ['phone', 'phone_numeric'], TRUE)) {
+ $fields['phone'] = $ruleField['rule_weight'];
+ $fields['phone_primary.phone'] = $ruleField['rule_weight'];
+ }
+ }
+ elseif ($ruleField['rule_field'] === 'email') {
+ $fields['email'] = $ruleField['rule_weight'];
+ $fields['email_primary.email'] = $ruleField['rule_weight'];
+ }
+ elseif ($ruleField['rule_table'] === 'civicrm_address') {
+ $fields[$ruleField['rule_field']] = $ruleField['rule_weight'];
+ $fields['address_primary' . $ruleField['rule_field']] = $ruleField['rule_weight'];
+ }
+ else {
+ // At this point it must be a custom field.
+ $customField = CustomField::get(FALSE)->addWhere('custom_group_id.table_name', '=', $ruleField['rule_table'])
+ ->addWhere('column_name', '=', $ruleField['rule_field'])
+ ->addSelect('id', 'name', 'custom_group_id.name')->execute()->first();
+ $fields['custom_' . $customField['id']] = $ruleField['rule_weight'];
+ $fields[$customField['custom_group_id.name'] . '.' . $customField['name']] = $ruleField['rule_weight'];
+ }
+ }
+ $this->dedupeRules[$name]['rule_message'] = ts('Missing required contact matching fields.') . " $fieldMessage " . ts('(Sum of all weights should be greater than or equal to threshold: %1).', [1 => $this->dedupeRules[$name]['threshold']]) . '<br />';
+
+ $this->dedupeRules[$name]['fields'] = $fields;
+ }
+ return $this->dedupeRules[$name];
+ }
+
/**
* This function adds the contact variable in $values to the
* parameter list $params. For most cases, $values should have length 1. If
*
* @return bool|CRM_Utils_Error
*
+ * @throws \CRM_Core_Exception
* @deprecated
*/
private function _civicrm_api3_deprecated_add_formatted_param(&$values, &$params) {
/**
* Get the field metadata for fields to be be offered to match the contact.
+ * @todo this is very similar to getContactFields - this is called by participant and that
+ * by contribution import. They should be reconciled - but note that one is being fixed
+ * to support api4 style fields on contribution import - with this import to follow.
*
* @return array
* @noinspection PhpDocMissingThrowsInspection
return $contactID;
}
+ /**
+ * Get the fields for the dedupe rule.
+ *
+ * @param string $contactType
+ *
+ * @return array
+ * @throws \CRM_Core_Exception
+ */
+ protected function getDedupeFields(string $contactType): array {
+ return $this->getDedupeRule($contactType)['fields'];
+ }
+
}
$this->runImport($values, CRM_Import_Parser::DUPLICATE_UPDATE);
$contribution = $this->callAPISuccess('Contribution', 'get', ['contact_id' => $contactID, $this->getCustomFieldName('radio') => 'Red Testing']);
$this->assertEquals(5, $contribution['values'][$contribution['id']]['custom_' . $this->ids['CustomField']['radio']]);
- $this->callAPISuccess('CustomField', 'delete', ['id' => $this->ids['CustomField']['radio']]);
- $this->callAPISuccess('CustomGroup', 'delete', ['id' => $this->ids['CustomGroup']['Custom Group']]);
}
/**
]);
$parser = new CRM_Contribute_Import_Parser_Contribution();
$parser->setUserJobID($this->getUserJobID());
- $fields = $parser->getAvailableFields();
+ $fields = $parser->getFieldsMetadata();
$this->assertArrayHasKey('phone', $fields);
$this->callApiSuccess('RuleGroup', 'create', [
'id' => $unsupervisedRuleGroup['id'],
$this->runImport($values, CRM_Import_Parser::DUPLICATE_UPDATE, NULL);
$updatedContribution = $this->callAPISuccessGetSingle('Contribution', ['id' => $initialContribution['id']]);
- $this->assertNotContains('L', $updatedContribution[$customField], "Contribution Duplicate Update Import does not contain L");
- $this->assertContains('V', $updatedContribution[$customField], "Contribution Duplicate Update Import contains V");
+ $this->assertNotContains('L', $updatedContribution[$customField], 'Contribution Duplicate Update Import does not contain L');
+ $this->assertContains('V', $updatedContribution[$customField], 'Contribution Duplicate Update Import contains V');
}
['name' => 'financial_type_id'],
['name' => 'total_amount'],
];
- foreach ($data['fields'] as $field) {
+ foreach ($data['fields'] as $field) {
$mappings[] = ['name' => $field === 'custom' ? $this->getCustomFieldName() : $field];
}
$this->submitDataSourceForm('contributions.csv', ['onDuplicate' => CRM_Import_Parser::DUPLICATE_SKIP]);
* @return array
*/
public function validateData(): array {
- return [
- 'email_first_name_last_name' => [['fields' => ['email', 'first_name', 'last_name'], 'valid' => TRUE]],
- 'email_last_name' => [['fields' => ['email', 'last_name'], 'valid' => TRUE]],
- 'email_first_name' => [['fields' => ['email', 'first_name',], 'valid' => TRUE]],
- 'first_name_last_name' => [['fields' => ['first_name', 'last_name'], 'valid' => TRUE]],
- 'email' => [['fields' => ['email'], 'valid' => TRUE]],
- 'first_name' => [['fields' => ['first_name'], 'valid' => FALSE]],
- 'last_name' => [['fields' => ['last_name'], 'valid' => FALSE]],
- 'last_name_custom' => [['fields' => ['last_name', 'custom'], 'valid' => TRUE]],
- 'first_name_custom' => [['fields' => ['first_name', 'custom'], 'valid' => TRUE]],
- 'custom' => [['fields' => ['first_name', 'custom'], 'valid' => FALSE]],
- ];
+ return [
+ 'email_first_name_last_name' => [['fields' => ['email', 'first_name', 'last_name'], 'valid' => TRUE]],
+ 'email_last_name' => [['fields' => ['email', 'last_name'], 'valid' => TRUE]],
+ 'email_first_name' => [['fields' => ['email', 'first_name'], 'valid' => TRUE]],
+ 'first_name_last_name' => [['fields' => ['first_name', 'last_name'], 'valid' => TRUE]],
+ 'email' => [['fields' => ['email'], 'valid' => TRUE]],
+ 'first_name' => [['fields' => ['first_name'], 'valid' => FALSE]],
+ 'last_name' => [['fields' => ['last_name'], 'valid' => FALSE]],
+ 'last_name_custom' => [['fields' => ['last_name', 'custom'], 'valid' => TRUE]],
+ 'first_name_custom' => [['fields' => ['first_name', 'custom'], 'valid' => TRUE]],
+ 'custom' => [['fields' => ['custom'], 'valid' => FALSE]],
+ ];
}
/**