From 7868c13ce19cde352a1437661fc1b74e69b2ef53 Mon Sep 17 00:00:00 2001 From: Jaap Jansma Date: Tue, 19 Jun 2018 17:37:11 +0200 Subject: [PATCH 1/4] #18: added functionality to update membership end date when a membership payment is added. --- .../Form/Setting/MembershipExtension.php | 9 +++- CRM/Membership/PaidByLogic.php | 53 ++++++++++++++++--- membership.php | 4 ++ .../Form/Setting/MembershipExtension.hlp | 6 ++- .../Form/Setting/MembershipExtension.tpl | 5 +- 5 files changed, 65 insertions(+), 12 deletions(-) diff --git a/CRM/Admin/Form/Setting/MembershipExtension.php b/CRM/Admin/Form/Setting/MembershipExtension.php index 34a1bd1..cbbe78e 100644 --- a/CRM/Admin/Form/Setting/MembershipExtension.php +++ b/CRM/Admin/Form/Setting/MembershipExtension.php @@ -109,8 +109,12 @@ public function buildQuickForm( ) { array('multiple' => "multiple", 'class' => 'crm-select2')); $this->addElement('checkbox', - "hide_auto_renewal", - ts("Hide Auto Renewal")); + "update_membership_status", + ts("Update membership status and end date")); + + $this->addElement('checkbox', + "hide_auto_renewal", + ts("Hide Auto Renewal")); $this->addElement('select', "paid_by_field", @@ -161,6 +165,7 @@ function postProcess() { $settings->setSetting('membership_number_show', CRM_Utils_Array::value('membership_number_show', $values), FALSE); $settings->setSetting('hide_auto_renewal', CRM_Utils_Array::value('membership_number_show', $values), FALSE); $settings->setSetting('paid_via_field', $values['paid_via_field'], FALSE); + $settings->setSetting('update_membership_status', $values['update_membership_status'], FALSE); $settings->setSetting('paid_by_field', $values['paid_by_field'], FALSE); if (is_array($values['live_statuses']) && !empty($values['live_statuses'])) { $settings->setSetting('live_statuses', $values['live_statuses'], FALSE); diff --git a/CRM/Membership/PaidByLogic.php b/CRM/Membership/PaidByLogic.php index 23f763a..78b9eed 100644 --- a/CRM/Membership/PaidByLogic.php +++ b/CRM/Membership/PaidByLogic.php @@ -161,14 +161,11 @@ public function assignSepaInstallment($mandate_id, $contribution_recur_id, $cont $paid_via = $settings->getPaidViaField(); if (!$paid_via) return; - // then assign - CRM_Core_DAO::executeQuery(" - INSERT IGNORE INTO civicrm_membership_payment (membership_id, contribution_id) - SELECT - entity_id AS membership_id, - {$contribution_id} AS contribution_id - FROM {$paid_via['table_name']} - WHERE {$paid_via['column_name']} = {$contribution_recur_id};"); + // Create membership payment with the api. So that the pre and post hooks are invoked. + $membership_dao = CRM_Core_DAO::executeQuery("SELECT entity_id AS membership_id FROM {$paid_via['table_name']} WHERE {$paid_via['column_name']} = {$contribution_recur_id}"); + while($membership_dao->fetch()) { + civicrm_api3('MembershipPayment', 'create', array('membership_id' => $membership_dao->membership_id, 'contribution_id' => $contribution_id)); + } } @@ -442,4 +439,44 @@ public function membershipUpdatePOST($membership_id, $object) { } } } + + + /** + * MEMBERSHIP PAYMENT STATUS MONITORING + * + * If a membership payment is added also update the end date of the membership. + * We don't check the status of the contribution as we assume only pending or completed contributions + * will be added to the membership. + * + * @param $contribution_id integer Contribution ID + * @param $membership_id object Contribution BAO object (?) + * @throws Exception only if something's wrong with the pre/post call sequence - shouldn't happen + */ + public function membershipPaymentCreatePOST($contribution_id, $membership_id) { + $settings = CRM_Membership_Settings::getSettings(); + if (!$settings->getSetting('update_membership_status')) { + return; + } + + // Calculate new end date and set this as the new membership end date. + $contribution = civicrm_api3('Contribution', 'getsingle', array('id' => $contribution_id)); + $membership = civicrm_api3('Membership', 'getsingle', array('id' => $membership_id)); + $currentEndDate = new DateTime($membership['end_date']); + $newDates = CRM_Member_BAO_MembershipType::getRenewalDatesForMembershipType($membership_id, $contribution['receive_date']); + $newEndDate = new DateTime($newDates['end_date']); + if ($newEndDate > $currentEndDate) { + $membershipStatus = CRM_Member_BAO_MembershipStatus::getMembershipStatusByDate( + CRM_Utils_Date::customFormat($membership['start_date'], '%Y%m%d'), + $newDates['end_date'], + CRM_Utils_Date::customFormat($membership['join_date'], '%Y%m%d'), + 'today', + FALSE, + $membership['membership_type_id'] + ); + $membershipParams['status_id'] = $membershipStatus['id']; + $membershipParams['id'] = $membership_id; + $membershipParams['end_date'] = $newDates['end_date']; + civicrm_api3('Membership', 'create', $membershipParams); + } + } } \ No newline at end of file diff --git a/membership.php b/membership.php index 6939b30..94039f5 100644 --- a/membership.php +++ b/membership.php @@ -172,6 +172,10 @@ function membership_civicrm_post($op, $objectName, $objectId, &$objectRef) { $logic->membershipUpdatePOST($objectId, $objectRef); } } + if ($objectName == 'MembershipPayment' && $op == 'create') { + $logic = CRM_Membership_PaidByLogic::getSingleton(); + $logic->membershipPaymentCreatePOST($objectRef->contribution_id, $objectRef->membership_id); + } } diff --git a/templates/CRM/Admin/Form/Setting/MembershipExtension.hlp b/templates/CRM/Admin/Form/Setting/MembershipExtension.hlp index 50e8380..f00d6ad 100644 --- a/templates/CRM/Admin/Form/Setting/MembershipExtension.hlp +++ b/templates/CRM/Admin/Form/Setting/MembershipExtension.hlp @@ -38,6 +38,10 @@

{ts}The field has to be an integer type, active, read only, and searchable custom field to be eligible.{/ts}

{/htxt} +{htxt id='update-membership-status'} +

{ts}As soon as the sepa mandate is debitted the contribution is created and added to the membership. If you check this box the end date and the status of the membership will be updated.{/ts}

+{/htxt} + {htxt id='id-paid-via-end-status'}

{ts}For any membership status you select, the linked recurring contribution or mandate will be ended automatically should a membership be set to that status.{/ts}

{/htxt} @@ -70,4 +74,4 @@ {htxt id='id-hide-auto-renewal'}

{ts}The built-in auto renewal feature interferes with the P60 membership concept, so we recommend not to use it. If you activate this option the fields will be hidden from the UI and the column in the tab and the search result will be freed up for something else.{/ts}

-{/htxt} \ No newline at end of file +{/htxt} diff --git a/templates/CRM/Admin/Form/Setting/MembershipExtension.tpl b/templates/CRM/Admin/Form/Setting/MembershipExtension.tpl index 3d3a858..efc5813 100644 --- a/templates/CRM/Admin/Form/Setting/MembershipExtension.tpl +++ b/templates/CRM/Admin/Form/Setting/MembershipExtension.tpl @@ -61,6 +61,10 @@ {$form.paid_via_field.label}  {$form.paid_via_field.html} + + {$form.update_membership_status.label}  + {$form.update_membership_status.html} + {$form.paid_via_end_with_status.label}  {$form.paid_via_end_with_status.html} @@ -69,7 +73,6 @@ {$form.hide_auto_renewal.label}  {$form.hide_auto_renewal.html} - From f36348cb2891e0e69558e363d25e7c3abd636ee2 Mon Sep 17 00:00:00 2001 From: Jaap Jansma Date: Wed, 20 Jun 2018 17:15:38 +0200 Subject: [PATCH 2/4] #18 modified because it should only extend a membership as soon as a contribution is completed. --- .../Form/Setting/MembershipExtension.php | 2 +- CRM/Membership/PaidByLogic.php | 64 ++++++++++++++++++- membership.php | 8 +++ .../Form/Setting/MembershipExtension.hlp | 3 +- .../Form/Setting/MembershipExtension.tpl | 8 +-- 5 files changed, 78 insertions(+), 7 deletions(-) diff --git a/CRM/Admin/Form/Setting/MembershipExtension.php b/CRM/Admin/Form/Setting/MembershipExtension.php index cbbe78e..8f871bf 100644 --- a/CRM/Admin/Form/Setting/MembershipExtension.php +++ b/CRM/Admin/Form/Setting/MembershipExtension.php @@ -110,7 +110,7 @@ public function buildQuickForm( ) { $this->addElement('checkbox', "update_membership_status", - ts("Update membership status and end date")); + ts("Extend membership when contribution is completed")); $this->addElement('checkbox', "hide_auto_renewal", diff --git a/CRM/Membership/PaidByLogic.php b/CRM/Membership/PaidByLogic.php index 78b9eed..5134257 100644 --- a/CRM/Membership/PaidByLogic.php +++ b/CRM/Membership/PaidByLogic.php @@ -31,6 +31,9 @@ class CRM_Membership_PaidByLogic /** stores the pre/post hook records */ protected $monitoring_stack = array(); + /** stores the pre/post hook records for contribution status changed */ + protected $contribution_status_monitoring_stack = array(); + public static function getSingleton() { if (self::$singleton === NULL) { @@ -458,8 +461,13 @@ public function membershipPaymentCreatePOST($contribution_id, $membership_id) { return; } - // Calculate new end date and set this as the new membership end date. + $completed_status = civicrm_api3('OptionValue', 'getvalue', array('name' => 'Completed', 'option_group_id' => 'contribution_status', 'return' => 'value')); $contribution = civicrm_api3('Contribution', 'getsingle', array('id' => $contribution_id)); + if ($contribution['contribution_status_id'] != $completed_status) { + return; // Do not calculate the new end date as the contribution is not yet completed. + } + + // Calculate new end date and set this as the new membership end date. $membership = civicrm_api3('Membership', 'getsingle', array('id' => $membership_id)); $currentEndDate = new DateTime($membership['end_date']); $newDates = CRM_Member_BAO_MembershipType::getRenewalDatesForMembershipType($membership_id, $contribution['receive_date']); @@ -479,4 +487,58 @@ public function membershipPaymentCreatePOST($contribution_id, $membership_id) { civicrm_api3('Membership', 'create', $membershipParams); } } + + /** + * Contribution status monitor function. + * + * Monitor for contribution status changed to completed to update the membership end date. + * + * @param $contribution_id + * @param $params + * @throws Exception only if something's wrong with the pre/post call sequence - shouldn't happen + */ + public function contributionUpdatePRE($contribution_id, $params) { + $settings = CRM_Membership_Settings::getSettings(); + if (!$settings->getSetting('update_membership_status')) { + return; + } + + $completed_status = civicrm_api3('OptionValue', 'getvalue', array('name' => 'Completed', 'option_group_id' => 'contribution_status', 'return' => 'value')); + $contribution = civicrm_api3('Contribution', 'getsingle', array('id' => $contribution_id)); + if ($params['contribution_status_id'] != $completed_status) { + return; + } + if ($params['contribution_status_id'] == $contribution['contribution_status_id']) { + return; + } + + $this->contribution_status_monitoring_stack[$contribution_id] = $contribution; + } + + /** + * Contribution status monitor function. + * + * Monitor for contribution status changed to completed to update the membership end date. + * + * @param $contribution_id + * @param $object + * @throws Exception only if something's wrong with the pre/post call sequence - shouldn't happen + */ + public function contributionUpdatePOST($contribution_id, $object) { + $settings = CRM_Membership_Settings::getSettings(); + if (!$settings->getSetting('update_membership_status')) { + return; + } + + if (!isset($this->contribution_status_monitoring_stack[$contribution_id])) { + return; + } + + $membershipPayments = civicrm_api3('MembershipPayment', 'get', array('contribution_id' => $contribution_id, 'options' => array('limit' => 0))); + foreach($membershipPayments['values'] as $membershipPayment) { + $this->membershipPaymentCreatePOST($contribution_id, $membershipPayment['membership_id']); + } + + unset($this->contribution_status_monitoring_stack[$contribution_id]); + } } \ No newline at end of file diff --git a/membership.php b/membership.php index 94039f5..441e074 100644 --- a/membership.php +++ b/membership.php @@ -156,6 +156,10 @@ function membership_civicrm_pre($op, $objectName, $id, &$params) { $logic->membershipUpdatePre($id, $params); } } + if ($objectName == 'Contribution' && $op == 'edit') { + $logic = CRM_Membership_PaidByLogic::getSingleton(); + $logic->contributionUpdatePRE($id, $params); + } } /** @@ -176,6 +180,10 @@ function membership_civicrm_post($op, $objectName, $objectId, &$objectRef) { $logic = CRM_Membership_PaidByLogic::getSingleton(); $logic->membershipPaymentCreatePOST($objectRef->contribution_id, $objectRef->membership_id); } + if ($objectName == 'Contribution' && $op == 'edit') { + $logic = CRM_Membership_PaidByLogic::getSingleton(); + $logic->contributionUpdatePOST($objectId, $objectRef); + } } diff --git a/templates/CRM/Admin/Form/Setting/MembershipExtension.hlp b/templates/CRM/Admin/Form/Setting/MembershipExtension.hlp index f00d6ad..7b630d6 100644 --- a/templates/CRM/Admin/Form/Setting/MembershipExtension.hlp +++ b/templates/CRM/Admin/Form/Setting/MembershipExtension.hlp @@ -39,7 +39,8 @@ {/htxt} {htxt id='update-membership-status'} -

{ts}As soon as the sepa mandate is debitted the contribution is created and added to the membership. If you check this box the end date and the status of the membership will be updated.{/ts}

+

{ts}Extend the membership as soon as the membership contribution is set to completed.{/ts}

+

{ts}This will calculate the new end date and a new status.{/ts}

{/htxt} {htxt id='id-paid-via-end-status'} diff --git a/templates/CRM/Admin/Form/Setting/MembershipExtension.tpl b/templates/CRM/Admin/Form/Setting/MembershipExtension.tpl index efc5813..38c5062 100644 --- a/templates/CRM/Admin/Form/Setting/MembershipExtension.tpl +++ b/templates/CRM/Admin/Form/Setting/MembershipExtension.tpl @@ -61,10 +61,6 @@ {$form.paid_via_field.label}  {$form.paid_via_field.html} - - {$form.update_membership_status.label}  - {$form.update_membership_status.html} - {$form.paid_via_end_with_status.label}  {$form.paid_via_end_with_status.html} @@ -73,6 +69,10 @@ {$form.hide_auto_renewal.label}  {$form.hide_auto_renewal.html} + + {$form.update_membership_status.label}  + {$form.update_membership_status.html} + From a3c4256de88a83c148872c0596ea6571351282b1 Mon Sep 17 00:00:00 2001 From: Jaap Jansma Date: Thu, 2 Aug 2018 16:38:21 +0200 Subject: [PATCH 3/4] Fixed issue with setting a contribution to completed trhough the ui --- CRM/Membership/PaidByLogic.php | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/CRM/Membership/PaidByLogic.php b/CRM/Membership/PaidByLogic.php index 5134257..fd4bbd2 100644 --- a/CRM/Membership/PaidByLogic.php +++ b/CRM/Membership/PaidByLogic.php @@ -34,6 +34,23 @@ class CRM_Membership_PaidByLogic /** stores the pre/post hook records for contribution status changed */ protected $contribution_status_monitoring_stack = array(); + /** + * Contains a list of memberships which have been renewed by + * the logic in this class. + * + * Why do we need this? If one updates a contribution related to a membership and puts the status + * to completed we want the membership to be renewed. However if one does this through the UI + * civicrm core does handle the renewal, but if the renewal is done through the api (e.g. with civi banking or sepa). then + * the renewal is not handled. + * This class contains functionality to cater for the latter but the side effect is that if you set a membership contribution to completed through + * the ui the membership is renewed twice (e.g. for two periods instead of one). + * So the solution is as soon as this extension renews a membership store the end date in this array and use the membership_pre hook to reset the end date + * to our date. + * + * @var array + */ + protected $renewed_memberships = array(); + public static function getSingleton() { if (self::$singleton === NULL) { @@ -411,10 +428,18 @@ protected function getFinancialType($financial_type_id) * @param $membership_id integer Membership ID * @param $update array Membership update */ - public function membershipUpdatePre($membership_id, $update) { + public function membershipUpdatePre($membership_id, &$update) { // simply store the update params on the stack, will be evaluated in membershipUpdatePOST $update['membership_id'] = $membership_id; // just to be on the save side :) array_push($this->monitoring_stack, $update); + + // Check whether we have need to reset the end date after we have done a renewal. + // Read the explanation at the variable declaration of $renewed_memberships of this class. + if (isset($this->renewed_memberships[$membership_id])) { + if (isset($update['end_date'])) { + $update['end_date'] = $this->renewed_memberships[$membership_id]['end_date']; + } + } } @@ -485,6 +510,7 @@ public function membershipPaymentCreatePOST($contribution_id, $membership_id) { $membershipParams['id'] = $membership_id; $membershipParams['end_date'] = $newDates['end_date']; civicrm_api3('Membership', 'create', $membershipParams); + $this->renewed_memberships[$membership_id] = $membershipParams; } } From cc2cbba69a08504becd03efbcc780c8f96ee8cb3 Mon Sep 17 00:00:00 2001 From: Jaap Jansma Date: Fri, 3 Aug 2018 10:45:35 +0200 Subject: [PATCH 4/4] Fixed status messages. --- CRM/Membership/PaidByLogic.php | 71 ++++++++++++++++++++++++++++++++++ membership.php | 17 ++++++++ 2 files changed, 88 insertions(+) diff --git a/CRM/Membership/PaidByLogic.php b/CRM/Membership/PaidByLogic.php index fd4bbd2..eddb407 100644 --- a/CRM/Membership/PaidByLogic.php +++ b/CRM/Membership/PaidByLogic.php @@ -51,6 +51,23 @@ class CRM_Membership_PaidByLogic */ protected $renewed_memberships = array(); + /** + * Contains a list with strings which should be replaced in the status messages. + * + * Use the function replaceStatusMessages to do the actual replacements. + * The replacement does a search and replace in the status text. + * + * So far we only replace status messages from the postProcess hook and when the form + * is a contribution form. + * + * Every item in the array consists of a subarray with two keys + * - original: the original translated message text + * - new: the new translated message text + * + * @var array + */ + protected $replacementStatusMessages = array(); + public static function getSingleton() { if (self::$singleton === NULL) { @@ -59,6 +76,32 @@ public static function getSingleton() return self::$singleton; } + /** + * Replaces text within a status message + * See also the description at the variable declaration $replacementStatusMessages + */ + public function replaceStatusMessages() { + $session = CRM_Core_Session::singleton(); + // Get the message buffer and clear it. That is ok as we are going to readd the + // messages anyway. + $statusMsgs = $session->getStatus(true); + // Check for replacements and replace the text in the messages. + foreach($this->replacementStatusMessages as $replacement) { + foreach($statusMsgs as $key => $statusMsg) { + if (stripos($statusMsg['text'], $replacement['original'])) { + $statusMsgs[$key]['text'] = str_replace($replacement['original'], $replacement['new'], $statusMsg['text']); + } + } + } + // Readd the messages. + foreach($statusMsgs as $statusMsg) { + if (!is_array($statusMsg['options'])) { + $statusMsg['options'] = array(); + } + CRM_Core_Session::setStatus($statusMsg['text'], $statusMsg['title'], $statusMsg['type'], $statusMsg['options']); + } + } + /** * Change the payment contract for a membership */ @@ -437,6 +480,34 @@ public function membershipUpdatePre($membership_id, &$update) { // Read the explanation at the variable declaration of $renewed_memberships of this class. if (isset($this->renewed_memberships[$membership_id])) { if (isset($update['end_date'])) { + // Create a replament for the status messages. + $formattedOriginalEndDate = CRM_Utils_Date::customFormat($update['end_date'], '%B %E%f, %Y'); + $formattedNewEndDate = CRM_Utils_Date::customFormat($this->renewed_memberships[$membership_id]['end_date'],'%B %E%f, %Y'); + // Retrieve displayNamne + $displayName = CRM_Core_DAO::singleValueQuery(" + SELECT display_name + FROM civicrm_membership + INNER JOIN civicrm_contact ON civicrm_membership.contact_id = civicrm_contact.id + WHERE civicrm_membership.id = %1", + array ( + 1=>array($membership_id, 'Integer') + ) + ); + $replaceStatusMessage['original'] = ts("Membership for %1 has been updated. The membership End Date is %2.", + array( + 1 => $displayName, + 2 => $formattedOriginalEndDate, + ) + ); + $replaceStatusMessage['new'] = ts("Membership for %1 has been updated. The membership End Date is %2.", + array( + 1 => $displayName, + 2 => $formattedNewEndDate, + ) + ); + $this->replacementStatusMessages[] = $replaceStatusMessage; + + // Now correct the end date $update['end_date'] = $this->renewed_memberships[$membership_id]['end_date']; } } diff --git a/membership.php b/membership.php index 441e074..78eb43a 100644 --- a/membership.php +++ b/membership.php @@ -17,6 +17,23 @@ require_once 'membership.civix.php'; use CRM_Membership_ExtensionUtil as E; +/** + * Implements hook_civicrm_postProcess(). + * + * @param string $formName + * @param CRM_Core_Form $form + */ +function membership_civicrm_postProcess($formName, &$form) { + if ($form instanceof CRM_Contribute_Form_Contribution) { + // Reset the status message after our logic has renewed a membership after completing + // a contribution. The civicrm status message from core would + // indicate a wrong end date and that end date is shown to the user + // so that is confusing. + $paidByLogic = CRM_Membership_PaidByLogic::getSingleton(); + $paidByLogic->replaceStatusMessages(); + } +} + /** * Add an action for creating donation receipts after doing a search *