Merge pull request #18427 from mattwire/membershipbackendcached
[civicrm-core.git] / CRM / Member / BAO / Membership.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
bc77d7c0 4 | Copyright CiviCRM LLC. All rights reserved. |
6a488035 5 | |
bc77d7c0
TO
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
6a488035 9 +--------------------------------------------------------------------+
d25dd0ee 10 */
6a488035
TO
11
12/**
13 *
14 * @package CRM
ca5cec67 15 * @copyright CiviCRM LLC https://civicrm.org/licensing
6a488035
TO
16 */
17class CRM_Member_BAO_Membership extends CRM_Member_DAO_Membership {
18
19 /**
fe482240 20 * Static field for all the membership information that we can potentially import.
6a488035
TO
21 *
22 * @var array
6a488035 23 */
971e129b 24 public static $_importableFields = NULL;
8ef12e64 25
971e129b 26 public static $_renewalActType = NULL;
8ef12e64 27
971e129b 28 public static $_signupActType = NULL;
8ef12e64 29
bb3a214a 30 /**
fe482240 31 * Class constructor.
bb3a214a 32 */
00be9182 33 public function __construct() {
6a488035
TO
34 parent::__construct();
35 }
36
37 /**
fe482240 38 * Takes an associative array and creates a membership object.
6a488035
TO
39 *
40 * the function extracts all the params it needs to initialize the created
41 * membership object. The params array could contain additional unused name/value
42 * pairs
43 *
b2363ea8
TO
44 * @param array $params
45 * (reference ) an assoc array of name/value pairs.
6a488035 46 *
16b10e64 47 * @return CRM_Member_BAO_Membership
aaa7b0e6 48 * @throws \CiviCRM_API3_Exception
6a488035 49 */
946a49bd 50 public static function add(&$params) {
a5e2b32e 51 $oldStatus = $oldType = NULL;
f880fd16
EM
52 if ($params['id']) {
53 CRM_Utils_Hook::pre('edit', 'Membership', $params['id'], $params);
54 }
55 else {
1acc24d5 56 CRM_Utils_Hook::pre('create', 'Membership', NULL, $params);
f880fd16
EM
57 }
58 $id = $params['id'];
59 // we do this after the hooks are called in case it has been altered
60 if ($id) {
61 $membershipObj = new CRM_Member_DAO_Membership();
62 $membershipObj->id = $id;
6a488035
TO
63 $membershipObj->find();
64 while ($membershipObj->fetch()) {
65 $oldStatus = $membershipObj->status_id;
66 $oldType = $membershipObj->membership_type_id;
67 }
68 }
6a488035
TO
69
70 if (array_key_exists('is_override', $params) && !$params['is_override']) {
71 $params['is_override'] = 'null';
72 }
73
74 $membership = new CRM_Member_BAO_Membership();
75 $membership->copyValues($params);
f880fd16 76 $membership->id = $id;
6a488035
TO
77
78 $membership->save();
6a488035 79
6a488035
TO
80 if (empty($membership->contact_id) || empty($membership->status_id)) {
81 // this means we are in renewal mode and are just updating the membership
82 // record or this is an API update call and all fields are not present in the update record
b204fd50 83 // however the hooks don't care and want all data CRM-7784
6a488035
TO
84 $tempMembership = new CRM_Member_DAO_Membership();
85 $tempMembership->id = $membership->id;
86 $tempMembership->find(TRUE);
87 $membership = $tempMembership;
88 }
89
90 //get the log start date.
91 //it is set during renewal of membership.
9c1bc317 92 $logStartDate = $params['log_start_date'] ?? NULL;
6a488035 93 $logStartDate = ($logStartDate) ? CRM_Utils_Date::isoToMysql($logStartDate) : CRM_Utils_Date::isoToMysql($membership->start_date);
e0556ebe 94 $values = self::getStatusANDTypeValues($membership->id);
6a488035 95
b2ae7d75 96 $membershipLog = [
6a488035
TO
97 'membership_id' => $membership->id,
98 'status_id' => $membership->status_id,
99 'start_date' => $logStartDate,
100 'end_date' => CRM_Utils_Date::isoToMysql($membership->end_date),
101 'modified_date' => date('Ymd'),
102 'membership_type_id' => $values[$membership->id]['membership_type_id'],
103 'max_related' => $membership->max_related,
b2ae7d75 104 ];
6a488035 105
6dbee28b 106 if (!empty($params['modified_id'])) {
107 $membershipLog['modified_id'] = $params['modified_id'];
108 }
6a488035 109 // If we have an authenticated session, set modified_id to that user's contact_id, else set to membership.contact_id
6dbee28b 110 elseif (CRM_Core_Session::singleton()->get('userID')) {
111 $membershipLog['modified_id'] = CRM_Core_Session::singleton()->get('userID');
6a488035 112 }
6a488035
TO
113 else {
114 $membershipLog['modified_id'] = $membership->contact_id;
115 }
116
4ed92e91 117 CRM_Member_BAO_MembershipLog::add($membershipLog);
6a488035
TO
118
119 // reset the group contact cache since smart groups might be affected due to this
2b68a50c 120 CRM_Contact_BAO_GroupContactCache::opportunisticCacheFlush();
6a488035 121
b6d493f3 122 $allStatus = CRM_Member_BAO_Membership::buildOptions('status_id', 'get');
b2ae7d75 123 $activityParams = [
699e088e 124 'status_id' => $params['membership_activity_status'] ?? 'Completed',
b2ae7d75 125 ];
126 if (in_array($allStatus[$membership->status_id], ['Pending', 'Grace'])) {
98f0683a 127 $activityParams['status_id'] = 'Scheduled';
b6d493f3 128 }
98f0683a 129 $activityParams['status_id'] = CRM_Core_PseudoConstant::getKey('CRM_Activity_BAO_Activity', 'activity_status_id', $activityParams['status_id']);
b6d493f3 130
d2460a89
MD
131 $targetContactID = $membership->contact_id;
132 if (!empty($params['is_for_organization'])) {
946a49bd 133 // @todo - deprecate is_for_organization, require modified_id
9c1bc317 134 $targetContactID = $params['modified_id'] ?? NULL;
d2460a89 135 }
9563db0d
SL
136
137 // add custom field values
138 if (!empty($params['custom']) && is_array($params['custom'])
139 ) {
140 CRM_Core_BAO_CustomValueTable::store($params['custom'], 'civicrm_membership', $membership->id);
141 }
142
f880fd16 143 if ($id) {
6a488035 144 if ($membership->status_id != $oldStatus) {
d2460a89
MD
145 CRM_Activity_BAO_Activity::addActivity($membership,
146 'Change Membership Status',
147 NULL,
b2ae7d75 148 [
d2460a89
MD
149 'subject' => "Status changed from {$allStatus[$oldStatus]} to {$allStatus[$membership->status_id]}",
150 'source_contact_id' => $membershipLog['modified_id'],
66a1e31f 151 'priority_id' => 'Normal',
b2ae7d75 152 ]
6a488035 153 );
6a488035
TO
154 }
155 if (isset($membership->membership_type_id) && $membership->membership_type_id != $oldType) {
4d63cfde 156 $membershipTypes = CRM_Member_BAO_Membership::buildOptions('membership_type_id', 'get');
d2460a89
MD
157 CRM_Activity_BAO_Activity::addActivity($membership,
158 'Change Membership Type',
159 NULL,
b2ae7d75 160 [
d2460a89
MD
161 'subject' => "Type changed from {$membershipTypes[$oldType]} to {$membershipTypes[$membership->membership_type_id]}",
162 'source_contact_id' => $membershipLog['modified_id'],
66a1e31f 163 'priority_id' => 'Normal',
b2ae7d75 164 ]
6a488035 165 );
d2460a89 166 }
b6d493f3 167
b2ae7d75 168 foreach (['Membership Signup', 'Membership Renewal'] as $activityType) {
699e088e
MW
169 $activityParams['id'] = civicrm_api3('Activity', 'Get', [
170 'source_record_id' => $membership->id,
171 'activity_type_id' => $activityType,
172 'status_id' => 'Scheduled',
173 ])['id'] ?? NULL;
b6d493f3
MD
174 // 1. Update Schedule Membership Signup/Renwal activity to completed on successful payment of pending membership
175 // 2. OR Create renewal activity scheduled if its membership renewal will be paid later
c329a76a 176 if (!empty($params['membership_activity_status']) && (!empty($activityParams['id']) || $activityType == 'Membership Renewal')) {
b6d493f3
MD
177 CRM_Activity_BAO_Activity::addActivity($membership, $activityType, $targetContactID, $activityParams);
178 break;
179 }
6a488035 180 }
b6d493f3 181
6a488035
TO
182 CRM_Utils_Hook::post('edit', 'Membership', $membership->id, $membership);
183 }
184 else {
b6d493f3 185 CRM_Activity_BAO_Activity::addActivity($membership, 'Membership Signup', $targetContactID, $activityParams);
6a488035
TO
186 CRM_Utils_Hook::post('create', 'Membership', $membership->id, $membership);
187 }
188
189 return $membership;
190 }
191
192 /**
7b835f7c 193 * Fetch the object and store the values in the values array.
6a488035 194 *
b2363ea8
TO
195 * @param array $params
196 * Input parameters to find object.
197 * @param array $values
198 * Output values of the object.
199 * @param bool $active
200 * Do you want only active memberships to.
6a488035 201 * be returned
7b835f7c
EM
202 *
203 * @return CRM_Member_BAO_Membership|null
204 * The found object or null
6a488035 205 */
58e4c87b 206 public static function &getValues(&$params, &$values, $active = FALSE) {
6a488035
TO
207 if (empty($params)) {
208 return NULL;
209 }
210 $membership = new CRM_Member_BAO_Membership();
211
212 $membership->copyValues($params);
213 $membership->find();
b2ae7d75 214 $memberships = [];
6a488035
TO
215 while ($membership->fetch()) {
216 if ($active &&
217 (!CRM_Core_DAO::getFieldValue('CRM_Member_DAO_MembershipStatus',
e0556ebe
TO
218 $membership->status_id,
219 'is_current_member'
220 ))
6a488035
TO
221 ) {
222 continue;
223 }
224
225 CRM_Core_DAO::storeValues($membership, $values[$membership->id]);
226 $memberships[$membership->id] = $membership;
227 }
228
229 return $memberships;
230 }
231
232 /**
fe482240 233 * Takes an associative array and creates a membership object.
6a488035 234 *
b2363ea8
TO
235 * @param array $params
236 * (reference ) an assoc array of name/value pairs.
237 * @param array $ids
13a16f43 238 * Deprecated parameter The array that holds all the db ids.
6a488035 239 *
906e6120 240 * @return CRM_Member_BAO_Membership|CRM_Core_Error
2b1f6808 241 * @throws \CiviCRM_API3_Exception
242 *
243 * @throws CRM_Core_Exception
6a488035 244 */
2b1f6808 245 public static function create(&$params, &$ids = []) {
6a488035
TO
246 // always calculate status if is_override/skipStatusCal is not true.
247 // giving respect to is_override during import. CRM-4012
248
249 // To skip status calculation we should use 'skipStatusCal'.
250 // eg pay later membership, membership update cron CRM-3984
251
8cc574cf 252 if (empty($params['is_override']) && empty($params['skipStatusCal'])) {
e6d0c736 253 // @todo - we should be able to count on dates being correctly formatted by they time they hit the BAO.
254 // Maybe do some tests & throw some deprecation warnings if they aren't?
255 $params['start_date'] = trim($params['start_date']) ? date('Ymd', strtotime(trim($params['start_date']))) : 'null';
256 $params['end_date'] = trim($params['end_date']) ? date('Ymd', strtotime(trim($params['end_date']))) : 'null';
257 $params['join_date'] = trim($params['join_date']) ? date('Ymd', strtotime(trim($params['join_date']))) : 'null';
6a488035
TO
258
259 //fix for CRM-3570, during import exclude the statuses those having is_admin = 1
699e088e 260 $excludeIsAdmin = $params['exclude_is_admin'] ?? FALSE;
6a488035
TO
261
262 //CRM-3724 always skip is_admin if is_override != true.
8cc574cf 263 if (!$excludeIsAdmin && empty($params['is_override'])) {
6a488035
TO
264 $excludeIsAdmin = TRUE;
265 }
266
e6d0c736 267 $calcStatus = CRM_Member_BAO_MembershipStatus::getMembershipStatusByDate($params['start_date'], $params['end_date'], $params['join_date'],
2cb64970 268 'now', $excludeIsAdmin, $params['membership_type_id'] ?? NULL, $params
6a488035
TO
269 );
270 if (empty($calcStatus)) {
e6d0c736 271 throw new CRM_Core_Exception(ts("The membership cannot be saved because the status cannot be calculated for start_date: {$params['start_date']} end_date {$params['end_date']} join_date {$params['join_date']} as at " . date('Y-m-d H:i:s')));
6a488035
TO
272 }
273 $params['status_id'] = $calcStatus['id'];
274 }
275
276 // data cleanup only: all verifications on number of related memberships are done upstream in:
7b835f7c
EM
277 // CRM_Member_BAO_Membership::createRelatedMemberships()
278 // CRM_Contact_BAO_Relationship::relatedMemberships()
039f3e71 279 if (!empty($params['owner_membership_id'])) {
6a488035 280 unset($params['max_related']);
8efea814
EM
281 }
282 else {
6a488035
TO
283 // if membership allows related, default max_related to value in membership_type
284 if (!array_key_exists('max_related', $params) && !empty($params['membership_type_id'])) {
285 $membershipType = CRM_Member_BAO_MembershipType::getMembershipTypeDetails($params['membership_type_id']);
286 if (isset($membershipType['relationship_type_id'])) {
9c1bc317 287 $params['max_related'] = $membershipType['max_related'] ?? NULL;
6a488035
TO
288 }
289 }
290 }
291
292 $transaction = new CRM_Core_Transaction();
293
699e088e 294 $params['id'] = $params['id'] ?? $ids['membership'] ?? NULL;
946a49bd 295 $membership = self::add($params);
6a488035
TO
296
297 if (is_a($membership, 'CRM_Core_Error')) {
298 $transaction->rollback();
299 return $membership;
300 }
301
6a488035 302 $params['membership_id'] = $membership->id;
f57cb50c 303 // @todo further cleanup required to remove use of $ids['contribution'] from here
6a488035
TO
304 if (isset($ids['membership'])) {
305 $ids['contribution'] = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_MembershipPayment',
08fd4b45 306 $membership->id,
6a488035
TO
307 'contribution_id',
308 'membership_id'
309 );
f57cb50c
MWMC
310 // @todo this is a temporary step to removing $ids['contribution'] completely
311 if (empty($params['contribution_id']) && !empty($ids['contribution'])) {
312 $params['contribution_id'] = $ids['contribution'];
313 }
6a488035 314 }
3c0201c9 315
f57cb50c 316 // This code ensures a line item is created but it is recommended you pass in 'skipLineItem' or 'line_item'
356bfcaf 317 if (empty($params['line_item']) && !empty($params['membership_type_id']) && empty($params['skipLineItem'])) {
318 CRM_Price_BAO_LineItem::getLineItemArray($params, NULL, 'membership', $params['membership_type_id']);
319 }
d5b95619 320 $params['skipLineItem'] = TRUE;
3c0201c9 321
976e393c 322 // Record contribution for this membership and create a MembershipPayment
8cc574cf 323 if (!empty($params['contribution_status_id']) && empty($params['relate_contribution_id'])) {
b2ae7d75 324 $memInfo = array_merge($params, ['membership_id' => $membership->id]);
f57cb50c 325 $params['contribution'] = self::recordMembershipContribution($memInfo);
6a488035 326 }
3c0201c9 327
976e393c
MWMC
328 // Add/update MembershipPayment record for this membership if it is a related contribution
329 if (!empty($params['relate_contribution_id'])) {
330 $membershipPaymentParams = [
331 'membership_id' => $membership->id,
332 'membership_type_id' => $membership->membership_type_id,
333 'contribution_id' => $params['relate_contribution_id'],
334 ];
335 civicrm_api3('MembershipPayment', 'create', $membershipPaymentParams);
336 }
337
82cc6775
PN
338 if (!empty($params['lineItems'])) {
339 $params['line_item'] = $params['lineItems'];
340 }
341
f57cb50c 342 // do cleanup line items if membership edit the Membership type.
82cc6775
PN
343 if (empty($ids['contribution']) && !empty($ids['membership'])) {
344 CRM_Price_BAO_LineItem::deleteLineItems($ids['membership'], 'civicrm_membership');
345 }
0dea0c7c 346 // @todo - we should ONLY do the below if a contribution is created. Let's
347 // get some deprecation notices in here & see where it's hit & work to eliminate.
13a16f43 348 // This could happen if there is no contribution or we are in one of many
349 // weird and wonderful flows. This is scary code. Keep adding tests.
c80e6652 350 if (!empty($params['line_item']) && empty($ids['contribution']) && empty($params['contribution_id'])) {
13a16f43 351
352 foreach ($params['line_item'] as $priceSetId => $lineItems) {
353 foreach ($lineItems as $lineIndex => $lineItem) {
9c1bc317 354 $lineMembershipType = $lineItem['membership_type_id'] ?? NULL;
de6c59ca 355 if (!empty($params['contribution'])) {
13a16f43 356 $params['line_item'][$priceSetId][$lineIndex]['contribution_id'] = $params['contribution']->id;
357 }
699e088e 358 if ($lineMembershipType && $lineMembershipType == ($params['membership_type_id'] ?? NULL)) {
13a16f43 359 $params['line_item'][$priceSetId][$lineIndex]['entity_id'] = $membership->id;
360 $params['line_item'][$priceSetId][$lineIndex]['entity_table'] = 'civicrm_membership';
361 }
de6c59ca 362 elseif (!$lineMembershipType && !empty($params['contribution'])) {
13a16f43 363 $params['line_item'][$priceSetId][$lineIndex]['entity_id'] = $params['contribution']->id;
364 $params['line_item'][$priceSetId][$lineIndex]['entity_table'] = 'civicrm_contribution';
365 }
366 }
367 }
356bfcaf 368 CRM_Price_BAO_LineItem::processPriceSet(
369 $membership->id,
370 $params['line_item'],
699e088e 371 $params['contribution'] ?? NULL
356bfcaf 372 );
d5b95619 373 }
6a488035 374
6a488035
TO
375 $transaction->commit();
376
377 self::createRelatedMemberships($params, $membership);
378
a7488080 379 if (empty($params['skipRecentView'])) {
26f180f4
MW
380 self::addToRecentItems($membership);
381 }
6a488035 382
26f180f4
MW
383 return $membership;
384 }
6a488035 385
26f180f4
MW
386 /**
387 * @param \CRM_Member_DAO_Membership $membership
388 */
389 private static function addToRecentItems($membership) {
390 $url = CRM_Utils_System::url('civicrm/contact/view/membership',
391 "action=view&reset=1&id={$membership->id}&cid={$membership->contact_id}&context=home"
392 );
393 if (empty($membership->membership_type_id)) {
394 // ie in an update situation.
395 $membership->find(TRUE);
396 }
a89e0a3e 397 $title = CRM_Contact_BAO_Contact::displayName($membership->contact_id) . ' - ' . ts('Membership Type:')
398 . ' ' . CRM_Core_PseudoConstant::getLabel('CRM_Member_BAO_Membership', 'membership_type_id', $membership->membership_type_id);
26f180f4
MW
399
400 $recentOther = [];
401 if (CRM_Core_Permission::checkActionPermission('CiviMember', CRM_Core_Action::UPDATE)) {
402 $recentOther['editUrl'] = CRM_Utils_System::url('civicrm/contact/view/membership',
403 "action=update&reset=1&id={$membership->id}&cid={$membership->contact_id}&context=home"
404 );
405 }
406 if (CRM_Core_Permission::checkActionPermission('CiviMember', CRM_Core_Action::DELETE)) {
407 $recentOther['deleteUrl'] = CRM_Utils_System::url('civicrm/contact/view/membership',
408 "action=delete&reset=1&id={$membership->id}&cid={$membership->contact_id}&context=home"
6a488035
TO
409 );
410 }
411
26f180f4
MW
412 // add the recently created Membership
413 CRM_Utils_Recent::add($title,
414 $url,
415 $membership->id,
416 'Membership',
417 $membership->contact_id,
418 NULL,
419 $recentOther
420 );
6a488035
TO
421 }
422
423 /**
fe482240 424 * Check the membership extended through relationship.
6a488035 425 *
5c3a7abf
AP
426 * @param int $membershipTypeID
427 * Membership type id.
b2363ea8
TO
428 * @param int $contactId
429 * Contact id.
ada8a833 430 *
b2363ea8 431 * @param int $action
6a488035 432 *
608e6658 433 * @return array
a6c01b45 434 * array of contact_id of all related contacts.
923dfabb 435 *
436 * @throws \CRM_Core_Exception
437 * @throws \CiviCRM_API3_Exception
6a488035 438 */
5c3a7abf 439 public static function checkMembershipRelationship($membershipTypeID, $contactId, $action = CRM_Core_Action::ADD) {
b2ae7d75 440 $contacts = [];
6a488035
TO
441
442 $membershipType = CRM_Member_BAO_MembershipType::getMembershipTypeDetails($membershipTypeID);
b2ae7d75 443 $relationships = [];
6a488035
TO
444 if (isset($membershipType['relationship_type_id'])) {
445 $relationships = CRM_Contact_BAO_Relationship::getRelationship($contactId,
446 CRM_Contact_BAO_Relationship::CURRENT
447 );
448 if ($action & CRM_Core_Action::UPDATE) {
449 $pastRelationships = CRM_Contact_BAO_Relationship::getRelationship($contactId,
450 CRM_Contact_BAO_Relationship::PAST
451 );
452 $relationships = array_merge($relationships, $pastRelationships);
453 }
454 }
455
456 if (!empty($relationships)) {
457 // check for each contact relationships
458 foreach ($relationships as $values) {
459 //get details of the relationship type
b2ae7d75 460 $relType = ['id' => $values['civicrm_relationship_type_id']];
461 $relValues = [];
6a488035
TO
462 CRM_Contact_BAO_RelationshipType::retrieve($relType, $relValues);
463 // Check if contact's relationship type exists in membership type
b2ae7d75 464 $relTypeDirs = [];
e0556ebe 465 $relTypeIds = explode(CRM_Core_DAO::VALUE_SEPARATOR, $membershipType['relationship_type_id']);
6a488035
TO
466 $relDirections = explode(CRM_Core_DAO::VALUE_SEPARATOR, $membershipType['relationship_direction']);
467 $bidirectional = FALSE;
468 foreach ($relTypeIds as $key => $value) {
469 $relTypeDirs[] = $value . '_' . $relDirections[$key];
470 if (in_array($value, $relType) &&
471 $relValues['name_a_b'] == $relValues['name_b_a']
472 ) {
473 $bidirectional = TRUE;
474 break;
475 }
476 }
477 $relTypeDir = $values['civicrm_relationship_type_id'] . '_' . $values['rtype'];
478 if ($bidirectional || in_array($relTypeDir, $relTypeDirs)) {
479 // $values['status'] is going to have value for
480 // current or past relationships.
481 $contacts[$values['cid']] = $values['status'];
482 }
483 }
484 }
485
486 // Sort by contact_id ascending
487 ksort($contacts);
488 return $contacts;
489 }
490
491 /**
fe482240
EM
492 * Retrieve DB object based on input parameters.
493 *
494 * It also stores all the retrieved values in the default array.
6a488035 495 *
b2363ea8
TO
496 * @param array $params
497 * (reference ) an assoc array of name/value pairs.
498 * @param array $defaults
499 * (reference ) an assoc array to hold the name / value pairs.
6a488035 500 * in a hierarchical manner
8efea814 501 *
16b10e64 502 * @return CRM_Member_BAO_Membership
6a488035 503 */
00be9182 504 public static function retrieve(&$params, &$defaults) {
6a488035
TO
505 $membership = new CRM_Member_DAO_Membership();
506
507 $membership->copyValues($params);
508
509 if ($membership->find(TRUE)) {
510 CRM_Core_DAO::storeValues($membership, $defaults);
511
512 //get the membership status and type values.
513 $statusANDType = self::getStatusANDTypeValues($membership->id);
aaa7b0e6 514 foreach (['status', 'membership_type'] as $fld) {
9c1bc317 515 $defaults[$fld] = $statusANDType[$membership->id][$fld] ?? NULL;
6a488035 516 }
a7488080 517 if (!empty($statusANDType[$membership->id]['is_current_member'])) {
6a488035
TO
518 $defaults['active'] = TRUE;
519 }
520
6a488035
TO
521 return $membership;
522 }
523
524 return NULL;
525 }
526
527 /**
7b835f7c 528 * Get membership status and membership type values.
6a488035 529 *
b2363ea8
TO
530 * @param int $membershipId
531 * Membership id of values to return.
6a488035 532 *
a6c01b45 533 * @return array
16b10e64 534 * Array of key value pairs
6a488035 535 */
00be9182 536 public static function getStatusANDTypeValues($membershipId) {
b2ae7d75 537 $values = [];
6a488035
TO
538 if (!$membershipId) {
539 return $values;
540 }
541 $sql = '
542 SELECT membership.id as id,
543 status.id as status_id,
544 status.label as status,
545 status.is_current_member as is_current_member,
546 type.id as membership_type_id,
547 type.name as membership_type,
548 type.relationship_type_id as relationship_type_id
549 FROM civicrm_membership membership
550INNER JOIN civicrm_membership_status status ON ( status.id = membership.status_id )
551INNER JOIN civicrm_membership_type type ON ( type.id = membership.membership_type_id )
552 WHERE membership.id = %1';
b2ae7d75 553 $dao = CRM_Core_DAO::executeQuery($sql, [1 => [$membershipId, 'Positive']]);
554 $properties = [
e0556ebe
TO
555 'status',
556 'status_id',
557 'membership_type',
558 'membership_type_id',
559 'is_current_member',
21dfd5f5 560 'relationship_type_id',
b2ae7d75 561 ];
6a488035
TO
562 while ($dao->fetch()) {
563 foreach ($properties as $property) {
564 $values[$dao->id][$property] = $dao->$property;
565 }
566 }
567
568 return $values;
569 }
570
3506b6cd 571 /**
100fef9d 572 * Delete membership.
7b835f7c 573 *
f5e53870 574 * Wrapper for most delete calls. Use this unless you JUST want to delete related memberships w/o deleting the parent.
3506b6cd 575 *
b2363ea8
TO
576 * @param int $membershipId
577 * Membership id that needs to be deleted.
971e129b 578 * @param bool $preserveContrib
3506b6cd 579 *
7b835f7c
EM
580 * @return int
581 * Id of deleted Membership on success, false otherwise.
3506b6cd 582 */
086fea8e 583 public static function del($membershipId, $preserveContrib = FALSE) {
3506b6cd
DG
584 //delete related first and then delete parent.
585 self::deleteRelatedMemberships($membershipId);
ed4cc29d 586 return self::deleteMembership($membershipId, $preserveContrib);
3506b6cd 587 }
d824fb6e 588
6a488035 589 /**
100fef9d 590 * Delete membership.
6a488035 591 *
b2363ea8
TO
592 * @param int $membershipId
593 * Membership id that needs to be deleted.
971e129b 594 * @param bool $preserveContrib
6a488035 595 *
7b835f7c
EM
596 * @return int
597 * Id of deleted Membership on success, false otherwise.
6a488035 598 */
086fea8e 599 public static function deleteMembership($membershipId, $preserveContrib = FALSE) {
46d8f506 600 // CRM-12147, retrieve membership data before we delete it for hooks
b2ae7d75 601 $params = ['id' => $membershipId];
602 $memValues = [];
46d8f506 603 $memberships = self::getValues($params, $memValues);
3506b6cd 604
46d8f506
DL
605 $membership = $memberships[$membershipId];
606
607 CRM_Utils_Hook::pre('delete', 'Membership', $membershipId, $memValues);
6a488035
TO
608
609 $transaction = new CRM_Core_Transaction();
610
611 $results = NULL;
612 //delete activity record
613 $activityTypes = CRM_Core_PseudoConstant::activityType(TRUE, FALSE, FALSE, 'name');
614
b2ae7d75 615 $params = [];
e0556ebe 616 $deleteActivity = FALSE;
b2ae7d75 617 $membershipActivities = [
46d8f506
DL
618 'Membership Signup',
619 'Membership Renewal',
620 'Change Membership Status',
621 'Change Membership Type',
21dfd5f5 622 'Membership Renewal Reminder',
b2ae7d75 623 ];
e0556ebe 624 foreach ($membershipActivities as $membershipActivity) {
6a488035
TO
625 $activityId = array_search($membershipActivity, $activityTypes);
626 if ($activityId) {
627 $params['activity_type_id'][] = $activityId;
e0556ebe 628 $deleteActivity = TRUE;
6a488035
TO
629 }
630 }
631 if ($deleteActivity) {
632 $params['source_record_id'] = $membershipId;
46d8f506 633 CRM_Activity_BAO_Activity::deleteActivity($params);
6a488035 634 }
ed4cc29d 635 self::deleteMembershipPayment($membershipId, $preserveContrib);
2883b934 636 CRM_Price_BAO_LineItem::deleteLineItems($membershipId, 'civicrm_membership');
6a488035 637
d0fbc816 638 $results = $membership->delete();
6a488035
TO
639 $transaction->commit();
640
641 CRM_Utils_Hook::post('delete', 'Membership', $membership->id, $membership);
642
643 // delete the recently created Membership
b2ae7d75 644 $membershipRecent = [
6a488035
TO
645 'id' => $membershipId,
646 'type' => 'Membership',
b2ae7d75 647 ];
6a488035
TO
648 CRM_Utils_Recent::del($membershipRecent);
649
650 return $results;
651 }
652
3506b6cd 653 /**
fe482240 654 * Delete related memberships.
3506b6cd
DG
655 *
656 * @param int $ownerMembershipId
657 * @param int $contactId
658 *
659 * @return null
3506b6cd 660 */
00be9182 661 public static function deleteRelatedMemberships($ownerMembershipId, $contactId = NULL) {
3506b6cd 662 if (!$ownerMembershipId && !$contactId) {
608e6658 663 return FALSE;
3506b6cd
DG
664 }
665
666 $membership = new CRM_Member_DAO_Membership();
667 $membership->owner_membership_id = $ownerMembershipId;
668
669 if ($contactId) {
670 $membership->contact_id = $contactId;
671 }
672
673 $membership->find();
674 while ($membership->fetch()) {
675 //delete related first and then delete parent.
676 self::deleteRelatedMemberships($membership->id);
677 self::deleteMembership($membership->id);
678 }
3506b6cd
DG
679 }
680
6a488035 681 /**
100fef9d 682 * Obtain active/inactive memberships from the list of memberships passed to it.
6a488035 683 *
b2363ea8
TO
684 * @param array $memberships
685 * Membership records.
686 * @param string $status
687 * Active or inactive.
6a488035 688 *
a6c01b45
CW
689 * @return array
690 * array of memberships based on status
6a488035 691 */
00be9182 692 public static function activeMembers($memberships, $status = 'active') {
b2ae7d75 693 $actives = [];
6a488035
TO
694 if ($status == 'active') {
695 foreach ($memberships as $f => $v) {
a7488080 696 if (!empty($v['active'])) {
6a488035
TO
697 $actives[$f] = $v;
698 }
699 }
700 return $actives;
701 }
702 elseif ($status == 'inactive') {
703 foreach ($memberships as $f => $v) {
a7488080 704 if (empty($v['active'])) {
6a488035
TO
705 $actives[$f] = $v;
706 }
707 }
708 return $actives;
709 }
710 return NULL;
711 }
712
6a488035 713 /**
fe482240 714 * Return Membership Block info in Contribution Pages.
6a488035 715 *
b2363ea8
TO
716 * @param int $pageID
717 * Contribution page id.
e46e9a0b
EM
718 *
719 * @return array|null
6a488035 720 */
00be9182 721 public static function getMembershipBlock($pageID) {
b2ae7d75 722 $membershipBlock = [];
e0556ebe 723 $dao = new CRM_Member_DAO_MembershipBlock();
6a488035
TO
724 $dao->entity_table = 'civicrm_contribution_page';
725
726 $dao->entity_id = $pageID;
727 $dao->is_active = 1;
728 if ($dao->find(TRUE)) {
729 CRM_Core_DAO::storeValues($dao, $membershipBlock);
a7488080 730 if (!empty($membershipBlock['membership_types'])) {
f24846d5 731 $membershipTypes = CRM_Utils_String::unserialize($membershipBlock['membership_types']);
6a488035
TO
732 if (!is_array($membershipTypes)) {
733 return $membershipBlock;
734 }
b2ae7d75 735 $memTypes = [];
6a488035
TO
736 foreach ($membershipTypes as $key => $value) {
737 $membershipBlock['auto_renew'][$key] = $value;
738 $memTypes[$key] = $key;
739 }
740 $membershipBlock['membership_types'] = implode(',', $memTypes);
741 }
742 }
743 else {
744 return NULL;
745 }
746
747 return $membershipBlock;
748 }
749
750 /**
fe482240 751 * Return a current membership of given contact.
7b835f7c 752 *
c490a46a 753 * NB: if more than one membership meets criteria, a randomly selected one is returned.
6a488035 754 *
b2363ea8
TO
755 * @param int $contactID
756 * Contact id.
757 * @param int $memType
758 * Membership type, null to retrieve all types.
6a488035 759 * @param int $isTest
b2363ea8
TO
760 * @param int $membershipId
761 * If provided, then determine if it is current.
762 * @param bool $onlySameParentOrg
763 * True if only Memberships with same parent org as the $memType wanted, false otherwise.
e46e9a0b
EM
764 *
765 * @return array|bool
aaa7b0e6 766 * @throws \CiviCRM_API3_Exception
6a488035 767 */
00be9182 768 public static function getContactMembership($contactID, $memType, $isTest, $membershipId = NULL, $onlySameParentOrg = FALSE) {
388d10d8
TC
769 //check for owner membership id, if it exists update that membership instead: CRM-15992
770 if ($membershipId) {
771 $ownerMemberId = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_Membership',
772 $membershipId,
773 'owner_membership_id', 'id'
774 );
775 if ($ownerMemberId) {
776 $membershipId = $ownerMemberId;
777 $contactID = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_Membership',
778 $membershipId,
779 'contact_id', 'id'
780 );
781 }
782 }
783
6a488035
TO
784 $dao = new CRM_Member_DAO_Membership();
785 if ($membershipId) {
786 $dao->id = $membershipId;
787 }
788 $dao->contact_id = $contactID;
789 $dao->membership_type_id = $memType;
790
791 //fetch proper membership record.
792 if ($isTest) {
793 $dao->is_test = $isTest;
794 }
795 else {
796 $dao->whereAdd('is_test IS NULL OR is_test = 0');
797 }
5624f515 798
6a488035 799 //avoid pending membership as current membership: CRM-3027
b2ae7d75 800 $statusIds = [array_search('Pending', CRM_Member_PseudoConstant::membershipStatus())];
b5a62499 801 if (!$membershipId) {
7ff60806
PN
802 // CRM-15475
803 $statusIds[] = array_search(
4c16123d 804 'Cancelled',
7ff60806 805 CRM_Member_PseudoConstant::membershipStatus(
4c16123d
EM
806 NULL,
807 " name = 'Cancelled' ",
808 'name',
809 FALSE,
7ff60806
PN
810 TRUE
811 )
812 );
b5a62499 813 }
e0556ebe 814 $dao->whereAdd('status_id NOT IN ( ' . implode(',', $statusIds) . ')');
5624f515 815
6a488035
TO
816 // order by start date to find most recent membership first, CRM-4545
817 $dao->orderBy('start_date DESC');
818
819 // CRM-8141
820 if ($onlySameParentOrg && $memType) {
821 // require the same parent org as the $memType
b2ae7d75 822 $params = ['id' => $memType];
823 $membershipType = [];
6a488035 824 if (CRM_Member_BAO_MembershipType::retrieve($params, $membershipType)) {
b2ae7d75 825 $memberTypesSameParentOrg = civicrm_api3('MembershipType', 'get', [
4c50ac3a 826 'member_of_contact_id' => $membershipType['member_of_contact_id'],
b2ae7d75 827 'options' => [
c8eada29 828 'limit' => 0,
b2ae7d75 829 ],
830 ]);
699e088e 831 $memberTypesSameParentOrgList = implode(',', array_keys($memberTypesSameParentOrg['values'] ?? []));
6a488035
TO
832 $dao->whereAdd('membership_type_id IN (' . $memberTypesSameParentOrgList . ')');
833 }
834 }
835
836 if ($dao->find(TRUE)) {
b2ae7d75 837 $membership = [];
6a488035
TO
838 CRM_Core_DAO::storeValues($dao, $membership);
839 $membership['is_current_member'] = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_MembershipStatus',
840 $membership['status_id'],
841 'is_current_member', 'id'
842 );
0829c697
TC
843 $ownerMemberId = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_Membership',
844 $membership['id'],
845 'owner_membership_id', 'id'
846 );
847 if ($ownerMemberId) {
848 $membership['id'] = $membership['membership_id'] = $ownerMemberId;
849 $membership['membership_contact_id'] = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_Membership',
850 $membership['id'],
851 'contact_id', 'id'
852 );
853 }
6a488035
TO
854 return $membership;
855 }
856
857 // CRM-8141
858 if ($onlySameParentOrg && $memType) {
859 // see if there is a membership that has same parent as $memType but different parent than $membershipID
5682e889 860 if ($dao->id && CRM_Core_Permission::check('edit memberships')) {
861 // CRM-10016, This is probably a backend renewal, and make sure we return the same membership thats being renewed.
e0556ebe 862 $dao->whereAdd();
5682e889 863 }
864 else {
865 unset($dao->id);
866 }
6a488035
TO
867
868 unset($dao->membership_type_id);
869 if ($dao->find(TRUE)) {
b2ae7d75 870 $membership = [];
6a488035
TO
871 CRM_Core_DAO::storeValues($dao, $membership);
872 $membership['is_current_member'] = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_MembershipStatus',
873 $membership['status_id'],
874 'is_current_member', 'id'
875 );
876 return $membership;
877 }
878 }
879 return FALSE;
880 }
881
882 /**
fe482240 883 * Combine all the importable fields from the lower levels object.
6a488035 884 *
b2363ea8
TO
885 * @param string $contactType
886 * Contact type.
887 * @param bool $status
6a488035 888 *
a6c01b45
CW
889 * @return array
890 * array of importable Fields
24a67831 891 * @throws \CRM_Core_Exception
6a488035 892 */
24a67831 893 public static function importableFields($contactType = 'Individual', $status = TRUE) {
894 $fields = Civi::cache('fields')->get('membership_importable_fields' . $contactType . $status);
895 if (!$fields) {
6a488035 896 if (!$status) {
b2ae7d75 897 $fields = ['' => ['title' => '- ' . ts('do not import') . ' -']];
6a488035
TO
898 }
899 else {
b2ae7d75 900 $fields = ['' => ['title' => '- ' . ts('Membership Fields') . ' -']];
6a488035
TO
901 }
902
903 $tmpFields = CRM_Member_DAO_Membership::import();
904 $contactFields = CRM_Contact_BAO_Contact::importableFields($contactType, NULL);
905
906 // Using new Dedupe rule.
b2ae7d75 907 $ruleParams = [
6a488035 908 'contact_type' => $contactType,
e0556ebe 909 'used' => 'Unsupervised',
b2ae7d75 910 ];
6a488035
TO
911 $fieldsArray = CRM_Dedupe_BAO_Rule::dedupeRuleFields($ruleParams);
912
b2ae7d75 913 $tmpContactField = [];
6a488035
TO
914 if (is_array($fieldsArray)) {
915 foreach ($fieldsArray as $value) {
916 $customFieldId = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField',
917 $value,
918 'id',
919 'column_name'
920 );
921 $value = $customFieldId ? 'custom_' . $customFieldId : $value;
9c1bc317 922 $tmpContactField[trim($value)] = $contactFields[trim($value)] ?? NULL;
6a488035
TO
923 if (!$status) {
924 $title = $tmpContactField[trim($value)]['title'] . " " . ts('(match to contact)');
925 }
926 else {
927 $title = $tmpContactField[trim($value)]['title'];
928 }
929 $tmpContactField[trim($value)]['title'] = $title;
930 }
931 }
932 $tmpContactField['external_identifier'] = $contactFields['external_identifier'];
24a67831 933 $tmpContactField['external_identifier']['title'] = $contactFields['external_identifier']['title'] . ' ' . ts('(match to contact)');
6a488035 934
24a67831 935 $tmpFields['membership_contact_id']['title'] .= ' ' . ts('(match to contact)');
6a488035
TO
936
937 $fields = array_merge($fields, $tmpContactField);
938 $fields = array_merge($fields, $tmpFields);
939 $fields = array_merge($fields, CRM_Core_BAO_CustomField::getFieldsForImport('Membership'));
24a67831 940 Civi::cache('fields')->set('membership_importable_fields' . $contactType . $status, $fields);
6a488035 941 }
24a67831 942 return $fields;
6a488035
TO
943 }
944
945 /**
fe482240 946 * Get all exportable fields.
6a488035 947 *
06d062ce 948 * @return array return array of all exportable fields
6a488035 949 */
00be9182 950 public static function &exportableFields() {
6a488035 951 $expFieldMembership = CRM_Member_DAO_Membership::export();
6a488035
TO
952
953 $expFieldsMemType = CRM_Member_DAO_MembershipType::export();
e0556ebe
TO
954 $fields = array_merge($expFieldMembership, $expFieldsMemType);
955 $fields = array_merge($fields, $expFieldMembership);
b2ae7d75 956 $membershipStatus = [
957 'membership_status' => [
e300cf31 958 'title' => ts('Membership Status'),
6a488035
TO
959 'name' => 'membership_status',
960 'type' => CRM_Utils_Type::T_STRING,
961 'where' => 'civicrm_membership_status.name',
b2ae7d75 962 ],
963 ];
6a488035
TO
964 //CRM-6161 fix for customdata export
965 $fields = array_merge($fields, $membershipStatus, CRM_Core_BAO_CustomField::getFieldsForImport('Membership'));
f243df22 966 $fields['membership_status_id'] = $membershipStatus['membership_status'];
6a488035
TO
967 return $fields;
968 }
969
970 /**
7b835f7c
EM
971 * Get membership joins/renewals for a specified membership type.
972 *
973 * Specifically, retrieves a count of memberships whose "Membership
4e636a74 974 * Signup" or "Membership Renewal" activity falls in the given date range.
8ef12e64 975 * Dates match the pattern "yyyy-mm-dd".
6a488035 976 *
b2363ea8
TO
977 * @param int $membershipTypeId
978 * Membership type id.
979 * @param int $startDate
980 * Date on which to start counting.
981 * @param int $endDate
982 * Date on which to end counting.
e46e9a0b
EM
983 * @param bool|int $isTest if true, membership is for a test site
984 * @param bool|int $isOwner if true, only retrieve membership records for owners //LCD
6a488035 985 *
df8d3074 986 * @return int
a6c01b45 987 * the number of members of type $membershipTypeId whose
688d37c6 988 * start_date is between $startDate and $endDate
6a488035 989 */
6a488035 990 public static function getMembershipStarts($membershipTypeId, $startDate, $endDate, $isTest = 0, $isOwner = 0) {
c969a1f2
SL
991 // Ensure that the dates that are passed to the query are in the format of yyyy-mm-dd
992 $dates = ['startDate', 'endDate'];
993 foreach ($dates as $date) {
994 if (strlen($$date) === 8) {
995 $$date = date('Y-m-d', strtotime($$date));
996 }
997 }
8ef12e64 998
4e636a74
AH
999 $testClause = 'membership.is_test = 1';
1000 if (!$isTest) {
1001 $testClause = '( membership.is_test IS NULL OR membership.is_test = 0 )';
1002 }
8ef12e64 1003
4e636a74
AH
1004 if (!self::$_signupActType || !self::$_renewalActType) {
1005 self::_getActTypes();
1006 }
8ef12e64 1007
4e636a74
AH
1008 if (!self::$_signupActType || !self::$_renewalActType) {
1009 return 0;
1010 }
1011
1012 $query = "
1013 SELECT COUNT(DISTINCT membership.id) as member_count
1014 FROM civicrm_membership membership
1015INNER JOIN civicrm_activity activity ON (activity.source_record_id = membership.id AND activity.activity_type_id in (%1, %2))
1016INNER JOIN civicrm_membership_status status ON ( membership.status_id = status.id AND status.is_current_member = 1 )
1017INNER JOIN civicrm_contact contact ON ( contact.id = membership.contact_id AND contact.is_deleted = 0 )
1018 WHERE membership.membership_type_id = %3
1019 AND activity.activity_date_time >= '$startDate' AND activity.activity_date_time <= '$endDate 23:59:59'
1020 AND {$testClause}";
8ef12e64 1021
6a488035 1022 $query .= ($isOwner) ? ' AND owner_membership_id IS NULL' : '';
4e636a74 1023
b2ae7d75 1024 $params = [
1025 1 => [self::$_signupActType, 'Integer'],
1026 2 => [self::$_renewalActType, 'Integer'],
1027 3 => [$membershipTypeId, 'Integer'],
1028 ];
8ef12e64 1029
6a488035 1030 $memberCount = CRM_Core_DAO::singleValueQuery($query, $params);
e0556ebe 1031 return (int) $memberCount;
6a488035
TO
1032 }
1033
1034 /**
7b835f7c
EM
1035 * Get a count of membership for a specified membership type, optionally for a specified date.
1036 *
1037 * The date must have the form yyyy-mm-dd.
6a488035
TO
1038 *
1039 * If $date is omitted, this function counts as a member anyone whose
1040 * membership status_id indicates they're a current member.
1041 * If $date is given, this function counts as a member anyone who:
1042 * -- Has a start_date before $date and end_date after $date, or
1043 * -- Has a start_date before $date and is currently a member, as indicated
1044 * by the the membership's status_id.
1045 * The second condition takes care of records that have no end_date. These
1046 * are assumed to be lifetime memberships.
1047 *
b2363ea8
TO
1048 * @param int $membershipTypeId
1049 * Membership type id.
1050 * @param string $date
1051 * The date for which to retrieve the count.
e46e9a0b
EM
1052 * @param bool|int $isTest if true, membership is for a test site
1053 * @param bool|int $isOwner if true, only retrieve membership records for owners //LCD
6a488035 1054 *
72b3a70c
CW
1055 * @return int
1056 * the number of members of type $membershipTypeId as of $date.
6a488035
TO
1057 */
1058 public static function getMembershipCount($membershipTypeId, $date = NULL, $isTest = 0, $isOwner = 0) {
4e636a74 1059 if (!CRM_Utils_Rule::date($date)) {
4cf017d1 1060 throw new CRM_Core_Exception(ts('Invalid date "%1" (must have form yyyy-mm-dd).', [1 => $date]));
6a488035
TO
1061 }
1062
b2ae7d75 1063 $params = [
1064 1 => [$membershipTypeId, 'Integer'],
1065 2 => [$isTest, 'Boolean'],
1066 ];
6a488035
TO
1067 $query = "SELECT count(civicrm_membership.id ) as member_count
1068FROM civicrm_membership left join civicrm_membership_status on ( civicrm_membership.status_id = civicrm_membership_status.id )
1069WHERE civicrm_membership.membership_type_id = %1
1070AND civicrm_membership.contact_id NOT IN (SELECT id FROM civicrm_contact WHERE is_deleted = 1)
1071AND civicrm_membership.is_test = %2";
1072 if (!$date) {
1073 $query .= " AND civicrm_membership_status.is_current_member = 1";
1074 }
1075 else {
6a488035
TO
1076 $query .= " AND civicrm_membership.start_date <= '$date' AND civicrm_membership_status.is_current_member = 1";
1077 }
1078 // LCD
1079 $query .= ($isOwner) ? ' AND owner_membership_id IS NULL' : '';
1080 $memberCount = CRM_Core_DAO::singleValueQuery($query, $params);
e0556ebe 1081 return (int) $memberCount;
6a488035
TO
1082 }
1083
1084 /**
fe482240 1085 * Function check the status of the membership before adding membership for a contact.
6a488035 1086 *
e46e9a0b 1087 * @return int
6a488035 1088 */
6a38708b 1089 public static function statusAvailabilty() {
6a488035
TO
1090 $membership = new CRM_Member_DAO_MembershipStatus();
1091 $membership->whereAdd('is_active=1');
c905ba98 1092 return $membership->count();
6a488035
TO
1093 }
1094
cd125a40 1095 /**
603fb32e 1096 * @deprecated This is not used anywhere and should be removed soon!
7b835f7c 1097 * Function for updating a membership record's contribution_recur_id.
cd125a40 1098 *
c866eb5f 1099 * @param CRM_Member_DAO_Membership $membership
5901ddf9 1100 * @param \CRM_Contribute_BAO_Contribution|\CRM_Contribute_DAO_Contribution $contribution
cd125a40 1101 */
971e129b 1102 public static function updateRecurMembership(CRM_Member_DAO_Membership $membership, CRM_Contribute_BAO_Contribution $contribution) {
603fb32e 1103 CRM_Core_Error::deprecatedFunctionWarning('Use the API instead');
cd125a40
FG
1104
1105 if (empty($contribution->contribution_recur_id)) {
1106 return;
1107 }
1108
b2ae7d75 1109 $params = [
1110 1 => [$contribution->contribution_recur_id, 'Integer'],
1111 2 => [$membership->id, 'Integer'],
1112 ];
cd125a40
FG
1113
1114 $sql = "UPDATE civicrm_membership SET contribution_recur_id = %1 WHERE id = %2";
1115 CRM_Core_DAO::executeQuery($sql, $params);
1116 }
1117
6a488035 1118 /**
fe482240 1119 * Method to fix membership status of stale membership.
6a488035
TO
1120 *
1121 * This method first checks if the membership is stale. If it is,
1122 * then status will be updated based on existing start and end
1123 * dates and log will be added for the status change.
1124 *
b2363ea8
TO
1125 * @param array $currentMembership
1126 * Reference to the array.
688d37c6
CW
1127 * containing all values of
1128 * the current membership
81716ddb
EE
1129 * @param string $changeToday
1130 * In case today needs
688d37c6 1131 * to be customised, null otherwise
aaa7b0e6 1132 *
1133 * @throws \CRM_Core_Exception
6a488035 1134 */
00be9182 1135 public static function fixMembershipStatusBeforeRenew(&$currentMembership, $changeToday) {
2cb64970 1136 $today = 'now';
6a488035
TO
1137 if ($changeToday) {
1138 $today = CRM_Utils_Date::processDate($changeToday, NULL, FALSE, 'Y-m-d');
1139 }
1140
1141 $status = CRM_Member_BAO_MembershipStatus::getMembershipStatusByDate(
699e088e
MW
1142 $currentMembership['start_date'] ?? NULL,
1143 $currentMembership['end_date'] ?? NULL,
1144 $currentMembership['join_date'] ?? NULL,
6a488035 1145 $today,
5f11bbcc
EM
1146 TRUE,
1147 $currentMembership['membership_type_id'],
1148 $currentMembership
6a488035
TO
1149 );
1150
3f6cbd33
MWMC
1151 if (empty($status) || empty($status['id'])) {
1152 throw new CRM_Core_Exception(ts('Oops, it looks like there is no valid membership status corresponding to the membership start and end dates for this membership. Contact the site administrator for assistance.'));
6a488035
TO
1153 }
1154
1155 $currentMembership['today_date'] = $today;
1156
1157 if ($status['id'] !== $currentMembership['status_id']) {
c329a76a 1158 $oldStatus = $currentMembership['status_id'];
6a488035
TO
1159 $memberDAO = new CRM_Member_DAO_Membership();
1160 $memberDAO->id = $currentMembership['id'];
1161 $memberDAO->find(TRUE);
1162
1163 $memberDAO->status_id = $status['id'];
6a488035
TO
1164 $memberDAO->save();
1165 CRM_Core_DAO::storeValues($memberDAO, $currentMembership);
6a488035
TO
1166
1167 $currentMembership['is_current_member'] = CRM_Core_DAO::getFieldValue(
1168 'CRM_Member_DAO_MembershipStatus',
1169 $currentMembership['status_id'],
1170 'is_current_member'
1171 );
1172 $format = '%Y%m%d';
1173
b2ae7d75 1174 $logParams = [
6a488035
TO
1175 'membership_id' => $currentMembership['id'],
1176 'status_id' => $status['id'],
1177 'start_date' => CRM_Utils_Date::customFormat(
1178 $currentMembership['start_date'],
1179 $format
1180 ),
1181 'end_date' => CRM_Utils_Date::customFormat(
1182 $currentMembership['end_date'],
1183 $format
1184 ),
1185 'modified_date' => CRM_Utils_Date::customFormat(
1186 $currentMembership['today_date'],
1187 $format
1188 ),
1189 'membership_type_id' => $currentMembership['membership_type_id'],
699e088e 1190 'max_related' => $currentMembership['max_related'] ?? 0,
b2ae7d75 1191 ];
6a488035
TO
1192
1193 $session = CRM_Core_Session::singleton();
1194 // If we have an authenticated session, set modified_id to that user's contact_id, else set to membership.contact_id
1195 if ($session->get('userID')) {
1196 $logParams['modified_id'] = $session->get('userID');
1197 }
1198 else {
1199 $logParams['modified_id'] = $currentMembership['contact_id'];
1200 }
c329a76a
JP
1201
1202 //Create activity for status change.
1203 $allStatus = CRM_Member_BAO_Membership::buildOptions('status_id', 'get');
1204 CRM_Activity_BAO_Activity::addActivity($memberDAO,
1205 'Change Membership Status',
1206 NULL,
b2ae7d75 1207 [
c329a76a
JP
1208 'subject' => "Status changed from {$allStatus[$oldStatus]} to {$allStatus[$status['id']]}",
1209 'source_contact_id' => $logParams['modified_id'],
1210 'priority_id' => 'Normal',
b2ae7d75 1211 ]
c329a76a
JP
1212 );
1213
4ed92e91 1214 CRM_Member_BAO_MembershipLog::add($logParams);
6a488035
TO
1215 }
1216 }
1217
1218 /**
fe482240 1219 * Get the contribution page id from the membership record.
6a488035 1220 *
688d37c6 1221 * @param int $membershipID
6a488035 1222 *
a6c01b45 1223 * @return int
688d37c6 1224 * contribution page id
6a488035 1225 */
00be9182 1226 public static function getContributionPageId($membershipID) {
6a488035
TO
1227 $query = "
1228SELECT c.contribution_page_id as pageID
1229 FROM civicrm_membership_payment mp, civicrm_contribution c
1230 WHERE mp.contribution_id = c.id
1231 AND c.contribution_page_id IS NOT NULL
1232 AND mp.membership_id = " . CRM_Utils_Type::escape($membershipID, 'Integer')
e0556ebe 1233 . " ORDER BY mp.id DESC";
6a488035 1234
e03e1641 1235 return CRM_Core_DAO::singleValueQuery($query);
6a488035
TO
1236 }
1237
6a488035 1238 /**
fe482240 1239 * Updated related memberships.
6a488035 1240 *
b2363ea8
TO
1241 * @param int $ownerMembershipId
1242 * Owner Membership Id.
1243 * @param array $params
1244 * Formatted array of key => value.
6a488035 1245 */
00be9182 1246 public static function updateRelatedMemberships($ownerMembershipId, $params) {
6a488035
TO
1247 $membership = new CRM_Member_DAO_Membership();
1248 $membership->owner_membership_id = $ownerMembershipId;
1249 $membership->find();
1250
1251 while ($membership->fetch()) {
1252 $relatedMembership = new CRM_Member_DAO_Membership();
1253 $relatedMembership->id = $membership->id;
1254 $relatedMembership->copyValues($params);
1255 $relatedMembership->save();
6a488035
TO
1256 }
1257
6a488035
TO
1258 }
1259
1260 /**
fe482240 1261 * Get list of membership fields for profile.
7b835f7c 1262 *
6a488035
TO
1263 * For now we only allow custom membership fields to be in
1264 * profile
1265 *
e46e9a0b 1266 * @param null $mode
688d37c6 1267 * FIXME: This param is ignored
e46e9a0b 1268 *
a6c01b45
CW
1269 * @return array
1270 * the list of membership fields
6a488035 1271 */
00be9182 1272 public static function getMembershipFields($mode = NULL) {
6a488035
TO
1273 $fields = CRM_Member_DAO_Membership::export();
1274
6a488035
TO
1275 unset($fields['membership_contact_id']);
1276 $fields = array_merge($fields, CRM_Core_BAO_CustomField::getFieldsForImport('Membership'));
1277
1278 $membershipType = CRM_Member_DAO_MembershipType::export();
1279
1280 $membershipStatus = CRM_Member_DAO_MembershipStatus::export();
1281
1282 $fields = array_merge($fields, $membershipType, $membershipStatus);
1283
1284 return $fields;
1285 }
1286
1287 /**
fe482240 1288 * Get the sort name of a contact for a particular membership.
6a488035 1289 *
b2363ea8
TO
1290 * @param int $id
1291 * Id of the membership.
6a488035 1292 *
72b3a70c
CW
1293 * @return null|string
1294 * sort name of the contact if found
6a488035 1295 */
00be9182 1296 public static function sortName($id) {
6a488035
TO
1297 $id = CRM_Utils_Type::escape($id, 'Integer');
1298
1299 $query = "
1300SELECT civicrm_contact.sort_name
1301FROM civicrm_membership, civicrm_contact
1302WHERE civicrm_membership.contact_id = civicrm_contact.id
1303 AND civicrm_membership.id = {$id}
1304";
f7969dcf 1305 return CRM_Core_DAO::singleValueQuery($query);
6a488035
TO
1306 }
1307
1308 /**
7b835f7c 1309 * Create memberships for related contacts, taking into account the maximum related memberships.
6a488035 1310 *
b2363ea8
TO
1311 * @param array $params
1312 * Array of key - value pairs.
1313 * @param CRM_Core_DAO $dao
1314 * Membership object.
6a488035 1315 *
06d062ce 1316 * @throws \CRM_Core_Exception
aaa7b0e6 1317 * @throws \CiviCRM_API3_Exception
6a488035 1318 */
696737b5 1319 public static function createRelatedMemberships($params, $dao) {
6a488035
TO
1320
1321 $membership = new CRM_Member_DAO_Membership();
1322 $membership->id = $dao->id;
1323
1324 // required since create method doesn't return all the
1325 // parameters in the returned membership object
1326 if (!$membership->find(TRUE)) {
1327 return;
1328 }
1329 $deceasedStatusId = array_search('Deceased', CRM_Member_PseudoConstant::membershipStatus());
1330 // FIXME : While updating/ renewing the
1331 // membership, if the relationship is PAST then
1332 // the membership of the related contact must be
1333 // expired.
1334 // For that, getting Membership Status for which
1335 // is_current_member is 0. It works for the
1336 // generated data as there is only one membership
1337 // status having is_current_member = 0.
1338 // But this wont work exactly if there will be
1339 // more than one status having is_current_member = 0.
1340 $membershipStatus = new CRM_Member_DAO_MembershipStatus();
1341 $membershipStatus->is_current_member = 0;
1342 if ($membershipStatus->find(TRUE)) {
1343 $expiredStatusId = $membershipStatus->id;
e0556ebe
TO
1344 }
1345 else {
6a488035
TO
1346 $expiredStatusId = array_search('Expired', CRM_Member_PseudoConstant::membershipStatus());
1347 }
1348
b2ae7d75 1349 $relatedContacts = [];
e072b01c 1350 $allRelatedContacts = CRM_Member_BAO_Membership::checkMembershipRelationship($membership->membership_type_id,
1351 $membership->contact_id,
699e088e 1352 $params['action'] ?? NULL
e072b01c 1353 );
6a488035 1354
49903ed2 1355 // CRM-4213, CRM-19735 check for loops, using static variable to record contacts already processed.
696737b5 1356 // Remove repeated related contacts, which already inherited membership of this type$relatedContactIds[$membership->contact_id][$membership->membership_type_id] = TRUE;
6a488035 1357 foreach ($allRelatedContacts as $cid => $status) {
696737b5 1358 // relatedContactIDs is always empty now - will remove next roud because of whitespace readability.
49903ed2
DJ
1359 if (empty($relatedContactIds[$cid]) || empty($relatedContactIds[$cid][$membership->membership_type_id])) {
1360 $relatedContactIds[$cid][$membership->membership_type_id] = TRUE;
6a488035
TO
1361
1362 //don't create membership again for owner contact.
1363 $nestedRelationship = FALSE;
1364 if ($membership->owner_membership_id) {
1365 $nestedRelMembership = new CRM_Member_DAO_Membership();
1366 $nestedRelMembership->id = $membership->owner_membership_id;
1367 $nestedRelMembership->contact_id = $cid;
1368 $nestedRelationship = $nestedRelMembership->find(TRUE);
6a488035
TO
1369 }
1370 if (!$nestedRelationship) {
1371 $relatedContacts[$cid] = $status;
1372 }
1373 }
1374 }
1375
1376 //lets cleanup related membership if any.
1377 if (empty($relatedContacts)) {
3506b6cd 1378 self::deleteRelatedMemberships($membership->id);
6a488035
TO
1379 }
1380 else {
1381 // Edit the params array
1382 unset($params['id']);
1383 // Reminder should be sent only to the direct membership
1384 unset($params['reminder_date']);
1385 // unset the custom value ids
e01bf597 1386 if (isset($params['custom']) && is_array($params['custom'])) {
3d03e8fb
KE
1387 foreach ($params['custom'] as $k => $values) {
1388 foreach ($values as $i => $value) {
1389 unset($params['custom'][$k][$i]['id']);
1390 }
6a488035
TO
1391 }
1392 }
1393 if (!isset($params['membership_type_id'])) {
1394 $params['membership_type_id'] = $membership->membership_type_id;
1395 }
1396
1397 // max_related should be set in the parent membership
1398 unset($params['max_related']);
1399 // Number of inherited memberships available - NULL is interpreted as unlimited, '0' as none
e48744e2 1400 $numRelatedAvailable = ($membership->max_related == NULL ? PHP_INT_MAX : $membership->max_related);
7b835f7c 1401 // will be used to queue potential memberships to be created.
b2ae7d75 1402 $queue = [];
6a488035
TO
1403
1404 foreach ($relatedContacts as $contactId => $relationshipStatus) {
1405 //use existing membership record.
1406 $relMembership = new CRM_Member_DAO_Membership();
1407 $relMembership->contact_id = $contactId;
1408 $relMembership->owner_membership_id = $membership->id;
25f128c3 1409
6a488035 1410 if ($relMembership->find(TRUE)) {
f57cb50c 1411 $params['id'] = $relMembership->id;
6a488035 1412 }
25f128c3
CR
1413 else {
1414 unset($params['id']);
1415 }
1416
6a488035
TO
1417 $params['contact_id'] = $contactId;
1418 $params['owner_membership_id'] = $membership->id;
1419
1420 // set status_id as it might have been changed for
1421 // past relationship
1422 $params['status_id'] = $membership->status_id;
1423
1424 if ($deceasedStatusId &&
1425 CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $contactId, 'is_deceased')
1426 ) {
1427 $params['status_id'] = $deceasedStatusId;
1428 }
699e088e 1429 elseif ((($params['action'] ?? NULL) & CRM_Core_Action::UPDATE) &&
6a488035
TO
1430 ($relationshipStatus == CRM_Contact_BAO_Relationship::PAST)
1431 ) {
e0556ebe
TO
1432 $params['status_id'] = $expiredStatusId;
1433 }
6a488035
TO
1434
1435 //don't calculate status again in create( );
1436 $params['skipStatusCal'] = TRUE;
1437
1438 //do create activity if we changed status.
1439 if ($params['status_id'] != $relMembership->status_id) {
1440 $params['createActivity'] = TRUE;
1441 }
1442
79192d1d
BS
1443 //CRM-20707 - include start/end date
1444 $params['start_date'] = $membership->start_date;
1445 $params['end_date'] = $membership->end_date;
1446
6a488035
TO
1447 // we should not created contribution record for related contacts, CRM-3371
1448 unset($params['contribution_status_id']);
1449
63e1c32b
WA
1450 //CRM-16857: Do not create multiple line-items for inherited membership through priceset.
1451 unset($params['lineItems']);
1452 unset($params['line_item']);
1453
e53bcaa3
DJ
1454 // CRM-20966: Do not create membership_payment record for inherited membership.
1455 unset($params['relate_contribution_id']);
1456
f57cb50c 1457 $ids = [];
6a488035
TO
1458 if (($params['status_id'] == $deceasedStatusId) || ($params['status_id'] == $expiredStatusId)) {
1459 // related membership is not active so does not count towards maximum
696737b5 1460 if (!self::hasExistingInheritedMembership($params)) {
1461 CRM_Member_BAO_Membership::create($params);
1462 }
8efea814
EM
1463 }
1464 else {
6a488035
TO
1465 // related membership already exists, so this is just an update
1466 if (isset($params['id'])) {
e48744e2 1467 if ($numRelatedAvailable > 0) {
db0cbfd0 1468 CRM_Member_BAO_Membership::create($params);
e48744e2 1469 $numRelatedAvailable--;
e0556ebe 1470 }
608e6658 1471 else {
1472 // we have run out of inherited memberships, so delete extras
f5e53870 1473 self::deleteMembership($params['id']);
6a488035 1474 }
e0556ebe
TO
1475 // we need to first check if there will remain inherited memberships, so queue it up
1476 }
1477 else {
6a488035
TO
1478 $queue[] = $params;
1479 }
1480 }
1481 }
1482 // now go over the queue and create any available related memberships
e48744e2
SL
1483 foreach ($queue as $params) {
1484 if ($numRelatedAvailable <= 0) {
1485 break;
1486 }
696737b5 1487 if (!self::hasExistingInheritedMembership($params)) {
1488 CRM_Member_BAO_Membership::create($params);
1489 }
e48744e2 1490 $numRelatedAvailable--;
6a488035
TO
1491 }
1492 }
1493 }
1494
1495 /**
fe482240 1496 * Delete the record that are associated with this Membership Payment.
6a488035 1497 *
b2363ea8 1498 * @param int $membershipId
971e129b 1499 * @param bool $preserveContrib
6a488035 1500 *
608e6658 1501 * @return object
1502 * $membershipPayment deleted membership payment object
6a488035 1503 */
086fea8e 1504 public static function deleteMembershipPayment($membershipId, $preserveContrib = FALSE) {
6a488035 1505
608e6658 1506 $membershipPayment = new CRM_Member_DAO_MembershipPayment();
1507 $membershipPayment->membership_id = $membershipId;
1508 $membershipPayment->find();
6a488035 1509
608e6658 1510 while ($membershipPayment->fetch()) {
ed4cc29d
JT
1511 if (!$preserveContrib) {
1512 CRM_Contribute_BAO_Contribution::deleteContribution($membershipPayment->contribution_id);
1513 }
608e6658 1514 CRM_Utils_Hook::pre('delete', 'MembershipPayment', $membershipPayment->id, $membershipPayment);
061a8a1e 1515 $membershipPayment->delete();
608e6658 1516 CRM_Utils_Hook::post('delete', 'MembershipPayment', $membershipPayment->id, $membershipPayment);
6a488035 1517 }
608e6658 1518 return $membershipPayment;
6a488035
TO
1519 }
1520
bb3a214a 1521 /**
ab30e033
EM
1522 * Build an array of available membership types.
1523 *
c490a46a 1524 * @param CRM_Core_Form $form
1a00ea3c 1525 * @param array $membershipTypeID
ab30e033
EM
1526 * @param bool $activeOnly
1527 * Do we only want active ones?
1528 * (probably this should default to TRUE but as a newly added parameter we are leaving default b
1529 * behaviour unchanged).
bb3a214a
EM
1530 *
1531 * @return array
322b59f0 1532 *
1533 * @throws \CiviCRM_API3_Exception
bb3a214a 1534 */
322b59f0 1535 public static function buildMembershipTypeValues($form, $membershipTypeID = [], $activeOnly = FALSE) {
1a00ea3c 1536 $membershipTypeIDS = (array) $membershipTypeID;
322b59f0 1537 $membershipTypeValues = CRM_Member_BAO_MembershipType::getPermissionedMembershipTypes();
6a488035 1538
322b59f0 1539 // MembershipTypes are already filtered by domain, filter as appropriate by is_active & a passed in list of ids.
1540 foreach ($membershipTypeValues as $id => $type) {
1541 if (($activeOnly && empty($type['is_active']))
1542 || (!empty($membershipTypeIDS) && !in_array($id, $membershipTypeIDS, FALSE))
1543 ) {
1544 unset($membershipTypeValues[$id]);
6a488035
TO
1545 }
1546 }
6a488035
TO
1547
1548 CRM_Utils_Hook::membershipTypeValues($form, $membershipTypeValues);
1549
1550 if (is_numeric($membershipTypeID) &&
1551 $membershipTypeID > 0
1552 ) {
322b59f0 1553 CRM_Core_Error::deprecatedFunctionWarning('Non arrays deprecated');
6a488035
TO
1554 return $membershipTypeValues[$membershipTypeID];
1555 }
322b59f0 1556 return $membershipTypeValues;
6a488035
TO
1557 }
1558
1559 /**
fe482240 1560 * Get membership record count for a Contact.
6a488035 1561 *
c490a46a 1562 * @param int $contactID
b2363ea8 1563 * @param bool $activeOnly
6a488035 1564 *
c490a46a 1565 * @return null|string
6a488035 1566 */
00be9182 1567 public static function getContactMembershipCount($contactID, $activeOnly = FALSE) {
51b9c47e
SL
1568 CRM_Financial_BAO_FinancialType::getAvailableMembershipTypes($membershipTypes);
1569 $addWhere = " AND membership_type_id IN (0)";
1570 if (!empty($membershipTypes)) {
1571 $addWhere = " AND membership_type_id IN (" . implode(',', array_keys($membershipTypes)) . ")";
1572 }
6a488035 1573 $select = "SELECT count(*) FROM civicrm_membership ";
e0556ebe 1574 $where = "WHERE civicrm_membership.contact_id = {$contactID} AND civicrm_membership.is_test = 0 ";
6a488035
TO
1575
1576 // CRM-6627, all status below 3 (active, pending, grace) are considered active
1577 if ($activeOnly) {
1578 $select .= " INNER JOIN civicrm_membership_status ON civicrm_membership.status_id = civicrm_membership_status.id ";
e0556ebe 1579 $where .= " and civicrm_membership_status.is_current_member = 1";
6a488035
TO
1580 }
1581
51b9c47e 1582 $query = $select . $where . $addWhere;
6a488035
TO
1583 return CRM_Core_DAO::singleValueQuery($query);
1584 }
1585
1586 /**
7b835f7c 1587 * Check whether payment processor supports cancellation of membership subscription.
6a488035 1588 *
b2363ea8
TO
1589 * @param int $mid
1590 * Membership id.
6a488035 1591 *
e46e9a0b
EM
1592 * @param bool $isNotCancelled
1593 *
608e6658 1594 * @return bool
6a488035 1595 */
00be9182 1596 public static function isCancelSubscriptionSupported($mid, $isNotCancelled = TRUE) {
6a488035
TO
1597 $cacheKeyString = "$mid";
1598 $cacheKeyString .= $isNotCancelled ? '_1' : '_0';
1599
b2ae7d75 1600 static $supportsCancel = [];
6a488035
TO
1601
1602 if (!array_key_exists($cacheKeyString, $supportsCancel)) {
1603 $supportsCancel[$cacheKeyString] = FALSE;
1604 $isCancelled = FALSE;
1605
1606 if ($isNotCancelled) {
1607 $isCancelled = self::isSubscriptionCancelled($mid);
1608 }
1609
1610 $paymentObject = CRM_Financial_BAO_PaymentProcessor::getProcessorForEntity($mid, 'membership', 'obj');
1611 if (!empty($paymentObject)) {
1524a007 1612 $supportsCancel[$cacheKeyString] = $paymentObject->supports('cancelRecurring') && !$isCancelled;
6a488035
TO
1613 }
1614 }
1615 return $supportsCancel[$cacheKeyString];
1616 }
1617
1618 /**
fe482240 1619 * Check whether subscription is already cancelled.
6a488035 1620 *
b2363ea8
TO
1621 * @param int $mid
1622 * Membership id.
6a488035 1623 *
a6c01b45
CW
1624 * @return string
1625 * contribution status
6a488035 1626 */
00be9182 1627 public static function isSubscriptionCancelled($mid) {
6a488035
TO
1628 $sql = "
1629 SELECT cr.contribution_status_id
1630 FROM civicrm_contribution_recur cr
1631LEFT JOIN civicrm_membership mem ON ( cr.id = mem.contribution_recur_id )
1632 WHERE mem.id = %1 LIMIT 1";
b2ae7d75 1633 $params = [1 => [$mid, 'Integer']];
6a488035 1634 $statusId = CRM_Core_DAO::singleValueQuery($sql, $params);
e0556ebe 1635 $status = CRM_Contribute_PseudoConstant::contributionStatus($statusId, 'name');
6a488035
TO
1636 if ($status == 'Cancelled') {
1637 return TRUE;
1638 }
1639 return FALSE;
1640 }
1641
1642 /**
7b835f7c
EM
1643 * Get membership joins for a specified membership type.
1644 *
1645 * Specifically, retrieves a count of still current memberships whose
4e636a74 1646 * join_date and start_date are within a specified date range. Dates match
8ef12e64 1647 * the pattern "yyyy-mm-dd".
6a488035 1648 *
b2363ea8
TO
1649 * @param int $membershipTypeId
1650 * Membership type id.
1651 * @param int $startDate
1652 * Date on which to start counting.
1653 * @param int $endDate
1654 * Date on which to end counting.
e46e9a0b 1655 * @param bool|int $isTest if true, membership is for a test site
6a488035 1656 *
72b3a70c
CW
1657 * @return int
1658 * the number of members of type $membershipTypeId
1659 * whose join_date is between $startDate and $endDate and
1660 * whose start_date is between $startDate and $endDate
6a488035 1661 */
00be9182 1662 public static function getMembershipJoins($membershipTypeId, $startDate, $endDate, $isTest = 0) {
6a488035
TO
1663 $testClause = 'membership.is_test = 1';
1664 if (!$isTest) {
1665 $testClause = '( membership.is_test IS NULL OR membership.is_test = 0 )';
1666 }
4e636a74
AH
1667 if (!self::$_signupActType) {
1668 self::_getActTypes();
1669 }
8ef12e64 1670
4e636a74
AH
1671 if (!self::$_signupActType) {
1672 return 0;
1673 }
6a488035
TO
1674
1675 $query = "
8ef12e64 1676 SELECT COUNT(DISTINCT membership.id) as member_count
6a488035 1677 FROM civicrm_membership membership
8ef12e64 1678INNER JOIN civicrm_activity activity ON (activity.source_record_id = membership.id AND activity.activity_type_id = %1)
6a488035 1679INNER JOIN civicrm_membership_status status ON ( membership.status_id = status.id AND status.is_current_member = 1 )
4e636a74
AH
1680INNER JOIN civicrm_contact contact ON ( contact.id = membership.contact_id AND contact.is_deleted = 0 )
1681 WHERE membership.membership_type_id = %2
1682 AND activity.activity_date_time >= '$startDate' AND activity.activity_date_time <= '$endDate 23:59:59'
6a488035
TO
1683 AND {$testClause}";
1684
b2ae7d75 1685 $params = [
1686 1 => [self::$_signupActType, 'Integer'],
1687 2 => [$membershipTypeId, 'Integer'],
1688 ];
4e636a74 1689
6a488035
TO
1690 $memberCount = CRM_Core_DAO::singleValueQuery($query, $params);
1691
e0556ebe 1692 return (int) $memberCount;
6a488035
TO
1693 }
1694
1695 /**
7b835f7c
EM
1696 * Get membership renewals for a specified membership type.
1697 *
1698 * Specifically, retrieves a count of still current memberships
8ef12e64 1699 * whose join_date is before and start_date is within a specified date
4e636a74 1700 * range. Dates match the pattern "yyyy-mm-dd".
6a488035 1701 *
b2363ea8
TO
1702 * @param int $membershipTypeId
1703 * Membership type id.
1704 * @param int $startDate
1705 * Date on which to start counting.
1706 * @param int $endDate
1707 * Date on which to end counting.
e46e9a0b 1708 * @param bool|int $isTest if true, membership is for a test site
6a488035 1709 *
df8d3074 1710 * @return int
a6c01b45 1711 * returns the number of members of type $membershipTypeId
688d37c6
CW
1712 * whose join_date is before $startDate and
1713 * whose start_date is between $startDate and $endDate
6a488035 1714 */
00be9182 1715 public static function getMembershipRenewals($membershipTypeId, $startDate, $endDate, $isTest = 0) {
6a488035
TO
1716 $testClause = 'membership.is_test = 1';
1717 if (!$isTest) {
1718 $testClause = '( membership.is_test IS NULL OR membership.is_test = 0 )';
1719 }
4e636a74
AH
1720 if (!self::$_renewalActType) {
1721 self::_getActTypes();
1722 }
8ef12e64 1723
4e636a74
AH
1724 if (!self::$_renewalActType) {
1725 return 0;
1726 }
6a488035
TO
1727
1728 $query = "
8ef12e64 1729 SELECT COUNT(DISTINCT membership.id) as member_count
6a488035 1730 FROM civicrm_membership membership
4e636a74 1731INNER JOIN civicrm_activity activity ON (activity.source_record_id = membership.id AND activity.activity_type_id = %1)
6a488035
TO
1732INNER JOIN civicrm_membership_status status ON ( membership.status_id = status.id AND status.is_current_member = 1 )
1733INNER JOIN civicrm_contact contact ON ( contact.id = membership.contact_id AND contact.is_deleted = 0 )
4e636a74 1734 WHERE membership.membership_type_id = %2
8ef12e64 1735 AND activity.activity_date_time >= '$startDate' AND activity.activity_date_time <= '$endDate 23:59:59'
6a488035
TO
1736 AND {$testClause}";
1737
b2ae7d75 1738 $params = [
1739 1 => [self::$_renewalActType, 'Integer'],
1740 2 => [$membershipTypeId, 'Integer'],
1741 ];
6a488035
TO
1742 $memberCount = CRM_Core_DAO::singleValueQuery($query, $params);
1743
e0556ebe 1744 return (int) $memberCount;
6a488035
TO
1745 }
1746
c98997b9 1747 /**
b2363ea8 1748 * @param int $contactID
100fef9d 1749 * @param int $membershipTypeID
c98997b9 1750 * @param bool $is_test
81716ddb 1751 * @param string $changeToday
b2363ea8 1752 * @param int $modifiedID
c98997b9
EM
1753 * @param $customFieldsFormatted
1754 * @param $numRenewTerms
100fef9d 1755 * @param int $membershipID
c98997b9 1756 * @param $pending
b2363ea8 1757 * @param int $contributionRecurID
c98997b9 1758 * @param $membershipSource
c98997b9 1759 * @param $isPayLater
b2363ea8 1760 * @param int $campaignId
620ce374 1761 * @param array $formDates
13a16f43 1762 * @param null|CRM_Contribute_BAO_Contribution $contribution
8e8d287f 1763 * @param array $lineItems
c98997b9 1764 *
c98997b9 1765 * @return array
13a16f43 1766 * @throws \CRM_Core_Exception
aaa7b0e6 1767 * @throws \CiviCRM_API3_Exception
c98997b9 1768 */
b2ae7d75 1769 public static function processMembership($contactID, $membershipTypeID, $is_test, $changeToday, $modifiedID, $customFieldsFormatted, $numRenewTerms, $membershipID, $pending, $contributionRecurID, $membershipSource, $isPayLater, $campaignId, $formDates = [], $contribution = NULL, $lineItems = []) {
c98997b9 1770 $renewalMode = $updateStatusId = FALSE;
61767a1d
EM
1771 $allStatus = CRM_Member_PseudoConstant::membershipStatus();
1772 $format = '%Y%m%d';
1773 $statusFormat = '%Y-%m-%d';
1774 $membershipTypeDetails = CRM_Member_BAO_MembershipType::getMembershipTypeDetails($membershipTypeID);
b2ae7d75 1775 $dates = [];
1776 $ids = [];
603fb32e 1777
d81c67a2 1778 // CRM-7297 - allow membership type to be be changed during renewal so long as the parent org of new membershipType
c98997b9
EM
1779 // is the same as the parent org of an existing membership of the contact
1780 $currentMembership = CRM_Member_BAO_Membership::getContactMembership($contactID, $membershipTypeID,
1781 $is_test, $membershipID, TRUE
1782 );
1783 if ($currentMembership) {
c98997b9
EM
1784 $renewalMode = TRUE;
1785
1786 // Do NOT do anything.
1787 //1. membership with status : PENDING/CANCELLED (CRM-2395)
1788 //2. Paylater/IPN renew. CRM-4556.
b2ae7d75 1789 if ($pending || in_array($currentMembership['status_id'], [
aaa7b0e6 1790 array_search('Pending', $allStatus),
1791 // CRM-15475
1792 array_search('Cancelled', CRM_Member_PseudoConstant::membershipStatus(NULL, " name = 'Cancelled' ", 'name', FALSE, TRUE)),
1793 ])) {
c98997b9 1794
b2ae7d75 1795 $memParams = [
356bfcaf 1796 'id' => $currentMembership['id'],
1797 'contribution' => $contribution,
1798 'status_id' => $currentMembership['status_id'],
1799 'start_date' => $currentMembership['start_date'],
1800 'end_date' => $currentMembership['end_date'],
13a16f43 1801 'line_item' => $lineItems,
356bfcaf 1802 'join_date' => $currentMembership['join_date'],
c98997b9 1803 'membership_type_id' => $membershipTypeID,
ef0abb4c 1804 'max_related' => !empty($membershipTypeDetails['max_related']) ? $membershipTypeDetails['max_related'] : NULL,
98f0683a 1805 'membership_activity_status' => ($pending || $isPayLater) ? 'Scheduled' : 'Completed',
b2ae7d75 1806 ];
14065266 1807 if ($contributionRecurID) {
356bfcaf 1808 $memParams['contribution_recur_id'] = $contributionRecurID;
c98997b9 1809 }
ebe673d0 1810
1811 $membership = self::create($memParams);
b2ae7d75 1812 return [$membership, $renewalMode, $dates];
c98997b9
EM
1813 }
1814
1815 // Check and fix the membership if it is STALE
1816 self::fixMembershipStatusBeforeRenew($currentMembership, $changeToday);
1817
1818 // Now Renew the membership
1819 if (!$currentMembership['is_current_member']) {
1820 // membership is not CURRENT
1821
1822 // CRM-7297 Membership Upsell - calculate dates based on new membership type
1823 $dates = CRM_Member_BAO_MembershipType::getRenewalDatesForMembershipType($currentMembership['id'],
1824 $changeToday,
1825 $membershipTypeID,
1826 $numRenewTerms
1827 );
1828
1829 $currentMembership['join_date'] = CRM_Utils_Date::customFormat($currentMembership['join_date'], $format);
b2ae7d75 1830 foreach (['start_date', 'end_date'] as $dateType) {
9c1bc317 1831 $currentMembership[$dateType] = $formDates[$dateType] ?? NULL;
620ce374 1832 if (empty($currentMembership[$dateType])) {
9c1bc317 1833 $currentMembership[$dateType] = $dates[$dateType] ?? NULL;
620ce374 1834 }
1835 }
c98997b9
EM
1836 $currentMembership['is_test'] = $is_test;
1837
1838 if (!empty($membershipSource)) {
1839 $currentMembership['source'] = $membershipSource;
1840 }
c98997b9
EM
1841
1842 if (!empty($currentMembership['id'])) {
1843 $ids['membership'] = $currentMembership['id'];
1844 }
1845 $memParams = $currentMembership;
1846 $memParams['membership_type_id'] = $membershipTypeID;
1847
1848 //set the log start date.
1849 $memParams['log_start_date'] = CRM_Utils_Date::customFormat($dates['log_start_date'], $format);
1850 }
1851 else {
1852
1853 // CURRENT Membership
1854 $membership = new CRM_Member_DAO_Membership();
1855 $membership->id = $currentMembership['id'];
1856 $membership->find(TRUE);
1857 // CRM-7297 Membership Upsell - calculate dates based on new membership type
1858 $dates = CRM_Member_BAO_MembershipType::getRenewalDatesForMembershipType($membership->id,
1859 $changeToday,
1860 $membershipTypeID,
1861 $numRenewTerms
1862 );
1863
1864 // Insert renewed dates for CURRENT membership
b2ae7d75 1865 $memParams = [];
c98997b9 1866 $memParams['join_date'] = CRM_Utils_Date::isoToMysql($membership->join_date);
699e088e 1867 $memParams['start_date'] = $formDates['start_date'] ?? CRM_Utils_Date::isoToMysql($membership->start_date);
9c1bc317 1868 $memParams['end_date'] = $formDates['end_date'] ?? NULL;
620ce374 1869 if (empty($memParams['end_date'])) {
9c1bc317 1870 $memParams['end_date'] = $dates['end_date'] ?? NULL;
620ce374 1871 }
c98997b9
EM
1872 $memParams['membership_type_id'] = $membershipTypeID;
1873
1874 //set the log start date.
1875 $memParams['log_start_date'] = CRM_Utils_Date::customFormat($dates['log_start_date'], $format);
e7d41583
BS
1876
1877 //CRM-18067
1878 if (!empty($membershipSource)) {
1879 $memParams['source'] = $membershipSource;
1880 }
1881 elseif (empty($membership->source)) {
1882 $memParams['source'] = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_Membership',
1883 $currentMembership['id'],
1884 'source'
1885 );
c98997b9
EM
1886 }
1887
1888 if (!empty($currentMembership['id'])) {
1889 $ids['membership'] = $currentMembership['id'];
1890 }
c329a76a 1891 $memParams['membership_activity_status'] = ($pending || $isPayLater) ? 'Scheduled' : 'Completed';
c98997b9 1892 }
c98997b9
EM
1893 }
1894 else {
1895 // NEW Membership
b2ae7d75 1896 $memParams = [
c98997b9
EM
1897 'contact_id' => $contactID,
1898 'membership_type_id' => $membershipTypeID,
b2ae7d75 1899 ];
c98997b9
EM
1900
1901 if (!$pending) {
1902 $dates = CRM_Member_BAO_MembershipType::getDatesForMembershipType($membershipTypeID, NULL, NULL, NULL, $numRenewTerms);
1903
b2ae7d75 1904 foreach (['join_date', 'start_date', 'end_date'] as $dateType) {
9c1bc317 1905 $memParams[$dateType] = $formDates[$dateType] ?? NULL;
620ce374 1906 if (empty($memParams[$dateType])) {
9c1bc317 1907 $memParams[$dateType] = $dates[$dateType] ?? NULL;
620ce374 1908 }
1909 }
c98997b9
EM
1910
1911 $status = CRM_Member_BAO_MembershipStatus::getMembershipStatusByDate(CRM_Utils_Date::customFormat($dates['start_date'],
b2ae7d75 1912 $statusFormat
1913 ),
c98997b9
EM
1914 CRM_Utils_Date::customFormat($dates['end_date'],
1915 $statusFormat
1916 ),
1917 CRM_Utils_Date::customFormat($dates['join_date'],
1918 $statusFormat
1919 ),
2cb64970 1920 'now',
c98997b9
EM
1921 TRUE,
1922 $membershipTypeID,
1923 $memParams
1924 );
9c1bc317 1925 $updateStatusId = $status['id'] ?? NULL;
c98997b9
EM
1926 }
1927 else {
1928 // if IPN/Pay-Later set status to: PENDING
1929 $updateStatusId = array_search('Pending', $allStatus);
1930 }
1931
1932 if (!empty($membershipSource)) {
1933 $memParams['source'] = $membershipSource;
1934 }
c98997b9
EM
1935 $memParams['is_test'] = $is_test;
1936 $memParams['is_pay_later'] = $isPayLater;
1937 }
14065266 1938 // Putting this in an IF is precautionary as it seems likely that it would be ignored if empty, but
1939 // perhaps shouldn't be?
1940 if ($contributionRecurID) {
1941 $memParams['contribution_recur_id'] = $contributionRecurID;
1942 }
c98997b9
EM
1943 //CRM-4555
1944 //if we decided status here and want to skip status
1945 //calculation in create( ); then need to pass 'skipStatusCal'.
1946 if ($updateStatusId) {
1947 $memParams['status_id'] = $updateStatusId;
1948 $memParams['skipStatusCal'] = TRUE;
1949 }
1950
1951 //since we are renewing,
1952 //make status override false.
1953 $memParams['is_override'] = FALSE;
1954
1955 //CRM-4027, create log w/ individual contact.
1956 if ($modifiedID) {
def3192c 1957 // @todo this param is likely unused now.
c98997b9
EM
1958 $memParams['is_for_organization'] = TRUE;
1959 }
def3192c 1960 $params['modified_id'] = $modifiedID ?? $contactID;
c98997b9
EM
1961
1962 //inherit campaign from contrib page.
1963 if (isset($campaignId)) {
1964 $memParams['campaign_id'] = $campaignId;
1965 }
1966
356bfcaf 1967 $memParams['contribution'] = $contribution;
c98997b9 1968 $memParams['custom'] = $customFieldsFormatted;
13a16f43 1969 // Load all line items & process all in membership. Don't do in contribution.
1970 // Relevant tests in api_v3_ContributionPageTest.
1971 $memParams['line_item'] = $lineItems;
f57cb50c 1972 // @todo stop passing $ids (membership and userId may be set by this point)
2b1f6808 1973 $membership = self::create($memParams, $ids);
c98997b9
EM
1974
1975 // not sure why this statement is here, seems quite odd :( - Lobo: 12/26/2010
1976 // related to: http://forum.civicrm.org/index.php/topic,11416.msg49072.html#msg49072
1977 $membership->find(TRUE);
1978
b2ae7d75 1979 return [$membership, $renewalMode, $dates];
c98997b9
EM
1980 }
1981
27b6ce36
EM
1982 /**
1983 * Get line items representing the default price set.
1984 *
1985 * @param int $membershipOrg
1986 * @param int $membershipTypeID
1987 * @param float $total_amount
ccb02c2d 1988 * @param int $priceSetId
27b6ce36
EM
1989 *
1990 * @return array
1991 */
ccb02c2d 1992 public static function setQuickConfigMembershipParameters($membershipOrg, $membershipTypeID, $total_amount, $priceSetId) {
27b6ce36
EM
1993 $priceSets = current(CRM_Price_BAO_PriceSet::getSetDetail($priceSetId));
1994
1995 // The name of the price field corresponds to the membership_type organization contact.
b2ae7d75 1996 $params = [
27b6ce36
EM
1997 'price_set_id' => $priceSetId,
1998 'name' => $membershipOrg,
b2ae7d75 1999 ];
2000 $results = [];
27b6ce36
EM
2001 CRM_Price_BAO_PriceField::retrieve($params, $results);
2002
2003 if (!empty($results)) {
2004 $fields[$results['id']] = $priceSets['fields'][$results['id']];
2005 $fid = $results['id'];
b2ae7d75 2006 $editedFieldParams = [
27b6ce36
EM
2007 'price_field_id' => $results['id'],
2008 'membership_type_id' => $membershipTypeID,
b2ae7d75 2009 ];
2010 $results = [];
27b6ce36
EM
2011 CRM_Price_BAO_PriceFieldValue::retrieve($editedFieldParams, $results);
2012 $fields[$fid]['options'][$results['id']] = $priceSets['fields'][$fid]['options'][$results['id']];
2013 if (!empty($total_amount)) {
2014 $fields[$fid]['options'][$results['id']]['amount'] = $total_amount;
2015 }
2016 }
2017
2018 $fieldID = key($fields);
b2ae7d75 2019 $returnParams = [
27b6ce36
EM
2020 'price_set_id' => $priceSetId,
2021 'price_sets' => $priceSets,
2022 'fields' => $fields,
b2ae7d75 2023 'price_fields' => [
6b409353 2024 'price_' . $fieldID => $results['id'] ?? NULL,
b2ae7d75 2025 ],
2026 ];
27b6ce36
EM
2027 return $returnParams;
2028 }
2029
a392809d
JV
2030 /**
2031 * Update the status of all deceased members to deceased.
2032 *
2033 * @return int
2034 * Count of updated contacts.
aaa7b0e6 2035 *
2036 * @throws \CiviCRM_API3_Exception
2037 * @throws \CRM_Core_Exception
a392809d
JV
2038 */
2039 protected static function updateDeceasedMembersStatuses() {
2040 $count = 0;
acd68908
OT
2041
2042 $deceasedStatusId = CRM_Core_PseudoConstant::getKey('CRM_Member_BAO_Membership', 'status_id', 'Deceased');
2043
a392809d
JV
2044 // 'create' context for buildOptions returns only if enabled.
2045 $allStatus = self::buildOptions('status_id', 'create');
acd68908 2046 if (array_key_exists($deceasedStatusId, $allStatus) === FALSE) {
a392809d
JV
2047 // Deceased status is an admin status & is required. We want to fail early if
2048 // it is not present or active.
2049 // We could make the case 'some databases just don't use deceased so we will check
2050 // for the presence of a deceased contact in the DB before rejecting.
2051 if (CRM_Core_DAO::singleValueQuery('
2052 SELECT count(*) FROM civicrm_contact WHERE is_deceased = 0'
2053 )) {
2054 throw new CRM_Core_Exception(
2055 ts("Deceased Membership status is missing or not active. <a href='%1'>Click here to check</a>.",
2056 [1 => CRM_Utils_System::url('civicrm/admin/member/membershipStatus', 'reset=1')]
2057 ));
2058 }
2059 }
2060 $deceasedDAO = CRM_Core_DAO::executeQuery(
2061 $baseQuery = "
2062 SELECT membership.id as membership_id
2063 FROM civicrm_membership membership
2064 INNER JOIN civicrm_contact ON membership.contact_id = civicrm_contact.id
2065 INNER JOIN civicrm_membership_type ON membership.membership_type_id = civicrm_membership_type.id
2066 AND civicrm_membership_type.is_active = 1
2067 WHERE membership.is_test = 0
2068 AND civicrm_contact.is_deceased = 1
2069 AND membership.status_id <> %1
2070 ",
2071 [1 => [$deceasedStatusId, 'Integer']]
2072 );
2073 while ($deceasedDAO->fetch()) {
2074 civicrm_api3('membership', 'create', [
2075 'id' => $deceasedDAO->membership_id,
2076 'status_id' => $deceasedStatusId,
2077 'createActivity' => TRUE,
2078 'skipStatusCal' => TRUE,
2079 'skipRecentView' => TRUE,
2080 ]);
2081 $count++;
2082 }
a392809d
JV
2083 return $count;
2084 }
2085
696737b5 2086 /**
2087 * Does the existing membership match the required membership.
2088 *
2089 * Check before updating that the params are not a match - this is part of avoiding
2090 * a loop if we have already updated.
2091 *
2092 * https://issues.civicrm.org/jira/browse/CRM-4213
2093 * @param array $params
2094 *
2095 * @param array $membership
2096 *
2097 * @return bool
2098 */
2099 protected static function matchesRequiredMembership($params, $membership) {
2100 foreach (['start_date', 'end_date'] as $date) {
2101 if (strtotime($params[$date]) !== strtotime($membership[$date])) {
2102 return FALSE;
2103 }
2104 if ((int) $params['status_id'] !== (int) $membership['status_id']) {
2105 return FALSE;
2106 }
2107 if ((int) $params['membership_type_id'] !== (int) $membership['membership_type_id']) {
2108 return FALSE;
2109 }
2110 }
2111 return TRUE;
2112 }
2113
2114 /**
2115 * Params of new membership.
2116 *
2117 * @param array $params
2118 *
2119 * @return bool
2120 * @throws \CiviCRM_API3_Exception
2121 */
2122 protected static function hasExistingInheritedMembership($params) {
2123 foreach (civicrm_api3('Membership', 'get', ['contact_id' => $params['contact_id']])['values'] as $membership) {
2124 if (!empty($membership['owner_membership_id'])
2125 && $membership['membership_type_id'] === $params['membership_type_id']
2126 && (int) $params['owner_membership_id'] !== (int) $membership['owner_membership_id']
2127 ) {
2128 // Inheriting it from another contact, don't update here.
2129 return TRUE;
2130 }
2131 if (self::matchesRequiredMembership($params, $membership)) {
2132 return TRUE;
2133 }
2134 }
2135 return FALSE;
2136 }
2137
6a488035 2138 /**
100fef9d 2139 * Process price set and line items.
6a488035 2140 *
100fef9d 2141 * @param int $membershipId
7b835f7c 2142 * @param array $lineItem
aaa7b0e6 2143 *
2144 * @throws \CiviCRM_API3_Exception
6a488035 2145 */
00be9182 2146 public function processPriceSet($membershipId, $lineItem) {
6a488035
TO
2147 //FIXME : need to move this too
2148 if (!$membershipId || !is_array($lineItem)
ca58d9b9 2149 || CRM_Utils_System::isNull($lineItem)
6a488035
TO
2150 ) {
2151 return;
2152 }
2153
2154 foreach ($lineItem as $priceSetId => $values) {
2155 if (!$priceSetId) {
2156 continue;
2157 }
2158 foreach ($values as $line) {
2159 $line['entity_table'] = 'civicrm_membership';
2160 $line['entity_id'] = $membershipId;
2161 CRM_Price_BAO_LineItem::create($line);
2162 }
2163 }
2164 }
2165
2166 /**
fe482240 2167 * Retrieve the contribution id for the associated Membership id.
ca266339 2168 * @todo we should get this off the line item
6a488035 2169 *
b2363ea8
TO
2170 * @param int $membershipId
2171 * Membership id.
971e129b 2172 * @param bool $all
fde55343 2173 * if more than one payment associated with membership id need to be returned.
6a488035 2174 *
81716ddb 2175 * @return int|int[]
a6c01b45 2176 * contribution id
b2ae7d75 2177 * @todo we should get this off the line item
2178 *
6a488035 2179 */
fde55343 2180 public static function getMembershipContributionId($membershipId, $all = FALSE) {
6a488035 2181
ca266339
EM
2182 $membershipPayment = new CRM_Member_DAO_MembershipPayment();
2183 $membershipPayment->membership_id = $membershipId;
fde55343 2184 if ($all && $membershipPayment->find()) {
b2ae7d75 2185 $contributionIds = [];
fde55343
JP
2186 while ($membershipPayment->fetch()) {
2187 $contributionIds[] = $membershipPayment->contribution_id;
2188 }
2189 return $contributionIds;
2190 }
2191
ca266339
EM
2192 if ($membershipPayment->find(TRUE)) {
2193 return $membershipPayment->contribution_id;
6a488035
TO
2194 }
2195 return NULL;
2196 }
2197
2198 /**
e0556ebe
TO
2199 * The function checks and updates the status of all membership records for a given domain using the
2200 * calc_membership_status and update_contact_membership APIs.
2201 *
2202 * IMPORTANT:
2203 * Sending renewal reminders has been migrated from this job to the Scheduled Reminders function as of 4.3.
2204 *
ac4d4d8c
MW
2205 * @param array $params
2206 * only_active_membership_types, exclude_test_memberships, exclude_membership_status_ids
2207 *
a6c01b45 2208 * @return array
aaa7b0e6 2209 *
2210 * @throws \CiviCRM_API3_Exception
2211 * @throws \CRM_Core_Exception
e0556ebe 2212 */
ac4d4d8c 2213 public static function updateAllMembershipStatus($params = []) {
4e60e375
AH
2214 // We want all of the statuses as id => name, even the disabled ones (cf.
2215 // CRM-15475), to identify which are Pending, Deceased, Cancelled, and
2216 // Expired.
2217 $allStatus = CRM_Member_BAO_Membership::buildOptions('status_id', 'validate');
ac4d4d8c
MW
2218 if (empty($params['exclude_membership_status_ids'])) {
2219 $params['exclude_membership_status_ids'] = [
2220 array_search('Pending', $allStatus),
2221 array_search('Cancelled', $allStatus),
2222 array_search('Expired', $allStatus) ?: 0,
2223 array_search('Deceased', $allStatus),
2224 ];
2225 }
2226 // Deceased is *always* excluded because it is has very specific processing below.
2227 elseif (!in_array(array_search('Deceased', $allStatus), $params['exclude_membership_status_ids'])) {
2228 $params['exclude_membership_status_ids'][] = array_search('Deceased', $allStatus);
2229 }
2230
2231 for ($index = 0; $index < count($params['exclude_membership_status_ids']); $index++) {
2232 $queryParams[$index] = [$params['exclude_membership_status_ids'][$index], 'Integer'];
2233 }
2234 $membershipStatusClause = 'civicrm_membership.status_id NOT IN (%' . implode(', %', array_keys($queryParams)) . ')';
2235
2236 // Tests for this function are in api_v3_JobTest. Please add tests for all updates.
2237
2238 $updateCount = $processCount = self::updateDeceasedMembersStatuses();
2239
2240 $whereClauses[] = 'civicrm_contact.is_deceased = 0';
2241 if ($params['exclude_test_memberships']) {
2242 $whereClauses[] = 'civicrm_membership.is_test = 0';
2243 }
2244 $whereClause = implode(' AND ', $whereClauses);
2245 $activeMembershipClause = '';
2246 if ($params['only_active_membership_types']) {
2247 $activeMembershipClause = ' AND civicrm_membership_type.is_active = 1';
2248 }
6a488035 2249
a392809d
JV
2250 // This query retrieves ALL memberships of active types.
2251 $baseQuery = "
6a488035
TO
2252SELECT civicrm_membership.id as membership_id,
2253 civicrm_membership.is_override as is_override,
e136f704 2254 civicrm_membership.status_override_end_date as status_override_end_date,
6a488035
TO
2255 civicrm_membership.membership_type_id as membership_type_id,
2256 civicrm_membership.status_id as status_id,
2257 civicrm_membership.join_date as join_date,
2258 civicrm_membership.start_date as start_date,
2259 civicrm_membership.end_date as end_date,
2260 civicrm_membership.source as source,
2261 civicrm_contact.id as contact_id,
6a488035
TO
2262 civicrm_membership.owner_membership_id as owner_membership_id,
2263 civicrm_membership.contribution_recur_id as recur_id
2264FROM civicrm_membership
2265INNER JOIN civicrm_contact ON ( civicrm_membership.contact_id = civicrm_contact.id )
85b6e9a2 2266INNER JOIN civicrm_membership_type ON
ac4d4d8c
MW
2267 (civicrm_membership.membership_type_id = civicrm_membership_type.id {$activeMembershipClause})
2268WHERE {$whereClause}";
6a488035 2269
354ee85e
J
2270 $query = $baseQuery . " AND civicrm_membership.is_override IS NOT NULL AND civicrm_membership.status_override_end_date IS NOT NULL";
2271 $dao1 = CRM_Core_DAO::executeQuery($query);
2272 while ($dao1->fetch()) {
2273 self::processOverriddenUntilDateMembership($dao1);
2274 }
6a488035 2275
2883b934 2276 $query = $baseQuery . " AND (civicrm_membership.is_override = 0 OR civicrm_membership.is_override IS NULL)
ac4d4d8c 2277 AND {$membershipStatusClause}
354ee85e 2278 AND civicrm_membership.owner_membership_id IS NULL ";
ac4d4d8c
MW
2279
2280 $allMembershipTypes = CRM_Member_BAO_MembershipType::getAllMembershipTypes();
2281
2282 $dao2 = CRM_Core_DAO::executeQuery($query, $queryParams);
354ee85e
J
2283
2284 while ($dao2->fetch()) {
6a488035
TO
2285 $processCount++;
2286
354ee85e
J
2287 // CRM-7248: added excludeIsAdmin param to the following fn call to prevent moving to admin statuses
2288 //get the membership status as per id.
7aeb8ff1 2289 $newStatus = civicrm_api3('membership_status', 'calc',
b2ae7d75 2290 [
354ee85e 2291 'membership_id' => $dao2->membership_id,
354ee85e 2292 'ignore_admin_only' => TRUE,
b2ae7d75 2293 ], TRUE
354ee85e 2294 );
9c1bc317 2295 $statusId = $newStatus['id'] ?? NULL;
6a488035 2296
354ee85e
J
2297 //process only when status change.
2298 if ($statusId &&
2299 $statusId != $dao2->status_id
6a488035 2300 ) {
5f0a0a71
CW
2301 $memberParams = [
2302 'id' => $dao2->membership_id,
2303 'skipStatusCal' => TRUE,
2304 'skipRecentView' => TRUE,
2305 'status_id' => $statusId,
2306 'createActivity' => TRUE,
2307 ];
6a488035 2308
354ee85e 2309 //process member record.
5f0a0a71 2310 civicrm_api3('membership', 'create', $memberParams);
354ee85e 2311 $updateCount++;
6a488035 2312 }
6a488035
TO
2313 }
2314 $result['is_error'] = 0;
b2ae7d75 2315 $result['messages'] = ts('Processed %1 membership records. Updated %2 records.', [
353ffa53
TO
2316 1 => $processCount,
2317 2 => $updateCount,
b2ae7d75 2318 ]);
6a488035
TO
2319 return $result;
2320 }
2321
e136f704
O
2322 /**
2323 * Set is_override for the 'overridden until date' membership to
2324 * False and clears the 'until date' field in case the 'until date'
2325 * is equal or after today date.
2326 *
2327 * @param CRM_Core_DAO $membership
2328 * The membership to be processed
aaa7b0e6 2329 *
2330 * @throws \CiviCRM_API3_Exception
e136f704
O
2331 */
2332 private static function processOverriddenUntilDateMembership($membership) {
2333 $isOverriddenUntilDate = !empty($membership->is_override) && !empty($membership->status_override_end_date);
2334 if (!$isOverriddenUntilDate) {
2335 return;
2336 }
2337
2338 $todayDate = new DateTime();
2339 $todayDate->setTime(0, 0);
2340
2341 $overrideEndDate = new DateTime($membership->status_override_end_date);
2342 $overrideEndDate->setTime(0, 0);
2343
2344 $datesDifference = $todayDate->diff($overrideEndDate);
2345 $daysDifference = (int) $datesDifference->format('%R%a');
2346 if ($daysDifference <= 0) {
b2ae7d75 2347 $params = [
e136f704
O
2348 'id' => $membership->membership_id,
2349 'is_override' => FALSE,
2350 'status_override_end_date' => 'null',
b2ae7d75 2351 ];
e136f704
O
2352 civicrm_api3('membership', 'create', $params);
2353 }
2354 }
2355
e46e9a0b 2356 /**
688d37c6 2357 * Returns the membership types for a particular contact
6a488035
TO
2358 * who has lifetime membership without end date.
2359 *
100fef9d 2360 * @param int $contactID
e46e9a0b
EM
2361 * @param bool $isTest
2362 * @param bool $onlyLifeTime
6a488035 2363 *
e46e9a0b 2364 * @return array
6a488035 2365 */
00be9182 2366 public static function getAllContactMembership($contactID, $isTest = FALSE, $onlyLifeTime = FALSE) {
b2ae7d75 2367 $contactMembershipType = [];
6a488035
TO
2368 if (!$contactID) {
2369 return $contactMembershipType;
2370 }
2371
e0556ebe 2372 $dao = new CRM_Member_DAO_Membership();
6a488035
TO
2373 $dao->contact_id = $contactID;
2374 $pendingStatusId = array_search('Pending', CRM_Member_PseudoConstant::membershipStatus());
2375 $dao->whereAdd("status_id != $pendingStatusId");
2376
2377 if ($isTest) {
2378 $dao->is_test = $isTest;
2379 }
2380 else {
2381 $dao->whereAdd('is_test IS NULL OR is_test = 0');
2382 }
2383
2384 if ($onlyLifeTime) {
2385 $dao->whereAdd('end_date IS NULL');
2386 }
2387
2388 $dao->find();
2389 while ($dao->fetch()) {
b2ae7d75 2390 $membership = [];
6a488035
TO
2391 CRM_Core_DAO::storeValues($dao, $membership);
2392 $contactMembershipType[$dao->membership_type_id] = $membership;
2393 }
2394 return $contactMembershipType;
2395 }
2396
2397 /**
fe482240 2398 * Record contribution record associated with membership.
976e393c
MWMC
2399 * This will update an existing contribution if $params['contribution_id'] is passed in.
2400 * This will create a MembershipPayment to link the contribution and membership
6a488035 2401 *
b2363ea8
TO
2402 * @param array $params
2403 * Array of submitted params.
b2ae7d75 2404 *
976e393c 2405 * @return CRM_Contribute_BAO_Contribution
aaa7b0e6 2406 * @throws \CRM_Core_Exception
976e393c 2407 * @throws \CiviCRM_API3_Exception
6a488035 2408 */
ac0c0323 2409 public static function recordMembershipContribution(&$params) {
d824fb6e 2410 $membershipId = $params['membership_id'];
b2ae7d75 2411 $contributionParams = [];
6a488035
TO
2412 $config = CRM_Core_Config::singleton();
2413 $contributionParams['currency'] = $config->defaultCurrency;
9126e134 2414 $contributionParams['receipt_date'] = !empty($params['receipt_date']) ? $params['receipt_date'] : 'null';
9c1bc317 2415 $contributionParams['source'] = $params['contribution_source'] ?? NULL;
6a488035 2416 $contributionParams['non_deductible_amount'] = 'null';
f8df19bb 2417 $contributionParams['skipCleanMoney'] = TRUE;
9c1bc317
CW
2418 $contributionParams['payment_processor'] = $params['payment_processor_id'] ?? NULL;
2419 $contributionSoftParams = $params['soft_credit'] ?? NULL;
b2ae7d75 2420 $recordContribution = [
e0556ebe 2421 'contact_id',
77623a96 2422 'fee_amount',
e0556ebe
TO
2423 'total_amount',
2424 'receive_date',
2425 'financial_type_id',
2426 'payment_instrument_id',
2427 'trxn_id',
2428 'invoice_id',
2429 'is_test',
2430 'contribution_status_id',
2431 'check_number',
2432 'campaign_id',
2433 'is_pay_later',
2434 'membership_id',
2435 'tax_amount',
21dfd5f5 2436 'skipLineItem',
e136edcf 2437 'contribution_recur_id',
a55e39e9 2438 'pan_truncation',
2439 'card_type_id',
b2ae7d75 2440 ];
6a488035 2441 foreach ($recordContribution as $f) {
9c1bc317 2442 $contributionParams[$f] = $params[$f] ?? NULL;
6a488035
TO
2443 }
2444
f57cb50c
MWMC
2445 if (!empty($params['contribution_id'])) {
2446 $contributionParams['id'] = $params['contribution_id'];
2447 }
6a488035 2448 // make entry in batch entity batch table
a7488080 2449 if (!empty($params['batch_id'])) {
6a488035
TO
2450 $contributionParams['batch_id'] = $params['batch_id'];
2451 }
2452
91ef9be0 2453 if (!empty($params['contribution_contact_id'])) {
2454 // deal with possibility of a different person paying for contribution
2455 $contributionParams['contact_id'] = $params['contribution_contact_id'];
2456 }
2457
a7488080 2458 if (!empty($params['processPriceSet']) &&
6a488035
TO
2459 !empty($params['lineItems'])
2460 ) {
9c1bc317 2461 $contributionParams['line_item'] = $params['lineItems'] ?? NULL;
6a488035
TO
2462 }
2463
f57cb50c 2464 $contribution = CRM_Contribute_BAO_Contribution::create($contributionParams);
6a488035 2465
c206647d 2466 //CRM-13981, create new soft-credit record as to record payment from different person for this membership
d80dbc14 2467 if (!empty($contributionSoftParams)) {
9e1854a1 2468 if (!empty($params['batch_id'])) {
2469 foreach ($contributionSoftParams as $contributionSoft) {
2470 $contributionSoft['contribution_id'] = $contribution->id;
2471 $contributionSoft['currency'] = $contribution->currency;
2472 CRM_Contribute_BAO_ContributionSoft::add($contributionSoft);
2473 }
2474 }
2475 else {
e0556ebe
TO
2476 $contributionSoftParams['contribution_id'] = $contribution->id;
2477 $contributionSoftParams['currency'] = $contribution->currency;
2478 $contributionSoftParams['amount'] = $contribution->total_amount;
2479 CRM_Contribute_BAO_ContributionSoft::add($contributionSoftParams);
a54e2c55 2480 }
d80dbc14 2481 }
2482
6a488035
TO
2483 // store contribution id
2484 $params['contribution_id'] = $contribution->id;
2485
b50fb6c8
MWMC
2486 // Create membership payment if it does not already exist
2487 $membershipPayment = civicrm_api3('MembershipPayment', 'get', [
2488 'contribution_id' => $contribution->id,
2489 ]);
2490 if (empty($membershipPayment['count'])) {
c9cc6e3a 2491 civicrm_api3('MembershipPayment', 'create', [
353ffa53
TO
2492 'membership_id' => $membershipId,
2493 'contribution_id' => $contribution->id,
c9cc6e3a 2494 ]);
6a488035 2495 }
c9cc6e3a 2496
6a488035
TO
2497 return $contribution;
2498 }
2499
e46e9a0b
EM
2500 /**
2501 * @todo document me - I seem a bit out of date....
2502 */
00be9182 2503 public static function _getActTypes() {
4e636a74
AH
2504 $activityTypes = CRM_Core_PseudoConstant::activityType(TRUE, FALSE, FALSE, 'name');
2505 self::$_renewalActType = CRM_Utils_Array::key('Membership Renewal', $activityTypes);
2506 self::$_signupActType = CRM_Utils_Array::key('Membership Signup', $activityTypes);
2507 }
5624f515 2508
b5a62499
PN
2509 /**
2510 * Get all Cancelled Membership(s) for a contact
2511 *
b2363ea8
TO
2512 * @param int $contactID
2513 * Contact id.
2514 * @param bool $isTest
2515 * Mode of payment.
b5a62499 2516 *
a6c01b45 2517 * @return array
16b10e64 2518 * Array of membership type
b5a62499 2519 */
00be9182 2520 public static function getContactsCancelledMembership($contactID, $isTest = FALSE) {
b5a62499 2521 if (!$contactID) {
b2ae7d75 2522 return [];
5624f515 2523 }
b5a62499 2524 $query = 'SELECT membership_type_id FROM civicrm_membership WHERE contact_id = %1 AND status_id = %2 AND is_test = %3';
b2ae7d75 2525 $queryParams = [
2526 1 => [$contactID, 'Integer'],
2527 2 => [
7ff60806
PN
2528 // CRM-15475
2529 array_search(
4c16123d 2530 'Cancelled',
7ff60806 2531 CRM_Member_PseudoConstant::membershipStatus(
4c16123d
EM
2532 NULL,
2533 " name = 'Cancelled' ",
2534 'name',
2535 FALSE,
7ff60806
PN
2536 TRUE
2537 )
4c16123d 2538 ),
21dfd5f5 2539 'Integer',
b2ae7d75 2540 ],
2541 3 => [$isTest, 'Boolean'],
2542 ];
5624f515 2543
b5a62499 2544 $dao = CRM_Core_DAO::executeQuery($query, $queryParams);
b2ae7d75 2545 $cancelledMembershipIds = [];
b5a62499
PN
2546 while ($dao->fetch()) {
2547 $cancelledMembershipIds[] = $dao->membership_type_id;
2548 }
2549 return $cancelledMembershipIds;
2550 }
96025800 2551
c31e68ae
OT
2552 /**
2553 * Merges the memberships from otherContactID to mainContactID.
2554 *
2555 * General idea is to merge memberships in regards to their type. We
2556 * move the other contact’s contributions to the main contact’s
2557 * membership which has the same type (if any) and then we update
2558 * membership to avoid loosing `join_date`, `end_date`, and
2559 * `status_id`. In this function, we don’t touch the contributions
2560 * directly (CRM_Dedupe_Merger::moveContactBelongings() takes care
2561 * of it).
2562 *
2563 * This function adds new SQL queries to the $sqlQueries parameter.
2564 *
2565 * @param int $mainContactID
2566 * Contact id of main contact record.
2567 * @param int $otherContactID
2568 * Contact id of record which is going to merge.
2569 * @param array $sqlQueries
2570 * (reference) array of SQL queries to be executed.
2571 * @param array $tables
2572 * List of tables that have to be merged.
2573 * @param array $tableOperations
2574 * Special options/params for some tables to be merged.
2575 *
2576 * @see CRM_Dedupe_Merger::cpTables()
2577 */
2578 public static function mergeMemberships($mainContactID, $otherContactID, &$sqlQueries, $tables, $tableOperations) {
2579 /*
2580 * If the user requests not to merge memberships but to add them,
2581 * just attribute the `civicrm_membership` to the
2582 * `$mainContactID`. We have to do this here since the general
2583 * merge process is bypassed by this function.
2584 */
2585 if (array_key_exists("civicrm_membership", $tableOperations) && $tableOperations['civicrm_membership']['add']) {
2586 $sqlQueries[] = "UPDATE IGNORE civicrm_membership SET contact_id = $mainContactID WHERE contact_id = $otherContactID";
2587 return;
2588 }
2589
2590 /*
2591 * Retrieve all memberships that belongs to each contacts and
2592 * keep track of each membership type.
2593 */
b2ae7d75 2594 $mainContactMemberships = [];
2595 $otherContactMemberships = [];
c31e68ae
OT
2596
2597 $sql = "SELECT id, membership_type_id FROM civicrm_membership membership WHERE contact_id = %1";
b2ae7d75 2598 $dao = CRM_Core_DAO::executeQuery($sql, [1 => [$mainContactID, "Integer"]]);
c31e68ae
OT
2599 while ($dao->fetch()) {
2600 $mainContactMemberships[$dao->id] = $dao->membership_type_id;
2601 }
2602
b2ae7d75 2603 $dao = CRM_Core_DAO::executeQuery($sql, [1 => [$otherContactID, "Integer"]]);
c31e68ae
OT
2604 while ($dao->fetch()) {
2605 $otherContactMemberships[$dao->id] = $dao->membership_type_id;
2606 }
2607
2608 /*
2609 * For each membership, move related contributions to the main
2610 * contact’s membership (by updating `membership_payments`). Then,
2611 * update membership’s `join_date` (if the other membership’s
2612 * join_date is older) and `end_date` (if the other membership’s
2613 * `end_date` is newer) and `status_id` (if the newly calculated
2614 * status is different).
2615 *
2616 * FIXME: what should we do if we have multiple memberships with
2617 * the same type (currently we only take the first one)?
2618 */
b2ae7d75 2619 $newSql = [];
c31e68ae
OT
2620 foreach ($otherContactMemberships as $otherMembershipId => $otherMembershipTypeId) {
2621 if ($newMembershipId = array_search($otherMembershipTypeId, $mainContactMemberships)) {
2622
2623 /*
2624 * Move other membership’s contributions to the main one only
2625 * if user requested to merge contributions.
2626 */
2627 if (!empty($tables) && in_array('civicrm_contribution', $tables)) {
2628 $newSql[] = "UPDATE civicrm_membership_payment SET membership_id=$newMembershipId WHERE membership_id=$otherMembershipId";
2629 }
2630
2631 $sql = "SELECT * FROM civicrm_membership membership WHERE id = %1";
2632
2633 $newMembership = CRM_Member_DAO_Membership::findById($newMembershipId);
2634 $otherMembership = CRM_Member_DAO_Membership::findById($otherMembershipId);
2635
b2ae7d75 2636 $updates = [];
c31e68ae
OT
2637 if (new DateTime($otherMembership->join_date) < new DateTime($newMembership->join_date)) {
2638 $updates["join_date"] = $otherMembership->join_date;
2639 }
2640
2641 if (new DateTime($otherMembership->end_date) > new DateTime($newMembership->end_date)) {
2642 $updates["end_date"] = $otherMembership->end_date;
2643 }
2644
2645 if (count($updates)) {
2646
2647 /*
2648 * Update status
2649 */
2650 $status = CRM_Member_BAO_MembershipStatus::getMembershipStatusByDate(
2e1f50d6
CW
2651 $updates["start_date"] ?? $newMembership->start_date,
2652 $updates["end_date"] ?? $newMembership->end_date,
2653 $updates["join_date"] ?? $newMembership->join_date,
2cb64970 2654 'now',
c31e68ae
OT
2655 FALSE,
2656 $newMembershipId,
2657 $newMembership
2658 );
2659
2660 if (!empty($status['id']) and $status['id'] != $newMembership->status_id) {
2661 $updates['status_id'] = $status['id'];
2662 }
2663
2664 $updates_sql = [];
2665 foreach ($updates as $k => $v) {
2666 $updates_sql[] = "$k = '{$v}'";
2667 }
2668
2669 $newSql[] = sprintf("UPDATE civicrm_membership SET %s WHERE id=%s", implode(", ", $updates_sql), $newMembershipId);
2670 $newSql[] = sprintf("DELETE FROM civicrm_membership WHERE id=%s", $otherMembershipId);
2671 }
2672
2673 }
2674 }
2675
2676 $sqlQueries = array_merge($sqlQueries, $newSql);
2677 }
2678
dfd10cd7
KK
2679 /**
2680 * Update membership status to deceased.
2681 * function return the status message for updated membership.
2682 *
2683 * @param array $deceasedParams
2684 * - contact id
2685 * - is_deceased
2686 * - deceased_date
2687 *
2688 * @param string $contactType
2689 *
2690 * @return null|string
2691 * $updateMembershipMsg string status message for updated membership.
2692 */
2693 public static function updateMembershipStatus($deceasedParams, $contactType) {
2694 $updateMembershipMsg = NULL;
f4cd6a9d 2695 $contactId = $deceasedParams['contact_id'];
2696 $deceasedDate = $deceasedParams['deceased_date'];
dfd10cd7
KK
2697
2698 // process to set membership status to deceased for both active/inactive membership
2699 if ($contactId &&
2700 $contactType === 'Individual' &&
2701 !empty($deceasedParams['is_deceased'])
2702 ) {
2703
f4cd6a9d 2704 $userId = CRM_Core_Session::getLoggedInContactID() ?: $contactId;
dfd10cd7
KK
2705
2706 // get deceased status id
2707 $allStatus = CRM_Member_PseudoConstant::membershipStatus();
2708 $deceasedStatusId = array_search('Deceased', $allStatus);
2709 if (!$deceasedStatusId) {
2710 return $updateMembershipMsg;
2711 }
2712
2713 $today = time();
2714 if ($deceasedDate && strtotime($deceasedDate) > $today) {
2715 return $updateMembershipMsg;
2716 }
2717
2718 // get non deceased membership
2719 $dao = new CRM_Member_DAO_Membership();
2720 $dao->contact_id = $contactId;
2721 $dao->whereAdd("status_id != $deceasedStatusId");
2722 $dao->find();
2723 $activityTypes = CRM_Core_PseudoConstant::activityType(TRUE, FALSE, FALSE, 'name');
2724 $allStatus = CRM_Member_PseudoConstant::membershipStatus();
2725 $memCount = 0;
2726 while ($dao->fetch()) {
2727 // update status to deceased (for both active/inactive membership )
2728 CRM_Core_DAO::setFieldValue('CRM_Member_DAO_Membership', $dao->id,
2729 'status_id', $deceasedStatusId
2730 );
2731
2732 // add membership log
2733 $membershipLog = [
2734 'membership_id' => $dao->id,
2735 'status_id' => $deceasedStatusId,
2736 'start_date' => CRM_Utils_Date::isoToMysql($dao->start_date),
2737 'end_date' => CRM_Utils_Date::isoToMysql($dao->end_date),
2738 'modified_id' => $userId,
2739 'modified_date' => date('Ymd'),
2740 'membership_type_id' => $dao->membership_type_id,
2741 'max_related' => $dao->max_related,
2742 ];
2743
2744 CRM_Member_BAO_MembershipLog::add($membershipLog);
2745
2746 //create activity when membership status is changed
2747 $activityParam = [
2748 'subject' => "Status changed from {$allStatus[$dao->status_id]} to {$allStatus[$deceasedStatusId]}",
2749 'source_contact_id' => $userId,
2750 'target_contact_id' => $dao->contact_id,
2751 'source_record_id' => $dao->id,
2752 'activity_type_id' => array_search('Change Membership Status', $activityTypes),
2753 'status_id' => 2,
2754 'version' => 3,
2755 'priority_id' => 2,
2756 'activity_date_time' => date('Y-m-d H:i:s'),
2757 'is_auto' => 0,
2758 'is_current_revision' => 1,
2759 'is_deleted' => 0,
2760 ];
f4cd6a9d 2761 civicrm_api('activity', 'create', $activityParam);
dfd10cd7
KK
2762
2763 $memCount++;
2764 }
2765
2766 // set status msg
2767 if ($memCount) {
f4cd6a9d 2768 CRM_Core_Session::setStatus(ts("%1 Current membership(s) for this contact have been set to 'Deceased' status.",
dfd10cd7 2769 [1 => $memCount]
f4cd6a9d 2770 ));
dfd10cd7
KK
2771 }
2772 }
dfd10cd7
KK
2773 return $updateMembershipMsg;
2774 }
cc94c90a 2775
6a488035 2776}