diff --git a/CRM/Contact/Import/Parser/Contact.php b/CRM/Contact/Import/Parser/Contact.php index 4db1a240ecac..c489e1b10991 100644 --- a/CRM/Contact/Import/Parser/Contact.php +++ b/CRM/Contact/Import/Parser/Contact.php @@ -10,6 +10,7 @@ */ use Civi\Api4\Contact; +use Civi\Api4\DedupeRuleGroup; use Civi\Api4\RelationshipType; use Civi\Api4\StateProvince; @@ -1050,7 +1051,7 @@ private function getSuccessMessage(): string { * * @param array $params * @param int|null $extIDMatch - * @param int|null $dedupeRuleID + * @param int|string $dedupeRuleID * * @return int|null * IDs of a possible. @@ -1058,21 +1059,19 @@ private function getSuccessMessage(): string { * @throws \CRM_Core_Exception * @throws \CiviCRM_API3_Exception */ - protected function getPossibleContactMatch(array $params, ?int $extIDMatch, ?int $dedupeRuleID): ?int { - $checkParams = ['check_permissions' => FALSE, 'match' => $params, 'dedupe_rule_id' => $dedupeRuleID]; - $possibleMatches = civicrm_api3('Contact', 'duplicatecheck', $checkParams); + protected function getPossibleContactMatch(array $params, ?int $extIDMatch, $dedupeRuleID): ?int { + $possibleMatches = $this->getPossibleMatchesByDedupeRule($params, $dedupeRuleID); if (!$extIDMatch) { - if (count($possibleMatches['values']) === 1) { - return array_key_last($possibleMatches['values']); + if (count($possibleMatches) === 1) { + return array_key_last($possibleMatches); } - if (count($possibleMatches['values']) > 1) { - throw new CRM_Core_Exception(ts('Record duplicates multiple contacts: ') . implode(',', array_keys($possibleMatches['values'])), CRM_Import_Parser::ERROR); - + if (count($possibleMatches) > 1) { + throw new CRM_Core_Exception(ts('Record duplicates multiple contacts: ') . implode(',', array_keys($possibleMatches)), CRM_Import_Parser::ERROR); } return NULL; } - if ($possibleMatches['count']) { - if (array_key_exists($extIDMatch, $possibleMatches['values'])) { + if (count($possibleMatches) > 0) { + if (array_key_exists($extIDMatch, $possibleMatches)) { return $extIDMatch; } throw new CRM_Core_Exception(ts('Matching this contact based on the de-dupe rule would cause an external ID conflict'), CRM_Import_Parser::ERROR); diff --git a/CRM/Import/Parser.php b/CRM/Import/Parser.php index 70e266ed99a7..3f3a2c36e6ca 100644 --- a/CRM/Import/Parser.php +++ b/CRM/Import/Parser.php @@ -12,6 +12,7 @@ use Civi\Api4\Campaign; use Civi\Api4\Contact; use Civi\Api4\CustomField; +use Civi\Api4\DedupeRuleGroup; use Civi\Api4\Event; use Civi\Api4\UserJob; use Civi\UserJob\UserJobInterface; @@ -947,6 +948,33 @@ protected function checkContactDuplicate(&$formatValues) { return ['is_error' => 0]; } + /** + * Get the default dedupe rule name for the contact type. + * + * @param string $contactType + * + * @return string + */ + protected function getDefaultRuleForContactType(string $contactType): string { + return $contactType . '.Unsupervised'; + } + + /** + * Get the dedupe rule name. + * + * @param int $id + * + * @return string + * + * @throws \CRM_Core_Exception + */ + protected function getDedupeRuleName(int $id): string { + return DedupeRuleGroup::get(FALSE) + ->addWhere('id', '=', $id) + ->addSelect('name') + ->execute()->first()['name']; + } + /** * This function adds the contact variable in $values to the * parameter list $params. For most cases, $values should have length 1. If @@ -1592,7 +1620,7 @@ protected function getContactMatchingFields(): array { * @throws \API_Exception */ protected function getFieldEntity(string $fieldName) { - if ($fieldName === 'do_not_import') { + if ($fieldName === 'do_not_import' || $fieldName === '') { return ''; } if (in_array($fieldName, ['email_greeting_id', 'postal_greeting_id', 'addressee_id'], TRUE)) { @@ -2026,4 +2054,66 @@ protected function lookupExternalIdentifier(?string $externalIdentifier, string return (int) $foundContact['id']; } + /** + * Get contacts that match the input parameters, using a dedupe rule. + * + * @param array $params + * @param int|null $dedupeRuleID + * + * @return array + * + * @throws \CRM_Core_Exception + */ + protected function getPossibleMatchesByDedupeRule(array $params, $dedupeRuleID): 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; + } + unset($params[$locationEntity]); + } + } + 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']); + $possibleMatches = Contact::getDuplicates(FALSE) + ->setValues($params) + ->setDedupeRule($dedupeRule) + ->execute(); + + $matchIDs = []; + foreach ($possibleMatches as $possibleMatch) { + $matchIDs[(int) $possibleMatch['id']] = (int) $possibleMatch['id']; + } + return $matchIDs; + } + + /** + * Get the Api4 name of a custom field. + * + * @param string $key + * + * @throws \CRM_Core_Exception + */ + protected function getApi4Name(string $key): string { + return Contact::getFields(FALSE) + ->addWhere('custom_field_id', '=', $this->getFieldMetadata($key)['custom_field_id']) + ->addSelect('name') + ->execute()->first()['name']; + } + } diff --git a/tests/phpunit/CRM/Contact/Import/Parser/ContactTest.php b/tests/phpunit/CRM/Contact/Import/Parser/ContactTest.php index c2d54066018b..753717e46307 100644 --- a/tests/phpunit/CRM/Contact/Import/Parser/ContactTest.php +++ b/tests/phpunit/CRM/Contact/Import/Parser/ContactTest.php @@ -69,9 +69,7 @@ public function tearDown(): void { /** * Test that import parser will add contact with employee of relationship. * - * @throws \API_Exception * @throws \CRM_Core_Exception - * @throws \CiviCRM_API3_Exception */ public function testImportParserWithEmployeeOfRelationship(): void { $this->organizationCreate([ @@ -561,7 +559,6 @@ public function testContactLocationBlockHandling(): void { $this->callAPISuccessGetCount('IM', ['contact_id' => $id], 1); } - /** * Test whether importing a contact using email match will match a non-primary. * @@ -2277,7 +2274,7 @@ protected function importValues($userJobID, array $values, string $expected): vo $parser->import($values); $dataSource = new CRM_Import_DataSource_SQL($userJobID); $row = $dataSource->getRow(); - $this->assertEquals($expected, $row['_status']); + $this->assertEquals($expected, $row['_status'], print_r($row, TRUE)); } } diff --git a/tests/phpunit/CRMTraits/Import/ParserTrait.php b/tests/phpunit/CRMTraits/Import/ParserTrait.php index 050f625bda45..a21182dc0593 100644 --- a/tests/phpunit/CRMTraits/Import/ParserTrait.php +++ b/tests/phpunit/CRMTraits/Import/ParserTrait.php @@ -74,7 +74,7 @@ protected function importCSV(string $csv, array $fieldMappings, array $submitted 'errorMode' => CRM_Queue_Runner::ERROR_ABORT, ]); $result = $runner->runAll(); - $this->assertEquals(TRUE, $result, $result === TRUE ? '' : $result['exception']->getMessage()); + $this->assertEquals(TRUE, $result, $result === TRUE ? '' : CRM_Core_Error::formatTextException($result['exception'])); } }