3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 |
9 +--------------------------------------------------------------------+
13 * File for the CiviCRM APIv3 job functions
15 * @package CiviCRM_APIv3
18 * @copyright CiviCRM LLC https://civicrm.org/licensing
22 * Class api_v3_JobTest
26 class api_v3_JobTest
extends CiviUnitTestCase
{
28 public $DBResetRequired = FALSE;
30 public $_entity = 'Job';
33 * Created membership type.
35 * Must be created outside the transaction due to it breaking the transaction.
39 public $membershipTypeID;
44 public function setUp() {
46 $this->membershipTypeID
= $this->membershipTypeCreate(['name' => 'General']);
47 $this->useTransaction(TRUE);
50 'name' => 'API_Test_Job',
51 'description' => 'A long description written by hand in cursive',
52 'run_frequency' => 'Daily',
53 'api_entity' => 'ApiTestEntity',
54 'api_action' => 'apitestaction',
55 'parameters' => 'Semi-formal explanation of runtime job parameters',
63 * @throws \CRM_Core_Exception
65 public function tearDown() {
67 // The membershipType create breaks transactions so this extra cleanup is needed.
68 $this->membershipTypeDelete(['id' => $this->membershipTypeID
]);
69 $this->cleanUpSetUpIDs();
70 $this->quickCleanUpFinancialEntities();
71 $this->quickCleanup(['civicrm_contact', 'civicrm_address', 'civicrm_email', 'civicrm_website', 'civicrm_phone'], TRUE);
78 public function testCreateWithoutName() {
82 $this->callAPIFailure('job', 'create', $params,
83 'Mandatory key(s) missing from params array: run_frequency, name, api_entity, api_action'
88 * Create job with an invalid "run_frequency" value.
90 public function testCreateWithInvalidFrequency() {
93 'name' => 'API_Test_Job',
94 'description' => 'A long description written by hand in cursive',
95 'run_frequency' => 'Fortnightly',
96 'api_entity' => 'ApiTestEntity',
97 'api_action' => 'apitestaction',
98 'parameters' => 'Semi-formal explanation of runtime job parameters',
101 $this->callAPIFailure('job', 'create', $params);
107 public function testCreate() {
108 $result = $this->callAPIAndDocument('job', 'create', $this->_params
, __FUNCTION__
, __FILE__
);
109 $this->assertNotNull($result['values'][0]['id']);
111 // mutate $params to match expected return value
112 unset($this->_params
['sequential']);
113 //assertDBState compares expected values in $result to actual values in the DB
114 $this->assertDBState('CRM_Core_DAO_Job', $result['id'], $this->_params
);
120 * @throws \CRM_Core_Exception
122 public function testClone() {
123 $createResult = $this->callAPISuccess('job', 'create', $this->_params
);
124 $params = ['id' => $createResult['id']];
125 $cloneResult = $this->callAPIAndDocument('job', 'clone', $params, __FUNCTION__
, __FILE__
);
126 $clonedJob = $cloneResult['values'][$cloneResult['id']];
127 $this->assertEquals($this->_params
['name'] . ' - Copy', $clonedJob['name']);
128 $this->assertEquals($this->_params
['description'], $clonedJob['description']);
129 $this->assertEquals($this->_params
['parameters'], $clonedJob['parameters']);
130 $this->assertEquals($this->_params
['is_active'], $clonedJob['is_active']);
131 $this->assertArrayNotHasKey('last_run', $clonedJob);
132 $this->assertArrayNotHasKey('scheduled_run_date', $clonedJob);
136 * Check if required fields are not passed.
138 public function testDeleteWithoutRequired() {
140 'name' => 'API_Test_PP',
141 'title' => 'API Test Payment Processor',
142 'class_name' => 'CRM_Core_Payment_APITest',
145 $result = $this->callAPIFailure('job', 'delete', $params);
146 $this->assertEquals($result['error_message'], 'Mandatory key(s) missing from params array: id');
150 * Check with incorrect required fields.
152 public function testDeleteWithIncorrectData() {
156 $this->callAPIFailure('job', 'delete', $params);
162 public function testDelete() {
163 $createResult = $this->callAPISuccess('job', 'create', $this->_params
);
164 $params = ['id' => $createResult['id']];
165 $this->callAPIAndDocument('job', 'delete', $params, __FUNCTION__
, __FILE__
);
166 $this->assertAPIDeleted($this->_entity
, $createResult['id']);
170 * Test greeting update job.
172 * Note that this test is about tesing the metadata / calling of the function & doesn't test the success of the called function
174 * @throws \CRM_Core_Exception
176 public function testCallUpdateGreetingSuccess() {
177 $this->callAPISuccess($this->_entity
, 'update_greeting', [
178 'gt' => 'postal_greeting',
179 'ct' => 'Individual',
184 * Test greeting update handles comma separated params.
186 * @throws \CRM_Core_Exception
188 public function testCallUpdateGreetingCommaSeparatedParamsSuccess() {
189 $gt = 'postal_greeting,email_greeting,addressee';
190 $ct = 'Individual,Household';
191 $this->callAPISuccess($this->_entity
, 'update_greeting', ['gt' => $gt, 'ct' => $ct]);
195 * Test the call reminder success sends more than 25 reminders & is not incorrectly limited.
197 * Note that this particular test sends the reminders to the additional recipients only
198 * as no real reminder person is configured
200 * Also note that this is testing a 'job' api so is in this class rather than scheduled_reminder - which
201 * seems a cleaner place to build up a collection of scheduled reminder testing functions. However, it seems
202 * that the api itself would need to be moved to the scheduled_reminder fn to do that with the job wrapper being respected for legacy functions
204 * @throws \CRM_Core_Exception
206 public function testCallSendReminderSuccessMoreThanDefaultLimit() {
207 $membershipTypeID = $this->membershipTypeCreate();
208 $this->membershipStatusCreate();
210 for ($i = 1; $i <= $createTotal; $i++
) {
211 $contactID = $this->individualCreate();
212 $groupID = $this->groupCreate(['name' => $i, 'title' => $i]);
213 $this->callAPISuccess('action_schedule', 'create', [
214 'title' => " job $i",
215 'subject' => "job $i",
216 'entity_value' => $membershipTypeID,
218 'start_action_date' => 'membership_join_date',
219 'start_action_offset' => 0,
220 'start_action_condition' => 'before',
221 'start_action_unit' => 'hour',
222 'group_id' => $groupID,
225 $this->callAPISuccess('group_contact', 'create', [
226 'contact_id' => $contactID,
228 'group_id' => $groupID,
231 $this->callAPISuccess('job', 'send_reminder', []);
232 $successfulCronCount = CRM_Core_DAO
::singleValueQuery("SELECT count(*) FROM civicrm_action_log");
233 $this->assertEquals($successfulCronCount, $createTotal);
237 * Test scheduled reminders respect limit to (since above identified addition_to handling issue).
239 * We create 3 contacts - 1 is in our group, 1 has our membership & the chosen one has both
240 * & check that only the chosen one got the reminder
242 * @throws \CRM_Core_Exception
243 * @throws \CiviCRM_API3_Exception
245 public function testCallSendReminderLimitToSMS() {
246 list($membershipTypeID, $groupID, $theChosenOneID, $provider) = $this->setUpMembershipSMSReminders();
247 $this->callAPISuccess('action_schedule', 'create', [
248 'title' => ' remind all Texans',
249 'subject' => 'drawling renewal',
250 'entity_value' => $membershipTypeID,
252 'start_action_date' => 'membership_start_date',
253 'start_action_offset' => 1,
254 'start_action_condition' => 'before',
255 'start_action_unit' => 'day',
256 'group_id' => $groupID,
258 'sms_provider_id' => $provider['id'],
259 'mode' => 'User_Preference',
261 $this->callAPISuccess('job', 'send_reminder', []);
262 $successfulCronCount = CRM_Core_DAO
::singleValueQuery("SELECT count(*) FROM civicrm_action_log");
263 $this->assertEquals($successfulCronCount, 1);
264 $sentToID = CRM_Core_DAO
::singleValueQuery("SELECT contact_id FROM civicrm_action_log");
265 $this->assertEquals($sentToID, $theChosenOneID);
266 $this->assertEquals(0, CRM_Core_DAO
::singleValueQuery("SELECT is_error FROM civicrm_action_log"));
267 $this->setupForSmsTests(TRUE);
271 * Test disabling expired relationships.
273 * @throws \CRM_Core_Exception
275 public function testCallDisableExpiredRelationships() {
276 $individualID = $this->individualCreate();
277 $orgID = $this->organizationCreate();
278 CRM_Utils_Hook_UnitTests
::singleton()->setHook('civicrm_pre', [$this, 'hookPreRelationship']);
279 $relationshipTypeID = $this->callAPISuccess('relationship_type', 'getvalue', [
281 'name_a_b' => 'Employee of',
283 $result = $this->callAPISuccess('relationship', 'create', [
284 'relationship_type_id' => $relationshipTypeID,
285 'contact_id_a' => $individualID,
286 'contact_id_b' => $orgID,
288 'end_date' => 'yesterday',
290 $relationshipID = $result['id'];
291 $this->assertEquals('Hooked', $result['values'][$relationshipID]['description']);
292 $this->callAPISuccess($this->_entity
, 'disable_expired_relationships', []);
293 $result = $this->callAPISuccess('relationship', 'get', []);
294 $this->assertEquals('Go Go you good thing', $result['values'][$relationshipID]['description']);
295 $this->contactDelete($individualID);
296 $this->contactDelete($orgID);
300 * Event templates should not send reminders to additional contacts.
302 * @throws \CRM_Core_Exception
304 public function testTemplateRemindAddlContacts() {
305 $contactId = $this->individualCreate();
306 $groupId = $this->groupCreate(['name' => 'Additional Contacts', 'title' => 'Additional Contacts']);
307 $this->callAPISuccess('GroupContact', 'create', [
308 'contact_id' => $contactId,
309 'group_id' => $groupId,
311 $event = $this->eventCreate(['is_template' => 1, 'template_title' => "I'm a template", 'title' => NULL]);
312 $eventId = $event['id'];
314 $this->callAPISuccess('action_schedule', 'create', [
315 'title' => 'Do not send me',
316 'subject' => 'I am a reminder attached to a template.',
317 'entity_value' => $eventId,
319 'start_action_date' => 'start_date',
320 'start_action_offset' => 1,
321 'start_action_condition' => 'before',
322 'start_action_unit' => 'day',
323 'group_id' => $groupId,
328 $this->callAPISuccess('job', 'send_reminder', []);
329 $successfulCronCount = CRM_Core_DAO
::singleValueQuery('SELECT count(*) FROM civicrm_action_log');
330 $this->assertEquals(0, $successfulCronCount);
334 * Test scheduled reminders respect limit to (since above identified addition_to handling issue).
336 * We create 3 contacts - 1 is in our group, 1 has our membership & the chosen one has both
337 * & check that only the chosen one got the reminder
339 * Also check no hard fail on cron job with running a reminder that has a deleted SMS provider
341 * @throws \CRM_Core_Exception
342 * @throws \CiviCRM_API3_Exception
344 public function testCallSendReminderLimitToSMSWithDeletedProvider() {
345 list($membershipTypeID, $groupID, $theChosenOneID, $provider) = $this->setUpMembershipSMSReminders();
346 $this->callAPISuccess('action_schedule', 'create', [
347 'title' => ' remind all Texans',
348 'subject' => 'drawling renewal',
349 'entity_value' => $membershipTypeID,
351 'start_action_date' => 'membership_start_date',
352 'start_action_offset' => 1,
353 'start_action_condition' => 'before',
354 'start_action_unit' => 'day',
355 'group_id' => $groupID,
357 'sms_provider_id' => $provider['id'],
360 $this->callAPISuccess('SmsProvider', 'delete', ['id' => $provider['id']]);
361 $this->callAPISuccess('job', 'send_reminder', []);
362 $cronCount = CRM_Core_DAO
::singleValueQuery("SELECT count(*) FROM civicrm_action_log");
363 $this->assertEquals($cronCount, 1);
364 $sentToID = CRM_Core_DAO
::singleValueQuery("SELECT contact_id FROM civicrm_action_log");
365 $this->assertEquals($sentToID, $theChosenOneID);
366 $cronlog = CRM_Core_DAO
::executeQuery("SELECT * FROM civicrm_action_log")->fetchAll()[0];
367 $this->assertEquals(1, $cronlog['is_error']);
368 $this->assertEquals('SMS reminder cannot be sent because the SMS provider has been deleted.', $cronlog['message']);
369 $this->setupForSmsTests(TRUE);
373 * Test the batch merge function.
375 * We are just checking it returns without error here.
377 * @throws \CRM_Core_Exception
379 public function testBatchMerge() {
380 $this->callAPISuccess('Job', 'process_batch_merge', []);
384 * Test the batch merge function actually works!
386 * @dataProvider getMergeSets
390 * @throws \CRM_Core_Exception
392 public function testBatchMergeWorks($dataSet) {
393 foreach ($dataSet['contacts'] as $params) {
394 $this->callAPISuccess('Contact', 'create', $params);
397 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['mode' => $dataSet['mode']]);
398 $this->assertCount($dataSet['skipped'], $result['values']['skipped'], 'Failed to skip the right number:' . $dataSet['skipped']);
399 $this->assertCount($dataSet['merged'], $result['values']['merged']);
400 $result = $this->callAPISuccess('Contact', 'get', [
401 'contact_sub_type' => 'Student',
403 'is_deceased' => ['IN' => [0, 1]],
404 'options' => ['sort' => 'id ASC'],
406 $this->assertEquals(count($dataSet['expected']), $result['count']);
407 foreach ($dataSet['expected'] as $index => $contact) {
408 foreach ($contact as $key => $value) {
409 if ($key === 'gender_id') {
412 $this->assertEquals($value, $result['values'][$index][$key]);
418 * Check that the merge carries across various related entities.
420 * Note the group combinations & expected results:
422 * @throws \CRM_Core_Exception
424 public function testBatchMergeWithAssets() {
425 $contactID = $this->individualCreate();
426 $contact2ID = $this->individualCreate();
427 $this->contributionCreate(['contact_id' => $contactID]);
428 $this->contributionCreate(['contact_id' => $contact2ID, 'invoice_id' => '2', 'trxn_id' => 2]);
429 $this->contactMembershipCreate(['contact_id' => $contactID]);
430 $this->contactMembershipCreate(['contact_id' => $contact2ID]);
431 $this->activityCreate(['source_contact_id' => $contactID, 'target_contact_id' => $contactID, 'assignee_contact_id' => $contactID]);
432 $this->activityCreate(['source_contact_id' => $contact2ID, 'target_contact_id' => $contact2ID, 'assignee_contact_id' => $contact2ID]);
433 $this->tagCreate(['name' => 'Tall']);
434 $this->tagCreate(['name' => 'Short']);
435 $this->entityTagAdd(['contact_id' => $contactID, 'tag_id' => 'Tall']);
436 $this->entityTagAdd(['contact_id' => $contact2ID, 'tag_id' => 'Short']);
437 $this->entityTagAdd(['contact_id' => $contact2ID, 'tag_id' => 'Tall']);
438 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['mode' => 'safe']);
439 $this->assertCount(0, $result['values']['skipped']);
440 $this->assertCount(1, $result['values']['merged']);
441 $this->callAPISuccessGetCount('Contribution', ['contact_id' => $contactID], 2);
442 $this->callAPISuccessGetCount('Contribution', ['contact_id' => $contact2ID], 0);
443 $this->callAPISuccessGetCount('FinancialItem', ['contact_id' => $contactID], 2);
444 $this->callAPISuccessGetCount('FinancialItem', ['contact_id' => $contact2ID], 0);
445 $this->callAPISuccessGetCount('Membership', ['contact_id' => $contactID], 2);
446 $this->callAPISuccessGetCount('Membership', ['contact_id' => $contact2ID], 0);
447 $this->callAPISuccessGetCount('EntityTag', ['contact_id' => $contactID], 2);
448 $this->callAPISuccessGetCount('EntityTag', ['contact_id' => $contact2ID], 0);
449 // 14 activities is one for each contribution (2), two (source + target) for each membership (+(2x2) = 6)
450 // 3 for each of the added activities as there are 3 roles (+6 = 12
451 // 2 for the (source & target) contact merged activity (+2 = 14)
452 $this->callAPISuccessGetCount('ActivityContact', ['contact_id' => $contactID], 14);
453 // 2 for the connection to the deleted by merge activity (source & target)
454 $this->callAPISuccessGetCount('ActivityContact', ['contact_id' => $contact2ID], 2);
458 * Test that non-contact entity tags are untouched in merge.
460 * @throws \CRM_Core_Exception
462 public function testContributionEntityTag() {
463 $this->callAPISuccess('OptionValue', 'create', ['option_group_id' => 'tag_used_for', 'value' => 'civicrm_contribution', 'label' => 'Contribution']);
464 $tagID = $this->tagCreate(['name' => 'Big', 'used_for' => 'civicrm_contribution'])['id'];
465 $contact1 = (int) $this->individualCreate();
466 $contact2 = (int) $this->individualCreate();
467 $contributionID = NULL;
468 while ($contributionID !== $contact2) {
469 $contributionID = (int) $this->callAPISuccess('Contribution', 'create', ['contact_id' => $contact1, 'total_amount' => 5, 'financial_type_id' => 'Donation'])['id'];
471 $entityTagParams = ['entity_id' => $contributionID, 'entity_table' => 'civicrm_contribution', 'tag_id' => $tagID];
472 $this->callAPISuccess('EntityTag', 'create', $entityTagParams);
473 $this->callAPISuccessGetSingle('EntityTag', $entityTagParams);
474 $this->callAPISuccess('Job', 'process_batch_merge', ['mode' => 'safe']);
475 $this->callAPISuccessGetSingle('EntityTag', $entityTagParams);
479 * Check that the merge carries across various related entities.
481 * Note the group combinations 'expected' results:
483 * Group 0 Added null Added
484 * Group 1 Added Added Added
485 * Group 2 Added Removed **** Added
486 * Group 3 Removed null **** null
487 * Group 4 Removed Added **** Added
488 * Group 5 Removed Removed **** null
489 * Group 6 null Added Added
490 * Group 7 null Removed **** null
492 * The ones with **** are the ones where I think a case could be made to change the behaviour.
494 * @throws \CRM_Core_Exception
496 public function testBatchMergeMergesGroups() {
497 $contactID = $this->individualCreate();
498 $contact2ID = $this->individualCreate();
500 for ($i = 0; $i < 8; $i++
) {
501 $groups[] = $this->groupCreate([
502 'name' => 'mergeGroup' . $i,
503 'title' => 'merge group' . $i,
507 $this->callAPISuccess('GroupContact', 'create', [
508 'contact_id' => $contactID,
509 'group_id' => $groups[0],
511 $this->callAPISuccess('GroupContact', 'create', [
512 'contact_id' => $contactID,
513 'group_id' => $groups[1],
515 $this->callAPISuccess('GroupContact', 'create', [
516 'contact_id' => $contactID,
517 'group_id' => $groups[2],
519 $this->callAPISuccess('GroupContact', 'create', [
520 'contact_id' => $contactID,
521 'group_id' => $groups[3],
522 'status' => 'Removed',
524 $this->callAPISuccess('GroupContact', 'create', [
525 'contact_id' => $contactID,
526 'group_id' => $groups[4],
527 'status' => 'Removed',
529 $this->callAPISuccess('GroupContact', 'create', [
530 'contact_id' => $contactID,
531 'group_id' => $groups[5],
532 'status' => 'Removed',
534 $this->callAPISuccess('GroupContact', 'create', [
535 'contact_id' => $contact2ID,
536 'group_id' => $groups[1],
538 $this->callAPISuccess('GroupContact', 'create', [
539 'contact_id' => $contact2ID,
540 'group_id' => $groups[2],
541 'status' => 'Removed',
543 $this->callAPISuccess('GroupContact', 'create', [
544 'contact_id' => $contact2ID,
545 'group_id' => $groups[4],
547 $this->callAPISuccess('GroupContact', 'create', [
548 'contact_id' => $contact2ID,
549 'group_id' => $groups[5],
550 'status' => 'Removed',
552 $this->callAPISuccess('GroupContact', 'create', [
553 'contact_id' => $contact2ID,
554 'group_id' => $groups[6],
556 $this->callAPISuccess('GroupContact', 'create', [
557 'contact_id' => $contact2ID,
558 'group_id' => $groups[7],
559 'status' => 'Removed',
561 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['mode' => 'safe']);
562 $this->assertCount(0, $result['values']['skipped']);
563 $this->assertCount(1, $result['values']['merged']);
564 $groupResult = $this->callAPISuccess('GroupContact', 'get', []);
565 $this->assertEquals(5, $groupResult['count']);
573 foreach ($groupResult['values'] as $groupValues) {
574 $this->assertEquals($contactID, $groupValues['contact_id']);
575 $this->assertEquals('Added', $groupValues['status']);
576 $this->assertContains($groupValues['group_id'], $expectedGroups);
582 * Test that we handle cache entries without clashes.
584 public function testMergeCaches() {
585 $contactID = $this->individualCreate();
586 $contact2ID = $this->individualCreate();
587 $groupID = $this->groupCreate();
588 $this->callAPISuccess('GroupContact', 'create', ['group_id' => $groupID, 'contact_id' => $contactID]);
589 $this->callAPISuccess('GroupContact', 'create', ['group_id' => $groupID, 'contact_id' => $contact2ID]);
590 CRM_Core_DAO
::executeQuery("INSERT INTO civicrm_group_contact_cache(group_id, contact_id) VALUES
591 ($groupID, $contactID),
592 ($groupID, $contact2ID)
594 $this->callAPISuccess('Job', 'process_batch_merge', ['mode' => 'safe']);
598 * Test that we handle cache entries without clashes.
600 public function testMergeSharedActivity() {
601 $contactID = $this->individualCreate();
602 $contact2ID = $this->individualCreate();
603 $activityID = $this->activityCreate(['target_contact_id' => [$contactID, $contact2ID]]);
604 $this->callAPISuccess('Job', 'process_batch_merge', ['mode' => 'safe']);
608 * Test the decisions made for addresses when merging.
610 * @dataProvider getMergeLocationData
613 * (the ones with **** could be disputed as whether it is the best outcome).
614 * 'matching_primary' - Primary matches, including location_type_id. One contact has an additional address.
615 * - result - primary is the shared one. Additional address is retained.
616 * 'matching_primary_reverse' - Primary matches, including location_type_id. Keep both. (opposite order)
617 * - result - primary is the shared one. Additional address is retained.
618 * 'only_one_has_address' - Only one contact has addresses (retain)
619 * - the (only) address is retained
620 * 'only_one_has_address_reverse'
621 * - the (only) address is retained
622 * 'different_primaries_with_different_location_type' Primaries are different but do not clash due to diff type
623 * - result - both addresses kept. The one from the kept (lowest ID) contact is primary
624 * 'different_primaries_with_different_location_type_reverse' Primaries are different but do not clash due to diff type
625 * - result - both addresses kept. The one from the kept (lowest ID) contact is primary
626 * 'different_primaries_location_match_only_one_address' per previous but a second address matches the primary but is not primary
627 * - result - both addresses kept. The one from the kept (lowest ID) contact is primary
628 * 'different_primaries_location_match_only_one_address_reverse' per previous but a second address matches the primary but is not primary
629 * - result - both addresses kept. The one from the kept (lowest ID) contact is primary
630 * 'same_primaries_different_location' Primary addresses are the same but have different location type IDs
631 * - result primary kept with the lowest ID. Other address retained too (to preserve location type info).
632 * 'same_primaries_different_location_reverse' Primary addresses are the same but have different location type IDs
633 * - result primary kept with the lowest ID. Other address retained too (to preserve location type info).
635 * @param array $dataSet
637 * @throws \CRM_Core_Exception
639 public function testBatchMergesAddresses($dataSet) {
640 $contactID1 = $this->individualCreate();
641 $contactID2 = $this->individualCreate();
642 foreach ($dataSet['contact_1'] as $address) {
643 $this->callAPISuccess($dataSet['entity'], 'create', array_merge(['contact_id' => $contactID1], $address));
645 foreach ($dataSet['contact_2'] as $address) {
646 $this->callAPISuccess($dataSet['entity'], 'create', array_merge(['contact_id' => $contactID2], $address));
649 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['mode' => 'safe']);
650 $this->assertCount(1, $result['values']['merged']);
651 $addresses = $this->callAPISuccess($dataSet['entity'], 'get', ['contact_id' => $contactID1, 'sequential' => 1]);
652 $this->assertEquals(count($dataSet['expected']), $addresses['count'], 'Did not get the expected result for ' . $dataSet['entity'] . (!empty($dataSet['description']) ?
" on dataset {$dataSet['description']}" : ''));
653 $locationTypes = $this->callAPISuccess($dataSet['entity'], 'getoptions', ['field' => 'location_type_id']);
654 foreach ($dataSet['expected'] as $index => $expectedAddress) {
655 foreach ($expectedAddress as $key => $value) {
656 if ($key === 'location_type_id') {
657 $this->assertEquals($locationTypes['values'][$addresses['values'][$index][$key]], $value);
660 $this->assertEquals($addresses['values'][$index][$key], $value, "mismatch on $key" . (!empty($dataSet['description']) ?
" on dataset {$dataSet['description']}" : ''));
667 * Test altering the address decision by hook.
669 * @dataProvider getMergeLocationData
671 * @param array $dataSet
673 * @throws \CRM_Core_Exception
675 public function testBatchMergesAddressesHook($dataSet) {
676 $contactID1 = $this->individualCreate();
677 $contactID2 = $this->individualCreate();
678 $this->contributionCreate(['contact_id' => $contactID1, 'receive_date' => '2010-01-01', 'invoice_id' => 1, 'trxn_id' => 1]);
679 $this->contributionCreate(['contact_id' => $contactID2, 'receive_date' => '2012-01-01', 'invoice_id' => 2, 'trxn_id' => 2]);
680 foreach ($dataSet['contact_1'] as $address) {
681 $this->callAPISuccess($dataSet['entity'], 'create', array_merge(['contact_id' => $contactID1], $address));
683 foreach ($dataSet['contact_2'] as $address) {
684 $this->callAPISuccess($dataSet['entity'], 'create', array_merge(['contact_id' => $contactID2], $address));
686 $this->hookClass
->setHook('civicrm_alterLocationMergeData', [$this, 'hookMostRecentDonor']);
688 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['mode' => 'safe']);
689 $this->assertCount(1, $result['values']['merged']);
690 $addresses = $this->callAPISuccess($dataSet['entity'], 'get', ['contact_id' => $contactID1, 'sequential' => 1]);
691 $this->assertEquals(count($dataSet['expected_hook']), $addresses['count']);
692 $locationTypes = $this->callAPISuccess($dataSet['entity'], 'getoptions', ['field' => 'location_type_id']);
693 foreach ($dataSet['expected_hook'] as $index => $expectedAddress) {
694 foreach ($expectedAddress as $key => $value) {
695 if ($key === 'location_type_id') {
696 $this->assertEquals($locationTypes['values'][$addresses['values'][$index][$key]], $value);
699 $this->assertEquals($value, $addresses['values'][$index][$key], $dataSet['entity'] . ': Unexpected value for ' . $key . (!empty($dataSet['description']) ?
" on dataset {$dataSet['description']}" : ''));
706 * Test the organization will not be matched to an individual.
708 * @throws \CRM_Core_Exception
710 public function testBatchMergeWillNotMergeOrganizationToIndividual() {
711 $individual = $this->callAPISuccess('Contact', 'create', [
712 'contact_type' => 'Individual',
713 'organization_name' => 'Anon',
714 'email' => 'anonymous@hacker.com',
716 $organization = $this->callAPISuccess('Contact', 'create', [
717 'contact_type' => 'Organization',
718 'organization_name' => 'Anon',
719 'email' => 'anonymous@hacker.com',
721 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['mode' => 'aggressive']);
722 $this->assertCount(0, $result['values']['skipped']);
723 $this->assertCount(0, $result['values']['merged']);
724 $this->callAPISuccessGetSingle('Contact', ['id' => $individual['id']]);
725 $this->callAPISuccessGetSingle('Contact', ['id' => $organization['id']]);
730 * Test hook allowing modification of the data calculated for merging locations.
732 * We are testing a nuanced real life situation where the address data of the
733 * most recent donor gets priority - resulting in the primary address being set
734 * to the primary address of the most recent donor and address data on a per
735 * location type basis also being set to the most recent donor. Hook also excludes
736 * a fully matching address with a different location.
738 * This has been added to the test suite to ensure the code supports more this
739 * type of intervention.
741 * @param array $blocksDAO
742 * Array of location DAO to be saved. These are arrays in 2 keys 'update' & 'delete'.
744 * Contact_id of the contact that survives the merge.
745 * @param int $otherId
746 * Contact_id of the contact that will be absorbed and deleted.
747 * @param array $migrationInfo
748 * Calculated migration info, informational only.
751 * @throws \CRM_Core_Exception
753 public function hookMostRecentDonor(&$blocksDAO, $mainId, $otherId, $migrationInfo) {
755 $lastDonorID = $this->callAPISuccessGetValue('Contribution', [
756 'return' => 'contact_id',
757 'contact_id' => ['IN' => [$mainId, $otherId]],
758 'options' => ['sort' => 'receive_date DESC', 'limit' => 1],
760 // Since the last donor is not the main ID we are prioritising info from the last donor.
761 // In the test this should always be true - but keep the check in case
762 // something changes that we need to detect.
763 if ($lastDonorID != $mainId) {
764 foreach ($migrationInfo['other_details']['location_blocks'] as $blockType => $blocks) {
765 foreach ($blocks as $block) {
766 if ($block['is_primary']) {
767 $primaryAddressID = $block['id'];
768 if (!empty($migrationInfo['main_details']['location_blocks'][$blockType])) {
769 foreach ($migrationInfo['main_details']['location_blocks'][$blockType] as $mainBlock) {
770 if (empty($blocksDAO[$blockType]['update'][$block['id']]) && $mainBlock['location_type_id'] == $block['location_type_id']) {
771 // This was an address match - we just need to check the is_primary
772 // is true on the matching kept address.
773 $primaryAddressID = $mainBlock['id'];
774 $blocksDAO[$blockType]['update'][$primaryAddressID] = _civicrm_api3_load_DAO($blockType);
775 $blocksDAO[$blockType]['update'][$primaryAddressID]->id
= $primaryAddressID;
777 $mainLocationTypeID = $mainBlock['location_type_id'];
778 // We also want to be more ruthless about removing matching addresses.
779 unset($mainBlock['location_type_id']);
780 if (CRM_Dedupe_Merger
::locationIsSame($block, $mainBlock)
781 && (!isset($blocksDAO[$blockType]['update']) ||
!isset($blocksDAO[$blockType]['update'][$mainBlock['id']]))
782 && (!isset($blocksDAO[$blockType]['delete']) ||
!isset($blocksDAO[$blockType]['delete'][$mainBlock['id']]))
784 $blocksDAO[$blockType]['delete'][$mainBlock['id']] = _civicrm_api3_load_DAO($blockType);
785 $blocksDAO[$blockType]['delete'][$mainBlock['id']]->id
= $mainBlock['id'];
787 // Arguably the right way to handle this is just to set is_primary for the primary
788 // and for the merge fn to call something like BAO::add & hooks to work etc.
789 // if that happens though this should keep working...
790 elseif ($mainBlock['is_primary'] && $mainLocationTypeID != $block['location_type_id']) {
791 $blocksDAO['address']['update'][$mainBlock['id']] = _civicrm_api3_load_DAO($blockType);
792 $blocksDAO['address']['update'][$mainBlock['id']]->is_primary
= 0;
793 $blocksDAO['address']['update'][$mainBlock['id']]->id
= $mainBlock['id'];
797 $blocksDAO[$blockType]['update'][$primaryAddressID]->is_primary
= 1;
806 * Get address combinations for the merge test.
810 public function getMergeLocationData() {
811 $address1 = ['street_address' => 'Buckingham Palace', 'city' => 'London'];
812 $address2 = ['street_address' => 'The Doghouse', 'supplemental_address_1' => 'under the blanket'];
813 $data = $this->getMergeLocations($address1, $address2, 'Address');
814 $data = array_merge($data, $this->getMergeLocations(['phone' => '12345', 'phone_type_id' => 1], ['phone' => '678910', 'phone_type_id' => 1], 'Phone'));
815 $data = array_merge($data, $this->getMergeLocations(['phone' => '12345'], ['phone' => '678910'], 'Phone'));
816 $data = array_merge($data, $this->getMergeLocations(['email' => 'mini@me.com'], ['email' => 'mini@me.org'], 'Email', [
818 'email' => 'anthony_anderson@civicrm.org',
819 'location_type_id' => 'Home',
827 * Test weird characters don't mess with merge & cause a fatal.
829 * @throws \CRM_Core_Exception
831 public function testNoErrorOnOdd() {
832 $this->individualCreate();
833 $this->individualCreate(['first_name' => 'Gerrit%0a%2e%0a']);
834 $this->callAPISuccess('Job', 'process_batch_merge', []);
836 $this->individualCreate();
837 $this->individualCreate(['first_name' => '[foo\\bar\'baz']);
838 $this->callAPISuccess('Job', 'process_batch_merge', []);
839 $this->callAPISuccessGetSingle('Contact', ['first_name' => '[foo\\bar\'baz']);
843 * Test the batch merge does not create duplicate emails.
845 * Test CRM-18546, a 4.7 regression whereby a merged contact gets duplicate emails.
847 * @throws \CRM_Core_Exception
849 public function testBatchMergeEmailHandling() {
850 for ($x = 0; $x <= 4; $x++
) {
851 $id = $this->individualCreate(['email' => 'batman@gotham.met']);
853 $result = $this->callAPISuccess('Job', 'process_batch_merge', []);
854 $this->assertCount(4, $result['values']['merged']);
855 $this->callAPISuccessGetCount('Contact', ['email' => 'batman@gotham.met'], 1);
856 $contacts = $this->callAPISuccess('Contact', 'get', ['is_deleted' => 0]);
857 $deletedContacts = $this->callAPISuccess('Contact', 'get', ['is_deleted' => 1]);
858 $this->callAPISuccessGetCount('Email', [
859 'email' => 'batman@gotham.met',
860 'contact_id' => ['IN' => array_keys($contacts['values'])],
862 $this->callAPISuccessGetCount('Email', [
863 'email' => 'batman@gotham.met',
864 'contact_id' => ['IN' => array_keys($deletedContacts['values'])],
869 * Test the batch merge respects email "on hold".
871 * Test CRM-19148, Batch merge - Email on hold data lost when there is a conflict.
873 * @dataProvider getOnHoldSets
875 * @param bool $onHold1
876 * @param bool $onHold2
878 * @param string $conflictText
880 * @throws \CRM_Core_Exception
882 public function testBatchMergeEmailOnHold($onHold1, $onHold2, $merge, $conflictText) {
883 $this->individualCreate([
884 'api.email.create' => [
885 'email' => 'batman@gotham.met',
886 'location_type_id' => 'Work',
888 'on_hold' => $onHold1,
891 $this->individualCreate([
892 'api.email.create' => [
893 'email' => 'batman@gotham.met',
894 'location_type_id' => 'Work',
896 'on_hold' => $onHold2,
899 $result = $this->callAPISuccess('Job', 'process_batch_merge', []);
900 $this->assertCount($merge, $result['values']['merged']);
902 $defaultRuleGroupID = $this->callAPISuccessGetValue('RuleGroup', [
903 'contact_type' => 'Individual',
904 'used' => 'Unsupervised',
906 'options' => ['limit' => 1],
909 $duplicates = $this->callAPISuccess('Dedupe', 'getduplicates', ['rule_group_id' => $defaultRuleGroupID]);
910 $this->assertEquals($conflictText, $duplicates['values'][0]['conflicts']);
915 * Data provider for testBatchMergeEmailOnHold: combinations of on_hold & expected outcomes.
917 public function getOnHoldSets() {
918 // Each row specifies: contact 1 on_hold, contact 2 on_hold, merge? (0 or 1),
921 [0, 1, 0, "Email 2 (Work): 'batman@gotham.met' vs. 'batman@gotham.met\n(On Hold)'"],
922 [1, 0, 0, "Email 2 (Work): 'batman@gotham.met\n(On Hold)' vs. 'batman@gotham.met'"],
928 * Test the batch merge does not fatal on an empty rule.
930 * @dataProvider getRuleSets
932 * @param string $contactType
933 * @param string $used
934 * @param string $name
935 * @param bool $isReserved
936 * @param int $threshold
938 * @throws \CRM_Core_Exception
940 public function testBatchMergeEmptyRule($contactType, $used, $name, $isReserved, $threshold) {
941 $ruleGroup = $this->callAPISuccess('RuleGroup', 'create', [
942 'contact_type' => $contactType,
943 'threshold' => $threshold,
946 'is_reserved' => $isReserved,
948 $this->callAPISuccess('Job', 'process_batch_merge', ['rule_group_id' => $ruleGroup['id']]);
949 $this->callAPISuccess('RuleGroup', 'delete', ['id' => $ruleGroup['id']]);
953 * Get the various rule combinations.
955 public function getRuleSets() {
956 $contactTypes = ['Individual', 'Organization', 'Household'];
957 $useds = ['Unsupervised', 'General', 'Supervised'];
959 foreach ($contactTypes as $contactType) {
960 foreach ($useds as $used) {
961 $ruleGroups[] = [$contactType, $used, 'Bob', FALSE, 0];
962 $ruleGroups[] = [$contactType, $used, 'Bob', FALSE, 10];
963 $ruleGroups[] = [$contactType, $used, 'Bob', TRUE, 10];
964 $ruleGroups[] = [$contactType, $used, $contactType . $used, FALSE, 10];
965 $ruleGroups[] = [$contactType, $used, $contactType . $used, TRUE, 10];
972 * Test the batch merge does not create duplicate emails.
974 * Test CRM-18546, a 4.7 regression whereby a merged contact gets duplicate emails.
976 * @throws \CRM_Core_Exception
978 public function testBatchMergeMatchingAddress() {
979 for ($x = 0; $x <= 2; $x++
) {
980 $this->individualCreate([
981 'api.address.create' => [
982 'location_type_id' => 'Home',
983 'street_address' => 'Appt 115, The Batcave',
985 'postal_code' => 'Nananananana',
989 // Different location type, still merge, identical.
990 $this->individualCreate([
991 'api.address.create' => [
992 'location_type_id' => 'Main',
993 'street_address' => 'Appt 115, The Batcave',
995 'postal_code' => 'Nananananana',
999 $this->individualCreate([
1000 'api.address.create' => [
1001 'location_type_id' => 'Home',
1002 'street_address' => 'Appt 115, The Batcave',
1004 'postal_code' => 'Batman',
1008 $result = $this->callAPISuccess('Job', 'process_batch_merge', []);
1009 $this->assertEquals(3, count($result['values']['merged']));
1010 $this->assertEquals(1, count($result['values']['skipped']));
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', [
1015 'street_address' => 'Appt 115, The Batcave',
1016 'contact_id' => ['IN' => array_keys($contacts['values'])],
1019 $this->callAPISuccessGetCount('Address', [
1020 'street_address' => 'Appt 115, The Batcave',
1021 'contact_id' => ['IN' => array_keys($deletedContacts['values'])],
1026 * Test the batch merge by id range.
1028 * We have 2 sets of 5 matches & set the merge only to merge the lower set.
1030 * @throws \CRM_Core_Exception
1032 public function testBatchMergeIDRange() {
1033 for ($x = 0; $x <= 4; $x++
) {
1034 $id = $this->individualCreate(['email' => 'batman@gotham.met']);
1036 for ($x = 0; $x <= 4; $x++
) {
1037 $this->individualCreate(['email' => 'robin@gotham.met']);
1039 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['criteria' => ['contact' => ['id' => ['<' => $id]]]]);
1040 $this->assertEquals(4, count($result['values']['merged']));
1041 $this->callAPISuccessGetCount('Contact', ['email' => 'batman@gotham.met'], 1);
1042 $this->callAPISuccessGetCount('Contact', ['email' => 'robin@gotham.met'], 5);
1043 $contacts = $this->callAPISuccess('Contact', 'get', ['is_deleted' => 0]);
1044 $deletedContacts = $this->callAPISuccess('Contact', 'get', ['is_deleted' => 0]);
1045 $this->callAPISuccessGetCount('Email', [
1046 'email' => 'batman@gotham.met',
1047 'contact_id' => ['IN' => array_keys($contacts['values'])],
1049 $this->callAPISuccessGetCount('Email', [
1050 'email' => 'batman@gotham.met',
1051 'contact_id' => ['IN' => array_keys($deletedContacts['values'])],
1053 $this->callAPISuccessGetCount('Email', [
1054 'email' => 'robin@gotham.met',
1055 'contact_id' => ['IN' => array_keys($contacts['values'])],
1061 * Test the batch merge copes with view only custom data field.
1063 * @throws \CRM_Core_Exception
1065 public function testBatchMergeCustomDataViewOnlyField() {
1066 CRM_Core_Config
::singleton()->userPermissionClass
->permissions
= ['access CiviCRM', 'edit my contact'];
1067 $mouseParams = ['first_name' => 'Mickey', 'last_name' => 'Mouse', 'email' => 'tha_mouse@mouse.com'];
1068 $this->individualCreate($mouseParams);
1070 $customGroup = $this->customGroupCreate();
1071 $customField = $this->customFieldCreate(['custom_group_id' => $customGroup['id'], 'is_view' => 1]);
1072 $this->individualCreate(array_merge($mouseParams, ['custom_' . $customField['id'] => 'blah']));
1074 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['check_permissions' => 0, 'mode' => 'safe']);
1075 $this->assertEquals(1, count($result['values']['merged']));
1076 $mouseParams['return'] = 'custom_' . $customField['id'];
1077 $mouse = $this->callAPISuccess('Contact', 'getsingle', $mouseParams);
1078 $this->assertEquals('blah', $mouse['custom_' . $customField['id']]);
1080 $this->customFieldDelete($customField['id']);
1081 $this->customGroupDelete($customGroup['id']);
1085 * Test the batch merge retains 0 as a valid custom field value.
1087 * Note that we set 0 on 2 fields with one on each contact to ensure that
1088 * both merged & mergee fields are respected.
1090 * @throws \CRM_Core_Exception
1092 public function testBatchMergeCustomDataZeroValueField() {
1093 $customGroup = $this->customGroupCreate();
1094 $customField = $this->customFieldCreate(['custom_group_id' => $customGroup['id'], 'default_value' => NULL]);
1096 $mouseParams = ['first_name' => 'Mickey', 'last_name' => 'Mouse', 'email' => 'tha_mouse@mouse.com'];
1097 $this->individualCreate(array_merge($mouseParams, ['custom_' . $customField['id'] => '']));
1098 $this->individualCreate(array_merge($mouseParams, ['custom_' . $customField['id'] => 0]));
1100 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['check_permissions' => 0, 'mode' => 'safe']);
1101 $this->assertCount(1, $result['values']['merged']);
1102 $mouseParams['return'] = 'custom_' . $customField['id'];
1103 $mouse = $this->callAPISuccess('Contact', 'getsingle', $mouseParams);
1104 $this->assertEquals(0, $mouse['custom_' . $customField['id']]);
1106 $this->individualCreate(array_merge($mouseParams, ['custom_' . $customField['id'] => NULL]));
1107 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['check_permissions' => 0, 'mode' => 'safe']);
1108 $this->assertEquals(1, count($result['values']['merged']));
1109 $mouseParams['return'] = 'custom_' . $customField['id'];
1110 $mouse = $this->callAPISuccess('Contact', 'getsingle', $mouseParams);
1111 $this->assertEquals(0, $mouse['custom_' . $customField['id']]);
1113 $this->customFieldDelete($customField['id']);
1114 $this->customGroupDelete($customGroup['id']);
1118 * Test the batch merge treats 0 vs 1 as a conflict.
1120 * @throws \CRM_Core_Exception
1122 public function testBatchMergeCustomDataZeroValueFieldWithConflict() {
1123 $customGroup = $this->customGroupCreate();
1124 $customField = $this->customFieldCreate(['custom_group_id' => $customGroup['id'], 'default_value' => NULL]);
1126 $mouseParams = ['first_name' => 'Mickey', 'last_name' => 'Mouse', 'email' => 'tha_mouse@mouse.com'];
1127 $mouse1 = $this->individualCreate(array_merge($mouseParams, ['custom_' . $customField['id'] => 0]));
1128 $mouse2 = $this->individualCreate(array_merge($mouseParams, ['custom_' . $customField['id'] => 1]));
1130 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['check_permissions' => 0, 'mode' => 'safe']);
1131 $this->assertCount(0, $result['values']['merged']);
1133 // Reverse which mouse has the zero to test we still get a conflict.
1134 $this->individualCreate(array_merge($mouseParams, ['id' => $mouse1, 'custom_' . $customField['id'] => 1]));
1135 $this->individualCreate(array_merge($mouseParams, ['id' => $mouse2, 'custom_' . $customField['id'] => 0]));
1136 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['check_permissions' => 0, 'mode' => 'safe']);
1137 $this->assertEquals(0, count($result['values']['merged']));
1139 $this->customFieldDelete($customField['id']);
1140 $this->customGroupDelete($customGroup['id']);
1144 * Test the batch merge function actually works!
1146 * @dataProvider getMergeSets
1148 * @param array $dataSet
1150 * @throws \CRM_Core_Exception
1152 public function testBatchMergeWorksCheckPermissionsTrue($dataSet) {
1153 CRM_Core_Config
::singleton()->userPermissionClass
->permissions
= ['access CiviCRM', 'administer CiviCRM', 'merge duplicate contacts', 'force merge duplicate contacts'];
1154 foreach ($dataSet['contacts'] as $params) {
1155 $this->callAPISuccess('Contact', 'create', $params);
1158 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['check_permissions' => 1, 'mode' => $dataSet['mode']]);
1159 $this->assertCount(0, $result['values']['merged'], 'User does not have permission to any contacts, so no merging');
1160 $this->assertCount(0, $result['values']['skipped'], 'User does not have permission to any contacts, so no skip visibility');
1164 * Test the batch merge function actually works!
1166 * @dataProvider getMergeSets
1168 * @param array $dataSet
1170 * @throws \CRM_Core_Exception
1172 public function testBatchMergeWorksCheckPermissionsFalse($dataSet) {
1173 CRM_Core_Config
::singleton()->userPermissionClass
->permissions
= ['access CiviCRM', 'edit my contact'];
1174 foreach ($dataSet['contacts'] as $params) {
1175 $this->callAPISuccess('Contact', 'create', $params);
1178 $result = $this->callAPISuccess('Job', 'process_batch_merge', ['check_permissions' => 0, 'mode' => $dataSet['mode']]);
1179 $this->assertEquals($dataSet['skipped'], count($result['values']['skipped']), 'Failed to skip the right number:' . $dataSet['skipped']);
1180 $this->assertEquals($dataSet['merged'], count($result['values']['merged']));
1184 * Get data for batch merge.
1186 public function getMergeSets() {
1193 'first_name' => 'Michael',
1194 'last_name' => 'Jackson',
1195 'email' => 'michael@neverland.com',
1196 'contact_type' => 'Individual',
1197 'contact_sub_type' => 'Student',
1198 'api.Address.create' => [
1199 'street_address' => 'big house',
1200 'location_type_id' => 'Home',
1204 'first_name' => 'Michael',
1205 'last_name' => 'Jackson',
1206 'email' => 'michael@neverland.com',
1207 'contact_type' => 'Individual',
1208 'contact_sub_type' => 'Student',
1215 'first_name' => 'Michael',
1216 'last_name' => 'Jackson',
1217 'email' => 'michael@neverland.com',
1218 'contact_type' => 'Individual',
1228 'first_name' => 'Michael',
1229 'last_name' => 'Jackson',
1230 'email' => 'michael@neverland.com',
1231 'contact_type' => 'Individual',
1232 'contact_sub_type' => 'Student',
1233 'api.Address.create' => [
1234 'street_address' => 'big house',
1235 'location_type_id' => 'Home',
1239 'first_name' => 'Michael',
1240 'last_name' => 'Jackson',
1241 'email' => 'michael@neverland.com',
1242 'contact_type' => 'Individual',
1243 'contact_sub_type' => 'Student',
1244 'api.Address.create' => [
1245 'street_address' => 'bigger house',
1246 'location_type_id' => 'Home',
1254 'first_name' => 'Michael',
1255 'last_name' => 'Jackson',
1256 'email' => 'michael@neverland.com',
1257 'contact_type' => 'Individual',
1258 'street_address' => 'big house',
1261 'first_name' => 'Michael',
1262 'last_name' => 'Jackson',
1263 'email' => 'michael@neverland.com',
1264 'contact_type' => 'Individual',
1265 'street_address' => 'bigger house',
1275 'first_name' => 'Michael',
1276 'last_name' => 'Jackson',
1277 'email' => 'michael@neverland.com',
1278 'contact_type' => 'Individual',
1279 'contact_sub_type' => 'Student',
1280 'api.Email.create' => [
1281 'email' => 'big.slog@work.co.nz',
1282 'location_type_id' => 'Work',
1286 'first_name' => 'Michael',
1287 'last_name' => 'Jackson',
1288 'email' => 'michael@neverland.com',
1289 'contact_type' => 'Individual',
1290 'contact_sub_type' => 'Student',
1291 'api.Email.create' => [
1292 'email' => 'big.slog@work.com',
1293 'location_type_id' => 'Work',
1301 'first_name' => 'Michael',
1302 'last_name' => 'Jackson',
1303 'email' => 'michael@neverland.com',
1304 'contact_type' => 'Individual',
1307 'first_name' => 'Michael',
1308 'last_name' => 'Jackson',
1309 'email' => 'michael@neverland.com',
1310 'contact_type' => 'Individual',
1320 'first_name' => 'Michael',
1321 'last_name' => 'Jackson',
1322 'email' => 'michael@neverland.com',
1323 'contact_type' => 'Individual',
1324 'contact_sub_type' => 'Student',
1325 'api.Phone.create' => [
1326 'phone' => '123456',
1327 'location_type_id' => 'Work',
1331 'first_name' => 'Michael',
1332 'last_name' => 'Jackson',
1333 'email' => 'michael@neverland.com',
1334 'contact_type' => 'Individual',
1335 'contact_sub_type' => 'Student',
1336 'api.Phone.create' => [
1338 'location_type_id' => 'Work',
1346 'first_name' => 'Michael',
1347 'last_name' => 'Jackson',
1348 'email' => 'michael@neverland.com',
1349 'contact_type' => 'Individual',
1352 'first_name' => 'Michael',
1353 'last_name' => 'Jackson',
1354 'email' => 'michael@neverland.com',
1355 'contact_type' => 'Individual',
1362 'mode' => 'aggressive',
1365 'first_name' => 'Michael',
1366 'last_name' => 'Jackson',
1367 'email' => 'michael@neverland.com',
1368 'contact_type' => 'Individual',
1369 'contact_sub_type' => 'Student',
1370 'api.Address.create' => [
1371 'street_address' => 'big house',
1372 'location_type_id' => 'Home',
1376 'first_name' => 'Michael',
1377 'last_name' => 'Jackson',
1378 'email' => 'michael@neverland.com',
1379 'contact_type' => 'Individual',
1380 'contact_sub_type' => 'Student',
1381 'api.Address.create' => [
1382 'street_address' => 'bigger house',
1383 'location_type_id' => 'Home',
1391 'first_name' => 'Michael',
1392 'last_name' => 'Jackson',
1393 'email' => 'michael@neverland.com',
1394 'contact_type' => 'Individual',
1395 'street_address' => 'big house',
1405 'first_name' => 'Michael',
1406 'last_name' => 'Jackson',
1407 'email' => 'michael@neverland.com',
1408 'contact_type' => 'Individual',
1409 'contact_sub_type' => 'Student',
1410 'api.Address.create' => [
1411 'street_address' => 'big house',
1412 'location_type_id' => 'Home',
1416 'first_name' => 'Michael',
1417 'last_name' => 'Jackson',
1418 'email' => 'michael@neverland.com',
1419 'contact_type' => 'Individual',
1420 'contact_sub_type' => 'Student',
1428 'first_name' => 'Michael',
1429 'last_name' => 'Jackson',
1430 'email' => 'michael@neverland.com',
1431 'contact_type' => 'Individual',
1435 'first_name' => 'Michael',
1436 'last_name' => 'Jackson',
1437 'email' => 'michael@neverland.com',
1438 'contact_type' => 'Individual',
1449 'first_name' => 'Michael',
1450 'last_name' => 'Jackson',
1451 'email' => 'michael@neverland.com',
1452 'contact_type' => 'Individual',
1453 'contact_sub_type' => 'Student',
1454 'api.Address.create' => [
1455 'street_address' => 'big house',
1456 'location_type_id' => 'Home',
1461 'first_name' => 'Michael',
1462 'last_name' => 'Jackson',
1463 'email' => 'michael@neverland.com',
1464 'contact_type' => 'Individual',
1465 'contact_sub_type' => 'Student',
1472 'first_name' => 'Michael',
1473 'last_name' => 'Jackson',
1474 'email' => 'michael@neverland.com',
1475 'contact_type' => 'Individual',
1479 'first_name' => 'Michael',
1480 'last_name' => 'Jackson',
1481 'email' => 'michael@neverland.com',
1482 'contact_type' => 'Individual',
1491 'first_name' => 'Dianna',
1492 'last_name' => 'McAndrew',
1493 'middle_name' => 'Prancer',
1494 'birth_date' => '2015-12-25',
1495 'gender_id' => 'Female',
1496 'job_title' => 'Thriller',
1499 foreach ($conflictPairs as $key => $value) {
1501 'first_name' => 'Michael',
1502 'middle_name' => 'Dancer',
1503 'last_name' => 'Jackson',
1504 'birth_date' => '2015-02-25',
1505 'email' => 'michael@neverland.com',
1506 'contact_type' => 'Individual',
1507 'contact_sub_type' => ['Student'],
1508 'gender_id' => 'Male',
1509 'job_title' => 'Entertainer',
1511 $contact2 = $contactParams;
1513 $contact2[$key] = $value;
1514 $data[$key . '_conflict'] = [
1517 'contacts' => [$contactParams, $contact2],
1520 'expected' => [$contactParams, $contact2],
1529 * Implements pre hook on relationships.
1532 * @param string $objectName
1534 * @param array $params
1536 public function hookPreRelationship($op, $objectName, $id, &$params) {
1537 if ($op === 'delete') {
1540 if ($params['is_active']) {
1541 $params['description'] = 'Hooked';
1544 $params['description'] = 'Go Go you good thing';
1549 * Get the location data set.
1551 * @param array $locationParams1
1552 * @param array $locationParams2
1553 * @param string $entity
1554 * @param array $additionalExpected
1558 public function getMergeLocations($locationParams1, $locationParams2, $entity, $additionalExpected = []) {
1561 'matching_primary' => [
1562 'entity' => $entity,
1565 'location_type_id' => 'Main',
1567 ], $locationParams1),
1569 'location_type_id' => 'Work',
1571 ], $locationParams2),
1575 'location_type_id' => 'Main',
1577 ], $locationParams1),
1579 'expected' => array_merge($additionalExpected, [
1581 'location_type_id' => 'Main',
1583 ], $locationParams1),
1585 'location_type_id' => 'Work',
1587 ], $locationParams2),
1589 'expected_hook' => array_merge($additionalExpected, [
1591 'location_type_id' => 'Main',
1593 ], $locationParams1),
1595 'location_type_id' => 'Work',
1597 ], $locationParams2),
1602 'matching_primary_reverse' => [
1603 'entity' => $entity,
1606 'location_type_id' => 'Main',
1608 ], $locationParams1),
1612 'location_type_id' => 'Main',
1614 ], $locationParams1),
1616 'location_type_id' => 'Work',
1618 ], $locationParams2),
1620 'expected' => array_merge($additionalExpected, [
1622 'location_type_id' => 'Main',
1624 ], $locationParams1),
1626 'location_type_id' => 'Work',
1628 ], $locationParams2),
1630 'expected_hook' => array_merge($additionalExpected, [
1632 'location_type_id' => 'Main',
1634 ], $locationParams1),
1636 'location_type_id' => 'Work',
1638 ], $locationParams2),
1643 'only_one_has_address' => [
1644 'entity' => $entity,
1647 'location_type_id' => 'Main',
1649 ], $locationParams1),
1651 'location_type_id' => 'Work',
1653 ], $locationParams2),
1656 'expected' => array_merge($additionalExpected, [
1658 'location_type_id' => 'Main',
1660 ], $locationParams1),
1662 'location_type_id' => 'Work',
1664 ], $locationParams2),
1666 'expected_hook' => array_merge($additionalExpected, [
1668 'location_type_id' => 'Main',
1669 // When dealing with email we don't have a clean slate - the existing
1670 // primary will be primary.
1671 'is_primary' => ($entity == 'Email' ?
0 : 1),
1672 ], $locationParams1),
1674 'location_type_id' => 'Work',
1676 ], $locationParams2),
1681 'only_one_has_address_reverse' => [
1682 'description' => 'The destination contact does not have an address. secondary contact should be merged in.',
1683 'entity' => $entity,
1687 'location_type_id' => 'Main',
1689 ], $locationParams1),
1691 'location_type_id' => 'Work',
1693 ], $locationParams2),
1695 'expected' => array_merge($additionalExpected, [
1697 'location_type_id' => 'Main',
1698 // When dealing with email we don't have a clean slate - the existing
1699 // primary will be primary.
1700 'is_primary' => ($entity == 'Email' ?
0 : 1),
1701 ], $locationParams1),
1703 'location_type_id' => 'Work',
1705 ], $locationParams2),
1707 'expected_hook' => array_merge($additionalExpected, [
1709 'location_type_id' => 'Main',
1711 ], $locationParams1),
1713 'location_type_id' => 'Work',
1715 ], $locationParams2),
1720 'different_primaries_with_different_location_type' => [
1721 'description' => 'Primaries are different with different location. Keep both addresses. Set primary to be that of lower id',
1722 'entity' => $entity,
1725 'location_type_id' => 'Main',
1727 ], $locationParams1),
1731 'location_type_id' => 'Work',
1733 ], $locationParams2),
1735 'expected' => array_merge($additionalExpected, [
1737 'location_type_id' => 'Main',
1739 ], $locationParams1),
1741 'location_type_id' => 'Work',
1743 ], $locationParams2),
1745 'expected_hook' => array_merge($additionalExpected, [
1747 'location_type_id' => 'Main',
1749 ], $locationParams1),
1751 'location_type_id' => 'Work',
1753 ], $locationParams2),
1758 'different_primaries_with_different_location_type_reverse' => [
1759 'entity' => $entity,
1762 'location_type_id' => 'Work',
1764 ], $locationParams2),
1768 'location_type_id' => 'Main',
1770 ], $locationParams1),
1772 'expected' => array_merge($additionalExpected, [
1774 'location_type_id' => 'Work',
1776 ], $locationParams2),
1778 'location_type_id' => 'Main',
1780 ], $locationParams1),
1782 'expected_hook' => array_merge($additionalExpected, [
1784 'location_type_id' => 'Work',
1786 ], $locationParams2),
1788 'location_type_id' => 'Main',
1790 ], $locationParams1),
1795 'different_primaries_location_match_only_one_address' => [
1796 'entity' => $entity,
1799 'location_type_id' => 'Main',
1801 ], $locationParams1),
1803 'location_type_id' => 'Work',
1805 ], $locationParams2),
1809 'location_type_id' => 'Work',
1811 ], $locationParams2),
1814 'expected' => array_merge($additionalExpected, [
1816 'location_type_id' => 'Main',
1818 ], $locationParams1),
1820 'location_type_id' => 'Work',
1822 ], $locationParams2),
1824 'expected_hook' => array_merge($additionalExpected, [
1826 'location_type_id' => 'Main',
1828 ], $locationParams1),
1830 'location_type_id' => 'Work',
1832 ], $locationParams2),
1837 'different_primaries_location_match_only_one_address_reverse' => [
1838 'entity' => $entity,
1841 'location_type_id' => 'Work',
1843 ], $locationParams2),
1847 'location_type_id' => 'Main',
1849 ], $locationParams1),
1851 'location_type_id' => 'Work',
1853 ], $locationParams2),
1855 'expected' => array_merge($additionalExpected, [
1857 'location_type_id' => 'Work',
1859 ], $locationParams2),
1861 'location_type_id' => 'Main',
1863 ], $locationParams1),
1865 'expected_hook' => array_merge($additionalExpected, [
1867 'location_type_id' => 'Work',
1869 ], $locationParams2),
1871 'location_type_id' => 'Main',
1873 ], $locationParams1),
1878 'same_primaries_different_location' => [
1879 'entity' => $entity,
1882 'location_type_id' => 'Main',
1884 ], $locationParams1),
1888 'location_type_id' => 'Work',
1890 ], $locationParams1),
1893 'expected' => array_merge($additionalExpected, [
1895 'location_type_id' => 'Main',
1897 ], $locationParams1),
1899 'location_type_id' => 'Work',
1901 ], $locationParams1),
1903 'expected_hook' => array_merge($additionalExpected, [
1905 'location_type_id' => 'Work',
1907 ], $locationParams1),
1912 'same_primaries_different_location_reverse' => [
1913 'entity' => $entity,
1916 'location_type_id' => 'Work',
1918 ], $locationParams1),
1922 'location_type_id' => 'Main',
1924 ], $locationParams1),
1926 'expected' => array_merge($additionalExpected, [
1928 'location_type_id' => 'Work',
1930 ], $locationParams1),
1932 'location_type_id' => 'Main',
1934 ], $locationParams1),
1936 'expected_hook' => array_merge($additionalExpected, [
1938 'location_type_id' => 'Main',
1940 ], $locationParams1),
1948 * Test processing membership for deceased contacts.
1950 * @throws \CRM_Core_Exception
1952 public function testProcessMembershipDeceased() {
1953 $this->callAPISuccess('Job', 'process_membership', []);
1954 $deadManWalkingID = $this->individualCreate();
1955 $membershipID = $this->contactMembershipCreate(['contact_id' => $deadManWalkingID]);
1956 $this->callAPISuccess('Contact', 'create', ['id' => $deadManWalkingID, 'is_deceased' => 1]);
1957 $this->callAPISuccess('Job', 'process_membership', []);
1958 $membership = $this->callAPISuccessGetSingle('Membership', ['id' => $membershipID]);
1959 $deceasedStatusId = CRM_Core_PseudoConstant
::getKey('CRM_Member_BAO_Membership', 'status_id', 'Deceased');
1960 $this->assertEquals($deceasedStatusId, $membership['status_id']);
1964 * Test we get an error is deceased status is disabled.
1966 * @throws \CRM_Core_Exception
1968 public function testProcessMembershipNoDeceasedStatus() {
1969 $deceasedStatusId = CRM_Core_PseudoConstant
::getKey('CRM_Member_BAO_Membership', 'status_id', 'Deceased');
1970 $this->callAPISuccess('MembershipStatus', 'create', ['is_active' => 0, 'id' => $deceasedStatusId]);
1971 CRM_Core_PseudoConstant
::flush();
1973 $deadManWalkingID = $this->individualCreate();
1974 $this->contactMembershipCreate(['contact_id' => $deadManWalkingID]);
1975 $this->callAPISuccess('Contact', 'create', ['id' => $deadManWalkingID, 'is_deceased' => 1]);
1976 $this->callAPIFailure('Job', 'process_membership', []);
1978 $this->callAPISuccess('MembershipStatus', 'create', ['is_active' => 1, 'id' => $deceasedStatusId]);
1982 * Test processing membership: check that status is updated when it should be
1983 * and left alone when it shouldn't.
1985 * @throws \CRM_Core_Exception
1986 * @throws \CiviCRM_API3_Exception
1988 public function testProcessMembershipUpdateStatus() {
1989 $this->ids
['MembershipType'] = $this->membershipTypeCreate();
1991 // Create admin-only membership status and get all statuses.
1992 $this->callAPISuccess('membership_status', 'create', ['name' => 'Admin', 'is_admin' => 1])['id'];
1994 // Create membership with incorrect statuses for the given dates and also some (pending, cancelled, admin override) which should not be updated.
1997 'start_date' => 'now',
1998 'end_date' => '+ 1 year',
1999 'initial_status' => 'Current',
2000 'expected_processed_status' => 'New',
2003 'start_date' => '- 6 month',
2004 'end_date' => '+ 6 month',
2005 'initial_status' => 'New',
2006 'expected_processed_status' => 'Current',
2009 'start_date' => '- 53 week',
2010 'end_date' => '-1 week',
2011 'initial_status' => 'Current',
2012 'expected_processed_status' => 'Grace',
2015 'start_date' => '- 16 month',
2016 'end_date' => '- 4 month',
2017 'initial_status' => 'Grace',
2018 'expected_processed_status' => 'Expired',
2021 'start_date' => 'now',
2022 'end_date' => '+ 1 year',
2023 'initial_status' => 'Pending',
2024 'expected_processed_status' => 'Pending',
2027 'start_date' => '- 6 month',
2028 'end_date' => '+ 6 month',
2029 'initial_status' => 'Cancelled',
2030 'expected_processed_status' => 'Cancelled',
2033 'start_date' => '- 16 month',
2034 'end_date' => '- 4 month',
2035 'initial_status' => 'Current',
2036 'is_override' => TRUE,
2037 'expected_processed_status' => 'Current',
2040 // @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.
2041 // 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.
2042 'start_date' => '- 16 month',
2043 'end_date' => '- 4 month',
2044 'initial_status' => 'Admin',
2045 'is_override' => TRUE,
2046 'expected_processed_status' => 'Admin',
2049 foreach ($memberships as $index => $membership) {
2050 $memberships[$index]['id'] = $this->createMembershipNeedingStatusProcessing($membership['start_date'], $membership['end_date'], $membership['initial_status'], $membership['is_override'] ??
FALSE);
2054 * Create membership type with inheritence and check processing of secondary memberships.
2056 $employerRelationshipId = $this->callAPISuccessGetValue('RelationshipType', [
2058 'name_b_a' => 'Employer Of',
2060 // Create membership type: inherited through employment.
2061 $membershipOrgId = $this->organizationCreate();
2063 'name' => 'Corporate Membership',
2064 'duration_unit' => 'year',
2065 'duration_interval' => 1,
2066 'period_type' => 'rolling',
2067 'member_of_contact_id' => $membershipOrgId,
2069 'financial_type_id' => 1,
2070 'relationship_type_id' => $employerRelationshipId,
2071 'relationship_direction' => 'b_a',
2074 $membershipTypeId = $this->callAPISuccess('membership_type', 'create', $params)['id'];
2076 // Create employer and first employee
2077 $employerId = $this->organizationCreate([], 1);
2078 $memberContactId = $this->individualCreate(['employer_id' => $employerId], 0);
2080 // Create inherited membership with incorrect status but dates implying status Expired.
2082 'contact_id' => $employerId,
2083 'membership_type_id' => $membershipTypeId,
2084 'source' => 'Test suite',
2085 'join_date' => date('Y-m-d', strtotime('now - 16 month')),
2086 'start_date' => date('Y-m-d', strtotime('now - 16 month')),
2087 'end_date' => date('Y-m-d', strtotime('now - 4 month')),
2088 // Intentionally incorrect status.
2089 'status_id' => 'Grace',
2090 // Don't calculate status.
2091 'skipStatusCal' => 1,
2093 $organizationMembershipID = $this->contactMembershipCreate($params);
2095 // Check that the employee inherited the membership and status.
2096 $expiredInheritedRelationship = $this->callAPISuccessGetSingle('membership', [
2097 'contact_id' => $memberContactId,
2098 'membership_type_id' => $membershipTypeId,
2100 $this->assertEquals($organizationMembershipID, $expiredInheritedRelationship['owner_membership_id']);
2101 $this->assertMembershipStatus('Grace', (int) $expiredInheritedRelationship['status_id']);
2103 // Reset static $relatedContactIds array in createRelatedMemberships(),
2104 // to avoid bug where inherited membership gets deleted.
2106 CRM_Member_BAO_Membership
::createRelatedMemberships($var, $var, TRUE);
2107 // Check that after running process_membership job, statuses are correct.
2108 $this->callAPISuccess('Job', 'process_membership', []);
2110 foreach ($memberships as $expectation) {
2111 $membership = $this->callAPISuccessGetSingle('membership', ['id' => $expectation['id']]);
2112 $this->assertMembershipStatus($expectation['expected_processed_status'], (int) $membership['status_id']);
2115 // Inherit Expired - should get updated.
2116 $membership = $this->callAPISuccess('membership', 'getsingle', ['id' => $expiredInheritedRelationship['id']]);
2117 $this->assertMembershipStatus('Expired', $membership['status_id']);
2121 * Test procesing membership where is_override is set to 0 rather than NULL
2123 * @throws \CRM_Core_Exception
2125 public function testProcessMembershipIsOverrideNotNullNot1either() {
2126 $membershipTypeId = $this->membershipTypeCreate();
2128 // Create admin-only membership status and get all statuses.
2129 $result = $this->callAPISuccess('membership_status', 'create', ['name' => 'Admin', 'is_admin' => 1, 'sequential' => 1]);
2130 $membershipStatusIdAdmin = $result['values'][0]['id'];
2131 $memStatus = CRM_Member_PseudoConstant
::membershipStatus();
2133 // Default params, which we'll expand on below.
2135 'membership_type_id' => $membershipTypeId,
2136 // Don't calculate status.
2137 'skipStatusCal' => 1,
2142 // Create membership with incorrect status but dates implying status Current.
2143 $params['contact_id'] = $this->individualCreate();
2144 $params['join_date'] = date('Y-m-d', strtotime('now - 6 month'));
2145 $params['start_date'] = date('Y-m-d', strtotime('now - 6 month'));
2146 $params['end_date'] = date('Y-m-d', strtotime('now + 6 month'));
2147 // Intentionally incorrect status.
2148 $params['status_id'] = 'New';
2149 $resultCurrent = $this->callAPISuccess('Membership', 'create', $params);
2150 // Ensure that is_override is set to 0 by doing through DB given API not seem to accept id
2151 CRM_Core_DAO
::executeQuery("Update civicrm_membership SET is_override = 0 WHERE id = %1", [1 => [$resultCurrent['id'], 'Positive']]);
2152 $this->assertEquals(array_search('New', $memStatus, TRUE), $resultCurrent['values'][0]['status_id']);
2153 $jobResult = $this->callAPISuccess('Job', 'process_membership', []);
2154 $this->assertEquals('Processed 1 membership records. Updated 1 records.', $jobResult['values']);
2155 $this->assertEquals(array_search('Current', $memStatus, TRUE), $this->callAPISuccess('Membership', 'get', ['id' => $resultCurrent['id']])['values'][$resultCurrent['id']]['status_id']);
2159 * @param string $expectedStatusName
2160 * @param int $actualStatusID
2162 protected function assertMembershipStatus(string $expectedStatusName, int $actualStatusID) {
2163 $this->assertEquals($expectedStatusName, CRM_Core_PseudoConstant
::getName('CRM_Member_BAO_Membership', 'status_id', $actualStatusID));
2167 * @param string $startDate
2168 * Date in strtotime format - e.g 'now', '+1 day'
2169 * @param string $endDate
2170 * Date in strtotime format - e.g 'now', '+1 day'
2171 * @param string $status
2173 * @param bool $isAdminOverride
2174 * Is administratively overridden (if so the status is fixed).
2178 * @throws \CRM_Core_Exception
2180 protected function createMembershipNeedingStatusProcessing(string $startDate, string $endDate, string $status, bool $isAdminOverride = FALSE): int {
2182 'membership_type_id' => $this->ids
['MembershipType'],
2183 // Don't calculate status.
2184 'skipStatusCal' => 1,
2188 $params['contact_id'] = $this->individualCreate();
2189 $params['join_date'] = date('Y-m-d', strtotime($startDate));
2190 $params['start_date'] = date('Y-m-d', strtotime($startDate));
2191 $params['end_date'] = date('Y-m-d', strtotime($endDate));
2192 $params['sequential'] = TRUE;
2193 $params['is_override'] = $isAdminOverride;
2194 // Intentionally incorrect status.
2195 $params['status_id'] = $status;
2196 $resultNew = $this->callAPISuccess('Membership', 'create', $params);
2197 $this->assertMembershipStatus($status, (int) $resultNew['values'][0]['status_id']);
2198 return (int) $resultNew['id'];
2202 * Shared set up for SMS reminder tests.
2206 * @throws \CRM_Core_Exception
2207 * @throws \CiviCRM_API3_Exception
2209 protected function setUpMembershipSMSReminders(): array {
2210 $membershipTypeID = $this->membershipTypeCreate();
2211 $this->membershipStatusCreate();
2213 $groupID = $this->groupCreate(['name' => 'Texan drawlers', 'title' => 'a...']);
2214 for ($i = 1; $i <= $createTotal; $i++
) {
2215 $contactID = $this->individualCreate();
2216 $this->callAPISuccess('Phone', 'create', [
2217 'contact_id' => $contactID,
2218 'phone' => '555 123 1234',
2219 'phone_type_id' => 'Mobile',
2220 'location_type_id' => 'Billing',
2223 $theChosenOneID = $contactID;
2226 $this->callAPISuccess('group_contact', 'create', [
2227 'contact_id' => $contactID,
2228 'status' => 'Added',
2229 'group_id' => $groupID,
2233 $this->callAPISuccess('membership', 'create', [
2234 'contact_id' => $contactID,
2235 'membership_type_id' => $membershipTypeID,
2236 'join_date' => 'now',
2237 'start_date' => '+ 1 day',
2241 $this->setupForSmsTests();
2242 $provider = civicrm_api3('SmsProvider', 'create', [
2243 'name' => 'CiviTestSMSProvider',
2248 'api_params' => 'a=1',
2249 'is_default' => '1',
2253 return [$membershipTypeID, $groupID, $theChosenOneID, $provider];