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