3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 +--------------------------------------------------------------------+
14 * File for the CRM_Contact_Imports_Parser_ContactTest class.
17 use Civi\Api4\Address
;
18 use Civi\Api4\Contact
;
19 use Civi\Api4\ContactType
;
20 use Civi\Api4\LocationType
;
21 use Civi\Api4\RelationshipType
;
22 use Civi\Api4\UserJob
;
25 * Test contact import parser.
30 class CRM_Contact_Import_Parser_ContactTest
extends CiviUnitTestCase
{
31 use CRMTraits_Custom_CustomDataTrait
;
34 * Main entity for the class.
38 protected $entity = 'Contact';
41 * Tear down after test.
43 public function tearDown(): void
{
44 $this->quickCleanup(['civicrm_address', 'civicrm_phone', 'civicrm_email', 'civicrm_user_job', 'civicrm_relationship'], TRUE);
45 RelationshipType
::delete()->addWhere('name_a_b', '=', 'Dad to')->execute();
46 ContactType
::delete()->addWhere('name', '=', 'baby')->execute();
51 * Test that import parser will add contact with employee of relationship.
53 * @throws \API_Exception
54 * @throws \CRM_Core_Exception
55 * @throws \CiviCRM_API3_Exception
56 * @throws \Civi\API\Exception\UnauthorizedException
58 public function testImportParserWithEmployeeOfRelationship(): void
{
59 $this->organizationCreate([
60 'organization_name' => 'Agileware',
61 'legal_name' => 'Agileware',
63 $contactImportValues = [
64 'first_name' => 'Alok',
65 'last_name' => 'Patel',
66 'Employee of' => 'Agileware',
69 $fields = array_keys($contactImportValues);
70 $values = array_values($contactImportValues);
71 $userJobID = $this->getUserJobID([
72 'mapper' => [['first_name'], ['last_name'], ['5_a_b', 'organization_name']],
75 $parser = new CRM_Contact_Import_Parser_Contact($fields);
76 $parser->setUserJobID($userJobID);
77 $parser->_onDuplicate
= CRM_Import_Parser
::DUPLICATE_UPDATE
;
80 $this->assertEquals(CRM_Import_Parser
::VALID
, $parser->import(CRM_Import_Parser
::DUPLICATE_UPDATE
, $values), 'Return code from parser import was not as expected');
81 $this->callAPISuccess('Contact', 'get', [
82 'first_name' => 'Alok',
83 'last_name' => 'Patel',
84 'organization_name' => 'Agileware',
89 * Test that import parser will not fail when same external_identifier found
92 * @throws \API_Exception
93 * @throws \CRM_Core_Exception
94 * @throws \CiviCRM_API3_Exception
96 public function testImportParserWithDeletedContactExternalIdentifier(): void
{
97 $contactId = $this->individualCreate([
98 'external_identifier' => 'ext-1',
100 $this->callAPISuccess('Contact', 'delete', ['id' => $contactId]);
101 [$originalValues, $result] = $this->setUpBaseContact([
102 'external_identifier' => 'ext-1',
104 $originalValues['nick_name'] = 'Old Bill';
105 $this->runImport($originalValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
);
106 $originalValues['id'] = $result['id'];
107 $this->assertEquals('ext-1', $this->callAPISuccessGetValue('Contact', ['id' => $result['id'], 'return' => 'external_identifier']));
108 $this->callAPISuccessGetSingle('Contact', $originalValues);
112 * Test import parser will update based on a rule match.
114 * In this case the contact has no external identifier.
116 * @throws \API_Exception
117 * @throws \CRM_Core_Exception
118 * @throws \CiviCRM_API3_Exception
120 public function testImportParserWithUpdateWithoutExternalIdentifier(): void
{
121 [$originalValues, $result] = $this->setUpBaseContact();
122 $originalValues['nick_name'] = 'Old Bill';
123 $this->runImport($originalValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
);
124 $originalValues['id'] = $result['id'];
125 $this->assertEquals('Old Bill', $this->callAPISuccessGetValue('Contact', ['id' => $result['id'], 'return' => 'nick_name']));
126 $this->callAPISuccessGetSingle('Contact', $originalValues);
130 * Test import parser will update based on a custom rule match.
132 * In this case the contact has no external identifier.
134 * @throws \API_Exception
135 * @throws \CRM_Core_Exception
136 * @throws \CiviCRM_API3_Exception
138 public function testImportParserWithUpdateWithCustomRule(): void
{
139 $this->createCustomGroupWithFieldsOfAllTypes();
141 $ruleGroup = $this->callAPISuccess('RuleGroup', 'create', [
142 'contact_type' => 'Individual',
145 'name' => 'TestRule',
146 'title' => 'TestRule',
149 $this->callAPISuccess('Rule', 'create', [
150 'dedupe_rule_group_id' => $ruleGroup['id'],
151 'rule_table' => $this->getCustomGroupTable(),
153 'rule_field' => $this->getCustomFieldColumnName('text'),
157 $this->getCustomFieldName('select_string') => 'Yellow',
158 $this->getCustomFieldName('text') => 'Duplicate',
161 [$originalValues, $result] = $this->setUpBaseContact($extra);
164 'first_name' => 'Tim',
165 'last_name' => 'Cook',
166 'email' => 'tim.cook@apple.com',
167 'nick_name' => 'Steve',
168 $this->getCustomFieldName('select_string') => 'Red',
169 $this->getCustomFieldName('text') => 'Duplicate',
172 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
, [], NULL, $ruleGroup['id']);
173 $contactValues['id'] = $result['id'];
174 $this->assertEquals('R', $this->callAPISuccessGetValue('Contact', ['id' => $result['id'], 'return' => $this->getCustomFieldName('select_string')]));
175 $this->callAPISuccessGetSingle('Contact', $contactValues);
177 $foundDupes = CRM_Dedupe_Finder
::dupes($ruleGroup['id']);
178 $this->assertCount(0, $foundDupes);
182 * Test import parser will update based on a custom rule match.
184 * In this case the contact has no external identifier.
186 * @throws \API_Exception
187 * @throws \CRM_Core_Exception
188 * @throws \CiviCRM_API3_Exception
190 public function testImportParserWithUpdateWithCustomRuleNoExternalIDMatch(): void
{
191 $this->createCustomGroupWithFieldsOfAllTypes();
193 $ruleGroup = $this->callAPISuccess('RuleGroup', 'create', [
194 'contact_type' => 'Individual',
197 'name' => 'TestRule',
198 'title' => 'TestRule',
201 $this->callAPISuccess('Rule', 'create', [
202 'dedupe_rule_group_id' => $ruleGroup['id'],
203 'rule_table' => $this->getCustomGroupTable(),
205 'rule_field' => $this->getCustomFieldColumnName('text'),
209 $this->getCustomFieldName('select_string') => 'Yellow',
210 $this->getCustomFieldName('text') => 'Duplicate',
211 'external_identifier' => 'ext-2',
214 [$originalValues, $result] = $this->setUpBaseContact($extra);
217 'first_name' => 'Tim',
218 'last_name' => 'Cook',
219 'email' => 'tim.cook@apple.com',
220 'nick_name' => 'Steve',
221 'external_identifier' => 'ext-1',
222 $this->getCustomFieldName('select_string') => 'Red',
223 $this->getCustomFieldName('text') => 'Duplicate',
226 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
, [], NULL, $ruleGroup['id']);
227 $contactValues['id'] = $result['id'];
228 $this->assertEquals('R', $this->callAPISuccessGetValue('Contact', ['id' => $result['id'], 'return' => $this->getCustomFieldName('select_string')]));
229 $this->callAPISuccessGetSingle('Contact', $contactValues);
231 $foundDupes = CRM_Dedupe_Finder
::dupes($ruleGroup['id']);
232 $this->assertCount(0, $foundDupes);
236 * Test import parser will update contacts with an external identifier.
238 * This is the basic test where the identifier matches the import parameters.
242 public function testImportParserWithUpdateWithExternalIdentifier(): void
{
243 [$originalValues, $result] = $this->setUpBaseContact(['external_identifier' => 'windows']);
245 $this->assertEquals($result['id'], CRM_Core_DAO
::getFieldValue('CRM_Contact_DAO_Contact', 'windows', 'id', 'external_identifier', TRUE));
246 $this->assertEquals('windows', $result['external_identifier']);
248 $originalValues['nick_name'] = 'Old Bill';
249 $this->runImport($originalValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
);
250 $originalValues['id'] = $result['id'];
252 $this->assertEquals('Old Bill', $this->callAPISuccessGetValue('Contact', ['id' => $result['id'], 'return' => 'nick_name']));
253 $this->callAPISuccessGetSingle('Contact', $originalValues);
257 * Test updating an existing contact with external_identifier match but subtype mismatch.
259 * The subtype is updated, as there is no conflicting contact data.
263 public function testImportParserWithUpdateWithExternalIdentifierSubtypeChange(): void
{
264 $contactID = $this->individualCreate(['external_identifier' => 'billy', 'first_name' => 'William', 'contact_sub_type' => 'Parent']);
266 'external_identifier' => 'billy',
267 'nick_name' => 'Old Bill',
268 'contact_sub_type' => 'Staff',
269 ], CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
);
270 $contact = $this->callAPISuccessGetSingle('Contact', ['id' => $contactID]);
271 $this->assertEquals('Old Bill', $contact['nick_name']);
272 $this->assertEquals('William', $contact['first_name']);
273 $this->assertEquals('billy', $contact['external_identifier']);
274 $this->assertEquals(['Staff'], $contact['contact_sub_type']);
278 * Test updating an existing contact with external_identifier match but subtype mismatch.
280 * The subtype is not updated, as there is conflicting contact data.
284 public function testImportParserUpdateWithExternalIdentifierSubtypeChangeFail(): void
{
285 $contactID = $this->individualCreate(['external_identifier' => 'billy', 'first_name' => 'William', 'contact_sub_type' => 'Parent']);
286 $this->addChild($contactID);
289 'external_identifier' => 'billy',
290 'nick_name' => 'Old Bill',
291 'contact_sub_type' => 'Staff',
292 ], CRM_Import_Parser
::DUPLICATE_UPDATE
, NULL);
293 $contact = $this->callAPISuccessGetSingle('Contact', ['id' => $contactID]);
294 $this->assertEquals('', $contact['nick_name']);
295 $this->assertEquals(['Parent'], $contact['contact_sub_type']);
299 * Test updating an existing contact with external_identifier match but subtype mismatch.
303 public function testImportParserWithUpdateWithTypeMismatch(): void
{
304 $contactID = $this->organizationCreate(['external_identifier' => 'billy']);
306 'external_identifier' => 'billy',
307 'nick_name' => 'Old Bill',
308 ], CRM_Import_Parser
::DUPLICATE_UPDATE
, FALSE);
309 $contact = $this->callAPISuccessGetSingle('Contact', ['id' => $contactID]);
310 $this->assertEquals('', $contact['nick_name']);
311 $this->assertEquals('billy', $contact['external_identifier']);
312 $this->assertEquals('Organization', $contact['contact_type']);
316 'nick_name' => 'Old Bill',
317 ], CRM_Import_Parser
::DUPLICATE_UPDATE
, FALSE);
318 $contact = $this->callAPISuccessGetSingle('Contact', ['id' => $contactID]);
319 $this->assertEquals('', $contact['nick_name']);
320 $this->assertEquals('billy', $contact['external_identifier']);
321 $this->assertEquals('Organization', $contact['contact_type']);
326 * Test import parser will fallback to external identifier.
328 * In this case no primary match exists (e.g the details are not supplied) so it falls back on external identifier.
330 * @see https://issues.civicrm.org/jira/browse/CRM-17275
334 public function testImportParserWithUpdateWithExternalIdentifierButNoPrimaryMatch(): void
{
335 [$originalValues, $result] = $this->setUpBaseContact([
336 'external_identifier' => 'windows',
340 $this->assertEquals('windows', $result['external_identifier']);
342 $originalValues['nick_name'] = 'Old Bill';
343 $this->runImport($originalValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
);
344 $originalValues['id'] = $result['id'];
346 $this->assertEquals('Old Bill', $this->callAPISuccessGetValue('Contact', ['id' => $result['id'], 'return' => 'nick_name']));
347 $this->callAPISuccessGetSingle('Contact', $originalValues);
351 * Test import parser will fallback to external identifier.
353 * In this case no primary match exists (e.g the details are not supplied) so it falls back on external identifier.
355 * @see https://issues.civicrm.org/jira/browse/CRM-17275
359 public function testImportParserWithUpdateWithContactID(): void
{
360 [$originalValues, $result] = $this->setUpBaseContact([
361 'external_identifier' => '',
364 $updateValues = ['id' => $result['id'], 'email' => 'bill@example.com'];
365 // This is some deep weirdness - this sets a flag for updatingBlankLocinfo - allowing input to be blanked
366 // (which IS a good thing but it's pretty weird & all to do with legacy profile stuff).
367 CRM_Core_Session
::singleton()->set('authSrc', CRM_Core_Permission
::AUTH_SRC_CHECKSUM
);
368 $this->runImport($updateValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
, [NULL, 1]);
369 $originalValues['id'] = $result['id'];
370 $this->callAPISuccessGetSingle('Email', ['contact_id' => $originalValues['id'], 'is_primary' => 1]);
371 $this->callAPISuccessGetSingle('Contact', $originalValues);
375 * Test that the import parser adds the external identifier where none is set.
379 public function testImportParserWithUpdateWithNoExternalIdentifier(): void
{
380 [$originalValues, $result] = $this->setUpBaseContact();
381 $originalValues['nick_name'] = 'Old Bill';
382 $originalValues['external_identifier'] = 'windows';
383 $this->runImport($originalValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
);
384 $originalValues['id'] = $result['id'];
385 $this->assertEquals('Old Bill', $this->callAPISuccessGetValue('Contact', ['id' => $result['id'], 'return' => 'nick_name']));
386 $this->callAPISuccessGetSingle('Contact', $originalValues);
390 * Test that the import parser changes the external identifier when there is a dedupe match.
394 public function testImportParserWithUpdateWithChangedExternalIdentifier() {
395 [$contactValues, $result] = $this->setUpBaseContact(['external_identifier' => 'windows']);
396 $contact_id = $result['id'];
397 $contactValues['nick_name'] = 'Old Bill';
398 $contactValues['external_identifier'] = 'android';
399 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
);
400 $contactValues['id'] = $contact_id;
401 $this->assertEquals('Old Bill', $this->callAPISuccessGetValue('Contact', ['id' => $contact_id, 'return' => 'nick_name']));
402 $this->callAPISuccessGetSingle('Contact', $contactValues);
406 * Test that the import parser adds the address to the right location.
408 * @throws \API_Exception
409 * @throws \CRM_Core_Exception
410 * @throws \CiviCRM_API3_Exception
412 public function testImportBillingAddress(): void
{
413 [$contactValues] = $this->setUpBaseContact();
414 $contactValues['nick_name'] = 'Old Bill';
415 $contactValues['external_identifier'] = 'android';
416 $contactValues['street_address'] = 'Big Mansion';
417 $contactValues['phone'] = '911';
418 $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]);
419 $address = $this->callAPISuccessGetSingle('Address', ['street_address' => 'Big Mansion']);
420 $this->assertEquals(2, $address['location_type_id']);
422 $phone = $this->callAPISuccessGetSingle('Phone', ['phone' => '911']);
423 $this->assertEquals(2, $phone['location_type_id']);
425 $contact = $this->callAPISuccessGetSingle('Contact', $contactValues);
426 $this->callAPISuccess('Contact', 'delete', ['id' => $contact['id']]);
430 * Test that the not-really-encouraged way of creating locations via contact.create doesn't mess up primaries.
432 public function testContactLocationBlockHandling() {
433 $id = $this->individualCreate([
436 'location_type_id' => 1,
437 'phone' => '987654321',
440 'location_type_id' => 2,
441 'phone' => '456-7890',
446 'location_type_id' => 1,
450 'location_type_id' => 2,
456 'location_type_id' => 1,
460 'location_type_id' => 2,
466 'location_type_id' => 1,
467 'email' => 'bob@example.com',
470 'location_type_id' => 2,
471 'email' => 'fred@example.com',
475 $phones = $this->callAPISuccess('Phone', 'get', ['contact_id' => $id])['values'];
476 $emails = $this->callAPISuccess('Email', 'get', ['contact_id' => $id])['values'];
477 $openIDs = $this->callAPISuccess('OpenID', 'get', ['contact_id' => $id])['values'];
478 $ims = $this->callAPISuccess('IM', 'get', ['contact_id' => $id])['values'];
479 $this->assertCount(2, $phones);
480 $this->assertCount(2, $emails);
481 $this->assertCount(2, $ims);
482 $this->assertCount(2, $openIDs);
484 $this->assertLocationValidity();
485 $this->callAPISuccess('Contact', 'create', [
487 // This is secret code for 'delete this phone'.
488 'updateBlankLocInfo' => TRUE,
491 'id' => key($phones),
496 'id' => key($emails),
506 'id' => key($openIDs),
510 $this->assertLocationValidity();
511 $this->callAPISuccessGetCount('Phone', ['contact_id' => $id], 1);
512 $this->callAPISuccessGetCount('Email', ['contact_id' => $id], 1);
513 $this->callAPISuccessGetCount('OpenID', ['contact_id' => $id], 1);
514 $this->callAPISuccessGetCount('IM', ['contact_id' => $id], 1);
518 * Test that the import parser adds the address to the primary location.
522 public function testImportPrimaryAddress() {
523 [$contactValues] = $this->setUpBaseContact();
524 $contactValues['nick_name'] = 'Old Bill';
525 $contactValues['external_identifier'] = 'android';
526 $contactValues['street_address'] = 'Big Mansion';
527 $contactValues['phone'] = 12334;
528 $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']);
529 $address = $this->callAPISuccessGetSingle('Address', ['street_address' => 'Big Mansion']);
530 $this->assertEquals(1, $address['location_type_id']);
531 $this->assertEquals(1, $address['is_primary']);
533 $phone = $this->callAPISuccessGetSingle('Phone', ['phone' => '12334']);
534 $this->assertEquals(1, $phone['location_type_id']);
536 $this->callAPISuccessGetSingle('Email', ['email' => 'bill.gates@microsoft.com']);
538 $contact = $this->callAPISuccessGetSingle('Contact', $contactValues);
539 $this->callAPISuccess('Contact', 'delete', ['id' => $contact['id']]);
543 * Test that address location type id is ignored for dedupe purposes on import.
547 public function testIgnoreLocationTypeId() {
548 // Create a rule that matches on last name and street address.
549 $rgid = $this->createRuleGroup()['id'];
550 $this->callAPISuccess('Rule', 'create', [
551 'dedupe_rule_group_id' => $rgid,
552 'rule_field' => 'last_name',
553 'rule_table' => 'civicrm_contact',
556 $this->callAPISuccess('Rule', 'create', [
557 'dedupe_rule_group_id' => $rgid,
558 'rule_field' => 'street_address',
559 'rule_table' => 'civicrm_address',
562 // Create a contact with an address of location_type_id 1.
564 'contact_type' => 'Individual',
565 'first_name' => 'Original',
566 'last_name' => 'Smith',
568 $contact1 = $this->callAPISuccess('Contact', 'create', $contact1Params);
569 $this->callAPISuccess('Address', 'create', [
570 'contact_id' => $contact1['id'],
571 'location_type_id' => 1,
572 'street_address' => 'Big Mansion',
576 'first_name' => 'New',
577 'last_name' => 'Smith',
578 'street_address' => 'Big Mansion',
581 // We want to import with a location_type_id of 4.
582 $importLocationTypeId = '4';
583 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_SKIP
, CRM_Import_Parser
::DUPLICATE
, [0 => NULL, 1 => NULL, 2 => $importLocationTypeId], NULL, $rgid);
584 $address = $this->callAPISuccessGetSingle('Address', ['street_address' => 'Big Mansion']);
585 $this->assertEquals(1, $address['location_type_id']);
586 $contact = $this->callAPISuccessGetSingle('Contact', $contact1Params);
587 $this->callAPISuccess('Contact', 'delete', ['id' => $contact['id']]);
591 * Test that address custom fields can be imported
594 * @throws \CRM_Core_Exception
596 public function testAddressWithCustomData() {
597 $ids = $this->entityCustomGroupWithSingleFieldCreate('Address', 'AddressTest.php');
598 [$contactValues] = $this->setUpBaseContact();
599 $contactValues['nick_name'] = 'Old Bill';
600 $contactValues['external_identifier'] = 'android';
601 $contactValues['street_address'] = 'Big Mansion';
602 $contactValues['custom_' . $ids['custom_field_id']] = 'Update';
603 $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']);
604 $address = $this->callAPISuccessGetSingle('Address', ['street_address' => 'Big Mansion', 'return' => 'custom_' . $ids['custom_field_id']]);
605 $this->assertEquals('Update', $address['custom_' . $ids['custom_field_id']]);
609 * Test gender works when you specify the label.
611 * There is an expectation that you can import by label here.
613 * @throws \CRM_Core_Exception
615 public function testGenderLabel() {
617 'first_name' => 'Bill',
618 'last_name' => 'Gates',
619 'email' => 'bill.gates@microsoft.com',
620 'nick_name' => 'Billy-boy',
621 'gender_id' => 'Female',
623 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
, [NULL, NULL, 'Primary', NULL, NULL]);
624 $this->callAPISuccessGetSingle('Contact', $contactValues);
628 * Test prefix & suffix work when you specify the label.
630 * There is an expectation that you can import by label here.
632 * @throws \API_Exception
633 * @throws \CRM_Core_Exception
634 * @throws \CiviCRM_API3_Exception
636 public function testPrefixLabel(): void
{
637 $this->callAPISuccess('OptionValue', 'create', ['option_group_id' => 'individual_prefix', 'name' => 'new_one', 'label' => 'special', 'value' => 70]);
639 ['name' => 'first_name', 'column_number' => 0],
640 ['name' => 'last_name', 'column_number' => 1],
641 ['name' => 'email', 'column_number' => 2, 'location_type_id' => CRM_Core_PseudoConstant
::getKey('CRM_Core_BAO_Email', 'location_type_id', 'Home')],
642 ['name' => 'prefix_id', 'column_number' => 3],
643 ['name' => 'suffix_id', 'column_number' => 4],
645 $mapperInput = [['first_name'], ['last_name'], ['email', CRM_Core_PseudoConstant
::getKey('CRM_Core_BAO_Email', 'location_type_id', 'Home')], ['prefix_id'], ['suffix_id']];
647 $processor = new CRM_Import_ImportProcessor();
648 $processor->setMappingFields($mapping);
649 $userJobID = $this->getUserJobID(['mapper' => $mapperInput]);
650 $processor->setUserJobID($userJobID);
651 $importer = $processor->getImporterObject();
656 'bill.gates@microsoft.com',
660 $importer->import(CRM_Import_Parser
::DUPLICATE_NOCHECK
, $contactValues);
662 $contact = $this->callAPISuccessGetSingle('Contact', ['first_name' => 'Bill', 'prefix_id' => 'new_one', 'suffix_id' => 'III']);
663 $this->assertEquals('special Bill Gates III', $contact['display_name']);
667 * Test that labels work for importing custom data.
669 * @throws \API_Exception
670 * @throws \CRM_Core_Exception
671 * @throws \CiviCRM_API3_Exception
673 public function testCustomDataLabel(): void
{
674 $this->createCustomGroupWithFieldOfType([], 'select');
676 'first_name' => 'Bill',
677 'last_name' => 'Gates',
678 'email' => 'bill.gates@microsoft.com',
679 'nick_name' => 'Billy-boy',
680 $this->getCustomFieldName('select') => 'Yellow',
682 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
, [NULL, NULL, 'Primary', NULL, NULL]);
683 $contact = $this->callAPISuccessGetSingle('Contact', array_merge($contactValues, ['return' => $this->getCustomFieldName('select')]));
684 $this->assertEquals('Y', $contact[$this->getCustomFieldName('select')]);
688 * Test that names work for importing custom data.
690 * @throws \CRM_Core_Exception
692 public function testCustomDataName() {
693 $this->createCustomGroupWithFieldOfType([], 'select');
695 'first_name' => 'Bill',
696 'last_name' => 'Gates',
697 'email' => 'bill.gates@microsoft.com',
698 'nick_name' => 'Billy-boy',
699 $this->getCustomFieldName('select') => 'Y',
701 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
, [NULL, NULL, 'Primary', NULL, NULL]);
702 $contact = $this->callAPISuccessGetSingle('Contact', array_merge($contactValues, ['return' => $this->getCustomFieldName('select')]));
703 $this->assertEquals('Y', $contact[$this->getCustomFieldName('select')]);
707 * Test importing in the Preferred Language Field
709 * @throws \CRM_Core_Exception
711 public function testPreferredLanguageImport() {
713 'first_name' => 'Bill',
714 'last_name' => 'Gates',
715 'email' => 'bill.gates@microsoft.com',
716 'nick_name' => 'Billy-boy',
717 'preferred_language' => 'English (Australia)',
719 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
, [NULL, NULL, 'Primary', NULL, NULL]);
723 * Test that the import parser adds the address to the primary location.
727 public function testImportDeceased() {
728 [$contactValues] = $this->setUpBaseContact();
729 CRM_Core_Session
::singleton()->set("dateTypes", 1);
730 $contactValues['birth_date'] = '1910-12-17';
731 $contactValues['deceased_date'] = '2010-12-17';
732 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
);
733 $contact = $this->callAPISuccessGetSingle('Contact', $contactValues);
734 $this->assertEquals('1910-12-17', $contact['birth_date']);
735 $this->assertEquals('2010-12-17', $contact['deceased_date']);
736 $this->assertEquals(1, $contact['is_deceased']);
737 $this->callAPISuccess('Contact', 'delete', ['id' => $contact['id']]);
741 * Test that the import parser adds the address to the primary location.
745 public function testImportTwoAddressFirstPrimary() {
746 [$contactValues] = $this->setUpBaseContact();
747 $contactValues['nick_name'] = 'Old Bill';
748 $contactValues['external_identifier'] = 'android';
749 $contactValues['street_address'] = 'Big Mansion';
750 $contactValues['phone'] = 12334;
751 $fields = array_keys($contactValues);
752 $contactValues['street_address_2'] = 'Teeny Mansion';
753 $contactValues['phone_2'] = 4444;
754 $fields[] = 'street_address';
756 $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);
757 $contact = $this->callAPISuccessGetSingle('Contact', ['external_identifier' => 'android']);
758 $address = $this->callAPISuccess('Address', 'get', ['contact_id' => $contact['id'], 'sequential' => 1]);
760 $this->assertEquals(3, $address['values'][0]['location_type_id']);
761 $this->assertEquals(0, $address['values'][0]['is_primary']);
762 $this->assertEquals('Teeny Mansion', $address['values'][0]['street_address']);
764 $this->assertEquals(1, $address['values'][1]['location_type_id']);
765 $this->assertEquals(1, $address['values'][1]['is_primary']);
766 $this->assertEquals('Big Mansion', $address['values'][1]['street_address']);
768 $phone = $this->callAPISuccess('Phone', 'get', ['contact_id' => $contact['id'], 'sequential' => 1]);
769 $this->assertEquals(1, $phone['values'][0]['location_type_id']);
770 $this->assertEquals(1, $phone['values'][0]['is_primary']);
771 $this->assertEquals(12334, $phone['values'][0]['phone']);
772 $this->assertEquals(3, $phone['values'][1]['location_type_id']);
773 $this->assertEquals(0, $phone['values'][1]['is_primary']);
774 $this->assertEquals(4444, $phone['values'][1]['phone']);
776 $this->callAPISuccess('Contact', 'delete', ['id' => $contact['id']]);
780 * Test importing 2 phones of different types.
782 * @throws \API_Exception
783 * @throws \CRM_Core_Exception
784 * @throws \CiviCRM_API3_Exception
786 public function testImportTwoPhonesDifferentTypes(): void
{
787 $processor = new CRM_Import_ImportProcessor();
788 $processor->setUserJobID($this->getUserJobID([
789 'mapper' => [['first_name'], ['last_name'], ['email'], ['phone', 1, 2], ['phone', 1, 1]],
791 $processor->setMappingFields(
793 ['name' => 'first_name'],
794 ['name' => 'last_name'],
796 ['name' => 'phone', 'location_type_id' => 1, 'phone_type_id' => 2],
797 ['name' => 'phone', 'location_type_id' => 1, 'phone_type_id' => 1],
800 $importer = $processor->getImporterObject();
801 $fields = ['First Name', 'new last name', 'bob@example.com', '1234', '5678'];
802 $importer->import(CRM_Import_Parser
::DUPLICATE_UPDATE
, $fields);
803 $contact = $this->callAPISuccessGetSingle('Contact', ['last_name' => 'new last name']);
804 $phones = $this->callAPISuccess('Phone', 'get', ['contact_id' => $contact['id']])['values'];
805 $this->assertCount(2, $phones);
809 * Test that the import parser adds the address to the primary location.
813 public function testImportTwoAddressSecondPrimary() {
814 [$contactValues] = $this->setUpBaseContact();
815 $contactValues['nick_name'] = 'Old Bill';
816 $contactValues['external_identifier'] = 'android';
817 $contactValues['street_address'] = 'Big Mansion';
818 $contactValues['phone'] = 12334;
819 $fields = array_keys($contactValues);
820 $contactValues['street_address_2'] = 'Teeny Mansion';
821 $contactValues['phone_2'] = 4444;
822 $fields[] = 'street_address';
824 $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);
825 $contact = $this->callAPISuccessGetSingle('Contact', ['external_identifier' => 'android']);
826 $address = $this->callAPISuccess('Address', 'get', ['contact_id' => $contact['id'], 'sequential' => 1])['values'];
828 $this->assertEquals(1, $address[1]['location_type_id']);
829 $this->assertEquals(1, $address[1]['is_primary']);
830 $this->assertEquals('Teeny Mansion', $address[1]['street_address']);
832 $this->assertEquals(3, $address[0]['location_type_id']);
833 $this->assertEquals(0, $address[0]['is_primary']);
834 $this->assertEquals('Big Mansion', $address[0]['street_address']);
836 $phone = $this->callAPISuccess('Phone', 'get', ['contact_id' => $contact['id'], 'sequential' => 1, 'options' => ['sort' => 'is_primary DESC']])['values'];
837 $this->assertEquals(3, $phone[1]['location_type_id']);
838 $this->assertEquals(0, $phone[1]['is_primary']);
839 $this->assertEquals(12334, $phone[1]['phone']);
840 $this->assertEquals(1, $phone[0]['location_type_id']);
841 $this->assertEquals(1, $phone[0]['is_primary']);
842 $this->assertEquals(4444, $phone[0]['phone']);
844 $this->callAPISuccess('Contact', 'delete', ['id' => $contact['id']]);
848 * Test that the import parser updates the address on the existing primary location.
852 public function testImportPrimaryAddressUpdate() {
853 [$contactValues] = $this->setUpBaseContact(['external_identifier' => 'android']);
854 $contactValues['email'] = 'melinda.gates@microsoft.com';
855 $contactValues['phone'] = '98765';
856 $contactValues['external_identifier'] = 'android';
857 $contactValues['street_address'] = 'Big Mansion';
858 $contactValues['city'] = 'Big City';
859 $contactID = $this->callAPISuccessGetValue('Contact', ['external_identifier' => 'android', 'return' => 'id']);
860 $originalAddress = $this->callAPISuccess('Address', 'create', ['location_type_id' => 2, 'street_address' => 'small house', 'contact_id' => $contactID]);
861 $originalPhone = $this->callAPISuccess('phone', 'create', ['location_type_id' => 2, 'phone' => '1234', 'contact_id' => $contactID, 'phone_type_id' => 1]);
862 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
, []);
863 $phone = $this->callAPISuccessGetSingle('Phone', ['phone' => '98765']);
864 $this->assertEquals(2, $phone['location_type_id']);
865 $this->assertEquals($originalPhone['id'], $phone['id']);
866 $email = $this->callAPISuccess('Email', 'getsingle', ['contact_id' => $contactID]);
867 $address = $this->callAPISuccessGetSingle('Address', ['street_address' => 'Big Mansion']);
868 $this->assertEquals(2, $address['location_type_id']);
869 $this->assertEquals($originalAddress['id'], $address['id']);
870 $this->assertEquals('Big City', $address['city']);
871 $this->callAPISuccessGetSingle('Contact', $contactValues);
875 * Test the determination of whether a custom field is valid.
877 public function testCustomFieldValidation(): void
{
879 $customGroup = $this->customGroupCreate([
880 'extends' => 'Contact',
883 $customField = $this->customFieldOptionValueCreate($customGroup, 'fieldABC', ['html_type' => 'Select', 'serialize' => 1]);
885 'custom_' . $customField['id'] => 'Label1|Label2',
887 CRM_Contact_Import_Parser_Contact
::isErrorInCustomData($params, $errorMessage);
888 $this->assertEquals(NULL, $errorMessage);
892 * Test the import validation.
894 * @dataProvider validateDataProvider
897 * @param array $mapper Mapping as entered on MapField form.
898 * e.g [['first_name']['email', 1]].
899 * {@see \CRM_Contact_Import_Parser_Contact::getMappingFieldFromMapperInput}
900 * @param string $expectedError
901 * @param array $submittedValues
904 * @throws \API_Exception
906 public function testValidation(string $csv, array $mapper, string $expectedError = '', $submittedValues = []): void
{
908 $this->validateCSV($csv, $mapper, $submittedValues);
910 catch (CRM_Core_Exception
$e) {
911 $this->assertSame($expectedError, $e->getMessage());
914 if ($expectedError) {
915 $this->fail('expected error :' . $expectedError);
920 * Get combinations to test for validation.
924 public function validateDataProvider(): array {
926 'individual_required' => [
927 'csv' => 'individual_invalid_missing_name.csv',
928 'mapper' => [['last_name']],
929 'expected_error' => 'Missing required fields: First Name OR Email Address',
931 'individual_related_required_met' => [
932 'csv' => 'individual_valid_with_related_email.csv',
933 'mapper' => [['first_name'], ['last_name'], ['1_a_b', 'email']],
934 'expected_error' => '',
936 'individual_related_required_not_met' => [
937 'csv' => 'individual_invalid_with_related_phone.csv',
938 'mapper' => [['first_name'], ['last_name'], ['1_a_b', 'phone', 1, 2]],
939 'expected_error' => '(Child of) Missing required fields: First Name and Last Name OR Email Address OR External Identifier',
941 'individual_bad_email' => [
942 'csv' => 'individual_invalid_email.csv',
943 'mapper' => [['email', 1], ['first_name'], ['last_name']],
944 'expected_error' => 'Invalid value for field(s) : email',
946 'individual_related_bad_email' => [
947 'csv' => 'individual_invalid_related_email.csv',
948 'mapper' => [['1_a_b', 'email', 1], ['first_name'], ['last_name']],
949 'expected_error' => 'Invalid value for field(s) : email',
951 'individual_invalid_external_identifier_only' => [
952 // External identifier is only enough in upgrade mode.
953 'csv' => 'individual_invalid_external_identifier_only.csv',
954 'mapper' => [['external_identifier'], ['gender_id']],
955 'expected_error' => 'Missing required fields: First Name and Last Name OR Email Address',
957 'individual_invalid_external_identifier_only_update_mode' => [
958 // External identifier only enough in upgrade mode, so no error here.
959 'csv' => 'individual_invalid_external_identifier_only.csv',
960 'mapper' => [['external_identifier'], ['gender_id']],
961 'expected_error' => '',
962 'submitted_values' => ['onDuplicate' => CRM_Import_Parser
::DUPLICATE_UPDATE
],
964 'organization_email_no_organization_name' => [
965 // Email is only enough in upgrade mode.
966 'csv' => 'organization_email_no_organization_name.csv',
967 'mapper' => [['email'], ['phone', 1, 1]],
968 'expected_error' => 'Missing required fields: Organization Name',
969 'submitted_values' => ['onDuplicate' => CRM_Import_Parser
::DUPLICATE_SKIP
, 'contactType' => CRM_Import_Parser
::CONTACT_ORGANIZATION
],
971 'organization_email_no_organization_name_update_mode' => [
972 // Email is enough in upgrade mode (at least to pass validate).
973 'csv' => 'organization_email_no_organization_name.csv',
974 'mapper' => [['email'], ['phone', 1, 1]],
975 'expected_error' => '',
976 'submitted_values' => ['onDuplicate' => CRM_Import_Parser
::DUPLICATE_UPDATE
, 'contactType' => CRM_Import_Parser
::CONTACT_ORGANIZATION
],
984 * @dataProvider importDataProvider
986 * @throws \API_Exception
987 * @throws \CRM_Core_Exception
989 public function testImport($csv, $mapper, $expectedError, $expectedOutcomes = []): void
{
991 $this->importCSV($csv, $mapper);
993 catch (CRM_Core_Exception
$e) {
994 $this->assertSame($expectedError, $e->getMessage());
997 if ($expectedError) {
998 $this->fail('expected error :' . $expectedError);
1000 $dataSource = new CRM_Import_DataSource_CSV(UserJob
::get(FALSE)->setSelect(['id'])->execute()->first()['id']);
1001 foreach ($expectedOutcomes as $outcome => $count) {
1002 $this->assertEquals($dataSource->getRowCount([$outcome]), $count);
1007 * Get combinations to test for validation.
1011 public function importDataProvider(): array {
1013 'individual_invalid_sub_type' => [
1014 'csv' => 'individual_invalid_contact_sub_type.csv',
1015 'mapper' => [['first_name'], ['last_name'], ['contact_sub_type']],
1016 'expected_error' => '',
1017 'expected_outcomes' => [CRM_Import_Parser
::ERROR
=> 1],
1023 * Test the handling of validation when importing genders.
1025 * If it's not gonna import it should fail at the validation stage...
1027 * @throws \API_Exception
1028 * @throws \CRM_Core_Exception
1030 public function testImportGenders(): void
{
1035 ['1_a_b', 'first_name'],
1036 ['1_a_b', 'last_name'],
1037 ['1_a_b', 'gender_id'],
1040 $csv = 'individual_genders.csv';
1041 $this->validateMultiRowCsv($csv, $mapper, 'gender');
1043 $this->importCSV($csv, $mapper);
1044 $contacts = Contact
::get()
1045 ->addWhere('first_name', '=', 'Madame')
1046 ->addSelect('gender_id:name')->execute();
1047 foreach ($contacts as $contact) {
1048 $this->assertEquals('Female', $contact['gender_id:name']);
1050 $this->assertCount(8, $contacts);
1054 * Test date validation.
1056 * @dataProvider dateDataProvider
1058 * @param string $csv
1059 * @param int $dateType
1061 * @throws \API_Exception
1062 * @throws \CRM_Core_Exception
1064 public function testValidateDateData($csv, $dateType): void
{
1065 $addressCustomGroupID = $this->createCustomGroup(['extends' => 'Address', 'name' => 'Address']);
1066 $contactCustomGroupID = $this->createCustomGroup(['extends' => 'Contact', 'name' => 'Contact']);
1067 $addressCustomFieldID = $this->createDateCustomField(['custom_group_id' => $addressCustomGroupID])['id'];
1068 $contactCustomFieldID = $this->createDateCustomField(['custom_group_id' => $contactCustomGroupID])['id'];
1074 ['custom_' . $contactCustomFieldID],
1075 ['custom_' . $addressCustomFieldID, 1],
1076 ['street_address', 1],
1079 // Date types should be picked up from submitted values but still some clean up to do.
1080 CRM_Core_Session
::singleton()->set('dateTypes', $dateType);
1081 $this->validateMultiRowCsv($csv, $mapper, 'custom_date_one', ['dateFormats' => $dateType]);
1083 'contact_id.birth_date',
1084 'contact_id.deceased_date',
1085 'contact_id.is_deceased',
1086 'contact_id.custom_' . $contactCustomFieldID,
1087 $addressCustomFieldID,
1089 $contacts = Address
::get()->addWhere('contact_id.first_name', '=', 'Joe')->setSelect($fields)->execute();
1090 foreach ($contacts as $contact) {
1091 foreach ($fields as $field) {
1092 if ($field === 'contact_is_deceased') {
1093 $this->assertTrue($contact[$field]);
1096 $this->assertEquals('2008-09-01', $contact[$field]);
1103 * @throws \API_Exception
1105 public function testImportContactSubTypes(): void
{
1106 ContactType
::create()->setValues([
1108 'label' => 'Infant',
1109 'parent_id:name' => 'Individual',
1114 ['5_a_b', 'organization_name'],
1115 ['contact_sub_type'],
1116 ['5_a_b', 'contact_sub_type'],
1118 $csv = 'individual_contact_sub_types.csv';
1119 $field = 'contact_sub_type';
1121 $this->validateMultiRowCsv($csv, $mapper, $field);
1122 $this->importCSV($csv, $mapper);
1123 $contacts = Contact
::get()
1124 ->addWhere('last_name', '=', 'Green')
1125 ->addSelect('contact_sub_type:name')->execute();
1126 foreach ($contacts as $contact) {
1127 $this->assertEquals(['baby'], $contact['contact_sub_type:name']);
1129 $this->assertCount(3, $contacts);
1133 * Data provider for date tests.
1137 public function dateDataProvider(): array {
1139 'type_1' => ['csv' => 'individual_dates_type1.csv', 'dateType' => CRM_Core_Form_Date
::DATE_yyyy_mm_dd
],
1140 'type_2' => ['csv' => 'individual_dates_type2.csv', 'dateType' => CRM_Core_Form_Date
::DATE_mm_dd_yy
],
1141 'type_4' => ['csv' => 'individual_dates_type4.csv', 'dateType' => CRM_Core_Form_Date
::DATE_mm_dd_yyyy
],
1142 'type_8' => ['csv' => 'individual_dates_type8.csv', 'dateType' => CRM_Core_Form_Date
::DATE_Month_dd_yyyy
],
1143 'type_16' => ['csv' => 'individual_dates_type16.csv', 'dateType' => CRM_Core_Form_Date
::DATE_dd_mon_yy
],
1144 'type_32' => ['csv' => 'individual_dates_type32.csv', 'dateType' => CRM_Core_Form_Date
::DATE_dd_mm_yyyy
],
1149 * Test location importing, including for related contacts.
1151 * @throws \CRM_Core_Exception
1152 * @throws \API_Exception
1154 public function testImportLocations(): void
{
1155 $csv = 'individual_locations_with_related.csv';
1156 $relationships = (array) RelationshipType
::get()->addSelect('name_a_b', 'id')->addWhere('name_a_b', 'IN', [
1160 ])->execute()->indexBy('name_a_b');
1162 $childKey = $relationships['Child of']['id'] . '_a_b';
1163 $siblingKey = $relationships['Sibling of']['id'] . '_a_b';
1164 $employeeKey = $relationships['Employee of']['id'] . '_a_b';
1165 $locations = LocationType
::get()->execute()->indexBy('name');
1166 $phoneTypeID = CRM_Core_PseudoConstant
::getKey('CRM_Core_BAO_Phone', 'phone_type_id', 'Phone');
1167 $mobileTypeID = CRM_Core_PseudoConstant
::getKey('CRM_Core_BAO_Phone', 'phone_type_id', 'Mobile');
1168 $skypeTypeID = CRM_Core_PseudoConstant
::getKey('CRM_Core_BAO_IM', 'provider_id', 'Skype');
1169 $mainWebsiteTypeID = CRM_Core_PseudoConstant
::getKey('CRM_Core_BAO_Website', 'website_type_id', 'Main');
1170 $linkedInTypeID = CRM_Core_PseudoConstant
::getKey('CRM_Core_BAO_Website', 'website_type_id', 'LinkedIn');
1171 $homeID = $locations['Home']['id'];
1172 $workID = $locations['Work']['id'];
1177 ['street_address', $homeID],
1179 ['postal_code', $homeID],
1180 ['country', $homeID],
1181 ['state_province', $homeID],
1182 // No location type ID means 'Primary'
1185 ['im', NULL, $skypeTypeID],
1186 ['url', $mainWebsiteTypeID],
1187 ['phone', $homeID, $phoneTypeID],
1188 ['phone_ext', $homeID, $phoneTypeID],
1189 [$childKey, 'first_name'],
1190 [$childKey, 'last_name'],
1191 [$childKey, 'street_address'],
1192 [$childKey, 'city'],
1193 [$childKey, 'country'],
1194 [$childKey, 'state_province'],
1195 [$childKey, 'email', $homeID],
1196 [$childKey, 'signature_text', $homeID],
1197 [$childKey, 'im', $homeID, $skypeTypeID],
1198 [$childKey, 'url', $linkedInTypeID],
1199 // Same location type, different phone typ in these phones
1200 [$childKey, 'phone', $homeID, $phoneTypeID],
1201 [$childKey, 'phone_ext', $homeID, $phoneTypeID],
1202 [$childKey, 'phone', $homeID, $mobileTypeID],
1203 [$childKey, 'phone_ext', $homeID, $mobileTypeID],
1204 [$siblingKey, 'street_address', $homeID],
1205 [$siblingKey, 'city', $homeID],
1206 [$siblingKey, 'country', $homeID],
1207 [$siblingKey, 'state_province', $homeID],
1208 [$siblingKey, 'email', $homeID],
1209 [$siblingKey, 'signature_text', $homeID],
1210 [$childKey, 'im', $homeID, $skypeTypeID],
1211 // The 2 is website_type_id (yes, small hard-coding cheat)
1212 [$siblingKey, 'url', $linkedInTypeID],
1213 [$siblingKey, 'phone', $workID, $phoneTypeID],
1214 [$siblingKey, 'phone_ext', $workID, $phoneTypeID],
1215 [$employeeKey, 'organization_name'],
1216 [$employeeKey, 'url', $mainWebsiteTypeID],
1217 [$employeeKey, 'email', $homeID],
1218 [$employeeKey, 'do_not_import'],
1219 [$employeeKey, 'street_address', $homeID],
1220 [$employeeKey, 'supplemental_address_1', $homeID],
1221 [$employeeKey, 'do_not_import'],
1222 // Second website, different type.
1223 [$employeeKey, 'url', $linkedInTypeID],
1225 $this->validateCSV($csv, $mapper);
1229 * Test that setting duplicate action to fill doesn't blow away data
1230 * that exists, but does fill in where it's empty.
1234 public function testImportFill() {
1235 // Create a custom field group for testing.
1236 $this->createCustomGroup([
1237 'title' => 'importFillGroup',
1238 'extends' => 'Individual',
1239 'is_active' => TRUE,
1241 $customGroupID = $this->ids
['CustomGroup']['importFillGroup'];
1243 // Add two custom fields.
1245 'custom_group_id' => $customGroupID,
1246 'label' => 'importFillField1',
1247 'html_type' => 'Select',
1248 'data_type' => 'String',
1249 'option_values' => [
1254 $result = $this->callAPISuccess('custom_field', 'create', $api_params);
1255 $customField1 = $result['id'];
1258 'custom_group_id' => $customGroupID,
1259 'label' => 'importFillField2',
1260 'html_type' => 'Select',
1261 'data_type' => 'String',
1262 'option_values' => [
1267 $result = $this->callAPISuccess('custom_field', 'create', $api_params);
1268 $customField2 = $result['id'];
1270 // Now set up values.
1271 $original_gender = 'Male';
1272 $original_custom1 = 'foo';
1273 $original_email = 'test-import-fill@example.org';
1275 $import_gender = 'Female';
1276 $import_custom1 = 'bar';
1277 $import_job_title = 'Chief data importer';
1278 $import_custom2 = 'baz';
1280 // Create contact with both one known core field and one custom
1283 'contact_type' => 'Individual',
1284 'email' => $original_email,
1285 'gender' => $original_gender,
1286 'custom_' . $customField1 => $original_custom1,
1288 $result = $this->callAPISuccess('contact', 'create', $api_params);
1289 $contact_id = $result['id'];
1293 'email' => $original_email,
1294 'gender_id' => $import_gender,
1295 'custom_' . $customField1 => $import_custom1,
1296 'job_title' => $import_job_title,
1297 'custom_' . $customField2 => $import_custom2,
1300 $this->runImport($import, CRM_Import_Parser
::DUPLICATE_FILL
, CRM_Import_Parser
::VALID
);
1303 'gender' => $original_gender,
1304 'custom_' . $customField1 => $original_custom1,
1305 'job_title' => $import_job_title,
1306 'custom_' . $customField2 => $import_custom2,
1310 'id' => $contact_id,
1313 'custom_' . $customField1,
1315 'custom_' . $customField2,
1318 $result = civicrm_api3('Contact', 'get', $params);
1319 $values = array_pop($result['values']);
1320 foreach ($expected as $field => $expected_value) {
1321 if (!isset($values[$field])) {
1322 $given_value = NULL;
1325 $given_value = $values[$field];
1329 // job_title: Chief Data Importer
1330 // importFillField1: foo
1331 // importFillField2: baz
1332 $this->assertEquals($expected_value, $given_value, "$field properly handled during Fill import");
1337 * CRM-19888 default country should be used if ambigous.
1339 * @throws \CRM_Core_Exception
1341 public function testImportAmbiguousStateCountry(): void
{
1342 $this->callAPISuccess('Setting', 'create', ['defaultContactCountry' => 1228]);
1343 $countries = CRM_Core_PseudoConstant
::country(FALSE, FALSE);
1344 $this->callAPISuccess('Setting', 'create', ['countryLimit' => [array_search('United States', $countries), array_search('Guyana', $countries), array_search('Netherlands', $countries)]]);
1345 $this->callAPISuccess('Setting', 'create', ['provinceLimit' => [array_search('United States', $countries), array_search('Guyana', $countries), array_search('Netherlands', $countries)]]);
1346 $mapper = [0 => NULL, 1 => NULL, 2 => 'Primary', 3 => NULL];
1347 [$contactValues] = $this->setUpBaseContact();
1348 $fields = array_keys($contactValues);
1350 'street_address' => 'PO Box 2716',
1352 'state_province' => 'UT',
1353 'postal_code' => 84049,
1354 'country' => 'United States',
1356 $locationTypes = $this->callAPISuccess('Address', 'getoptions', ['field' => 'location_type_id']);
1357 $locationTypes = $locationTypes['values'];
1358 foreach ($addressValues as $field => $value) {
1359 $contactValues['home_' . $field] = $value;
1360 $mapper[] = array_search('Home', $locationTypes);
1361 $contactValues['work_' . $field] = $value;
1362 $mapper[] = array_search('Work', $locationTypes);
1366 $contactValues['work_country'] = '';
1368 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
, $mapper, $fields);
1369 $addresses = $this->callAPISuccess('Address', 'get', ['contact_id' => ['>' => 2], 'sequential' => 1]);
1370 $this->assertEquals(2, $addresses['count']);
1371 $this->assertEquals(array_search('United States', $countries), $addresses['values'][0]['country_id']);
1372 $this->assertEquals(array_search('United States', $countries), $addresses['values'][1]['country_id']);
1376 * Test importing fields with various options.
1378 * Ensure we can import multiple preferred_communication_methods, single
1379 * gender, and single preferred language using both labels and values.
1381 * @throws \API_Exception
1382 * @throws \CRM_Core_Exception
1383 * @throws \CiviCRM_API3_Exception
1385 public function testImportFieldsWithVariousOptions(): void
{
1386 $processor = new CRM_Import_ImportProcessor();
1387 $processor->setUserJobID($this->getUserJobID([
1388 'mapper' => [['first_name'], ['last_name'], ['preferred_communication_method'], ['gender_id'], ['preferred_language']],
1390 $processor->setMappingFields(
1392 ['name' => 'first_name'],
1393 ['name' => 'last_name'],
1394 ['name' => 'preferred_communication_method'],
1395 ['name' => 'gender_id'],
1396 ['name' => 'preferred_language'],
1399 $importer = $processor->getImporterObject();
1400 $fields = ['Ima', 'Texter', 'SMS,Phone', 'Female', 'Danish'];
1401 $importer->import(CRM_Import_Parser
::DUPLICATE_NOCHECK
, $fields);
1402 $contact = $this->callAPISuccessGetSingle('Contact', ['last_name' => 'Texter']);
1404 $this->assertEquals([4, 1], $contact['preferred_communication_method'], "Import multiple preferred communication methods using labels.");
1405 $this->assertEquals(1, $contact['gender_id'], "Import gender with label.");
1406 $this->assertEquals('da_DK', $contact['preferred_language'], "Import preferred language with label.");
1408 $importer = $processor->getImporterObject();
1409 $fields = ['Ima', 'Texter', "4,1", "1", "da_DK"];
1410 $importer->import(CRM_Import_Parser
::DUPLICATE_NOCHECK
, $fields);
1411 $contact = $this->callAPISuccessGetSingle('Contact', ['last_name' => 'Texter']);
1413 $this->assertEquals([4, 1], $contact['preferred_communication_method'], "Import multiple preferred communication methods using values.");
1414 $this->assertEquals(1, $contact['gender_id'], "Import gender with id.");
1415 $this->assertEquals('da_DK', $contact['preferred_language'], "Import preferred language with value.");
1419 * Run the import parser.
1421 * @param array $originalValues
1423 * @param int $onDuplicateAction
1424 * @param int $expectedResult
1425 * @param array|null $mapperLocType
1426 * Array of location types that map to the input arrays.
1427 * @param array|null $fields
1428 * Array of field names. Will be calculated from $originalValues if not passed in, but
1429 * that method does not cope with duplicates.
1430 * @param int|null $ruleGroupId
1431 * To test against a specific dedupe rule group, pass its ID as this argument.
1433 * @throws \API_Exception
1434 * @throws \CRM_Core_Exception
1435 * @throws \CiviCRM_API3_Exception
1437 protected function runImport(array $originalValues, $onDuplicateAction, $expectedResult, $mapperLocType = [], $fields = NULL, int $ruleGroupId = NULL): void
{
1439 $fields = array_keys($originalValues);
1441 $values = array_values($originalValues);
1443 foreach ($fields as $index => $field) {
1444 $mapper[] = [$field, $mapperLocType[$index] ??
NULL, $field === 'phone' ?
1 : NULL];
1446 $userJobID = $this->getUserJobID(['mapper' => $mapper, 'onDuplicate' => $onDuplicateAction, 'dedupe_rule_id' => $ruleGroupId]);
1447 $parser = new CRM_Contact_Import_Parser_Contact($fields);
1448 $parser->setUserJobID($userJobID);
1449 $parser->_dedupeRuleGroupID
= $ruleGroupId;
1451 $this->assertEquals($expectedResult, $parser->import($onDuplicateAction, $values), 'Return code from parser import was not as expected');
1455 * @param string $csv
1456 * @param array $mapper Mapping as entered on MapField form.
1457 * e.g [['first_name']['email', 1]].
1458 * {@see \CRM_Contact_Import_Parser_Contact::getMappingFieldFromMapperInput}
1459 * @param array $submittedValues
1462 * @throws \API_Exception
1463 * @throws \Civi\API\Exception\UnauthorizedException
1465 protected function getDataSourceAndParser(string $csv, array $mapper, array $submittedValues): array {
1466 $userJobID = $this->getUserJobID(array_merge([
1467 'uploadFile' => ['name' => __DIR__
. '/../Form/data/' . $csv],
1468 'skipColumnHeader' => TRUE,
1469 'fieldSeparator' => ',',
1470 'onDuplicate' => CRM_Import_Parser
::DUPLICATE_SKIP
,
1471 'contactType' => CRM_Import_Parser
::CONTACT_INDIVIDUAL
,
1472 'mapper' => $mapper,
1473 'dataSource' => 'CRM_Import_DataSource_CSV',
1474 ], $submittedValues));
1476 $dataSource = new CRM_Import_DataSource_CSV($userJobID);
1477 $parser = new CRM_Contact_Import_Parser_Contact();
1478 $parser->setUserJobID($userJobID);
1480 return [$dataSource, $parser];
1484 * @param int $contactID
1486 * @throws \API_Exception
1487 * @throws \Civi\API\Exception\UnauthorizedException
1489 protected function addChild(int $contactID): void
{
1490 $relatedContactID = $this->individualCreate();
1491 $relationshipTypeID = RelationshipType
::create()->setValues([
1492 'name_a_b' => 'Dad to',
1493 'name_b_a' => 'Sleep destroyer of',
1494 'contact_type_a' => 'Individual',
1495 'contact_type_b' => 'Individual',
1496 'contact_sub_type_a' => 'Parent',
1497 ])->execute()->first()['id'];
1498 \Civi\Api4\Relationship
::create()->setValues([
1499 'relationship_type_id' => $relationshipTypeID,
1500 'contact_id_a' => $contactID,
1501 'contact_id_b' => $relatedContactID,
1506 * @param array $fields Array of fields to be imported
1507 * @param array $allfields Array of all fields which can be part of import
1509 private function mapRelationshipFields(&$fields, $allfields) {
1510 foreach ($allfields as $key => $fieldtocheck) {
1511 $elementIndex = array_search($fieldtocheck->_title
, $fields);
1512 if ($elementIndex !== FALSE) {
1513 $fields[$elementIndex] = $key;
1519 * Test mapping fields within the Parser class.
1521 * @throws \API_Exception
1522 * @throws \Civi\API\Exception\UnauthorizedException
1524 public function testMapFields(): void
{
1525 $parser = new CRM_Contact_Import_Parser_Contact(
1526 // Array of field names
1527 ['first_name', 'phone', NULL, 'im', NULL],
1528 // Array of location types, ie columns 2 & 4 have types.
1529 [NULL, 1, NULL, 1, NULL],
1530 // Array of phone types
1531 [NULL, 1, NULL, NULL, NULL],
1532 // Array of im provider types
1533 [NULL, NULL, NULL, 1, NULL],
1534 // Array of filled in relationship values.
1535 [NULL, NULL, '5_a_b', NULL, '5_a_b'],
1536 // Array of the contact type to map to - note this can be determined from ^^
1537 [NULL, NULL, 'Organization', NULL, 'Organization'],
1538 // Related contact field names
1539 [NULL, NULL, 'url', NULL, 'phone'],
1540 // Related contact location types
1541 [NULL, NULL, NULL, NULL, 1],
1542 // Related contact phone types
1543 [NULL, NULL, NULL, NULL, 1],
1544 // Related contact im provider types
1545 [NULL, NULL, NULL, NULL, NULL],
1547 [NULL, NULL, NULL, NULL, NULL],
1548 // Related contact website types
1549 [NULL, NULL, 1, NULL, NULL]
1551 $parser->setUserJobID($this->getUserJobID([
1555 ['5_a_b', 'url', 1],
1557 ['5_a_b', 'phone', 1, 1],
1561 $params = $parser->getMappedRow(
1562 ['Bob', '123', 'https://example.org', 'my-handle', '456']
1564 $this->assertEquals([
1565 'first_name' => 'Bob',
1569 'location_type_id' => 1,
1570 'phone_type_id' => 1,
1574 'contact_type' => 'Organization',
1579 'url' => 'https://example.org',
1580 'website_type_id' => 1,
1587 'location_type_id' => 1,
1588 'phone_type_id' => 1,
1596 'im' => 'my-handle',
1597 'location_type_id' => 1,
1601 'contact_type' => 'Individual',
1606 * Set up the underlying contact.
1608 * @param array $params
1609 * Optional extra parameters to set.
1612 * @throws \CRM_Core_Exception
1614 protected function setUpBaseContact($params = []) {
1615 $originalValues = array_merge([
1616 'first_name' => 'Bill',
1617 'last_name' => 'Gates',
1618 'email' => 'bill.gates@microsoft.com',
1619 'nick_name' => 'Billy-boy',
1621 $this->runImport($originalValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
);
1622 $result = $this->callAPISuccessGetSingle('Contact', $originalValues);
1623 return [$originalValues, $result];
1628 * @throws \API_Exception
1629 * @throws \Civi\API\Exception\UnauthorizedException
1631 protected function getUserJobID($submittedValues = []) {
1632 $userJobID = UserJob
::create()->setValues([
1634 'submitted_values' => array_merge([
1635 'contactType' => CRM_Import_Parser
::CONTACT_INDIVIDUAL
,
1636 'contactSubType' => '',
1637 'doGeocodeAddress' => 0,
1638 'dataSource' => 'CRM_Import_DataSource_SQL',
1639 'sqlQuery' => 'SELECT first_name FROM civicrm_contact',
1640 'onDuplicate' => CRM_Import_Parser
::DUPLICATE_SKIP
,
1641 'dedupe_rule_id' => NULL,
1642 'dateFormats' => CRM_Core_Form_Date
::DATE_yyyy_mm_dd
,
1643 ], $submittedValues),
1645 'status_id:name' => 'draft',
1646 'type_id:name' => 'contact_import',
1647 ])->execute()->first()['id'];
1648 if ($submittedValues['dataSource'] ??
NULL === 'CRM_Import_DataSource') {
1649 $dataSource = new CRM_Import_DataSource_CSV($userJobID);
1652 $dataSource = new CRM_Import_DataSource_SQL($userJobID);
1654 $dataSource->initialize();
1659 * Validate the csv file values.
1661 * @param string $csv Name of csv file.
1662 * @param array $mapper Mapping as entered on MapField form.
1663 * e.g [['first_name']['email', 1]].
1664 * {@see \CRM_Contact_Import_Parser_Contact::getMappingFieldFromMapperInput}
1665 * @param array $submittedValues
1666 * Any submitted values overrides.
1668 * @throws \API_Exception
1670 protected function validateCSV(string $csv, array $mapper, array $submittedValues = []): void
{
1671 [$dataSource, $parser] = $this->getDataSourceAndParser($csv, $mapper, $submittedValues);
1672 $parser->validateValues(array_values($dataSource->getRow()));
1676 * Import the csv file values.
1678 * This function uses a flow that mimics the UI flow.
1680 * @param string $csv Name of csv file.
1681 * @param array $mapper Mapping as entered on MapField form.
1682 * e.g [['first_name']['email', 1]].
1683 * {@see \CRM_Contact_Import_Parser_Contact::getMappingFieldFromMapperInput}
1684 * @param array $submittedValues
1686 protected function importCSV(string $csv, array $mapper, array $submittedValues = []): void
{
1687 $submittedValues = array_merge([
1688 'uploadFile' => ['name' => __DIR__
. '/../Form/data/' . $csv],
1689 'skipColumnHeader' => TRUE,
1690 'fieldSeparator' => ',',
1691 'contactType' => CRM_Import_Parser
::CONTACT_INDIVIDUAL
,
1692 'mapper' => $mapper,
1693 'dataSource' => 'CRM_Import_DataSource_CSV',
1694 'file' => ['name' => $csv],
1695 'dateFormats' => CRM_Core_Form_Date
::DATE_yyyy_mm_dd
,
1696 'onDuplicate' => CRM_Import_Parser
::DUPLICATE_UPDATE
,
1698 ], $submittedValues);
1699 $form = $this->getFormObject('CRM_Contact_Import_Form_DataSource', $submittedValues);
1701 $form->postProcess();
1702 $userJobID = $form->getUserJobID();
1703 /* @var CRM_Contact_Import_Form_MapField $form */
1704 $form = $this->getFormObject('CRM_Contact_Import_Form_MapField', $submittedValues);
1705 $form->setUserJobID($userJobID);
1707 $form->postProcess();
1708 /* @var CRM_Contact_Import_Form_MapField $form */
1709 $form = $this->getFormObject('CRM_Contact_Import_Form_Preview', $submittedValues);
1710 $form->setUserJobID($userJobID);
1712 $form->postProcess();
1716 * Validate a csv with multiple rows in it.
1718 * @param string $csv
1719 * @param array $mapper Mapping as entered on MapField form.
1720 * e.g [['first_name']['email', 1]].
1721 * @param string $field
1722 * Name of the field whose data should be output in the error message.
1723 * @param array $submittedValues
1724 * Values submitted in the form process.
1726 * @throws \API_Exception
1727 * @throws \CRM_Core_Exception
1728 * @throws \Civi\API\Exception\UnauthorizedException
1730 private function validateMultiRowCsv(string $csv, array $mapper, string $field, $submittedValues = []): void
{
1731 /* @var CRM_Import_DataSource_CSV $dataSource */
1732 /* @var \CRM_Contact_Import_Parser_Contact $parser */
1733 [$dataSource, $parser] = $this->getDataSourceAndParser($csv, $mapper, $submittedValues);
1734 while ($values = $dataSource->getRow()) {
1736 $parser->validateValues(array_values($values));
1737 if ($values['expected'] !== 'Valid') {
1738 $this->fail($values[$field] . ' should not have been valid');
1741 catch (CRM_Core_Exception
$e) {
1742 if ($values['expected'] !== 'Invalid') {
1743 $this->fail($values[$field] . ' should have been valid');