Skip to content

Commit

Permalink
CRM-19151: Add the ability to merge memberships without data loss
Browse files Browse the repository at this point in the history
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
  • Loading branch information
otetard committed Jan 31, 2018
1 parent 2c106bf commit 17a6ec9
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 0 deletions.
1 change: 1 addition & 0 deletions CRM/Dedupe/Merger.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
127 changes: 127 additions & 0 deletions CRM/Member/BAO/Membership.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

}

0 comments on commit 17a6ec9

Please sign in to comment.