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