From d425cb573ede2cabd077e48bf8fde23e295f585d Mon Sep 17 00:00:00 2001 From: Omar Hussein Date: Fri, 4 Aug 2023 16:03:37 +0100 Subject: [PATCH 1/2] CIWEMB-347: Allow external payment processors extensions to cancel things on their side --- .../Form/RecurringContribution/Cancel.php | 308 ++++++++++-------- .../Hook/Links/RecurringContribution.php | 5 +- .../Form/RecurringContribution/Cancel.tpl | 70 ++-- 3 files changed, 221 insertions(+), 162 deletions(-) diff --git a/CRM/MembershipExtras/Form/RecurringContribution/Cancel.php b/CRM/MembershipExtras/Form/RecurringContribution/Cancel.php index 1463947a..29f5f5c1 100644 --- a/CRM/MembershipExtras/Form/RecurringContribution/Cancel.php +++ b/CRM/MembershipExtras/Form/RecurringContribution/Cancel.php @@ -1,128 +1,180 @@ -id = CRM_Utils_Request::retrieve('crid', 'Positive', $this); - $this->contactID = CRM_Utils_Request::retrieve('cid', 'Positive', $this); - } - - /** - * @inheritdoc - */ - public function buildQuickForm() { - CRM_Utils_System::setTitle(ts('Payment Plan Settings')); - - $this->add( - 'checkbox', - 'cancel_pending_installments', - ts('Do you wish to cancel any pending instalment contribution?'), - '', - FALSE - ); - - $this->add( - 'checkbox', - 'cancel_memberships', - ts('Do you wish to cancel any linked membership?'), - '', - FALSE - ); - - $this->addButtons([ - [ - 'type' => 'submit', - 'name' => ts('Yes'), - 'isDefault' => TRUE, - ], - [ - 'type' => 'cancel', - 'name' => ts('No'), - ], - ]); - } - - /** - * @@inheritdoc - */ - public function postProcess() { - $submittedValues = $this->controller->exportValues($this->_name); - - if ($submittedValues['cancel_memberships']) { - $this->cancelMemberships(); - } - - if ($submittedValues['cancel_pending_installments']) { - $this->cancelPendingInstallments(); - } - - $this->cancelRecurringContribution(); - } - - /** - * Cancels memberships being payed for with the current recurring - * contribution. - */ - private function cancelMemberships() { - civicrm_api3('MembershipPayment', 'get', [ - 'sequential' => 1, - 'contribution_id.contribution_recur_id.id' => $this->id, - 'options' => ['limit' => 0], - 'api.Membership.create' => [ - 'id' => '$value.membership_id', - 'is_override' => 1, - 'status_override_end_date' => '', - 'status_id' => 'Cancelled', - ], - ]); - } - - /** - * Cancels pending contributions associated to current recurring contribution. - */ - private function cancelPendingInstallments() { - civicrm_api3('Contribution', 'get', [ - 'sequential' => 1, - 'contribution_recur_id' => $this->id, - 'contact_id' => $this->contactId, - 'contribution_status_id' => 'Pending', - 'options' => ['limit' => 0], - 'api.Contribution.create' => array( - 'id' => '$value.id', - 'contribution_status_id' => 'Cancelled', - 'cancel_date' => date('Y-m-d H:i:s'), - 'cancel_reason' => 'Cancelled because related recurring contribution was cancelled.', - ), - ]); - } - - /** - * Cancels current recurring contribution. - */ - private function cancelRecurringContribution() { - civicrm_api3('ContributionRecur', 'cancel', array( - 'id' => $this->id, - )); - } - -} +id = CRM_Utils_Request::retrieve('crid', 'Positive', $this); + $this->contactID = CRM_Utils_Request::retrieve('cid', 'Positive', $this); + + $this->recurContribution = \Civi\Api4\ContributionRecur::get() + ->addSelect('payment_processor_id', 'payment_processor_id:name', 'custom.*') + ->addWhere('id', '=', $this->id) + ->addOrderBy('id', 'DESC') + ->execute() + ->first(); + + // These two payment processors are special case given they are both are not external payment processors. + $isOfflinePaymentProcessor = in_array($this->recurContribution['payment_processor_id:name'], ['Offline Recurring Contribution', 'Direct Debit']); + $this->assign('isOfflinePaymentProcessor', $isOfflinePaymentProcessor); + } + + /** + * @inheritdoc + */ + public function buildQuickForm() { + CRM_Utils_System::setTitle(ts('Payment Plan Settings')); + + $this->add( + 'checkbox', + 'cancel_pending_installments', + ts('Do you wish to cancel any pending instalment contribution?'), + '', + FALSE + ); + + $this->add( + 'checkbox', + 'cancel_memberships', + ts('Do you wish to cancel any linked membership?'), + '', + FALSE + ); + + $this->addButtons([ + [ + 'type' => 'submit', + 'name' => ts('Yes'), + 'isDefault' => TRUE, + ], + [ + 'type' => 'cancel', + 'name' => ts('No'), + ], + ]); + } + + /** + * @@inheritdoc + */ + public function postProcess() { + $submittedValues = $this->controller->exportValues($this->_name); + + if (!$this->isOfflinePaymentProcessor) { + $isProcessedExternallySuccessfully = $this->invokePreRecurContributionCancellationHook(); + if ($isProcessedExternallySuccessfully === FALSE) { + CRM_Core_Session::setStatus(ts('An error occurred while trying to cancel this recurring contribution.'), ts('Cancellation Failed'), 'error'); + return; + } + } + + $transaction = new CRM_Core_Transaction(); + try { + if ($submittedValues['cancel_memberships']) { + $this->cancelMemberships(); + } + + if ($submittedValues['cancel_pending_installments']) { + $this->cancelPendingInstallments(); + } + + $this->cancelRecurringContribution(); + + $transaction->commit(); + } + catch (Exception $e) { + $transaction->rollback(); + CRM_Core_Session::setStatus(ts('An error occurred while trying to cancel this recurring contribution: ') . ':' . $e->getMessage(), ts('Cancellation Failed'), 'error'); + } + } + + private function invokePreRecurContributionCancellationHook() { + $nullObject = CRM_Utils_Hook::$_nullObject; + $isProcessedExternallySuccessfully = FALSE; + CRM_Utils_Hook::singleton()->invoke( + ['recurContribution', 'isProcessedExternallySuccessfully'], + $this->recurContribution, $isProcessedExternallySuccessfully, + $nullObject, $nullObject, $nullObject, $nullObject, + 'membershipextras_preRecurContributionCancellation' + ); + + return $isProcessedExternallySuccessfully; + } + + /** + * Cancels memberships being payed for with the current recurring + * contribution. + */ + private function cancelMemberships() { + civicrm_api3('MembershipPayment', 'get', [ + 'sequential' => 1, + 'contribution_id.contribution_recur_id.id' => $this->id, + 'options' => ['limit' => 0], + 'api.Membership.create' => [ + 'id' => '$value.membership_id', + 'is_override' => 1, + 'status_override_end_date' => '', + 'status_id' => 'Cancelled', + ], + ]); + } + + /** + * Cancels pending contributions associated to current recurring contribution. + */ + private function cancelPendingInstallments() { + civicrm_api3('Contribution', 'get', [ + 'sequential' => 1, + 'contribution_recur_id' => $this->id, + 'contact_id' => $this->contactId, + 'contribution_status_id' => 'Pending', + 'options' => ['limit' => 0], + 'api.Contribution.create' => array( + 'id' => '$value.id', + 'contribution_status_id' => 'Cancelled', + 'cancel_date' => date('Y-m-d H:i:s'), + 'cancel_reason' => 'Cancelled because related recurring contribution was cancelled.', + ), + ]); + } + + /** + * Cancels current recurring contribution. + */ + private function cancelRecurringContribution() { + civicrm_api3('ContributionRecur', 'cancel', array( + 'id' => $this->id, + )); + } + +} diff --git a/CRM/MembershipExtras/Hook/Links/RecurringContribution.php b/CRM/MembershipExtras/Hook/Links/RecurringContribution.php index eaf9f4c9..45ba5fd3 100755 --- a/CRM/MembershipExtras/Hook/Links/RecurringContribution.php +++ b/CRM/MembershipExtras/Hook/Links/RecurringContribution.php @@ -72,13 +72,14 @@ private function getRecurringContribution() { */ public function alterLinks() { foreach ($this->links as &$actionLink) { - if ($actionLink['name'] == 'Cancel' && $this->isSupportedPaymentPlan()) { + $isSupportedPaymentPlan = $this->isSupportedPaymentPlan(); + if ($actionLink['name'] == 'Cancel' && $isSupportedPaymentPlan) { unset($actionLink['ref']); $actionLink['url'] = 'civicrm/recurring-contribution/cancel'; $actionLink['qs'] = 'reset=1&crid=%%crid%%&cid=%%cid%%&context=contribution'; } - if ($actionLink['name'] == 'Edit' && $this->isSupportedPaymentPlan()) { + if ($actionLink['name'] == 'Edit' && $isSupportedPaymentPlan) { $this->mask |= CRM_Core_Action::UPDATE; } } diff --git a/templates/CRM/MembershipExtras/Form/RecurringContribution/Cancel.tpl b/templates/CRM/MembershipExtras/Form/RecurringContribution/Cancel.tpl index c2c70e14..fe6995b9 100644 --- a/templates/CRM/MembershipExtras/Form/RecurringContribution/Cancel.tpl +++ b/templates/CRM/MembershipExtras/Form/RecurringContribution/Cancel.tpl @@ -1,32 +1,38 @@ -
-
-
- WARNING - This action sets the CiviCRM recurring contribution status to cancelled, but does NOT send a cancellation request to the payment processor. You will need to ensure that this recurring payment (subscription) is cancelled by the payment processor. -
-

- Are you sure you want to mark this recurring contribution as cancelled? -

- - - - - - - - - - - -
- - - {$form.cancel_pending_installments.html} -
- - - {$form.cancel_memberships.html} -
-
- {include file="CRM/common/formButtons.tpl" location="bottom"} -
-
+
+
+ {if $isOfflinePaymentProcessor} +
+ WARNING - This action sets the CiviCRM recurring contribution status to cancelled, but does NOT send a cancellation request to the payment processor. You will need to ensure that this recurring payment (subscription) is cancelled by the payment processor. +
+ {else} +
+ WARNING: Cancelling the payment plan will also cancel any future payments that have not yet been submitted from being taken by the payment processor. Note, that any payments which have already been submitted will continue to process. +
+ {/if} +

+ Are you sure you want to mark this recurring contribution as cancelled? +

+ + + + + + + + + + + +
+ + + {$form.cancel_pending_installments.html} +
+ + + {$form.cancel_memberships.html} +
+
+ {include file="CRM/common/formButtons.tpl" location="bottom"} +
+ From 9a2a0d1563c22db990496f35b9dec31bbd15d271 Mon Sep 17 00:00:00 2001 From: Omar Hussein Date: Fri, 4 Aug 2023 17:29:44 +0100 Subject: [PATCH 2/2] CIWEMB-347: Don't allow cancelling memberships or contributions for non Membershipextras payment plans --- .../Form/RecurringContribution/Cancel.php | 56 +++++++++++-------- .../Form/RecurringContribution/Cancel.tpl | 2 + 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/CRM/MembershipExtras/Form/RecurringContribution/Cancel.php b/CRM/MembershipExtras/Form/RecurringContribution/Cancel.php index 29f5f5c1..e0332855 100644 --- a/CRM/MembershipExtras/Form/RecurringContribution/Cancel.php +++ b/CRM/MembershipExtras/Form/RecurringContribution/Cancel.php @@ -30,6 +30,11 @@ class CRM_MembershipExtras_Form_RecurringContribution_Cancel extends CRM_Core_Fo */ private $isOfflinePaymentProcessor; + /** + * @var bool + */ + private $isMembershipextrasPaymentPlan; + /** * @inheritdoc */ @@ -45,8 +50,13 @@ public function preProcess() { ->first(); // These two payment processors are special case given they are both are not external payment processors. - $isOfflinePaymentProcessor = in_array($this->recurContribution['payment_processor_id:name'], ['Offline Recurring Contribution', 'Direct Debit']); - $this->assign('isOfflinePaymentProcessor', $isOfflinePaymentProcessor); + $this->isOfflinePaymentProcessor = in_array($this->recurContribution['payment_processor_id:name'], ['Offline Recurring Contribution', 'Direct Debit']); + $this->assign('isOfflinePaymentProcessor', $this->isOfflinePaymentProcessor); + + // If the is_active field is not set, it means this recurring contribution was not created using Membershipextras + // and might have been created using different method such as using Contribution Pages. + $this->isMembershipextrasPaymentPlan = $this->recurContribution['payment_plan_extra_attributes.is_active'] !== NULL; + $this->assign('isMembershipextrasPaymentPlan', $this->isMembershipextrasPaymentPlan); } /** @@ -55,21 +65,23 @@ public function preProcess() { public function buildQuickForm() { CRM_Utils_System::setTitle(ts('Payment Plan Settings')); - $this->add( - 'checkbox', - 'cancel_pending_installments', - ts('Do you wish to cancel any pending instalment contribution?'), - '', - FALSE - ); - - $this->add( - 'checkbox', - 'cancel_memberships', - ts('Do you wish to cancel any linked membership?'), - '', - FALSE - ); + if ($this->isMembershipextrasPaymentPlan) { + $this->add( + 'checkbox', + 'cancel_pending_installments', + ts('Do you wish to cancel any pending instalment contribution?'), + '', + FALSE + ); + + $this->add( + 'checkbox', + 'cancel_memberships', + ts('Do you wish to cancel any linked membership?'), + '', + FALSE + ); + } $this->addButtons([ [ @@ -100,11 +112,11 @@ public function postProcess() { $transaction = new CRM_Core_Transaction(); try { - if ($submittedValues['cancel_memberships']) { + if (!empty($submittedValues['cancel_memberships'])) { $this->cancelMemberships(); } - if ($submittedValues['cancel_pending_installments']) { + if (!empty($submittedValues['cancel_pending_installments'])) { $this->cancelPendingInstallments(); } @@ -122,9 +134,9 @@ private function invokePreRecurContributionCancellationHook() { $nullObject = CRM_Utils_Hook::$_nullObject; $isProcessedExternallySuccessfully = FALSE; CRM_Utils_Hook::singleton()->invoke( - ['recurContribution', 'isProcessedExternallySuccessfully'], - $this->recurContribution, $isProcessedExternallySuccessfully, - $nullObject, $nullObject, $nullObject, $nullObject, + ['recurContribution', 'isMembershipextrasPaymentPlan', 'isProcessedExternallySuccessfully'], + $this->recurContribution, $this->isMembershipextrasPaymentPlan, $isProcessedExternallySuccessfully, + $nullObject, $nullObject, $nullObject, 'membershipextras_preRecurContributionCancellation' ); diff --git a/templates/CRM/MembershipExtras/Form/RecurringContribution/Cancel.tpl b/templates/CRM/MembershipExtras/Form/RecurringContribution/Cancel.tpl index fe6995b9..44d0f54e 100644 --- a/templates/CRM/MembershipExtras/Form/RecurringContribution/Cancel.tpl +++ b/templates/CRM/MembershipExtras/Form/RecurringContribution/Cancel.tpl @@ -12,6 +12,7 @@

Are you sure you want to mark this recurring contribution as cancelled?

+ {if $isMembershipextrasPaymentPlan} @@ -32,6 +33,7 @@
+ {/if}
{include file="CRM/common/formButtons.tpl" location="bottom"}