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