Merge pull request #19801 from eileenmcnaughton/mem_terms
[civicrm-core.git] / CRM / Member / Form / Membership.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
bc77d7c0 4 | Copyright CiviCRM LLC. All rights reserved. |
6a488035 5 | |
bc77d7c0
TO
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 |
6a488035 9 +--------------------------------------------------------------------+
d25dd0ee 10 */
6a488035
TO
11
12/**
13 *
14 * @package CRM
ca5cec67 15 * @copyright CiviCRM LLC https://civicrm.org/licensing
6a488035
TO
16 */
17
18/**
5e56c7a5 19 * This class generates form components for offline membership form.
6a488035
TO
20 */
21class CRM_Member_Form_Membership extends CRM_Member_Form {
22
23 protected $_memType = NULL;
24
6a488035
TO
25 public $_mode;
26
27 public $_contributeMode = 'direct';
28
29 protected $_recurMembershipTypes;
30
31 protected $_memTypeSelected;
32
d424ffde 33 /**
fe482240 34 * Display name of the member.
d424ffde
CW
35 *
36 * @var string
6a488035 37 */
b11c92be 38 protected $_memberDisplayName = NULL;
6a488035 39
d424ffde
CW
40 /**
41 * email of the person paying for the membership (used for receipts)
971e129b 42 * @var string
d424ffde 43 */
b11c92be 44 protected $_memberEmail = NULL;
6a488035 45
d424ffde 46 /**
fe482240 47 * Contact ID of the member.
d424ffde
CW
48 *
49 * @var int
50 */
cc984198 51 public $_contactID = NULL;
6a488035 52
d424ffde
CW
53 /**
54 * Display name of the person paying for the membership (used for receipts)
55 *
56 * @var string
57 */
b11c92be 58 protected $_contributorDisplayName = NULL;
6a488035 59
d424ffde 60 /**
303007a3 61 * Email of the person paying for the membership (used for receipts).
62 *
971e129b 63 * @var string
b11c92be 64 */
303007a3 65 protected $_contributorEmail;
6a488035 66
d424ffde
CW
67 /**
68 * email of the person paying for the membership (used for receipts)
69 *
70 * @var int
71 */
b11c92be 72 protected $_contributorContactID = NULL;
6a488035 73
d424ffde 74 /**
fe482240 75 * ID of the person the receipt is to go to.
d424ffde
CW
76 *
77 * @var int
b11c92be 78 */
79 protected $_receiptContactId = NULL;
6a488035 80
d424ffde 81 /**
4aa7d844 82 * Keep a class variable for ALL membership IDs so
6a488035 83 * postProcess hook function can do something with it
d424ffde
CW
84 *
85 * @var array
6a488035 86 */
be2fb01f 87 protected $_membershipIDs = [];
6a488035 88
e4f5c851 89 /**
59c798c9 90 * Set entity fields to be assigned to the form.
91 */
92 protected function setEntityFields() {
93 $this->entityFields = [
94 'join_date' => [
95 'name' => 'join_date',
96 'description' => ts('Member Since'),
97 ],
98 'start_date' => [
99 'name' => 'start_date',
100 'description' => ts('Start Date'),
101 ],
102 'end_date' => [
103 'name' => 'end_date',
104 'description' => ts('End Date'),
105 ],
106 ];
107 }
108
109 /**
110 * Set the delete message.
e4f5c851 111 *
59c798c9 112 * We do this from the constructor in order to do a translation.
e4f5c851 113 */
59c798c9 114 public function setDeleteMessage() {
115 $this->deleteMessage = '<span class="font-red bold">'
303007a3 116 . ts('WARNING: Deleting this membership will also delete any related payment (contribution) records.' . ts('This action cannot be undone.')
59c798c9 117 . '</span><p>'
303007a3 118 . ts('Consider modifying the membership status instead if you want to maintain an audit trail and avoid losing payment data. You can set the status to Cancelled by editing the membership and clicking the Status Override checkbox.')
59c798c9 119 . '</p><p>'
120 . ts("Click 'Delete' if you want to continue.") . '</p>');
e4f5c851 121 }
122
59c798c9 123 /**
124 * Overriding this entity trait function as not yet tested.
125 *
126 * We continue to rely on legacy handling.
127 */
128 public function addCustomDataToForm() {}
129
59c798c9 130 /**
131 * Overriding this entity trait function as not yet tested.
132 *
133 * We continue to rely on legacy handling.
134 */
135 public function addFormButtons() {}
136
18aa3e1e 137 /**
4efc56ef
EM
138 * Get selected membership type from the form values.
139 *
e4a6290d 140 * @param array $priceSet
4efc56ef
EM
141 * @param array $params
142 *
143 * @return array
303007a3 144 * @throws \CRM_Core_Exception
18aa3e1e 145 */
e4a6290d 146 public static function getSelectedMemberships($priceSet, $params) {
be2fb01f 147 $memTypeSelected = [];
e4a6290d 148 $priceFieldIDS = self::getPriceFieldIDs($params, $priceSet);
ccb02c2d 149 if (isset($params['membership_type_id']) && !empty($params['membership_type_id'][1])) {
be2fb01f 150 $memTypeSelected = [$params['membership_type_id'][1] => $params['membership_type_id'][1]];
ccb02c2d 151 }
152 else {
18aa3e1e
EM
153 foreach ($priceFieldIDS as $priceFieldId) {
154 if ($id = CRM_Core_DAO::getFieldValue('CRM_Price_DAO_PriceFieldValue', $priceFieldId, 'membership_type_id')) {
155 $memTypeSelected[$id] = $id;
156 }
157 }
158 }
18aa3e1e
EM
159 return $memTypeSelected;
160 }
161
162 /**
163 * Extract price set fields and values from $params.
164 *
5e56c7a5 165 * @param array $params
e4a6290d 166 * @param array $priceSet
5e56c7a5 167 *
18aa3e1e
EM
168 * @return array
169 */
e4a6290d 170 public static function getPriceFieldIDs($params, $priceSet) {
be2fb01f 171 $priceFieldIDS = [];
e4a6290d 172 if (isset($priceSet['fields']) && is_array($priceSet['fields'])) {
15724d0a 173 foreach ($priceSet['fields'] as $fieldId => $field) {
e4a6290d 174 if (!empty($params['price_' . $fieldId])) {
175 if (is_array($params['price_' . $fieldId])) {
176 foreach ($params['price_' . $fieldId] as $priceFldVal => $isSet) {
177 if ($isSet) {
178 $priceFieldIDS[] = $priceFldVal;
179 }
18aa3e1e
EM
180 }
181 }
15724d0a 182 elseif (!$field['is_enter_qty']) {
e4a6290d 183 $priceFieldIDS[] = $params['price_' . $fieldId];
184 }
18aa3e1e
EM
185 }
186 }
187 }
188 return $priceFieldIDS;
189 }
190
5e56c7a5 191 /**
192 * Form preProcess function.
303007a3 193 *
194 * @throws \CRM_Core_Exception
a090fe98 195 * @throws \CiviCRM_API3_Exception
5e56c7a5 196 */
6a488035 197 public function preProcess() {
186a737c
EM
198 // This string makes up part of the class names, differentiating them (not sure why) from the membership fields.
199 $this->assign('formClass', 'membership');
a6513ad5 200 parent::preProcess();
6cc27bfa 201
6a488035 202 // get price set id.
9c1bc317 203 $this->_priceSetId = $_GET['priceSetId'] ?? NULL;
6a488035
TO
204 $this->set('priceSetId', $this->_priceSetId);
205 $this->assign('priceSetId', $this->_priceSetId);
206
6a488035
TO
207 if ($this->_action & CRM_Core_Action::DELETE) {
208 $contributionID = CRM_Member_BAO_Membership::getMembershipContributionId($this->_id);
209 // check delete permission for contribution
210 if ($this->_id && $contributionID && !CRM_Core_Permission::checkActionPermission('CiviContribute', $this->_action)) {
89bfc54a 211 CRM_Core_Error::statusBounce(ts("This Membership is linked to a contribution. You must have 'delete in CiviContribute' permission in order to delete this record."));
6a488035
TO
212 }
213 }
214
6a488035 215 if ($this->_action & CRM_Core_Action::ADD) {
6a488035
TO
216 if ($this->_contactID) {
217 //check whether contact has a current membership so we can alert user that they may want to do a renewal instead
be2fb01f
CW
218 $contactMemberships = [];
219 $memParams = ['contact_id' => $this->_contactID];
4256ea25 220 CRM_Member_BAO_Membership::getValues($memParams, $contactMemberships, TRUE);
be2fb01f 221 $cMemTypes = [];
4256ea25
AH
222 foreach ($contactMemberships as $mem) {
223 $cMemTypes[] = $mem['membership_type_id'];
224 }
225 if (count($cMemTypes) > 0) {
df1ebfea
MW
226 foreach ($cMemTypes as $memTypeID) {
227 $memberorgs[$memTypeID] = CRM_Member_BAO_MembershipType::getMembershipType($memTypeID)['member_of_contact_id'];
228 }
be2fb01f 229 $mems_by_org = [];
2e8b13d1 230 foreach ($contactMemberships as $mem) {
9c1bc317 231 $mem['member_of_contact_id'] = $memberorgs[$mem['membership_type_id']] ?? NULL;
a7488080 232 if (!empty($mem['membership_end_date'])) {
89bfc54a 233 $mem['membership_end_date'] = CRM_Utils_Date::customFormat($mem['membership_end_date']);
4256ea25
AH
234 }
235 $mem['membership_type'] = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_MembershipType',
236 $mem['membership_type_id'],
237 'name', 'id'
6a488035 238 );
4256ea25
AH
239 $mem['membership_status'] = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_MembershipStatus',
240 $mem['status_id'],
241 'label', 'id'
6a488035 242 );
74dd0d90
CW
243 $mem['renewUrl'] = CRM_Utils_System::url('civicrm/contact/view/membership',
244 "reset=1&action=renew&cid={$this->_contactID}&id={$mem['id']}&context=membership&selectedChild=member"
245 . ($this->_mode ? '&mode=live' : '')
246 );
4256ea25
AH
247 $mem['membershipTab'] = CRM_Utils_System::url('civicrm/contact/view',
248 "reset=1&force=1&cid={$this->_contactID}&selectedChild=member"
249 );
250 $mems_by_org[$mem['member_of_contact_id']] = $mem;
6a488035 251 }
74dd0d90 252 $this->assign('existingContactMemberships', $mems_by_org);
6a488035
TO
253 }
254 }
1001e556 255 else {
74dd0d90 256 // In standalone mode we don't have a contact id yet so lookup will be done client-side with this script:
d292601b
AH
257 $resources = CRM_Core_Resources::singleton();
258 $resources->addScriptFile('civicrm', 'templates/CRM/Member/Form/MembershipStandalone.js');
be2fb01f 259 $passthru = [
d292601b 260 'typeorgs' => CRM_Member_BAO_MembershipType::getMembershipTypeOrganization(),
74dd0d90
CW
261 'memtypes' => CRM_Core_PseudoConstant::get('CRM_Member_BAO_Membership', 'membership_type_id'),
262 'statuses' => CRM_Core_PseudoConstant::get('CRM_Member_BAO_Membership', 'status_id'),
be2fb01f
CW
263 ];
264 $resources->addSetting(['existingMems' => $passthru]);
d292601b 265 }
6a488035
TO
266 }
267
97ae4877 268 if (!$this->_memType) {
269 $params = CRM_Utils_Request::exportValues();
06470609 270 if (!empty($params['membership_type_id'][1])) {
71098424 271 $this->_memType = $params['membership_type_id'][1];
272 }
97ae4877 273 }
6452294d
MW
274
275 // Add custom data to form
276 CRM_Custom_Form_CustomData::addToForm($this, $this->_memType);
e2046b33 277 $this->setPageTitle(ts('Membership'));
6a488035
TO
278 }
279
280 /**
5e56c7a5 281 * Set default values for the form.
6a488035
TO
282 */
283 public function setDefaultValues() {
6a488035
TO
284
285 if ($this->_priceSetId) {
9da8dc8c 286 return CRM_Price_BAO_PriceSet::setDefaultPriceSet($this, $defaults);
6a488035
TO
287 }
288
289 $defaults = parent::setDefaultValues();
290
291 //setting default join date and receive date
6a488035 292 if ($this->_action == CRM_Core_Action::ADD) {
42347843 293 $defaults['receive_date'] = CRM_Utils_Time::date('Y-m-d H:i:s');
6a488035
TO
294 }
295
6a488035
TO
296 $defaults['num_terms'] = 1;
297
a7488080 298 if (!empty($defaults['id'])) {
f8cb4d16 299 $contributionId = CRM_Core_DAO::singleValueQuery("
300SELECT contribution_id
301FROM civicrm_membership_payment
302WHERE membership_id = $this->_id
303ORDER BY contribution_id
304DESC limit 1");
305
306 if ($contributionId) {
307 $defaults['record_contribution'] = $contributionId;
6a488035
TO
308 }
309 }
ac0ef1dc 310 else {
311 if ($this->_contactID) {
312 $defaults['contact_id'] = $this->_contactID;
313 }
314 }
133e2c99 315
374a4dd6 316 //set Soft Credit Type to Gift by default
303007a3 317 $scTypes = CRM_Core_OptionGroup::values('soft_credit_type');
374a4dd6 318 $defaults['soft_credit_type_id'] = CRM_Utils_Array::value(ts('Gift'), array_flip($scTypes));
319
d96cf288 320 //CRM-13420
a7488080 321 if (empty($defaults['payment_instrument_id'])) {
d96cf288
DG
322 $defaults['payment_instrument_id'] = key(CRM_Core_OptionGroup::values('payment_instrument', FALSE, FALSE, FALSE, 'AND is_default = 1'));
323 }
6a488035
TO
324
325 // User must explicitly choose to send a receipt in both add and update mode.
326 $defaults['send_receipt'] = 0;
327
328 if ($this->_action & CRM_Core_Action::UPDATE) {
329 // in this mode by default uncheck this checkbox
330 unset($defaults['record_contribution']);
331 }
332
2e8b13d1 333 $subscriptionCancelled = FALSE;
a7488080 334 if (!empty($defaults['id'])) {
6a488035
TO
335 $subscriptionCancelled = CRM_Member_BAO_Membership::isSubscriptionCancelled($this->_id);
336 }
337
338 $alreadyAutoRenew = FALSE;
a7488080 339 if (!empty($defaults['contribution_recur_id']) && !$subscriptionCancelled) {
6a488035
TO
340 $defaults['auto_renew'] = 1;
341 $alreadyAutoRenew = TRUE;
342 }
343 $this->assign('alreadyAutoRenew', $alreadyAutoRenew);
344
15d6c8be 345 $this->assign('member_is_test', $defaults['member_is_test'] ?? NULL);
346 $this->assign('membership_status_id', $defaults['status_id'] ?? NULL);
347 $this->assign('is_pay_later', !empty($defaults['is_pay_later']));
6a488035 348
6a488035 349 if ($this->_mode) {
3b8e6c3f 350 $defaults = $this->getBillingDefaults($defaults);
6a488035
TO
351 }
352
6a488035 353 //setting default join date if there is no join date
a7488080 354 if (empty($defaults['join_date'])) {
42347843 355 $defaults['join_date'] = CRM_Utils_Time::date('Y-m-d');
6a488035
TO
356 }
357
a7488080 358 if (!empty($defaults['membership_end_date'])) {
6a488035
TO
359 $this->assign('endDate', $defaults['membership_end_date']);
360 }
361
362 return $defaults;
363 }
364
365 /**
fe482240 366 * Build the form object.
303007a3 367 *
368 * @throws \CRM_Core_Exception
6a488035
TO
369 */
370 public function buildQuickForm() {
6a488035 371
59c798c9 372 $this->buildQuickEntityForm();
9d3a6f9f 373 $this->assign('currency_symbol', CRM_Core_BAO_Country::defaultCurrencySymbol());
53d0a906 374 $isUpdateToExistingRecurringMembership = $this->isUpdateToExistingRecurringMembership();
6a488035
TO
375 // build price set form.
376 $buildPriceSet = FALSE;
8cc574cf 377 if ($this->_priceSetId || !empty($_POST['price_set_id'])) {
a7488080 378 if (!empty($_POST['price_set_id'])) {
6a488035
TO
379 $buildPriceSet = TRUE;
380 }
381 $getOnlyPriceSetElements = TRUE;
382 if (!$this->_priceSetId) {
383 $this->_priceSetId = $_POST['price_set_id'];
384 $getOnlyPriceSetElements = FALSE;
385 }
386
387 $this->set('priceSetId', $this->_priceSetId);
9da8dc8c 388 CRM_Price_BAO_PriceSet::buildPriceSet($this);
6a488035 389
be2fb01f 390 $optionsMembershipTypes = [];
6a488035
TO
391 foreach ($this->_priceSet['fields'] as $pField) {
392 if (empty($pField['options'])) {
393 continue;
394 }
395 foreach ($pField['options'] as $opId => $opValues) {
396 $optionsMembershipTypes[$opId] = CRM_Utils_Array::value('membership_type_id', $opValues, 0);
397 }
398 }
399
9da8dc8c 400 $this->assign('autoRenewOption', CRM_Price_BAO_PriceSet::checkAutoRenewForPriceSet($this->_priceSetId));
6a488035
TO
401
402 $this->assign('optionsMembershipTypes', $optionsMembershipTypes);
5a9c4d4a 403 $this->assign('contributionType', CRM_Utils_Array::value('financial_type_id', $this->_priceSet));
6a488035
TO
404
405 // get only price set form elements.
406 if ($getOnlyPriceSetElements) {
407 return;
408 }
409 }
410
411 // use to build form during form rule.
412 $this->assign('buildPriceSet', $buildPriceSet);
413
414 if ($this->_action & CRM_Core_Action::ADD) {
415 $buildPriceSet = FALSE;
9da8dc8c 416 $priceSets = CRM_Price_BAO_PriceSet::getAssoc(FALSE, 'CiviMember');
6a488035
TO
417 if (!empty($priceSets)) {
418 $buildPriceSet = TRUE;
419 }
420
421 if ($buildPriceSet) {
422 $this->add('select', 'price_set_id', ts('Choose price set'),
be2fb01f 423 [
21dfd5f5 424 '' => ts('Choose price set'),
be2fb01f
CW
425 ] + $priceSets,
426 NULL, ['onchange' => "buildAmount( this.value );"]
6a488035
TO
427 );
428 }
429 $this->assign('hasPriceSets', $buildPriceSet);
430 }
431
6a488035 432 if ($this->_action & CRM_Core_Action::DELETE) {
be2fb01f
CW
433 $this->addButtons([
434 [
c5c263ca
AH
435 'type' => 'next',
436 'name' => ts('Delete'),
437 'spacing' => '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;',
438 'isDefault' => TRUE,
be2fb01f
CW
439 ],
440 [
c5c263ca
AH
441 'type' => 'cancel',
442 'name' => ts('Cancel'),
be2fb01f
CW
443 ],
444 ]);
6a488035
TO
445 return;
446 }
447
d4ca7732 448 $contactField = $this->addEntityRef('contact_id', ts('Member'), ['create' => TRUE, 'api' => ['extra' => ['email']]], TRUE);
303007a3 449 if ($this->_context !== 'standalone') {
ac0ef1dc 450 $contactField->freeze();
6a488035
TO
451 }
452
453 $selOrgMemType[0][0] = $selMemTypeOrg[0] = ts('- select -');
454
c60d2e2c 455 // Throw status bounce when no Membership type or priceset is present
85e3b778 456 if (empty($this->allMembershipTypeDetails) && empty($priceSets)
7284a1a4 457 ) {
c60d2e2c
PN
458 CRM_Core_Error::statusBounce(ts('You do not have all the permissions needed for this page.'));
459 }
6a488035 460 // retrieve all memberships
be2fb01f 461 $allMembershipInfo = [];
ab30e033
EM
462 foreach ($this->allMembershipTypeDetails as $key => $values) {
463 if ($this->_mode && empty($values['minimum_fee'])) {
464 continue;
465 }
466 else {
9c1bc317 467 $memberOfContactId = $values['member_of_contact_id'] ?? NULL;
ab30e033
EM
468 if (empty($selMemTypeOrg[$memberOfContactId])) {
469 $selMemTypeOrg[$memberOfContactId] = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact',
470 $memberOfContactId,
471 'display_name',
472 'id'
473 );
6a488035 474
ab30e033 475 $selOrgMemType[$memberOfContactId][0] = ts('- select -');
6a488035 476 }
ab30e033 477 if (empty($selOrgMemType[$memberOfContactId][$key])) {
9c1bc317 478 $selOrgMemType[$memberOfContactId][$key] = $values['name'] ?? NULL;
6a488035 479 }
6a488035 480 }
9c1bc317 481 $totalAmount = $values['minimum_fee'] ?? NULL;
ab30e033
EM
482 // build membership info array, which is used when membership type is selected to:
483 // - set the payment information block
484 // - set the max related block
be2fb01f 485 $allMembershipInfo[$key] = [
6b409353 486 'financial_type_id' => $values['financial_type_id'] ?? NULL,
476a47e9 487 'total_amount' => CRM_Utils_Money::formatLocaleNumericRoundedForDefaultCurrency($totalAmount),
decbd3d0 488 'total_amount_numeric' => $totalAmount,
6b409353 489 'auto_renew' => $values['auto_renew'] ?? NULL,
f837be9e 490 'tax_rate' => $values['tax_rate'],
ab30e033 491 'has_related' => isset($values['relationship_type_id']),
6b409353 492 'max_related' => $values['max_related'] ?? NULL,
be2fb01f 493 ];
6a488035
TO
494 }
495
496 $this->assign('allMembershipInfo', json_encode($allMembershipInfo));
497
498 // show organization by default, if only one organization in
499 // the list
500 if (count($selMemTypeOrg) == 2) {
501 unset($selMemTypeOrg[0], $selOrgMemType[0][0]);
502 }
503 //sort membership organization and type, CRM-6099
504 natcasesort($selMemTypeOrg);
505 foreach ($selOrgMemType as $index => $orgMembershipType) {
506 natcasesort($orgMembershipType);
507 $selOrgMemType[$index] = $orgMembershipType;
508 }
509
be2fb01f 510 $memTypeJs = [
ab30e033 511 'onChange' => "buildMaxRelated(this.value,true); CRM.buildCustomData('Membership', this.value);",
be2fb01f 512 ];
8deb20a3 513
ab30e033 514 if (!empty($this->_recurPaymentProcessors)) {
51a6bf4f 515 $memTypeJs['onChange'] = "" . $memTypeJs['onChange'] . " buildAutoRenew(this.value, null, '{$this->_mode}');";
ab30e033 516 }
6a488035 517
6a488035
TO
518 $this->add('text', 'max_related', ts('Max related'),
519 CRM_Core_DAO::getAttribute('CRM_Member_DAO_Membership', 'max_related')
520 );
521
353ffa53 522 $sel = &$this->addElement('hierselect',
6a488035
TO
523 'membership_type_id',
524 ts('Membership Organization and Type'),
525 $memTypeJs
526 );
527
be2fb01f 528 $sel->setOptions([$selMemTypeOrg, $selOrgMemType]);
6a488035 529
6a488035 530 if ($this->_action & CRM_Core_Action::ADD) {
be2fb01f 531 $this->add('number', 'num_terms', ts('Number of Terms'), ['size' => 6]);
6a488035
TO
532 }
533
6a488035
TO
534 $this->add('text', 'source', ts('Source'),
535 CRM_Core_DAO::getAttribute('CRM_Member_DAO_Membership', 'source')
536 );
537
538 //CRM-7362 --add campaigns.
539 $campaignId = NULL;
540 if ($this->_id) {
541 $campaignId = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_Membership', $this->_id, 'campaign_id');
542 }
543 CRM_Campaign_BAO_Campaign::addCampaign($this, $campaignId);
544
545 if (!$this->_mode) {
546 $this->add('select', 'status_id', ts('Membership Status'),
be2fb01f 547 ['' => ts('- select -')] + CRM_Member_PseudoConstant::membershipStatus(NULL, NULL, 'label')
6a488035 548 );
e136f704
O
549
550 $statusOverride = $this->addElement('select', 'is_override', ts('Status Override?'),
551 CRM_Member_StatusOverrideTypes::getSelectOptions()
6a488035 552 );
6a488035 553
42347843 554 $this->add('datepicker', 'status_override_end_date', ts('Status Override End Date'), '', FALSE, ['minDate' => CRM_Utils_Time::date('Y-m-d'), 'time' => FALSE]);
e136f704 555
6a488035
TO
556 $this->addElement('checkbox', 'record_contribution', ts('Record Membership Payment?'));
557
6a488035
TO
558 $this->add('text', 'total_amount', ts('Amount'));
559 $this->addRule('total_amount', ts('Please enter a valid amount.'), 'money');
560
9eb6085d 561 $this->add('datepicker', 'receive_date', ts('Received'), [], FALSE, ['time' => TRUE]);
6a488035
TO
562
563 $this->add('select', 'payment_instrument_id',
4db803dd 564 ts('Payment Method'),
be2fb01f
CW
565 ['' => ts('- select -')] + CRM_Contribute_PseudoConstant::paymentInstrument(),
566 FALSE, ['onChange' => "return showHideByValue('payment_instrument_id','4','checkNumber','table-row','select',false);"]
6a488035
TO
567 );
568 $this->add('text', 'trxn_id', ts('Transaction ID'));
569 $this->addRule('trxn_id', ts('Transaction ID already exists in Database.'),
be2fb01f 570 'objectExists', [
5e56c7a5 571 'CRM_Contribute_DAO_Contribution',
572 $this->_id,
573 'trxn_id',
be2fb01f 574 ]
6a488035
TO
575 );
576
6a488035 577 $this->add('select', 'contribution_status_id',
7fe7b63d 578 ts('Payment Status'), CRM_Contribute_BAO_Contribution_Utils::getContributionStatuses('membership')
6a488035
TO
579 );
580 $this->add('text', 'check_number', ts('Check Number'),
581 CRM_Core_DAO::getAttribute('CRM_Contribute_DAO_Contribution', 'check_number')
582 );
583 }
584 else {
585 //add field for amount to allow an amount to be entered that differs from minimum
586 $this->add('text', 'total_amount', ts('Amount'));
587 }
5a9c4d4a
PN
588 $this->add('select', 'financial_type_id',
589 ts('Financial Type'),
be2fb01f 590 ['' => ts('- select -')] + CRM_Financial_BAO_FinancialType::getAvailableFinancialTypes($financialTypes, $this->_action)
5a9c4d4a 591 );
d80dbc14 592
d80dbc14 593 $this->addElement('checkbox', 'is_different_contribution_contact', ts('Record Payment from a Different Contact?'));
186a737c 594
be2fb01f
CW
595 $this->addSelect('soft_credit_type_id', ['entity' => 'contribution_soft']);
596 $this->addEntityRef('soft_credit_contact_id', ts('Payment From'), ['create' => TRUE]);
d80dbc14 597
6a488035
TO
598 $this->addElement('checkbox',
599 'send_receipt',
600 ts('Send Confirmation and Receipt?'), NULL,
be2fb01f 601 ['onclick' => "showEmailOptions()"]
6a488035
TO
602 );
603
604 $this->add('select', 'from_email_address', ts('Receipt From'), $this->_fromEmails);
605
186a737c 606 $this->add('textarea', 'receipt_text', ts('Receipt Message'));
6a488035
TO
607
608 // Retrieve the name and email of the contact - this will be the TO for receipt email
609 if ($this->_contactID) {
ebf7e65f 610 [$this->_memberDisplayName, $this->_memberEmail] = CRM_Contact_BAO_Contact_Location::getEmailDetails($this->_contactID);
6a488035
TO
611
612 $this->assign('emailExists', $this->_memberEmail);
613 $this->assign('displayName', $this->_memberDisplayName);
614 }
615
50b85bf9 616 if ($isUpdateToExistingRecurringMembership && CRM_Member_BAO_Membership::isCancelSubscriptionSupported($this->_id)) {
617 $this->assign('cancelAutoRenew',
618 CRM_Utils_System::url('civicrm/contribute/unsubscribe', "reset=1&mid={$this->_id}")
619 );
620 }
53d0a906 621
50b85bf9 622 $this->assign('isRecur', $isUpdateToExistingRecurringMembership);
6a488035 623
be2fb01f 624 $this->addFormRule(['CRM_Member_Form_Membership', 'formRule'], $this);
8c80f3f9 625 $mailingInfo = Civi::settings()->get('mailing_backend');
626 $this->assign('isEmailEnabledForSite', ($mailingInfo['outBound_option'] != 2));
6a488035
TO
627
628 parent::buildQuickForm();
629 }
630
631 /**
fe482240 632 * Validation.
6a488035 633 *
b2363ea8
TO
634 * @param array $params
635 * (ref.) an assoc array of name/value pairs.
6a488035 636 *
5e56c7a5 637 * @param array $files
638 * @param CRM_Member_Form_Membership $self
2a6da8d7 639 *
72b3a70c
CW
640 * @return bool|array
641 * mixed true or array of errors
a090fe98 642 *
643 * @throws \CRM_Core_Exception
644 * @throws CiviCRM_API3_Exception
6a488035 645 */
00be9182 646 public static function formRule($params, $files, $self) {
be2fb01f 647 $errors = [];
6a488035 648
cd595bef 649 $priceSetId = $self->getPriceSetID($params);
650 $priceSetDetails = $self->getPriceSetDetails($params);
6a488035 651
e4a6290d 652 $selectedMemberships = self::getSelectedMemberships($priceSetDetails[$priceSetId], $params);
ccb02c2d 653
654 if (!empty($params['price_set_id'])) {
9da8dc8c 655 CRM_Price_BAO_PriceField::priceSetValidation($priceSetId, $params, $errors);
6a488035 656
15724d0a 657 $priceFieldIDS = self::getPriceFieldIDs($params, $priceSetDetails[$priceSetId]);
6a488035
TO
658
659 if (!empty($priceFieldIDS)) {
660 $ids = implode(',', $priceFieldIDS);
661
9da8dc8c 662 $count = CRM_Price_BAO_PriceSet::getMembershipCount($ids);
2e8b13d1 663 foreach ($count as $occurrence) {
b44e3f84 664 if ($occurrence > 1) {
6a488035
TO
665 $errors['_qf_default'] = ts('Select at most one option associated with the same membership type.');
666 }
667 }
6a488035 668 }
867047cd 669 // Return error if empty $self->_memTypeSelected
670 if (empty($errors) && empty($selectedMemberships)) {
671 $errors['_qf_default'] = ts('Select at least one membership option.');
672 }
673 if (!$self->_mode && empty($params['record_contribution'])) {
674 $errors['record_contribution'] = ts('Record Membership Payment is required when you use a price set.');
675 }
6a488035 676 }
867047cd 677 else {
678 if (empty($params['membership_type_id'][1])) {
679 $errors['membership_type_id'] = ts('Please select a membership type.');
680 }
9c1bc317 681 $numterms = $params['num_terms'] ?? NULL;
6a488035
TO
682 if ($numterms && intval($numterms) != $numterms) {
683 $errors['num_terms'] = ts('Please enter an integer for the number of terms.');
684 }
6a488035 685
356b5786 686 if (($self->_mode || isset($params['record_contribution'])) && empty($params['financial_type_id'])) {
867047cd 687 $errors['financial_type_id'] = ts('Please enter the financial Type.');
688 }
6a488035
TO
689 }
690
18aa3e1e
EM
691 if (!empty($errors) && (count($selectedMemberships) > 1)) {
692 $memberOfContacts = CRM_Member_BAO_MembershipType::getMemberOfContactByMemTypes($selectedMemberships);
6a488035
TO
693 $duplicateMemberOfContacts = array_count_values($memberOfContacts);
694 foreach ($duplicateMemberOfContacts as $countDuplicate) {
695 if ($countDuplicate > 1) {
696 $errors['_qf_default'] = ts('Please do not select more than one membership associated with the same organization.');
697 }
698 }
699 }
700
6a488035
TO
701 if (!empty($errors)) {
702 return $errors;
703 }
704
8cc574cf 705 if (!empty($params['record_contribution']) && empty($params['payment_instrument_id'])) {
4db803dd 706 $errors['payment_instrument_id'] = ts('Payment Method is a required field.');
d96cf288 707 }
6a488035 708
a7488080
CW
709 if (!empty($params['is_different_contribution_contact'])) {
710 if (empty($params['soft_credit_type_id'])) {
133e2c99 711 $errors['soft_credit_type_id'] = ts('Please Select a Soft Credit Type');
712 }
d80dbc14 713 if (empty($params['soft_credit_contact_id'])) {
714 $errors['soft_credit_contact_id'] = ts('Please select a contact');
133e2c99 715 }
716 }
717
a7488080 718 if (!empty($params['payment_processor_id'])) {
a479fe60 719 // validate payment instrument (e.g. credit card number)
f48e6cf7 720 CRM_Core_Payment_Form::validatePaymentInstrument($params['payment_processor_id'], $params, $errors, NULL);
6a488035
TO
721 }
722
723 $joinDate = NULL;
a7488080 724 if (!empty($params['join_date'])) {
6a488035
TO
725
726 $joinDate = CRM_Utils_Date::processDate($params['join_date']);
727
18aa3e1e 728 foreach ($selectedMemberships as $memType) {
6a488035 729 $startDate = NULL;
a7488080 730 if (!empty($params['start_date'])) {
6a488035
TO
731 $startDate = CRM_Utils_Date::processDate($params['start_date']);
732 }
733
734 // if end date is set, ensure that start date is also set
735 // and that end date is later than start date
6a488035 736 $endDate = NULL;
a7488080 737 if (!empty($params['end_date'])) {
6a488035
TO
738 $endDate = CRM_Utils_Date::processDate($params['end_date']);
739 }
740
df1ebfea 741 $membershipDetails = CRM_Member_BAO_MembershipType::getMembershipType($memType);
303007a3 742 if ($startDate && CRM_Utils_Array::value('period_type', $membershipDetails) === 'rolling') {
6a488035
TO
743 if ($startDate < $joinDate) {
744 $errors['start_date'] = ts('Start date must be the same or later than Member since.');
745 }
746 }
747
748 if ($endDate) {
303007a3 749 if ($membershipDetails['duration_unit'] === 'lifetime') {
f476dde8 750 // Check if status is NOT cancelled or similar. For lifetime memberships, there is no automated
ae3d69ec 751 // process to update status based on end-date. The user must change the status now.
be2fb01f 752 $result = civicrm_api3('MembershipStatus', 'get', [
ae3d69ec
SG
753 'sequential' => 1,
754 'is_current_member' => 0,
be2fb01f 755 ]);
f476dde8 756 $tmp_statuses = $result['values'];
be2fb01f 757 $status_ids = [];
22e263ad 758 foreach ($tmp_statuses as $cur_stat) {
8efea814 759 $status_ids[] = $cur_stat['id'];
f476dde8 760 }
e136f704 761
481a74f4 762 if (empty($params['status_id']) || in_array($params['status_id'], $status_ids) == FALSE) {
f476dde8 763 $errors['status_id'] = ts('Please enter a status that does NOT represent a current membership status.');
e136f704
O
764 }
765
766 if (!empty($params['is_override']) && !CRM_Member_StatusOverrideTypes::isPermanent($params['is_override'])) {
767 $errors['is_override'] = ts('Because you set an End Date for a lifetime membership, This must be set to "Override Permanently"');
f476dde8 768 }
6a488035
TO
769 }
770 else {
771 if (!$startDate) {
772 $errors['start_date'] = ts('Start date must be set if end date is set.');
773 }
774 if ($endDate < $startDate) {
775 $errors['end_date'] = ts('End date must be the same or later than start date.');
776 }
777 }
778 }
779
5e56c7a5 780 // Default values for start and end dates if not supplied on the form.
6a488035
TO
781 $defaultDates = CRM_Member_BAO_MembershipType::getDatesForMembershipType($memType,
782 $joinDate,
783 $startDate,
784 $endDate
785 );
786
787 if (!$startDate) {
788 $startDate = CRM_Utils_Array::value('start_date',
789 $defaultDates
790 );
791 }
792 if (!$endDate) {
793 $endDate = CRM_Utils_Array::value('end_date',
794 $defaultDates
795 );
796 }
797
798 //CRM-3724, check for availability of valid membership status.
e136f704 799 if ((empty($params['is_override']) || CRM_Member_StatusOverrideTypes::isNo($params['is_override'])) && !isset($errors['_qf_default'])) {
6a488035
TO
800 $calcStatus = CRM_Member_BAO_MembershipStatus::getMembershipStatusByDate($startDate,
801 $endDate,
802 $joinDate,
2cb64970 803 'now',
5f11bbcc
EM
804 TRUE,
805 $memType,
806 $params
6a488035
TO
807 );
808 if (empty($calcStatus)) {
809 $url = CRM_Utils_System::url('civicrm/admin/member/membershipStatus', 'reset=1&action=browse');
810 $errors['_qf_default'] = ts('There is no valid Membership Status available for selected membership dates.');
be2fb01f 811 $status = ts('Oops, it looks like there is no valid membership status available for the given membership dates. You can <a href="%1">Configure Membership Status Rules</a>.', [1 => $url]);
6a488035 812 if (!$self->_mode) {
e136f704 813 $status .= ' ' . ts('OR You can sign up by setting Status Override? to something other than "NO".');
6a488035
TO
814 }
815 CRM_Core_Session::setStatus($status, ts('Membership Status Error'), 'error');
816 }
817 }
818 }
819 }
820 else {
821 $errors['join_date'] = ts('Please enter the Member Since.');
822 }
823
e136f704
O
824 if (!empty($params['is_override']) && CRM_Member_StatusOverrideTypes::isOverridden($params['is_override']) && empty($params['status_id'])) {
825 $errors['status_id'] = ts('Please enter the Membership status.');
826 }
827
828 if (!empty($params['is_override']) && CRM_Member_StatusOverrideTypes::isUntilDate($params['is_override'])) {
829 if (empty($params['status_override_end_date'])) {
830 $errors['status_override_end_date'] = ts('Please enter the Membership override end date.');
831 }
6a488035
TO
832 }
833
834 //total amount condition arise when membership type having no
835 //minimum fee
836 if (isset($params['record_contribution'])) {
6a488035
TO
837 if (CRM_Utils_System::isNull($params['total_amount'])) {
838 $errors['total_amount'] = ts('Please enter the contribution.');
839 }
840 }
841
6a488035
TO
842 return empty($errors) ? TRUE : $errors;
843 }
844
845 /**
fe482240 846 * Process the form submission.
a090fe98 847 *
848 * @throws \CRM_Core_Exception
849 * @throws \CiviCRM_API3_Exception
6a488035
TO
850 */
851 public function postProcess() {
852 if ($this->_action & CRM_Core_Action::DELETE) {
3506b6cd 853 CRM_Member_BAO_Membership::del($this->_id);
6a488035
TO
854 return;
855 }
7865d848
EM
856 // get the submitted form values.
857 $this->_params = $this->controller->exportValues($this->_name);
e9fe9519 858 $this->prepareStatusOverrideValues();
6a488035 859
09108d7d 860 $this->submit();
7865d848
EM
861
862 $this->setUserContext();
863 }
864
e9fe9519
O
865 /**
866 * Prepares the values related to status override.
867 */
868 private function prepareStatusOverrideValues() {
869 $this->setOverrideDateValue();
870 $this->convertIsOverrideValue();
871 }
872
873 /**
874 * Sets status override end date to empty value if
875 * the selected override option is not 'until date'.
876 */
877 private function setOverrideDateValue() {
393f1657 878 if (!CRM_Member_StatusOverrideTypes::isUntilDate(CRM_Utils_Array::value('is_override', $this->_params))) {
e9fe9519
O
879 $this->_params['status_override_end_date'] = '';
880 }
881 }
882
e136f704
O
883 /**
884 * Convert the value of selected (status override?)
885 * option to TRUE if it indicate an overridden status
886 * or FALSE otherwise.
887 */
888 private function convertIsOverrideValue() {
f5c93de2 889 $this->_params['is_override'] = CRM_Member_StatusOverrideTypes::isOverridden($this->_params['is_override'] ?? CRM_Member_StatusOverrideTypes::NO);
e136f704
O
890 }
891
7865d848
EM
892 /**
893 * Send email receipt.
894 *
895 * @param CRM_Core_Form $form
896 * Form object.
897 * @param array $formValues
898 * @param object $membership
899 * Object.
900 *
901 * @return bool
902 * true if mail was sent successfully
a090fe98 903 * @throws \CRM_Core_Exception
904 *
905 * @deprecated
906 * This function is shared with Batch_Entry which has limited overlap
907 * & needs rationalising.
908 *
7865d848 909 */
48b9327c 910 public static function emailReceipt($form, &$formValues, $membership) {
7865d848 911 // retrieve 'from email id' for acknowledgement
9c1bc317 912 $receiptFrom = $formValues['from_email_address'] ?? NULL;
7865d848 913
efc0e24a 914 // @todo figure out how much of the stuff below is genuinely shared with the batch form & a logical shared place.
7865d848
EM
915 if (!empty($formValues['payment_instrument_id'])) {
916 $paymentInstrument = CRM_Contribute_PseudoConstant::paymentInstrument();
917 $formValues['paidBy'] = $paymentInstrument[$formValues['payment_instrument_id']];
918 }
919
7865d848
EM
920 $form->assign('module', 'Membership');
921 $form->assign('contactID', $formValues['contact_id']);
922
923 $form->assign('membershipID', CRM_Utils_Array::value('membership_id', $form->_params, CRM_Utils_Array::value('membership_id', $form->_defaultValues)));
924
925 if (!empty($formValues['contribution_id'])) {
926 $form->assign('contributionID', $formValues['contribution_id']);
927 }
7865d848
EM
928
929 if (!empty($formValues['contribution_status_id'])) {
930 $form->assign('contributionStatusID', $formValues['contribution_status_id']);
931 $form->assign('contributionStatus', CRM_Contribute_PseudoConstant::contributionStatus($formValues['contribution_status_id'], 'name'));
932 }
933
934 if (!empty($formValues['is_renew'])) {
935 $form->assign('receiptType', 'membership renewal');
936 }
937 else {
938 $form->assign('receiptType', 'membership signup');
939 }
9eb6085d 940 $form->assign('receive_date', CRM_Utils_Array::value('receive_date', $formValues));
7865d848
EM
941 $form->assign('formValues', $formValues);
942
48b9327c 943 $form->assign('mem_start_date', CRM_Utils_Date::formatDateOnlyLong($membership->start_date));
944 if (!CRM_Utils_System::isNull($membership->end_date)) {
945 $form->assign('mem_end_date', CRM_Utils_Date::formatDateOnlyLong($membership->end_date));
7865d848 946 }
48b9327c 947 $form->assign('membership_name', CRM_Member_PseudoConstant::membershipType($membership->membership_type_id));
7865d848 948
efc0e24a 949 // @todo - if we have to figure out if this is for batch processing it doesn't belong in the shared function.
7865d848
EM
950 $isBatchProcess = is_a($form, 'CRM_Batch_Form_Entry');
951 if ((empty($form->_contributorDisplayName) || empty($form->_contributorEmail)) || $isBatchProcess) {
952 // in this case the form is being called statically from the batch editing screen
953 // having one class in the form layer call another statically is not greate
954 // & we should aim to move this function to the BAO layer in future.
955 // however, we can assume that the contact_id passed in by the batch
956 // function will be the recipient
2a320359 957 [$form->_contributorDisplayName, $form->_contributorEmail]
7865d848
EM
958 = CRM_Contact_BAO_Contact_Location::getEmailDetails($formValues['contact_id']);
959 if (empty($form->_receiptContactId) || $isBatchProcess) {
960 $form->_receiptContactId = $formValues['contact_id'];
961 }
962 }
7865d848 963
4f4d52d5 964 CRM_Core_BAO_MessageTemplate::sendTemplate(
be2fb01f 965 [
7865d848
EM
966 'groupName' => 'msg_tpl_workflow_membership',
967 'valueName' => 'membership_offline_receipt',
968 'contactId' => $form->_receiptContactId,
969 'from' => $receiptFrom,
970 'toName' => $form->_contributorDisplayName,
971 'toEmail' => $form->_contributorEmail,
972 'PDFFilename' => ts('receipt') . '.pdf',
1ed8d778 973 'isEmailPdf' => Civi::settings()->get('invoicing') && Civi::settings()->get('invoice_is_email_pdf'),
7865d848
EM
974 'contributionId' => $formValues['contribution_id'],
975 'isTest' => (bool) ($form->_action & CRM_Core_Action::PREVIEW),
be2fb01f 976 ]
7865d848
EM
977 );
978
979 return TRUE;
980 }
981
982 /**
5e56c7a5 983 * Submit function.
984 *
985 * This is also accessed by unit tests.
303007a3 986 *
987 * @throws \CRM_Core_Exception
988 * @throws \CiviCRM_API3_Exception
7865d848 989 */
65e42c27 990 public function submit(): void {
09108d7d 991 $this->storeContactFields($this->_params);
ba2f3f65 992 $this->beginPostProcess();
0931f801 993 $endDate = NULL;
ebf7e65f 994 $membership = $calcDate = [];
995
be2fb01f 996 $params = $softParams = $ids = [];
7865d848 997
64412b4e 998 $mailSend = FALSE;
09108d7d 999 $this->processBillingAddress();
64412b4e
MWMC
1000 $formValues = $this->_params;
1001 $formValues = $this->setPriceSetParameters($formValues);
7865d848 1002
08fd4b45
EM
1003 if ($this->_id) {
1004 $ids['membership'] = $params['id'] = $this->_id;
1005 }
08fd4b45 1006
7865d848
EM
1007 // Set variables that we normally get from context.
1008 // In form mode these are set in preProcess.
1009 //TODO: set memberships, fixme
1010 $this->setContextVariables($formValues);
ccb02c2d 1011
e4a6290d 1012 $this->_memTypeSelected = self::getSelectedMemberships(
ccb02c2d 1013 $this->_priceSet,
e4a6290d 1014 $formValues
1015 );
be2e79c8 1016 $formValues['financial_type_id'] = $this->getFinancialTypeID();
be2fb01f 1017 $membershipTypeValues = [];
6a488035
TO
1018 foreach ($this->_memTypeSelected as $memType) {
1019 $membershipTypeValues[$memType]['membership_type_id'] = $memType;
1020 }
1021
1022 //take the required membership recur values.
84f3d874 1023 if ($this->isCreateRecurringContribution()) {
6a488035
TO
1024 $count = 0;
1025 foreach ($this->_memTypeSelected as $memType) {
1026 $recurMembershipTypeValues = CRM_Utils_Array::value($memType,
ec7af55f 1027 $this->allMembershipTypeDetails, []
6a488035 1028 );
ec7af55f 1029 if (!$recurMembershipTypeValues['auto_renew']) {
1030 continue;
1031 }
1032 foreach ([
1033 'frequency_interval' => 'duration_interval',
1034 'frequency_unit' => 'duration_unit',
1035 ] as $mapVal => $mapParam) {
6a488035 1036 if (!$count) {
ccb02c2d 1037 $formValues[$mapVal] = CRM_Utils_Array::value($mapParam,
6a488035
TO
1038 $recurMembershipTypeValues
1039 );
1040 }
1041 }
1042 $count++;
1043 }
6a488035
TO
1044 }
1045
ccb02c2d 1046 $isQuickConfig = $this->_priceSet['is_quick_config'];
1047
be2fb01f 1048 $termsByType = [];
9f1bc5dc 1049
20df462f 1050 $lineItem = [$this->order->getPriceSetID() => $this->order->getLineItems()];
ccb02c2d 1051
20df462f 1052 $params['tax_amount'] = $this->order->getTotalTaxAmount();
1053 $params['total_amount'] = $this->order->getTotalAmount();
6a488035
TO
1054 $params['contact_id'] = $this->_contactID;
1055
d04c9885 1056 $params = array_merge($params, $this->getFormMembershipParams());
6a488035 1057
0931f801 1058 $joinDate = $formValues['join_date'];
1059 $startDate = $formValues['start_date'];
1060 $endDate = $formValues['end_date'];
6a488035 1061
be2fb01f 1062 $calcDates = [];
e84c5dc2 1063 foreach ($this->order->getMembershipLineItems() as $membershipLineItem) {
1064 $memTypeNumTerms = $this->getSubmittedValue('num_terms') ?: $membershipLineItem['membership_num_terms'];
1065 $calcDates[$membershipLineItem['membership_type_id']] = CRM_Member_BAO_MembershipType::getDatesForMembershipType($membershipLineItem['membership_type_id'],
6a488035
TO
1066 $joinDate, $startDate, $endDate, $memTypeNumTerms
1067 );
1068 }
1069
1070 foreach ($calcDates as $memType => $calcDate) {
0931f801 1071 foreach (['join_date', 'start_date', 'end_date'] as $d) {
6a488035 1072 //first give priority to form values then calDates.
9c1bc317 1073 $date = $formValues[$d] ?? NULL;
6a488035 1074 if (!$date) {
9c1bc317 1075 $date = $calcDate[$d] ?? NULL;
6a488035
TO
1076 }
1077
1078 $membershipTypeValues[$memType][$d] = CRM_Utils_Date::processDate($date);
6a488035
TO
1079 }
1080 }
1081
6a488035
TO
1082 foreach ($this->_memTypeSelected as $memType) {
1083 if (array_key_exists('max_related', $formValues)) {
c26225d2 1084 // max related memberships - take from form or inherit from membership type
9c1bc317 1085 $membershipTypeValues[$memType]['max_related'] = $formValues['max_related'] ?? NULL;
6a488035 1086 }
6a488035
TO
1087 }
1088
6a488035 1089 // Retrieve the name and email of the current user - this will be the FROM for the receipt email
2a320359 1090 [$userName] = CRM_Contact_BAO_Contact_Location::getEmailDetails(CRM_Core_Session::getLoggedInContactID());
6a488035 1091
d80dbc14 1092 //CRM-13981, allow different person as a soft-contributor of chosen type
b11c92be 1093 if ($this->_contributorContactID != $this->_contactID) {
91ef9be0 1094 $params['contribution_contact_id'] = $this->_contributorContactID;
ccb02c2d 1095 if (!empty($formValues['soft_credit_type_id'])) {
1096 $softParams['soft_credit_type_id'] = $formValues['soft_credit_type_id'];
91ef9be0 1097 $softParams['contact_id'] = $this->_contactID;
6a488035
TO
1098 }
1099 }
c26225d2
MWMC
1100
1101 $pendingMembershipStatusId = CRM_Core_PseudoConstant::getKey('CRM_Member_BAO_Membership', 'status_id', 'Pending');
1102
a7488080 1103 if (!empty($formValues['record_contribution'])) {
be2fb01f 1104 $recordContribution = [
b11c92be 1105 'total_amount',
b11c92be 1106 'payment_instrument_id',
1107 'trxn_id',
1108 'contribution_status_id',
1109 'check_number',
b11c92be 1110 'receive_date',
a55e39e9 1111 'card_type_id',
1112 'pan_truncation',
be2fb01f 1113 ];
6a488035
TO
1114
1115 foreach ($recordContribution as $f) {
9c1bc317 1116 $params[$f] = $formValues[$f] ?? NULL;
6a488035 1117 }
be2e79c8 1118 $params['financial_type_id'] = $this->getFinancialTypeID();
a628c77b 1119 $params['campaign_id'] = $this->getSubmittedValue('campaign_id');
6a488035 1120
f8cb4d16 1121 if (empty($formValues['source'])) {
1122 $params['contribution_source'] = ts('%1 Membership: Offline signup (by %2)', [
ebf7e65f 1123 1 => $this->getSelectedMembershipLabels(),
f8cb4d16 1124 2 => $userName,
1125 ]);
1126 }
1127 else {
1128 $params['contribution_source'] = $formValues['source'];
0e81467c 1129 }
6a488035 1130
c26225d2 1131 $completedContributionStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
a7488080 1132 if (empty($params['is_override']) &&
c26225d2 1133 CRM_Utils_Array::value('contribution_status_id', $params) != $completedContributionStatusId
6a488035 1134 ) {
c26225d2 1135 $params['status_id'] = $pendingMembershipStatusId;
6a488035
TO
1136 $params['skipStatusCal'] = TRUE;
1137 $params['is_pay_later'] = 1;
1138 $this->assign('is_pay_later', 1);
1139 }
1140
6aef1389 1141 if ($this->getSubmittedValue('send_receipt')) {
9c1bc317 1142 $params['receipt_date'] = $formValues['receive_date'] ?? NULL;
6a488035
TO
1143 }
1144
1145 //insert financial type name in receipt.
1146 $formValues['contributionType_name'] = CRM_Core_DAO::getFieldValue('CRM_Financial_DAO_FinancialType',
be2e79c8 1147 $this->getFinancialTypeID()
6a488035
TO
1148 );
1149 }
1150
1151 // process line items, until no previous line items.
1152 if (!empty($lineItem)) {
1153 $params['lineItems'] = $lineItem;
1154 $params['processPriceSet'] = TRUE;
1155 }
be2fb01f 1156 $createdMemberships = [];
6a488035 1157 if ($this->_mode) {
ccb02c2d 1158 $params['total_amount'] = CRM_Utils_Array::value('total_amount', $formValues, 0);
a7f2d5fd 1159
1160 //CRM-20264 : Store CC type and number (last 4 digit) during backoffice or online payment
9c1bc317
CW
1161 $params['card_type_id'] = $this->_params['card_type_id'] ?? NULL;
1162 $params['pan_truncation'] = $this->_params['pan_truncation'] ?? NULL;
be2e79c8 1163 $params['financial_type_id'] = $this->getFinancialTypeID();
6a488035 1164
ba2f3f65 1165 //get the payment processor id as per mode. Try removing in favour of beginPostProcess.
ccb02c2d 1166 $params['payment_processor_id'] = $formValues['payment_processor_id'] = $this->_paymentProcessor['id'];
42347843 1167 $params['register_date'] = CRM_Utils_Time::date('YmdHis');
6a488035 1168
a1a94e61 1169 // add all the additional payment params we need
ccb02c2d 1170 $formValues['amount'] = $params['total_amount'];
cd98958c 1171 $formValues['currencyID'] = $this->getCurrency();
ccb02c2d 1172 $formValues['description'] = ts("Contribution submitted by a staff person using member's credit card for signup");
66de72bc 1173 $formValues['invoiceID'] = $this->getInvoiceID();
be2e79c8 1174 $formValues['financial_type_id'] = $this->getFinancialTypeID();
6a488035
TO
1175
1176 // at this point we've created a contact and stored its address etc
1177 // all the payment processors expect the name and address to be in the
1178 // so we copy stuff over to first_name etc.
ccb02c2d 1179 $paymentParams = $formValues;
6a488035
TO
1180 $paymentParams['contactID'] = $this->_contributorContactID;
1181 //CRM-10377 if payment is by an alternate contact then we need to set that person
1182 // as the contact in the payment params
b11c92be 1183 if ($this->_contributorContactID != $this->_contactID) {
ccb02c2d 1184 if (!empty($formValues['soft_credit_type_id'])) {
133e2c99 1185 $softParams['contact_id'] = $params['contact_id'];
ccb02c2d 1186 $softParams['soft_credit_type_id'] = $formValues['soft_credit_type_id'];
6a488035
TO
1187 }
1188 }
6aef1389 1189 if ($this->getSubmittedValue('send_receipt')) {
6a488035
TO
1190 $paymentParams['email'] = $this->_contributorEmail;
1191 }
1192
ba2f3f65 1193 // This is a candidate for shared beginPostProcess function.
64412b4e 1194 // @todo Do we need this now we have $this->formatParamsForPaymentProcessor() ?
ccb02c2d 1195 CRM_Core_Payment_Form::mapParams($this->_bltID, $formValues, $paymentParams, TRUE);
6a488035 1196 // CRM-7137 -for recurring membership,
b44e3f84 1197 // we do need contribution and recurring records.
6a488035 1198 $result = NULL;
84f3d874 1199 if ($this->isCreateRecurringContribution()) {
ccb02c2d 1200 $this->_params = $formValues;
18135422 1201
2a320359 1202 $contribution = $this->processContribution(
6a488035 1203 $paymentParams,
be2fb01f 1204 [
f6261e9d 1205 'contact_id' => $this->_contributorContactID,
eb5a5dac 1206 'line_item' => [$this->order->getPriceSetID() => $this->order->getLineItems()],
65e42c27 1207 'is_test' => $this->isTest(),
a628c77b 1208 'campaign_id' => $this->getSubmittedValue('campaign_id'),
3febe800 1209 'source' => CRM_Utils_Array::value('source', $paymentParams, CRM_Utils_Array::value('description', $paymentParams)),
c2b07971 1210 'payment_instrument_id' => $this->getPaymentInstrumentID(),
be2e79c8 1211 'financial_type_id' => $this->getFinancialTypeID(),
cd98958c 1212 'receive_date' => CRM_Utils_Time::date('YmdHis'),
eb5a5dac 1213 'tax_amount' => $this->order->getTotalTaxAmount(),
598e828c 1214 'total_amount' => $this->order->getTotalAmount(),
cd98958c 1215 'invoice_id' => $this->getInvoiceID(),
1216 'currency' => $this->getCurrency(),
1217 'is_pay_later' => $params['is_pay_later'] ?? 0,
1218 'skipLineItem' => $params['skipLineItem'] ?? 0,
598e828c 1219 'contribution_status_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending'),
1220 'receipt_date' => $this->getSubmittedValue('send_receipt') ? date('YmdHis') : NULL,
f8bcbc1f 1221 ]
6a488035 1222 );
133e2c99 1223
1224 //create new soft-credit record, CRM-13981
00c1cd97
CW
1225 if ($softParams) {
1226 $softParams['contribution_id'] = $contribution->id;
28d43453 1227 $softParams['currency'] = $this->getCurrency();
00c1cd97
CW
1228 $softParams['amount'] = $contribution->total_amount;
1229 CRM_Contribute_BAO_ContributionSoft::add($softParams);
1230 }
133e2c99 1231
a22bd791 1232 $paymentParams['contactID'] = $this->_contactID;
6a488035 1233 $paymentParams['contributionID'] = $contribution->id;
6a488035 1234 $paymentParams['contributionRecurID'] = $contribution->contribution_recur_id;
84f3d874 1235 $paymentParams['is_recur'] = $this->isCreateRecurringContribution();
f57cb50c 1236 $params['contribution_id'] = $paymentParams['contributionID'];
6a488035 1237 $params['contribution_recur_id'] = $paymentParams['contributionRecurID'];
6a488035 1238 }
c26225d2 1239 $paymentStatus = NULL;
6a488035
TO
1240
1241 if ($params['total_amount'] > 0.0) {
ab30e033 1242 $payment = $this->_paymentProcessor['object'];
06d062ce
EM
1243 try {
1244 $result = $payment->doPayment($paymentParams);
ccb02c2d 1245 $formValues = array_merge($formValues, $result);
c26225d2 1246 $paymentStatus = CRM_Core_PseudoConstant::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', $formValues['payment_status_id']);
ab30e033
EM
1247 // Assign amount to template if payment was successful.
1248 $this->assign('amount', $params['total_amount']);
6a488035 1249 }
31176a73 1250 catch (\Civi\Payment\Exception\PaymentProcessorException $e) {
06d062ce 1251 if (!empty($paymentParams['contributionID'])) {
ab30e033
EM
1252 CRM_Contribute_BAO_Contribution::failPayment($paymentParams['contributionID'], $this->_contactID,
1253 $e->getMessage());
06d062ce
EM
1254 }
1255 if (!empty($paymentParams['contributionRecurID'])) {
1256 CRM_Contribute_BAO_ContributionRecur::deleteRecurContribution($paymentParams['contributionRecurID']);
1257 }
1258
63dd64f2 1259 CRM_Core_Session::singleton()->setStatus($e->getMessage());
06d062ce 1260 CRM_Utils_System::redirect(CRM_Utils_System::url('civicrm/contact/view/membership',
31176a73 1261 "reset=1&action=add&cid={$this->_contactID}&context=membership&mode={$this->_mode}"
06d062ce 1262 ));
6a488035 1263
06d062ce 1264 }
6a488035
TO
1265 }
1266
c26225d2
MWMC
1267 if ($paymentStatus !== 'Completed') {
1268 $params['status_id'] = $pendingMembershipStatusId;
7d193e45 1269 $params['skipStatusCal'] = TRUE;
7d193e45 1270 //as membership is pending set dates to null.
11a83113 1271 foreach ($this->_memTypeSelected as $memType) {
1272 $membershipTypeValues[$memType]['joinDate'] = NULL;
1273 $membershipTypeValues[$memType]['startDate'] = NULL;
1274 $membershipTypeValues[$memType]['endDate'] = NULL;
7d193e45 1275 }
11a83113 1276 $endDate = $startDate = NULL;
7d193e45 1277 }
42347843
TO
1278 $now = CRM_Utils_Time::date('YmdHis');
1279 $params['receive_date'] = CRM_Utils_Time::date('Y-m-d H:i:s');
66de72bc 1280 $params['invoice_id'] = $this->getInvoiceID();
6a488035 1281 $params['contribution_source'] = ts('%1 Membership Signup: Credit card or direct debit (by %2)',
ebf7e65f 1282 [1 => $this->getSelectedMembershipLabels(), 2 => $userName]
6a488035 1283 );
15d6c8be 1284 $params['source'] = $formValues['source'] ?: $params['contribution_source'];
9c1bc317 1285 $params['trxn_id'] = $result['trxn_id'] ?? NULL;
65e42c27 1286 $params['is_test'] = $this->isTest();
6aef1389 1287 $params['receipt_date'] = NULL;
1288 if ($this->getSubmittedValue('send_receipt') && $paymentStatus === 'Completed') {
1289 // @todo this should be updated by the send function once sent rather than
1290 // set here.
6a488035
TO
1291 $params['receipt_date'] = $now;
1292 }
6a488035 1293
ccb02c2d 1294 $this->set('params', $formValues);
6a488035
TO
1295 $this->assign('trxn_id', CRM_Utils_Array::value('trxn_id', $result));
1296 $this->assign('receive_date',
1297 CRM_Utils_Date::mysqlToIso($params['receive_date'])
1298 );
1299
1300 // required for creating membership for related contacts
1301 $params['action'] = $this->_action;
1302
1303 //create membership record.
1304 $count = 0;
1305 foreach ($this->_memTypeSelected as $memType) {
1306 if ($count &&
1307 ($relateContribution = CRM_Member_BAO_Membership::getMembershipContributionId($membership->id))
1308 ) {
1309 $membershipTypeValues[$memType]['relate_contribution_id'] = $relateContribution;
1310 }
1311
1312 $membershipParams = array_merge($membershipTypeValues[$memType], $params);
87d0f881 1313 //CRM-15366
84f3d874 1314 if (!empty($softParams) && !$this->isCreateRecurringContribution()) {
396e62d8 1315 $membershipParams['soft_credit'] = $softParams;
1316 }
77623a96 1317 if (isset($result['fee_amount'])) {
1318 $membershipParams['fee_amount'] = $result['fee_amount'];
1319 }
8a7b41d1
EM
1320 // This is required to trigger the recording of the membership contribution in the
1321 // CRM_Member_BAO_Membership::Create function.
1322 // @todo stop setting this & 'teach' the create function to respond to something
1323 // appropriate as part of our 2-step always create the pending contribution & then finally add the payment
1324 // process -
1325 // @see http://wiki.civicrm.org/confluence/pages/viewpage.action?pageId=261062657#Payments&AccountsRoadmap-Movetowardsalwaysusinga2-steppaymentprocess
9c1bc317 1326 $membershipParams['contribution_status_id'] = $result['payment_status_id'] ?? NULL;
84f3d874 1327 if ($this->isCreateRecurringContribution()) {
ccb02c2d 1328 // The earlier process created the line items (although we want to get rid of the earlier one in favour
1329 // of a single path!
1330 unset($membershipParams['lineItems']);
1331 }
c2b07971 1332 $membershipParams['payment_instrument_id'] = $this->getPaymentInstrumentID();
f57cb50c 1333 // @todo stop passing $ids (membership and userId only are set above)
6a488035 1334 $membership = CRM_Member_BAO_Membership::create($membershipParams, $ids);
9c1bc317 1335 $params['contribution'] = $membershipParams['contribution'] ?? NULL;
3e228d81 1336 unset($params['lineItems']);
6a488035
TO
1337 $this->_membershipIDs[] = $membership->id;
1338 $createdMemberships[$memType] = $membership;
1339 $count++;
1340 }
1341
6a488035
TO
1342 }
1343 else {
1344 $params['action'] = $this->_action;
e0ec7c62 1345 foreach ($lineItem[$this->_priceSetId] as $id => $lineItemValues) {
1346 if (empty($lineItemValues['membership_type_id'])) {
1347 continue;
6a488035
TO
1348 }
1349
f8cb4d16 1350 // @todo figure out why recieve_date isn't being set right here.
1351 if (empty($params['receive_date'])) {
42347843 1352 $params['receive_date'] = CRM_Utils_Time::date('Y-m-d H:i:s');
6a488035 1353 }
e0ec7c62 1354 $membershipParams = array_merge($params, $membershipTypeValues[$lineItemValues['membership_type_id']]);
d80dbc14 1355
f8cb4d16 1356 if (!empty($softParams)) {
1357 $membershipParams['soft_credit'] = $softParams;
6a488035 1358 }
e0ec7c62 1359 unset($membershipParams['contribution_status_id']);
1360 $membershipParams['skipLineItem'] = TRUE;
1361 unset($membershipParams['lineItems']);
f8cb4d16 1362 // @todo stop passing $ids (membership and userId only are set above)
1363 $membership = CRM_Member_BAO_Membership::create($membershipParams, $ids);
e0ec7c62 1364 $lineItem[$this->_priceSetId][$id]['entity_id'] = $membership->id;
1365 $lineItem[$this->_priceSetId][$id]['entity_table'] = 'civicrm_membership';
f8cb4d16 1366
1367 $this->_membershipIDs[] = $membership->id;
e0ec7c62 1368 $createdMemberships[$membership->membership_type_id] = $membership;
1369 }
1370 $params['lineItems'] = $lineItem;
1371 if (!empty($formValues['record_contribution'])) {
1372 CRM_Member_BAO_Membership::recordMembershipContribution($params);
6a488035
TO
1373 }
1374 }
1bd1288b 1375 if (($this->_action & CRM_Core_Action::UPDATE)) {
1376 $this->addStatusMessage($this->getStatusMessageForUpdate($membership, $endDate));
1377 }
1378 elseif (($this->_action & CRM_Core_Action::ADD)) {
751bcac5 1379 $this->addStatusMessage($this->getStatusMessageForCreate($endDate, $createdMemberships,
84f3d874 1380 $this->isCreateRecurringContribution(), $calcDates));
1bd1288b 1381 }
6a488035 1382
7f2c42bf 1383 // This would always be true as we always add price set id into both
1384 // quick config & non quick config price sets.
ccb02c2d 1385 if (!empty($lineItem[$this->_priceSetId])) {
ccb02c2d 1386 foreach ($lineItem[$this->_priceSetId] as & $priceFieldOp) {
a7488080 1387 if (!empty($priceFieldOp['membership_type_id'])) {
56f54f02 1388 $priceFieldOp['start_date'] = $membershipTypeValues[$priceFieldOp['membership_type_id']]['start_date'] ? CRM_Utils_Date::formatDateOnlyLong($membershipTypeValues[$priceFieldOp['membership_type_id']]['start_date']) : '-';
1389 $priceFieldOp['end_date'] = $membershipTypeValues[$priceFieldOp['membership_type_id']]['end_date'] ? CRM_Utils_Date::formatDateOnlyLong($membershipTypeValues[$priceFieldOp['membership_type_id']]['end_date']) : '-';
6a488035
TO
1390 }
1391 else {
1392 $priceFieldOp['start_date'] = $priceFieldOp['end_date'] = 'N/A';
1393 }
79d001a2 1394 }
4d540f37 1395 if (Civi::settings()->get('invoicing')) {
be2fb01f 1396 $dataArray = [];
ccb02c2d 1397 foreach ($lineItem[$this->_priceSetId] as $key => $value) {
03b412ae
PB
1398 if (isset($value['tax_amount']) && isset($value['tax_rate'])) {
1399 if (isset($dataArray[$value['tax_rate']])) {
1400 $dataArray[$value['tax_rate']] = $dataArray[$value['tax_rate']] + CRM_Utils_Array::value('tax_amount', $value);
0db6c3e1
TO
1401 }
1402 else {
9c1bc317 1403 $dataArray[$value['tax_rate']] = $value['tax_amount'] ?? NULL;
03b412ae 1404 }
0e81467c 1405 }
03b412ae 1406 }
4d540f37 1407
1408 $this->assign('totalTaxAmount', $this->order->getTotalTaxAmount());
1409 // Not sure why would need this on Submit.... unless it's being used when sending mails in which case this is the wrong place
1410 $this->assign('taxTerm', $this->getSalesTaxTerm());
03b412ae 1411 $this->assign('dataArray', $dataArray);
6a488035
TO
1412 }
1413 }
1414 $this->assign('lineItem', !empty($lineItem) && !$isQuickConfig ? $lineItem : FALSE);
1415
1416 $receiptSend = FALSE;
7d193e45
LS
1417 $contributionId = CRM_Member_BAO_Membership::getMembershipContributionId($membership->id);
1418 $membershipIds = $this->_membershipIDs;
1419 if ($contributionId && !empty($membershipIds)) {
1420 $contributionDetails = CRM_Contribute_BAO_Contribution::getContributionDetails(
1421 CRM_Export_Form_Select::MEMBER_EXPORT, $this->_membershipIDs);
a090fe98 1422 if ($contributionDetails[$membership->id]['contribution_status'] === 'Completed') {
aadd21c2 1423 $receiptSend = TRUE;
7d193e45
LS
1424 }
1425 }
6a488035 1426
7e0c2ccd 1427 $receiptSent = FALSE;
6aef1389 1428 if ($this->getSubmittedValue('send_receipt') && $receiptSend) {
b11c92be 1429 $formValues['contact_id'] = $this->_contactID;
7d193e45 1430 $formValues['contribution_id'] = $contributionId;
186a737c
EM
1431 // We really don't need a distinct receipt_text_signup vs receipt_text_renewal as they are
1432 // handled in the receipt. But by setting one we avoid breaking templates for now
1433 // although at some point we should switch in the templates.
1434 $formValues['receipt_text_signup'] = $formValues['receipt_text'];
6a488035 1435 // send email receipt
09108d7d 1436 $this->assignBillingName();
efc0e24a 1437 $mailSend = $this->emailMembershipReceipt($formValues, $membership);
7e0c2ccd 1438 $receiptSent = TRUE;
6a488035 1439 }
6a488035 1440
7865d848
EM
1441 // finally set membership id if already not set
1442 if (!$this->_id) {
1443 $this->_id = $membership->id;
6a488035 1444 }
6a488035 1445
268a84f2 1446 $this->updateContributionOnMembershipTypeChange($params, $membership);
5ce8b943 1447 if ($receiptSent && $mailSend) {
be2fb01f 1448 $this->addStatusMessage(ts('A membership confirmation and receipt has been sent to %1.', [1 => $this->_contributorEmail]));
5ce8b943 1449 }
1450
1451 CRM_Core_Session::setStatus($this->getStatusMessage(), ts('Complete'), 'success');
1452 $this->setStatusMessage($membership);
7865d848
EM
1453 }
1454
268a84f2 1455 /**
1456 * Update related contribution of a membership if update_contribution_on_membership_type_change
1457 * contribution setting is enabled and type is changed on edit
1458 *
1459 * @param array $inputParams
1460 * submitted form values
1461 * @param CRM_Member_DAO_Membership $membership
1462 * Updated membership object
1463 *
a090fe98 1464 * @throws \CRM_Core_Exception
1465 * @throws \CiviCRM_API3_Exception
268a84f2 1466 */
1467 protected function updateContributionOnMembershipTypeChange($inputParams, $membership) {
1468 if (Civi::settings()->get('update_contribution_on_membership_type_change') &&
971e129b
SL
1469 // on update
1470 ($this->_action & CRM_Core_Action::UPDATE) &&
1471 // if ID is present
1472 $this->_id &&
1473 // if selected membership doesn't match with earlier membership
1474 !in_array($this->_memType, $this->_memTypeSelected)
268a84f2 1475 ) {
84f3d874 1476 if ($this->isCreateRecurringContribution()) {
268a84f2 1477 CRM_Core_Session::setStatus(ts('Associated recurring contribution cannot be updated on membership type change.', ts('Error'), 'error'));
1478 return;
1479 }
1480
1481 // fetch lineitems by updated membership ID
1482 $lineItems = CRM_Price_BAO_LineItem::getLineItems($membership->id, 'membership');
1483 // retrieve the related contribution ID
1484 $contributionID = CRM_Core_DAO::getFieldValue(
1485 'CRM_Member_DAO_MembershipPayment',
1486 $membership->id,
1487 'contribution_id',
1488 'membership_id'
1489 );
1490 // get price fields of chosen price-set
1491 $priceSetDetails = CRM_Utils_Array::value(
1492 $this->_priceSetId,
1493 CRM_Price_BAO_PriceSet::getSetDetail(
1494 $this->_priceSetId,
1495 TRUE,
1496 TRUE
1497 )
1498 );
1499
1500 // add price field information in $inputParams
1501 self::addPriceFieldByMembershipType($inputParams, $priceSetDetails['fields'], $membership->membership_type_id);
6dde7f04 1502
268a84f2 1503 // update related contribution and financial records
1504 CRM_Price_BAO_LineItem::changeFeeSelections(
1505 $inputParams,
1506 $membership->id,
1507 'membership',
1508 $contributionID,
1509 $priceSetDetails['fields'],
6dde7f04 1510 $lineItems
268a84f2 1511 );
1512 CRM_Core_Session::setStatus(ts('Associated contribution is updated on membership type change.'), ts('Success'), 'success');
1513 }
1514 }
1515
1516 /**
1517 * Add selected price field information in $formValues
1518 *
1519 * @param array $formValues
1520 * submitted form values
1521 * @param array $priceFields
1522 * Price fields of selected Priceset ID
1523 * @param int $membershipTypeID
1524 * Selected membership type ID
1525 *
1526 */
1527 public static function addPriceFieldByMembershipType(&$formValues, $priceFields, $membershipTypeID) {
1528 foreach ($priceFields as $priceFieldID => $priceField) {
1529 if (isset($priceField['options']) && count($priceField['options'])) {
1530 foreach ($priceField['options'] as $option) {
1531 if ($option['membership_type_id'] == $membershipTypeID) {
1532 $formValues["price_{$priceFieldID}"] = $option['id'];
1533 break;
1534 }
1535 }
1536 }
1537 }
1538 }
971e129b 1539
5e56c7a5 1540 /**
1541 * Set context in session.
1542 */
7865d848 1543 protected function setUserContext() {
6a488035 1544 $buttonName = $this->controller->getButtonName();
7865d848
EM
1545 $session = CRM_Core_Session::singleton();
1546
e5777079
MW
1547 if ($buttonName == $this->getButtonName('upload', 'new')) {
1548 if ($this->_context === 'standalone') {
1549 $url = CRM_Utils_System::url('civicrm/member/add',
b11c92be 1550 'reset=1&action=add&context=standalone'
e5777079 1551 );
6a488035
TO
1552 }
1553 else {
e5777079
MW
1554 $url = CRM_Utils_System::url('civicrm/contact/view/membership',
1555 "reset=1&action=add&context=membership&cid={$this->_contactID}"
1556 );
6a488035
TO
1557 }
1558 }
e5777079
MW
1559 else {
1560 $url = CRM_Utils_System::url('civicrm/contact/view',
1561 "reset=1&cid={$this->_contactID}&selectedChild=member"
1562 );
6a488035 1563 }
e5777079 1564 $session->replaceUserContext($url);
6a488035
TO
1565 }
1566
1567 /**
5e56c7a5 1568 * Get status message for updating membership.
1569 *
1570 * @param CRM_Member_BAO_Membership $membership
1571 * @param string $endDate
5e56c7a5 1572 *
7865d848 1573 * @return string
6a488035 1574 */
6d5b9c63 1575 protected function getStatusMessageForUpdate($membership, $endDate) {
5e56c7a5 1576 // End date can be modified by hooks, so if end date is set then use it.
7865d848 1577 $endDate = ($membership->end_date) ? $membership->end_date : $endDate;
6a488035 1578
be2fb01f 1579 $statusMsg = ts('Membership for %1 has been updated.', [1 => $this->_memberDisplayName]);
7865d848
EM
1580 if ($endDate && $endDate !== 'null') {
1581 $endDate = CRM_Utils_Date::customFormat($endDate);
be2fb01f 1582 $statusMsg .= ' ' . ts('The membership End Date is %1.', [1 => $endDate]);
6a488035 1583 }
7865d848
EM
1584 return $statusMsg;
1585 }
6a488035 1586
7865d848 1587 /**
5e56c7a5 1588 * Get status message for create action.
1589 *
1590 * @param string $endDate
5e56c7a5 1591 * @param array $createdMemberships
5b217d3f 1592 * @param bool $isRecur
5e56c7a5 1593 * @param array $calcDates
5e56c7a5 1594 *
7865d848
EM
1595 * @return array|string
1596 */
751bcac5 1597 protected function getStatusMessageForCreate($endDate, $createdMemberships,
6d5b9c63 1598 $isRecur, $calcDates) {
7865d848
EM
1599 // FIX ME: fix status messages
1600
be2fb01f 1601 $statusMsg = [];
751bcac5 1602 foreach ($this->_memTypeSelected as $membershipTypeID) {
1603 $statusMsg[$membershipTypeID] = ts('%1 membership for %2 has been added.', [
1604 1 => $this->allMembershipTypeDetails[$membershipTypeID]['name'],
7865d848 1605 2 => $this->_memberDisplayName,
be2fb01f 1606 ]);
6a488035 1607
751bcac5 1608 $membership = $createdMemberships[$membershipTypeID];
15d6c8be 1609 $memEndDate = $membership->end_date ?: $endDate;
6a488035 1610
7865d848 1611 //get the end date from calculated dates.
5b217d3f 1612 if (!$memEndDate && !$isRecur) {
751bcac5 1613 $memEndDate = $calcDates[$membershipTypeID]['end_date'] ?? NULL;
35fa23f8 1614 }
6a488035 1615
7865d848 1616 if ($memEndDate && $memEndDate !== 'null') {
56f54f02 1617 $memEndDate = CRM_Utils_Date::formatDateOnlyLong($memEndDate);
751bcac5 1618 $statusMsg[$membershipTypeID] .= ' ' . ts('The new membership End Date is %1.', [1 => $memEndDate]);
b11c92be 1619 }
6a488035 1620 }
7865d848 1621 $statusMsg = implode('<br/>', $statusMsg);
7865d848 1622 return $statusMsg;
6a488035 1623 }
96025800 1624
5b217d3f 1625 /**
1626 * @param $membership
5b217d3f 1627 */
5ce8b943 1628 protected function setStatusMessage($membership) {
5b217d3f 1629 //CRM-15187
1630 // display message when membership type is changed
1631 if (($this->_action & CRM_Core_Action::UPDATE) && $this->_id && !in_array($this->_memType, $this->_memTypeSelected)) {
0fedbc88
PN
1632 $lineItem = CRM_Price_BAO_LineItem::getLineItems($this->_id, 'membership');
1633 $maxID = max(array_keys($lineItem));
1634 $lineItem = $lineItem[$maxID];
1635 $membershipTypeDetails = $this->allMembershipTypeDetails[$membership->membership_type_id];
1636 if ($membershipTypeDetails['financial_type_id'] != $lineItem['financial_type_id']) {
1637 CRM_Core_Session::setStatus(
1638 ts('The financial types associated with the old and new membership types are different. You may want to edit the contribution associated with this membership to adjust its financial type.'),
1639 ts('Warning')
1640 );
1641 }
1642 if ($membershipTypeDetails['minimum_fee'] != $lineItem['line_total']) {
1643 CRM_Core_Session::setStatus(
1644 ts('The cost of the old and new membership types are different. You may want to edit the contribution associated with this membership to adjust its amount.'),
1645 ts('Warning')
1646 );
1647 }
5b217d3f 1648 }
1649 }
1650
50b85bf9 1651 /**
1652 * @return bool
a090fe98 1653 * @throws \CRM_Core_Exception
50b85bf9 1654 */
1655 protected function isUpdateToExistingRecurringMembership() {
1656 $isRecur = FALSE;
1657 if ($this->_action & CRM_Core_Action::UPDATE
59c798c9 1658 && CRM_Core_DAO::getFieldValue('CRM_Member_DAO_Membership', $this->getEntityId(),
50b85bf9 1659 'contribution_recur_id')
59c798c9 1660 && !CRM_Member_BAO_Membership::isSubscriptionCancelled($this->getEntityId())) {
50b85bf9 1661
1662 $isRecur = TRUE;
1663 }
1664 return $isRecur;
1665 }
1666
829df55e 1667 /**
1668 * Send a receipt for the membership.
1669 *
1670 * @param array $formValues
efc0e24a 1671 * @param \CRM_Member_BAO_Membership $membership
1672 *
1673 * @return bool
a090fe98 1674 * @throws \CRM_Core_Exception
829df55e 1675 */
1676 protected function emailMembershipReceipt($formValues, $membership) {
efc0e24a 1677 $customValues = $this->getCustomValuesForReceipt($formValues, $membership);
48b9327c 1678 $this->assign('customValues', $customValues);
efc0e24a 1679
5c6ad1b1 1680 if ($this->_mode) {
1681 // @todo move this outside shared code as Batch entry just doesn't
1682 $this->assign('address', CRM_Utils_Address::getFormattedBillingAddressFieldsFromParameters(
1683 $this->_params,
1684 $this->_bltID
1685 ));
1686
1687 $valuesForForm = CRM_Contribute_Form_AbstractEditPayment::formatCreditCardDetails($this->_params);
1688 $this->assignVariables($valuesForForm, ['credit_card_exp_date', 'credit_card_type', 'credit_card_number']);
1689 $this->assign('is_pay_later', 0);
1690 $this->assign('isPrimary', 1);
1691 }
48b9327c 1692 return self::emailReceipt($this, $formValues, $membership);
efc0e24a 1693 }
1694
1695 /**
1696 * Filter the custom values from the input parameters (for display in the email).
1697 *
1698 * @todo figure out why the scary code this calls does & document.
1699 *
1700 * @param array $formValues
1701 * @param \CRM_Member_BAO_Membership $membership
1702 * @return array
1703 */
1704 protected function getCustomValuesForReceipt($formValues, $membership) {
1705 $customFields = $customValues = [];
1706 if (property_exists($this, '_groupTree')
1707 && !empty($this->_groupTree)
1708 ) {
1709 foreach ($this->_groupTree as $groupID => $group) {
a090fe98 1710 if ($groupID === 'info') {
efc0e24a 1711 continue;
1712 }
1713 foreach ($group['fields'] as $k => $field) {
1714 $field['title'] = $field['label'];
1715 $customFields["custom_{$k}"] = $field;
1716 }
1717 }
1718 }
1719
1720 $members = [['member_id', '=', $membership->id, 0, 0]];
1721 // check whether its a test drive
a090fe98 1722 if ($this->_mode === 'test') {
efc0e24a 1723 $members[] = ['member_test', '=', 1, 0, 0];
1724 }
1725
1726 CRM_Core_BAO_UFGroup::getValues($formValues['contact_id'], $customFields, $customValues, FALSE, $members);
1727 return $customValues;
829df55e 1728 }
1729
ebf7e65f 1730 /**
1731 * Get the selected memberships as a string of labels.
1732 *
1733 * @return string
1734 */
1735 protected function getSelectedMembershipLabels(): string {
1736 $return = [];
1737 foreach ($this->_memTypeSelected as $membershipTypeID) {
1738 $return[] = $this->allMembershipTypeDetails[$membershipTypeID]['name'];
1739 }
1740 return implode(', ', $return);
1741 }
1742
8a594d37 1743 /**
1744 * Legacy contribution processing function.
1745 *
1746 * This is copied from a shared function in order to clean it up. Most of the
1747 * stuff in it, maybe all except the ContributionRecur create is
1748 * not applicable to this form & can be removed in follow up cleanup.
1749 *
1750 * It's like the contribution create being done here is actively bad and
1751 * being fixed later.
1752 *
8a594d37 1753 * @param array $params
8a594d37 1754 * @param array $contributionParams
1755 * Parameters to be passed to contribution create action.
1756 * This differs from params in that we are currently adding params to it and 1) ensuring they are being
1757 * passed consistently & 2) documenting them here.
1758 * - contact_id
1759 * - line_item
1760 * - is_test
1761 * - campaign_id
8a594d37 1762 * - source
1763 * - payment_type_id
8a594d37 1764 *
8a594d37 1765 * @return \CRM_Contribute_DAO_Contribution
1766 *
1767 * @throws \CRM_Core_Exception
1768 * @throws \CiviCRM_API3_Exception
1769 */
2a320359 1770 protected function processContribution(
8a594d37 1771 $params,
f8bcbc1f 1772 $contributionParams
8a594d37 1773 ) {
8a594d37 1774 $contactID = $contributionParams['contact_id'];
84f3d874 1775 $contributionParams['contribution_recur_id'] = $this->legacyProcessRecurringContribution($params, $contactID);
5072c894 1776 return CRM_Contribute_BAO_Contribution::add($contributionParams);
8a594d37 1777 }
1778
66c8ea62 1779 /**
84f3d874 1780 * Create the recurring contribution record if the form submission requires it.
66c8ea62 1781 *
1782 * This function was copied from another form & needs cleanup.
1783 *
1784 * @param array $params
1785 * @param int $contactID
66c8ea62 1786 *
84f3d874 1787 * @return int|null
28d43453 1788 * @throws \CiviCRM_API3_Exception
66c8ea62 1789 */
84f3d874 1790 protected function legacyProcessRecurringContribution(array $params, $contactID): ?int {
1791 if (!$this->isCreateRecurringContribution()) {
1792 return NULL;
1793 }
66c8ea62 1794 $recurParams = ['contact_id' => $contactID];
28d43453 1795 $recurParams['amount'] = $this->order->getTotalAmount();
66c8ea62 1796 $recurParams['auto_renew'] = $params['auto_renew'] ?? NULL;
1797 $recurParams['frequency_unit'] = $params['frequency_unit'] ?? NULL;
1798 $recurParams['frequency_interval'] = $params['frequency_interval'] ?? NULL;
1799 $recurParams['installments'] = $params['installments'] ?? NULL;
be2e79c8 1800 $recurParams['financial_type_id'] = $this->getFinancialTypeID();
28d43453 1801 $recurParams['currency'] = $this->getCurrency();
c2b07971 1802 $recurParams['payment_instrument_id'] = $this->getPaymentInstrumentID();
65e42c27 1803 $recurParams['is_test'] = $this->isTest();
66c8ea62 1804
42347843 1805 $recurParams['start_date'] = $recurParams['create_date'] = $recurParams['modified_date'] = CRM_Utils_Time::date('YmdHis');
66c8ea62 1806 if (!empty($params['receive_date'])) {
ccd459b8 1807 $recurParams['start_date'] = date('YmdHis', CRM_Utils_Time::strtotime($params['receive_date']));
66c8ea62 1808 }
66de72bc 1809 $recurParams['invoice_id'] = $this->getInvoiceID();
66c8ea62 1810 $recurParams['contribution_status_id'] = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending');
1811 $recurParams['payment_processor_id'] = $params['payment_processor_id'] ?? NULL;
6aef1389 1812 $recurParams['is_email_receipt'] = (bool) $this->getSubmittedValue('send_receipt');
66c8ea62 1813 // we need to add a unique trxn_id to avoid a unique key error
1814 // in paypal IPN we reset this when paypal sends us the real trxn id, CRM-2991
66de72bc 1815 $recurParams['trxn_id'] = $params['trxn_id'] ?? $this->getInvoiceID();
a628c77b 1816 $recurParams['campaign_id'] = $this->getSubmittedValue('campaign_id');
e659743e 1817 return CRM_Contribute_BAO_ContributionRecur::add($recurParams)->id;
66c8ea62 1818 }
1819
65e42c27 1820 /**
1821 * Is the form being submitted in test mode.
1822 *
1823 * @return bool
1824 */
28d43453 1825 protected function isTest(): bool {
65e42c27 1826 return ($this->_mode === 'test') ? TRUE : FALSE;
1827 }
1828
be2e79c8 1829 /**
1830 * Get the financial type id relevant to the contribution.
1831 *
1832 * Financial type id is optional when price sets are in use.
1833 * Otherwise they are required for the form to submit.
1834 *
1835 * @return int
1836 */
1837 protected function getFinancialTypeID(): int {
1838 return (int) $this->getSubmittedValue('financial_type_id') ?: $this->order->getFinancialTypeID();
1839 }
1840
d04c9885 1841 /**
1842 * Get values that should be passed to all membership create actions.
1843 *
1844 * These parameters are generic to all memberships created from the form,
1845 * whether a single membership or multiple by price set (although
1846 * the form will not expose all in the latter case.
1847 *
1848 * By referencing the submitted values directly we can call this
1849 * from anywhere in postProcess and get the same result (protects
1850 * against breakage if code is moved around).
1851 *
1852 * @return array
1853 */
1854 protected function getFormMembershipParams(): array {
1855 $submittedValues = $this->controller->exportValues($this->_name);
1856 return [
1857 'status_id' => $this->getSubmittedValue('status_id'),
1858 'source' => $this->getSubmittedValue('source'),
1859 'is_override' => $this->getSubmittedValue('is_override'),
1860 'status_override_end_date' => $this->getSubmittedValue('status_override_end_date'),
1861 'campaign_id' => $this->getSubmittedValue('campaign_id'),
1862 'custom' => CRM_Core_BAO_CustomField::postProcess($submittedValues,
1863 $this->_id,
1864 'Membership'
1865 ),
1866 // fix for CRM-3724
1867 // when is_override false ignore is_admin statuses during membership
1868 // status calculation. similarly we did fix for import in CRM-3570.
1869 'exclude_is_admin' => !$this->getSubmittedValue('is_override'),
1870 ];
1871 }
1872
84f3d874 1873 /**
1874 * Is it necessary to create a recurring contribution.
1875 *
1876 * @return bool
1877 */
1878 protected function isCreateRecurringContribution(): bool {
1879 return $this->_mode && $this->getSubmittedValue('auto_renew');
1880 }
1881
6a488035 1882}