Switch Contact import to use new v4 dedupe lookup
authorEileen McNaughton <emcnaughton@wikimedia.org>
Mon, 29 Aug 2022 03:42:42 +0000 (15:42 +1200)
committerEileen McNaughton <emcnaughton@wikimedia.org>
Mon, 29 Aug 2022 19:32:41 +0000 (07:32 +1200)
CRM/Contact/Import/Parser/Contact.php
CRM/Import/Parser.php
tests/phpunit/CRM/Contact/Import/Parser/ContactTest.php
tests/phpunit/CRMTraits/Import/ParserTrait.php

index 4db1a240ecacd78ab808353c0177e7481e842a36..04c7c91f2fdefd1074c67604d819eea2cf31afca 100644 (file)
@@ -1050,7 +1050,7 @@ class CRM_Contact_Import_Parser_Contact extends CRM_Import_Parser {
    *
    * @param array $params
    * @param int|null $extIDMatch
-   * @param int|null $dedupeRuleID
+   * @param int|string $dedupeRuleID
    *
    * @return int|null
    *   IDs of a possible.
@@ -1058,21 +1058,19 @@ class CRM_Contact_Import_Parser_Contact extends CRM_Import_Parser {
    * @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);
index 70e266ed99a7f2549ec5ea81674bf99bfe850c46..3b91e9dbcedd4f8e72f94083d76441e2afb21884 100644 (file)
@@ -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 @@ abstract class CRM_Import_Parser implements UserJobInterface {
     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 @@ abstract class CRM_Import_Parser implements UserJobInterface {
    * @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 @@ abstract class CRM_Import_Parser implements UserJobInterface {
     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 = 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;
+        }
+        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'];
+  }
+
 }
index c2d54066018b7902a54bbdbb98ed1da98b652021..753717e4630796a853111a34d0aec7629bae6a21 100644 (file)
@@ -69,9 +69,7 @@ class CRM_Contact_Import_Parser_ContactTest extends CiviUnitTestCase {
   /**
    * 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 @@ class CRM_Contact_Import_Parser_ContactTest extends CiviUnitTestCase {
     $this->callAPISuccessGetCount('IM', ['contact_id' => $id], 1);
   }
 
-
   /**
    * Test whether importing a contact using email match will match a non-primary.
    *
@@ -2277,7 +2274,7 @@ class CRM_Contact_Import_Parser_ContactTest extends CiviUnitTestCase {
     $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));
   }
 
 }
index 050f625bda4524f4b884e4cb9e682a05cbfc374e..a21182dc0593f47bd4db94c15ef08452e23ebf31 100644 (file)
@@ -74,7 +74,7 @@ trait CRMTraits_Import_ParserTrait {
         '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']));
     }
   }