3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
14 * @copyright CiviCRM LLC https://civicrm.org/licensing
17 use Civi\Payment\Exception\PaymentProcessorException
;
20 * This class generates form components generic to recurring contributions.
22 * It delegates the work to lower level subclasses and integrates the changes
23 * back in. It also uses a lot of functionality with the CRM API's, so any change
24 * made here could potentially affect the API etc. Be careful, be aware, use unit tests.
26 class CRM_Contribute_Form_UpdateSubscription
extends CRM_Contribute_Form_ContributionRecur
{
28 protected $_subscriptionDetails = NULL;
30 public $_paymentProcessor = NULL;
32 public $_paymentProcessorObj = NULL;
35 * Fields that affect the schedule and are defined as editable by the processor.
39 protected $editableScheduleFields = [];
42 * The id of the contact associated with this recurring contribution.
49 * Pre-processing for the form.
53 public function preProcess() {
56 $this->setAction(CRM_Core_Action
::UPDATE
);
59 $this->_paymentProcessor
= CRM_Financial_BAO_PaymentProcessor
::getProcessorForEntity($this->_coid
, 'contribute', 'info');
60 // @todo test & replace with $this->_paymentProcessorObj = Civi\Payment\System::singleton()->getById($this->_paymentProcessor['id']);
61 $this->_paymentProcessorObj
= CRM_Financial_BAO_PaymentProcessor
::getProcessorForEntity($this->_coid
, 'contribute', 'obj');
62 $this->contributionRecurID
= $this->_subscriptionDetails
->recur_id
;
64 elseif ($this->contributionRecurID
) {
65 $this->_coid
= CRM_Core_DAO
::getFieldValue('CRM_Contribute_DAO_Contribution', $this->contributionRecurID
, 'id', 'contribution_recur_id');
68 if (!$this->contributionRecurID ||
!$this->_subscriptionDetails
) {
69 CRM_Core_Error
::statusBounce(ts('Required information missing.'));
72 if ($this->_subscriptionDetails
->membership_id
&& $this->_subscriptionDetails
->auto_renew
) {
73 // Add Membership details to form
74 $membership = civicrm_api3('Membership', 'get', [
75 'contribution_recur_id' => $this->contributionRecurID
,
77 if (!empty($membership['count'])) {
78 $membershipDetails = reset($membership['values']);
79 $values['membership_id'] = $membershipDetails['id'];
80 $values['membership_name'] = $membershipDetails['membership_name'];
82 $this->assign('recurMembership', $values);
83 $this->assign('contactId', $this->_subscriptionDetails
->contact_id
);
86 $this->assign('self_service', $this->isSelfService());
87 $this->assign('recur_frequency_interval', $this->_subscriptionDetails
->frequency_interval
);
88 $this->assign('recur_frequency_unit', $this->_subscriptionDetails
->frequency_unit
);
90 $this->editableScheduleFields
= $this->_paymentProcessorObj
->getEditableRecurringScheduleFields();
92 $changeHelpText = $this->_paymentProcessorObj
->getRecurringScheduleUpdateHelpText();
93 if (!in_array('amount', $this->editableScheduleFields
)) {
94 // Not sure if this is good behaviour - maintaining this existing behaviour for now.
95 CRM_Core_Session
::setStatus($changeHelpText, ts('Warning'), 'alert');
98 $this->assign('changeHelpText', $changeHelpText);
100 $alreadyHardCodedFields = ['amount', 'installments'];
101 foreach ($this->editableScheduleFields
as $editableScheduleField) {
102 if (!in_array($editableScheduleField, $alreadyHardCodedFields)) {
103 $this->addField($editableScheduleField, ['entity' => 'ContributionRecur'], FALSE, FALSE);
107 // when custom data is included in this page
108 if (!empty($_POST['hidden_custom']) && !$this->isSelfService()) {
109 CRM_Custom_Form_CustomData
::preProcess($this, NULL, NULL, 1, 'ContributionRecur', $this->contributionRecurID
);
110 CRM_Custom_Form_CustomData
::buildQuickForm($this);
111 CRM_Custom_Form_CustomData
::setDefaultValues($this);
114 $this->assign('editableScheduleFields', array_diff($this->editableScheduleFields
, $alreadyHardCodedFields));
116 if ($this->_subscriptionDetails
->contact_id
) {
117 [$this->_donorDisplayName
, $this->_donorEmail
] = CRM_Contact_BAO_Contact
::getContactDetails($this->_subscriptionDetails
->contact_id
);
120 $this->setTitle(ts('Update Recurring Contribution'));
122 // Handle context redirection.
123 CRM_Contribute_BAO_ContributionRecur
::setSubscriptionContext();
127 * Set default values for the form.
129 * Note that in edit/view mode the default values are retrieved from the database.
131 public function setDefaultValues() {
132 $this->_defaults
= [];
133 $this->_defaults
['amount'] = $this->_subscriptionDetails
->amount
;
134 $this->_defaults
['installments'] = $this->_subscriptionDetails
->installments
;
135 $this->_defaults
['campaign_id'] = $this->_subscriptionDetails
->campaign_id
;
136 $this->_defaults
['financial_type_id'] = $this->_subscriptionDetails
->financial_type_id
;
137 foreach ($this->editableScheduleFields
as $field) {
138 $this->_defaults
[$field] = $this->_subscriptionDetails
->$field ??
NULL;
141 return $this->_defaults
;
145 * Actually build the components of the form.
147 public function buildQuickForm() {
148 // CRM-16398: If current recurring contribution got > 1 lineitems then make amount field readonly
149 $amtAttr = ['size' => 20];
150 $lineItems = CRM_Price_BAO_LineItem
::getLineItemsByContributionID($this->_coid
);
151 if (count($lineItems) > 1) {
152 $amtAttr +
= ['readonly' => TRUE];
154 $amountField = $this->addMoney('amount', ts('Recurring Contribution Amount'), TRUE, $amtAttr,
155 TRUE, 'currency', $this->_subscriptionDetails
->currency
, TRUE
158 // https://lab.civicrm.org/dev/financial/-/issues/197 https://github.com/civicrm/civicrm-core/pull/23796
159 // Revert freezing on total_amount field on recurring form - particularly affects IATs
160 // This will need revisiting in the future as updating amount on recur does not work for multiple lineitems.
161 // Also there are "point of truth" issues ie. is the amount on template contribution or recur the current one?
162 // The amount on the recurring contribution should not be updated directly. If we update the amount using a template contribution the recurring contribution
163 // will be updated automatically.
164 // $paymentProcessorObj = Civi\Payment\System::singleton()->getById(CRM_Contribute_BAO_ContributionRecur::getPaymentProcessorID($this->contributionRecurID));
165 // $templateContribution = CRM_Contribute_BAO_ContributionRecur::getTemplateContribution($this->contributionRecurID);
166 // if (!empty($templateContribution['id']) && $paymentProcessorObj->supportsEditRecurringContribution()) {
167 // $amountField->freeze();
170 $this->add('text', 'installments', ts('Number of Installments'), ['size' => 20], FALSE);
172 if ($this->_donorEmail
) {
173 $this->add('checkbox', 'is_notify', ts('Notify Contributor?'));
176 if (CRM_Core_Permission
::check('edit contributions')) {
177 CRM_Campaign_BAO_Campaign
::addCampaign($this, $this->_subscriptionDetails
->campaign_id
);
180 if (CRM_Contribute_BAO_ContributionRecur
::supportsFinancialTypeChange($this->contributionRecurID
)) {
181 $this->addEntityRef('financial_type_id', ts('Financial Type'), ['entity' => 'FinancialType'], !$this->isSelfService());
185 $this->assign('customDataType', 'ContributionRecur');
186 $this->assign('entityID', $this->contributionRecurID
);
189 if ($this->isSelfService()) {
193 // define the buttons
197 'name' => ts('Save'),
202 'name' => ts('Cancel'),
208 * Called after the user submits the form.
210 * @throws \API_Exception
211 * @throws \CRM_Core_Exception
213 public function postProcess() {
214 // store the submitted values in an array
215 $params = $this->exportValues();
217 if ($this->isSelfService() && $this->_donorEmail
) {
218 // for self service force notify
219 $params['is_notify'] = 1;
222 // if this is an update of an existing recurring contribution, pass the ID
223 $params['contributionRecurID'] = $params['id'] = $this->getContributionRecurID();
226 $params['recurProcessorID'] = $params['subscriptionId'] = $this->getSubscriptionDetails()->processor_id
;
228 $updateSubscription = TRUE;
229 if ($this->_paymentProcessorObj
->supports('changeSubscriptionAmount')) {
231 $updateSubscription = $this->_paymentProcessorObj
->changeSubscriptionAmount($message, $params);
232 if ($updateSubscription instanceof CRM_Core_Error
) {
233 CRM_Core_Error
::deprecatedWarning('An exception should be thrown');
234 throw new PaymentProcessorException(ts('Could not update the Recurring contribution details'));
237 catch (PaymentProcessorException
$e) {
238 CRM_Core_Error
::statusBounce($e->getMessage());
241 if ($updateSubscription) {
242 // Handle custom data
243 $params['custom'] = CRM_Core_BAO_CustomField
::postProcess($params, $this->contributionRecurID
, 'ContributionRecur');
245 CRM_Contribute_BAO_ContributionRecur
::add($params);
246 $status = ts('Recurring contribution has been updated to: %1, every %2 %3(s) for %4 installments.',
248 1 => CRM_Utils_Money
::format($params['amount'], $this->_subscriptionDetails
->currency
),
249 2 => $this->_subscriptionDetails
->frequency_interval
,
250 3 => $this->_subscriptionDetails
->frequency_unit
,
251 4 => $params['installments'],
255 $msgTitle = ts('Update Success');
256 $msgType = 'success';
257 $msg = ts('Recurring Contribution Updated');
258 $contactID = $this->_subscriptionDetails
->contact_id
;
260 if ($this->_subscriptionDetails
->amount
!= $params['amount']) {
261 $message .= "<br /> " . ts("Recurring contribution amount has been updated from %1 to %2 for this subscription.",
263 1 => CRM_Utils_Money
::format($this->_subscriptionDetails
->amount
, $this->_subscriptionDetails
->currency
),
264 2 => CRM_Utils_Money
::format($params['amount'], $this->_subscriptionDetails
->currency
),
266 if ($this->_subscriptionDetails
->amount
< $params['amount']) {
267 $msg = ts('Recurring Contribution Updated - increased installment amount');
270 $msg = ts('Recurring Contribution Updated - decreased installment amount');
274 if ($this->_subscriptionDetails
->installments
!= $params['installments']) {
275 $message .= "<br /> " . ts("Recurring contribution installments have been updated from %1 to %2 for this subscription.", [
276 1 => $this->_subscriptionDetails
->installments
,
277 2 => $params['installments'],
282 'source_contact_id' => $contactID,
283 'activity_type_id' => CRM_Core_PseudoConstant
::getKey('CRM_Activity_BAO_Activity', 'activity_type_id', 'Update Recurring Contribution'),
285 'details' => $message,
286 'activity_date_time' => date('YmdHis'),
287 'status_id' => CRM_Core_PseudoConstant
::getKey('CRM_Activity_BAO_Activity', 'activity_status_id', 'Completed'),
290 $session = CRM_Core_Session
::singleton();
291 $cid = $session->get('userID');
294 $activityParams['target_contact_id'][] = $activityParams['source_contact_id'];
295 $activityParams['source_contact_id'] = $cid;
297 CRM_Activity_BAO_Activity
::create($activityParams);
299 if (!empty($params['is_notify'])) {
300 $receiptFrom = CRM_Contribute_BAO_ContributionRecur
::getRecurFromAddress($this->getContributionRecurID());
302 [$donorDisplayName, $donorEmail] = CRM_Contact_BAO_Contact
::getContactDetails($contactID);
304 $sendTemplateParams = [
305 'groupName' => 'msg_tpl_workflow_contribution',
306 'valueName' => 'contribution_recurring_edit',
307 'contactId' => $contactID,
308 'tplParams' => ['receipt_from_email' => $receiptFrom],
309 'isTest' => $this->_subscriptionDetails
->is_test
,
310 'PDFFilename' => 'receipt.pdf',
311 'from' => $receiptFrom,
312 'toName' => $donorDisplayName,
313 'toEmail' => $donorEmail,
314 'tokenContext' => ['contribution_recurId' => $this->getContributionRecurID()],
316 CRM_Core_BAO_MessageTemplate
::sendTemplate($sendTemplateParams);
320 $session = CRM_Core_Session
::singleton();
321 $userID = $session->get('userID');
322 if ($userID && $status) {
323 CRM_Core_Session
::setStatus($status, $msgTitle, $msgType);
327 CRM_Utils_System
::setUFMessage($status);
329 // keep result as 1, since we not displaying anything on the redirected page anyway
330 CRM_Utils_System
::redirect(CRM_Utils_System
::url('civicrm/contribute/subscriptionstatus',
331 "reset=1&task=update&result=1"));