Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP CRM-21682 automatic membership renewal cleanup #11556

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 59 additions & 71 deletions CRM/Contribute/BAO/Contribution.php
Original file line number Diff line number Diff line change
Expand Up @@ -3930,6 +3930,7 @@ public static function recordAdditionalPayment($contributionId, $trxnsData, $pay
}
}

// update membership details
self::updateMembershipBasedOnCompletionOfContribution(
$contributionDAO,
$contributionId,
Expand Down Expand Up @@ -5406,99 +5407,86 @@ protected static function isPaymentInstrumentChange(&$params, $pendingStatuses)
* @param int $primaryContributionID
* @param string $changeDate
*
* @throws \CRM_Core_Exception
* @throws \CiviCRM_API3_Exception
*/
protected static function updateMembershipBasedOnCompletionOfContribution($contribution, $primaryContributionID, $changeDate) {
$contribution->loadRelatedMembershipObjects();
if (empty($contribution->_relatedObjects['membership'])) {
return;
// Load memberships from contribution info. There may be multiple memberships (or none)
$membershipFields = array('id', 'contact_id', 'is_test', 'membership_type_id', 'contribution_recur_id', 'status_id.is_current_member',
'start_date', 'join_date', 'end_date', 'status_id');
$memberships = array();
if (!empty($contribution->contribution_recur_id)) {
// Load memberships associated with recurring contribution
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this part a change in behaviour?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eileenmcnaughton I believe the expected behaviour was that you could have multiple memberships linked to a single recurring contribution and all of the code except this one point supports that. I believe it actually fixes a bug. It relates directly to the notes on the PR:

Before: When repeattransaction is called ONLY the membership linked to the original contribution will be renewed (if the contribution is in the correct state (ie. Completed - see Before/After)). loadRelatedObjects only loads one membership but the code is expected all memberships to be loaded so does handle multiple memberships if passed in.
After: A recurring contribution can be linked to multiple memberships and all memberships will be updated/renewed if all other conditions are met.

Part of the issue with this area of code is that there is no documentation about what it is actually meant to do! I am still getting reports of auto-renewals failing for some clients in obscure circumstances but I really need this patch reviewing/merging before I can get to the bottom of those issues (as the current core code is littered with inconsistencies including comparisons using getLabel instead of getKey/getName).

$membershipResult = civicrm_api3('Membership', 'get', array(
'contribution_recur_id' => $contribution->contribution_recur_id,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is unlikely to have more than 25 memberships associated with the same contribution, but still would be better to add 'limit' => 0 option here. same for all the API calls below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

'return' => $membershipFields,
'options' => ['limit' => 0],
));
}
$memberships = $contribution->_relatedObjects['membership'];
foreach ($memberships as $membershipTypeIdKey => $membership) {
elseif (!empty($primaryContributionID)) {
// Load membership associated with original contribution
$membershipPaymentResult = civicrm_api3('MembershipPayment', 'get', array(
'contribution_id' => $primaryContributionID,
'options' => ['limit' => 0],
));
if (!empty($membershipPaymentResult['count'])) {
foreach ($membershipPaymentResult['values'] as $payment) {
$membershipIDs[] = $payment['membership_id'];
}
$membershipResult = civicrm_api3('Membership', 'get', array(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm, why not just get the memberships using $primaryContributionID directly instead of fetching the payments IDs from the payments first ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@omarabuhussein We are working with a single contribution at this point. Membership.get requires a recurring contribution ID which we don't have. Am I missing something?

'id' => array('IN' => $membershipIDs),
'return' => $membershipFields,
'options' => ['limit' => 0],
));
}
}
if (isset($membershipResult) && !empty($membershipResult['count'])) {
$memberships = $membershipResult['values'];
}

// Loop through all found memberships and update status/renew
foreach ($memberships as $membershipId => $membership) {
if ($membership) {
if ((!empty($contribution->contribution_recur_id))
&& (!CRM_Member_BAO_Membership::isRecurFrequencyEqualToMembershipType($membership['membership_type_id'], $membership['contribution_recur_id']))) {
Civi::log()->warning('You have enabled auto-renew on membership (id=' . $membership['id'] . ') but the frequencies do not match! The membership will not be auto-renewed.');
continue;
}

$membershipParams = array(
'id' => $membership->id,
'contact_id' => $membership->contact_id,
'is_test' => $membership->is_test,
'membership_type_id' => $membership->membership_type_id,
'id' => $membership['id'],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add space before

$membershipParams = array(..etc 

line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

'contact_id' => $membership['contact_id'],
'is_test' => $membership['is_test'],
'membership_type_id' => $membership['membership_type_id'],
'membership_activity_status' => 'Completed',
);

$currentMembership = CRM_Member_BAO_Membership::getContactMembership($membershipParams['contact_id'],
$membershipParams['membership_type_id'],
$membershipParams['is_test'],
$membershipParams['id']
);

// CRM-8141 update the membership type with the value recorded in log when membership created/renewed
// this picks up membership type changes during renewals
// @todo this is almost certainly an obsolete sql call, the pre-change
// membership is accessible via $this->_relatedObjects
$sql = "
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm, but what if the membership type changed ? , your new code removed this query that handle that but did not offer a replacement

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@omarabuhussein Can you give an example of when this could happen? I thought from the comments that we can just get the pre-change membership directly.

SELECT membership_type_id
FROM civicrm_membership_log
WHERE membership_id={$membershipParams['id']}
ORDER BY id DESC
LIMIT 1;";
$dao = CRM_Core_DAO::executeQuery($sql);
if ($dao->fetch()) {
if (!empty($dao->membership_type_id)) {
$membershipParams['membership_type_id'] = $dao->membership_type_id;
}
}
$dao->free();

$membershipParams['num_terms'] = $contribution->getNumTermsByContributionAndMembershipType(
$membershipParams['membership_type_id'],
$primaryContributionID
);
// @todo remove all this stuff in favour of letting the api call further down handle in
// (it is a duplication of what the api does).
$dates = array_fill_keys(array(
'join_date',
'start_date',
'end_date',
), NULL);
if ($currentMembership) {

// Does the contact have a "current" membership (ie. not expired/pending etc).
if (!empty($membership['status_id.is_current_member'])) {
/*
* Fixed FOR CRM-4433
* In BAO/Membership.php(renewMembership function), we skip the extend membership date and status
* when Contribution mode is notify and membership is for renewal )
* FIXME: Is this still required?
*/
CRM_Member_BAO_Membership::fixMembershipStatusBeforeRenew($currentMembership, $changeDate);

// @todo - we should pass membership_type_id instead of null here but not
// adding as not sure of testing
$dates = CRM_Member_BAO_MembershipType::getRenewalDatesForMembershipType($membershipParams['id'],
$changeDate, NULL, $membershipParams['num_terms']
);
$dates['join_date'] = $currentMembership['join_date'];
CRM_Member_BAO_Membership::fixMembershipStatusBeforeRenew($membership, $changeDate);
}

//get the status for membership.
$calcStatus = CRM_Member_BAO_MembershipStatus::getMembershipStatusByDate($dates['start_date'],
$dates['end_date'],
$dates['join_date'],
'today',
TRUE,
$membershipParams['membership_type_id'],
$membershipParams
);

unset($dates['end_date']);
$membershipParams['status_id'] = CRM_Utils_Array::value('id', $calcStatus, 'New');
//we might be renewing membership,
//so make status override false.
// Tell the Membership BAO to calculate membership status.
$membershipParams['skipStatusCal'] = 0;
$membershipParams['exclude_is_admin'] = TRUE;
$membershipParams['is_override'] = FALSE;
$membershipParams['status_override_end_date'] = 'null';

//CRM-17723 - reset static $relatedContactIds array()
// @todo move it to Civi Statics.
$var = TRUE;
CRM_Member_BAO_Membership::createRelatedMemberships($var, $var, TRUE);
civicrm_api3('Membership', 'create', $membershipParams);
}

//CRM-17723 - reset static $relatedContactIds array()
// @todo move it to Civi Statics.
$var = TRUE;
CRM_Member_BAO_Membership::createRelatedMemberships($var, $var, TRUE);
civicrm_api3('Membership', 'create', $membershipParams);
}
}

Expand Down
34 changes: 30 additions & 4 deletions CRM/Member/BAO/Membership.php
Original file line number Diff line number Diff line change
Expand Up @@ -1164,10 +1164,8 @@ public static function fixMembershipStatusBeforeRenew(&$currentMembership, $chan
$currentMembership
);

if (empty($status) ||
empty($status['id'])
) {
CRM_Core_Error::fatal(ts('Oops, it looks like there is no valid membership status corresponding to the membership start and end dates for this membership. Contact the site administrator for assistance.'));
if (empty($status) || empty($status['id'])) {
throw new CRM_Core_Exception(ts('Oops, it looks like there is no valid membership status corresponding to the membership start and end dates for this membership. Contact the site administrator for assistance.'));
}

$currentMembership['today_date'] = $today;
Expand Down Expand Up @@ -1707,6 +1705,34 @@ public static function isSubscriptionCancelled($mid) {
return FALSE;
}

/**
* Check if the membership type has the same frequency as the recurring contribution
* @param $membershipTypeId
* @param $contributionRecurId
*
* @return bool
*/
public static function isRecurFrequencyEqualToMembershipType($membershipTypeId, $contributionRecurId) {
try {
$membershipType = civicrm_api3('MembershipType', 'getsingle', array(
'return' => array("duration_unit", "duration_interval"),
'id' => $membershipTypeId,
));
$contributionRecur = civicrm_api3('ContributionRecur', 'getsingle', array(
'return' => array("frequency_unit", "frequency_interval"),
'id' => $contributionRecurId,
));
if (($membershipType['duration_unit'] === $contributionRecur['frequency_unit'])
&& ($membershipType['duration_interval'] === $contributionRecur['frequency_interval'])) {
return TRUE;
}
}
catch (CiviCRM_API3_Exception $e) {
return FALSE;
}
return FALSE;
}

/**
* Get membership joins for a specified membership type.
*
Expand Down
6 changes: 3 additions & 3 deletions tests/phpunit/api/v3/ContributionPageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -838,9 +838,9 @@ public function testSubmitMembershipPriceSetPaymentPaymentProcessorRecurInstantP
* - the first creates a new membership, completed contribution, in progress recurring. Check these
* - create another - end date should be extended
*/
//public function testSubmitMembershipPriceSetPaymentPaymentProcessorRecurInstantPaymentDifferentFrequency() {
// $this->doSubmitMembershipPriceSetPaymentPaymentProcessorRecurInstantPayment(array('duration_unit' => 'year', 'recur_frequency_unit' => 'month'));
//}
public function testSubmitMembershipPriceSetPaymentPaymentProcessorRecurInstantPaymentDifferentFrequency() {
$this->doSubmitMembershipPriceSetPaymentPaymentProcessorRecurInstantPayment(array('duration_unit' => 'year', 'recur_frequency_unit' => 'month'));
}

/**
* Helper function for testSubmitMembershipPriceSetPaymentProcessorRecurInstantPayment*
Expand Down