Fix Dedupe entity_tag mangling bug
[civicrm-core.git] / tests / phpunit / api / v3 / JobTest.php
CommitLineData
6a488035 1<?php
6a488035
TO
2/*
3 +--------------------------------------------------------------------+
7d61e75f 4 | Copyright CiviCRM LLC. All rights reserved. |
6a488035 5 | |
7d61e75f
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
TO
9 +--------------------------------------------------------------------+
10 */
11
12/**
13 * File for the CiviCRM APIv3 job functions
14 *
15 * @package CiviCRM_APIv3
16 * @subpackage API_Job
17 *
ca5cec67 18 * @copyright CiviCRM LLC https://civicrm.org/licensing
6a488035 19 */
4cbe18b8
EM
20
21/**
22 * Class api_v3_JobTest
e07cf53e 23 *
acb109b7 24 * @group headless
4cbe18b8 25 */
6a488035 26class api_v3_JobTest extends CiviUnitTestCase {
b7c9bc4c 27
6a488035 28 public $DBResetRequired = FALSE;
e07cf53e 29
6a488035 30 public $_entity = 'Job';
e07cf53e 31
5d8b37be 32 /**
33 * Created membership type.
34 *
35 * Must be created outside the transaction due to it breaking the transaction.
36 *
39b959db 37 * @var int
5d8b37be 38 */
39 public $membershipTypeID;
6a488035 40
2149ddb0
EM
41 /**
42 * Set up for tests.
43 */
00be9182 44 public function setUp() {
6a488035 45 parent::setUp();
e07cf53e 46 $this->membershipTypeID = $this->membershipTypeCreate(['name' => 'General']);
6510b20a 47 $this->useTransaction(TRUE);
e07cf53e 48 $this->_params = [
ef1672da 49 'sequential' => 1,
50 'name' => 'API_Test_Job',
51 'description' => 'A long description written by hand in cursive',
52 'run_frequency' => 'Daily',
53 'api_entity' => 'ApiTestEntity',
54 'api_action' => 'apitestaction',
55 'parameters' => 'Semi-formal explanation of runtime job parameters',
56 'is_active' => 1,
e07cf53e 57 ];
6a488035
TO
58 }
59
2f0b4fc5 60 /**
61 * Cleanup after test.
62 *
63 * @throws \CRM_Core_Exception
64 */
5d8b37be 65 public function tearDown() {
66 parent::tearDown();
204beda3 67 // The membershipType create breaks transactions so this extra cleanup is needed.
e07cf53e 68 $this->membershipTypeDelete(['id' => $this->membershipTypeID]);
5738e2e2 69 $this->cleanUpSetUpIDs();
eca28463 70 $this->quickCleanUpFinancialEntities();
26b7ff28 71 $this->quickCleanup(['civicrm_contact', 'civicrm_address', 'civicrm_email', 'civicrm_website', 'civicrm_phone'], TRUE);
fda18dc3 72 parent::tearDown();
5d8b37be 73 }
74
6a488035 75 /**
6d6dc885 76 * Check with no name.
6a488035 77 */
00be9182 78 public function testCreateWithoutName() {
e07cf53e 79 $params = [
5896d037 80 'is_active' => 1,
e07cf53e 81 ];
6d6dc885 82 $this->callAPIFailure('job', 'create', $params,
f39bacdf 83 'Mandatory key(s) missing from params array: run_frequency, name, api_entity, api_action'
84 );
85 }
6a488035
TO
86
87 /**
fe482240 88 * Create job with an invalid "run_frequency" value.
6a488035 89 */
00be9182 90 public function testCreateWithInvalidFrequency() {
e07cf53e 91 $params = [
6a488035
TO
92 'sequential' => 1,
93 'name' => 'API_Test_Job',
94 'description' => 'A long description written by hand in cursive',
95 'run_frequency' => 'Fortnightly',
96 'api_entity' => 'ApiTestEntity',
97 'api_action' => 'apitestaction',
98 'parameters' => 'Semi-formal explanation of runtime job parameters',
99 'is_active' => 1,
e07cf53e 100 ];
76ddbc8e 101 $this->callAPIFailure('job', 'create', $params);
6a488035
TO
102 }
103
104 /**
6d6dc885 105 * Create job.
6a488035 106 */
00be9182 107 public function testCreate() {
ef1672da 108 $result = $this->callAPIAndDocument('job', 'create', $this->_params, __FUNCTION__, __FILE__);
ba4a1892 109 $this->assertNotNull($result['values'][0]['id']);
6a488035
TO
110
111 // mutate $params to match expected return value
ef1672da 112 unset($this->_params['sequential']);
6a488035 113 //assertDBState compares expected values in $result to actual values in the DB
ef1672da 114 $this->assertDBState('CRM_Core_DAO_Job', $result['id'], $this->_params);
6a488035
TO
115 }
116
125918dd
MW
117 /**
118 * Clone job
2f0b4fc5 119 *
120 * @throws \CRM_Core_Exception
125918dd
MW
121 */
122 public function testClone() {
123 $createResult = $this->callAPISuccess('job', 'create', $this->_params);
e07cf53e 124 $params = ['id' => $createResult['id']];
125918dd
MW
125 $cloneResult = $this->callAPIAndDocument('job', 'clone', $params, __FUNCTION__, __FILE__);
126 $clonedJob = $cloneResult['values'][$cloneResult['id']];
127 $this->assertEquals($this->_params['name'] . ' - Copy', $clonedJob['name']);
128 $this->assertEquals($this->_params['description'], $clonedJob['description']);
129 $this->assertEquals($this->_params['parameters'], $clonedJob['parameters']);
130 $this->assertEquals($this->_params['is_active'], $clonedJob['is_active']);
131 $this->assertArrayNotHasKey('last_run', $clonedJob);
132 $this->assertArrayNotHasKey('scheduled_run_date', $clonedJob);
133 }
134
6a488035 135 /**
6d6dc885 136 * Check if required fields are not passed.
6a488035 137 */
00be9182 138 public function testDeleteWithoutRequired() {
e07cf53e 139 $params = [
6a488035
TO
140 'name' => 'API_Test_PP',
141 'title' => 'API Test Payment Processor',
142 'class_name' => 'CRM_Core_Payment_APITest',
e07cf53e 143 ];
6a488035 144
d0e1eff2
CW
145 $result = $this->callAPIFailure('job', 'delete', $params);
146 $this->assertEquals($result['error_message'], 'Mandatory key(s) missing from params array: id');
6a488035
TO
147 }
148
149 /**
6d6dc885 150 * Check with incorrect required fields.
6a488035 151 */
00be9182 152 public function testDeleteWithIncorrectData() {
e07cf53e 153 $params = [
5896d037 154 'id' => 'abcd',
e07cf53e 155 ];
2149ddb0 156 $this->callAPIFailure('job', 'delete', $params);
6a488035
TO
157 }
158
159 /**
6d6dc885 160 * Check job delete.
6a488035 161 */
00be9182 162 public function testDelete() {
ef1672da 163 $createResult = $this->callAPISuccess('job', 'create', $this->_params);
e07cf53e 164 $params = ['id' => $createResult['id']];
2149ddb0 165 $this->callAPIAndDocument('job', 'delete', $params, __FUNCTION__, __FILE__);
ef1672da 166 $this->assertAPIDeleted($this->_entity, $createResult['id']);
6a488035
TO
167 }
168
169 /**
2f0b4fc5 170 * Test greeting update job.
5896d037 171 *
6c6e6187 172 * Note that this test is about tesing the metadata / calling of the function & doesn't test the success of the called function
2f0b4fc5 173 *
174 * @throws \CRM_Core_Exception
6c6e6187 175 */
6a488035 176 public function testCallUpdateGreetingSuccess() {
e07cf53e 177 $this->callAPISuccess($this->_entity, 'update_greeting', [
92915c55
TO
178 'gt' => 'postal_greeting',
179 'ct' => 'Individual',
e07cf53e 180 ]);
6c6e6187 181 }
6a488035 182
2f0b4fc5 183 /**
184 * Test greeting update handles comma separated params.
185 *
186 * @throws \CRM_Core_Exception
187 */
6a488035
TO
188 public function testCallUpdateGreetingCommaSeparatedParamsSuccess() {
189 $gt = 'postal_greeting,email_greeting,addressee';
190 $ct = 'Individual,Household';
e07cf53e 191 $this->callAPISuccess($this->_entity, 'update_greeting', ['gt' => $gt, 'ct' => $ct]);
6a488035 192 }
49f8272d 193
d946b676 194 /**
fe482240
EM
195 * Test the call reminder success sends more than 25 reminders & is not incorrectly limited.
196 *
d946b676 197 * Note that this particular test sends the reminders to the additional recipients only
198 * as no real reminder person is configured
199 *
200 * Also note that this is testing a 'job' api so is in this class rather than scheduled_reminder - which
201 * seems a cleaner place to build up a collection of scheduled reminder testing functions. However, it seems
202 * that the api itself would need to be moved to the scheduled_reminder fn to do that with the job wrapper being respected for legacy functions
2f0b4fc5 203 *
204 * @throws \CRM_Core_Exception
d946b676 205 */
206 public function testCallSendReminderSuccessMoreThanDefaultLimit() {
207 $membershipTypeID = $this->membershipTypeCreate();
0090e3d2 208 $this->membershipStatusCreate();
d946b676 209 $createTotal = 30;
5896d037 210 for ($i = 1; $i <= $createTotal; $i++) {
d946b676 211 $contactID = $this->individualCreate();
e07cf53e 212 $groupID = $this->groupCreate(['name' => $i, 'title' => $i]);
213 $this->callAPISuccess('action_schedule', 'create', [
d946b676 214 'title' => " job $i",
215 'subject' => "job $i",
216 'entity_value' => $membershipTypeID,
217 'mapping_id' => 4,
218 'start_action_date' => 'membership_join_date',
219 'start_action_offset' => 0,
220 'start_action_condition' => 'before',
221 'start_action_unit' => 'hour',
222 'group_id' => $groupID,
223 'limit_to' => FALSE,
e07cf53e 224 ]);
225 $this->callAPISuccess('group_contact', 'create', [
92915c55
TO
226 'contact_id' => $contactID,
227 'status' => 'Added',
228 'group_id' => $groupID,
e07cf53e 229 ]);
d946b676 230 }
e07cf53e 231 $this->callAPISuccess('job', 'send_reminder', []);
d946b676 232 $successfulCronCount = CRM_Core_DAO::singleValueQuery("SELECT count(*) FROM civicrm_action_log");
233 $this->assertEquals($successfulCronCount, $createTotal);
234 }
235
ff32c128 236 /**
fe482240
EM
237 * Test scheduled reminders respect limit to (since above identified addition_to handling issue).
238 *
ff32c128
EM
239 * We create 3 contacts - 1 is in our group, 1 has our membership & the chosen one has both
240 * & check that only the chosen one got the reminder
2f0b4fc5 241 *
242 * @throws \CRM_Core_Exception
243 * @throws \CiviCRM_API3_Exception
ff32c128 244 */
51c566a3 245 public function testCallSendReminderLimitToSMS() {
2f0b4fc5 246 list($membershipTypeID, $groupID, $theChosenOneID, $provider) = $this->setUpMembershipSMSReminders();
e07cf53e 247 $this->callAPISuccess('action_schedule', 'create', [
2f0b4fc5 248 'title' => ' remind all Texans',
249 'subject' => 'drawling renewal',
ff32c128
EM
250 'entity_value' => $membershipTypeID,
251 'mapping_id' => 4,
a60ba2b3 252 'start_action_date' => 'membership_start_date',
207a414e 253 'start_action_offset' => 1,
ff32c128 254 'start_action_condition' => 'before',
207a414e 255 'start_action_unit' => 'day',
ff32c128
EM
256 'group_id' => $groupID,
257 'limit_to' => TRUE,
51c566a3
SL
258 'sms_provider_id' => $provider['id'],
259 'mode' => 'User_Preference',
e07cf53e 260 ]);
261 $this->callAPISuccess('job', 'send_reminder', []);
ff32c128
EM
262 $successfulCronCount = CRM_Core_DAO::singleValueQuery("SELECT count(*) FROM civicrm_action_log");
263 $this->assertEquals($successfulCronCount, 1);
264 $sentToID = CRM_Core_DAO::singleValueQuery("SELECT contact_id FROM civicrm_action_log");
265 $this->assertEquals($sentToID, $theChosenOneID);
51c566a3
SL
266 $this->assertEquals(0, CRM_Core_DAO::singleValueQuery("SELECT is_error FROM civicrm_action_log"));
267 $this->setupForSmsTests(TRUE);
ff32c128
EM
268 }
269
2f0b4fc5 270 /**
271 * Test disabling expired relationships.
272 *
273 * @throws \CRM_Core_Exception
274 */
49f8272d
E
275 public function testCallDisableExpiredRelationships() {
276 $individualID = $this->individualCreate();
277 $orgID = $this->organizationCreate();
e07cf53e 278 CRM_Utils_Hook_UnitTests::singleton()->setHook('civicrm_pre', [$this, 'hookPreRelationship']);
279 $relationshipTypeID = $this->callAPISuccess('relationship_type', 'getvalue', [
92915c55
TO
280 'return' => 'id',
281 'name_a_b' => 'Employee of',
e07cf53e 282 ]);
283 $result = $this->callAPISuccess('relationship', 'create', [
49f8272d
E
284 'relationship_type_id' => $relationshipTypeID,
285 'contact_id_a' => $individualID,
286 'contact_id_b' => $orgID,
287 'is_active' => 1,
288 'end_date' => 'yesterday',
e07cf53e 289 ]);
49f8272d
E
290 $relationshipID = $result['id'];
291 $this->assertEquals('Hooked', $result['values'][$relationshipID]['description']);
e07cf53e 292 $this->callAPISuccess($this->_entity, 'disable_expired_relationships', []);
293 $result = $this->callAPISuccess('relationship', 'get', []);
49f8272d
E
294 $this->assertEquals('Go Go you good thing', $result['values'][$relationshipID]['description']);
295 $this->contactDelete($individualID);
296 $this->contactDelete($orgID);
297 }
298
5b4d40ab
JG
299 /**
300 * Event templates should not send reminders to additional contacts.
2f0b4fc5 301 *
302 * @throws \CRM_Core_Exception
5b4d40ab
JG
303 */
304 public function testTemplateRemindAddlContacts() {
305 $contactId = $this->individualCreate();
306 $groupId = $this->groupCreate(['name' => 'Additional Contacts', 'title' => 'Additional Contacts']);
307 $this->callAPISuccess('GroupContact', 'create', [
308 'contact_id' => $contactId,
309 'group_id' => $groupId,
310 ]);
311 $event = $this->eventCreate(['is_template' => 1, 'template_title' => "I'm a template", 'title' => NULL]);
312 $eventId = $event['id'];
313
2f0b4fc5 314 $this->callAPISuccess('action_schedule', 'create', [
315 'title' => 'Do not send me',
316 'subject' => 'I am a reminder attached to a template.',
5b4d40ab
JG
317 'entity_value' => $eventId,
318 'mapping_id' => 5,
319 'start_action_date' => 'start_date',
320 'start_action_offset' => 1,
321 'start_action_condition' => 'before',
322 'start_action_unit' => 'day',
323 'group_id' => $groupId,
324 'limit_to' => FALSE,
325 'mode' => 'Email',
326 ]);
327
328 $this->callAPISuccess('job', 'send_reminder', []);
2f0b4fc5 329 $successfulCronCount = CRM_Core_DAO::singleValueQuery('SELECT count(*) FROM civicrm_action_log');
5b4d40ab
JG
330 $this->assertEquals(0, $successfulCronCount);
331 }
332
51c566a3
SL
333 /**
334 * Test scheduled reminders respect limit to (since above identified addition_to handling issue).
335 *
336 * We create 3 contacts - 1 is in our group, 1 has our membership & the chosen one has both
337 * & check that only the chosen one got the reminder
338 *
339 * Also check no hard fail on cron job with running a reminder that has a deleted SMS provider
2f0b4fc5 340 *
341 * @throws \CRM_Core_Exception
342 * @throws \CiviCRM_API3_Exception
51c566a3 343 */
2f0b4fc5 344 public function testCallSendReminderLimitToSMSWithDeletedProvider() {
345 list($membershipTypeID, $groupID, $theChosenOneID, $provider) = $this->setUpMembershipSMSReminders();
e07cf53e 346 $this->callAPISuccess('action_schedule', 'create', [
2f0b4fc5 347 'title' => ' remind all Texans',
348 'subject' => 'drawling renewal',
51c566a3
SL
349 'entity_value' => $membershipTypeID,
350 'mapping_id' => 4,
351 'start_action_date' => 'membership_start_date',
352 'start_action_offset' => 1,
353 'start_action_condition' => 'before',
354 'start_action_unit' => 'day',
355 'group_id' => $groupID,
356 'limit_to' => TRUE,
357 'sms_provider_id' => $provider['id'],
358 'mode' => 'SMS',
e07cf53e 359 ]);
51c566a3 360 $this->callAPISuccess('SmsProvider', 'delete', ['id' => $provider['id']]);
e07cf53e 361 $this->callAPISuccess('job', 'send_reminder', []);
51c566a3
SL
362 $cronCount = CRM_Core_DAO::singleValueQuery("SELECT count(*) FROM civicrm_action_log");
363 $this->assertEquals($cronCount, 1);
364 $sentToID = CRM_Core_DAO::singleValueQuery("SELECT contact_id FROM civicrm_action_log");
365 $this->assertEquals($sentToID, $theChosenOneID);
366 $cronlog = CRM_Core_DAO::executeQuery("SELECT * FROM civicrm_action_log")->fetchAll()[0];
367 $this->assertEquals(1, $cronlog['is_error']);
7f38cf7f 368 $this->assertEquals('SMS reminder cannot be sent because the SMS provider has been deleted.', $cronlog['message']);
51c566a3
SL
369 $this->setupForSmsTests(TRUE);
370 }
371
76ddbc8e 372 /**
373 * Test the batch merge function.
a45614cc 374 *
375 * We are just checking it returns without error here.
2f0b4fc5 376 *
377 * @throws \CRM_Core_Exception
76ddbc8e 378 */
379 public function testBatchMerge() {
e07cf53e 380 $this->callAPISuccess('Job', 'process_batch_merge', []);
76ddbc8e 381 }
382
a45614cc 383 /**
384 * Test the batch merge function actually works!
385 *
386 * @dataProvider getMergeSets
387 *
388 * @param $dataSet
2f0b4fc5 389 *
390 * @throws \CRM_Core_Exception
a45614cc 391 */
392 public function testBatchMergeWorks($dataSet) {
393 foreach ($dataSet['contacts'] as $params) {
394 $this->callAPISuccess('Contact', 'create', $params);
395 }
396
e07cf53e 397 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['mode' => $dataSet['mode']]);
2f0b4fc5 398 $this->assertCount($dataSet['skipped'], $result['values']['skipped'], 'Failed to skip the right number:' . $dataSet['skipped']);
399 $this->assertCount($dataSet['merged'], $result['values']['merged']);
e07cf53e 400 $result = $this->callAPISuccess('Contact', 'get', [
92a77772 401 'contact_sub_type' => 'Student',
402 'sequential' => 1,
e07cf53e 403 'is_deceased' => ['IN' => [0, 1]],
404 'options' => ['sort' => 'id ASC'],
405 ]);
962f4484 406 $this->assertEquals(count($dataSet['expected']), $result['count']);
407 foreach ($dataSet['expected'] as $index => $contact) {
408 foreach ($contact as $key => $value) {
2f0b4fc5 409 if ($key === 'gender_id') {
92a77772 410 $key = 'gender';
5d8b37be 411 }
a45614cc 412 $this->assertEquals($value, $result['values'][$index][$key]);
413 }
414 }
415 }
416
5d8b37be 417 /**
418 * Check that the merge carries across various related entities.
419 *
420 * Note the group combinations & expected results:
2f0b4fc5 421 *
422 * @throws \CRM_Core_Exception
5d8b37be 423 */
424 public function testBatchMergeWithAssets() {
425 $contactID = $this->individualCreate();
426 $contact2ID = $this->individualCreate();
e07cf53e 427 $this->contributionCreate(['contact_id' => $contactID]);
428 $this->contributionCreate(['contact_id' => $contact2ID, 'invoice_id' => '2', 'trxn_id' => 2]);
429 $this->contactMembershipCreate(['contact_id' => $contactID]);
430 $this->contactMembershipCreate(['contact_id' => $contact2ID]);
431 $this->activityCreate(['source_contact_id' => $contactID, 'target_contact_id' => $contactID, 'assignee_contact_id' => $contactID]);
432 $this->activityCreate(['source_contact_id' => $contact2ID, 'target_contact_id' => $contact2ID, 'assignee_contact_id' => $contact2ID]);
433 $this->tagCreate(['name' => 'Tall']);
434 $this->tagCreate(['name' => 'Short']);
435 $this->entityTagAdd(['contact_id' => $contactID, 'tag_id' => 'Tall']);
436 $this->entityTagAdd(['contact_id' => $contact2ID, 'tag_id' => 'Short']);
437 $this->entityTagAdd(['contact_id' => $contact2ID, 'tag_id' => 'Tall']);
438 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['mode' => 'safe']);
eca28463 439 $this->assertCount(0, $result['values']['skipped']);
440 $this->assertCount(1, $result['values']['merged']);
e07cf53e 441 $this->callAPISuccessGetCount('Contribution', ['contact_id' => $contactID], 2);
442 $this->callAPISuccessGetCount('Contribution', ['contact_id' => $contact2ID], 0);
443 $this->callAPISuccessGetCount('FinancialItem', ['contact_id' => $contactID], 2);
444 $this->callAPISuccessGetCount('FinancialItem', ['contact_id' => $contact2ID], 0);
445 $this->callAPISuccessGetCount('Membership', ['contact_id' => $contactID], 2);
446 $this->callAPISuccessGetCount('Membership', ['contact_id' => $contact2ID], 0);
447 $this->callAPISuccessGetCount('EntityTag', ['contact_id' => $contactID], 2);
448 $this->callAPISuccessGetCount('EntityTag', ['contact_id' => $contact2ID], 0);
6150b2a0
MD
449 // 14 activities is one for each contribution (2), two (source + target) for each membership (+(2x2) = 6)
450 // 3 for each of the added activities as there are 3 roles (+6 = 12
451 // 2 for the (source & target) contact merged activity (+2 = 14)
e07cf53e 452 $this->callAPISuccessGetCount('ActivityContact', ['contact_id' => $contactID], 14);
5d8b37be 453 // 2 for the connection to the deleted by merge activity (source & target)
e07cf53e 454 $this->callAPISuccessGetCount('ActivityContact', ['contact_id' => $contact2ID], 2);
5d8b37be 455 }
456
eca28463 457 /**
458 * Test that non-contact entity tags are untouched in merge.
459 *
460 * @throws \CRM_Core_Exception
461 */
462 public function testContributionEntityTag() {
463 $this->callAPISuccess('OptionValue', 'create', ['option_group_id' => 'tag_used_for', 'value' => 'civicrm_contribution', 'label' => 'Contribution']);
464 $tagID = $this->tagCreate(['name' => 'Big', 'used_for' => 'civicrm_contribution'])['id'];
465 $contact1 = (int) $this->individualCreate();
466 $contact2 = (int) $this->individualCreate();
467 $contributionID = NULL;
468 while ($contributionID !== $contact2) {
469 $contributionID = (int) $this->callAPISuccess('Contribution', 'create', ['contact_id' => $contact1, 'total_amount' => 5, 'financial_type_id' => 'Donation'])['id'];
470 }
471 $entityTagParams = ['entity_id' => $contributionID, 'entity_table' => 'civicrm_contribution', 'tag_id' => $tagID];
472 $this->callAPISuccess('EntityTag', 'create', $entityTagParams);
473 $this->callAPISuccessGetSingle('EntityTag', $entityTagParams);
474 $this->callAPISuccess('Job', 'process_batch_merge', ['mode' => 'safe']);
475 $this->callAPISuccessGetSingle('EntityTag', $entityTagParams);
476 }
477
5d8b37be 478 /**
479 * Check that the merge carries across various related entities.
480 *
481 * Note the group combinations 'expected' results:
482 *
483 * Group 0 Added null Added
484 * Group 1 Added Added Added
485 * Group 2 Added Removed **** Added
486 * Group 3 Removed null **** null
487 * Group 4 Removed Added **** Added
488 * Group 5 Removed Removed **** null
489 * Group 6 null Added Added
490 * Group 7 null Removed **** null
491 *
492 * The ones with **** are the ones where I think a case could be made to change the behaviour.
2f0b4fc5 493 *
494 * @throws \CRM_Core_Exception
5d8b37be 495 */
496 public function testBatchMergeMergesGroups() {
497 $contactID = $this->individualCreate();
498 $contact2ID = $this->individualCreate();
e07cf53e 499 $groups = [];
5d8b37be 500 for ($i = 0; $i < 8; $i++) {
e07cf53e 501 $groups[] = $this->groupCreate([
fc3d8f72 502 'name' => 'mergeGroup' . $i,
503 'title' => 'merge group' . $i,
e07cf53e 504 ]);
5d8b37be 505 }
506
e07cf53e 507 $this->callAPISuccess('GroupContact', 'create', [
fc3d8f72 508 'contact_id' => $contactID,
509 'group_id' => $groups[0],
e07cf53e 510 ]);
511 $this->callAPISuccess('GroupContact', 'create', [
fc3d8f72 512 'contact_id' => $contactID,
513 'group_id' => $groups[1],
e07cf53e 514 ]);
515 $this->callAPISuccess('GroupContact', 'create', [
fc3d8f72 516 'contact_id' => $contactID,
517 'group_id' => $groups[2],
e07cf53e 518 ]);
519 $this->callAPISuccess('GroupContact', 'create', [
fc3d8f72 520 'contact_id' => $contactID,
521 'group_id' => $groups[3],
522 'status' => 'Removed',
e07cf53e 523 ]);
524 $this->callAPISuccess('GroupContact', 'create', [
fc3d8f72 525 'contact_id' => $contactID,
526 'group_id' => $groups[4],
527 'status' => 'Removed',
e07cf53e 528 ]);
529 $this->callAPISuccess('GroupContact', 'create', [
fc3d8f72 530 'contact_id' => $contactID,
531 'group_id' => $groups[5],
532 'status' => 'Removed',
e07cf53e 533 ]);
534 $this->callAPISuccess('GroupContact', 'create', [
fc3d8f72 535 'contact_id' => $contact2ID,
536 'group_id' => $groups[1],
e07cf53e 537 ]);
538 $this->callAPISuccess('GroupContact', 'create', [
fc3d8f72 539 'contact_id' => $contact2ID,
540 'group_id' => $groups[2],
541 'status' => 'Removed',
e07cf53e 542 ]);
543 $this->callAPISuccess('GroupContact', 'create', [
fc3d8f72 544 'contact_id' => $contact2ID,
545 'group_id' => $groups[4],
e07cf53e 546 ]);
547 $this->callAPISuccess('GroupContact', 'create', [
fc3d8f72 548 'contact_id' => $contact2ID,
549 'group_id' => $groups[5],
550 'status' => 'Removed',
e07cf53e 551 ]);
552 $this->callAPISuccess('GroupContact', 'create', [
fc3d8f72 553 'contact_id' => $contact2ID,
554 'group_id' => $groups[6],
e07cf53e 555 ]);
556 $this->callAPISuccess('GroupContact', 'create', [
fc3d8f72 557 'contact_id' => $contact2ID,
558 'group_id' => $groups[7],
559 'status' => 'Removed',
e07cf53e 560 ]);
561 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['mode' => 'safe']);
2f0b4fc5 562 $this->assertCount(0, $result['values']['skipped']);
563 $this->assertCount(1, $result['values']['merged']);
e07cf53e 564 $groupResult = $this->callAPISuccess('GroupContact', 'get', []);
5d8b37be 565 $this->assertEquals(5, $groupResult['count']);
e07cf53e 566 $expectedGroups = [
fc3d8f72 567 $groups[0],
568 $groups[1],
569 $groups[2],
570 $groups[4],
571 $groups[6],
e07cf53e 572 ];
5d8b37be 573 foreach ($groupResult['values'] as $groupValues) {
574 $this->assertEquals($contactID, $groupValues['contact_id']);
575 $this->assertEquals('Added', $groupValues['status']);
576 $this->assertTrue(in_array($groupValues['group_id'], $expectedGroups));
fc3d8f72 577
578 }
579 }
580
581 /**
582 * Test the decisions made for addresses when merging.
583 *
13cc4cac 584 * @dataProvider getMergeLocationData
fc3d8f72 585 *
586 * Scenarios:
587 * (the ones with **** could be disputed as whether it is the best outcome).
588 * 'matching_primary' - Primary matches, including location_type_id. One contact has an additional address.
589 * - result - primary is the shared one. Additional address is retained.
590 * 'matching_primary_reverse' - Primary matches, including location_type_id. Keep both. (opposite order)
591 * - result - primary is the shared one. Additional address is retained.
592 * 'only_one_has_address' - Only one contact has addresses (retain)
593 * - the (only) address is retained
594 * 'only_one_has_address_reverse'
595 * - the (only) address is retained
13cc4cac 596 * 'different_primaries_with_different_location_type' Primaries are different but do not clash due to diff type
fc3d8f72 597 * - result - both addresses kept. The one from the kept (lowest ID) contact is primary
13cc4cac 598 * 'different_primaries_with_different_location_type_reverse' Primaries are different but do not clash due to diff type
fc3d8f72 599 * - result - both addresses kept. The one from the kept (lowest ID) contact is primary
13cc4cac 600 * 'different_primaries_location_match_only_one_address' per previous but a second address matches the primary but is not primary
fc3d8f72 601 * - result - both addresses kept. The one from the kept (lowest ID) contact is primary
13cc4cac 602 * 'different_primaries_location_match_only_one_address_reverse' per previous but a second address matches the primary but is not primary
fc3d8f72 603 * - result - both addresses kept. The one from the kept (lowest ID) contact is primary
13cc4cac 604 * 'same_primaries_different_location' Primary addresses are the same but have different location type IDs
605 * - result primary kept with the lowest ID. Other address retained too (to preserve location type info).
606 * 'same_primaries_different_location_reverse' Primary addresses are the same but have different location type IDs
607 * - result primary kept with the lowest ID. Other address retained too (to preserve location type info).
fc3d8f72 608 *
609 * @param array $dataSet
2f0b4fc5 610 *
611 * @throws \CRM_Core_Exception
fc3d8f72 612 */
613 public function testBatchMergesAddresses($dataSet) {
614 $contactID1 = $this->individualCreate();
615 $contactID2 = $this->individualCreate();
616 foreach ($dataSet['contact_1'] as $address) {
e07cf53e 617 $this->callAPISuccess($dataSet['entity'], 'create', array_merge(['contact_id' => $contactID1], $address));
fc3d8f72 618 }
619 foreach ($dataSet['contact_2'] as $address) {
e07cf53e 620 $this->callAPISuccess($dataSet['entity'], 'create', array_merge(['contact_id' => $contactID2], $address));
fc3d8f72 621 }
622
e07cf53e 623 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['mode' => 'safe']);
2f0b4fc5 624 $this->assertCount(1, $result['values']['merged']);
e07cf53e 625 $addresses = $this->callAPISuccess($dataSet['entity'], 'get', ['contact_id' => $contactID1, 'sequential' => 1]);
2f0b4fc5 626 $this->assertEquals(count($dataSet['expected']), $addresses['count'], 'Did not get the expected result for ' . $dataSet['entity'] . (!empty($dataSet['description']) ? " on dataset {$dataSet['description']}" : ''));
e07cf53e 627 $locationTypes = $this->callAPISuccess($dataSet['entity'], 'getoptions', ['field' => 'location_type_id']);
fc3d8f72 628 foreach ($dataSet['expected'] as $index => $expectedAddress) {
629 foreach ($expectedAddress as $key => $value) {
2f0b4fc5 630 if ($key === 'location_type_id') {
fc3d8f72 631 $this->assertEquals($locationTypes['values'][$addresses['values'][$index][$key]], $value);
632 }
633 else {
5d5b49ad 634 $this->assertEquals($addresses['values'][$index][$key], $value, "mismatch on $key" . (!empty($dataSet['description']) ? " on dataset {$dataSet['description']}" : ''));
fc3d8f72 635 }
636 }
637 }
638 }
639
640 /**
641 * Test altering the address decision by hook.
642 *
13cc4cac 643 * @dataProvider getMergeLocationData
fc3d8f72 644 *
645 * @param array $dataSet
2f0b4fc5 646 *
647 * @throws \CRM_Core_Exception
fc3d8f72 648 */
649 public function testBatchMergesAddressesHook($dataSet) {
650 $contactID1 = $this->individualCreate();
651 $contactID2 = $this->individualCreate();
e07cf53e 652 $this->contributionCreate(['contact_id' => $contactID1, 'receive_date' => '2010-01-01', 'invoice_id' => 1, 'trxn_id' => 1]);
653 $this->contributionCreate(['contact_id' => $contactID2, 'receive_date' => '2012-01-01', 'invoice_id' => 2, 'trxn_id' => 2]);
fc3d8f72 654 foreach ($dataSet['contact_1'] as $address) {
e07cf53e 655 $this->callAPISuccess($dataSet['entity'], 'create', array_merge(['contact_id' => $contactID1], $address));
fc3d8f72 656 }
657 foreach ($dataSet['contact_2'] as $address) {
e07cf53e 658 $this->callAPISuccess($dataSet['entity'], 'create', array_merge(['contact_id' => $contactID2], $address));
fc3d8f72 659 }
e07cf53e 660 $this->hookClass->setHook('civicrm_alterLocationMergeData', [$this, 'hookMostRecentDonor']);
fc3d8f72 661
e07cf53e 662 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['mode' => 'safe']);
2f0b4fc5 663 $this->assertCount(1, $result['values']['merged']);
e07cf53e 664 $addresses = $this->callAPISuccess($dataSet['entity'], 'get', ['contact_id' => $contactID1, 'sequential' => 1]);
fc3d8f72 665 $this->assertEquals(count($dataSet['expected_hook']), $addresses['count']);
e07cf53e 666 $locationTypes = $this->callAPISuccess($dataSet['entity'], 'getoptions', ['field' => 'location_type_id']);
fc3d8f72 667 foreach ($dataSet['expected_hook'] as $index => $expectedAddress) {
668 foreach ($expectedAddress as $key => $value) {
2f0b4fc5 669 if ($key === 'location_type_id') {
fc3d8f72 670 $this->assertEquals($locationTypes['values'][$addresses['values'][$index][$key]], $value);
671 }
672 else {
5d5b49ad 673 $this->assertEquals($value, $addresses['values'][$index][$key], $dataSet['entity'] . ': Unexpected value for ' . $key . (!empty($dataSet['description']) ? " on dataset {$dataSet['description']}" : ''));
fc3d8f72 674 }
675 }
5d8b37be 676 }
677 }
678
679 /**
680 * Test the organization will not be matched to an individual.
2f0b4fc5 681 *
682 * @throws \CRM_Core_Exception
5d8b37be 683 */
684 public function testBatchMergeWillNotMergeOrganizationToIndividual() {
e07cf53e 685 $individual = $this->callAPISuccess('Contact', 'create', [
fc3d8f72 686 'contact_type' => 'Individual',
687 'organization_name' => 'Anon',
688 'email' => 'anonymous@hacker.com',
e07cf53e 689 ]);
690 $organization = $this->callAPISuccess('Contact', 'create', [
fc3d8f72 691 'contact_type' => 'Organization',
692 'organization_name' => 'Anon',
693 'email' => 'anonymous@hacker.com',
e07cf53e 694 ]);
695 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['mode' => 'aggressive']);
2f0b4fc5 696 $this->assertCount(0, $result['values']['skipped']);
697 $this->assertCount(0, $result['values']['merged']);
e07cf53e 698 $this->callAPISuccessGetSingle('Contact', ['id' => $individual['id']]);
699 $this->callAPISuccessGetSingle('Contact', ['id' => $organization['id']]);
fc3d8f72 700
701 }
702
703 /**
92a77772 704 * Test hook allowing modification of the data calculated for merging locations.
705 *
706 * We are testing a nuanced real life situation where the address data of the
707 * most recent donor gets priority - resulting in the primary address being set
708 * to the primary address of the most recent donor and address data on a per
709 * location type basis also being set to the most recent donor. Hook also excludes
710 * a fully matching address with a different location.
fc3d8f72 711 *
92a77772 712 * This has been added to the test suite to ensure the code supports more this
713 * type of intervention.
714 *
715 * @param array $blocksDAO
716 * Array of location DAO to be saved. These are arrays in 2 keys 'update' & 'delete'.
fc3d8f72 717 * @param int $mainId
92a77772 718 * Contact_id of the contact that survives the merge.
fc3d8f72 719 * @param int $otherId
92a77772 720 * Contact_id of the contact that will be absorbed and deleted.
721 * @param array $migrationInfo
722 * Calculated migration info, informational only.
723 *
724 * @return mixed
2f0b4fc5 725 * @throws \CRM_Core_Exception
fc3d8f72 726 */
92a77772 727 public function hookMostRecentDonor(&$blocksDAO, $mainId, $otherId, $migrationInfo) {
728
e07cf53e 729 $lastDonorID = $this->callAPISuccessGetValue('Contribution', [
92a77772 730 'return' => 'contact_id',
e07cf53e 731 'contact_id' => ['IN' => [$mainId, $otherId]],
732 'options' => ['sort' => 'receive_date DESC', 'limit' => 1],
733 ]);
92a77772 734 // Since the last donor is not the main ID we are prioritising info from the last donor.
735 // In the test this should always be true - but keep the check in case
736 // something changes that we need to detect.
737 if ($lastDonorID != $mainId) {
738 foreach ($migrationInfo['other_details']['location_blocks'] as $blockType => $blocks) {
739 foreach ($blocks as $block) {
740 if ($block['is_primary']) {
741 $primaryAddressID = $block['id'];
742 if (!empty($migrationInfo['main_details']['location_blocks'][$blockType])) {
743 foreach ($migrationInfo['main_details']['location_blocks'][$blockType] as $mainBlock) {
744 if (empty($blocksDAO[$blockType]['update'][$block['id']]) && $mainBlock['location_type_id'] == $block['location_type_id']) {
745 // This was an address match - we just need to check the is_primary
746 // is true on the matching kept address.
747 $primaryAddressID = $mainBlock['id'];
748 $blocksDAO[$blockType]['update'][$primaryAddressID] = _civicrm_api3_load_DAO($blockType);
749 $blocksDAO[$blockType]['update'][$primaryAddressID]->id = $primaryAddressID;
750 }
751 $mainLocationTypeID = $mainBlock['location_type_id'];
752 // We also want to be more ruthless about removing matching addresses.
753 unset($mainBlock['location_type_id']);
5d5b49ad 754 if (CRM_Dedupe_Merger::locationIsSame($block, $mainBlock)
92a77772 755 && (!isset($blocksDAO[$blockType]['update']) || !isset($blocksDAO[$blockType]['update'][$mainBlock['id']]))
756 && (!isset($blocksDAO[$blockType]['delete']) || !isset($blocksDAO[$blockType]['delete'][$mainBlock['id']]))
757 ) {
758 $blocksDAO[$blockType]['delete'][$mainBlock['id']] = _civicrm_api3_load_DAO($blockType);
759 $blocksDAO[$blockType]['delete'][$mainBlock['id']]->id = $mainBlock['id'];
760 }
761 // Arguably the right way to handle this is just to set is_primary for the primary
762 // and for the merge fn to call something like BAO::add & hooks to work etc.
763 // if that happens though this should keep working...
764 elseif ($mainBlock['is_primary'] && $mainLocationTypeID != $block['location_type_id']) {
765 $blocksDAO['address']['update'][$mainBlock['id']] = _civicrm_api3_load_DAO($blockType);
766 $blocksDAO['address']['update'][$mainBlock['id']]->is_primary = 0;
767 $blocksDAO['address']['update'][$mainBlock['id']]->id = $mainBlock['id'];
768 }
769
770 }
771 $blocksDAO[$blockType]['update'][$primaryAddressID]->is_primary = 1;
772 }
773 }
774 }
775 }
fc3d8f72 776 }
fc3d8f72 777 }
778
779 /**
780 * Get address combinations for the merge test.
781 *
782 * @return array
783 */
13cc4cac 784 public function getMergeLocationData() {
e07cf53e 785 $address1 = ['street_address' => 'Buckingham Palace', 'city' => 'London'];
786 $address2 = ['street_address' => 'The Doghouse', 'supplemental_address_1' => 'under the blanket'];
13cc4cac 787 $data = $this->getMergeLocations($address1, $address2, 'Address');
e07cf53e 788 $data = array_merge($data, $this->getMergeLocations(['phone' => '12345', 'phone_type_id' => 1], ['phone' => '678910', 'phone_type_id' => 1], 'Phone'));
789 $data = array_merge($data, $this->getMergeLocations(['phone' => '12345'], ['phone' => '678910'], 'Phone'));
790 $data = array_merge($data, $this->getMergeLocations(['email' => 'mini@me.com'], ['email' => 'mini@me.org'], 'Email', [
791 [
39b959db
SL
792 'email' => 'anthony_anderson@civicrm.org',
793 'location_type_id' => 'Home',
e07cf53e 794 ],
795 ]));
fc3d8f72 796 return $data;
5d5b49ad 797
5d8b37be 798 }
799
2e09a60f 800 /**
801 * Test weird characters don't mess with merge & cause a fatal.
802 *
803 * @throws \CRM_Core_Exception
804 */
805 public function testNoErrorOnOdd() {
806 $this->individualCreate();
807 $this->individualCreate(['first_name' => 'Gerrit%0a%2e%0a']);
808 $this->callAPISuccess('Job', 'process_batch_merge', []);
809
810 $this->individualCreate();
811 $this->individualCreate(['first_name' => '[foo\\bar\'baz']);
812 $this->callAPISuccess('Job', 'process_batch_merge', []);
813 $this->callAPISuccessGetSingle('Contact', ['first_name' => '[foo\\bar\'baz']);
814 }
815
99140301 816 /**
c08fae40 817 * Test the batch merge does not create duplicate emails.
818 *
819 * Test CRM-18546, a 4.7 regression whereby a merged contact gets duplicate emails.
2f0b4fc5 820 *
821 * @throws \CRM_Core_Exception
99140301 822 */
c08fae40 823 public function testBatchMergeEmailHandling() {
824 for ($x = 0; $x <= 4; $x++) {
e07cf53e 825 $id = $this->individualCreate(['email' => 'batman@gotham.met']);
c08fae40 826 }
e07cf53e 827 $result = $this->callAPISuccess('Job', 'process_batch_merge', []);
2f0b4fc5 828 $this->assertCount(4, $result['values']['merged']);
e07cf53e 829 $this->callAPISuccessGetCount('Contact', ['email' => 'batman@gotham.met'], 1);
830 $contacts = $this->callAPISuccess('Contact', 'get', ['is_deleted' => 0]);
831 $deletedContacts = $this->callAPISuccess('Contact', 'get', ['is_deleted' => 1]);
832 $this->callAPISuccessGetCount('Email', [
c08fae40 833 'email' => 'batman@gotham.met',
e07cf53e 834 'contact_id' => ['IN' => array_keys($contacts['values'])],
835 ], 1);
836 $this->callAPISuccessGetCount('Email', [
c08fae40 837 'email' => 'batman@gotham.met',
e07cf53e 838 'contact_id' => ['IN' => array_keys($deletedContacts['values'])],
839 ], 4);
99140301 840 }
841
861bd030
DJ
842 /**
843 * Test the batch merge respects email "on hold".
844 *
845 * Test CRM-19148, Batch merge - Email on hold data lost when there is a conflict.
846 *
847 * @dataProvider getOnHoldSets
848 *
fb820b09 849 * @param bool $onHold1
850 * @param bool $onHold2
851 * @param bool $merge
852 * @param string $conflictText
e07cf53e 853 *
854 * @throws \CRM_Core_Exception
861bd030 855 */
fb820b09 856 public function testBatchMergeEmailOnHold($onHold1, $onHold2, $merge, $conflictText) {
857 $this->individualCreate([
e07cf53e 858 'api.email.create' => [
861bd030
DJ
859 'email' => 'batman@gotham.met',
860 'location_type_id' => 'Work',
861 'is_primary' => 1,
862 'on_hold' => $onHold1,
e07cf53e 863 ],
864 ]);
fb820b09 865 $this->individualCreate([
e07cf53e 866 'api.email.create' => [
861bd030
DJ
867 'email' => 'batman@gotham.met',
868 'location_type_id' => 'Work',
869 'is_primary' => 1,
870 'on_hold' => $onHold2,
e07cf53e 871 ],
872 ]);
873 $result = $this->callAPISuccess('Job', 'process_batch_merge', []);
fb820b09 874 $this->assertCount($merge, $result['values']['merged']);
875 if ($conflictText) {
876 $defaultRuleGroupID = $this->callAPISuccessGetValue('RuleGroup', [
877 'contact_type' => 'Individual',
878 'used' => 'Unsupervised',
879 'return' => 'id',
880 'options' => ['limit' => 1],
881 ]);
882
883 $duplicates = $this->callAPISuccess('Dedupe', 'getduplicates', ['rule_group_id' => $defaultRuleGroupID]);
884 $this->assertEquals($conflictText, $duplicates['values'][0]['conflicts']);
885 }
861bd030
DJ
886 }
887
888 /**
889 * Data provider for testBatchMergeEmailOnHold: combinations of on_hold & expected outcomes.
890 */
891 public function getOnHoldSets() {
892 // Each row specifies: contact 1 on_hold, contact 2 on_hold, merge? (0 or 1),
2f0b4fc5 893 return [
fb820b09 894 [0, 0, 1, NULL],
5214f03b 895 [0, 1, 0, "Email 2 (Work): 'batman@gotham.met' vs. 'batman@gotham.met\n(On Hold)'"],
896 [1, 0, 0, "Email 2 (Work): 'batman@gotham.met\n(On Hold)' vs. 'batman@gotham.met'"],
fb820b09 897 [1, 1, 1, NULL],
e07cf53e 898 ];
861bd030
DJ
899 }
900
6c866f0c 901 /**
902 * Test the batch merge does not fatal on an empty rule.
903 *
904 * @dataProvider getRuleSets
905 *
906 * @param string $contactType
907 * @param string $used
39b959db 908 * @param string $name
6c866f0c 909 * @param bool $isReserved
910 * @param int $threshold
2f0b4fc5 911 *
912 * @throws \CRM_Core_Exception
6c866f0c 913 */
914 public function testBatchMergeEmptyRule($contactType, $used, $name, $isReserved, $threshold) {
e07cf53e 915 $ruleGroup = $this->callAPISuccess('RuleGroup', 'create', [
6c866f0c 916 'contact_type' => $contactType,
917 'threshold' => $threshold,
918 'used' => $used,
919 'name' => $name,
920 'is_reserved' => $isReserved,
e07cf53e 921 ]);
922 $this->callAPISuccess('Job', 'process_batch_merge', ['rule_group_id' => $ruleGroup['id']]);
923 $this->callAPISuccess('RuleGroup', 'delete', ['id' => $ruleGroup['id']]);
6c866f0c 924 }
925
926 /**
927 * Get the various rule combinations.
928 */
929 public function getRuleSets() {
e07cf53e 930 $contactTypes = ['Individual', 'Organization', 'Household'];
931 $useds = ['Unsupervised', 'General', 'Supervised'];
932 $ruleGroups = [];
6c866f0c 933 foreach ($contactTypes as $contactType) {
934 foreach ($useds as $used) {
e07cf53e 935 $ruleGroups[] = [$contactType, $used, 'Bob', FALSE, 0];
936 $ruleGroups[] = [$contactType, $used, 'Bob', FALSE, 10];
937 $ruleGroups[] = [$contactType, $used, 'Bob', TRUE, 10];
938 $ruleGroups[] = [$contactType, $used, $contactType . $used, FALSE, 10];
939 $ruleGroups[] = [$contactType, $used, $contactType . $used, TRUE, 10];
6c866f0c 940 }
941 }
942 return $ruleGroups;
943 }
944
d48ad2c0 945 /**
946 * Test the batch merge does not create duplicate emails.
947 *
948 * Test CRM-18546, a 4.7 regression whereby a merged contact gets duplicate emails.
2f0b4fc5 949 *
950 * @throws \CRM_Core_Exception
d48ad2c0 951 */
952 public function testBatchMergeMatchingAddress() {
953 for ($x = 0; $x <= 2; $x++) {
e07cf53e 954 $this->individualCreate([
955 'api.address.create' => [
d48ad2c0 956 'location_type_id' => 'Home',
957 'street_address' => 'Appt 115, The Batcave',
958 'city' => 'Gotham',
959 'postal_code' => 'Nananananana',
e07cf53e 960 ],
961 ]);
d48ad2c0 962 }
963 // Different location type, still merge, identical.
e07cf53e 964 $this->individualCreate([
965 'api.address.create' => [
d48ad2c0 966 'location_type_id' => 'Main',
967 'street_address' => 'Appt 115, The Batcave',
968 'city' => 'Gotham',
969 'postal_code' => 'Nananananana',
e07cf53e 970 ],
971 ]);
d48ad2c0 972
e07cf53e 973 $this->individualCreate([
974 'api.address.create' => [
d48ad2c0 975 'location_type_id' => 'Home',
976 'street_address' => 'Appt 115, The Batcave',
977 'city' => 'Gotham',
978 'postal_code' => 'Batman',
e07cf53e 979 ],
980 ]);
d48ad2c0 981
e07cf53e 982 $result = $this->callAPISuccess('Job', 'process_batch_merge', []);
d48ad2c0 983 $this->assertEquals(3, count($result['values']['merged']));
984 $this->assertEquals(1, count($result['values']['skipped']));
e07cf53e 985 $this->callAPISuccessGetCount('Contact', ['street_address' => 'Appt 115, The Batcave'], 2);
986 $contacts = $this->callAPISuccess('Contact', 'get', ['is_deleted' => 0]);
987 $deletedContacts = $this->callAPISuccess('Contact', 'get', ['is_deleted' => 1]);
988 $this->callAPISuccessGetCount('Address', [
d48ad2c0 989 'street_address' => 'Appt 115, The Batcave',
e07cf53e 990 'contact_id' => ['IN' => array_keys($contacts['values'])],
991 ], 3);
d48ad2c0 992
e07cf53e 993 $this->callAPISuccessGetCount('Address', [
d48ad2c0 994 'street_address' => 'Appt 115, The Batcave',
e07cf53e 995 'contact_id' => ['IN' => array_keys($deletedContacts['values'])],
996 ], 2);
d48ad2c0 997 }
998
e23e26ec 999 /**
1000 * Test the batch merge by id range.
1001 *
1002 * We have 2 sets of 5 matches & set the merge only to merge the lower set.
2f0b4fc5 1003 *
1004 * @throws \CRM_Core_Exception
e23e26ec 1005 */
1006 public function testBatchMergeIDRange() {
1007 for ($x = 0; $x <= 4; $x++) {
e07cf53e 1008 $id = $this->individualCreate(['email' => 'batman@gotham.met']);
e23e26ec 1009 }
1010 for ($x = 0; $x <= 4; $x++) {
e07cf53e 1011 $this->individualCreate(['email' => 'robin@gotham.met']);
e23e26ec 1012 }
e07cf53e 1013 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['criteria' => ['contact' => ['id' => ['<' => $id]]]]);
e23e26ec 1014 $this->assertEquals(4, count($result['values']['merged']));
e07cf53e 1015 $this->callAPISuccessGetCount('Contact', ['email' => 'batman@gotham.met'], 1);
1016 $this->callAPISuccessGetCount('Contact', ['email' => 'robin@gotham.met'], 5);
1017 $contacts = $this->callAPISuccess('Contact', 'get', ['is_deleted' => 0]);
1018 $deletedContacts = $this->callAPISuccess('Contact', 'get', ['is_deleted' => 0]);
1019 $this->callAPISuccessGetCount('Email', [
e23e26ec 1020 'email' => 'batman@gotham.met',
e07cf53e 1021 'contact_id' => ['IN' => array_keys($contacts['values'])],
1022 ], 1);
1023 $this->callAPISuccessGetCount('Email', [
e23e26ec 1024 'email' => 'batman@gotham.met',
e07cf53e 1025 'contact_id' => ['IN' => array_keys($deletedContacts['values'])],
1026 ], 1);
1027 $this->callAPISuccessGetCount('Email', [
e23e26ec 1028 'email' => 'robin@gotham.met',
e07cf53e 1029 'contact_id' => ['IN' => array_keys($contacts['values'])],
1030 ], 5);
e23e26ec 1031
1032 }
1033
ca38a45b 1034 /**
1035 * Test the batch merge copes with view only custom data field.
2f0b4fc5 1036 *
1037 * @throws \CRM_Core_Exception
ca38a45b 1038 */
1039 public function testBatchMergeCustomDataViewOnlyField() {
e07cf53e 1040 CRM_Core_Config::singleton()->userPermissionClass->permissions = ['access CiviCRM', 'edit my contact'];
ca38a45b 1041 $mouseParams = ['first_name' => 'Mickey', 'last_name' => 'Mouse', 'email' => 'tha_mouse@mouse.com'];
1042 $this->individualCreate($mouseParams);
1043
2f0b4fc5 1044 $customGroup = $this->customGroupCreate();
e07cf53e 1045 $customField = $this->customFieldCreate(['custom_group_id' => $customGroup['id'], 'is_view' => 1]);
ca38a45b 1046 $this->individualCreate(array_merge($mouseParams, ['custom_' . $customField['id'] => 'blah']));
1047
e07cf53e 1048 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['check_permissions' => 0, 'mode' => 'safe']);
ca38a45b 1049 $this->assertEquals(1, count($result['values']['merged']));
1050 $mouseParams['return'] = 'custom_' . $customField['id'];
1051 $mouse = $this->callAPISuccess('Contact', 'getsingle', $mouseParams);
1052 $this->assertEquals('blah', $mouse['custom_' . $customField['id']]);
1053
26b7ff28 1054 $this->customFieldDelete($customField['id']);
1055 $this->customGroupDelete($customGroup['id']);
1056 }
1057
1058 /**
1059 * Test the batch merge retains 0 as a valid custom field value.
1060 *
1061 * Note that we set 0 on 2 fields with one on each contact to ensure that
1062 * both merged & mergee fields are respected.
ffa59d18 1063 *
1064 * @throws \CRM_Core_Exception
26b7ff28 1065 */
1066 public function testBatchMergeCustomDataZeroValueField() {
ffa59d18 1067 $customGroup = $this->customGroupCreate();
e07cf53e 1068 $customField = $this->customFieldCreate(['custom_group_id' => $customGroup['id'], 'default_value' => NULL]);
26b7ff28 1069
1070 $mouseParams = ['first_name' => 'Mickey', 'last_name' => 'Mouse', 'email' => 'tha_mouse@mouse.com'];
b51b102b 1071 $this->individualCreate(array_merge($mouseParams, ['custom_' . $customField['id'] => '']));
26b7ff28 1072 $this->individualCreate(array_merge($mouseParams, ['custom_' . $customField['id'] => 0]));
26b7ff28 1073
e07cf53e 1074 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['check_permissions' => 0, 'mode' => 'safe']);
2f0b4fc5 1075 $this->assertCount(1, $result['values']['merged']);
26b7ff28 1076 $mouseParams['return'] = 'custom_' . $customField['id'];
1077 $mouse = $this->callAPISuccess('Contact', 'getsingle', $mouseParams);
1078 $this->assertEquals(0, $mouse['custom_' . $customField['id']]);
1079
b51b102b 1080 $this->individualCreate(array_merge($mouseParams, ['custom_' . $customField['id'] => NULL]));
e07cf53e 1081 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['check_permissions' => 0, 'mode' => 'safe']);
b51b102b
MD
1082 $this->assertEquals(1, count($result['values']['merged']));
1083 $mouseParams['return'] = 'custom_' . $customField['id'];
1084 $mouse = $this->callAPISuccess('Contact', 'getsingle', $mouseParams);
1085 $this->assertEquals(0, $mouse['custom_' . $customField['id']]);
1086
26b7ff28 1087 $this->customFieldDelete($customField['id']);
26b7ff28 1088 $this->customGroupDelete($customGroup['id']);
1089 }
1090
1091 /**
1092 * Test the batch merge treats 0 vs 1 as a conflict.
e07cf53e 1093 *
1094 * @throws \CRM_Core_Exception
26b7ff28 1095 */
1096 public function testBatchMergeCustomDataZeroValueFieldWithConflict() {
e07cf53e 1097 $customGroup = $this->customGroupCreate();
1098 $customField = $this->customFieldCreate(['custom_group_id' => $customGroup['id'], 'default_value' => NULL]);
26b7ff28 1099
1100 $mouseParams = ['first_name' => 'Mickey', 'last_name' => 'Mouse', 'email' => 'tha_mouse@mouse.com'];
1101 $mouse1 = $this->individualCreate(array_merge($mouseParams, ['custom_' . $customField['id'] => 0]));
1102 $mouse2 = $this->individualCreate(array_merge($mouseParams, ['custom_' . $customField['id'] => 1]));
1103
e07cf53e 1104 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['check_permissions' => 0, 'mode' => 'safe']);
2f0b4fc5 1105 $this->assertCount(0, $result['values']['merged']);
26b7ff28 1106
1107 // Reverse which mouse has the zero to test we still get a conflict.
1108 $this->individualCreate(array_merge($mouseParams, ['id' => $mouse1, 'custom_' . $customField['id'] => 1]));
1109 $this->individualCreate(array_merge($mouseParams, ['id' => $mouse2, 'custom_' . $customField['id'] => 0]));
e07cf53e 1110 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['check_permissions' => 0, 'mode' => 'safe']);
26b7ff28 1111 $this->assertEquals(0, count($result['values']['merged']));
1112
1113 $this->customFieldDelete($customField['id']);
ca38a45b 1114 $this->customGroupDelete($customGroup['id']);
1115 }
1116
3058f4d9 1117 /**
1118 * Test the batch merge function actually works!
1119 *
1120 * @dataProvider getMergeSets
1121 *
2f0b4fc5 1122 * @param array $dataSet
1123 *
1124 * @throws \CRM_Core_Exception
3058f4d9 1125 */
1126 public function testBatchMergeWorksCheckPermissionsTrue($dataSet) {
418ffc5b 1127 CRM_Core_Config::singleton()->userPermissionClass->permissions = ['access CiviCRM', 'administer CiviCRM', 'merge duplicate contacts', 'force merge duplicate contacts'];
3058f4d9 1128 foreach ($dataSet['contacts'] as $params) {
1129 $this->callAPISuccess('Contact', 'create', $params);
1130 }
1131
e07cf53e 1132 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['check_permissions' => 1, 'mode' => $dataSet['mode']]);
2f0b4fc5 1133 $this->assertCount(0, $result['values']['merged'], 'User does not have permission to any contacts, so no merging');
1134 $this->assertCount(0, $result['values']['skipped'], 'User does not have permission to any contacts, so no skip visibility');
3058f4d9 1135 }
1136
1137 /**
1138 * Test the batch merge function actually works!
1139 *
1140 * @dataProvider getMergeSets
1141 *
2f0b4fc5 1142 * @param array $dataSet
1143 *
1144 * @throws \CRM_Core_Exception
3058f4d9 1145 */
1146 public function testBatchMergeWorksCheckPermissionsFalse($dataSet) {
e07cf53e 1147 CRM_Core_Config::singleton()->userPermissionClass->permissions = ['access CiviCRM', 'edit my contact'];
3058f4d9 1148 foreach ($dataSet['contacts'] as $params) {
1149 $this->callAPISuccess('Contact', 'create', $params);
1150 }
1151
e07cf53e 1152 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['check_permissions' => 0, 'mode' => $dataSet['mode']]);
3058f4d9 1153 $this->assertEquals($dataSet['skipped'], count($result['values']['skipped']), 'Failed to skip the right number:' . $dataSet['skipped']);
1154 $this->assertEquals($dataSet['merged'], count($result['values']['merged']));
1155 }
1156
a45614cc 1157 /**
1158 * Get data for batch merge.
1159 */
1160 public function getMergeSets() {
e07cf53e 1161 $data = [
1162 [
1163 [
99140301 1164 'mode' => 'safe',
e07cf53e 1165 'contacts' => [
1166 [
a45614cc 1167 'first_name' => 'Michael',
1168 'last_name' => 'Jackson',
1169 'email' => 'michael@neverland.com',
1170 'contact_type' => 'Individual',
1171 'contact_sub_type' => 'Student',
e07cf53e 1172 'api.Address.create' => [
a45614cc 1173 'street_address' => 'big house',
1174 'location_type_id' => 'Home',
e07cf53e 1175 ],
1176 ],
1177 [
a45614cc 1178 'first_name' => 'Michael',
1179 'last_name' => 'Jackson',
1180 'email' => 'michael@neverland.com',
1181 'contact_type' => 'Individual',
1182 'contact_sub_type' => 'Student',
e07cf53e 1183 ],
1184 ],
a45614cc 1185 'skipped' => 0,
1186 'merged' => 1,
e07cf53e 1187 'expected' => [
1188 [
a45614cc 1189 'first_name' => 'Michael',
1190 'last_name' => 'Jackson',
1191 'email' => 'michael@neverland.com',
1192 'contact_type' => 'Individual',
e07cf53e 1193 ],
1194 ],
1195 ],
1196 ],
1197 [
1198 [
99140301 1199 'mode' => 'safe',
e07cf53e 1200 'contacts' => [
1201 [
a45614cc 1202 'first_name' => 'Michael',
1203 'last_name' => 'Jackson',
1204 'email' => 'michael@neverland.com',
1205 'contact_type' => 'Individual',
1206 'contact_sub_type' => 'Student',
e07cf53e 1207 'api.Address.create' => [
a45614cc 1208 'street_address' => 'big house',
1209 'location_type_id' => 'Home',
e07cf53e 1210 ],
1211 ],
1212 [
a45614cc 1213 'first_name' => 'Michael',
1214 'last_name' => 'Jackson',
1215 'email' => 'michael@neverland.com',
1216 'contact_type' => 'Individual',
1217 'contact_sub_type' => 'Student',
e07cf53e 1218 'api.Address.create' => [
a45614cc 1219 'street_address' => 'bigger house',
1220 'location_type_id' => 'Home',
e07cf53e 1221 ],
1222 ],
1223 ],
a45614cc 1224 'skipped' => 1,
1225 'merged' => 0,
e07cf53e 1226 'expected' => [
1227 [
a45614cc 1228 'first_name' => 'Michael',
1229 'last_name' => 'Jackson',
1230 'email' => 'michael@neverland.com',
1231 'contact_type' => 'Individual',
1232 'street_address' => 'big house',
e07cf53e 1233 ],
1234 [
a45614cc 1235 'first_name' => 'Michael',
1236 'last_name' => 'Jackson',
1237 'email' => 'michael@neverland.com',
1238 'contact_type' => 'Individual',
1239 'street_address' => 'bigger house',
e07cf53e 1240 ],
1241 ],
1242 ],
1243 ],
1244 [
1245 [
5d5b49ad 1246 'mode' => 'safe',
e07cf53e 1247 'contacts' => [
1248 [
5d5b49ad 1249 'first_name' => 'Michael',
1250 'last_name' => 'Jackson',
1251 'email' => 'michael@neverland.com',
1252 'contact_type' => 'Individual',
1253 'contact_sub_type' => 'Student',
e07cf53e 1254 'api.Email.create' => [
5d5b49ad 1255 'email' => 'big.slog@work.co.nz',
1256 'location_type_id' => 'Work',
e07cf53e 1257 ],
1258 ],
1259 [
5d5b49ad 1260 'first_name' => 'Michael',
1261 'last_name' => 'Jackson',
1262 'email' => 'michael@neverland.com',
1263 'contact_type' => 'Individual',
1264 'contact_sub_type' => 'Student',
e07cf53e 1265 'api.Email.create' => [
5d5b49ad 1266 'email' => 'big.slog@work.com',
1267 'location_type_id' => 'Work',
e07cf53e 1268 ],
1269 ],
1270 ],
5d5b49ad 1271 'skipped' => 1,
1272 'merged' => 0,
e07cf53e 1273 'expected' => [
1274 [
5d5b49ad 1275 'first_name' => 'Michael',
1276 'last_name' => 'Jackson',
1277 'email' => 'michael@neverland.com',
1278 'contact_type' => 'Individual',
e07cf53e 1279 ],
1280 [
5d5b49ad 1281 'first_name' => 'Michael',
1282 'last_name' => 'Jackson',
1283 'email' => 'michael@neverland.com',
1284 'contact_type' => 'Individual',
e07cf53e 1285 ],
1286 ],
1287 ],
1288 ],
1289 [
1290 [
5d5b49ad 1291 'mode' => 'safe',
e07cf53e 1292 'contacts' => [
1293 [
5d5b49ad 1294 'first_name' => 'Michael',
1295 'last_name' => 'Jackson',
1296 'email' => 'michael@neverland.com',
1297 'contact_type' => 'Individual',
1298 'contact_sub_type' => 'Student',
e07cf53e 1299 'api.Phone.create' => [
5d5b49ad 1300 'phone' => '123456',
1301 'location_type_id' => 'Work',
e07cf53e 1302 ],
1303 ],
1304 [
5d5b49ad 1305 'first_name' => 'Michael',
1306 'last_name' => 'Jackson',
1307 'email' => 'michael@neverland.com',
1308 'contact_type' => 'Individual',
1309 'contact_sub_type' => 'Student',
e07cf53e 1310 'api.Phone.create' => [
5d5b49ad 1311 'phone' => '23456',
1312 'location_type_id' => 'Work',
e07cf53e 1313 ],
1314 ],
1315 ],
5d5b49ad 1316 'skipped' => 1,
1317 'merged' => 0,
e07cf53e 1318 'expected' => [
1319 [
5d5b49ad 1320 'first_name' => 'Michael',
1321 'last_name' => 'Jackson',
1322 'email' => 'michael@neverland.com',
1323 'contact_type' => 'Individual',
e07cf53e 1324 ],
1325 [
5d5b49ad 1326 'first_name' => 'Michael',
1327 'last_name' => 'Jackson',
1328 'email' => 'michael@neverland.com',
1329 'contact_type' => 'Individual',
e07cf53e 1330 ],
1331 ],
1332 ],
1333 ],
1334 [
1335 [
99140301 1336 'mode' => 'aggressive',
e07cf53e 1337 'contacts' => [
1338 [
99140301 1339 'first_name' => 'Michael',
1340 'last_name' => 'Jackson',
1341 'email' => 'michael@neverland.com',
1342 'contact_type' => 'Individual',
1343 'contact_sub_type' => 'Student',
e07cf53e 1344 'api.Address.create' => [
99140301 1345 'street_address' => 'big house',
1346 'location_type_id' => 'Home',
e07cf53e 1347 ],
1348 ],
1349 [
99140301 1350 'first_name' => 'Michael',
1351 'last_name' => 'Jackson',
1352 'email' => 'michael@neverland.com',
1353 'contact_type' => 'Individual',
1354 'contact_sub_type' => 'Student',
e07cf53e 1355 'api.Address.create' => [
99140301 1356 'street_address' => 'bigger house',
1357 'location_type_id' => 'Home',
e07cf53e 1358 ],
1359 ],
1360 ],
99140301 1361 'skipped' => 0,
1362 'merged' => 1,
e07cf53e 1363 'expected' => [
1364 [
99140301 1365 'first_name' => 'Michael',
1366 'last_name' => 'Jackson',
1367 'email' => 'michael@neverland.com',
1368 'contact_type' => 'Individual',
1369 'street_address' => 'big house',
e07cf53e 1370 ],
1371 ],
1372 ],
1373 ],
1374 [
1375 [
962f4484 1376 'mode' => 'safe',
e07cf53e 1377 'contacts' => [
1378 [
962f4484 1379 'first_name' => 'Michael',
1380 'last_name' => 'Jackson',
1381 'email' => 'michael@neverland.com',
1382 'contact_type' => 'Individual',
1383 'contact_sub_type' => 'Student',
e07cf53e 1384 'api.Address.create' => [
962f4484 1385 'street_address' => 'big house',
1386 'location_type_id' => 'Home',
e07cf53e 1387 ],
1388 ],
1389 [
962f4484 1390 'first_name' => 'Michael',
1391 'last_name' => 'Jackson',
1392 'email' => 'michael@neverland.com',
1393 'contact_type' => 'Individual',
1394 'contact_sub_type' => 'Student',
1395 'is_deceased' => 1,
e07cf53e 1396 ],
1397 ],
962f4484 1398 'skipped' => 1,
1399 'merged' => 0,
e07cf53e 1400 'expected' => [
1401 [
962f4484 1402 'first_name' => 'Michael',
1403 'last_name' => 'Jackson',
1404 'email' => 'michael@neverland.com',
1405 'contact_type' => 'Individual',
1406 'is_deceased' => 0,
e07cf53e 1407 ],
1408 [
962f4484 1409 'first_name' => 'Michael',
1410 'last_name' => 'Jackson',
1411 'email' => 'michael@neverland.com',
1412 'contact_type' => 'Individual',
1413 'is_deceased' => 1,
e07cf53e 1414 ],
1415 ],
1416 ],
1417 ],
1418 [
1419 [
962f4484 1420 'mode' => 'safe',
e07cf53e 1421 'contacts' => [
1422 [
962f4484 1423 'first_name' => 'Michael',
1424 'last_name' => 'Jackson',
1425 'email' => 'michael@neverland.com',
1426 'contact_type' => 'Individual',
1427 'contact_sub_type' => 'Student',
e07cf53e 1428 'api.Address.create' => [
962f4484 1429 'street_address' => 'big house',
1430 'location_type_id' => 'Home',
e07cf53e 1431 ],
962f4484 1432 'is_deceased' => 1,
e07cf53e 1433 ],
1434 [
962f4484 1435 'first_name' => 'Michael',
1436 'last_name' => 'Jackson',
1437 'email' => 'michael@neverland.com',
1438 'contact_type' => 'Individual',
1439 'contact_sub_type' => 'Student',
e07cf53e 1440 ],
1441 ],
962f4484 1442 'skipped' => 1,
1443 'merged' => 0,
e07cf53e 1444 'expected' => [
1445 [
962f4484 1446 'first_name' => 'Michael',
1447 'last_name' => 'Jackson',
1448 'email' => 'michael@neverland.com',
1449 'contact_type' => 'Individual',
1450 'is_deceased' => 1,
e07cf53e 1451 ],
1452 [
962f4484 1453 'first_name' => 'Michael',
1454 'last_name' => 'Jackson',
1455 'email' => 'michael@neverland.com',
1456 'contact_type' => 'Individual',
1457 'is_deceased' => 0,
e07cf53e 1458 ],
1459 ],
1460 ],
1461 ],
1462 ];
5d8b37be 1463
e07cf53e 1464 $conflictPairs = [
5d8b37be 1465 'first_name' => 'Dianna',
1466 'last_name' => 'McAndrew',
1467 'middle_name' => 'Prancer',
1468 'birth_date' => '2015-12-25',
1469 'gender_id' => 'Female',
1470 'job_title' => 'Thriller',
e07cf53e 1471 ];
5d8b37be 1472
1473 foreach ($conflictPairs as $key => $value) {
e07cf53e 1474 $contactParams = [
5d8b37be 1475 'first_name' => 'Michael',
1476 'middle_name' => 'Dancer',
1477 'last_name' => 'Jackson',
1478 'birth_date' => '2015-02-25',
1479 'email' => 'michael@neverland.com',
1480 'contact_type' => 'Individual',
e07cf53e 1481 'contact_sub_type' => ['Student'],
5d8b37be 1482 'gender_id' => 'Male',
1483 'job_title' => 'Entertainer',
e07cf53e 1484 ];
5d8b37be 1485 $contact2 = $contactParams;
1486
1487 $contact2[$key] = $value;
e07cf53e 1488 $data[$key . '_conflict'] = [
1489 [
5d8b37be 1490 'mode' => 'safe',
e07cf53e 1491 'contacts' => [$contactParams, $contact2],
5d8b37be 1492 'skipped' => 1,
1493 'merged' => 0,
e07cf53e 1494 'expected' => [$contactParams, $contact2],
1495 ],
1496 ];
5d8b37be 1497 }
1498
a45614cc 1499 return $data;
1500 }
1501
4cbe18b8 1502 /**
2f0b4fc5 1503 * Implements pre hook on relationships.
1504 *
1505 * @param string $op
100fef9d
CW
1506 * @param string $objectName
1507 * @param int $id
c490a46a 1508 * @param array $params
4cbe18b8 1509 */
6c6e6187 1510 public function hookPreRelationship($op, $objectName, $id, &$params) {
e07cf53e 1511 if ($op === 'delete') {
49f8272d
E
1512 return;
1513 }
5896d037 1514 if ($params['is_active']) {
49f8272d
E
1515 $params['description'] = 'Hooked';
1516 }
1517 else {
1518 $params['description'] = 'Go Go you good thing';
1519 }
1520 }
96025800 1521
13cc4cac 1522 /**
1523 * Get the location data set.
1524 *
1525 * @param array $locationParams1
1526 * @param array $locationParams2
1527 * @param string $entity
39b959db 1528 * @param array $additionalExpected
e07cf53e 1529 *
13cc4cac 1530 * @return array
1531 */
e07cf53e 1532 public function getMergeLocations($locationParams1, $locationParams2, $entity, $additionalExpected = []) {
2f0b4fc5 1533 return [
e07cf53e 1534 [
1535 'matching_primary' => [
13cc4cac 1536 'entity' => $entity,
e07cf53e 1537 'contact_1' => [
1538 array_merge([
5d5b49ad 1539 'location_type_id' => 'Main',
13cc4cac 1540 'is_primary' => 1,
e07cf53e 1541 ], $locationParams1),
1542 array_merge([
13cc4cac 1543 'location_type_id' => 'Work',
1544 'is_primary' => 0,
e07cf53e 1545 ], $locationParams2),
1546 ],
1547 'contact_2' => [
1548 array_merge([
5d5b49ad 1549 'location_type_id' => 'Main',
13cc4cac 1550 'is_primary' => 1,
e07cf53e 1551 ], $locationParams1),
1552 ],
1553 'expected' => array_merge($additionalExpected, [
1554 array_merge([
5d5b49ad 1555 'location_type_id' => 'Main',
13cc4cac 1556 'is_primary' => 1,
e07cf53e 1557 ], $locationParams1),
1558 array_merge([
13cc4cac 1559 'location_type_id' => 'Work',
1560 'is_primary' => 0,
e07cf53e 1561 ], $locationParams2),
1562 ]),
1563 'expected_hook' => array_merge($additionalExpected, [
1564 array_merge([
5d5b49ad 1565 'location_type_id' => 'Main',
13cc4cac 1566 'is_primary' => 1,
e07cf53e 1567 ], $locationParams1),
1568 array_merge([
13cc4cac 1569 'location_type_id' => 'Work',
1570 'is_primary' => 0,
e07cf53e 1571 ], $locationParams2),
1572 ]),
1573 ],
1574 ],
1575 [
1576 'matching_primary_reverse' => [
13cc4cac 1577 'entity' => $entity,
e07cf53e 1578 'contact_1' => [
1579 array_merge([
5d5b49ad 1580 'location_type_id' => 'Main',
13cc4cac 1581 'is_primary' => 1,
e07cf53e 1582 ], $locationParams1),
1583 ],
1584 'contact_2' => [
1585 array_merge([
5d5b49ad 1586 'location_type_id' => 'Main',
13cc4cac 1587 'is_primary' => 1,
e07cf53e 1588 ], $locationParams1),
1589 array_merge([
13cc4cac 1590 'location_type_id' => 'Work',
1591 'is_primary' => 0,
e07cf53e 1592 ], $locationParams2),
1593 ],
1594 'expected' => array_merge($additionalExpected, [
1595 array_merge([
5d5b49ad 1596 'location_type_id' => 'Main',
13cc4cac 1597 'is_primary' => 1,
e07cf53e 1598 ], $locationParams1),
1599 array_merge([
13cc4cac 1600 'location_type_id' => 'Work',
1601 'is_primary' => 0,
e07cf53e 1602 ], $locationParams2),
1603 ]),
1604 'expected_hook' => array_merge($additionalExpected, [
1605 array_merge([
5d5b49ad 1606 'location_type_id' => 'Main',
13cc4cac 1607 'is_primary' => 1,
e07cf53e 1608 ], $locationParams1),
1609 array_merge([
13cc4cac 1610 'location_type_id' => 'Work',
1611 'is_primary' => 0,
e07cf53e 1612 ], $locationParams2),
1613 ]),
1614 ],
1615 ],
1616 [
1617 'only_one_has_address' => [
13cc4cac 1618 'entity' => $entity,
e07cf53e 1619 'contact_1' => [
1620 array_merge([
5d5b49ad 1621 'location_type_id' => 'Main',
13cc4cac 1622 'is_primary' => 1,
e07cf53e 1623 ], $locationParams1),
1624 array_merge([
13cc4cac 1625 'location_type_id' => 'Work',
1626 'is_primary' => 0,
e07cf53e 1627 ], $locationParams2),
1628 ],
1629 'contact_2' => [],
1630 'expected' => array_merge($additionalExpected, [
1631 array_merge([
5d5b49ad 1632 'location_type_id' => 'Main',
13cc4cac 1633 'is_primary' => 1,
e07cf53e 1634 ], $locationParams1),
1635 array_merge([
13cc4cac 1636 'location_type_id' => 'Work',
1637 'is_primary' => 0,
e07cf53e 1638 ], $locationParams2),
1639 ]),
1640 'expected_hook' => array_merge($additionalExpected, [
1641 array_merge([
5d5b49ad 1642 'location_type_id' => 'Main',
1643 // When dealing with email we don't have a clean slate - the existing
1644 // primary will be primary.
1645 'is_primary' => ($entity == 'Email' ? 0 : 1),
e07cf53e 1646 ], $locationParams1),
1647 array_merge([
13cc4cac 1648 'location_type_id' => 'Work',
1649 'is_primary' => 0,
e07cf53e 1650 ], $locationParams2),
1651 ]),
1652 ],
1653 ],
1654 [
1655 'only_one_has_address_reverse' => [
5d5b49ad 1656 'description' => 'The destination contact does not have an address. secondary contact should be merged in.',
13cc4cac 1657 'entity' => $entity,
e07cf53e 1658 'contact_1' => [],
1659 'contact_2' => [
1660 array_merge([
5d5b49ad 1661 'location_type_id' => 'Main',
13cc4cac 1662 'is_primary' => 1,
e07cf53e 1663 ], $locationParams1),
1664 array_merge([
13cc4cac 1665 'location_type_id' => 'Work',
1666 'is_primary' => 0,
e07cf53e 1667 ], $locationParams2),
1668 ],
1669 'expected' => array_merge($additionalExpected, [
1670 array_merge([
5d5b49ad 1671 'location_type_id' => 'Main',
1672 // When dealing with email we don't have a clean slate - the existing
1673 // primary will be primary.
1674 'is_primary' => ($entity == 'Email' ? 0 : 1),
e07cf53e 1675 ], $locationParams1),
1676 array_merge([
13cc4cac 1677 'location_type_id' => 'Work',
1678 'is_primary' => 0,
e07cf53e 1679 ], $locationParams2),
1680 ]),
1681 'expected_hook' => array_merge($additionalExpected, [
1682 array_merge([
5d5b49ad 1683 'location_type_id' => 'Main',
13cc4cac 1684 'is_primary' => 1,
e07cf53e 1685 ], $locationParams1),
1686 array_merge([
13cc4cac 1687 'location_type_id' => 'Work',
1688 'is_primary' => 0,
e07cf53e 1689 ], $locationParams2),
1690 ]),
1691 ],
1692 ],
1693 [
1694 'different_primaries_with_different_location_type' => [
5d5b49ad 1695 'description' => 'Primaries are different with different location. Keep both addresses. Set primary to be that of lower id',
13cc4cac 1696 'entity' => $entity,
e07cf53e 1697 'contact_1' => [
1698 array_merge([
5d5b49ad 1699 'location_type_id' => 'Main',
13cc4cac 1700 'is_primary' => 1,
e07cf53e 1701 ], $locationParams1),
1702 ],
1703 'contact_2' => [
1704 array_merge([
13cc4cac 1705 'location_type_id' => 'Work',
1706 'is_primary' => 1,
e07cf53e 1707 ], $locationParams2),
1708 ],
1709 'expected' => array_merge($additionalExpected, [
1710 array_merge([
5d5b49ad 1711 'location_type_id' => 'Main',
13cc4cac 1712 'is_primary' => 1,
e07cf53e 1713 ], $locationParams1),
1714 array_merge([
13cc4cac 1715 'location_type_id' => 'Work',
1716 'is_primary' => 0,
e07cf53e 1717 ], $locationParams2),
1718 ]),
1719 'expected_hook' => array_merge($additionalExpected, [
1720 array_merge([
5d5b49ad 1721 'location_type_id' => 'Main',
13cc4cac 1722 'is_primary' => 0,
e07cf53e 1723 ], $locationParams1),
1724 array_merge([
13cc4cac 1725 'location_type_id' => 'Work',
1726 'is_primary' => 1,
e07cf53e 1727 ], $locationParams2),
1728 ]),
1729 ],
1730 ],
1731 [
1732 'different_primaries_with_different_location_type_reverse' => [
13cc4cac 1733 'entity' => $entity,
e07cf53e 1734 'contact_1' => [
1735 array_merge([
13cc4cac 1736 'location_type_id' => 'Work',
1737 'is_primary' => 1,
e07cf53e 1738 ], $locationParams2),
1739 ],
1740 'contact_2' => [
1741 array_merge([
5d5b49ad 1742 'location_type_id' => 'Main',
13cc4cac 1743 'is_primary' => 1,
e07cf53e 1744 ], $locationParams1),
1745 ],
1746 'expected' => array_merge($additionalExpected, [
1747 array_merge([
13cc4cac 1748 'location_type_id' => 'Work',
1749 'is_primary' => 1,
e07cf53e 1750 ], $locationParams2),
1751 array_merge([
5d5b49ad 1752 'location_type_id' => 'Main',
13cc4cac 1753 'is_primary' => 0,
e07cf53e 1754 ], $locationParams1),
1755 ]),
1756 'expected_hook' => array_merge($additionalExpected, [
1757 array_merge([
13cc4cac 1758 'location_type_id' => 'Work',
1759 'is_primary' => 0,
e07cf53e 1760 ], $locationParams2),
1761 array_merge([
5d5b49ad 1762 'location_type_id' => 'Main',
13cc4cac 1763 'is_primary' => 1,
e07cf53e 1764 ], $locationParams1),
1765 ]),
1766 ],
1767 ],
1768 [
1769 'different_primaries_location_match_only_one_address' => [
13cc4cac 1770 'entity' => $entity,
e07cf53e 1771 'contact_1' => [
1772 array_merge([
5d5b49ad 1773 'location_type_id' => 'Main',
13cc4cac 1774 'is_primary' => 1,
e07cf53e 1775 ], $locationParams1),
1776 array_merge([
13cc4cac 1777 'location_type_id' => 'Work',
1778 'is_primary' => 0,
e07cf53e 1779 ], $locationParams2),
1780 ],
1781 'contact_2' => [
1782 array_merge([
13cc4cac 1783 'location_type_id' => 'Work',
1784 'is_primary' => 1,
e07cf53e 1785 ], $locationParams2),
13cc4cac 1786
e07cf53e 1787 ],
1788 'expected' => array_merge($additionalExpected, [
1789 array_merge([
5d5b49ad 1790 'location_type_id' => 'Main',
13cc4cac 1791 'is_primary' => 1,
e07cf53e 1792 ], $locationParams1),
1793 array_merge([
13cc4cac 1794 'location_type_id' => 'Work',
1795 'is_primary' => 0,
e07cf53e 1796 ], $locationParams2),
1797 ]),
1798 'expected_hook' => array_merge($additionalExpected, [
1799 array_merge([
5d5b49ad 1800 'location_type_id' => 'Main',
13cc4cac 1801 'is_primary' => 0,
e07cf53e 1802 ], $locationParams1),
1803 array_merge([
13cc4cac 1804 'location_type_id' => 'Work',
1805 'is_primary' => 1,
e07cf53e 1806 ], $locationParams2),
1807 ]),
1808 ],
1809 ],
1810 [
1811 'different_primaries_location_match_only_one_address_reverse' => [
13cc4cac 1812 'entity' => $entity,
e07cf53e 1813 'contact_1' => [
1814 array_merge([
13cc4cac 1815 'location_type_id' => 'Work',
1816 'is_primary' => 1,
e07cf53e 1817 ], $locationParams2),
1818 ],
1819 'contact_2' => [
1820 array_merge([
5d5b49ad 1821 'location_type_id' => 'Main',
13cc4cac 1822 'is_primary' => 1,
e07cf53e 1823 ], $locationParams1),
1824 array_merge([
13cc4cac 1825 'location_type_id' => 'Work',
1826 'is_primary' => 0,
e07cf53e 1827 ], $locationParams2),
1828 ],
1829 'expected' => array_merge($additionalExpected, [
1830 array_merge([
13cc4cac 1831 'location_type_id' => 'Work',
1832 'is_primary' => 1,
e07cf53e 1833 ], $locationParams2),
1834 array_merge([
5d5b49ad 1835 'location_type_id' => 'Main',
13cc4cac 1836 'is_primary' => 0,
e07cf53e 1837 ], $locationParams1),
1838 ]),
1839 'expected_hook' => array_merge($additionalExpected, [
1840 array_merge([
13cc4cac 1841 'location_type_id' => 'Work',
1842 'is_primary' => 0,
e07cf53e 1843 ], $locationParams2),
1844 array_merge([
5d5b49ad 1845 'location_type_id' => 'Main',
13cc4cac 1846 'is_primary' => 1,
e07cf53e 1847 ], $locationParams1),
1848 ]),
1849 ],
1850 ],
1851 [
1852 'same_primaries_different_location' => [
13cc4cac 1853 'entity' => $entity,
e07cf53e 1854 'contact_1' => [
1855 array_merge([
5d5b49ad 1856 'location_type_id' => 'Main',
13cc4cac 1857 'is_primary' => 1,
e07cf53e 1858 ], $locationParams1),
1859 ],
1860 'contact_2' => [
1861 array_merge([
13cc4cac 1862 'location_type_id' => 'Work',
1863 'is_primary' => 1,
e07cf53e 1864 ], $locationParams1),
13cc4cac 1865
e07cf53e 1866 ],
1867 'expected' => array_merge($additionalExpected, [
1868 array_merge([
5d5b49ad 1869 'location_type_id' => 'Main',
13cc4cac 1870 'is_primary' => 1,
e07cf53e 1871 ], $locationParams1),
1872 array_merge([
13cc4cac 1873 'location_type_id' => 'Work',
1874 'is_primary' => 0,
e07cf53e 1875 ], $locationParams1),
1876 ]),
1877 'expected_hook' => array_merge($additionalExpected, [
1878 array_merge([
13cc4cac 1879 'location_type_id' => 'Work',
1880 'is_primary' => 1,
e07cf53e 1881 ], $locationParams1),
1882 ]),
1883 ],
1884 ],
1885 [
1886 'same_primaries_different_location_reverse' => [
13cc4cac 1887 'entity' => $entity,
e07cf53e 1888 'contact_1' => [
1889 array_merge([
13cc4cac 1890 'location_type_id' => 'Work',
1891 'is_primary' => 1,
e07cf53e 1892 ], $locationParams1),
1893 ],
1894 'contact_2' => [
1895 array_merge([
5d5b49ad 1896 'location_type_id' => 'Main',
13cc4cac 1897 'is_primary' => 1,
e07cf53e 1898 ], $locationParams1),
1899 ],
1900 'expected' => array_merge($additionalExpected, [
1901 array_merge([
13cc4cac 1902 'location_type_id' => 'Work',
1903 'is_primary' => 1,
e07cf53e 1904 ], $locationParams1),
1905 array_merge([
5d5b49ad 1906 'location_type_id' => 'Main',
13cc4cac 1907 'is_primary' => 0,
e07cf53e 1908 ], $locationParams1),
1909 ]),
1910 'expected_hook' => array_merge($additionalExpected, [
1911 array_merge([
5d5b49ad 1912 'location_type_id' => 'Main',
13cc4cac 1913 'is_primary' => 1,
e07cf53e 1914 ], $locationParams1),
1915 ]),
1916 ],
1917 ],
1918 ];
13cc4cac 1919 }
1920
a392809d
JV
1921 /**
1922 * Test processing membership for deceased contacts.
2f0b4fc5 1923 *
1924 * @throws \CRM_Core_Exception
a392809d 1925 */
6d3742b9 1926 public function testProcessMembershipDeceased() {
a392809d
JV
1927 $this->callAPISuccess('Job', 'process_membership', []);
1928 $deadManWalkingID = $this->individualCreate();
e07cf53e 1929 $membershipID = $this->contactMembershipCreate(['contact_id' => $deadManWalkingID]);
a392809d
JV
1930 $this->callAPISuccess('Contact', 'create', ['id' => $deadManWalkingID, 'is_deceased' => 1]);
1931 $this->callAPISuccess('Job', 'process_membership', []);
1932 $membership = $this->callAPISuccessGetSingle('Membership', ['id' => $membershipID]);
1933 $deceasedStatusId = CRM_Core_PseudoConstant::getKey('CRM_Member_BAO_Membership', 'status_id', 'Deceased');
1934 $this->assertEquals($deceasedStatusId, $membership['status_id']);
1935 }
1936
1937 /**
1938 * Test we get an error is deceased status is disabled.
85458266 1939 *
1940 * @throws \CRM_Core_Exception
a392809d
JV
1941 */
1942 public function testProcessMembershipNoDeceasedStatus() {
1943 $deceasedStatusId = CRM_Core_PseudoConstant::getKey('CRM_Member_BAO_Membership', 'status_id', 'Deceased');
1944 $this->callAPISuccess('MembershipStatus', 'create', ['is_active' => 0, 'id' => $deceasedStatusId]);
1945 CRM_Core_PseudoConstant::flush();
1946
1947 $deadManWalkingID = $this->individualCreate();
e07cf53e 1948 $this->contactMembershipCreate(['contact_id' => $deadManWalkingID]);
a392809d
JV
1949 $this->callAPISuccess('Contact', 'create', ['id' => $deadManWalkingID, 'is_deceased' => 1]);
1950 $this->callAPIFailure('Job', 'process_membership', []);
1951
1952 $this->callAPISuccess('MembershipStatus', 'create', ['is_active' => 1, 'id' => $deceasedStatusId]);
1953 }
1954
6d3742b9
DJ
1955 /**
1956 * Test processing membership: check that status is updated when it should be
1957 * and left alone when it shouldn't.
85458266 1958 *
1959 * @throws \CRM_Core_Exception
1960 * @throws \CiviCRM_API3_Exception
6d3742b9
DJ
1961 */
1962 public function testProcessMembershipUpdateStatus() {
85458266 1963 $this->ids['MembershipType'] = $this->membershipTypeCreate();
6d3742b9
DJ
1964
1965 // Create admin-only membership status and get all statuses.
2f0b4fc5 1966 $this->callAPISuccess('membership_status', 'create', ['name' => 'Admin', 'is_admin' => 1])['id'];
6d3742b9 1967
85458266 1968 // Create membership with incorrect statuses for the given dates and also some (pending, cancelled, admin override) which should not be updated.
1969 $memberships = [
1970 [
1971 'start_date' => 'now',
1972 'end_date' => '+ 1 year',
1973 'initial_status' => 'Current',
1974 'expected_processed_status' => 'New',
1975 ],
1976 [
1977 'start_date' => '- 6 month',
1978 'end_date' => '+ 6 month',
1979 'initial_status' => 'New',
1980 'expected_processed_status' => 'Current',
1981 ],
1982 [
1983 'start_date' => '- 53 week',
1984 'end_date' => '-1 week',
1985 'initial_status' => 'Current',
1986 'expected_processed_status' => 'Grace',
1987 ],
1988 [
1989 'start_date' => '- 16 month',
1990 'end_date' => '- 4 month',
1991 'initial_status' => 'Grace',
1992 'expected_processed_status' => 'Expired',
1993 ],
1994 [
1995 'start_date' => 'now',
1996 'end_date' => '+ 1 year',
1997 'initial_status' => 'Pending',
1998 'expected_processed_status' => 'Pending',
1999 ],
2000 [
2001 'start_date' => '- 6 month',
2002 'end_date' => '+ 6 month',
2003 'initial_status' => 'Cancelled',
2004 'expected_processed_status' => 'Cancelled',
2005 ],
2006 [
2007 'start_date' => '- 16 month',
2008 'end_date' => '- 4 month',
2009 'initial_status' => 'Current',
2010 'is_override' => TRUE,
2011 'expected_processed_status' => 'Current',
2012 ],
2013 [
2014 // @todo this looks like it's covering something up. If we pass isAdminOverride it is the same as the line above. Without it the test fails.
2015 // this smells of something that should work (or someone thought should work & so put in a test) doesn't & test has been adjusted to cope.
2016 'start_date' => '- 16 month',
2017 'end_date' => '- 4 month',
2018 'initial_status' => 'Admin',
2019 'is_override' => TRUE,
2020 'expected_processed_status' => 'Admin',
2021 ],
6d3742b9 2022 ];
85458266 2023 foreach ($memberships as $index => $membership) {
2024 $memberships[$index]['id'] = $this->createMembershipNeedingStatusProcessing($membership['start_date'], $membership['end_date'], $membership['initial_status'], $membership['is_override'] ?? FALSE);
2025 }
6d3742b9 2026
0c6bcd7e
DJ
2027 /*
2028 * Create membership type with inheritence and check processing of secondary memberships.
2029 */
2030 $employerRelationshipId = $this->callAPISuccessGetValue('RelationshipType', [
85458266 2031 'return' => 'id',
2032 'name_b_a' => 'Employer Of',
0c6bcd7e
DJ
2033 ]);
2034 // Create membership type: inherited through employment.
2035 $membershipOrgId = $this->organizationCreate();
2036 $params = [
2037 'name' => 'Corporate Membership',
2038 'duration_unit' => 'year',
2039 'duration_interval' => 1,
2040 'period_type' => 'rolling',
2041 'member_of_contact_id' => $membershipOrgId,
2042 'domain_id' => 1,
2043 'financial_type_id' => 1,
2044 'relationship_type_id' => $employerRelationshipId,
2045 'relationship_direction' => 'b_a',
2046 'is_active' => 1,
2047 ];
85458266 2048 $membershipTypeId = $this->callAPISuccess('membership_type', 'create', $params)['id'];
0c6bcd7e
DJ
2049
2050 // Create employer and first employee
2051 $employerId = $this->organizationCreate([], 1);
2052 $memberContactId = $this->individualCreate(['employer_id' => $employerId], 0);
2053
2054 // Create inherited membership with incorrect status but dates implying status Expired.
2055 $params = [
2056 'contact_id' => $employerId,
2057 'membership_type_id' => $membershipTypeId,
2058 'source' => 'Test suite',
2059 'join_date' => date('Y-m-d', strtotime('now - 16 month')),
2060 'start_date' => date('Y-m-d', strtotime('now - 16 month')),
2061 'end_date' => date('Y-m-d', strtotime('now - 4 month')),
39b959db
SL
2062 // Intentionally incorrect status.
2063 'status_id' => 'Grace',
2064 // Don't calculate status.
2065 'skipStatusCal' => 1,
0c6bcd7e
DJ
2066 ];
2067 $organizationMembershipID = $this->contactMembershipCreate($params);
2068
2069 // Check that the employee inherited the membership and status.
85458266 2070 $expiredInheritedRelationship = $this->callAPISuccessGetSingle('membership', [
0c6bcd7e
DJ
2071 'contact_id' => $memberContactId,
2072 'membership_type_id' => $membershipTypeId,
85458266 2073 ]);
2074 $this->assertEquals($organizationMembershipID, $expiredInheritedRelationship['owner_membership_id']);
2075 $this->assertMembershipStatus('Grace', (int) $expiredInheritedRelationship['status_id']);
0c6bcd7e
DJ
2076
2077 // Reset static $relatedContactIds array in createRelatedMemberships(),
2078 // to avoid bug where inherited membership gets deleted.
2079 $var = TRUE;
2080 CRM_Member_BAO_Membership::createRelatedMemberships($var, $var, TRUE);
6d3742b9
DJ
2081 // Check that after running process_membership job, statuses are correct.
2082 $this->callAPISuccess('Job', 'process_membership', []);
2083
85458266 2084 foreach ($memberships as $expectation) {
2085 $membership = $this->callAPISuccessGetSingle('membership', ['id' => $expectation['id']]);
2086 $this->assertMembershipStatus($expectation['expected_processed_status'], (int) $membership['status_id']);
2087 }
0c6bcd7e
DJ
2088
2089 // Inherit Expired - should get updated.
85458266 2090 $membership = $this->callAPISuccess('membership', 'getsingle', ['id' => $expiredInheritedRelationship['id']]);
2091 $this->assertMembershipStatus('Expired', $membership['status_id']);
6d3742b9
DJ
2092 }
2093
57710579
SL
2094 /**
2095 * Test procesing membership where is_override is set to 0 rather than NULL
e07cf53e 2096 *
2097 * @throws \CRM_Core_Exception
57710579
SL
2098 */
2099 public function testProcessMembershipIsOverrideNotNullNot1either() {
2100 $membershipTypeId = $this->membershipTypeCreate();
2101
2102 // Create admin-only membership status and get all statuses.
2103 $result = $this->callAPISuccess('membership_status', 'create', ['name' => 'Admin', 'is_admin' => 1, 'sequential' => 1]);
2104 $membershipStatusIdAdmin = $result['values'][0]['id'];
2105 $memStatus = CRM_Member_PseudoConstant::membershipStatus();
2106
2107 // Default params, which we'll expand on below.
2108 $params = [
2109 'membership_type_id' => $membershipTypeId,
2110 // Don't calculate status.
2111 'skipStatusCal' => 1,
2112 'source' => 'Test',
2113 'sequential' => 1,
2114 ];
2115
2116 // Create membership with incorrect status but dates implying status Current.
2117 $params['contact_id'] = $this->individualCreate();
2118 $params['join_date'] = date('Y-m-d', strtotime('now - 6 month'));
2119 $params['start_date'] = date('Y-m-d', strtotime('now - 6 month'));
2120 $params['end_date'] = date('Y-m-d', strtotime('now + 6 month'));
2121 // Intentionally incorrect status.
2122 $params['status_id'] = 'New';
2123 $resultCurrent = $this->callAPISuccess('Membership', 'create', $params);
2124 // Ensure that is_override is set to 0 by doing through DB given API not seem to accept id
2125 CRM_Core_DAO::executeQuery("Update civicrm_membership SET is_override = 0 WHERE id = %1", [1 => [$resultCurrent['id'], 'Positive']]);
2f0b4fc5 2126 $this->assertEquals(array_search('New', $memStatus, TRUE), $resultCurrent['values'][0]['status_id']);
57710579
SL
2127 $jobResult = $this->callAPISuccess('Job', 'process_membership', []);
2128 $this->assertEquals('Processed 1 membership records. Updated 1 records.', $jobResult['values']);
2f0b4fc5 2129 $this->assertEquals(array_search('Current', $memStatus, TRUE), $this->callAPISuccess('Membership', 'get', ['id' => $resultCurrent['id']])['values'][$resultCurrent['id']]['status_id']);
57710579
SL
2130 }
2131
85458266 2132 /**
2133 * @param string $expectedStatusName
2134 * @param int $actualStatusID
2135 */
2136 protected function assertMembershipStatus(string $expectedStatusName, int $actualStatusID) {
2137 $this->assertEquals($expectedStatusName, CRM_Core_PseudoConstant::getName('CRM_Member_BAO_Membership', 'status_id', $actualStatusID));
2138 }
2139
2140 /**
2141 * @param string $startDate
2142 * Date in strtotime format - e.g 'now', '+1 day'
2143 * @param string $endDate
2144 * Date in strtotime format - e.g 'now', '+1 day'
2145 * @param string $status
2146 * Status override
2147 * @param bool $isAdminOverride
2148 * Is administratively overridden (if so the status is fixed).
2149 *
2150 * @return int
2151 *
2152 * @throws \CRM_Core_Exception
2153 */
2154 protected function createMembershipNeedingStatusProcessing(string $startDate, string $endDate, string $status, bool $isAdminOverride = FALSE): int {
2155 $params = [
2156 'membership_type_id' => $this->ids['MembershipType'],
2157 // Don't calculate status.
2158 'skipStatusCal' => 1,
2159 'source' => 'Test',
2160 'sequential' => 1,
2161 ];
2162 $params['contact_id'] = $this->individualCreate();
2163 $params['join_date'] = date('Y-m-d', strtotime($startDate));
2164 $params['start_date'] = date('Y-m-d', strtotime($startDate));
2165 $params['end_date'] = date('Y-m-d', strtotime($endDate));
2166 $params['sequential'] = TRUE;
2167 $params['is_override'] = $isAdminOverride;
2168 // Intentionally incorrect status.
2169 $params['status_id'] = $status;
2170 $resultNew = $this->callAPISuccess('Membership', 'create', $params);
2171 $this->assertMembershipStatus($status, (int) $resultNew['values'][0]['status_id']);
2172 return (int) $resultNew['id'];
2173 }
2174
2f0b4fc5 2175 /**
2176 * Shared set up for SMS reminder tests.
2177 *
2178 * @return array
2179 *
2180 * @throws \CRM_Core_Exception
2181 * @throws \CiviCRM_API3_Exception
2182 */
2183 protected function setUpMembershipSMSReminders(): array {
2184 $membershipTypeID = $this->membershipTypeCreate();
2185 $this->membershipStatusCreate();
2186 $createTotal = 3;
2187 $groupID = $this->groupCreate(['name' => 'Texan drawlers', 'title' => 'a...']);
2188 for ($i = 1; $i <= $createTotal; $i++) {
2189 $contactID = $this->individualCreate();
2190 $this->callAPISuccess('Phone', 'create', [
2191 'contact_id' => $contactID,
2192 'phone' => '555 123 1234',
2193 'phone_type_id' => 'Mobile',
2194 'location_type_id' => 'Billing',
2195 ]);
2196 if ($i === 2) {
2197 $theChosenOneID = $contactID;
2198 }
2199 if ($i < 3) {
2200 $this->callAPISuccess('group_contact', 'create', [
2201 'contact_id' => $contactID,
2202 'status' => 'Added',
2203 'group_id' => $groupID,
2204 ]);
2205 }
2206 if ($i > 1) {
2207 $this->callAPISuccess('membership', 'create', [
2208 'contact_id' => $contactID,
2209 'membership_type_id' => $membershipTypeID,
2210 'join_date' => 'now',
2211 'start_date' => '+ 1 day',
2212 ]);
2213 }
2214 }
2215 $this->setupForSmsTests();
2216 $provider = civicrm_api3('SmsProvider', 'create', [
2217 'name' => 'CiviTestSMSProvider',
2218 'api_type' => '1',
2219 'username' => '1',
2220 'password' => '1',
2221 'api_url' => '1',
2222 'api_params' => 'a=1',
2223 'is_default' => '1',
2224 'is_active' => '1',
2225 'domain_id' => '1',
2226 ]);
2227 return [$membershipTypeID, $groupID, $theChosenOneID, $provider];
2228 }
2229
6a488035 2230}