From 43d4ad93e20f69869f0ddfdf51d5fa33941a39b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20T=C3=A9tard?= Date: Mon, 20 Nov 2017 15:03:03 +0100 Subject: [PATCH 1/2] Merger: Add some more parameters to custom processing functions. When calling for some custom processing function in the merge process, add tables that needs to be merged and related options. --- CRM/Dedupe/Merger.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CRM/Dedupe/Merger.php b/CRM/Dedupe/Merger.php index 3a6eba7f6d0f..11f1a68384b4 100644 --- a/CRM/Dedupe/Merger.php +++ b/CRM/Dedupe/Merger.php @@ -518,7 +518,7 @@ public static function moveContactBelongings($mainId, $otherId, $tables = FALSE, // Call custom processing function for objects that require it if (isset($cpTables[$table])) { foreach ($cpTables[$table] as $className => $fnName) { - $className::$fnName($mainId, $otherId, $sqls); + $className::$fnName($mainId, $otherId, $sqls, $tables, $tableOperations); } // Skip normal processing continue; From c31e68ae2ff49765af3840b388b76a6be4d40d48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20T=C3=A9tard?= Date: Sat, 18 Nov 2017 18:24:34 +0100 Subject: [PATCH 2/2] CRM-19151: Add the ability to merge memberships without data loss MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new custom processing function `mergeMemberships()` in `CRM_Member_BAO_Membership` that allows to merge memberships. General idea is to merge memberships in regards to their type. We move the other contact’s contributions to the main contact’s membership which has the same type (if any) and then we update membership to avoid loosing `join_date`, `end_date`, etc. Related issue: https://issues.civicrm.org/jira/browse/CRM-19151 --- CRM/Dedupe/Merger.php | 1 + CRM/Member/BAO/Membership.php | 127 ++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/CRM/Dedupe/Merger.php b/CRM/Dedupe/Merger.php index 11f1a68384b4..a2b25399dddd 100644 --- a/CRM/Dedupe/Merger.php +++ b/CRM/Dedupe/Merger.php @@ -322,6 +322,7 @@ public static function cpTables() { // Empty array == do nothing - this table is handled by mergeGroupContact 'civicrm_subscription_history' => array(), 'civicrm_relationship' => array('CRM_Contact_BAO_Relationship' => 'mergeRelationships'), + 'civicrm_membership' => array('CRM_Member_BAO_Membership' => 'mergeMemberships'), ); } return $tables; diff --git a/CRM/Member/BAO/Membership.php b/CRM/Member/BAO/Membership.php index c7dc32629347..d75062a474b6 100644 --- a/CRM/Member/BAO/Membership.php +++ b/CRM/Member/BAO/Membership.php @@ -2570,4 +2570,131 @@ public static function getContactsCancelledMembership($contactID, $isTest = FALS return $cancelledMembershipIds; } + /** + * Merges the memberships from otherContactID to mainContactID. + * + * General idea is to merge memberships in regards to their type. We + * move the other contact’s contributions to the main contact’s + * membership which has the same type (if any) and then we update + * membership to avoid loosing `join_date`, `end_date`, and + * `status_id`. In this function, we don’t touch the contributions + * directly (CRM_Dedupe_Merger::moveContactBelongings() takes care + * of it). + * + * This function adds new SQL queries to the $sqlQueries parameter. + * + * @param int $mainContactID + * Contact id of main contact record. + * @param int $otherContactID + * Contact id of record which is going to merge. + * @param array $sqlQueries + * (reference) array of SQL queries to be executed. + * @param array $tables + * List of tables that have to be merged. + * @param array $tableOperations + * Special options/params for some tables to be merged. + * + * @see CRM_Dedupe_Merger::cpTables() + */ + public static function mergeMemberships($mainContactID, $otherContactID, &$sqlQueries, $tables, $tableOperations) { + /* + * If the user requests not to merge memberships but to add them, + * just attribute the `civicrm_membership` to the + * `$mainContactID`. We have to do this here since the general + * merge process is bypassed by this function. + */ + if (array_key_exists("civicrm_membership", $tableOperations) && $tableOperations['civicrm_membership']['add']) { + $sqlQueries[] = "UPDATE IGNORE civicrm_membership SET contact_id = $mainContactID WHERE contact_id = $otherContactID"; + return; + } + + /* + * Retrieve all memberships that belongs to each contacts and + * keep track of each membership type. + */ + $mainContactMemberships = array(); + $otherContactMemberships = array(); + + $sql = "SELECT id, membership_type_id FROM civicrm_membership membership WHERE contact_id = %1"; + $dao = CRM_Core_DAO::executeQuery($sql, array(1 => array($mainContactID, "Integer"))); + while ($dao->fetch()) { + $mainContactMemberships[$dao->id] = $dao->membership_type_id; + } + + $dao = CRM_Core_DAO::executeQuery($sql, array(1 => array($otherContactID, "Integer"))); + while ($dao->fetch()) { + $otherContactMemberships[$dao->id] = $dao->membership_type_id; + } + + /* + * For each membership, move related contributions to the main + * contact’s membership (by updating `membership_payments`). Then, + * update membership’s `join_date` (if the other membership’s + * join_date is older) and `end_date` (if the other membership’s + * `end_date` is newer) and `status_id` (if the newly calculated + * status is different). + * + * FIXME: what should we do if we have multiple memberships with + * the same type (currently we only take the first one)? + */ + $newSql = array(); + foreach ($otherContactMemberships as $otherMembershipId => $otherMembershipTypeId) { + if ($newMembershipId = array_search($otherMembershipTypeId, $mainContactMemberships)) { + + /* + * Move other membership’s contributions to the main one only + * if user requested to merge contributions. + */ + if (!empty($tables) && in_array('civicrm_contribution', $tables)) { + $newSql[] = "UPDATE civicrm_membership_payment SET membership_id=$newMembershipId WHERE membership_id=$otherMembershipId"; + } + + $sql = "SELECT * FROM civicrm_membership membership WHERE id = %1"; + + $newMembership = CRM_Member_DAO_Membership::findById($newMembershipId); + $otherMembership = CRM_Member_DAO_Membership::findById($otherMembershipId); + + $updates = array(); + if (new DateTime($otherMembership->join_date) < new DateTime($newMembership->join_date)) { + $updates["join_date"] = $otherMembership->join_date; + } + + if (new DateTime($otherMembership->end_date) > new DateTime($newMembership->end_date)) { + $updates["end_date"] = $otherMembership->end_date; + } + + if (count($updates)) { + + /* + * Update status + */ + $status = CRM_Member_BAO_MembershipStatus::getMembershipStatusByDate( + isset($updates["start_date"]) ? $updates["start_date"] : $newMembership->start_date, + isset($updates["end_date"]) ? $updates["end_date"] : $newMembership->end_date, + isset($updates["join_date"]) ? $updates["join_date"] : $newMembership->join_date, + 'today', + FALSE, + $newMembershipId, + $newMembership + ); + + if (!empty($status['id']) and $status['id'] != $newMembership->status_id) { + $updates['status_id'] = $status['id']; + } + + $updates_sql = []; + foreach ($updates as $k => $v) { + $updates_sql[] = "$k = '{$v}'"; + } + + $newSql[] = sprintf("UPDATE civicrm_membership SET %s WHERE id=%s", implode(", ", $updates_sql), $newMembershipId); + $newSql[] = sprintf("DELETE FROM civicrm_membership WHERE id=%s", $otherMembershipId); + } + + } + } + + $sqlQueries = array_merge($sqlQueries, $newSql); + } + }