Merge pull request #22428 from civicrm/5.46
[civicrm-core.git] / CRM / Contact / BAO / Relationship.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/**
bfa4da96 13 * Class CRM_Contact_BAO_Relationship.
6a488035
TO
14 */
15class CRM_Contact_BAO_Relationship extends CRM_Contact_DAO_Relationship {
16
17 /**
fe482240 18 * Various constants to indicate different type of relationships.
6a488035
TO
19 *
20 * @var int
21 */
a4dc03f7 22 const ALL = 0, PAST = 1, DISABLED = 2, CURRENT = 4, INACTIVE = 8;
6a488035 23
f871c3a9
AS
24 /**
25 * Constants for is_permission fields.
26 * Note: the slightly non-obvious ordering is due to history...
27 */
28 const NONE = 0, EDIT = 1, VIEW = 2;
29
16b745b0
MWMC
30 /**
31 * The list of column headers
32 * @var array
33 */
34 private static $columnHeaders;
35
7ecf6275 36 /**
d49c2966 37 * Create function - use the API instead.
bfa4da96 38 *
2da59b29 39 * Note that the previous create function has been renamed 'legacyCreateMultiple'
7ecf6275 40 * and this is new in 4.6
2da59b29 41 * All existing calls have been changed to legacyCreateMultiple except the api call - however, it is recommended
bfa4da96
EM
42 * that you call that as the end to end testing here is based on the api & refactoring may still be done.
43 *
16b10e64 44 * @param array $params
bfa4da96 45 *
7ecf6275
EM
46 * @return \CRM_Contact_BAO_Relationship
47 * @throws \CRM_Core_Exception
48 */
49 public static function create(&$params) {
609d8c32 50
4f9f6ede 51 $extendedParams = self::loadExistingRelationshipDetails($params);
609d8c32
TM
52 // When id is specified we always wan't to update, so we don't need to
53 // check for duplicate relations.
4f9f6ede 54 if (!isset($params['id']) && self::checkDuplicateRelationship($extendedParams, $extendedParams['contact_id_a'], $extendedParams['contact_id_b'], CRM_Utils_Array::value('id', $extendedParams, 0))) {
7ecf6275
EM
55 throw new CRM_Core_Exception('Duplicate Relationship');
56 }
4f9f6ede 57 $params = $extendedParams;
7ecf6275 58 if (self::checkValidRelationship($params, $params, 0)) {
936e43d6 59 throw new CRM_Core_Exception('Invalid Relationship');
7ecf6275
EM
60 }
61 $relationship = self::add($params);
936e43d6 62 if (!empty($params['contact_id_a'])) {
52335bb5 63 $ids = [
04ad3598
EM
64 'contactTarget' => $relationship->contact_id_b,
65 'contact' => $params['contact_id_a'],
52335bb5 66 ];
6c25ede2 67
690c56c3 68 //CRM-16087 removed additional call to function relatedMemberships which is already called by disableEnableRelationship
7a44a255 69 //resulting in membership being created twice
6946fcbf 70 if (array_key_exists('is_active', $params) && empty($params['is_active'])) {
71 $action = CRM_Core_Action::DISABLE;
72 $active = FALSE;
73 }
74 else {
75 $action = CRM_Core_Action::ENABLE;
76 $active = TRUE;
77 }
91244f1d 78 $id = empty($params['id']) ? $relationship->id : $params['id'];
79 self::disableEnableRelationship($id, $action, $params, $ids, $active);
6c25ede2 80 }
04ad3598 81
aeacb9a9
MM
82 if (empty($params['skipRecentView'])) {
83 self::addRecent($params, $relationship);
84 }
85
7ecf6275
EM
86 return $relationship;
87 }
7f3647bc 88
2da59b29 89 /**
d49c2966 90 * Create multiple relationships for one contact.
2da59b29 91 *
d49c2966
EM
92 * The relationship details are the same for each relationship except the secondary contact
93 * id can be an array.
94 *
95 * @param array $params
96 * Parameters for creating multiple relationships.
97 * The parameters are the same as for relationship create function except that the non-primary
98 * end of the relationship should be an array of one or more contact IDs.
99 * @param string $primaryContactLetter
100 * a or b to denote the primary contact for this action. The secondary may be multiple contacts
101 * and should be an array.
2da59b29
EM
102 *
103 * @return array
104 * @throws \CRM_Core_Exception
105 */
106 public static function createMultiple($params, $primaryContactLetter) {
107 $secondaryContactLetter = ($primaryContactLetter == 'a') ? 'b' : 'a';
108 $secondaryContactIDs = $params['contact_id_' . $secondaryContactLetter];
109 $valid = $invalid = $duplicate = $saved = 0;
52335bb5 110 $relationshipIDs = [];
2da59b29
EM
111 foreach ($secondaryContactIDs as $secondaryContactID) {
112 try {
113 $params['contact_id_' . $secondaryContactLetter] = $secondaryContactID;
114 $relationship = civicrm_api3('relationship', 'create', $params);
d49c2966 115 $relationshipIDs[] = $relationship['id'];
bfa4da96 116 $valid++;
2da59b29
EM
117 }
118 catch (CiviCRM_API3_Exception $e) {
119 switch ($e->getMessage()) {
bfa4da96
EM
120 case 'Duplicate Relationship':
121 $duplicate++;
2da59b29
EM
122 break;
123
bfa4da96
EM
124 case 'Invalid Relationship':
125 $invalid++;
2da59b29
EM
126 break;
127
128 default:
129 throw new CRM_Core_Exception('unknown relationship create error ' . $e->getMessage());
130 }
131 }
132 }
133
52335bb5 134 return [
d49c2966
EM
135 'valid' => $valid,
136 'invalid' => $invalid,
137 'duplicate' => $duplicate,
138 'saved' => $saved,
139 'relationship_ids' => $relationshipIDs,
52335bb5 140 ];
2da59b29
EM
141 }
142
6a488035 143 /**
152f6da6
EM
144 * Only called from import now... plus one place outside of core & tests.
145 *
146 * @todo - deprecate more aggressively - will involve copying to the import
147 * class, adding a deprecation notice here & removing from tests.
148 *
fe482240 149 * Takes an associative array and creates a relationship object.
52335bb5 150 *
7ecf6275
EM
151 * @deprecated For single creates use the api instead (it's tested).
152 * For multiple a new variant of this function needs to be written and migrated to as this is a bit
153 * nasty
6a488035 154 *
77c5b619
TO
155 * @param array $params
156 * (reference ) an assoc array of name/value pairs.
157 * @param array $ids
158 * The array that holds all the db ids.
16b10e64 159 * per http://wiki.civicrm.org/confluence/display/CRM/Database+layer
26dcc9eb 160 * "we are moving away from the $ids param "
6a488035 161 *
52335bb5 162 * @return array
163 * @throws \CRM_Core_Exception
6a488035 164 */
52335bb5 165 public static function legacyCreateMultiple(&$params, $ids = []) {
6a488035 166 $valid = $invalid = $duplicate = $saved = 0;
52335bb5 167 $relationships = $relationshipIds = [];
dab33b37
EM
168 // clarify that the only key ever pass in the ids array is 'contact'
169 // There is legacy handling for other keys but a universe search on
170 // calls to this function (not supported to be called from outside core)
171 // only returns 2 calls - one in CRM_Contact_Import_Parser_Contact
172 // and the other in jma grant applications (CRM_Grant_Form_Grant_Confirm)
173 // both only pass in contact as a key here.
174 $ids = ['contact' => $ids['contact']];
175 // Likewise neither place ever passed in relationshipID
176 $relationshipId = NULL;
26dcc9eb 177
6a488035
TO
178 if (!$relationshipId) {
179 // creating a new relationship
52335bb5 180 $relationshipIds = [];
d6979a54 181 foreach (array_keys($params['contact_check']) as $relatedContactID) {
6a488035
TO
182 // check if the relationship is valid between contacts.
183 // step 1: check if the relationship is valid if not valid skip and keep the count
184 // step 2: check the if two contacts already have a relationship if yes skip and keep the count
185 // step 3: if valid relationship then add the relation and keep the count
186
187 // step 1
d6979a54
EM
188 $contactFields = self::setContactABFromIDs($params, $ids, $relatedContactID);
189 $errors = self::checkValidRelationship($contactFields, $ids, $relatedContactID);
6a488035
TO
190 if ($errors) {
191 $invalid++;
192 continue;
193 }
194
ef6463ad 195 //CRM-16978:check duplicate relationship as per case id.
ce9a0d03 196 // https://issues.civicrm.org/jira/browse/CRM-16978
ef6463ad 197 if ($caseId = CRM_Utils_Array::value('case_id', $params)) {
ce9a0d03 198 CRM_Core_Error::deprecatedWarning('this code is believed to be unreachable');
ef6463ad
SB
199 $contactFields['case_id'] = $caseId;
200 }
6a488035 201 if (
7f3647bc 202 self::checkDuplicateRelationship(
2001204e 203 $contactFields,
7f3647bc
TO
204 CRM_Utils_Array::value('contact', $ids),
205 // step 2
d6979a54 206 $relatedContactID
7f3647bc 207 )
6a488035
TO
208 ) {
209 $duplicate++;
210 continue;
211 }
2001204e 212
bfa4da96 213 $singleInstanceParams = array_merge($params, $contactFields);
eff45dce 214 $relationship = self::add($singleInstanceParams);
6a488035 215 $relationshipIds[] = $relationship->id;
26dcc9eb 216 $relationships[$relationship->id] = $relationship;
6a488035
TO
217 $valid++;
218 }
219 // editing the relationship
220 }
6a488035
TO
221
222 // do not add to recent items for import, CRM-4399
8cc574cf 223 if (!(!empty($params['skipRecentView']) || $invalid || $duplicate)) {
7ecf6275 224 self::addRecent($params, $relationship);
6a488035
TO
225 }
226
c64f77cc 227 return [$valid, $duplicate];
6a488035
TO
228 }
229
230 /**
bfa4da96 231 * This is the function that check/add if the relationship created is valid.
6a488035 232 *
77c5b619 233 * @param array $params
f42bcfb1 234 * Array of name/value pairs.
77c5b619
TO
235 * @param array $ids
236 * The array that holds all the db ids.
6a488035 237 *
c490a46a 238 * @return CRM_Contact_BAO_Relationship
f42bcfb1
MD
239 *
240 * @throws \CiviCRM_API3_Exception
6a488035 241 */
4535334c 242 public static function add($params, $ids = []) {
e5ccee43 243 $params['id'] = CRM_Utils_Array::value('relationship', $ids, CRM_Utils_Array::value('id', $params));
f0fc26a5 244
49f8272d 245 $hook = 'create';
e5ccee43 246 if ($params['id']) {
49f8272d 247 $hook = 'edit';
6a488035 248 }
e5ccee43 249 CRM_Utils_Hook::pre($hook, 'Relationship', $params['id'], $params);
6a488035 250
9c1bc317 251 $relationshipTypes = $params['relationship_type_id'] ?? NULL;
f0fc26a5
DL
252 // explode the string with _ to get the relationship type id
253 // and to know which contact has to be inserted in
6a488035 254 // contact_id_a and which one in contact_id_b
50288497
MWMC
255 list($relationshipTypeID) = explode('_', $relationshipTypes);
256
257 $relationship = new CRM_Contact_BAO_Relationship();
258 if (!empty($params['id'])) {
259 $relationship->id = $params['id'];
260 // Only load the relationship if we're missing required params
261 $requiredParams = ['contact_id_a', 'contact_id_b', 'relationship_type_id'];
262 foreach ($requiredParams as $requiredKey) {
263 if (!isset($params[$requiredKey])) {
264 $relationship->find(TRUE);
265 break;
266 }
267 }
268
269 }
270 $relationship->copyValues($params);
271 // @todo we could probably set $params['relationship_type_id'] above but it's unclear
272 // what that would do with the code below this. So for now be conservative and set it manually.
273 if (!empty($relationshipTypeID)) {
274 $relationship->relationship_type_id = $relationshipTypeID;
275 }
276
277 $params['contact_id_a'] = $relationship->contact_id_a;
278 $params['contact_id_b'] = $relationship->contact_id_b;
6a488035 279
f0fc26a5
DL
280 // check if the relationship type is Head of Household then update the
281 // household's primary contact with this contact.
eabf1c29
MWMC
282 try {
283 $headOfHouseHoldID = civicrm_api3('RelationshipType', 'getvalue', [
284 'return' => "id",
285 'name_a_b' => "Head of Household for",
286 ]);
287 if ($relationshipTypeID == $headOfHouseHoldID) {
288 CRM_Contact_BAO_Household::updatePrimaryContact($relationship->contact_id_b, $relationship->contact_id_a);
289 }
290 }
291 catch (Exception $e) {
292 // No "Head of Household" relationship found so we skip specific processing
df0c42cc 293 }
f42bcfb1 294
50288497
MWMC
295 if (!empty($params['id']) && self::isCurrentEmployerNeedingToBeCleared($relationship->toArray(), $params['id'], $relationshipTypeID)) {
296 CRM_Contact_BAO_Contact_Utils::clearCurrentEmployer($relationship->contact_id_a);
297 }
6a488035 298
52335bb5 299 $dateFields = ['end_date', 'start_date'];
6a488035 300
9b873358
TO
301 foreach (self::getdefaults() as $defaultField => $defaultValue) {
302 if (isset($params[$defaultField])) {
303 if (in_array($defaultField, $dateFields)) {
6a488035 304 $relationship->$defaultField = CRM_Utils_Date::format(CRM_Utils_Array::value($defaultField, $params));
9b873358 305 if (!$relationship->$defaultField) {
6a488035
TO
306 $relationship->$defaultField = 'NULL';
307 }
308 }
92e4c2a5 309 else {
6a488035
TO
310 $relationship->$defaultField = $params[$defaultField];
311 }
312 }
e5ccee43 313 elseif (empty($params['id'])) {
6a488035
TO
314 $relationship->$defaultField = $defaultValue;
315 }
316 }
317
318 $relationship->save();
f42bcfb1
MD
319 // is_current_employer is an optional parameter that triggers updating the employer_id field to reflect
320 // the relationship being updated. As of writing only truthy versions of the parameter are respected.
321 // https://github.com/civicrm/civicrm-core/pull/13331 attempted to cover both but stalled in QA
322 // so currently we have a cut down version.
323 if (!empty($params['is_current_employer'])) {
324 if (!$relationship->relationship_type_id || !$relationship->contact_id_a || !$relationship->contact_id_b) {
325 $relationship->fetch();
326 }
327 if (self::isRelationshipTypeCurrentEmployer($relationship->relationship_type_id)) {
328 CRM_Contact_BAO_Contact_Utils::setCurrentEmployer([$relationship->contact_id_a => $relationship->contact_id_b]);
329 }
330 }
6a488035 331 // add custom field values
a7488080 332 if (!empty($params['custom'])) {
6a488035
TO
333 CRM_Core_BAO_CustomValueTable::store($params['custom'], 'civicrm_relationship', $relationship->id);
334 }
335
5c208c3f 336 CRM_Utils_Hook::post($hook, 'Relationship', $relationship->id, $relationship);
6a488035
TO
337
338 return $relationship;
339 }
7ecf6275
EM
340
341 /**
fe482240 342 * Add relationship to recent links.
bfa4da96 343 *
7ecf6275
EM
344 * @param array $params
345 * @param CRM_Contact_DAO_Relationship $relationship
346 */
347 public static function addRecent($params, $relationship) {
348 $url = CRM_Utils_System::url('civicrm/contact/view/rel',
349 "action=view&reset=1&id={$relationship->id}&cid={$relationship->contact_id_a}&context=home"
350 );
351 $session = CRM_Core_Session::singleton();
52335bb5 352 $recentOther = [];
7ecf6275
EM
353 if (($session->get('userID') == $relationship->contact_id_a) ||
354 CRM_Contact_BAO_Contact_Permission::allow($relationship->contact_id_a, CRM_Core_Permission::EDIT)
355 ) {
356 $rType = substr(CRM_Utils_Array::value('relationship_type_id', $params), -3);
52335bb5 357 $recentOther = [
7ecf6275
EM
358 'editUrl' => CRM_Utils_System::url('civicrm/contact/view/rel',
359 "action=update&reset=1&id={$relationship->id}&cid={$relationship->contact_id_a}&rtype={$rType}&context=home"
360 ),
361 'deleteUrl' => CRM_Utils_System::url('civicrm/contact/view/rel',
362 "action=delete&reset=1&id={$relationship->id}&cid={$relationship->contact_id_a}&rtype={$rType}&context=home"
363 ),
52335bb5 364 ];
7ecf6275
EM
365 }
366 $title = CRM_Contact_BAO_Contact::displayName($relationship->contact_id_a) . ' (' . CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_RelationshipType',
367 $relationship->relationship_type_id, 'label_a_b'
368 ) . ' ' . CRM_Contact_BAO_Contact::displayName($relationship->contact_id_b) . ')';
369
370 CRM_Utils_Recent::add($title,
371 $url,
372 $relationship->id,
373 'Relationship',
374 $relationship->contact_id_a,
375 NULL,
376 $recentOther
377 );
378 }
379
d49c2966
EM
380 /**
381 * Load contact ids and relationship type id when doing a create call if not provided.
382 *
383 * There are are various checks done in create which require this information which is optional
384 * when using id.
385 *
386 * @param array $params
387 * Parameters passed to create call.
388 *
389 * @return array
390 * Parameters with missing fields added if required.
391 */
392 public static function loadExistingRelationshipDetails($params) {
393 if (!empty($params['contact_id_a'])
394 && !empty($params['contact_id_b'])
395 && is_numeric($params['relationship_type_id'])) {
396 return $params;
397 }
398 if (empty($params['id'])) {
399 return $params;
400 }
401
52335bb5 402 $fieldsToFill = ['contact_id_a', 'contact_id_b', 'relationship_type_id'];
403 $result = CRM_Core_DAO::executeQuery("SELECT " . implode(',', $fieldsToFill) . " FROM civicrm_relationship WHERE id = %1", [
404 1 => [
d49c2966
EM
405 $params['id'],
406 'Integer',
52335bb5 407 ],
408 ]);
d49c2966
EM
409 while ($result->fetch()) {
410 foreach ($fieldsToFill as $field) {
411 $params[$field] = !empty($params[$field]) ? $params[$field] : $result->$field;
412 }
413 }
414 return $params;
415 }
416
7ecf6275 417 /**
bfa4da96 418 * Resolve passed in contact IDs to contact_id_a & contact_id_b.
eff45dce 419 *
16b10e64 420 * @param array $params
7ecf6275
EM
421 * @param array $ids
422 * @param null $contactID
eff45dce
EM
423 *
424 * @return array
7ecf6275
EM
425 * @throws \CRM_Core_Exception
426 */
52335bb5 427 public static function setContactABFromIDs($params, $ids = [], $contactID = NULL) {
428 $returnFields = [];
d49c2966
EM
429
430 // $ids['contact'] is deprecated but comes from legacyCreateMultiple function.
7ecf6275
EM
431 if (empty($ids['contact'])) {
432 if (!empty($params['id'])) {
d49c2966 433 return self::loadExistingRelationshipDetails($params);
7ecf6275
EM
434 }
435 throw new CRM_Core_Exception('Cannot create relationship, insufficient contact IDs provided');
436 }
2001204e 437 if (isset($params['relationship_type_id']) && !is_numeric($params['relationship_type_id'])) {
9c1bc317 438 $relationshipTypes = $params['relationship_type_id'] ?? NULL;
eff45dce 439 list($relationshipTypeID, $first) = explode('_', $relationshipTypes);
2001204e
EM
440 $returnFields['relationship_type_id'] = $relationshipTypeID;
441
52335bb5 442 foreach (['a', 'b'] as $contactLetter) {
d49c2966
EM
443 if (empty($params['contact_' . $contactLetter])) {
444 if ($first == $contactLetter) {
9c1bc317 445 $returnFields['contact_id_' . $contactLetter] = $ids['contact'] ?? NULL;
d49c2966
EM
446 }
447 else {
448 $returnFields['contact_id_' . $contactLetter] = $contactID;
449 }
7ecf6275
EM
450 }
451 }
452 }
d49c2966 453
eff45dce 454 return $returnFields;
7ecf6275
EM
455 }
456
6a488035 457 /**
d49c2966 458 * Specify defaults for creating a relationship.
6a488035 459 *
a6c01b45
CW
460 * @return array
461 * array of defaults for creating relationship
6a488035 462 */
00be9182 463 public static function getdefaults() {
52335bb5 464 return [
0c1bf04e 465 'is_active' => 1,
f871c3a9
AS
466 'is_permission_a_b' => self::NONE,
467 'is_permission_b_a' => self::NONE,
6a488035
TO
468 'description' => '',
469 'start_date' => 'NULL',
470 'case_id' => NULL,
471 'end_date' => 'NULL',
52335bb5 472 ];
6a488035
TO
473 }
474
6a488035 475 /**
fe482240 476 * Check if there is data to create the object.
6a488035 477 *
77c5b619 478 * @param array $params
6a488035 479 *
4dd83d9e 480 * @deprecated
c301f76e 481 * @return bool
6a488035 482 */
e01bf597 483 public static function dataExists($params) {
4dd83d9e 484 CRM_Core_Error::deprecatedFunctionWarning('obsolete');
e01bf597 485 return (isset($params['contact_check']) && is_array($params['contact_check']));
6a488035
TO
486 }
487
488 /**
100fef9d 489 * Get get list of relationship type based on the contact type.
6a488035 490 *
77c5b619
TO
491 * @param int $contactId
492 * This is the contact id of the current contact.
fd31fa4c 493 * @param null $contactSuffix
77c5b619
TO
494 * @param string $relationshipId
495 * The id of the existing relationship if any.
496 * @param string $contactType
497 * Contact type.
498 * @param bool $all
499 * If true returns relationship types in both the direction.
500 * @param string $column
501 * Name/label that going to retrieve from db.
fd31fa4c 502 * @param bool $biDirectional
4091c7e7 503 * @param array $contactSubType
bfa4da96 504 * Includes relationship types between this subtype.
77c5b619 505 * @param bool $onlySubTypeRelationTypes
bfa4da96
EM
506 * If set only subtype which is passed by $contactSubType
507 * related relationship types get return
6a488035 508 *
a6c01b45
CW
509 * @return array
510 * array reference of all relationship types with context to current contact.
6a488035 511 */
c301f76e 512 public static function getContactRelationshipType(
51ccfbbe 513 $contactId = NULL,
6a488035
TO
514 $contactSuffix = NULL,
515 $relationshipId = NULL,
516 $contactType = NULL,
517 $all = FALSE,
518 $column = 'label',
519 $biDirectional = TRUE,
520 $contactSubType = NULL,
521 $onlySubTypeRelationTypes = FALSE
522 ) {
d49c2966 523
52335bb5 524 $relationshipType = [];
6a488035
TO
525 $allRelationshipType = CRM_Core_PseudoConstant::relationshipType($column);
526
527 $otherContactType = NULL;
528 if ($relationshipId) {
529 $relationship = new CRM_Contact_DAO_Relationship();
530 $relationship->id = $relationshipId;
531 if ($relationship->find(TRUE)) {
532 $contact = new CRM_Contact_DAO_Contact();
b277f8d1 533 $contact->id = ($relationship->contact_id_a == $contactId) ? $relationship->contact_id_b : $relationship->contact_id_a;
6a488035
TO
534
535 if ($contact->find(TRUE)) {
536 $otherContactType = $contact->contact_type;
537 //CRM-5125 for contact subtype specific relationshiptypes
538 if ($contact->contact_sub_type) {
539 $otherContactSubType = $contact->contact_sub_type;
540 }
541 }
542 }
543 }
544
318c4e1c 545 $contactSubType = (array) $contactSubType;
6a488035
TO
546 if ($contactId) {
547 $contactType = CRM_Contact_BAO_Contact::getContactType($contactId);
548 $contactSubType = CRM_Contact_BAO_Contact::getContactSubType($contactId);
549 }
550
551 foreach ($allRelationshipType as $key => $value) {
552 // the contact type is required or matches
553 if (((!$value['contact_type_a']) ||
554 $value['contact_type_a'] == $contactType
555 ) &&
556 // the other contact type is required or present or matches
557 ((!$value['contact_type_b']) ||
558 (!$otherContactType) ||
559 $value['contact_type_b'] == $otherContactType
560 ) &&
91c0d0d8 561 (in_array($value['contact_sub_type_a'], $contactSubType) ||
91c0d0d8 562 (!$value['contact_sub_type_a'] && !$onlySubTypeRelationTypes)
6a488035
TO
563 )
564 ) {
565 $relationshipType[$key . '_a_b'] = $value["{$column}_a_b"];
566 }
6a488035
TO
567
568 if (((!$value['contact_type_b']) ||
569 $value['contact_type_b'] == $contactType
570 ) &&
571 ((!$value['contact_type_a']) ||
572 (!$otherContactType) ||
573 $value['contact_type_a'] == $otherContactType
574 ) &&
91c0d0d8 575 (in_array($value['contact_sub_type_b'], $contactSubType) ||
91c0d0d8 576 (!$value['contact_sub_type_b'] && !$onlySubTypeRelationTypes)
6a488035
TO
577 )
578 ) {
579 $relationshipType[$key . '_b_a'] = $value["{$column}_b_a"];
580 }
6a488035
TO
581
582 if ($all) {
583 $relationshipType[$key . '_a_b'] = $value["{$column}_a_b"];
584 $relationshipType[$key . '_b_a'] = $value["{$column}_b_a"];
585 }
586 }
587
588 if ($biDirectional) {
02e028a0 589 $relationshipType = self::removeRelationshipTypeDuplicates($relationshipType, $contactSuffix);
6a488035
TO
590 }
591
592 // sort the relationshipType in ascending order CRM-7736
593 asort($relationshipType);
594 return $relationshipType;
595 }
596
02e028a0
AS
597 /**
598 * Given a list of relationship types, return the list with duplicate types
599 * removed, being careful to retain only the duplicate which matches the given
600 * 'a_b' or 'b_a' suffix.
601 *
602 * @param array $relationshipTypeList A list of relationship types, in the format
603 * returned by self::getContactRelationshipType().
604 * @param string $suffix Either 'a_b' or 'b_a'; defaults to 'a_b'
605 *
606 * @return array The modified value of $relationshipType
607 */
608 public static function removeRelationshipTypeDuplicates($relationshipTypeList, $suffix = NULL) {
609 if (empty($suffix)) {
610 $suffix = 'a_b';
611 }
612
613 // Find those labels which are listed more than once.
614 $duplicateValues = array_diff_assoc($relationshipTypeList, array_unique($relationshipTypeList));
615
616 // For each duplicate label, find its keys, and remove from $relationshipType
617 // the key which does not match $suffix.
618 foreach ($duplicateValues as $value) {
619 $keys = array_keys($relationshipTypeList, $value);
620 foreach ($keys as $key) {
621 if (substr($key, -3) != $suffix) {
622 unset($relationshipTypeList[$key]);
623 }
624 }
625 }
626 return $relationshipTypeList;
627 }
628
86538308 629 /**
98a06ae0
EM
630 * Delete current employer relationship.
631 *
100fef9d 632 * @param int $id
98a06ae0 633 * @param int $action
86538308
EM
634 *
635 * @return CRM_Contact_DAO_Relationship
636 */
00be9182 637 public static function clearCurrentEmployer($id, $action) {
6a488035
TO
638 $relationship = new CRM_Contact_DAO_Relationship();
639 $relationship->id = $id;
640 $relationship->find(TRUE);
641
642 //to delete relationship between household and individual \
98a06ae0 643 //or between individual and organization
6a488035 644 if (($action & CRM_Core_Action::DISABLE) || ($action & CRM_Core_Action::DELETE)) {
52335bb5 645 $relTypes = CRM_Utils_Array::index(['name_a_b'], CRM_Core_PseudoConstant::relationshipType('name'));
81d1f764
MM
646 if (
647 (isset($relTypes['Employee of']) && $relationship->relationship_type_id == $relTypes['Employee of']['id']) ||
69ed0eb1 648 (isset($relTypes['Household Member of']) && $relationship->relationship_type_id == $relTypes['Household Member of']['id'])
7f3647bc 649 ) {
05802b8e
DG
650 $sharedContact = new CRM_Contact_DAO_Contact();
651 $sharedContact->id = $relationship->contact_id_a;
652 $sharedContact->find(TRUE);
653
bd655ee6
MV
654 // CRM-15881 UPDATES
655 // changed FROM "...relationship->relationship_type_id == 4..." TO "...relationship->relationship_type_id == 5..."
656 // As the system should be looking for type "employer of" (id 5) and not "sibling of" (id 4)
3421dcee
MV
657 // As suggested by @davecivicrm, the employee relationship type id is fetched using the CRM_Core_DAO::getFieldValue() class and method, since these ids differ from system to system.
658 $employerRelTypeId = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_RelationshipType', 'Employee of', 'id', 'name_a_b');
659
660 if ($relationship->relationship_type_id == $employerRelTypeId && $relationship->contact_id_b == $sharedContact->employer_id) {
05802b8e
DG
661 CRM_Contact_BAO_Contact_Utils::clearCurrentEmployer($relationship->contact_id_a);
662 }
3421dcee 663
6a488035
TO
664 }
665 }
7f3647bc 666 return $relationship;
6a488035
TO
667 }
668
669 /**
fe482240 670 * Delete the relationship.
6a488035 671 *
77c5b619
TO
672 * @param int $id
673 * Relationship id.
6a488035 674 *
310c2031 675 * @return CRM_Contact_DAO_Relationship
676 *
52335bb5 677 * @throws \CRM_Core_Exception
310c2031 678 * @throws \CiviCRM_API3_Exception
6a488035 679 */
00be9182 680 public static function del($id) {
6a488035 681 // delete from relationship table
be12df5a 682 CRM_Utils_Hook::pre('delete', 'Relationship', $id);
6a488035
TO
683
684 $relationship = self::clearCurrentEmployer($id, CRM_Core_Action::DELETE);
310c2031 685 $relationship->delete();
6a488035
TO
686 if (CRM_Core_Permission::access('CiviMember')) {
687 // create $params array which isrequired to delete memberships
688 // of the related contacts.
52335bb5 689 $params = [
6a488035 690 'relationship_type_id' => "{$relationship->relationship_type_id}_a_b",
52335bb5 691 'contact_check' => [$relationship->contact_id_b => 1],
692 ];
6a488035 693
52335bb5 694 $ids = [];
6a488035
TO
695 // calling relatedMemberships to delete the memberships of
696 // related contacts.
697 self::relatedMemberships($relationship->contact_id_a,
698 $params,
699 $ids,
700 CRM_Core_Action::DELETE,
701 FALSE
702 );
703 }
704
6a488035
TO
705 CRM_Core_Session::setStatus(ts('Selected relationship has been deleted successfully.'), ts('Record Deleted'), 'success');
706
5c208c3f 707 CRM_Utils_Hook::post('delete', 'Relationship', $id, $relationship);
6a488035 708
6a488035
TO
709 return $relationship;
710 }
711
712 /**
98a06ae0 713 * Disable/enable the relationship.
6a488035 714 *
77c5b619
TO
715 * @param int $id
716 * Relationship id.
6a488035 717 *
7a44a255
EM
718 * @param int $action
719 * @param array $params
720 * @param array $ids
721 * @param bool $active
52335bb5 722 *
723 * @throws \CRM_Core_Exception
310c2031 724 * @throws \CiviCRM_API3_Exception
6a488035 725 */
52335bb5 726 public static function disableEnableRelationship($id, $action, $params = [], $ids = [], $active = FALSE) {
6a488035 727 $relationship = self::clearCurrentEmployer($id, $action);
25f1372e 728
1b5fad8a 729 if ($id) {
25f1372e 730 // create $params array which is required to delete memberships
6a488035 731 // of the related contacts.
690c56c3 732 if (empty($params)) {
52335bb5 733 $params = [
690c56c3 734 'relationship_type_id' => "{$relationship->relationship_type_id}_a_b",
52335bb5 735 'contact_check' => [$relationship->contact_id_b => 1],
736 ];
690c56c3 737 }
690c56c3 738 $contact_id_a = empty($params['contact_id_a']) ? $relationship->contact_id_a : $params['contact_id_a'];
6a488035
TO
739 // calling relatedMemberships to delete/add the memberships of
740 // related contacts.
741 if ($action & CRM_Core_Action::DISABLE) {
310c2031 742 // @todo this could call a subset of the function that just relates to
743 // cleaning up no-longer-inherited relationships
690c56c3 744 CRM_Contact_BAO_Relationship::relatedMemberships($contact_id_a,
6a488035
TO
745 $params,
746 $ids,
747 CRM_Core_Action::DELETE,
690c56c3 748 $active
6a488035
TO
749 );
750 }
751 elseif ($action & CRM_Core_Action::ENABLE) {
91244f1d 752 $ids['contact'] = empty($ids['contact']) ? $contact_id_a : $ids['contact'];
690c56c3 753 CRM_Contact_BAO_Relationship::relatedMemberships($contact_id_a,
6a488035
TO
754 $params,
755 $ids,
690c56c3 756 empty($params['id']) ? CRM_Core_Action::ADD : CRM_Core_Action::UPDATE,
757 $active
6a488035
TO
758 );
759 }
760 }
761 }
762
763 /**
fe482240 764 * Delete the object records that are associated with this contact.
6a488035 765 *
77c5b619
TO
766 * @param int $contactId
767 * Id of the contact to delete.
6a488035 768 */
00be9182 769 public static function deleteContact($contactId) {
6a488035
TO
770 $relationship = new CRM_Contact_DAO_Relationship();
771 $relationship->contact_id_a = $contactId;
772 $relationship->delete();
773
774 $relationship = new CRM_Contact_DAO_Relationship();
775 $relationship->contact_id_b = $contactId;
776 $relationship->delete();
777
778 CRM_Contact_BAO_Household::updatePrimaryContact(NULL, $contactId);
779 }
780
781 /**
fe482240 782 * Get the other contact in a relationship.
6a488035 783 *
77c5b619
TO
784 * @param int $id
785 * Relationship id.
6a488035 786 *
bb088986 787 * $returns returns the contact ids in the relationship
77b97be7
EM
788 *
789 * @return \CRM_Contact_DAO_Relationship
6a488035 790 */
eff45dce 791 public static function getRelationshipByID($id) {
6a488035
TO
792 $relationship = new CRM_Contact_DAO_Relationship();
793
794 $relationship->id = $id;
795 $relationship->selectAdd();
796 $relationship->selectAdd('contact_id_a, contact_id_b');
797 $relationship->find(TRUE);
798
799 return $relationship;
800 }
801
802 /**
fe482240 803 * Check if the relationship type selected between two contacts is correct.
6a488035 804 *
77c5b619
TO
805 * @param int $contact_a
806 * 1st contact id.
807 * @param int $contact_b
808 * 2nd contact id.
809 * @param int $relationshipTypeId
810 * Relationship type id.
6a488035 811 *
c301f76e 812 * @return bool
a6c01b45 813 * true if it is valid relationship else false
6a488035 814 */
00be9182 815 public static function checkRelationshipType($contact_a, $contact_b, $relationshipTypeId) {
6a488035
TO
816 $relationshipType = new CRM_Contact_DAO_RelationshipType();
817 $relationshipType->id = $relationshipTypeId;
818 $relationshipType->selectAdd();
819 $relationshipType->selectAdd('contact_type_a, contact_type_b, contact_sub_type_a, contact_sub_type_b');
820 if ($relationshipType->find(TRUE)) {
821 $contact_type_a = CRM_Contact_BAO_Contact::getContactType($contact_a);
822 $contact_type_b = CRM_Contact_BAO_Contact::getContactType($contact_b);
823
824 $contact_sub_type_a = CRM_Contact_BAO_Contact::getContactSubType($contact_a);
825 $contact_sub_type_b = CRM_Contact_BAO_Contact::getContactSubType($contact_b);
826
827 if (((!$relationshipType->contact_type_a) || ($relationshipType->contact_type_a == $contact_type_a)) &&
828 ((!$relationshipType->contact_type_b) || ($relationshipType->contact_type_b == $contact_type_b)) &&
829 ((!$relationshipType->contact_sub_type_a) || (in_array($relationshipType->contact_sub_type_a,
7f3647bc
TO
830 $contact_sub_type_a
831 ))) &&
6a488035 832 ((!$relationshipType->contact_sub_type_b) || (in_array($relationshipType->contact_sub_type_b,
7f3647bc
TO
833 $contact_sub_type_b
834 )))
6a488035
TO
835 ) {
836 return TRUE;
837 }
838 else {
839 return FALSE;
840 }
841 }
842 return FALSE;
843 }
844
845 /**
fe482240 846 * This function does the validtion for valid relationship.
6a488035 847 *
77c5b619
TO
848 * @param array $params
849 * This array contains the values there are subitted by the form.
850 * @param array $ids
851 * The array that holds all the db ids.
852 * @param int $contactId
853 * This is contact id for adding relationship.
6a488035 854 *
2a6da8d7 855 * @return string
6a488035 856 */
7ecf6275 857 public static function checkValidRelationship($params, $ids, $contactId) {
6a488035 858 $errors = '';
6a488035
TO
859 // function to check if the relationship selected is correct
860 // i.e. employer relationship can exit between Individual and Organization (not between Individual and Individual)
c4dcea5b
EM
861 if (!CRM_Contact_BAO_Relationship::checkRelationshipType($params['contact_id_a'], $params['contact_id_b'],
862 $params['relationship_type_id'])) {
6a488035
TO
863 $errors = 'Please select valid relationship between these two contacts.';
864 }
865 return $errors;
866 }
867
868 /**
fe482240 869 * This function checks for duplicate relationship.
6a488035 870 *
77c5b619 871 * @param array $params
22b28b5c 872 * An assoc array of name/value pairs.
77c5b619
TO
873 * @param int $id
874 * This the id of the contact whom we are adding relationship.
875 * @param int $contactId
876 * This is contact id for adding relationship.
877 * @param int $relationshipId
878 * This is relationship id for the contact.
6a488035 879 *
c301f76e 880 * @return bool
a6c01b45 881 * true if record exists else false
6a488035 882 */
22b28b5c 883 public static function checkDuplicateRelationship($params, $id, $contactId = 0, $relationshipId = 0) {
9c1bc317 884 $relationshipTypeId = $params['relationship_type_id'] ?? NULL;
decc60a0 885 list($type) = explode('_', $relationshipTypeId);
6a488035 886
f0fc26a5
DL
887 $queryString = "
888SELECT id
889FROM civicrm_relationship
890WHERE relationship_type_id = " . CRM_Utils_Type::escape($type, 'Integer');
6a488035
TO
891
892 /*
e70a7fc0
TO
893 * CRM-11792 - date fields from API are in ISO format, but this function
894 * supports date arrays BAO has increasingly standardised to ISO format
895 * so I believe this function should support ISO rather than make API
896 * format it - however, need to support array format for now to avoid breakage
2da59b29 897 * @ time of writing this function is called from Relationship::legacyCreateMultiple (twice)
e70a7fc0
TO
898 * CRM_BAO_Contact_Utils::clearCurrentEmployer (seemingly without dates)
899 * CRM_Contact_Form_Task_AddToOrganization::postProcess &
900 * CRM_Contact_Form_Task_AddToHousehold::postProcess
901 * (I don't think the last 2 support dates but not sure
902 */
6a488035 903
52335bb5 904 $dateFields = ['end_date', 'start_date'];
9b873358 905 foreach ($dateFields as $dateField) {
22e263ad 906 if (array_key_exists($dateField, $params)) {
9b873358 907 if (empty($params[$dateField]) || $params[$dateField] == 'null') {
f0fc26a5
DL
908 //this is most likely coming from an api call & probably loaded
909 // from the DB to deal with some of the
910 // other myriad of excessive checks still in place both in
911 // the api & the create functions
6a488035
TO
912 $queryString .= " AND $dateField IS NULL";
913 continue;
914 }
9b873358 915 elseif (is_array($params[$dateField])) {
f0fc26a5
DL
916 $queryString .= " AND $dateField = " .
917 CRM_Utils_Type::escape(CRM_Utils_Date::format($params[$dateField]), 'Date');
6a488035 918 }
f0fc26a5
DL
919 else {
920 $queryString .= " AND $dateField = " .
921 CRM_Utils_Type::escape($params[$dateField], 'Date');
6a488035
TO
922 }
923 }
924 }
925
926 $queryString .=
22b28b5c
EM
927 ' AND ( ( contact_id_a = ' . CRM_Utils_Type::escape($id, 'Integer') .
928 ' AND contact_id_b = ' . CRM_Utils_Type::escape($contactId, 'Integer') .
929 ' ) OR ( contact_id_a = ' . CRM_Utils_Type::escape($contactId, 'Integer') .
930 ' AND contact_id_b = ' . CRM_Utils_Type::escape($id, 'Integer') . " ) ) ";
6a488035
TO
931
932 //if caseId is provided, include it duplicate checking.
933 if ($caseId = CRM_Utils_Array::value('case_id', $params)) {
22b28b5c 934 $queryString .= ' AND case_id = ' . CRM_Utils_Type::escape($caseId, 'Integer');
6a488035
TO
935 }
936
937 if ($relationshipId) {
22b28b5c 938 $queryString .= ' AND id !=' . CRM_Utils_Type::escape($relationshipId, 'Integer');
6a488035
TO
939 }
940
a36bd2a4 941 $relationship = CRM_Core_DAO::executeQuery($queryString);
6b284952
JV
942 while ($relationship->fetch()) {
943 // Check whether the custom field values are identical.
355e2775 944 if (self::checkDuplicateCustomFields($params['custom'] ?? [], $relationship->id)) {
6b284952
JV
945 return TRUE;
946 }
947 }
6b284952
JV
948 return FALSE;
949 }
950
951 /**
952 * this function checks whether the values of the custom fields in $params are
953 * the same as the values of the custom fields of the relation with given
954 * $relationshipId.
955 *
355e2775 956 * @param array $params an assoc array of name/value pairs
6b284952
JV
957 * @param int $relationshipId ID of an existing duplicate relation
958 *
959 * @return boolean true if custom field values are identical
960 * @access private
961 * @static
962 */
355e2775 963 private static function checkDuplicateCustomFields($params, $relationshipId) {
e312bf62 964 // Get the custom values of the existing relationship.
6b284952 965 $existingValues = CRM_Core_BAO_CustomValueTable::getEntityValues($relationshipId, 'Relationship');
e312bf62 966 // Create a similar array for the new relationship.
52335bb5 967 $newValues = [];
355e2775
EM
968 if (!is_array($params)) {
969 // No idea when this would happen....
970 CRM_Core_Error::deprecatedWarning('params should be an array');
971 }
972 else {
973 // $params seems to be an array, as it should be. Each value is again an array.
1bedc18d
JV
974 // This array contains one value (key -1), and this value seems to be
975 // an array with the information about the custom value.
355e2775 976 foreach ($params as $value) {
1bedc18d
JV
977 foreach ($value as $customValue) {
978 $newValues[$customValue['custom_field_id']] = $customValue['value'];
979 }
6b284952
JV
980 }
981 }
e312bf62
JV
982
983 // Calculate difference between arrays. If the only key-value pairs
984 // that are in one array but not in the other are empty, the
985 // custom fields are considered to be equal.
986 // See https://github.com/civicrm/civicrm-core/pull/6515#issuecomment-137985667
987 $diff1 = array_diff_assoc($existingValues, $newValues);
988 $diff2 = array_diff_assoc($newValues, $existingValues);
989
990 return !array_filter($diff1) && !array_filter($diff2);
6a488035
TO
991 }
992
993 /**
fe482240 994 * Update the is_active flag in the db.
6a488035 995 *
77c5b619
TO
996 * @param int $id
997 * Id of the database record.
998 * @param bool $is_active
999 * Value we want to set the is_active field.
6a488035 1000 *
52335bb5 1001 * @return bool
1002 *
77b97be7 1003 * @throws CiviCRM_API3_Exception
6a488035 1004 */
00be9182 1005 public static function setIsActive($id, $is_active) {
49f8272d
E
1006 // as both the create & add functions have a bunch of logic in them that
1007 // doesn't seem to cope with a normal update we will call the api which
1008 // has tested handling for this
1009 // however, a longer term solution would be to simplify the add, create & api functions
1010 // to be more standard. It is debatable @ that point whether it's better to call the BAO
1011 // direct as the api is more tested.
52335bb5 1012 $result = civicrm_api('relationship', 'create', [
80109b10 1013 'id' => $id,
1014 'is_active' => $is_active,
1015 'version' => 3,
52335bb5 1016 ]);
80109b10 1017
338ca861 1018 if (is_array($result) && !empty($result['is_error']) && $result['error_message'] != 'Duplicate Relationship') {
80109b10 1019 throw new CiviCRM_API3_Exception($result['error_message'], CRM_Utils_Array::value('error_code', $result, 'undefined'), $result);
1020 }
49f8272d 1021
6a488035
TO
1022 return TRUE;
1023 }
1024
1025 /**
98a06ae0 1026 * Fetch a relationship object and store the values in the values array.
6a488035 1027 *
77c5b619
TO
1028 * @param array $params
1029 * Input parameters to find object.
1030 * @param array $values
1031 * Output values of the object.
6a488035 1032 *
a6c01b45
CW
1033 * @return array
1034 * (reference) the values that could be potentially assigned to smarty
6a488035 1035 */
00be9182 1036 public static function &getValues(&$params, &$values) {
6a488035
TO
1037 if (empty($params)) {
1038 return NULL;
1039 }
52335bb5 1040 $v = [];
6a488035
TO
1041
1042 // get the specific number of relationship or all relationships.
a7488080 1043 if (!empty($params['numRelationship'])) {
6a488035
TO
1044 $v['data'] = &CRM_Contact_BAO_Relationship::getRelationship($params['contact_id'], NULL, $params['numRelationship']);
1045 }
1046 else {
1047 $v['data'] = CRM_Contact_BAO_Relationship::getRelationship($params['contact_id']);
1048 }
1049
1050 // get the total count of relationships
c23fcc07 1051 $v['totalCount'] = count($v['data']);
6a488035
TO
1052
1053 $values['relationship']['data'] = &$v['data'];
1054 $values['relationship']['totalCount'] = &$v['totalCount'];
1055
1056 return $v;
1057 }
1058
1059 /**
fe482240 1060 * Helper function to form the sql for relationship retrieval.
6a488035 1061 *
77c5b619
TO
1062 * @param int $contactId
1063 * Contact id.
1064 * @param int $status
1065 * (check const at top of file).
1066 * @param int $numRelationship
1067 * No of relationships to display (limit).
1068 * @param int $count
1069 * Get the no of relationships.
6a488035 1070 * $param int $relationshipId relationship id
100fef9d 1071 * @param int $relationshipId
77c5b619
TO
1072 * @param string $direction
1073 * The direction we are interested in a_b or b_a.
1074 * @param array $params
1075 * Array of extra values including relationship_type_id per api spec.
6a488035 1076 *
77b97be7 1077 * @return array
76e7a76c 1078 * [select, from, where]
923dfabb 1079 *
1080 * @throws \CRM_Core_Exception
1081 * @throws \CiviCRM_API3_Exception
6a488035 1082 */
52335bb5 1083 public static function makeURLClause($contactId, $status, $numRelationship, $count, $relationshipId, $direction, $params = []) {
6a488035
TO
1084 $select = $from = $where = '';
1085
1086 $select = '( ';
1087 if ($count) {
923dfabb 1088 if ($direction === 'a_b') {
6a488035
TO
1089 $select .= ' SELECT count(DISTINCT civicrm_relationship.id) as cnt1, 0 as cnt2 ';
1090 }
1091 else {
1092 $select .= ' SELECT 0 as cnt1, count(DISTINCT civicrm_relationship.id) as cnt2 ';
1093 }
1094 }
1095 else {
1096 $select .= ' SELECT civicrm_relationship.id as civicrm_relationship_id,
1097 civicrm_contact.sort_name as sort_name,
1098 civicrm_contact.display_name as display_name,
1099 civicrm_contact.job_title as job_title,
1100 civicrm_contact.employer_id as employer_id,
1101 civicrm_contact.organization_name as organization_name,
1102 civicrm_address.street_address as street_address,
1103 civicrm_address.city as city,
1104 civicrm_address.postal_code as postal_code,
1105 civicrm_state_province.abbreviation as state,
1106 civicrm_country.name as country,
1107 civicrm_email.email as email,
c6c691a5 1108 civicrm_contact.contact_type as contact_type,
20be4ac3 1109 civicrm_contact.contact_sub_type as contact_sub_type,
6a488035
TO
1110 civicrm_phone.phone as phone,
1111 civicrm_contact.id as civicrm_contact_id,
6a488035
TO
1112 civicrm_relationship.contact_id_b as contact_id_b,
1113 civicrm_relationship.contact_id_a as contact_id_a,
1114 civicrm_relationship_type.id as civicrm_relationship_type_id,
1115 civicrm_relationship.start_date as start_date,
1116 civicrm_relationship.end_date as end_date,
1117 civicrm_relationship.description as description,
1118 civicrm_relationship.is_active as is_active,
1119 civicrm_relationship.is_permission_a_b as is_permission_a_b,
1120 civicrm_relationship.is_permission_b_a as is_permission_b_a,
1121 civicrm_relationship.case_id as case_id';
1122
923dfabb 1123 if ($direction === 'a_b') {
6a488035 1124 $select .= ', civicrm_relationship_type.label_a_b as label_a_b,
c6c691a5 1125 civicrm_relationship_type.label_b_a as relation ';
6a488035
TO
1126 }
1127 else {
1128 $select .= ', civicrm_relationship_type.label_a_b as label_a_b,
c6c691a5 1129 civicrm_relationship_type.label_a_b as relation ';
6a488035
TO
1130 }
1131 }
1132
923dfabb 1133 $from = '
6a488035
TO
1134 FROM civicrm_relationship
1135INNER JOIN civicrm_relationship_type ON ( civicrm_relationship.relationship_type_id = civicrm_relationship_type.id )
923dfabb 1136INNER JOIN civicrm_contact ';
1137 if ($direction === 'a_b') {
6a488035
TO
1138 $from .= 'ON ( civicrm_contact.id = civicrm_relationship.contact_id_a ) ';
1139 }
1140 else {
1141 $from .= 'ON ( civicrm_contact.id = civicrm_relationship.contact_id_b ) ';
1142 }
b7d19957
CR
1143
1144 if (!$count) {
923dfabb 1145 $from .= '
6a488035
TO
1146LEFT JOIN civicrm_address ON (civicrm_address.contact_id = civicrm_contact.id AND civicrm_address.is_primary = 1)
1147LEFT JOIN civicrm_phone ON (civicrm_phone.contact_id = civicrm_contact.id AND civicrm_phone.is_primary = 1)
1148LEFT JOIN civicrm_email ON (civicrm_email.contact_id = civicrm_contact.id AND civicrm_email.is_primary = 1)
1149LEFT JOIN civicrm_state_province ON (civicrm_address.state_province_id = civicrm_state_province.id)
1150LEFT JOIN civicrm_country ON (civicrm_address.country_id = civicrm_country.id)
923dfabb 1151';
b7d19957
CR
1152 }
1153
6a488035
TO
1154 $where = 'WHERE ( 1 )';
1155 if ($contactId) {
923dfabb 1156 if ($direction === 'a_b') {
6a488035
TO
1157 $where .= ' AND civicrm_relationship.contact_id_b = ' . CRM_Utils_Type::escape($contactId, 'Positive');
1158 }
1159 else {
075fa74e 1160 $where .= ' AND civicrm_relationship.contact_id_a = ' . CRM_Utils_Type::escape($contactId, 'Positive') . '
1161 AND civicrm_relationship.contact_id_a != civicrm_relationship.contact_id_b ';
6a488035
TO
1162 }
1163 }
1164 if ($relationshipId) {
1165 $where .= ' AND civicrm_relationship.id = ' . CRM_Utils_Type::escape($relationshipId, 'Positive');
1166 }
1167
1168 $date = date('Y-m-d');
1169 if ($status == self::PAST) {
1170 //this case for showing past relationship
1171 $where .= ' AND civicrm_relationship.is_active = 1 ';
1172 $where .= " AND civicrm_relationship.end_date < '" . $date . "'";
1173 }
1174 elseif ($status == self::DISABLED) {
1175 // this case for showing disabled relationship
1176 $where .= ' AND civicrm_relationship.is_active = 0 ';
1177 }
1178 elseif ($status == self::CURRENT) {
1179 //this case for showing current relationship
1180 $where .= ' AND civicrm_relationship.is_active = 1 ';
1181 $where .= " AND (civicrm_relationship.end_date >= '" . $date . "' OR civicrm_relationship.end_date IS NULL) ";
1182 }
1183 elseif ($status == self::INACTIVE) {
1184 //this case for showing inactive relationships
1185 $where .= " AND (civicrm_relationship.end_date < '" . $date . "'";
1186 $where .= ' OR civicrm_relationship.is_active = 0 )';
1187 }
1188
1189 // CRM-6181
1190 $where .= ' AND civicrm_contact.is_deleted = 0';
22e263ad 1191 if (!empty($params['membership_type_id']) && empty($params['relationship_type_id'])) {
c9c41397 1192 $where .= self::membershipTypeToRelationshipTypes($params, $direction);
1193 }
22e263ad
TO
1194 if (!empty($params['relationship_type_id'])) {
1195 if (is_array($params['relationship_type_id'])) {
7f3647bc 1196 $where .= " AND " . CRM_Core_DAO::createSQLFilter('relationship_type_id', $params['relationship_type_id'], 'Integer');
75638074 1197 }
1198 else {
1199 $where .= ' AND relationship_type_id = ' . CRM_Utils_Type::escape($params['relationship_type_id'], 'Positive');
1200 }
1201 }
923dfabb 1202 if ($direction === 'a_b') {
6a488035
TO
1203 $where .= ' ) UNION ';
1204 }
1205 else {
1206 $where .= ' ) ';
1207 }
1208
52335bb5 1209 return [$select, $from, $where];
6a488035
TO
1210 }
1211
1212 /**
fe482240 1213 * Get a list of relationships.
6a488035 1214 *
77c5b619
TO
1215 * @param int $contactId
1216 * Contact id.
1217 * @param int $status
1218 * 1: Past 2: Disabled 3: Current.
1219 * @param int $numRelationship
1220 * No of relationships to display (limit).
1221 * @param int $count
1222 * Get the no of relationships.
77b97be7 1223 * @param int $relationshipId
76e7a76c
CW
1224 * @param array $links
1225 * the list of links to display
1226 * @param int $permissionMask
1227 * the permission mask to be applied for the actions
77b97be7 1228 * @param bool $permissionedContact
76e7a76c 1229 * to return only permissioned Contact
77b97be7 1230 * @param array $params
644587aa
SL
1231 * @param bool $includeTotalCount
1232 * Should we return a count of total accessable relationships
77b97be7
EM
1233 *
1234 * @return array|int
76e7a76c 1235 * relationship records
923dfabb 1236 *
1237 * @throws \CRM_Core_Exception
1238 * @throws \CiviCRM_API3_Exception
6a488035 1239 */
c301f76e 1240 public static function getRelationship(
51ccfbbe 1241 $contactId = NULL,
7f3647bc
TO
1242 $status = 0, $numRelationship = 0,
1243 $count = 0, $relationshipId = 0,
1244 $links = NULL, $permissionMask = NULL,
75638074 1245 $permissionedContact = FALSE,
52335bb5 1246 $params = [], $includeTotalCount = FALSE
6a488035 1247 ) {
52335bb5 1248 $values = [];
6a488035
TO
1249 if (!$contactId && !$relationshipId) {
1250 return $values;
1251 }
1252
1253 list($select1, $from1, $where1) = self::makeURLClause($contactId, $status, $numRelationship,
75638074 1254 $count, $relationshipId, 'a_b', $params
6a488035
TO
1255 );
1256 list($select2, $from2, $where2) = self::makeURLClause($contactId, $status, $numRelationship,
75638074 1257 $count, $relationshipId, 'b_a', $params
6a488035
TO
1258 );
1259
1260 $order = $limit = '';
1261 if (!$count) {
40458f6c 1262 if (empty($params['sort'])) {
1263 $order = ' ORDER BY civicrm_relationship_type_id, sort_name ';
1264 }
1265 else {
1266 $order = " ORDER BY {$params['sort']} ";
1267 }
1268
1269 $offset = 0;
630d1aaa 1270 if (!empty($params['offset']) && $params['offset'] > 0) {
40458f6c 1271 $offset = $params['offset'];
1272 }
6a488035
TO
1273
1274 if ($numRelationship) {
40458f6c 1275 $limit = " LIMIT {$offset}, $numRelationship";
6a488035
TO
1276 }
1277 }
1278
1279 // building the query string
e729799a 1280 $queryString = $select1 . $from1 . $where1 . $select2 . $from2 . $where2;
6a488035
TO
1281
1282 $relationship = new CRM_Contact_DAO_Relationship();
1283
e729799a 1284 $relationship->query($queryString . $order . $limit);
52335bb5 1285 $row = [];
6a488035
TO
1286 if ($count) {
1287 $relationshipCount = 0;
1288 while ($relationship->fetch()) {
1289 $relationshipCount += $relationship->cnt1 + $relationship->cnt2;
1290 }
1291 return $relationshipCount;
1292 }
1293 else {
1294
644587aa 1295 if ($includeTotalCount) {
e729799a 1296 $values['total_relationships'] = CRM_Core_DAO::singleValueQuery("SELECT count(*) FROM ({$queryString}) AS r");
644587aa
SL
1297 }
1298
6a488035
TO
1299 $mask = NULL;
1300 if ($status != self::INACTIVE) {
1301 if ($links) {
1302 $mask = array_sum(array_keys($links));
1303 if ($mask & CRM_Core_Action::DISABLE) {
1304 $mask -= CRM_Core_Action::DISABLE;
1305 }
1306 if ($mask & CRM_Core_Action::ENABLE) {
1307 $mask -= CRM_Core_Action::ENABLE;
1308 }
1309
1310 if ($status == self::CURRENT) {
1311 $mask |= CRM_Core_Action::DISABLE;
1312 }
1313 elseif ($status == self::DISABLED) {
1314 $mask |= CRM_Core_Action::ENABLE;
1315 }
6a488035 1316 }
75b35151 1317 // temporary hold the value of $mask.
1318 $tempMask = $mask;
6a488035 1319 }
c23fcc07 1320
6a488035
TO
1321 while ($relationship->fetch()) {
1322 $rid = $relationship->civicrm_relationship_id;
1323 $cid = $relationship->civicrm_contact_id;
a93664c8 1324
dc15bb38 1325 if ($permissionedContact &&
a93664c8 1326 (!CRM_Contact_BAO_Contact_Permission::allow($cid))
6a488035
TO
1327 ) {
1328 continue;
1329 }
75b35151 1330 if ($status != self::INACTIVE && $links) {
1331 // assign the original value to $mask
1332 $mask = $tempMask;
1333 // display action links if $cid has edit permission for the relationship.
1334 if (!($permissionMask & CRM_Core_Permission::EDIT) && CRM_Contact_BAO_Contact_Permission::allow($cid, CRM_Core_Permission::EDIT)) {
1335 $permissions[] = CRM_Core_Permission::EDIT;
1336 $permissions[] = CRM_Core_Permission::DELETE;
1337 $permissionMask = CRM_Core_Action::mask($permissions);
1338 }
1339 $mask = $mask & $permissionMask;
1340 }
6a488035
TO
1341 $values[$rid]['id'] = $rid;
1342 $values[$rid]['cid'] = $cid;
1343 $values[$rid]['contact_id_a'] = $relationship->contact_id_a;
1344 $values[$rid]['contact_id_b'] = $relationship->contact_id_b;
c6c691a5 1345 $values[$rid]['contact_type'] = $relationship->contact_type;
20be4ac3 1346 $values[$rid]['contact_sub_type'] = $relationship->contact_sub_type;
6a488035
TO
1347 $values[$rid]['relationship_type_id'] = $relationship->civicrm_relationship_type_id;
1348 $values[$rid]['relation'] = $relationship->relation;
1349 $values[$rid]['name'] = $relationship->sort_name;
1350 $values[$rid]['display_name'] = $relationship->display_name;
1351 $values[$rid]['job_title'] = $relationship->job_title;
1352 $values[$rid]['email'] = $relationship->email;
1353 $values[$rid]['phone'] = $relationship->phone;
1354 $values[$rid]['employer_id'] = $relationship->employer_id;
1355 $values[$rid]['organization_name'] = $relationship->organization_name;
1356 $values[$rid]['country'] = $relationship->country;
1357 $values[$rid]['city'] = $relationship->city;
1358 $values[$rid]['state'] = $relationship->state;
1359 $values[$rid]['start_date'] = $relationship->start_date;
1360 $values[$rid]['end_date'] = $relationship->end_date;
1361 $values[$rid]['description'] = $relationship->description;
1362 $values[$rid]['is_active'] = $relationship->is_active;
1363 $values[$rid]['is_permission_a_b'] = $relationship->is_permission_a_b;
1364 $values[$rid]['is_permission_b_a'] = $relationship->is_permission_b_a;
1365 $values[$rid]['case_id'] = $relationship->case_id;
1366
1367 if ($status) {
1368 $values[$rid]['status'] = $status;
1369 }
1370
1371 $values[$rid]['civicrm_relationship_type_id'] = $relationship->civicrm_relationship_type_id;
1372
1373 if ($relationship->contact_id_a == $contactId) {
1374 $values[$rid]['rtype'] = 'a_b';
1375 }
1376 else {
1377 $values[$rid]['rtype'] = 'b_a';
1378 }
1379
1380 if ($links) {
52335bb5 1381 $replace = [
6a488035
TO
1382 'id' => $rid,
1383 'rtype' => $values[$rid]['rtype'],
1384 'cid' => $contactId,
1385 'cbid' => $values[$rid]['cid'],
1386 'caseid' => $values[$rid]['case_id'],
1387 'clientid' => $contactId,
52335bb5 1388 ];
6a488035
TO
1389
1390 if ($status == self::INACTIVE) {
1391 // setting links for inactive relationships
1392 $mask = array_sum(array_keys($links));
1393 if (!$values[$rid]['is_active']) {
1394 $mask -= CRM_Core_Action::DISABLE;
1395 }
1396 else {
1397 $mask -= CRM_Core_Action::ENABLE;
1398 $mask -= CRM_Core_Action::DISABLE;
1399 }
cd056f13 1400 $mask = $mask & $permissionMask;
6a488035
TO
1401 }
1402
1403 // Give access to manage case link by copying to MAX_ACTION index temporarily, depending on case permission of user.
1404 if ($values[$rid]['case_id']) {
1405 // Borrowed logic from CRM_Case_Page_Tab
1406 $hasCaseAccess = FALSE;
1407 if (CRM_Core_Permission::check('access all cases and activities')) {
1408 $hasCaseAccess = TRUE;
1409 }
1410 else {
1411 $userCases = CRM_Case_BAO_Case::getCases(FALSE);
1412 if (array_key_exists($values[$rid]['case_id'], $userCases)) {
1413 $hasCaseAccess = TRUE;
1414 }
1415 }
1416
1417 if ($hasCaseAccess) {
1418 // give access by copying to MAX_ACTION temporarily, otherwise leave at NONE which won't display
1419 $links[CRM_Core_Action::MAX_ACTION] = $links[CRM_Core_Action::NONE];
52335bb5 1420 $links[CRM_Core_Action::MAX_ACTION]['name'] = ts('Manage Case #%1', [1 => $values[$rid]['case_id']]);
f89f2102 1421 $links[CRM_Core_Action::MAX_ACTION]['class'] = 'no-popup';
6a488035
TO
1422
1423 // Also make sure we have the right client cid since can get here from multiple relationship tabs.
1424 if ($values[$rid]['rtype'] == 'b_a') {
1425 $replace['clientid'] = $values[$rid]['cid'];
1426 }
13a3d214 1427 $values[$rid]['case'] = '<a href="' . CRM_Utils_System::url('civicrm/case/ajax/details', sprintf('caseId=%d&cid=%d&snippet=4', $values[$rid]['case_id'], $values[$rid]['cid'])) . '" class="action-item crm-hover-button crm-summary-link"><i class="crm-i fa-folder-open-o" aria-hidden="true"></i></a>';
6a488035
TO
1428 }
1429 }
1430
87dab4a4 1431 $values[$rid]['action'] = CRM_Core_Action::formLink(
1cfa04c4
EM
1432 $links,
1433 $mask,
87dab4a4
AH
1434 $replace,
1435 ts('more'),
1436 FALSE,
1437 'relationship.selector.row',
1438 'Relationship',
1439 $rid);
6a488035
TO
1440 unset($links[CRM_Core_Action::MAX_ACTION]);
1441 }
1442 }
1443
6a488035
TO
1444 return $values;
1445 }
1446 }
1447
1448 /**
7cf463d9
M
1449 * Get list of relationship type based on the target contact type.
1450 * Both directions of relationships are included if their labels are not the same.
6a488035 1451 *
77c5b619 1452 * @param string $targetContactType
7cf463d9 1453 * A valid contact type (may be Individual, Organization, Household).
6a488035 1454 *
a6c01b45 1455 * @return array
7cf463d9 1456 * array reference of all relationship types with context to current contact type.
6a488035 1457 */
69078420 1458 public static function getRelationType($targetContactType) {
52335bb5 1459 $relationshipType = [];
6a488035
TO
1460 $allRelationshipType = CRM_Core_PseudoConstant::relationshipType();
1461
1462 foreach ($allRelationshipType as $key => $type) {
7b1ed533 1463 if ($type['contact_type_b'] == $targetContactType || empty($type['contact_type_b'])) {
6a488035
TO
1464 $relationshipType[$key . '_a_b'] = $type['label_a_b'];
1465 }
7cf463d9
M
1466 if (($type['contact_type_a'] == $targetContactType || empty($type['contact_type_a']))
1467 && $type['label_a_b'] != $type['label_b_a']
1468 ) {
1469 $relationshipType[$key . '_b_a'] = $type['label_b_a'];
1470 }
6a488035
TO
1471 }
1472
1473 return $relationshipType;
1474 }
1475
1476 /**
100fef9d 1477 * Create / update / delete membership for related contacts.
6a488035
TO
1478 *
1479 * This function will create/update/delete membership for related
1480 * contact based on 1) contact have active membership 2) that
1481 * membership is is extedned by the same relationship type to that
1482 * of the existing relationship.
1483 *
5a4f6742
CW
1484 * @param int $contactId
1485 * contact id.
1486 * @param array $params
1487 * array of values submitted by POST.
1488 * @param array $ids
1489 * array of ids.
bfa4da96 1490 * @param \const|int $action which action called this function
6a488035 1491 *
dd244018 1492 * @param bool $active
6a488035 1493 *
bfa4da96 1494 * @throws \CRM_Core_Exception
0a2663fd 1495 * @throws \CiviCRM_API3_Exception
6a488035 1496 */
5c34e41f 1497 public static function relatedMemberships($contactId, $params, $ids, $action = CRM_Core_Action::ADD, $active = TRUE) {
6a488035 1498 // Check the end date and set the status of the relationship
decc60a0 1499 // accordingly.
6a488035 1500 $status = self::CURRENT;
52335bb5 1501 $targetContact = $targetContact = CRM_Utils_Array::value('contact_check', $params, []);
45089d88
CW
1502 $today = date('Ymd');
1503
1504 // If a relationship hasn't yet started, just return for now
1505 // TODO: handle edge-case of updating start_date of an existing relationship
1506 if (!empty($params['start_date'])) {
1507 $startDate = substr(CRM_Utils_Date::format($params['start_date']), 0, 8);
1508 if ($today < $startDate) {
1509 return;
1510 }
1511 }
6a488035
TO
1512
1513 if (!empty($params['end_date'])) {
45089d88 1514 $endDate = substr(CRM_Utils_Date::format($params['end_date']), 0, 8);
6a488035
TO
1515 if ($today > $endDate) {
1516 $status = self::PAST;
1517 }
1518 }
1519
45089d88
CW
1520 if (($action & CRM_Core_Action::ADD) && ($status & self::PAST)) {
1521 // If relationship is PAST and action is ADD, do nothing.
6a488035
TO
1522 return;
1523 }
1524
1525 $rel = explode('_', $params['relationship_type_id']);
1526
7f3647bc 1527 $relTypeId = $rel[0];
decc60a0
EM
1528 if (!empty($rel[1])) {
1529 $relDirection = "_{$rel[1]}_{$rel[2]}";
1530 }
1531 else {
1532 // this call is coming from somewhere where the direction was resolved early on (e.g an api call)
1533 // so we can assume _a_b
1534 $relDirection = "_a_b";
52335bb5 1535 $targetContact = [$params['contact_id_b'] => 1];
decc60a0 1536 }
04ad3598 1537
6a488035
TO
1538 if (($action & CRM_Core_Action::ADD) ||
1539 ($action & CRM_Core_Action::DELETE)
1540 ) {
1541 $contact = $contactId;
6a488035
TO
1542 }
1543 elseif ($action & CRM_Core_Action::UPDATE) {
6345c936 1544 $contact = (int) $ids['contact'];
52335bb5 1545 $targetContact = [$ids['contactTarget'] => 1];
6a488035
TO
1546 }
1547
1548 // Build the 'values' array for
1549 // 1. ContactA
1550 // 2. ContactB
1551 // This will allow us to check if either of the contacts in
1552 // relationship have active memberships.
1553
52335bb5 1554 $values = [];
6a488035
TO
1555
1556 // 1. ContactA
52335bb5 1557 $values[$contact] = [
6a488035
TO
1558 'relatedContacts' => $targetContact,
1559 'relationshipTypeId' => $relTypeId,
1560 'relationshipTypeDirection' => $relDirection,
52335bb5 1561 ];
6a488035
TO
1562 // 2. ContactB
1563 if (!empty($targetContact)) {
1564 foreach ($targetContact as $cid => $donCare) {
52335bb5 1565 $values[$cid] = [
1566 'relatedContacts' => [$contact => 1],
6a488035 1567 'relationshipTypeId' => $relTypeId,
52335bb5 1568 ];
6a488035 1569
52335bb5 1570 $relTypeParams = ['id' => $relTypeId];
1571 $relTypeValues = [];
6a488035
TO
1572 CRM_Contact_BAO_RelationshipType::retrieve($relTypeParams, $relTypeValues);
1573
1574 if (CRM_Utils_Array::value('name_a_b', $relTypeValues) == CRM_Utils_Array::value('name_b_a', $relTypeValues)) {
1575 $values[$cid]['relationshipTypeDirection'] = '_a_b';
1576 }
1577 else {
1578 $values[$cid]['relationshipTypeDirection'] = ($relDirection == '_a_b') ? '_b_a' : '_a_b';
1579 }
1580 }
1581 }
1582
73a949e4 1583 $deceasedStatusId = array_search('Deceased', CRM_Member_PseudoConstant::membershipStatus());
bd655ee6 1584
6345c936 1585 $relationshipProcessor = new CRM_Member_Utils_RelationshipProcessor(array_keys($values), $active);
6a488035 1586 foreach ($values as $cid => $details) {
52335bb5 1587 $relatedContacts = array_keys(CRM_Utils_Array::value('relatedContacts', $details, []));
ee17a760 1588 $mainRelatedContactId = reset($relatedContacts);
6a488035 1589
6345c936 1590 foreach ($relationshipProcessor->getRelationshipMembershipsForContact((int) $cid) as $membershipId => $membershipValues) {
0a2663fd 1591 $membershipInherittedFromContactID = NULL;
1592 if (!empty($membershipValues['owner_membership_id'])) {
6345c936 1593 // @todo - $membership already has this now.
0a2663fd 1594 // Use get not getsingle so that we get e-notice noise but not a fatal is the membership has already been deleted.
1595 $inheritedFromMembership = civicrm_api3('Membership', 'get', ['id' => $membershipValues['owner_membership_id'], 'sequential' => 1])['values'][0];
1596 $membershipInherittedFromContactID = (int) $inheritedFromMembership['contact_id'];
1597 }
52335bb5 1598 $relTypeIds = [];
6a488035 1599 if ($action & CRM_Core_Action::DELETE) {
98fd6fc8 1600 // @todo don't return relTypeId here - but it seems to be used later in a cryptic way (hint cryptic is not a complement).
310c2031 1601 list($relTypeId, $isDeletable) = self::isInheritedMembershipInvalidated($membershipValues, $values, $cid);
98fd6fc8 1602 if ($isDeletable) {
73a949e4 1603 CRM_Member_BAO_Membership::deleteRelatedMemberships($membershipValues['owner_membership_id'], $membershipValues['contact_id']);
6a488035
TO
1604 }
1605 continue;
1606 }
1607 if (($action & CRM_Core_Action::UPDATE) &&
1608 ($status & self::PAST) &&
1609 ($membershipValues['owner_membership_id'])
1610 ) {
1611 // If relationship is PAST and action is UPDATE
1612 // then delete the RELATED membership
1613 CRM_Member_BAO_Membership::deleteRelatedMemberships($membershipValues['owner_membership_id'],
73a949e4 1614 $membershipValues['contact_id']
6a488035
TO
1615 );
1616 continue;
1617 }
1618
1619 // add / edit the memberships for related
1620 // contacts.
1621
6345c936 1622 // @todo - all these lines get 'relTypeDirs' - but it's already a key in the $membership array.
6a488035 1623 // Get the Membership Type Details.
563f6487 1624 $membershipType = CRM_Member_BAO_MembershipType::getMembershipType($membershipValues['membership_type_id']);
6a488035 1625 // Check if contact's relationship type exists in membership type
52335bb5 1626 $relTypeDirs = [];
a7488080 1627 if (!empty($membershipType['relationship_type_id'])) {
563f6487 1628 $relTypeIds = (array) $membershipType['relationship_type_id'];
6a488035 1629 }
a7488080 1630 if (!empty($membershipType['relationship_direction'])) {
563f6487 1631 $relDirections = (array) $membershipType['relationship_direction'];
6a488035
TO
1632 }
1633 foreach ($relTypeIds as $key => $value) {
1634 $relTypeDirs[] = $value . '_' . $relDirections[$key];
1635 }
1636 $relTypeDir = $details['relationshipTypeId'] . $details['relationshipTypeDirection'];
1637 if (in_array($relTypeDir, $relTypeDirs)) {
1638 // Check if relationship being created/updated is
1639 // similar to that of membership type's
1640 // relationship.
1641
1642 $membershipValues['owner_membership_id'] = $membershipId;
1643 unset($membershipValues['id']);
6a488035
TO
1644 unset($membershipValues['contact_id']);
1645 unset($membershipValues['membership_id']);
1646 foreach ($details['relatedContacts'] as $relatedContactId => $donCare) {
1647 $membershipValues['contact_id'] = $relatedContactId;
1648 if ($deceasedStatusId &&
1649 CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $relatedContactId, 'is_deceased')
1650 ) {
1651 $membershipValues['status_id'] = $deceasedStatusId;
1652 $membershipValues['skipStatusCal'] = TRUE;
1653 }
6a488035 1654
6345c936 1655 if (in_array($action, [CRM_Core_Action::UPDATE, CRM_Core_Action::ADD, CRM_Core_Action::ENABLE])) {
e72dea83 1656 //if updated relationship is already related to contact don't delete existing inherited membership
6345c936 1657 if (in_array((int) $relatedContactId, $membershipValues['inheriting_contact_ids'], TRUE)
1658 || $relatedContactId === $membershipValues['owner_contact_id']
1659 ) {
e72dea83 1660 continue;
1661 }
1662
1663 //delete the membership record for related
1664 //contact before creating new membership record.
1665 CRM_Member_BAO_Membership::deleteRelatedMemberships($membershipId, $relatedContactId);
1666 }
00538ec6 1667 //skip status calculation for pay later memberships.
6345c936 1668 if ('Pending' === CRM_Core_PseudoConstant::getName('CRM_Member_BAO_Membership', 'status_id', $membershipValues['status_id'])) {
00538ec6
JP
1669 $membershipValues['skipStatusCal'] = TRUE;
1670 }
0a2663fd 1671 // As long as the membership itself was not created by inheritance from the same contact
1672 // that stands to inherit the membership we add an inherited membership.
1673 if ($membershipInherittedFromContactID !== (int) $membershipValues['contact_id']) {
1674 $membershipValues = self::addInheritedMembership($membershipValues);
1675 }
6a488035
TO
1676 }
1677 }
1678 elseif ($action & CRM_Core_Action::UPDATE) {
1679 // if action is update and updated relationship do
1680 // not match with the existing
1681 // membership=>relationship then we need to
e62adec9 1682 // change the status of the membership record to expired for
1683 // previous relationship -- CRM-12078.
1684 // CRM-16087 we need to pass ownerMembershipId to isRelatedMembershipExpired function
690c56c3 1685 if (empty($params['relationship_ids']) && !empty($params['id'])) {
52335bb5 1686 $relIds = [$params['id']];
690c56c3 1687 }
1688 else {
9c1bc317 1689 $relIds = $params['relationship_ids'] ?? NULL;
690c56c3 1690 }
e62adec9 1691 if (self::isRelatedMembershipExpired($relTypeIds, $contactId, $mainRelatedContactId, $relTypeId,
52335bb5 1692 $relIds) && !empty($membershipValues['owner_membership_id']
1693 ) && !empty($values[$mainRelatedContactId]['memberships'][$membershipValues['owner_membership_id']])) {
e62adec9 1694 $membershipValues['status_id'] = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_MembershipStatus', 'Expired', 'id', 'label');
1695 $type = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_MembershipType', $membershipValues['membership_type_id'], 'name', 'id');
1696 CRM_Member_BAO_Membership::add($membershipValues);
1836ab9e 1697 CRM_Core_Session::setStatus(ts("Inherited membership %1 status was changed to Expired due to the change in relationship type.", [1 => $type]), ts('Record Updated'), 'alert');
6a488035
TO
1698 }
1699 }
1700 }
1701 }
1702 }
1703
1704 /**
e62adec9 1705 * Helper function to check whether the membership is expired or not.
6616f7c1
EM
1706 *
1707 * Function takes a list of related membership types and if it is not also passed a
e62adec9 1708 * relationship ID of that types evaluates whether the membership status should be changed to expired.
6616f7c1
EM
1709 *
1710 * @param array $membershipTypeRelationshipTypeIDs
1711 * Relation type IDs related to the given membership type.
1712 * @param int $contactId
1713 * @param int $mainRelatedContactId
1714 * @param int $relTypeId
1715 * @param array $relIds
1716 *
1717 * @return bool
6a488035 1718 */
e62adec9 1719 public static function isRelatedMembershipExpired($membershipTypeRelationshipTypeIDs, $contactId, $mainRelatedContactId, $relTypeId, $relIds) {
6616f7c1 1720 if (empty($membershipTypeRelationshipTypeIDs) || in_array($relTypeId, $membershipTypeRelationshipTypeIDs)) {
eec8d770 1721 return FALSE;
6a488035
TO
1722 }
1723
1724 if (empty($relIds)) {
1725 return FALSE;
1726 }
1727
52335bb5 1728 $relParamas = [
1729 1 => [$contactId, 'Integer'],
1730 2 => [$mainRelatedContactId, 'Integer'],
1731 ];
6a488035
TO
1732
1733 if ($contactId == $mainRelatedContactId) {
6616f7c1
EM
1734 $recordsFound = (int) CRM_Core_DAO::singleValueQuery("SELECT COUNT(*) FROM civicrm_relationship WHERE relationship_type_id IN ( " . implode(',', $membershipTypeRelationshipTypeIDs) . " ) AND
1735contact_id_a IN ( %1 ) OR contact_id_b IN ( %1 ) AND id IN (" . implode(',', $relIds) . ")", $relParamas);
6a488035
TO
1736 if ($recordsFound) {
1737 return FALSE;
1738 }
1739 return TRUE;
1740 }
1741
6616f7c1 1742 $recordsFound = (int) CRM_Core_DAO::singleValueQuery("SELECT COUNT(*) FROM civicrm_relationship WHERE relationship_type_id IN ( " . implode(',', $membershipTypeRelationshipTypeIDs) . " ) AND contact_id_a IN ( %1, %2 ) AND contact_id_b IN ( %1, %2 ) AND id NOT IN (" . implode(',', $relIds) . ")", $relParamas);
6a488035
TO
1743
1744 if ($recordsFound) {
1745 return FALSE;
1746 }
1747
1748 return TRUE;
1749 }
1750
1751 /**
fe482240 1752 * Get Current Employer for Contact.
6a488035 1753 *
77c5b619
TO
1754 * @param $contactIds
1755 * Contact Ids.
6a488035 1756 *
a6c01b45
CW
1757 * @return array
1758 * array of the current employer
6a488035 1759 */
00be9182 1760 public static function getCurrentEmployer($contactIds) {
6a488035
TO
1761 $contacts = implode(',', $contactIds);
1762
1763 $query = "
1764SELECT organization_name, id, employer_id
1765FROM civicrm_contact
1766WHERE id IN ( {$contacts} )
1767";
1768
33621c4f 1769 $dao = CRM_Core_DAO::executeQuery($query);
52335bb5 1770 $currentEmployer = [];
6a488035
TO
1771 while ($dao->fetch()) {
1772 $currentEmployer[$dao->id]['org_id'] = $dao->employer_id;
1773 $currentEmployer[$dao->id]['org_name'] = $dao->organization_name;
1774 }
1775
1776 return $currentEmployer;
1777 }
1778
7f3647bc 1779 /**
fe482240 1780 * Function to return list of permissioned contacts for a given contact and relationship type.
7f3647bc 1781 *
5a4f6742
CW
1782 * @param int $contactID
1783 * contact id whose permissioned contacts are to be found.
b0076fe6 1784 * @param int $relTypeId
5a4f6742
CW
1785 * one or more relationship type id's.
1786 * @param string $name
33260076 1787 * @param string $contactType
7f3647bc 1788 *
a6c01b45 1789 * @return array
16b10e64 1790 * Array of contacts
7f3647bc 1791 */
33260076 1792 public static function getPermissionedContacts($contactID, $relTypeId = NULL, $name = NULL, $contactType = NULL) {
52335bb5 1793 $contacts = [];
1794 $args = [1 => [$contactID, 'Integer']];
33260076 1795 $relationshipTypeClause = $contactTypeClause = '';
51e89def 1796
6a488035 1797 if ($relTypeId) {
b0076fe6
EM
1798 // @todo relTypeId is only ever passed in as an int. Change this to reflect that -
1799 // probably being overly conservative by not doing so but working on stable release.
1800 $relationshipTypeClause = 'AND cr.relationship_type_id IN (%2) ';
52335bb5 1801 $args[2] = [$relTypeId, 'String'];
b0076fe6 1802 }
33260076 1803
1804 if ($contactType) {
1805 $contactTypeClause = ' AND cr.relationship_type_id = crt.id AND crt.contact_type_b = %3 ';
52335bb5 1806 $args[3] = [$contactType, 'String'];
33260076 1807 }
1808
4779abb3 1809 $query = "
6a488035 1810SELECT cc.id as id, cc.sort_name as name
33260076 1811FROM civicrm_relationship cr, civicrm_contact cc, civicrm_relationship_type crt
49f8272d 1812WHERE
51e89def 1813cr.contact_id_a = %1 AND
51e89def 1814cr.is_permission_a_b = 1 AND
6a488035
TO
1815IF(cr.end_date IS NULL, 1, (DATEDIFF( CURDATE( ), cr.end_date ) <= 0)) AND
1816cr.is_active = 1 AND
7ecf6275 1817cc.id = cr.contact_id_b AND
b0076fe6
EM
1818cc.is_deleted = 0
1819$relationshipTypeClause
33260076 1820$contactTypeClause
b0076fe6 1821";
51e89def 1822
4779abb3 1823 if (!empty($name)) {
1824 $name = CRM_Utils_Type::escape($name, 'String');
1825 $query .= "
49f8272d 1826AND cc.sort_name LIKE '%$name%'";
4779abb3 1827 }
6a488035 1828
4779abb3 1829 $dao = CRM_Core_DAO::executeQuery($query, $args);
1830 while ($dao->fetch()) {
52335bb5 1831 $contacts[$dao->id] = [
4779abb3 1832 'name' => $dao->name,
1833 'value' => $dao->id,
52335bb5 1834 ];
4779abb3 1835 }
b0076fe6 1836
51e89def 1837 return $contacts;
6a488035
TO
1838 }
1839
6a488035 1840 /**
fe482240 1841 * Merge relationships from otherContact to mainContact.
bfa4da96 1842 *
6a488035
TO
1843 * Called during contact merge operation
1844 *
77c5b619
TO
1845 * @param int $mainId
1846 * Contact id of main contact record.
1847 * @param int $otherId
1848 * Contact id of record which is going to merge.
1849 * @param array $sqls
1850 * (reference) array of sql statements to append to.
6a488035
TO
1851 *
1852 * @see CRM_Dedupe_Merger::cpTables()
6a488035 1853 */
00be9182 1854 public static function mergeRelationships($mainId, $otherId, &$sqls) {
6a488035
TO
1855 // Delete circular relationships
1856 $sqls[] = "DELETE FROM civicrm_relationship
6ce3ea65
JJ
1857 WHERE (contact_id_a = $mainId AND contact_id_b = $otherId AND case_id IS NULL)
1858 OR (contact_id_b = $mainId AND contact_id_a = $otherId AND case_id IS NULL)";
6a488035
TO
1859
1860 // Delete relationship from other contact if main contact already has that relationship
1861 $sqls[] = "DELETE r2
1862 FROM civicrm_relationship r1, civicrm_relationship r2
1863 WHERE r1.relationship_type_id = r2.relationship_type_id
1864 AND r1.id <> r2.id
6ce3ea65 1865 AND r1.case_id IS NULL AND r2.case_id IS NULL
6a488035
TO
1866 AND (
1867 r1.contact_id_a = $mainId AND r2.contact_id_a = $otherId AND r1.contact_id_b = r2.contact_id_b
1868 OR r1.contact_id_b = $mainId AND r2.contact_id_b = $otherId AND r1.contact_id_a = r2.contact_id_a
1869 OR (
1870 (r1.contact_id_a = $mainId AND r2.contact_id_b = $otherId AND r1.contact_id_b = r2.contact_id_a
1871 OR r1.contact_id_b = $mainId AND r2.contact_id_a = $otherId AND r1.contact_id_a = r2.contact_id_b)
1872 AND r1.relationship_type_id IN (SELECT id FROM civicrm_relationship_type WHERE name_b_a = name_a_b)
1873 )
1874 )";
1875
1876 // Move relationships
1877 $sqls[] = "UPDATE IGNORE civicrm_relationship SET contact_id_a = $mainId WHERE contact_id_a = $otherId";
1878 $sqls[] = "UPDATE IGNORE civicrm_relationship SET contact_id_b = $mainId WHERE contact_id_b = $otherId";
1879
1880 // Move current employer id (name will get updated later)
1881 $sqls[] = "UPDATE civicrm_contact SET employer_id = $mainId WHERE employer_id = $otherId";
1882 }
1883
1884 /**
1885 * Set 'is_valid' field to false for all relationships whose end date is in the past, ie. are expired.
1886 *
72b3a70c
CW
1887 * @return bool
1888 * True on success, false if error is encountered.
f42bcfb1 1889 * @throws \CiviCRM_API3_Exception
6a488035 1890 */
00be9182 1891 public static function disableExpiredRelationships() {
6a488035
TO
1892 $query = "SELECT id FROM civicrm_relationship WHERE is_active = 1 AND end_date < CURDATE()";
1893
1894 $dao = CRM_Core_DAO::executeQuery($query);
1895 while ($dao->fetch()) {
1896 $result = CRM_Contact_BAO_Relationship::setIsActive($dao->id, FALSE);
1897 // Result will be NULL if error occurred. We abort early if error detected.
1898 if ($result == NULL) {
1899 return FALSE;
1900 }
1901 }
1902 return TRUE;
1903 }
c9c41397 1904
1905 /**
fe482240 1906 * Function filters the query by possible relationships for the membership type.
98a06ae0 1907 *
c9c41397 1908 * It is intended to be called when constructing queries for the api (reciprocal & non-reciprocal)
1909 * and to add clauses to limit the return to those relationships which COULD inherit a membership type
1910 * (as opposed to those who inherit a particular membership
1911 *
77c5b619
TO
1912 * @param array $params
1913 * Api input array.
77b97be7
EM
1914 * @param null $direction
1915 *
c301f76e 1916 * @return array|void
52335bb5 1917 * @throws \CiviCRM_API3_Exception
c9c41397 1918 */
00be9182 1919 public static function membershipTypeToRelationshipTypes(&$params, $direction = NULL) {
52335bb5 1920 $membershipType = civicrm_api3('membership_type', 'getsingle', [
353ffa53
TO
1921 'id' => $params['membership_type_id'],
1922 'return' => 'relationship_type_id, relationship_direction',
52335bb5 1923 ]);
c9c41397 1924 $relationshipTypes = $membershipType['relationship_type_id'];
22e263ad 1925 if (empty($relationshipTypes)) {
c301f76e 1926 return NULL;
f201289d 1927 }
c9c41397 1928 // if we don't have any contact data we can only filter on type
22e263ad 1929 if (empty($params['contact_id']) && empty($params['contact_id_a']) && empty($params['contact_id_a'])) {
52335bb5 1930 $params['relationship_type_id'] = ['IN' => $relationshipTypes];
c301f76e 1931 return NULL;
c9c41397 1932 }
1933 else {
1a9b2ee8 1934 $relationshipDirections = (array) $membershipType['relationship_direction'];
c9c41397 1935 // if we have contact_id_a OR contact_id_b we can make a call here
1936 // if we have contact??
1937 foreach ($relationshipDirections as $index => $mtdirection) {
7f3647bc 1938 if (isset($params['contact_id_a']) && $mtdirection == 'a_b' || $direction == 'a_b') {
c9c41397 1939 $types[] = $relationshipTypes[$index];
1940 }
7f3647bc 1941 if (isset($params['contact_id_b']) && $mtdirection == 'b_a' || $direction == 'b_a') {
c9c41397 1942 $types[] = $relationshipTypes[$index];
1943 }
1944 }
22e263ad 1945 if (!empty($types)) {
52335bb5 1946 $params['relationship_type_id'] = ['IN' => $types];
c9c41397 1947 }
7f3647bc 1948 elseif (!empty($clauses)) {
c9c41397 1949 return explode(' OR ', $clauses);
1950 }
92e4c2a5 1951 else {
c9c41397 1952 // effectively setting it to return no results
1953 $params['relationship_type_id'] = 0;
1954 }
1955 }
1956 }
40458f6c 1957
40458f6c 1958 /**
98a06ae0 1959 * Wrapper for contact relationship selector.
40458f6c 1960 *
77c5b619
TO
1961 * @param array $params
1962 * Associated array for params record id.
40458f6c 1963 *
a6c01b45
CW
1964 * @return array
1965 * associated array of contact relationships
52335bb5 1966 * @throws \Exception
40458f6c 1967 */
1968 public static function getContactRelationshipSelector(&$params) {
1969 // format the params
7f3647bc 1970 $params['offset'] = ($params['page'] - 1) * $params['rp'];
9c1bc317 1971 $params['sort'] = $params['sortBy'] ?? NULL;
40458f6c 1972
1973 if ($params['context'] == 'past') {
1974 $relationshipStatus = CRM_Contact_BAO_Relationship::INACTIVE;
1975 }
62e6afce 1976 elseif ($params['context'] == 'all') {
6c292184
TM
1977 $relationshipStatus = CRM_Contact_BAO_Relationship::ALL;
1978 }
40458f6c 1979 else {
1980 $relationshipStatus = CRM_Contact_BAO_Relationship::CURRENT;
1981 }
1982
1983 // check logged in user for permission
1984 $page = new CRM_Core_Page();
1985 CRM_Contact_Page_View::checkUserPermission($page, $params['contact_id']);
52335bb5 1986 $permissions = [$page->_permission];
40458f6c 1987 if ($page->_permission == CRM_Core_Permission::EDIT) {
1988 $permissions[] = CRM_Core_Permission::DELETE;
1989 }
1990 $mask = CRM_Core_Action::mask($permissions);
1991
a93664c8 1992 $permissionedContacts = TRUE;
d0592c3d 1993 if ($params['context'] != 'user') {
1994 $links = CRM_Contact_Page_View_Relationship::links();
d0592c3d 1995 }
1996 else {
1997 $links = CRM_Contact_Page_View_UserDashBoard::links();
d0592c3d 1998 $mask = NULL;
1999 }
40458f6c 2000 // get contact relationships
2001 $relationships = CRM_Contact_BAO_Relationship::getRelationship($params['contact_id'],
2002 $relationshipStatus,
2003 $params['rp'], 0, 0,
d0592c3d 2004 $links, $mask,
2005 $permissionedContacts,
644587aa 2006 $params, TRUE
40458f6c 2007 );
2008
52335bb5 2009 $contactRelationships = [];
644587aa
SL
2010 $params['total'] = $relationships['total_relationships'];
2011 unset($relationships['total_relationships']);
40458f6c 2012 if (!empty($relationships)) {
40458f6c 2013
1250f3d8
AH
2014 $displayName = CRM_Contact_BAO_Contact::displayName($params['contact_id']);
2015
40458f6c 2016 // format params
2017 foreach ($relationships as $relationshipId => $values) {
52335bb5 2018 $relationship = [];
7d12de7f 2019
febb6506 2020 $relationship['DT_RowId'] = $values['id'];
7d12de7f
JL
2021 $relationship['DT_RowClass'] = 'crm-entity';
2022 if ($values['is_active'] == 0) {
2023 $relationship['DT_RowClass'] .= ' disabled';
2024 }
2025
52335bb5 2026 $relationship['DT_RowAttr'] = [];
febb6506
JL
2027 $relationship['DT_RowAttr']['data-entity'] = 'relationship';
2028 $relationship['DT_RowAttr']['data-id'] = $values['id'];
7d12de7f 2029
20be4ac3
BS
2030 //Add image icon for related contacts: CRM-14919; CRM-19668
2031 $contactType = (!empty($values['contact_sub_type'])) ? $values['contact_sub_type'] : $values['contact_type'];
2032 $icon = CRM_Contact_BAO_Contact_Utils::getImage($contactType,
67e4fb51
N
2033 FALSE,
2034 $values['cid']
2035 );
7d12de7f 2036 $relationship['sort_name'] = $icon . ' ' . CRM_Utils_System::href(
7f3647bc
TO
2037 $values['name'],
2038 'civicrm/contact/view',
2039 "reset=1&cid={$values['cid']}");
40458f6c 2040
0a61e567 2041 $relationship['relation'] = CRM_Utils_Array::value('case', $values, '') . CRM_Utils_System::href(
52335bb5 2042 $values['relation'],
2043 'civicrm/contact/view/rel',
2044 "action=view&reset=1&cid={$values['cid']}&id={$values['id']}&rtype={$values['rtype']}");
40458f6c 2045
66229fd5
AS
2046 if (!empty($values['description'])) {
2047 $relationship['relation'] .= "<p class='description'>{$values['description']}</p>";
2048 }
2049
d0592c3d 2050 if ($params['context'] == 'current') {
fee0416f
AH
2051 $smarty = CRM_Core_Smarty::singleton();
2052
2053 $contactCombos = [
2054 [
2055 'permContact' => $params['contact_id'],
2056 'permDisplayName' => $displayName,
2057 'otherContact' => $values['cid'],
2058 'otherDisplayName' => $values['display_name'],
2059 'columnKey' => 'sort_name',
2060 ],
2061 [
2062 'permContact' => $values['cid'],
2063 'permDisplayName' => $values['display_name'],
2064 'otherContact' => $params['contact_id'],
2065 'otherDisplayName' => $displayName,
2066 'columnKey' => 'relation',
2067 ],
2068 ];
2069
2070 foreach ($contactCombos as $combo) {
2071 foreach ([CRM_Contact_BAO_Relationship::EDIT, CRM_Contact_BAO_Relationship::VIEW] as $permType) {
2072 $smarty->assign('permType', $permType);
2073 if (($combo['permContact'] == $values['contact_id_a'] and $values['is_permission_a_b'] == $permType)
2074 || ($combo['permContact'] == $values['contact_id_b'] and $values['is_permission_b_a'] == $permType)
2075 ) {
2076 $smarty->assign('permDisplayName', $combo['permDisplayName']);
2077 $smarty->assign('otherDisplayName', $combo['otherDisplayName']);
2078 $relationship[$combo['columnKey']] .= $smarty->fetch('CRM/Contact/Page/View/RelationshipPerm.tpl');
2079 }
2080 }
f871c3a9 2081 }
40458f6c 2082 }
2083
7d12de7f
JL
2084 $relationship['start_date'] = CRM_Utils_Date::customFormat($values['start_date']);
2085 $relationship['end_date'] = CRM_Utils_Date::customFormat($values['end_date']);
2086 $relationship['city'] = $values['city'];
2087 $relationship['state'] = $values['state'];
2088 $relationship['email'] = $values['email'];
2089 $relationship['phone'] = $values['phone'];
2090 $relationship['links'] = $values['action'];
2091
2092 array_push($contactRelationships, $relationship);
40458f6c 2093 }
2094 }
7d12de7f 2095
16b745b0
MWMC
2096 $columnHeaders = self::getColumnHeaders();
2097 $selector = NULL;
2098 CRM_Utils_Hook::searchColumns('relationship.rows', $columnHeaders, $contactRelationships, $selector);
2099
52335bb5 2100 $relationshipsDT = [];
7d12de7f
JL
2101 $relationshipsDT['data'] = $contactRelationships;
2102 $relationshipsDT['recordsTotal'] = $params['total'];
2103 $relationshipsDT['recordsFiltered'] = $params['total'];
2104
2105 return $relationshipsDT;
40458f6c 2106 }
2107
16b745b0
MWMC
2108 /**
2109 * @return array
2110 */
2111 public static function getColumnHeaders() {
2112 return [
2113 'relation' => [
2114 'name' => ts('Relationship'),
2115 'sort' => 'relation',
2116 'direction' => CRM_Utils_Sort::ASCENDING,
2117 ],
2118 'sort_name' => [
2119 'name' => '',
2120 'sort' => 'sort_name',
2121 'direction' => CRM_Utils_Sort::ASCENDING,
2122 ],
2123 'start_date' => [
2124 'name' => ts('Start'),
2125 'sort' => 'start_date',
2126 'direction' => CRM_Utils_Sort::DONTCARE,
2127 ],
2128 'end_date' => [
2129 'name' => ts('End'),
2130 'sort' => 'end_date',
2131 'direction' => CRM_Utils_Sort::DONTCARE,
2132 ],
2133 'city' => [
2134 'name' => ts('City'),
2135 'sort' => 'city',
2136 'direction' => CRM_Utils_Sort::DONTCARE,
2137 ],
2138 'state' => [
2139 'name' => ts('State/Prov'),
2140 'sort' => 'state',
2141 'direction' => CRM_Utils_Sort::DONTCARE,
2142 ],
2143 'email' => [
2144 'name' => ts('Email'),
2145 'sort' => 'email',
2146 'direction' => CRM_Utils_Sort::DONTCARE,
2147 ],
2148 'phone' => [
2149 'name' => ts('Phone'),
2150 'sort' => 'phone',
2151 'direction' => CRM_Utils_Sort::DONTCARE,
2152 ],
2153 'links' => [
2154 'name' => '',
2155 'sort' => 'links',
2156 'direction' => CRM_Utils_Sort::DONTCARE,
2157 ],
2158 ];
2159 }
2160
89e45979
MD
2161 /**
2162 * @inheritdoc
2163 */
52335bb5 2164 public static function buildOptions($fieldName, $context = NULL, $props = []) {
1ac9bb56
CW
2165 // Quickform-specific format, for use when editing relationship type options in a popup from the contact relationship form
2166 if ($fieldName === 'relationship_type_id' && !empty($props['is_form'])) {
2167 return self::getContactRelationshipType(
2168 $props['contact_id'] ?? NULL,
2169 $props['relationship_direction'] ?? 'a_b',
2170 $props['relationship_id'] ?? NULL,
2171 $props['contact_type'] ?? NULL
2172 );
89e45979
MD
2173 }
2174
2175 return parent::buildOptions($fieldName, $context, $props);
2176 }
2177
df0c42cc
JP
2178 /**
2179 * Process the params from api, form and check if current
2180 * employer should be set or unset.
2181 *
2182 * @param array $params
2183 * @param int $relationshipId
e97c66ff 2184 * @param int|null $updatedRelTypeID
df0c42cc
JP
2185 *
2186 * @return bool
2187 * TRUE if current employer needs to be cleared.
f42bcfb1 2188 * @throws \CiviCRM_API3_Exception
df0c42cc
JP
2189 */
2190 public static function isCurrentEmployerNeedingToBeCleared($params, $relationshipId, $updatedRelTypeID = NULL) {
f42bcfb1 2191 $existingTypeID = (int) CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Relationship', $relationshipId, 'relationship_type_id');
df0c42cc 2192 $updatedRelTypeID = $updatedRelTypeID ? $updatedRelTypeID : $existingTypeID;
0c578c02 2193 $currentEmployerID = (int) civicrm_api3('Contact', 'getvalue', ['return' => 'current_employer_id', 'id' => $params['contact_id_a']]);
df0c42cc 2194
0c578c02 2195 if ($currentEmployerID !== (int) $params['contact_id_b'] || !self::isRelationshipTypeCurrentEmployer($existingTypeID)) {
df0c42cc
JP
2196 return FALSE;
2197 }
2198 //Clear employer if relationship is expired.
2199 if (!empty($params['end_date']) && strtotime($params['end_date']) < time()) {
2200 return TRUE;
2201 }
2202 //current employer checkbox is disabled on the form.
2203 //inactive or relationship type(employer of) is updated.
2204 if ((isset($params['is_current_employer']) && empty($params['is_current_employer']))
2205 || ((isset($params['is_active']) && empty($params['is_active'])))
2206 || $existingTypeID != $updatedRelTypeID) {
0c578c02
MD
2207 // If there are no other active employer relationships between the same 2 contacts...
2208 if (!civicrm_api3('Relationship', 'getcount', [
2209 'is_active' => 1,
2210 'relationship_type_id' => $existingTypeID,
2211 'id' => ['<>' => $params['id']],
2212 'contact_id_a' => $params['contact_id_a'],
2213 'contact_id_b' => $params['contact_id_b'],
2214 ])) {
2215 return TRUE;
2216 }
df0c42cc
JP
2217 }
2218
2219 return FALSE;
2220 }
2221
f42bcfb1
MD
2222 /**
2223 * Is this a current employer relationship type.
2224 *
2225 * @todo - this could use cached pseudoconstant lookups.
2226 *
2227 * @param int $existingTypeID
2228 *
2229 * @return bool
2230 */
2231 private static function isRelationshipTypeCurrentEmployer(int $existingTypeID): bool {
2232 $isCurrentEmployerRelationshipType = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_RelationshipType', $existingTypeID, 'name_b_a') === 'Employer of';
2233 return $isCurrentEmployerRelationshipType;
2234 }
2235
98fd6fc8 2236 /**
2237 * Is the inherited relationship invalidated by this relationship change.
2238 *
2239 * @param $membershipValues
2240 * @param array $values
2241 * @param int $cid
98fd6fc8 2242 *
2243 * @return array
eb151aab 2244 * @throws \CiviCRM_API3_Exception
98fd6fc8 2245 */
310c2031 2246 private static function isInheritedMembershipInvalidated($membershipValues, array $values, $cid): array {
2247 // @todo most of this can go - it's just the weird historical returning of $relTypeId that it does.
2248 // now we have caching the parent fn can just call CRM_Member_BAO_MembershipType::getMembershipType
eb151aab 2249 $membershipType = CRM_Member_BAO_MembershipType::getMembershipType($membershipValues['membership_type_id']);
2250 $relTypeIds = $membershipType['relationship_type_id'];
310c2031 2251 $membershipInheritedFrom = $membershipValues['owner_membership_id'] ?? NULL;
2252 if (!$membershipInheritedFrom || !in_array($values[$cid]['relationshipTypeId'], $relTypeIds)) {
eb151aab 2253 return [implode(',', $relTypeIds), FALSE];
2254 }
2255 //CRM-16300 check if owner membership exist for related membership
310c2031 2256 return [implode(',', $relTypeIds), !self::isContactHasValidRelationshipToInheritMembershipType((int) $cid, (int) $membershipValues['membership_type_id'], (int) $membershipValues['owner_membership_id'])];
2257 }
2258
2259 /**
2260 * Is there a valid relationship confering this membership type on this contact.
2261 *
2262 * @param int $contactID
2263 * @param int $membershipTypeID
2264 * @param int $parentMembershipID
2265 * Id of the membership being inherited.
2266 *
2267 * @return bool
2268 *
2269 * @throws \CiviCRM_API3_Exception
2270 */
2271 private static function isContactHasValidRelationshipToInheritMembershipType(int $contactID, int $membershipTypeID, int $parentMembershipID): bool {
2272 $membershipType = CRM_Member_BAO_MembershipType::getMembershipType($membershipTypeID);
2273 $existingRelationships = civicrm_api3('Relationship', 'get', [
2274 'contact_id_a' => $contactID,
2275 'contact_id_b' => $contactID,
2276 'relationship_type_id' => ['IN' => $membershipType['relationship_type_id']],
2277 'options' => ['or' => [['contact_id_a', 'contact_id_b']], 'limit' => 0],
2278 'is_active' => 1,
2279 ])['values'];
2280
2281 if (empty($existingRelationships)) {
2282 return FALSE;
2283 }
2284
2285 $membershipInheritedFromContactID = (int) civicrm_api3('Membership', 'getvalue', ['return' => 'contact_id', 'id' => $parentMembershipID]);
2286 // I don't think the api can correctly filter by start & end because of handling for NULL
2287 // so we filter them out here.
2288 foreach ($existingRelationships as $index => $existingRelationship) {
2289 $otherContactID = (int) (($contactID === (int) $existingRelationship['contact_id_a']) ? $existingRelationship['contact_id_b'] : $existingRelationship['contact_id_a']);
2290 if (!empty($existingRelationship['start_date'])
2291 && strtotime($existingRelationship['start_date']) > time()
2292 ) {
2293 unset($existingRelationships[$index]);
2294 continue;
2295 }
2296 if (!empty($existingRelationship['end_date'])
2297 && strtotime($existingRelationship['end_date']) < time()
2298 ) {
2299 unset($existingRelationships[$index]);
2300 continue;
2301 }
2302 if ($membershipInheritedFromContactID !== $otherContactID
2303 ) {
2304 // This is a weird scenario - they have been inheriting the membership
2305 // just not from this relationship - and some max_related calcs etc would be required
2306 // - ie because they are no longer inheriting from this relationship's 'allowance'
2307 // and now are inheriting from the other relationships 'allowance', if it has not
2308 // already hit 'max_related'
2309 // For now ignore here & hope it's handled elsewhere - at least that's consistent with
2310 // before this function was added.
2311 unset($existingRelationships[$index]);
2312 continue;
2313 }
2314 if (!civicrm_api3('Contact', 'getcount', ['id' => $otherContactID, 'is_deleted' => 0])) {
2315 // Can't inherit from a deleted contact.
2316 unset($existingRelationships[$index]);
2317 continue;
2318 }
2319 }
2320 return !empty($existingRelationships);
98fd6fc8 2321 }
2322
52494648 2323 /**
2324 * Add an inherited membership, provided max related not exceeded.
2325 *
2326 * @param array $membershipValues
2327 *
2328 * @return array
2329 * @throws \CRM_Core_Exception
2330 */
2331 protected static function addInheritedMembership($membershipValues) {
2332 $query = "
2333SELECT count(*)
2334 FROM civicrm_membership
2335 LEFT JOIN civicrm_membership_status ON (civicrm_membership_status.id = civicrm_membership.status_id)
2336 WHERE membership_type_id = {$membershipValues['membership_type_id']}
2337 AND owner_membership_id = {$membershipValues['owner_membership_id']}
2338 AND is_current_member = 1";
2339 $result = CRM_Core_DAO::singleValueQuery($query);
2340 if ($result < CRM_Utils_Array::value('max_related', $membershipValues, PHP_INT_MAX)) {
3566c1c3
JJ
2341 // Convert custom_xx_id fields to custom_xx
2342 // See https://lab.civicrm.org/dev/membership/-/issues/37
2343 // This makes sure the value is copied and not the looked up value.
2344 // Which is the case when the custom field is a contact reference field.
2345 // The custom_xx contains the display name of the contact, instead of the contact id.
2346 // The contact id is then available in custom_xx_id.
2347 foreach ($membershipValues as $field => $value) {
2348 if (stripos($field, 'custom_') !== 0) {
2349 // No a custom field
2350 continue;
2351 }
2352 $custom_id = substr($field, 7);
2353 if (substr($custom_id, -3) === '_id') {
2354 $custom_id = substr($custom_id, 0, -3);
2355 unset($membershipValues[$field]);
2356 $membershipValues['custom_' . $custom_id] = $value;
2357 }
2358 }
2359
058180bb 2360 civicrm_api3('Membership', 'create', $membershipValues);
52494648 2361 }
2362 return $membershipValues;
2363 }
2364
6a488035 2365}