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