Merge pull request #6429 from mallezie/crm-16976
[civicrm-core.git] / CRM / Dedupe / Merger.php
index 3f926f75899895248a1d1743322b0a9132e6f792..119164cbbb178b02317546519464ca20059bfadd 100644 (file)
@@ -1,9 +1,9 @@
 <?php
 /*
  +--------------------------------------------------------------------+
- | CiviCRM version 4.5                                                |
+ | CiviCRM version 4.6                                                |
  +--------------------------------------------------------------------+
- | Copyright CiviCRM LLC (c) 2004-2014                                |
+ | Copyright CiviCRM LLC (c) 2004-2015                                |
  +--------------------------------------------------------------------+
  | This file is a part of CiviCRM.                                    |
  |                                                                    |
  | GNU Affero General Public License or the licensing of CiviCRM,     |
  | see the CiviCRM license FAQ at http://civicrm.org/licensing        |
  +--------------------------------------------------------------------+
-*/
+ */
 
 /**
  *
  * @package CRM
- * @copyright CiviCRM LLC (c) 2004-2014
+ * @copyright CiviCRM LLC (c) 2004-2015
  * $Id$
  *
  */
 class CRM_Dedupe_Merger {
 
-  // FIXME: consider creating a common structure with cidRefs() and eidRefs()
-  // FIXME: the sub-pages references by the URLs should
-  // be loaded dynamically on the merge form instead
   /**
+   * FIXME: consider creating a common structure with cidRefs() and eidRefs()
+   * FIXME: the sub-pages references by the URLs should
+   * be loaded dynamically on the merge form instead
    * @return array
    */
-  static function relTables() {
+  public static function relTables() {
     static $relTables;
 
     $config = CRM_Core_Config::singleton();
@@ -156,9 +156,9 @@ class CRM_Dedupe_Merger {
   }
 
   /**
-   * Returns the related tables groups for which a contact has any info entered
+   * Returns the related tables groups for which a contact has any info entered.
    */
-  static function getActiveRelTables($cid) {
+  public static function getActiveRelTables($cid) {
     $cid = (int) $cid;
     $groups = array();
 
@@ -191,7 +191,7 @@ class CRM_Dedupe_Merger {
   /**
    * Return tables and their fields referencing civicrm_contact.contact_id explicitly
    */
-  static function cidRefs() {
+  public static function cidRefs() {
     static $cidRefs;
     if (!$cidRefs) {
       $sql = "
@@ -222,7 +222,7 @@ WHERE
   /**
    * Return tables and their fields referencing civicrm_contact.contact_id with entity_id
    */
-  static function eidRefs() {
+  public static function eidRefs() {
     static $eidRefs;
     if (!$eidRefs) {
       // FIXME: this should be generated dynamically from the schema
@@ -243,12 +243,12 @@ WHERE
   }
 
   /**
-   * Return tables using locations
+   * Return tables using locations.
    */
-  static function locTables() {
+  public static function locTables() {
     static $locTables;
     if (!$locTables) {
-      $locTables = array( 'civicrm_email', 'civicrm_address', 'civicrm_phone' );
+      $locTables = array('civicrm_email', 'civicrm_address', 'civicrm_phone');
 
       // Allow hook_civicrm_merge() to adjust $locTables
       CRM_Utils_Hook::merge('locTables', $locTables);
@@ -258,10 +258,11 @@ WHERE
 
   /**
    * We treat multi-valued custom sets as "related tables" similar to activities, contributions, etc.
-   * @param string $request 'relTables' or 'cidRefs'
+   * @param string $request
+   *   'relTables' or 'cidRefs'.
    * @see CRM-13836
    */
-  static function getMultiValueCustomSets($request) {
+  public static function getMultiValueCustomSets($request) {
     static $data = NULL;
     if ($data === NULL) {
       $data = array(
@@ -273,7 +274,7 @@ WHERE
         'extends' => array('IN' => array('Individual', 'Organization', 'Household', 'Contact')),
         'return' => array('id', 'title', 'table_name', 'style'),
       ));
-      foreach($result['values'] as $custom) {
+      foreach ($result['values'] as $custom) {
         $data['cidRefs'][$custom['table_name']] = array('entity_id');
         $urlSuffix = $custom['style'] == 'Tab' ? '&selectedChild=custom_' . $custom['id'] : '';
         $data['relTables']['rel_table_custom_' . $custom['id']] = array(
@@ -290,7 +291,7 @@ WHERE
    * Tables which require custom processing should declare functions to call here.
    * Doing so will override normal processing.
    */
-  static function cpTables() {
+  public static function cpTables() {
     static $tables;
     if (!$tables) {
       $tables = array(
@@ -305,9 +306,9 @@ WHERE
   }
 
   /**
-   * return payment related table.
+   * Return payment related table.
    */
-  static function paymentTables() {
+  public static function paymentTables() {
     static $tables;
     if (!$tables) {
       $tables = array('civicrm_pledge', 'civicrm_membership', 'civicrm_participant');
@@ -317,9 +318,9 @@ WHERE
   }
 
   /**
-   * return payment update Query.
+   * Return payment update Query.
    */
-  static function paymentSql($tableName, $mainContactId, $otherContactId) {
+  public static function paymentSql($tableName, $mainContactId, $otherContactId) {
     $sqls = array();
     if (!$tableName || !$mainContactId || !$otherContactId) {
       return $sqls;
@@ -363,46 +364,46 @@ INNER JOIN  civicrm_participant participant ON ( participant.id = payment.partic
   }
 
   /**
-   * @param $mainId
-   * @param $otherId
-   * @param $tableName
+   * @param int $mainId
+   * @param int $otherId
+   * @param string $tableName
    * @param array $tableOperations
    * @param string $mode
    *
    * @return array
    */
-  static function operationSql($mainId, $otherId, $tableName, $tableOperations = array(), $mode = 'add') {
+  public static function operationSql($mainId, $otherId, $tableName, $tableOperations = array(), $mode = 'add') {
     $sqls = array();
     if (!$tableName || !$mainId || !$otherId) {
       return $sqls;
     }
 
-
     switch ($tableName) {
       case 'civicrm_membership':
-        if (array_key_exists($tableName, $tableOperations) && $tableOperations[$tableName]['add'])
-        break;
-      if ($mode == 'add') {
-        $sqls[] = "
+        if (array_key_exists($tableName, $tableOperations) && $tableOperations[$tableName]['add']) {
+          break;
+        }
+        if ($mode == 'add') {
+          $sqls[] = "
 DELETE membership1.* FROM civicrm_membership membership1
  INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = membership2.membership_type_id
              AND membership1.contact_id = {$mainId}
              AND membership2.contact_id = {$otherId} ";
-      }
-      if ($mode == 'payment') {
-        $sqls[] = "
+        }
+        if ($mode == 'payment') {
+          $sqls[] = "
 DELETE contribution.* FROM civicrm_contribution contribution
 INNER JOIN  civicrm_membership_payment payment ON payment.contribution_id = contribution.id
 INNER JOIN  civicrm_membership membership1 ON membership1.id = payment.membership_id
             AND membership1.contact_id = {$mainId}
 INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = membership2.membership_type_id
             AND membership2.contact_id = {$otherId}";
-      }
-      break;
+        }
+        break;
 
       case 'civicrm_uf_match':
         // normal queries won't work for uf_match since that will lead to violation of unique constraint,
-        // failing to meet intended result. Therefore we introduce this additonal query:
+        // failing to meet intended result. Therefore we introduce this additional query:
         $sqls[] = "DELETE FROM civicrm_uf_match WHERE contact_id = {$mainId}";
         break;
     }
@@ -414,9 +415,8 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
    * Based on the provided two contact_ids and a set of tables, move the
    * belongings of the other contact to the main one.
    *
-   * @static
    */
-  static function moveContactBelongings($mainId, $otherId, $tables = FALSE, $tableOperations = array()) {
+  public static function moveContactBelongings($mainId, $otherId, $tables = FALSE, $tableOperations = array()) {
     $cidRefs = self::cidRefs();
     $eidRefs = self::eidRefs();
     $cpTables = self::cpTables();
@@ -517,13 +517,14 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
   /**
    * Find differences between contacts.
    *
-   * @param array $main contact details
-   * @param array $other contact details
+   * @param array $main
+   *   Contact details.
+   * @param array $other
+   *   Contact details.
    *
    * @return array
-   * @static
    */
-  static function findDifferences($main, $other) {
+  public static function findDifferences($main, $other) {
     $result = array(
       'contact' => array(),
       'custom' => array(),
@@ -552,25 +553,23 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
   }
 
   /**
-   * Function to batch merge a set of contacts based on rule-group and group.
+   * Batch merge a set of contacts based on rule-group and group.
    *
-   * @param  int $rgid rule group id
-   * @param  int $gid group id
-   * @param  string $mode helps decide how to behave when there are conflicts.
+   * @param int $rgid
+   *   Rule group id.
+   * @param int $gid
+   *   Group id.
+   * @param string $mode
+   *   Helps decide how to behave when there are conflicts.
    *                              A 'safe' value skips the merge if there are any un-resolved conflicts.
    *                              Does a force merge otherwise.
-   * @param  boolean $autoFlip wether to let api decide which contact to retain and which to delete.
-   *
-   *
+   * @param bool $autoFlip to let api decide which contact to retain and which to delete.
+   *   Wether to let api decide which contact to retain and which to delete.
    * @param bool $redirectForPerformance
    *
    * @return array|bool
-   * @internal param array $cacheParams prev-next-cache params based on which next pair of contacts are computed.
-   *                              Generally used with batch-merge.
-   * @static
-   * @access public
    */
-  static function batchMerge($rgid, $gid = NULL, $mode = 'safe', $autoFlip = TRUE, $redirectForPerformance = FALSE) {
+  public static function batchMerge($rgid, $gid = NULL, $mode = 'safe', $autoFlip = TRUE, $redirectForPerformance = FALSE) {
     $contactType = CRM_Core_DAO::getFieldValue('CRM_Dedupe_DAO_RuleGroup', $rgid, 'contact_type');
     $cacheKeyString = "merge {$contactType}";
     $cacheKeyString .= $rgid ? "_{$rgid}" : '_0';
@@ -598,26 +597,27 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
   }
 
   /**
-   * Function to merge given set of contacts. Performs core operation.
+   * Merge given set of contacts. Performs core operation.
    *
-   * @param  array $dupePairs set of pair of contacts for whom merge is to be done.
-   * @param  array $cacheParams prev-next-cache params based on which next pair of contacts are computed.
+   * @param array $dupePairs
+   *   Set of pair of contacts for whom merge is to be done.
+   * @param array $cacheParams
+   *   Prev-next-cache params based on which next pair of contacts are computed.
    *                              Generally used with batch-merge.
-   * @param  string $mode helps decide how to behave when there are conflicts.
+   * @param string $mode
+   *   Helps decide how to behave when there are conflicts.
    *                             A 'safe' value skips the merge if there are any un-resolved conflicts.
    *                             Does a force merge otherwise (aggressive mode).
-   * @param  boolean $autoFlip wether to let api decide which contact to retain and which to delete.
+   * @param bool $autoFlip to let api decide which contact to retain and which to delete.
+   *   Wether to let api decide which contact to retain and which to delete.
    *
    *
    * @param bool $redirectForPerformance
    *
    * @return array|bool
-   * @static
-   * @access public
    */
-  static function merge($dupePairs = array(
-    ), $cacheParams = array(), $mode = 'safe',
-    $autoFlip = TRUE, $redirectForPerformance = FALSE
+  public static function merge($dupePairs = array(), $cacheParams = array(), $mode = 'safe',
+                               $autoFlip = TRUE, $redirectForPerformance = FALSE
   ) {
     $cacheKeyString = CRM_Utils_Array::value('cache_key_string', $cacheParams);
     $resultStats = array('merged' => array(), 'skipped' => array());
@@ -692,18 +692,20 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
    * A function which uses various rules / algorithms for choosing which contact to bias to
    * when there's a conflict (to handle "gotchas"). Plus the safest route to merge.
    *
-   * @param  int $mainId main contact with whom merge has to happen
-   * @param  int $otherId duplicate contact which would be deleted after merge operation
-   * @param  array $migrationInfo array of information about which elements to merge.
-   * @param  string $mode helps decide how to behave when there are conflicts.
+   * @param int $mainId
+   *   Main contact with whom merge has to happen.
+   * @param int $otherId
+   *   Duplicate contact which would be deleted after merge operation.
+   * @param array $migrationInfo
+   *   Array of information about which elements to merge.
+   * @param string $mode
+   *   Helps decide how to behave when there are conflicts.
    *                                 A 'safe' value skips the merge if there are any un-resolved conflicts.
    *                                 Does a force merge otherwise (aggressive mode).
    *
    * @return bool
-   * @static
-   * @access public
    */
-  static function skipMerge($mainId, $otherId, &$migrationInfo, $mode = 'safe') {
+  public static function skipMerge($mainId, $otherId, &$migrationInfo, $mode = 'safe') {
     $conflicts = array();
     $migrationData = array(
       'old_migration_info' => $migrationInfo,
@@ -719,7 +721,8 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
       }
       elseif ((in_array(substr($key, 5), CRM_Dedupe_Merger::getContactFields()) or
           substr($key, 0, 12) == 'move_custom_'
-        ) and $val != NULL) {
+        ) and $val != NULL
+      ) {
         // Rule: if both main-contact has other-contact, let $mode decide if to merge a
         // particular field or not
         if (!empty($migrationInfo['rows'][$key]['main'])) {
@@ -746,7 +749,6 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
           if (!empty($migrationInfo['main_loc_block']) &&
               array_key_exists("main_address{$mainNewLocTypeId}", $migrationInfo['main_loc_block'])) {
             // main loc already has some address for the loc-type. Its a overwrite situation.
-
             // look for next available loc-type
             $newTypeId = NULL;
             foreach ($allLocationTypes as $typeId => $typeLabel) {
@@ -804,19 +806,23 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
   /**
    * A function to build an array of information required by merge function and the merge UI.
    *
-   * @param  int $mainId main contact with whom merge has to happen
-   * @param  int $otherId duplicate contact which would be deleted after merge operation
+   * @param int $mainId
+   *   Main contact with whom merge has to happen.
+   * @param int $otherId
+   *   Duplicate contact which would be deleted after merge operation.
    *
    * @return array|bool|int
-   * @static
-   * @access public
    */
-  static function getRowsElementsAndInfo($mainId, $otherId) {
+  public static function getRowsElementsAndInfo($mainId, $otherId) {
     $qfZeroBug = 'e8cddb72-a257-11dc-b9cc-0016d3330ee9';
 
     // Fetch contacts
     foreach (array('main' => $mainId, 'other' => $otherId) as $moniker => $cid) {
-      $params = array('contact_id' => $cid, 'version' => 3, 'return' => array_merge(array('display_name'), self::getContactFields()));
+      $params = array(
+        'contact_id' => $cid,
+        'version' => 3,
+        'return' => array_merge(array('display_name'), self::getContactFields()),
+      );
       $result = civicrm_api('contact', 'get', $params);
 
       if (empty($result['values'][$cid]['contact_type'])) {
@@ -831,9 +837,6 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
       CRM_Core_DAO::freeResult();
     }
 
-    // get all contact subtypes
-    $contactSubTypes = CRM_Contact_BAO_ContactType::subTypePairs(NULL, TRUE, '');
-
     // FIXME: there must be a better way
     foreach (array('main', 'other') as $moniker) {
       $contact = &$$moniker;
@@ -841,37 +844,22 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
       $value = empty($preferred_communication_method) ? array() : $preferred_communication_method;
       $specialValues[$moniker] = array(
         'preferred_communication_method' => $value,
-        'contact_sub_type' => $value,
         'communication_style_id' => $value,
       );
 
-      if (!empty($contact['preferred_communication_method'])){
-      // api 3 returns pref_comm_method as an array, which breaks the lookup; so we reconstruct
-      $prefCommList = is_array($specialValues[$moniker]['preferred_communication_method']) ?
-        implode(CRM_Core_DAO::VALUE_SEPARATOR, $specialValues[$moniker]['preferred_communication_method']) :
-        $specialValues[$moniker]['preferred_communication_method'];
+      if (!empty($contact['preferred_communication_method'])) {
+        // api 3 returns pref_comm_method as an array, which breaks the lookup; so we reconstruct
+        $prefCommList = is_array($specialValues[$moniker]['preferred_communication_method']) ? implode(CRM_Core_DAO::VALUE_SEPARATOR, $specialValues[$moniker]['preferred_communication_method']) : $specialValues[$moniker]['preferred_communication_method'];
         $specialValues[$moniker]['preferred_communication_method'] = CRM_Core_DAO::VALUE_SEPARATOR . $prefCommList . CRM_Core_DAO::VALUE_SEPARATOR;
       }
       $names = array(
-        'preferred_communication_method' =>
-        array(
+        'preferred_communication_method' => array(
           'newName' => 'preferred_communication_method_display',
           'groupName' => 'preferred_communication_method',
         ),
       );
       CRM_Core_OptionGroup::lookupValues($specialValues[$moniker], $names);
 
-      if (!empty($contact['contact_sub_type'])) {
-        $specialValues[$moniker]['contact_sub_type'] = implode(CRM_Core_DAO::VALUE_SEPARATOR, $contact['contact_sub_type']);
-
-        // fix contact sub type label for contact with sub type
-        $subtypes = array();
-        foreach ($contact['contact_sub_type'] as $key => $value) {
-          $subtypes[] = CRM_Utils_Array::retrieveValueRecursive($contactSubTypes, $value);
-        }
-        $contact['contact_sub_type_display'] = $specialValues[$moniker]['contact_sub_type_display'] = implode(', ', $subtypes);
-      }
-
       if (!empty($contact['communication_style'])) {
         $specialValues[$moniker]['communication_style_id_display'] = $contact['communication_style'];
       }
@@ -889,7 +877,13 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
 
     $rows = $elements = $relTableElements = $migrationInfo = array();
 
+    $genders = CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', 'gender_id');
+
     foreach ($diffs['contact'] as $field) {
+      if ($field == 'contact_sub_type') {
+        // CRM-15681 don't display sub-types in UI
+        continue;
+      }
       foreach (array('main', 'other') as $moniker) {
         $contact = &$$moniker;
         $value = CRM_Utils_Array::value($field, $contact);
@@ -924,6 +918,12 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
           $value = CRM_Utils_Array::value('suffix_id', $contact);
           $field = 'suffix_id';
         }
+        elseif ($field == 'gender_id' && !empty($value)) {
+          $label = $genders[$value];
+        }
+        elseif ($field == 'current_employer_id' && !empty($value)) {
+          $label = "$value (" . CRM_Contact_BAO_Contact::displayName($value) . ")";
+        }
         $rows["move_$field"][$moniker] = $label;
         if ($moniker == 'other') {
           //CRM-14334
@@ -1036,7 +1036,7 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
             array(
               1 => $block,
               2 => $count,
-              3 => $allLocationTypes[$locTypeId]
+              3 => $allLocationTypes[$locTypeId],
             )
           );
 
@@ -1055,8 +1055,11 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
             $js = array('onChange' => "mergeBlock('$name', this, $count );");
           }
           $elements[] = array(
-            'select', "location[{$name}][$count][locTypeId]", NULL,
-            $defaultLocType + $locTypeValues, $js,
+            'select',
+            "location[{$name}][$count][locTypeId]",
+            NULL,
+            $defaultLocType + $locTypeValues,
+            $js,
           );
           // keep location-type-id same as that of other-contact
           $migrationInfo['location'][$name][$count]['locTypeId'] = $locTypeId;
@@ -1170,9 +1173,9 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
               );
               if ($values['data'] === 0 || $values['data'] === '0') {
                 $values['data'] = $qfZeroBug;
-            }
+              }
               $value = ($values['data']) ? $values['data'] : $value;
-          }
+            }
           }
           $rows["move_custom_$fid"]['title'] = $field['label'];
 
@@ -1203,16 +1206,16 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
    * other contact to the main one - be it Location / CustomFields or Contact .. related info.
    * A superset of moveContactBelongings() function.
    *
-   * @param  int $mainId main contact with whom merge has to happen
-   * @param  int $otherId duplicate contact which would be deleted after merge operation
+   * @param int $mainId
+   *   Main contact with whom merge has to happen.
+   * @param int $otherId
+   *   Duplicate contact which would be deleted after merge operation.
    *
    * @param $migrationInfo
    *
    * @return bool
-   * @static
-   * @access public
    */
-  static function moveAllBelongings($mainId, $otherId, $migrationInfo) {
+  public static function moveAllBelongings($mainId, $otherId, $migrationInfo) {
     if (empty($migrationInfo)) {
       return FALSE;
     }
@@ -1225,7 +1228,7 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
         $value = '0';
       }
       if ((in_array(substr($key, 5), CRM_Dedupe_Merger::getContactFields()) ||
-        substr($key, 0, 12) == 'move_custom_') &&
+          substr($key, 0, 12) == 'move_custom_') &&
         $value != NULL
       ) {
         $submitted[substr($key, 5)] = $value;
@@ -1255,7 +1258,6 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
       }
     }
 
-
     // **** Do location related migration:
     if (!empty($locBlocks)) {
       $locComponent = array(
@@ -1291,7 +1293,7 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
           }
 
           if (isset($migrationInfo['main_details']['loc_block_ids'][$name])) {
-          $mainBlockId = CRM_Utils_Array::value($idKey, $migrationInfo['main_details']['loc_block_ids'][$name]);
+            $mainBlockId = CRM_Utils_Array::value($idKey, $migrationInfo['main_details']['loc_block_ids'][$name]);
           }
 
           if (!$otherBlockId) {
@@ -1313,7 +1315,7 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
           }
 
           // overwrite - need to delete block which belongs to main-contact.
-          if ($mainBlockId && ($operation == 2)) {
+          if (isset($mainBlockId) && $mainBlockId && ($operation == 2)) {
             $deleteDAO = new $daoName();
             $deleteDAO->id = $mainBlockId;
             $deleteDAO->find(TRUE);
@@ -1423,10 +1425,13 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
                 //keep state and country as array format.
                 //for checkbox and m-select format w/ VALUE_SEPARATOR
                 if (in_array($htmlType, array(
-                  'CheckBox', 'Multi-Select', 'AdvMulti-Select'))) {
+                  'CheckBox',
+                  'Multi-Select',
+                  'AdvMulti-Select',
+                ))) {
                   $submitted[$key] = CRM_Core_DAO::VALUE_SEPARATOR . implode(CRM_Core_DAO::VALUE_SEPARATOR,
-                    $mergeValue
-                  ) . CRM_Core_DAO::VALUE_SEPARATOR;
+                      $mergeValue
+                    ) . CRM_Core_DAO::VALUE_SEPARATOR;
                 }
                 else {
                   $submitted[$key] = $mergeValue;
@@ -1434,7 +1439,9 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
               }
             }
             elseif (in_array($htmlType, array(
-              'Multi-Select Country', 'Multi-Select State/Province'))) {
+              'Multi-Select Country',
+              'Multi-Select State/Province',
+            ))) {
               //we require submitted values should be in array format
               if ($value) {
                 $mergeValueArray = explode(CRM_Core_DAO::VALUE_SEPARATOR, $value);
@@ -1483,18 +1490,19 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
 
       // move the other contact's file to main contact
       //NYSS need to INSERT or UPDATE depending on whether main contact has an existing record
-      if ( CRM_Core_DAO::singleValueQuery("SELECT id FROM {$tableName} WHERE entity_id = {$mainId}") ) {
-      $sql = "UPDATE {$tableName} SET {$columnName} = {$fileIds[$otherId]} WHERE entity_id = {$mainId}";
+      if (CRM_Core_DAO::singleValueQuery("SELECT id FROM {$tableName} WHERE entity_id = {$mainId}")) {
+        $sql = "UPDATE {$tableName} SET {$columnName} = {$fileIds[$otherId]} WHERE entity_id = {$mainId}";
       }
       else {
         $sql = "INSERT INTO {$tableName} ( entity_id, {$columnName} ) VALUES ( {$mainId}, {$fileIds[$otherId]} )";
       }
       CRM_Core_DAO::executeQuery($sql, CRM_Core_DAO::$_nullArray);
 
-      if ( CRM_Core_DAO::singleValueQuery("
+      if (CRM_Core_DAO::singleValueQuery("
         SELECT id
         FROM civicrm_entity_file
-        WHERE entity_table = '{$tableName}' AND file_id = {$fileIds[$otherId]}") ) {
+        WHERE entity_table = '{$tableName}' AND file_id = {$fileIds[$otherId]}")
+      ) {
         $sql = "
           UPDATE civicrm_entity_file
           SET entity_id = {$mainId}
@@ -1548,6 +1556,15 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
 
     /*         } */
 
+    // CRM-15681 merge sub_types
+    if ($other_sub_types = CRM_Utils_array::value('contact_sub_type', $migrationInfo['other_details'])) {
+      if ($main_sub_types = CRM_Utils_array::value('contact_sub_type', $migrationInfo['main_details'])) {
+        $submitted['contact_sub_type'] = array_unique(array_merge($main_sub_types, $other_sub_types));
+      }
+      else {
+        $submitted['contact_sub_type'] = $other_sub_types;
+      }
+    }
 
     // **** Update contact related info for the main contact
     if (!empty($submitted)) {
@@ -1557,17 +1574,18 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
       if ($currentEmloyerId = CRM_Utils_Array::value('current_employer_id', $submitted)) {
         if (!CRM_Utils_System::isNull($currentEmloyerId)) {
           $submitted['current_employer'] = $submitted['current_employer_id'];
-        } else {
+        }
+        else {
           $submitted['current_employer'] = '';
         }
         unset($submitted['current_employer_id']);
       }
 
       //CRM-14312 include prefix/suffix from mainId if not overridden for proper construction of display/sort name
-      if ( !isset($submitted['prefix_id']) && !empty($migrationInfo['main_details']['prefix_id']) ) {
+      if (!isset($submitted['prefix_id']) && !empty($migrationInfo['main_details']['prefix_id'])) {
         $submitted['prefix_id'] = $migrationInfo['main_details']['prefix_id'];
       }
-      if ( !isset($submitted['suffix_id']) && !empty($migrationInfo['main_details']['suffix_id']) ) {
+      if (!isset($submitted['suffix_id']) && !empty($migrationInfo['main_details']['suffix_id'])) {
         $submitted['suffix_id'] = $migrationInfo['main_details']['suffix_id'];
       }
 
@@ -1575,16 +1593,29 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
       unset($submitted);
     }
 
+    CRM_Utils_Hook::post('merge', 'Contact', $mainId, CRM_Core_DAO::$_nullObject);
+
     return TRUE;
   }
 
   /**
-   * @return array of field names which will be compared, so everything except ID.
+   * @return array
+   *   Array of field names which will be compared, so everything except ID.
    */
-  static function getContactFields() {
+  public static function getContactFields() {
     $contactFields = CRM_Contact_DAO_Contact::fields();
-    $invalidFields = array('api_key', 'contact_is_deleted', 'created_date', 'display_name', 'hash', 'id', 'modified_date',
-      'primary_contact_id', 'sort_name', 'user_unique_id');
+    $invalidFields = array(
+      'api_key',
+      'contact_is_deleted',
+      'created_date',
+      'display_name',
+      'hash',
+      'id',
+      'modified_date',
+      'primary_contact_id',
+      'sort_name',
+      'user_unique_id',
+    );
     foreach ($contactFields as $field => $value) {
       if (in_array($field, $invalidFields)) {
         unset($contactFields[$field]);
@@ -1595,12 +1626,12 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
 
   /**
    * Added for CRM-12695
-   * Based on the contactId provided
+   * Based on the contactID provided
    * add/update membership(s) to related contacts
    *
-   * @param contactId
+   * @param int $contactID
    */
-  static function addMembershipToRealtedContacts($contactID) {
+  public static function addMembershipToRealtedContacts($contactID) {
     $dao = new CRM_Member_DAO_Membership();
     $dao->contact_id = $contactID;
     $dao->is_test = 0;
@@ -1618,11 +1649,12 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
           'start_date' => CRM_Utils_Date::isoToMysql($dao->start_date),
           'end_date' => CRM_Utils_Date::isoToMysql($dao->end_date),
           'source' => $dao->source,
-          'status_id' => $dao->status_id
+          'status_id' => $dao->status_id,
         );
         // create/update membership(s) for related contact(s)
         CRM_Member_BAO_Membership::createRelatedMemberships($membershipParams, $dao);
       } // end of if relationshipTypeId
     }
   }
+
 }