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