fix extra comma.
[civicrm-core.git] / tests / phpunit / CRM / Contact / Import / Parser / ContactTest.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This code 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 +--------------------------------------------------------------------+
10 */
11
12 /**
13 * @file
14 * File for the CRM_Contact_Imports_Parser_ContactTest class.
15 */
16
17 /**
18 * Test contact import parser.
19 *
20 * @package CiviCRM
21 * @group headless
22 */
23 class CRM_Contact_Import_Parser_ContactTest extends CiviUnitTestCase {
24 use CRMTraits_Custom_CustomDataTrait;
25
26 /**
27 * Main entity for the class.
28 *
29 * @var string
30 */
31 protected $entity = 'Contact';
32
33 /**
34 * Tear down after test.
35 *
36 * @throws \CRM_Core_Exception
37 */
38 public function tearDown(): void {
39 $this->quickCleanup(['civicrm_address', 'civicrm_phone', 'civicrm_email'], TRUE);
40 parent::tearDown();
41 }
42
43 /**
44 * Test that import parser will add contact with employee of relationship.
45 */
46 public function testImportParserWithEmployeeOfRelationship(): void {
47 $this->organizationCreate([
48 'organization_name' => 'Agileware',
49 'legal_name' => 'Agileware',
50 ]);
51 $contactImportValues = [
52 "first_name" => "Alok",
53 "last_name" => "Patel",
54 "Employee of" => "Agileware",
55 ];
56
57 $fields = array_keys($contactImportValues);
58 $values = array_values($contactImportValues);
59 $parser = new CRM_Contact_Import_Parser_Contact($fields, []);
60 $parser->_contactType = 'Individual';
61 $parser->init();
62 $this->mapRelationshipFields($fields, $parser->getAllFields());
63
64 $parser = new CRM_Contact_Import_Parser_Contact($fields, [], [], [], [
65 NULL,
66 NULL,
67 $fields[2],
68 ], [
69 NULL,
70 NULL,
71 "Organization",
72 ], [
73 NULL,
74 NULL,
75 "organization_name",
76 ], [], [], [], [], []);
77
78 $parser->_contactType = 'Individual';
79 $parser->_onDuplicate = CRM_Import_Parser::DUPLICATE_UPDATE;
80 $parser->init();
81
82 $this->assertEquals(CRM_Import_Parser::VALID, $parser->import(CRM_Import_Parser::DUPLICATE_UPDATE, $values), 'Return code from parser import was not as expected');
83 $this->callAPISuccess("Contact", "get", [
84 "first_name" => "Alok",
85 "last_name" => "Patel",
86 "organization_name" => "Agileware",
87 ]);
88 }
89
90 /**
91 * Test that import parser will not fail when same external_identifier found
92 * of deleted contact.
93 *
94 * @throws \CRM_Core_Exception
95 * @throws \CiviCRM_API3_Exception
96 */
97 public function testImportParserWithDeletedContactExternalIdentifier(): void {
98 $contactId = $this->individualCreate([
99 'external_identifier' => 'ext-1',
100 ]);
101 $this->callAPISuccess('Contact', 'delete', ['id' => $contactId]);
102 [$originalValues, $result] = $this->setUpBaseContact([
103 'external_identifier' => 'ext-1',
104 ]);
105 $originalValues['nick_name'] = 'Old Bill';
106 $this->runImport($originalValues, CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::VALID);
107 $originalValues['id'] = $result['id'];
108 $this->assertEquals('ext-1', $this->callAPISuccessGetValue('Contact', ['id' => $result['id'], 'return' => 'external_identifier']));
109 $this->callAPISuccessGetSingle('Contact', $originalValues);
110 }
111
112 /**
113 * Test import parser will update based on a rule match.
114 *
115 * In this case the contact has no external identifier.
116 *
117 * @throws \CRM_Core_Exception
118 */
119 public function testImportParserWithUpdateWithoutExternalIdentifier(): void {
120 [$originalValues, $result] = $this->setUpBaseContact();
121 $originalValues['nick_name'] = 'Old Bill';
122 $this->runImport($originalValues, CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::VALID);
123 $originalValues['id'] = $result['id'];
124 $this->assertEquals('Old Bill', $this->callAPISuccessGetValue('Contact', ['id' => $result['id'], 'return' => 'nick_name']));
125 $this->callAPISuccessGetSingle('Contact', $originalValues);
126 }
127
128 /**
129 * Test import parser will update contacts with an external identifier.
130 *
131 * This is the basic test where the identifier matches the import parameters.
132 *
133 * @throws \Exception
134 */
135 public function testImportParserWithUpdateWithExternalIdentifier() {
136 [$originalValues, $result] = $this->setUpBaseContact(['external_identifier' => 'windows']);
137
138 $this->assertEquals($result['id'], CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', 'windows', 'id', 'external_identifier', TRUE));
139 $this->assertEquals('windows', $result['external_identifier']);
140
141 $originalValues['nick_name'] = 'Old Bill';
142 $this->runImport($originalValues, CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::VALID);
143 $originalValues['id'] = $result['id'];
144
145 $this->assertEquals('Old Bill', $this->callAPISuccessGetValue('Contact', ['id' => $result['id'], 'return' => 'nick_name']));
146 $this->callAPISuccessGetSingle('Contact', $originalValues);
147 }
148
149 /**
150 * Test import parser will fallback to external identifier.
151 *
152 * In this case no primary match exists (e.g the details are not supplied) so it falls back on external identifier.
153 *
154 * @see https://issues.civicrm.org/jira/browse/CRM-17275
155 *
156 * @throws \Exception
157 */
158 public function testImportParserWithUpdateWithExternalIdentifierButNoPrimaryMatch() {
159 [$originalValues, $result] = $this->setUpBaseContact([
160 'external_identifier' => 'windows',
161 'email' => NULL,
162 ]);
163
164 $this->assertEquals('windows', $result['external_identifier']);
165
166 $originalValues['nick_name'] = 'Old Bill';
167 $this->runImport($originalValues, CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::VALID);
168 $originalValues['id'] = $result['id'];
169
170 $this->assertEquals('Old Bill', $this->callAPISuccessGetValue('Contact', ['id' => $result['id'], 'return' => 'nick_name']));
171 $this->callAPISuccessGetSingle('Contact', $originalValues);
172 }
173
174 /**
175 * Test import parser will fallback to external identifier.
176 *
177 * In this case no primary match exists (e.g the details are not supplied) so it falls back on external identifier.
178 *
179 * @see https://issues.civicrm.org/jira/browse/CRM-17275
180 *
181 * @throws \Exception
182 */
183 public function testImportParserWithUpdateWithContactID() {
184 [$originalValues, $result] = $this->setUpBaseContact([
185 'external_identifier' => '',
186 'email' => NULL,
187 ]);
188 $updateValues = ['id' => $result['id'], 'email' => 'bill@example.com'];
189 // This is some deep weirdness - this sets a flag for updatingBlankLocinfo - allowing input to be blanked
190 // (which IS a good thing but it's pretty weird & all to do with legacy profile stuff).
191 CRM_Core_Session::singleton()->set('authSrc', CRM_Core_Permission::AUTH_SRC_CHECKSUM);
192 $this->runImport($updateValues, CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::VALID, [NULL, 1]);
193 $originalValues['id'] = $result['id'];
194 $this->callAPISuccessGetSingle('Email', ['contact_id' => $originalValues['id'], 'is_primary' => 1]);
195 $this->callAPISuccessGetSingle('Contact', $originalValues);
196 }
197
198 /**
199 * Test that the import parser adds the external identifier where none is set.
200 *
201 * @throws \Exception
202 */
203 public function testImportParserWithUpdateWithNoExternalIdentifier() {
204 [$originalValues, $result] = $this->setUpBaseContact();
205 $originalValues['nick_name'] = 'Old Bill';
206 $originalValues['external_identifier'] = 'windows';
207 $this->runImport($originalValues, CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::VALID);
208 $originalValues['id'] = $result['id'];
209 $this->assertEquals('Old Bill', $this->callAPISuccessGetValue('Contact', ['id' => $result['id'], 'return' => 'nick_name']));
210 $this->callAPISuccessGetSingle('Contact', $originalValues);
211 }
212
213 /**
214 * Test that the import parser changes the external identifier when there is a dedupe match.
215 *
216 * @throws \Exception
217 */
218 public function testImportParserWithUpdateWithChangedExternalIdentifier() {
219 [$contactValues, $result] = $this->setUpBaseContact(['external_identifier' => 'windows']);
220 $contact_id = $result['id'];
221 $contactValues['nick_name'] = 'Old Bill';
222 $contactValues['external_identifier'] = 'android';
223 $this->runImport($contactValues, CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::VALID);
224 $contactValues['id'] = $contact_id;
225 $this->assertEquals('Old Bill', $this->callAPISuccessGetValue('Contact', ['id' => $contact_id, 'return' => 'nick_name']));
226 $this->callAPISuccessGetSingle('Contact', $contactValues);
227 }
228
229 /**
230 * Test that the import parser adds the address to the right location.
231 *
232 * @throws \Exception
233 */
234 public function testImportBillingAddress() {
235 [$contactValues] = $this->setUpBaseContact();
236 $contactValues['nick_name'] = 'Old Bill';
237 $contactValues['external_identifier'] = 'android';
238 $contactValues['street_address'] = 'Big Mansion';
239 $contactValues['phone'] = '911';
240 $this->runImport($contactValues, CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::VALID, [0 => NULL, 1 => NULL, 2 => NULL, 3 => NULL, 4 => NULL, 5 => 2, 6 => 2]);
241 $address = $this->callAPISuccessGetSingle('Address', ['street_address' => 'Big Mansion']);
242 $this->assertEquals(2, $address['location_type_id']);
243
244 $phone = $this->callAPISuccessGetSingle('Phone', ['phone' => '911']);
245 $this->assertEquals(2, $phone['location_type_id']);
246
247 $contact = $this->callAPISuccessGetSingle('Contact', $contactValues);
248 $this->callAPISuccess('Contact', 'delete', ['id' => $contact['id']]);
249 }
250
251 /**
252 * Test that the not-really-encouraged way of creating locations via contact.create doesn't mess up primaries.
253 */
254 public function testContactLocationBlockHandling() {
255 $id = $this->individualCreate([
256 'phone' => [
257 1 => [
258 'location_type_id' => 1,
259 'phone' => '987654321',
260 ],
261 2 => [
262 'location_type_id' => 2,
263 'phone' => '456-7890',
264 ],
265 ],
266 'im' => [
267 1 => [
268 'location_type_id' => 1,
269 'name' => 'bob',
270 ],
271 2 => [
272 'location_type_id' => 2,
273 'name' => 'fred',
274 ],
275 ],
276 'openid' => [
277 1 => [
278 'location_type_id' => 1,
279 'openid' => 'bob',
280 ],
281 2 => [
282 'location_type_id' => 2,
283 'openid' => 'fred',
284 ],
285 ],
286 'email' => [
287 1 => [
288 'location_type_id' => 1,
289 'email' => 'bob@example.com',
290 ],
291 2 => [
292 'location_type_id' => 2,
293 'email' => 'fred@example.com',
294 ],
295 ],
296 ]);
297 $phones = $this->callAPISuccess('Phone', 'get', ['contact_id' => $id])['values'];
298 $emails = $this->callAPISuccess('Email', 'get', ['contact_id' => $id])['values'];
299 $openIDs = $this->callAPISuccess('OpenID', 'get', ['contact_id' => $id])['values'];
300 $ims = $this->callAPISuccess('IM', 'get', ['contact_id' => $id])['values'];
301 $this->assertCount(2, $phones);
302 $this->assertCount(2, $emails);
303 $this->assertCount(2, $ims);
304 $this->assertCount(2, $openIDs);
305
306 $this->assertLocationValidity();
307 $this->callAPISuccess('Contact', 'create', [
308 'id' => $id,
309 // This is secret code for 'delete this phone'.
310 'updateBlankLocInfo' => TRUE,
311 'phone' => [
312 1 => [
313 'id' => key($phones),
314 ],
315 ],
316 'email' => [
317 1 => [
318 'id' => key($emails),
319 ],
320 ],
321 'im' => [
322 1 => [
323 'id' => key($ims),
324 ],
325 ],
326 'openid' => [
327 1 => [
328 'id' => key($openIDs),
329 ],
330 ],
331 ]);
332 $this->assertLocationValidity();
333 $this->callAPISuccessGetCount('Phone', ['contact_id' => $id], 1);
334 $this->callAPISuccessGetCount('Email', ['contact_id' => $id], 1);
335 $this->callAPISuccessGetCount('OpenID', ['contact_id' => $id], 1);
336 $this->callAPISuccessGetCount('IM', ['contact_id' => $id], 1);
337 }
338
339 /**
340 * Test that the import parser adds the address to the primary location.
341 *
342 * @throws \Exception
343 */
344 public function testImportPrimaryAddress() {
345 [$contactValues] = $this->setUpBaseContact();
346 $contactValues['nick_name'] = 'Old Bill';
347 $contactValues['external_identifier'] = 'android';
348 $contactValues['street_address'] = 'Big Mansion';
349 $contactValues['phone'] = 12334;
350 $this->runImport($contactValues, CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::VALID, [0 => NULL, 1 => NULL, 2 => 'Primary', 3 => NULL, 4 => NULL, 5 => 'Primary', 6 => 'Primary']);
351 $address = $this->callAPISuccessGetSingle('Address', ['street_address' => 'Big Mansion']);
352 $this->assertEquals(1, $address['location_type_id']);
353 $this->assertEquals(1, $address['is_primary']);
354
355 $phone = $this->callAPISuccessGetSingle('Phone', ['phone' => '12334']);
356 $this->assertEquals(1, $phone['location_type_id']);
357
358 $this->callAPISuccessGetSingle('Email', ['email' => 'bill.gates@microsoft.com']);
359
360 $contact = $this->callAPISuccessGetSingle('Contact', $contactValues);
361 $this->callAPISuccess('Contact', 'delete', ['id' => $contact['id']]);
362 }
363
364 /**
365 * Test that address location type id is ignored for dedupe purposes on import.
366 *
367 * @throws \Exception
368 */
369 public function testIgnoreLocationTypeId() {
370 // Create a rule that matches on last name and street address.
371 $rgid = $this->createRuleGroup()['id'];
372 $this->callAPISuccess('Rule', 'create', [
373 'dedupe_rule_group_id' => $rgid,
374 'rule_field' => 'last_name',
375 'rule_table' => 'civicrm_contact',
376 'rule_weight' => 4,
377 ]);
378 $this->callAPISuccess('Rule', 'create', [
379 'dedupe_rule_group_id' => $rgid,
380 'rule_field' => 'street_address',
381 'rule_table' => 'civicrm_address',
382 'rule_weight' => 4,
383 ]);
384 // Create a contact with an address of location_type_id 1.
385 $contact1Params = [
386 'contact_type' => 'Individual',
387 'first_name' => 'Original',
388 'last_name' => 'Smith',
389 ];
390 $contact1 = $this->callAPISuccess('Contact', 'create', $contact1Params);
391 $this->callAPISuccess('Address', 'create', [
392 'contact_id' => $contact1['id'],
393 'location_type_id' => 1,
394 'street_address' => 'Big Mansion',
395 ]);
396
397 $contactValues = [
398 'first_name' => 'New',
399 'last_name' => 'Smith',
400 'street_address' => 'Big Mansion',
401 ];
402
403 // We want to import with a location_type_id of 4.
404 $importLocationTypeId = '4';
405 $this->runImport($contactValues, CRM_Import_Parser::DUPLICATE_SKIP, CRM_Import_Parser::DUPLICATE, [0 => NULL, 1 => NULL, 2 => $importLocationTypeId], NULL, $rgid);
406 $address = $this->callAPISuccessGetSingle('Address', ['street_address' => 'Big Mansion']);
407 $this->assertEquals(1, $address['location_type_id']);
408 $contact = $this->callAPISuccessGetSingle('Contact', $contact1Params);
409 $this->callAPISuccess('Contact', 'delete', ['id' => $contact['id']]);
410 }
411
412 /**
413 * Test that address custom fields can be imported
414 * FIXME: Api4
415 *
416 * @throws \CRM_Core_Exception
417 */
418 public function testAddressWithCustomData() {
419 $ids = $this->entityCustomGroupWithSingleFieldCreate('Address', 'AddressTest.php');
420 [$contactValues] = $this->setUpBaseContact();
421 $contactValues['nick_name'] = 'Old Bill';
422 $contactValues['external_identifier'] = 'android';
423 $contactValues['street_address'] = 'Big Mansion';
424 $contactValues['custom_' . $ids['custom_field_id']] = 'Update';
425 $this->runImport($contactValues, CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::VALID, [0 => NULL, 1 => NULL, 2 => NULL, 3 => NULL, 4 => NULL, 5 => 'Primary', 6 => 'Primary']);
426 $address = $this->callAPISuccessGetSingle('Address', ['street_address' => 'Big Mansion', 'return' => 'custom_' . $ids['custom_field_id']]);
427 $this->assertEquals('Update', $address['custom_' . $ids['custom_field_id']]);
428 }
429
430 /**
431 * Test gender works when you specify the label.
432 *
433 * There is an expectation that you can import by label here.
434 *
435 * @throws \CRM_Core_Exception
436 */
437 public function testGenderLabel() {
438 $contactValues = [
439 'first_name' => 'Bill',
440 'last_name' => 'Gates',
441 'email' => 'bill.gates@microsoft.com',
442 'nick_name' => 'Billy-boy',
443 'gender_id' => 'Female',
444 ];
445 $this->runImport($contactValues, CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::VALID, [NULL, NULL, 'Primary', NULL, NULL]);
446 $this->callAPISuccessGetSingle('Contact', $contactValues);
447 }
448
449 /**
450 * Test prefix & suffix work when you specify the label.
451 *
452 * There is an expectation that you can import by label here.
453 *
454 * @throws \CRM_Core_Exception
455 * @throws \CiviCRM_API3_Exception
456 */
457 public function testPrefixLabel(): void {
458 $this->callAPISuccess('OptionValue', 'create', ['option_group_id' => 'individual_prefix', 'name' => 'new_one', 'label' => 'special', 'value' => 70]);
459 $mapping = [
460 ['name' => 'first_name', 'column_number' => 0],
461 ['name' => 'last_name', 'column_number' => 1],
462 ['name' => 'email', 'column_number' => 2, 'location_type_id' => CRM_Core_PseudoConstant::getKey('CRM_Core_BAO_Email', 'location_type_id', 'Home')],
463 ['name' => 'prefix_id', 'column_number' => 3],
464 ['name' => 'suffix_id', 'column_number' => 4],
465 ];
466 $processor = new CRM_Import_ImportProcessor();
467 $processor->setMappingFields($mapping);
468 $processor->setContactType('Individual');
469 $importer = $processor->getImporterObject();
470
471 $contactValues = [
472 'Bill',
473 'Gates',
474 'bill.gates@microsoft.com',
475 'special',
476 'III',
477 ];
478 $importer->import(CRM_Import_Parser::DUPLICATE_NOCHECK, $contactValues);
479
480 $contact = $this->callAPISuccessGetSingle('Contact', ['first_name' => 'Bill', 'prefix_id' => 'new_one', 'suffix_id' => 'III']);
481 $this->assertEquals('special Bill Gates III', $contact['display_name']);
482 }
483
484 /**
485 * Test that labels work for importing custom data.
486 *
487 * @throws \API_Exception
488 * @throws \CRM_Core_Exception
489 * @throws \CiviCRM_API3_Exception
490 */
491 public function testCustomDataLabel(): void {
492 $this->createCustomGroupWithFieldOfType([], 'select');
493 $contactValues = [
494 'first_name' => 'Bill',
495 'last_name' => 'Gates',
496 'email' => 'bill.gates@microsoft.com',
497 'nick_name' => 'Billy-boy',
498 $this->getCustomFieldName('select') => 'Yellow',
499 ];
500 $this->runImport($contactValues, CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::VALID, [NULL, NULL, 'Primary', NULL, NULL]);
501 $contact = $this->callAPISuccessGetSingle('Contact', array_merge($contactValues, ['return' => $this->getCustomFieldName('select')]));
502 $this->assertEquals('Y', $contact[$this->getCustomFieldName('select')]);
503 }
504
505 /**
506 * Test that names work for importing custom data.
507 *
508 * @throws \CRM_Core_Exception
509 */
510 public function testCustomDataName() {
511 $this->createCustomGroupWithFieldOfType([], 'select');
512 $contactValues = [
513 'first_name' => 'Bill',
514 'last_name' => 'Gates',
515 'email' => 'bill.gates@microsoft.com',
516 'nick_name' => 'Billy-boy',
517 $this->getCustomFieldName('select') => 'Y',
518 ];
519 $this->runImport($contactValues, CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::VALID, [NULL, NULL, 'Primary', NULL, NULL]);
520 $contact = $this->callAPISuccessGetSingle('Contact', array_merge($contactValues, ['return' => $this->getCustomFieldName('select')]));
521 $this->assertEquals('Y', $contact[$this->getCustomFieldName('select')]);
522 }
523
524 /**
525 * Test importing in the Preferred Language Field
526 *
527 * @throws \CRM_Core_Exception
528 */
529 public function testPreferredLanguageImport() {
530 $contactValues = [
531 'first_name' => 'Bill',
532 'last_name' => 'Gates',
533 'email' => 'bill.gates@microsoft.com',
534 'nick_name' => 'Billy-boy',
535 'preferred_language' => 'English (Australia)',
536 ];
537 $this->runImport($contactValues, CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::VALID, [NULL, NULL, 'Primary', NULL, NULL]);
538 }
539
540 /**
541 * Test that the import parser adds the address to the primary location.
542 *
543 * @throws \Exception
544 */
545 public function testImportDeceased() {
546 [$contactValues] = $this->setUpBaseContact();
547 CRM_Core_Session::singleton()->set("dateTypes", 1);
548 $contactValues['birth_date'] = '1910-12-17';
549 $contactValues['deceased_date'] = '2010-12-17';
550 $this->runImport($contactValues, CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::VALID);
551 $contact = $this->callAPISuccessGetSingle('Contact', $contactValues);
552 $this->assertEquals('1910-12-17', $contact['birth_date']);
553 $this->assertEquals('2010-12-17', $contact['deceased_date']);
554 $this->assertEquals(1, $contact['is_deceased']);
555 $this->callAPISuccess('Contact', 'delete', ['id' => $contact['id']]);
556 }
557
558 /**
559 * Test that the import parser adds the address to the primary location.
560 *
561 * @throws \Exception
562 */
563 public function testImportTwoAddressFirstPrimary() {
564 [$contactValues] = $this->setUpBaseContact();
565 $contactValues['nick_name'] = 'Old Bill';
566 $contactValues['external_identifier'] = 'android';
567 $contactValues['street_address'] = 'Big Mansion';
568 $contactValues['phone'] = 12334;
569 $fields = array_keys($contactValues);
570 $contactValues['street_address_2'] = 'Teeny Mansion';
571 $contactValues['phone_2'] = 4444;
572 $fields[] = 'street_address';
573 $fields[] = 'phone';
574 $this->runImport($contactValues, CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::VALID, [0 => NULL, 1 => NULL, 2 => NULL, 3 => NULL, 4 => NULL, 5 => 'Primary', 6 => 'Primary', 7 => 3, 8 => 3], $fields);
575 $contact = $this->callAPISuccessGetSingle('Contact', ['external_identifier' => 'android']);
576 $address = $this->callAPISuccess('Address', 'get', ['contact_id' => $contact['id'], 'sequential' => 1]);
577
578 $this->assertEquals(3, $address['values'][0]['location_type_id']);
579 $this->assertEquals(0, $address['values'][0]['is_primary']);
580 $this->assertEquals('Teeny Mansion', $address['values'][0]['street_address']);
581
582 $this->assertEquals(1, $address['values'][1]['location_type_id']);
583 $this->assertEquals(1, $address['values'][1]['is_primary']);
584 $this->assertEquals('Big Mansion', $address['values'][1]['street_address']);
585
586 $phone = $this->callAPISuccess('Phone', 'get', ['contact_id' => $contact['id'], 'sequential' => 1]);
587 $this->assertEquals(1, $phone['values'][0]['location_type_id']);
588 $this->assertEquals(1, $phone['values'][0]['is_primary']);
589 $this->assertEquals(12334, $phone['values'][0]['phone']);
590 $this->assertEquals(3, $phone['values'][1]['location_type_id']);
591 $this->assertEquals(0, $phone['values'][1]['is_primary']);
592 $this->assertEquals(4444, $phone['values'][1]['phone']);
593
594 $this->callAPISuccess('Contact', 'delete', ['id' => $contact['id']]);
595 }
596
597 /**
598 * Test importing 2 phones of different types.
599 *
600 * @throws \CRM_Core_Exception
601 * @throws \CiviCRM_API3_Exception
602 */
603 public function testImportTwoPhonesDifferentTypes() {
604 $processor = new CRM_Import_ImportProcessor();
605 $processor->setContactType('Individual');
606 $processor->setMappingFields(
607 [
608 ['name' => 'first_name'],
609 ['name' => 'last_name'],
610 ['name' => 'email'],
611 ['name' => 'phone', 'location_type_id' => 1, 'phone_type_id' => 2],
612 ['name' => 'phone', 'location_type_id' => 1, 'phone_type_id' => 1],
613 ]
614 );
615 $importer = $processor->getImporterObject();
616 $fields = ['First Name', 'new last name', 'bob@example.com', '1234', '5678'];
617 $importer->import(CRM_Import_Parser::DUPLICATE_UPDATE, $fields);
618 $contact = $this->callAPISuccessGetSingle('Contact', ['last_name' => 'new last name']);
619 $phones = $this->callAPISuccess('Phone', 'get', ['contact_id' => $contact['id']])['values'];
620 $this->assertCount(2, $phones);
621 }
622
623 /**
624 * Test that the import parser adds the address to the primary location.
625 *
626 * @throws \Exception
627 */
628 public function testImportTwoAddressSecondPrimary() {
629 [$contactValues] = $this->setUpBaseContact();
630 $contactValues['nick_name'] = 'Old Bill';
631 $contactValues['external_identifier'] = 'android';
632 $contactValues['street_address'] = 'Big Mansion';
633 $contactValues['phone'] = 12334;
634 $fields = array_keys($contactValues);
635 $contactValues['street_address_2'] = 'Teeny Mansion';
636 $contactValues['phone_2'] = 4444;
637 $fields[] = 'street_address';
638 $fields[] = 'phone';
639 $this->runImport($contactValues, CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::VALID, [0 => NULL, 1 => NULL, 2 => NULL, 3 => NULL, 4 => NULL, 5 => 3, 6 => 3, 7 => 'Primary', 8 => 'Primary'], $fields);
640 $contact = $this->callAPISuccessGetSingle('Contact', ['external_identifier' => 'android']);
641 $address = $this->callAPISuccess('Address', 'get', ['contact_id' => $contact['id'], 'sequential' => 1])['values'];
642
643 $this->assertEquals(1, $address[1]['location_type_id']);
644 $this->assertEquals(1, $address[1]['is_primary']);
645 $this->assertEquals('Teeny Mansion', $address[1]['street_address']);
646
647 $this->assertEquals(3, $address[0]['location_type_id']);
648 $this->assertEquals(0, $address[0]['is_primary']);
649 $this->assertEquals('Big Mansion', $address[0]['street_address']);
650
651 $phone = $this->callAPISuccess('Phone', 'get', ['contact_id' => $contact['id'], 'sequential' => 1, 'options' => ['sort' => 'is_primary DESC']])['values'];
652 $this->assertEquals(3, $phone[1]['location_type_id']);
653 $this->assertEquals(0, $phone[1]['is_primary']);
654 $this->assertEquals(12334, $phone[1]['phone']);
655 $this->assertEquals(1, $phone[0]['location_type_id']);
656 $this->assertEquals(1, $phone[0]['is_primary']);
657 $this->assertEquals(4444, $phone[0]['phone']);
658
659 $this->callAPISuccess('Contact', 'delete', ['id' => $contact['id']]);
660 }
661
662 /**
663 * Test that the import parser updates the address on the existing primary location.
664 *
665 * @throws \Exception
666 */
667 public function testImportPrimaryAddressUpdate() {
668 [$contactValues] = $this->setUpBaseContact(['external_identifier' => 'android']);
669 $contactValues['email'] = 'melinda.gates@microsoft.com';
670 $contactValues['phone'] = '98765';
671 $contactValues['external_identifier'] = 'android';
672 $contactValues['street_address'] = 'Big Mansion';
673 $contactValues['city'] = 'Big City';
674 $contactID = $this->callAPISuccessGetValue('Contact', ['external_identifier' => 'android', 'return' => 'id']);
675 $originalAddress = $this->callAPISuccess('Address', 'create', ['location_type_id' => 2, 'street_address' => 'small house', 'contact_id' => $contactID]);
676 $originalPhone = $this->callAPISuccess('phone', 'create', ['location_type_id' => 2, 'phone' => '1234', 'contact_id' => $contactID]);
677 $this->runImport($contactValues, CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::VALID, [0 => NULL, 1 => NULL, 2 => 'Primary', 3 => NULL, 4 => NULL, 5 => 'Primary', 6 => 'Primary', 7 => 'Primary']);
678 $phone = $this->callAPISuccessGetSingle('Phone', ['phone' => '98765']);
679 $this->assertEquals(2, $phone['location_type_id']);
680 $this->assertEquals($originalPhone['id'], $phone['id']);
681 $email = $this->callAPISuccess('Email', 'getsingle', ['contact_id' => $contactID]);
682 $address = $this->callAPISuccessGetSingle('Address', ['street_address' => 'Big Mansion']);
683 $this->assertEquals(2, $address['location_type_id']);
684 $this->assertEquals($originalAddress['id'], $address['id']);
685 $this->assertEquals('Big City', $address['city']);
686 $this->callAPISuccessGetSingle('Contact', $contactValues);
687 }
688
689 /**
690 * Test the determination of whether a custom field is valid.
691 */
692 public function testCustomFieldValidation(): void {
693 $errorMessage = [];
694 $customGroup = $this->customGroupCreate([
695 'extends' => 'Contact',
696 'title' => 'ABC',
697 ]);
698 $customField = $this->customFieldOptionValueCreate($customGroup, 'fieldABC', ['html_type' => 'Select', 'serialize' => 1]);
699 $params = [
700 'custom_' . $customField['id'] => 'Label1|Label2',
701 ];
702 CRM_Contact_Import_Parser_Contact::isErrorInCustomData($params, $errorMessage);
703 $this->assertEquals([], $errorMessage);
704 }
705
706 /**
707 * Test that setting duplicate action to fill doesn't blow away data
708 * that exists, but does fill in where it's empty.
709 *
710 * @throw \Exception
711 */
712 public function testImportFill() {
713 // Create a custom field group for testing.
714 $this->createCustomGroup([
715 'title' => 'importFillGroup',
716 'extends' => 'Individual',
717 'is_active' => TRUE,
718 ]);
719 $customGroupID = $this->ids['CustomGroup']['importFillGroup'];
720
721 // Add two custom fields.
722 $api_params = [
723 'custom_group_id' => $customGroupID,
724 'label' => 'importFillField1',
725 'html_type' => 'Select',
726 'data_type' => 'String',
727 'option_values' => [
728 'foo' => 'Foo',
729 'bar' => 'Bar',
730 ],
731 ];
732 $result = $this->callAPISuccess('custom_field', 'create', $api_params);
733 $customField1 = $result['id'];
734
735 $api_params = [
736 'custom_group_id' => $customGroupID,
737 'label' => 'importFillField2',
738 'html_type' => 'Select',
739 'data_type' => 'String',
740 'option_values' => [
741 'baz' => 'Baz',
742 'boo' => 'Boo',
743 ],
744 ];
745 $result = $this->callAPISuccess('custom_field', 'create', $api_params);
746 $customField2 = $result['id'];
747
748 // Now set up values.
749 $original_gender = 'Male';
750 $original_custom1 = 'foo';
751 $original_email = 'test-import-fill@example.org';
752
753 $import_gender = 'Female';
754 $import_custom1 = 'bar';
755 $import_job_title = 'Chief data importer';
756 $import_custom2 = 'baz';
757
758 // Create contact with both one known core field and one custom
759 // field filled in.
760 $api_params = [
761 'contact_type' => 'Individual',
762 'email' => $original_email,
763 'gender' => $original_gender,
764 'custom_' . $customField1 => $original_custom1,
765 ];
766 $result = $this->callAPISuccess('contact', 'create', $api_params);
767 $contact_id = $result['id'];
768
769 // Run an import.
770 $import = [
771 'email' => $original_email,
772 'gender_id' => $import_gender,
773 'custom_' . $customField1 => $import_custom1,
774 'job_title' => $import_job_title,
775 'custom_' . $customField2 => $import_custom2,
776 ];
777
778 $this->runImport($import, CRM_Import_Parser::DUPLICATE_FILL, CRM_Import_Parser::VALID);
779
780 $expected = [
781 'gender' => $original_gender,
782 'custom_' . $customField1 => $original_custom1,
783 'job_title' => $import_job_title,
784 'custom_' . $customField2 => $import_custom2,
785 ];
786
787 $params = [
788 'id' => $contact_id,
789 'return' => [
790 'gender',
791 'custom_' . $customField1,
792 'job_title',
793 'custom_' . $customField2,
794 ],
795 ];
796 $result = civicrm_api3('Contact', 'get', $params);
797 $values = array_pop($result['values']);
798 foreach ($expected as $field => $expected_value) {
799 if (!isset($values[$field])) {
800 $given_value = NULL;
801 }
802 else {
803 $given_value = $values[$field];
804 }
805 // We expect:
806 // gender: Male
807 // job_title: Chief Data Importer
808 // importFillField1: foo
809 // importFillField2: baz
810 $this->assertEquals($expected_value, $given_value, "$field properly handled during Fill import");
811 }
812 }
813
814 /**
815 * CRM-19888 default country should be used if ambigous.
816 *
817 * @throws \CRM_Core_Exception
818 */
819 public function testImportAmbiguousStateCountry(): void {
820 $this->callAPISuccess('Setting', 'create', ['defaultContactCountry' => 1228]);
821 $countries = CRM_Core_PseudoConstant::country(FALSE, FALSE);
822 $this->callAPISuccess('Setting', 'create', ['countryLimit' => [array_search('United States', $countries), array_search('Guyana', $countries), array_search('Netherlands', $countries)]]);
823 $this->callAPISuccess('Setting', 'create', ['provinceLimit' => [array_search('United States', $countries), array_search('Guyana', $countries), array_search('Netherlands', $countries)]]);
824 $mapper = [0 => NULL, 1 => NULL, 2 => 'Primary', 3 => NULL];
825 [$contactValues] = $this->setUpBaseContact();
826 $fields = array_keys($contactValues);
827 $addressValues = [
828 'street_address' => 'PO Box 2716',
829 'city' => 'Midway',
830 'state_province' => 'UT',
831 'postal_code' => 84049,
832 'country' => 'United States',
833 ];
834 $locationTypes = $this->callAPISuccess('Address', 'getoptions', ['field' => 'location_type_id']);
835 $locationTypes = $locationTypes['values'];
836 foreach ($addressValues as $field => $value) {
837 $contactValues['home_' . $field] = $value;
838 $mapper[] = array_search('Home', $locationTypes);
839 $contactValues['work_' . $field] = $value;
840 $mapper[] = array_search('Work', $locationTypes);
841 $fields[] = $field;
842 $fields[] = $field;
843 }
844 $contactValues['work_country'] = '';
845
846 $this->runImport($contactValues, CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::VALID, $mapper, $fields);
847 $addresses = $this->callAPISuccess('Address', 'get', ['contact_id' => ['>' => 2], 'sequential' => 1]);
848 $this->assertEquals(2, $addresses['count']);
849 $this->assertEquals(array_search('United States', $countries), $addresses['values'][0]['country_id']);
850 $this->assertEquals(array_search('United States', $countries), $addresses['values'][1]['country_id']);
851 }
852
853 /**
854 * Test importing fields with various options.
855 *
856 * Ensure we can import multiple preferred_communication_methods, single
857 * gender, and single preferred language using both labels and values.
858 *
859 * @throws \CRM_Core_Exception @throws \CiviCRM_API3_Exception
860 */
861 public function testImportFieldsWithVariousOptions() {
862 $processor = new CRM_Import_ImportProcessor();
863 $processor->setContactType('Individual');
864 $processor->setMappingFields(
865 [
866 ['name' => 'first_name'],
867 ['name' => 'last_name'],
868 ['name' => 'preferred_communication_method'],
869 ['name' => 'gender_id'],
870 ['name' => 'preferred_language'],
871 ]
872 );
873 $importer = $processor->getImporterObject();
874 $fields = ['Ima', 'Texter', "SMS,Phone", "Female", "Danish"];
875 $importer->import(CRM_Import_Parser::DUPLICATE_NOCHECK, $fields);
876 $contact = $this->callAPISuccessGetSingle('Contact', ['last_name' => 'Texter']);
877
878 $this->assertEquals([4, 1], $contact['preferred_communication_method'], "Import multiple preferred communication methods using labels.");
879 $this->assertEquals(1, $contact['gender_id'], "Import gender with label.");
880 $this->assertEquals('da_DK', $contact['preferred_language'], "Import preferred language with label.");
881
882 $importer = $processor->getImporterObject();
883 $fields = ['Ima', 'Texter', "4,1", "1", "da_DK"];
884 $importer->import(CRM_Import_Parser::DUPLICATE_NOCHECK, $fields);
885 $contact = $this->callAPISuccessGetSingle('Contact', ['last_name' => 'Texter']);
886
887 $this->assertEquals([4, 1], $contact['preferred_communication_method'], "Import multiple preferred communication methods using values.");
888 $this->assertEquals(1, $contact['gender_id'], "Import gender with id.");
889 $this->assertEquals('da_DK', $contact['preferred_language'], "Import preferred language with value.");
890 }
891
892 /**
893 * Run the import parser.
894 *
895 * @param array $originalValues
896 *
897 * @param int $onDuplicateAction
898 * @param int $expectedResult
899 * @param array|null $mapperLocType
900 * Array of location types that map to the input arrays.
901 * @param array|null $fields
902 * Array of field names. Will be calculated from $originalValues if not passed in, but
903 * that method does not cope with duplicates.
904 * @param int|null $ruleGroupId
905 * To test against a specific dedupe rule group, pass its ID as this argument.
906 *
907 * @throws \CRM_Core_Exception
908 * @throws \CiviCRM_API3_Exception
909 */
910 protected function runImport(array $originalValues, $onDuplicateAction, $expectedResult, $mapperLocType = [], $fields = NULL, int $ruleGroupId = NULL): void {
911 if (!$fields) {
912 $fields = array_keys($originalValues);
913 }
914 $values = array_values($originalValues);
915 $parser = new CRM_Contact_Import_Parser_Contact($fields, $mapperLocType);
916 $parser->_contactType = 'Individual';
917 $parser->_dedupeRuleGroupID = $ruleGroupId;
918 $parser->_onDuplicate = $onDuplicateAction;
919 $parser->init();
920 $this->assertEquals($expectedResult, $parser->import($onDuplicateAction, $values), 'Return code from parser import was not as expected');
921 }
922
923 /**
924 * @param array $fields Array of fields to be imported
925 * @param array $allfields Array of all fields which can be part of import
926 */
927 private function mapRelationshipFields(&$fields, $allfields) {
928 foreach ($allfields as $key => $fieldtocheck) {
929 $elementIndex = array_search($fieldtocheck->_title, $fields);
930 if ($elementIndex !== FALSE) {
931 $fields[$elementIndex] = $key;
932 }
933 }
934 }
935
936 /**
937 * Set up the underlying contact.
938 *
939 * @param array $params
940 * Optional extra parameters to set.
941 *
942 * @return array
943 * @throws \CRM_Core_Exception
944 */
945 protected function setUpBaseContact($params = []) {
946 $originalValues = array_merge([
947 'first_name' => 'Bill',
948 'last_name' => 'Gates',
949 'email' => 'bill.gates@microsoft.com',
950 'nick_name' => 'Billy-boy',
951 ], $params);
952 $this->runImport($originalValues, CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::VALID);
953 $result = $this->callAPISuccessGetSingle('Contact', $originalValues);
954 return [$originalValues, $result];
955 }
956
957 }