From 17a6ec9bc2cba807316f348fd3c10bb03753cd43 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] 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 49a4db9d5916..e6534ead4bb7 100644 --- a/CRM/Dedupe/Merger.php +++ b/CRM/Dedupe/Merger.php @@ -313,6 +313,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 7a98cd277150..cec482d3c1db 100644 --- a/CRM/Member/BAO/Membership.php +++ b/CRM/Member/BAO/Membership.php @@ -2532,4 +2532,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["join_date"]) ? $updates["join_date"] : $newMembership->join_date, + isset($updates["start_date"]) ? $updates["start_date"] : $newMembership->start_date, + isset($updates["end_date"]) ? $updates["end_date"] : $newMembership->end_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); + } + }