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
;
22 use Civi\Api4\GroupContact
;
24 use Civi\Api4\LocationType
;
27 use Civi\Api4\Relationship
;
28 use Civi\Api4\RelationshipType
;
29 use Civi\Api4\UserJob
;
30 use Civi\Api4\Website
;
33 * Test contact import parser.
38 class CRM_Contact_Import_Parser_ContactTest
extends CiviUnitTestCase
{
39 use CRMTraits_Custom_CustomDataTrait
;
40 use CRMTraits_Import_ParserTrait
;
43 * Main entity for the class.
47 protected $entity = 'Contact';
50 * Array of existing relationships.
54 private $relationships = [];
57 * Tear down after test.
59 public function tearDown(): void
{
60 $this->quickCleanup(['civicrm_address', 'civicrm_phone', 'civicrm_openid', 'civicrm_email', 'civicrm_user_job', 'civicrm_relationship', 'civicrm_im', 'civicrm_website', 'civicrm_queue', 'civicrm_queue_item'], TRUE);
61 RelationshipType
::delete()->addWhere('name_a_b', '=', 'Dad to')->execute();
62 ContactType
::delete()->addWhere('name', '=', 'baby')->execute();
67 * Test that import parser will add contact with employee of relationship.
69 * @throws \API_Exception
70 * @throws \CRM_Core_Exception
71 * @throws \CiviCRM_API3_Exception
73 public function testImportParserWithEmployeeOfRelationship(): void
{
74 $this->organizationCreate([
75 'organization_name' => 'Agileware',
76 'legal_name' => 'Agileware',
78 $contactImportValues = [
79 'first_name' => 'Alok',
80 'last_name' => 'Patel',
81 'Employee of' => 'Agileware',
84 $fields = array_keys($contactImportValues);
85 $values = array_values($contactImportValues);
86 $userJobID = $this->getUserJobID([
87 'mapper' => [['first_name'], ['last_name'], ['5_a_b', 'organization_name']],
88 'onDuplicate' => CRM_Import_Parser
::DUPLICATE_UPDATE
,
91 $parser = new CRM_Contact_Import_Parser_Contact($fields);
92 $parser->setUserJobID($userJobID);
95 $this->assertEquals(CRM_Import_Parser
::VALID
, $parser->import($values), 'Return code from parser import was not as expected');
96 $this->callAPISuccessGetSingle('Contact', [
97 'first_name' => 'Alok',
98 'last_name' => 'Patel',
99 'organization_name' => 'Agileware',
104 * Test that import parser will not fail when same external_identifier found
105 * of deleted contact.
107 * @throws \API_Exception
108 * @throws \CRM_Core_Exception
109 * @throws \CiviCRM_API3_Exception
111 public function testImportParserWithDeletedContactExternalIdentifier(): void
{
112 $contactId = $this->individualCreate([
113 'external_identifier' => 'ext-1',
115 $this->callAPISuccess('Contact', 'delete', ['id' => $contactId]);
116 [$originalValues, $result] = $this->setUpBaseContact([
117 'external_identifier' => 'ext-1',
119 $originalValues['nick_name'] = 'Old Bill';
120 $this->runImport($originalValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
);
121 $originalValues['id'] = $result['id'];
122 $this->assertEquals('ext-1', $this->callAPISuccessGetValue('Contact', ['id' => $result['id'], 'return' => 'external_identifier']));
123 $this->callAPISuccessGetSingle('Contact', $originalValues);
127 * Test import parser will update based on a rule match.
129 * In this case the contact has no external identifier.
131 * @throws \API_Exception
132 * @throws \CRM_Core_Exception
133 * @throws \CiviCRM_API3_Exception
135 public function testImportParserWithUpdateWithoutExternalIdentifier(): void
{
136 [$originalValues, $result] = $this->setUpBaseContact();
137 $originalValues['nick_name'] = 'Old Bill';
138 $this->runImport($originalValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
);
139 $originalValues['id'] = $result['id'];
140 $this->assertEquals('Old Bill', $this->callAPISuccessGetValue('Contact', ['id' => $result['id'], 'return' => 'nick_name']));
141 $this->callAPISuccessGetSingle('Contact', $originalValues);
145 * Test import parser will update based on a custom rule match.
147 * In this case the contact has no external identifier.
149 * @throws \API_Exception
150 * @throws \CRM_Core_Exception
151 * @throws \CiviCRM_API3_Exception
153 public function testImportParserWithUpdateWithCustomRule(): void
{
154 $this->createCustomGroupWithFieldsOfAllTypes();
156 $ruleGroup = $this->callAPISuccess('RuleGroup', 'create', [
157 'contact_type' => 'Individual',
160 'name' => 'TestRule',
161 'title' => 'TestRule',
164 $this->callAPISuccess('Rule', 'create', [
165 'dedupe_rule_group_id' => $ruleGroup['id'],
166 'rule_table' => $this->getCustomGroupTable(),
168 'rule_field' => $this->getCustomFieldColumnName('text'),
172 $this->getCustomFieldName('select_string') => 'Yellow',
173 $this->getCustomFieldName('text') => 'Duplicate',
176 [$originalValues, $result] = $this->setUpBaseContact($extra);
179 'first_name' => 'Tim',
180 'last_name' => 'Cook',
181 'email' => 'tim.cook@apple.com',
182 'nick_name' => 'Steve',
183 $this->getCustomFieldName('select_string') => 'Red',
184 $this->getCustomFieldName('text') => 'Duplicate',
187 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
, [], NULL, $ruleGroup['id']);
188 $contactValues['id'] = $result['id'];
189 $this->assertEquals('R', $this->callAPISuccessGetValue('Contact', ['id' => $result['id'], 'return' => $this->getCustomFieldName('select_string')]));
190 $this->callAPISuccessGetSingle('Contact', $contactValues);
192 $foundDupes = CRM_Dedupe_Finder
::dupes($ruleGroup['id']);
193 $this->assertCount(0, $foundDupes);
197 * Test import parser will update based on a custom rule match.
199 * In this case the contact has no external identifier.
201 * @throws \API_Exception
202 * @throws \CRM_Core_Exception
203 * @throws \CiviCRM_API3_Exception
205 public function testImportParserWithUpdateWithCustomRuleNoExternalIDMatch(): void
{
206 $this->createCustomGroupWithFieldsOfAllTypes();
208 $ruleGroup = $this->callAPISuccess('RuleGroup', 'create', [
209 'contact_type' => 'Individual',
212 'name' => 'TestRule',
213 'title' => 'TestRule',
216 $this->callAPISuccess('Rule', 'create', [
217 'dedupe_rule_group_id' => $ruleGroup['id'],
218 'rule_table' => $this->getCustomGroupTable(),
220 'rule_field' => $this->getCustomFieldColumnName('text'),
224 $this->getCustomFieldName('select_string') => 'Yellow',
225 $this->getCustomFieldName('text') => 'Duplicate',
226 'external_identifier' => 'ext-2',
229 [$originalValues, $result] = $this->setUpBaseContact($extra);
232 'first_name' => 'Tim',
233 'last_name' => 'Cook',
234 'email' => 'tim.cook@apple.com',
235 'nick_name' => 'Steve',
236 'external_identifier' => 'ext-1',
237 $this->getCustomFieldName('select_string') => 'Red',
238 $this->getCustomFieldName('text') => 'Duplicate',
241 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
, [], NULL, $ruleGroup['id']);
242 $contactValues['id'] = $result['id'];
243 $this->assertEquals('R', $this->callAPISuccessGetValue('Contact', ['id' => $result['id'], 'return' => $this->getCustomFieldName('select_string')]));
244 $this->callAPISuccessGetSingle('Contact', $contactValues);
246 $foundDupes = CRM_Dedupe_Finder
::dupes($ruleGroup['id']);
247 $this->assertCount(0, $foundDupes);
251 * Test import parser will update contacts with an external identifier.
253 * This is the basic test where the identifier matches the import parameters.
257 public function testImportParserWithUpdateWithExternalIdentifier(): void
{
258 [$originalValues, $result] = $this->setUpBaseContact(['external_identifier' => 'windows']);
260 $this->assertEquals($result['id'], CRM_Core_DAO
::getFieldValue('CRM_Contact_DAO_Contact', 'windows', 'id', 'external_identifier', TRUE));
261 $this->assertEquals('windows', $result['external_identifier']);
263 $originalValues['nick_name'] = 'Old Bill';
264 $this->runImport($originalValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
);
265 $originalValues['id'] = $result['id'];
267 $this->assertEquals('Old Bill', $this->callAPISuccessGetValue('Contact', ['id' => $result['id'], 'return' => 'nick_name']));
268 $this->callAPISuccessGetSingle('Contact', $originalValues);
272 * Test updating an existing contact with external_identifier match but subtype mismatch.
274 * The subtype is updated, as there is no conflicting contact data.
278 public function testImportParserWithUpdateWithExternalIdentifierSubtypeChange(): void
{
279 $contactID = $this->individualCreate(['external_identifier' => 'billy', 'first_name' => 'William', 'contact_sub_type' => 'Parent']);
281 'external_identifier' => 'billy',
282 'nick_name' => 'Old Bill',
283 'contact_sub_type' => 'Staff',
284 ], CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
);
285 $contact = $this->callAPISuccessGetSingle('Contact', ['id' => $contactID]);
286 $this->assertEquals('Old Bill', $contact['nick_name']);
287 $this->assertEquals('William', $contact['first_name']);
288 $this->assertEquals('billy', $contact['external_identifier']);
289 $this->assertEquals(['Staff'], $contact['contact_sub_type']);
293 * Test updating an existing contact with external_identifier match but subtype mismatch.
295 * The subtype is not updated, as there is conflicting contact data.
299 public function testImportParserUpdateWithExternalIdentifierSubtypeChangeFail(): void
{
300 $contactID = $this->individualCreate(['external_identifier' => 'billy', 'first_name' => 'William', 'contact_sub_type' => 'Parent']);
301 $this->addChild($contactID);
304 'external_identifier' => 'billy',
305 'nick_name' => 'Old Bill',
306 'contact_sub_type' => 'Staff',
307 ], CRM_Import_Parser
::DUPLICATE_UPDATE
, FALSE);
308 $contact = $this->callAPISuccessGetSingle('Contact', ['id' => $contactID]);
309 $this->assertEquals('', $contact['nick_name']);
310 $this->assertEquals(['Parent'], $contact['contact_sub_type']);
314 * Test updating an existing contact with external_identifier match but subtype mismatch.
316 * The subtype is not updated, as there is conflicting contact data.
318 public function testImportParserUpdateWithExistingRelatedMatch(): void
{
319 $contactID = $this->individualCreate([
320 'external_identifier' => 'billy',
321 'first_name' => 'William',
322 'last_name' => 'The Kid',
323 'email' => 'billy-the-kid@example.com',
324 'contact_sub_type' => 'Parent',
326 $this->addChild($contactID);
327 $this->importCSV('individual_related_create.csv', [
328 ['first_name'], ['last_name'], [$this->relationships
['Dad to'], 'first_name'], [$this->relationships
['Dad to'], 'last_name'], [$this->relationships
['Dad to'], 'email'],
330 'onDuplicate' => CRM_Import_Parser
::DUPLICATE_SKIP
,
332 $dataSource = $this->getDataSource();
333 $row = $dataSource->getRow();
334 $this->assertEquals('IMPORTED', $row['_status']);
335 $row = $dataSource->getRow();
336 $this->assertEquals('IMPORTED', $row['_status']);
337 $row = $dataSource->getRow();
338 $this->assertEquals('IMPORTED', $row['_status']);
339 $row = $dataSource->getRow();
340 // currently Error with the message (Dad to) Missing required fields: Last Name OR Email Address OR External Identifier
341 // $this->assertEquals('IMPORTED', $row['_status']);
345 * Test updating an existing contact with external_identifier match but subtype mismatch.
349 public function testImportParserWithUpdateWithTypeMismatch(): void
{
350 $contactID = $this->organizationCreate(['external_identifier' => 'billy']);
352 'external_identifier' => 'billy',
353 'nick_name' => 'Old Bill',
354 ], CRM_Import_Parser
::DUPLICATE_UPDATE
, FALSE);
355 $contact = $this->callAPISuccessGetSingle('Contact', ['id' => $contactID]);
356 $this->assertEquals('', $contact['nick_name']);
357 $this->assertEquals('billy', $contact['external_identifier']);
358 $this->assertEquals('Organization', $contact['contact_type']);
362 'nick_name' => 'Old Bill',
363 ], CRM_Import_Parser
::DUPLICATE_UPDATE
, FALSE);
364 $contact = $this->callAPISuccessGetSingle('Contact', ['id' => $contactID]);
365 $this->assertEquals('', $contact['nick_name']);
366 $this->assertEquals('billy', $contact['external_identifier']);
367 $this->assertEquals('Organization', $contact['contact_type']);
372 * Test import parser will fallback to external identifier.
374 * In this case no primary match exists (e.g the details are not supplied) so it falls back on external identifier.
376 * @see https://issues.civicrm.org/jira/browse/CRM-17275
380 public function testImportParserWithUpdateWithExternalIdentifierButNoPrimaryMatch(): void
{
381 [$originalValues, $result] = $this->setUpBaseContact([
382 'external_identifier' => 'windows',
386 $this->assertEquals('windows', $result['external_identifier']);
388 $originalValues['nick_name'] = 'Old Bill';
389 $this->runImport($originalValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
);
390 $originalValues['id'] = $result['id'];
392 $this->assertEquals('Old Bill', $this->callAPISuccessGetValue('Contact', ['id' => $result['id'], 'return' => 'nick_name']));
393 $this->callAPISuccessGetSingle('Contact', $originalValues);
397 * Test import parser will fallback to external identifier.
399 * In this case no primary match exists (e.g the details are not supplied) so it falls back on external identifier.
401 * @see https://issues.civicrm.org/jira/browse/CRM-17275
405 public function testImportParserWithUpdateWithContactID(): void
{
406 [$originalValues, $result] = $this->setUpBaseContact([
407 'external_identifier' => '',
410 $updateValues = ['id' => $result['id'], 'email' => 'bill@example.com'];
411 // This is some deep weirdness - this sets a flag for updatingBlankLocinfo - allowing input to be blanked
412 // (which IS a good thing but it's pretty weird & all to do with legacy profile stuff).
413 CRM_Core_Session
::singleton()->set('authSrc', CRM_Core_Permission
::AUTH_SRC_CHECKSUM
);
414 $this->runImport($updateValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
);
415 $originalValues['id'] = $result['id'];
416 $this->callAPISuccessGetSingle('Email', ['contact_id' => $originalValues['id'], 'is_primary' => 1]);
417 $this->callAPISuccessGetSingle('Contact', $originalValues);
421 * Test that the import parser adds the external identifier where none is set.
425 public function testImportParserWithUpdateWithNoExternalIdentifier(): void
{
426 [$originalValues, $result] = $this->setUpBaseContact();
427 $originalValues['nick_name'] = 'Old Bill';
428 $originalValues['external_identifier'] = 'windows';
429 $this->runImport($originalValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
);
430 $originalValues['id'] = $result['id'];
431 $this->assertEquals('Old Bill', $this->callAPISuccessGetValue('Contact', ['id' => $result['id'], 'return' => 'nick_name']));
432 $this->callAPISuccessGetSingle('Contact', $originalValues);
436 * Test that the import parser changes the external identifier when there is a dedupe match.
440 public function testImportParserWithUpdateWithChangedExternalIdentifier(): void
{
441 [$contactValues, $result] = $this->setUpBaseContact(['external_identifier' => 'windows']);
442 $contact_id = $result['id'];
443 $contactValues['nick_name'] = 'Old Bill';
444 $contactValues['external_identifier'] = 'android';
445 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
);
446 $contactValues['id'] = $contact_id;
447 $this->assertEquals('Old Bill', $this->callAPISuccessGetValue('Contact', ['id' => $contact_id, 'return' => 'nick_name']));
448 $this->callAPISuccessGetSingle('Contact', $contactValues);
452 * Test that the import parser adds the address to the right location.
454 * @throws \API_Exception
455 * @throws \CRM_Core_Exception
456 * @throws \CiviCRM_API3_Exception
458 public function testImportBillingAddress(): void
{
459 [$contactValues] = $this->setUpBaseContact();
460 $contactValues['nick_name'] = 'Old Bill';
461 $contactValues['external_identifier'] = 'android';
462 $contactValues['street_address'] = 'Big Mansion';
463 $contactValues['phone'] = '911';
464 $mapper = $this->getFieldMappingFromInput($contactValues, 2);
465 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
, $mapper);
466 $address = $this->callAPISuccessGetSingle('Address', ['street_address' => 'Big Mansion']);
467 $this->assertEquals(2, $address['location_type_id']);
469 $phone = $this->callAPISuccessGetSingle('Phone', ['phone' => '911']);
470 $this->assertEquals(2, $phone['location_type_id']);
472 $contact = $this->callAPISuccessGetSingle('Contact', $contactValues);
473 $this->callAPISuccess('Contact', 'delete', ['id' => $contact['id']]);
477 * Test that the not-really-encouraged way of creating locations via contact.create doesn't mess up primaries.
479 public function testContactLocationBlockHandling(): void
{
480 $id = $this->individualCreate([
483 'location_type_id' => 1,
484 'phone' => '987654321',
487 'location_type_id' => 2,
488 'phone' => '456-7890',
493 'location_type_id' => 1,
497 'location_type_id' => 2,
503 'location_type_id' => 1,
507 'location_type_id' => 2,
513 'location_type_id' => 1,
514 'email' => 'bob@example.com',
517 'location_type_id' => 2,
518 'email' => 'fred@example.com',
522 $phones = $this->callAPISuccess('Phone', 'get', ['contact_id' => $id])['values'];
523 $emails = $this->callAPISuccess('Email', 'get', ['contact_id' => $id])['values'];
524 $openIDs = $this->callAPISuccess('OpenID', 'get', ['contact_id' => $id])['values'];
525 $ims = $this->callAPISuccess('IM', 'get', ['contact_id' => $id])['values'];
526 $this->assertCount(2, $phones);
527 $this->assertCount(2, $emails);
528 $this->assertCount(2, $ims);
529 $this->assertCount(2, $openIDs);
531 $this->assertLocationValidity();
532 $this->callAPISuccess('Contact', 'create', [
534 // This is secret code for 'delete this phone'.
535 'updateBlankLocInfo' => TRUE,
538 'id' => key($phones),
543 'id' => key($emails),
553 'id' => key($openIDs),
557 $this->assertLocationValidity();
558 $this->callAPISuccessGetCount('Phone', ['contact_id' => $id], 1);
559 $this->callAPISuccessGetCount('Email', ['contact_id' => $id], 1);
560 $this->callAPISuccessGetCount('OpenID', ['contact_id' => $id], 1);
561 $this->callAPISuccessGetCount('IM', ['contact_id' => $id], 1);
565 * Test that the import parser adds the address to the primary location.
569 public function testImportPrimaryAddress(): void
{
570 [$contactValues] = $this->setUpBaseContact();
571 $contactValues['nick_name'] = 'Old Bill';
572 $contactValues['external_identifier'] = 'android';
573 $contactValues['street_address'] = 'Big Mansion';
574 $contactValues['phone'] = 12334;
575 $mapper = $this->getFieldMappingFromInput($contactValues);
576 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
, $mapper);
577 $address = $this->callAPISuccessGetSingle('Address', ['street_address' => 'Big Mansion']);
578 $this->assertEquals(1, $address['location_type_id']);
579 $this->assertEquals(1, $address['is_primary']);
581 $phone = $this->callAPISuccessGetSingle('Phone', ['phone' => '12334']);
582 $this->assertEquals(1, $phone['location_type_id']);
584 $this->callAPISuccessGetSingle('Email', ['email' => 'bill.gates@microsoft.com']);
586 $contact = $this->callAPISuccessGetSingle('Contact', $contactValues);
587 $this->callAPISuccess('Contact', 'delete', ['id' => $contact['id']]);
591 * Test that address location type id is ignored for dedupe purposes on import.
595 public function testIgnoreLocationTypeId(): void
{
596 // Create a rule that matches on last name and street address.
597 $rgid = $this->createRuleGroup()['id'];
598 $this->callAPISuccess('Rule', 'create', [
599 'dedupe_rule_group_id' => $rgid,
600 'rule_field' => 'last_name',
601 'rule_table' => 'civicrm_contact',
604 $this->callAPISuccess('Rule', 'create', [
605 'dedupe_rule_group_id' => $rgid,
606 'rule_field' => 'street_address',
607 'rule_table' => 'civicrm_address',
610 // Create a contact with an address of location_type_id 1.
612 'contact_type' => 'Individual',
613 'first_name' => 'Original',
614 'last_name' => 'Smith',
616 $contact1 = $this->callAPISuccess('Contact', 'create', $contact1Params);
617 $this->callAPISuccess('Address', 'create', [
618 'contact_id' => $contact1['id'],
619 'location_type_id' => 1,
620 'street_address' => 'Big Mansion',
624 'first_name' => 'New',
625 'last_name' => 'Smith',
626 'street_address' => 'Big Mansion',
629 // We want to import with a location_type_id of 4.
630 $fieldMapping = $this->getFieldMappingFromInput($contactValues, 4);
631 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_SKIP
, CRM_Import_Parser
::DUPLICATE
, $fieldMapping, NULL, $rgid);
632 $address = $this->callAPISuccessGetSingle('Address', ['street_address' => 'Big Mansion']);
633 $this->assertEquals(1, $address['location_type_id']);
634 $contact = $this->callAPISuccessGetSingle('Contact', $contact1Params);
635 $this->callAPISuccess('Contact', 'delete', ['id' => $contact['id']]);
639 * Test that address custom fields can be imported
642 * @throws \CRM_Core_Exception
644 public function testAddressWithCustomData(): void
{
645 $ids = $this->entityCustomGroupWithSingleFieldCreate('Address', 'AddressTest.php');
646 [$contactValues] = $this->setUpBaseContact();
647 $contactValues['nick_name'] = 'Old Bill';
648 $contactValues['external_identifier'] = 'android';
649 $contactValues['street_address'] = 'Big Mansion';
650 $contactValues['custom_' . $ids['custom_field_id']] = 'Update';
651 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
);
652 $address = $this->callAPISuccessGetSingle('Address', ['street_address' => 'Big Mansion', 'return' => 'custom_' . $ids['custom_field_id']]);
653 $this->assertEquals('Update', $address['custom_' . $ids['custom_field_id']]);
657 * Test gender works when you specify the label.
659 * There is an expectation that you can import by label here.
661 * @throws \CRM_Core_Exception
663 public function testGenderLabel() {
665 'first_name' => 'Bill',
666 'last_name' => 'Gates',
667 'email' => 'bill.gates@microsoft.com',
668 'nick_name' => 'Billy-boy',
669 'gender_id' => 'Female',
671 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
);
672 $this->callAPISuccessGetSingle('Contact', $contactValues);
676 * Test greeting imports.
678 * @throws \API_Exception
679 * @throws \CRM_Core_Exception
680 * @throws \CiviCRM_API3_Exception
682 public function testGreetings(): void
{
684 'first_name' => 'Bill',
685 'last_name' => 'Gates',
687 'email_greeting' => 'Dear {contact.prefix_id:label} {contact.first_name} {contact.last_name}',
689 'postal_greeting' => 'Dear {contact.prefix_id:label} {contact.last_name}',
691 'addressee' => '{contact.prefix_id:label}{ }{contact.first_name}{ }{contact.middle_name}{ }{contact.last_name}{ }{contact.suffix_id:label}',
694 $userJobID = $this->getUserJobID([
695 'mapper' => [['first_name'], ['last_name'], ['email_greeting'], ['postal_greeting'], ['addressee']],
696 'onDuplicate' => CRM_Import_Parser
::DUPLICATE_UPDATE
,
698 $parser = new CRM_Contact_Import_Parser_Contact();
699 $parser->setUserJobID($userJobID);
700 $values = array_values($contactValues);
701 $parser->import($values);
702 $contact = Contact
::get(FALSE)->addWhere('last_name', '=', 'Gates')->addSelect('email_greeting_id', 'postal_greeting_id', 'addressee_id')->execute()->first();
703 $this->assertEquals(2, $contact['email_greeting_id']);
704 $this->assertEquals(3, $contact['postal_greeting_id']);
705 $this->assertEquals(1, $contact['addressee_id']);
707 Contact
::delete()->addWhere('id', '=', $contact['id'])->setUseTrash(TRUE)->execute();
709 // Now try again with numbers.
713 $parser->import($values);
714 $contact = Contact
::get(FALSE)->addWhere('last_name', '=', 'Gates')->addSelect('email_greeting_id', 'postal_greeting_id', 'addressee_id')->execute()->first();
715 $this->assertEquals(2, $contact['email_greeting_id']);
716 $this->assertEquals(3, $contact['postal_greeting_id']);
717 $this->assertEquals(1, $contact['addressee_id']);
722 * Test prefix & suffix work when you specify the label.
724 * There is an expectation that you can import by label here.
726 * @throws \API_Exception
727 * @throws \CRM_Core_Exception
728 * @throws \CiviCRM_API3_Exception
730 public function testPrefixLabel(): void
{
731 $this->callAPISuccess('OptionValue', 'create', ['option_group_id' => 'individual_prefix', 'name' => 'new_one', 'label' => 'special', 'value' => 70]);
733 ['name' => 'first_name', 'column_number' => 0],
734 ['name' => 'last_name', 'column_number' => 1],
735 ['name' => 'email', 'column_number' => 2, 'location_type_id' => CRM_Core_PseudoConstant
::getKey('CRM_Core_BAO_Email', 'location_type_id', 'Home')],
736 ['name' => 'prefix_id', 'column_number' => 3],
737 ['name' => 'suffix_id', 'column_number' => 4],
739 $mapperInput = [['first_name'], ['last_name'], ['email', CRM_Core_PseudoConstant
::getKey('CRM_Core_BAO_Email', 'location_type_id', 'Home')], ['prefix_id'], ['suffix_id']];
741 $processor = new CRM_Import_ImportProcessor();
742 $processor->setMappingFields($mapping);
743 $userJobID = $this->getUserJobID(['mapper' => $mapperInput, 'onDuplicate' => CRM_Import_Parser
::DUPLICATE_NOCHECK
]);
744 $processor->setUserJobID($userJobID);
745 $importer = $processor->getImporterObject();
750 'bill.gates@microsoft.com',
754 $importer->import($contactValues);
756 $contact = $this->callAPISuccessGetSingle('Contact', ['first_name' => 'Bill', 'prefix_id' => 'new_one', 'suffix_id' => 'III']);
757 $this->assertEquals('special Bill Gates III', $contact['display_name']);
761 * Test that labels work for importing custom data.
763 * @throws \API_Exception
764 * @throws \CRM_Core_Exception
765 * @throws \CiviCRM_API3_Exception
767 public function testCustomDataLabel(): void
{
768 $this->createCustomGroupWithFieldOfType([], 'select');
770 'first_name' => 'Bill',
771 'last_name' => 'Gates',
772 'email' => 'bill.gates@microsoft.com',
773 'nick_name' => 'Billy-boy',
774 $this->getCustomFieldName('select') => 'Yellow',
776 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
);
777 $contact = $this->callAPISuccessGetSingle('Contact', array_merge($contactValues, ['return' => $this->getCustomFieldName('select')]));
778 $this->assertEquals('Y', $contact[$this->getCustomFieldName('select')]);
782 * Test that names work for importing custom data.
784 * @throws \CRM_Core_Exception
786 public function testCustomDataName() {
787 $this->createCustomGroupWithFieldOfType([], 'select');
789 'first_name' => 'Bill',
790 'last_name' => 'Gates',
791 'email' => 'bill.gates@microsoft.com',
792 'nick_name' => 'Billy-boy',
793 $this->getCustomFieldName('select') => 'Y',
795 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
);
796 $contact = $this->callAPISuccessGetSingle('Contact', array_merge($contactValues, ['return' => $this->getCustomFieldName('select')]));
797 $this->assertEquals('Y', $contact[$this->getCustomFieldName('select')]);
801 * Test importing in the Preferred Language Field
803 * @throws \CRM_Core_Exception
805 public function testPreferredLanguageImport() {
807 'first_name' => 'Bill',
808 'last_name' => 'Gates',
809 'email' => 'bill.gates@microsoft.com',
810 'nick_name' => 'Billy-boy',
811 'preferred_language' => 'English (Australia)',
813 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
);
817 * Test that the import parser adds the address to the primary location.
821 public function testImportDeceased() {
822 [$contactValues] = $this->setUpBaseContact();
823 CRM_Core_Session
::singleton()->set("dateTypes", 1);
824 $contactValues['birth_date'] = '1910-12-17';
825 $contactValues['deceased_date'] = '2010-12-17';
826 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
);
827 $contact = $this->callAPISuccessGetSingle('Contact', $contactValues);
828 $this->assertEquals('1910-12-17', $contact['birth_date']);
829 $this->assertEquals('2010-12-17', $contact['deceased_date']);
830 $this->assertEquals(1, $contact['is_deceased']);
831 $this->callAPISuccess('Contact', 'delete', ['id' => $contact['id']]);
835 * Test that the import parser adds the address to the primary location.
839 public function testImportTwoAddressFirstPrimary(): void
{
840 [$contactValues] = $this->setUpBaseContact();
841 $contactValues['nick_name'] = 'Old Bill';
842 $contactValues['external_identifier'] = 'android';
844 $contactValues['street_address'] = 'Big Mansion';
845 $contactValues['phone'] = 12334;
847 $fieldMapping = $this->getFieldMappingFromInput($contactValues);
848 $contactValues['street_address_2'] = 'Teeny Mansion';
849 $fieldMapping[] = ['name' => 'street_address', 'location_type_id' => 3];
850 $contactValues['phone_2'] = 4444;
851 $fieldMapping[] = ['name' => 'phone', 'location_type_id' => 3, 'phone_type_id' => 1];
853 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
, $fieldMapping);
854 $contact = $this->callAPISuccessGetSingle('Contact', ['external_identifier' => 'android']);
855 $address = $this->callAPISuccess('Address', 'get', ['contact_id' => $contact['id'], 'sequential' => 1]);
857 $this->assertEquals(3, $address['values'][0]['location_type_id']);
858 $this->assertEquals(0, $address['values'][0]['is_primary']);
859 $this->assertEquals('Teeny Mansion', $address['values'][0]['street_address']);
861 $this->assertEquals(1, $address['values'][1]['location_type_id']);
862 $this->assertEquals(1, $address['values'][1]['is_primary']);
863 $this->assertEquals('Big Mansion', $address['values'][1]['street_address']);
865 $phone = $this->callAPISuccess('Phone', 'get', ['contact_id' => $contact['id'], 'sequential' => 1]);
866 $this->assertEquals(1, $phone['values'][0]['location_type_id']);
867 $this->assertEquals(1, $phone['values'][0]['is_primary']);
868 $this->assertEquals(12334, $phone['values'][0]['phone']);
869 $this->assertEquals(3, $phone['values'][1]['location_type_id']);
870 $this->assertEquals(0, $phone['values'][1]['is_primary']);
871 $this->assertEquals(4444, $phone['values'][1]['phone']);
873 $this->callAPISuccess('Contact', 'delete', ['id' => $contact['id']]);
877 * Test importing 2 phones of different types.
879 * @throws \API_Exception
880 * @throws \CRM_Core_Exception
881 * @throws \CiviCRM_API3_Exception
883 public function testImportTwoPhonesDifferentTypes(): void
{
884 $processor = new CRM_Import_ImportProcessor();
885 $processor->setUserJobID($this->getUserJobID([
886 'mapper' => [['first_name'], ['last_name'], ['email'], ['phone', 1, 2], ['phone', 1, 1]],
887 'onDuplicate' => CRM_Import_Parser
::DUPLICATE_UPDATE
,
889 $processor->setMappingFields(
891 ['name' => 'first_name'],
892 ['name' => 'last_name'],
894 ['name' => 'phone', 'location_type_id' => 1, 'phone_type_id' => 2],
895 ['name' => 'phone', 'location_type_id' => 1, 'phone_type_id' => 1],
898 $importer = $processor->getImporterObject();
899 $fields = ['First Name', 'new last name', 'bob@example.com', '1234', '5678'];
900 $importer->import($fields);
901 $contact = $this->callAPISuccessGetSingle('Contact', ['last_name' => 'new last name']);
902 $phones = $this->callAPISuccess('Phone', 'get', ['contact_id' => $contact['id']])['values'];
903 $this->assertCount(2, $phones);
907 * Test that the import parser adds the address to the primary location.
911 public function testImportTwoAddressSecondPrimary(): void
{
912 [$contactValues] = $this->setUpBaseContact();
913 $contactValues['nick_name'] = 'Old Bill';
914 $contactValues['external_identifier'] = 'android';
915 $contactValues['street_address'] = 'Big Mansion';
916 $contactValues['phone'] = 12334;
918 $fieldMapping = $this->getFieldMappingFromInput($contactValues, 3);
920 $contactValues['street_address_2'] = 'Teeny Mansion';
921 $fieldMapping[] = ['name' => 'street_address', 'location_type_id' => 'Primary'];
922 $contactValues['phone_2'] = 4444;
923 $fieldMapping[] = ['name' => 'phone', 'location_type_id' => 'Primary', 'phone_type_id' => 1];
925 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
, $fieldMapping);
926 $contact = $this->callAPISuccessGetSingle('Contact', ['external_identifier' => 'android']);
927 $address = $this->callAPISuccess('Address', 'get', ['contact_id' => $contact['id'], 'sequential' => 1])['values'];
929 $this->assertEquals(1, $address[1]['location_type_id']);
930 $this->assertEquals(1, $address[1]['is_primary']);
931 $this->assertEquals('Teeny Mansion', $address[1]['street_address']);
933 $this->assertEquals(3, $address[0]['location_type_id']);
934 $this->assertEquals(0, $address[0]['is_primary']);
935 $this->assertEquals('Big Mansion', $address[0]['street_address']);
937 $phone = $this->callAPISuccess('Phone', 'get', ['contact_id' => $contact['id'], 'sequential' => 1, 'options' => ['sort' => 'is_primary DESC']])['values'];
938 $this->assertEquals(3, $phone[1]['location_type_id']);
939 $this->assertEquals(0, $phone[1]['is_primary']);
940 $this->assertEquals(12334, $phone[1]['phone']);
941 $this->assertEquals(1, $phone[0]['location_type_id']);
942 $this->assertEquals(1, $phone[0]['is_primary']);
943 $this->assertEquals(4444, $phone[0]['phone']);
945 $this->callAPISuccess('Contact', 'delete', ['id' => $contact['id']]);
949 * Test that the import parser updates the address on the existing primary location.
953 public function testImportPrimaryAddressUpdate(): void
{
954 [$contactValues] = $this->setUpBaseContact(['external_identifier' => 'android']);
955 $contactValues['email'] = 'melinda.gates@microsoft.com';
956 $contactValues['phone'] = '98765';
957 $contactValues['external_identifier'] = 'android';
958 $contactValues['street_address'] = 'Big Mansion';
959 $contactValues['city'] = 'Big City';
960 $contactID = $this->callAPISuccessGetValue('Contact', ['external_identifier' => 'android', 'return' => 'id']);
961 $originalAddress = $this->callAPISuccess('Address', 'create', ['location_type_id' => 2, 'street_address' => 'small house', 'contact_id' => $contactID]);
962 $originalPhone = $this->callAPISuccess('phone', 'create', ['location_type_id' => 2, 'phone' => '1234', 'contact_id' => $contactID, 'phone_type_id' => 1]);
963 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
, []);
964 $phone = $this->callAPISuccessGetSingle('Phone', ['phone' => '98765']);
965 $this->assertEquals(2, $phone['location_type_id']);
966 $this->assertEquals($originalPhone['id'], $phone['id']);
967 $email = $this->callAPISuccess('Email', 'getsingle', ['contact_id' => $contactID]);
968 $address = $this->callAPISuccessGetSingle('Address', ['street_address' => 'Big Mansion']);
969 $this->assertEquals(2, $address['location_type_id']);
970 $this->assertEquals($originalAddress['id'], $address['id']);
971 $this->assertEquals('Big City', $address['city']);
972 $this->callAPISuccessGetSingle('Contact', $contactValues);
976 * Test the determination of whether a custom field is valid.
978 public function testCustomFieldValidation(): void
{
980 $customGroup = $this->customGroupCreate([
981 'extends' => 'Contact',
984 $customField = $this->customFieldOptionValueCreate($customGroup, 'fieldABC', ['html_type' => 'Select', 'serialize' => 1]);
986 'custom_' . $customField['id'] => 'Label1|Label2',
988 $parser = new CRM_Contact_Import_Parser_Contact();
989 $parser->isErrorInCustomData($params, $errorMessage);
990 $this->assertEquals(NULL, $errorMessage);
994 * Test the import validation.
996 * @dataProvider validateDataProvider
999 * @param array $mapper Mapping as entered on MapField form.
1000 * e.g [['first_name']['email', 1]].
1001 * {@see \CRM_Contact_Import_Parser_Contact::getMappingFieldFromMapperInput}
1002 * @param string $expectedError
1003 * @param array $submittedValues
1006 * @throws \API_Exception
1008 public function testValidation(string $csv, array $mapper, string $expectedError = '', $submittedValues = []): void
{
1010 $this->validateCSV($csv, $mapper, $submittedValues);
1012 catch (CRM_Core_Exception
$e) {
1013 $this->assertSame($expectedError, $e->getMessage());
1016 if ($expectedError) {
1017 $this->fail('expected error :' . $expectedError);
1022 * Get combinations to test for validation.
1026 public function validateDataProvider(): array {
1028 'individual_required' => [
1029 'csv' => 'individual_invalid_missing_name.csv',
1030 'mapper' => [['last_name']],
1031 'expected_error' => 'Missing required fields: First Name OR Email Address',
1033 'individual_related_required_met' => [
1034 'csv' => 'individual_valid_with_related_email.csv',
1035 'mapper' => [['first_name'], ['last_name'], ['1_a_b', 'email']],
1036 'expected_error' => '',
1038 'individual_related_required_not_met' => [
1039 'csv' => 'individual_invalid_with_related_phone.csv',
1040 'mapper' => [['first_name'], ['last_name'], ['1_a_b', 'phone', 1, 2]],
1041 'expected_error' => '(Child of) Missing required fields: First Name and Last Name OR Email Address OR External Identifier',
1043 'individual_bad_email' => [
1044 'csv' => 'individual_invalid_email.csv',
1045 'mapper' => [['email', 1], ['first_name'], ['last_name']],
1046 'expected_error' => 'Invalid value for field(s) : Email',
1048 'individual_related_bad_email' => [
1049 'csv' => 'individual_invalid_related_email.csv',
1050 'mapper' => [['1_a_b', 'email', 1], ['first_name'], ['last_name']],
1051 'expected_error' => 'Invalid value for field(s) : (Child of) Email',
1053 'individual_invalid_external_identifier_only' => [
1054 // External identifier is only enough in upgrade mode.
1055 'csv' => 'individual_invalid_external_identifier_only.csv',
1056 'mapper' => [['external_identifier'], ['gender_id']],
1057 'expected_error' => 'Missing required fields: First Name and Last Name OR Email Address',
1059 'individual_invalid_external_identifier_only_update_mode' => [
1060 // External identifier only enough in upgrade mode, so no error here.
1061 'csv' => 'individual_invalid_external_identifier_only.csv',
1062 'mapper' => [['external_identifier'], ['gender_id']],
1063 'expected_error' => '',
1064 'submitted_values' => ['onDuplicate' => CRM_Import_Parser
::DUPLICATE_UPDATE
],
1066 'organization_email_no_organization_name' => [
1067 // Email is only enough in upgrade mode.
1068 'csv' => 'organization_email_no_organization_name.csv',
1069 'mapper' => [['email'], ['phone', 1, 1]],
1070 'expected_error' => 'Missing required fields: Organization Name',
1071 'submitted_values' => ['onDuplicate' => CRM_Import_Parser
::DUPLICATE_SKIP
, 'contactType' => CRM_Import_Parser
::CONTACT_ORGANIZATION
],
1073 'organization_email_no_organization_name_update_mode' => [
1074 // Email is enough in upgrade mode (at least to pass validate).
1075 'csv' => 'organization_email_no_organization_name.csv',
1076 'mapper' => [['email'], ['phone', 1, 1]],
1077 'expected_error' => '',
1078 'submitted_values' => ['onDuplicate' => CRM_Import_Parser
::DUPLICATE_UPDATE
, 'contactType' => CRM_Import_Parser
::CONTACT_ORGANIZATION
],
1086 * @dataProvider importDataProvider
1088 * @throws \API_Exception
1089 * @throws \CRM_Core_Exception
1090 * @throws \League\Csv\CannotInsertRecord
1092 public function testImport($csv, $mapper, $expectedOutcomes = [], $submittedValues = []): void
{
1093 $this->importCSV($csv, $mapper, $submittedValues);
1094 $dataSource = new CRM_Import_DataSource_CSV(UserJob
::get(FALSE)->setSelect(['id'])->execute()->first()['id']);
1095 foreach ($expectedOutcomes as $outcome => $count) {
1096 $this->assertEquals($dataSource->getRowCount([$outcome]), $count);
1099 $_REQUEST['user_job_id'] = $dataSource->getUserJobID();
1100 $_REQUEST['status'] = array_key_first($expectedOutcomes);
1102 CRM_Import_Forms
::outputCSV();
1104 catch (CRM_Core_Exception_PrematureExitException
$e) {
1105 // For now just check it got this far without error.
1110 $this->fail('Should have resulted in a premature exit exception');
1114 * @throws \API_Exception
1115 * @throws \CRM_Core_Exception
1117 public function testImportContactToGroup(): void
{
1118 $this->individualCreate();
1119 $this->importCSV('contact_id_only.csv', [['id']], [
1120 'newGroupName' => 'My New Group',
1122 $dataSource = new CRM_Import_DataSource_CSV(UserJob
::get(FALSE)->setSelect(['id'])->execute()->first()['id']);
1123 $row = $dataSource->getRow();
1124 $this->assertEquals('IMPORTED', $row['_status']);
1125 $group = Group
::get()->addWhere('title', '=', 'My New Group')->execute()->first();
1126 $this->assertCount(1, GroupContact
::get()->addWhere('group_id', '=', $group['id'])->execute());
1130 * Get combinations to test for validation.
1134 public function importDataProvider(): array {
1136 'column_names_casing.csv' => [
1137 'csv' => 'column_names_casing.csv',
1138 'mapper' => [['first_name'], ['last_name'], ['do_not_import'], ['do_not_import'], ['do_not_import'], ['do_not_import']],
1139 'expected_outcomes' => [CRM_Import_Parser
::VALID
=> 1],
1141 'individual_unicode.csv' => [
1142 'csv' => 'individual_unicode.csv',
1143 'mapper' => [['first_name'], ['last_name'], ['url', 1], ['country', 1]],
1144 'expected_outcomes' => [CRM_Import_Parser
::VALID
=> 1],
1146 'individual_invalid_sub_type' => [
1147 'csv' => 'individual_invalid_contact_sub_type.csv',
1148 'mapper' => [['first_name'], ['last_name'], ['contact_sub_type']],
1149 'expected_outcomes' => [CRM_Import_Parser
::ERROR
=> 1],
1151 //Record duplicates multiple contacts
1152 'organization_multiple_duplicates_invalid' => [
1153 'csv' => 'organization_multiple_duplicates_invalid.csv',
1154 'mapper' => [['organization_name'], ['email']],
1155 'expected_outcomes' => [
1156 CRM_Import_Parser
::VALID
=> 2,
1157 CRM_Import_Parser
::ERROR
=> 1,
1159 'submitted_values' => [
1160 'contactType' => CRM_Import_Parser
::CONTACT_ORGANIZATION
,
1163 //Matching this contact based on the de-dupe rule would cause an external ID conflict
1164 'individual_invalid_external_identifier_email_mismatch' => [
1165 'csv' => 'individual_invalid_external_identifier_email_mismatch.csv',
1166 'mapper' => [['first_name'], ['last_name'], ['email'], ['external_identifier']],
1167 'expected_outcomes' => [
1168 CRM_Import_Parser
::VALID
=> 2,
1169 CRM_Import_Parser
::ERROR
=> 1,
1176 * Test the handling of validation when importing genders.
1178 * If it's not gonna import it should fail at the validation stage...
1180 * @throws \API_Exception
1181 * @throws \CRM_Core_Exception
1183 public function testImportGenders(): void
{
1188 ['1_a_b', 'first_name'],
1189 ['1_a_b', 'last_name'],
1190 ['1_a_b', 'gender_id'],
1193 $csv = 'individual_genders.csv';
1194 $this->validateMultiRowCsv($csv, $mapper, 'gender');
1196 $this->importCSV($csv, $mapper);
1197 $contacts = Contact
::get()
1198 ->addWhere('first_name', '=', 'Madame')
1199 ->addSelect('gender_id:name')->execute();
1200 foreach ($contacts as $contact) {
1201 $this->assertEquals('Female', $contact['gender_id:name']);
1203 $this->assertCount(8, $contacts);
1207 * Test importing state country & county.
1209 * @throws \API_Exception
1210 * @throws \CRM_Core_Exception
1212 public function testImportCountryStateCounty(): void
{
1213 $childKey = $this->getRelationships()['Child of']['id'] . '_a_b';
1214 $addressCustomGroupID = $this->createCustomGroup(['extends' => 'Address', 'name' => 'Address']);
1215 $contactCustomGroupID = $this->createCustomGroup(['extends' => 'Contact', 'name' => 'Contact']);
1216 $addressCustomFieldID = $this->createCountryCustomField(['custom_group_id' => $addressCustomGroupID])['id'];
1217 $contactCustomFieldID = $this->createMultiCountryCustomField(['custom_group_id' => $contactCustomGroupID])['id'];
1218 $contactStateCustomFieldID = $this->createStateCustomField(['custom_group_id' => $contactCustomGroupID])['id'];
1219 $customField = 'custom_' . $contactCustomFieldID;
1220 $addressCustomField = 'custom_' . $addressCustomFieldID;
1221 $contactStateCustomField = 'custom_' . $contactStateCustomFieldID;
1230 [$contactStateCustomField],
1232 [$addressCustomField],
1233 // [$addressCustomField, 'state_province'],
1235 [$childKey, 'first_name'],
1236 [$childKey, 'last_name'],
1237 [$childKey, 'email'],
1238 [$childKey, 'state_province'],
1239 [$childKey, 'country'],
1240 [$childKey, 'county'],
1241 // [$childKey, $addressCustomField, 'country'],
1243 // [$childKey, $addressCustomField, 'state_province'],
1245 // [$childKey, $customField, 'country'],
1247 // [$childKey, $customField, 'state_province'],
1249 // mapField Form expects all fields to be mapped.
1253 $csv = 'individual_country_state_county_with_related.csv';
1254 $this->validateMultiRowCsv($csv, $mapper, 'error_value');
1256 $this->importCSV($csv, $mapper);
1257 $contacts = $this->getImportedContacts();
1258 foreach ($contacts as $contact) {
1259 $this->assertEquals(1013, $contact['address'][0]['country_id']);
1260 $this->assertEquals(1640, $contact['address'][0]['state_province_id']);
1262 $this->assertCount(2, $contacts);
1263 $dataSource = $this->getDataSource();
1264 $dataSource->setOffset(4);
1265 $dataSource->setLimit(1);
1266 $row = $dataSource->getRow();
1267 $this->assertEquals(1, $row['_related_contact_matched']);
1271 * Test date validation.
1273 * @dataProvider dateDataProvider
1275 * @param string $csv
1276 * @param int $dateType
1278 * @throws \API_Exception
1279 * @throws \CRM_Core_Exception
1281 public function testValidateDateData($csv, $dateType): void
{
1282 $addressCustomGroupID = $this->createCustomGroup(['extends' => 'Address', 'name' => 'Address']);
1283 $contactCustomGroupID = $this->createCustomGroup(['extends' => 'Contact', 'name' => 'Contact']);
1284 $addressCustomFieldID = $this->createDateCustomField(['custom_group_id' => $addressCustomGroupID])['id'];
1285 $contactCustomFieldID = $this->createDateCustomField(['custom_group_id' => $contactCustomGroupID])['id'];
1291 ['custom_' . $contactCustomFieldID],
1292 ['custom_' . $addressCustomFieldID, 1],
1293 ['street_address', 1],
1296 // Date types should be picked up from submitted values but still some clean up to do.
1297 CRM_Core_Session
::singleton()->set('dateTypes', $dateType);
1298 $this->validateMultiRowCsv($csv, $mapper, 'custom_date_one', ['dateFormats' => $dateType]);
1300 'contact_id.birth_date',
1301 'contact_id.deceased_date',
1302 'contact_id.is_deceased',
1303 'contact_id.custom_' . $contactCustomFieldID,
1304 $addressCustomFieldID,
1306 $contacts = Address
::get()->addWhere('contact_id.first_name', '=', 'Joe')->setSelect($fields)->execute();
1307 foreach ($contacts as $contact) {
1308 foreach ($fields as $field) {
1309 if ($field === 'contact_is_deceased') {
1310 $this->assertTrue($contact[$field]);
1313 $this->assertEquals('2008-09-01', $contact[$field]);
1320 * @throws \API_Exception
1322 public function testImportContactSubTypes(): void
{
1323 ContactType
::create()->setValues([
1325 'label' => 'Infant',
1326 'parent_id:name' => 'Individual',
1331 ['5_a_b', 'organization_name'],
1332 ['contact_sub_type'],
1333 ['5_a_b', 'contact_sub_type'],
1334 // mapField Form expects all fields to be mapped.
1339 $csv = 'individual_contact_sub_types.csv';
1340 $field = 'contact_sub_type';
1342 $this->validateMultiRowCsv($csv, $mapper, $field);
1343 $this->importCSV($csv, $mapper);
1344 $contacts = Contact
::get()
1345 ->addWhere('last_name', '=', 'Green')
1346 ->addSelect('contact_sub_type:name')->execute();
1347 foreach ($contacts as $contact) {
1348 $this->assertEquals(['baby'], $contact['contact_sub_type:name']);
1350 $this->assertCount(3, $contacts);
1354 * Data provider for date tests.
1358 public function dateDataProvider(): array {
1360 'type_1' => ['csv' => 'individual_dates_type1.csv', 'dateType' => CRM_Core_Form_Date
::DATE_yyyy_mm_dd
],
1361 'type_2' => ['csv' => 'individual_dates_type2.csv', 'dateType' => CRM_Core_Form_Date
::DATE_mm_dd_yy
],
1362 'type_4' => ['csv' => 'individual_dates_type4.csv', 'dateType' => CRM_Core_Form_Date
::DATE_mm_dd_yyyy
],
1363 'type_8' => ['csv' => 'individual_dates_type8.csv', 'dateType' => CRM_Core_Form_Date
::DATE_Month_dd_yyyy
],
1364 'type_16' => ['csv' => 'individual_dates_type16.csv', 'dateType' => CRM_Core_Form_Date
::DATE_dd_mon_yy
],
1365 'type_32' => ['csv' => 'individual_dates_type32.csv', 'dateType' => CRM_Core_Form_Date
::DATE_dd_mm_yyyy
],
1370 * Test location importing, including for related contacts.
1372 * @throws \API_Exception
1374 public function testImportLocations(): void
{
1375 $csv = 'individual_locations_with_related.csv';
1376 $relationships = $this->getRelationships();
1378 $childKey = $relationships['Child of']['id'] . '_a_b';
1379 $siblingKey = $relationships['Sibling of']['id'] . '_a_b';
1380 $employeeKey = $relationships['Employee of']['id'] . '_a_b';
1381 $locations = LocationType
::get()->execute()->indexBy('name');
1382 $phoneTypeID = CRM_Core_PseudoConstant
::getKey('CRM_Core_BAO_Phone', 'phone_type_id', 'Phone');
1383 $mobileTypeID = CRM_Core_PseudoConstant
::getKey('CRM_Core_BAO_Phone', 'phone_type_id', 'Mobile');
1384 $skypeTypeID = CRM_Core_PseudoConstant
::getKey('CRM_Core_BAO_IM', 'provider_id', 'Skype');
1385 $mainWebsiteTypeID = CRM_Core_PseudoConstant
::getKey('CRM_Core_BAO_Website', 'website_type_id', 'Main');
1386 $linkedInTypeID = CRM_Core_PseudoConstant
::getKey('CRM_Core_BAO_Website', 'website_type_id', 'LinkedIn');
1387 $homeID = $locations['Home']['id'];
1388 $workID = $locations['Work']['id'];
1393 ['street_address', $homeID],
1395 ['postal_code', $homeID],
1396 ['country', $homeID],
1397 ['state_province', $homeID],
1398 // No location type ID means 'Primary'
1401 ['im', NULL, $skypeTypeID],
1402 ['url', $mainWebsiteTypeID],
1403 ['phone', $homeID, $phoneTypeID],
1404 ['phone_ext', $homeID, $phoneTypeID],
1405 [$childKey, 'first_name'],
1406 [$childKey, 'last_name'],
1407 [$childKey, 'street_address'],
1408 [$childKey, 'city'],
1409 [$childKey, 'country'],
1410 [$childKey, 'state_province'],
1411 [$childKey, 'email', $homeID],
1412 [$childKey, 'signature_text', $homeID],
1413 [$childKey, 'im', $homeID, $skypeTypeID],
1414 [$childKey, 'url', $linkedInTypeID],
1415 // Same location type, different phone typ in these phones
1416 [$childKey, 'phone', $homeID, $phoneTypeID],
1417 [$childKey, 'phone_ext', $homeID, $phoneTypeID],
1418 [$childKey, 'phone', $homeID, $mobileTypeID],
1419 [$childKey, 'phone_ext', $homeID, $mobileTypeID],
1420 [$siblingKey, 'street_address', $homeID],
1421 [$siblingKey, 'city', $homeID],
1422 [$siblingKey, 'country', $homeID],
1423 [$siblingKey, 'state_province', $homeID],
1424 [$siblingKey, 'email', $homeID],
1425 [$siblingKey, 'signature_text', $homeID],
1426 [$siblingKey, 'im', $homeID, $skypeTypeID],
1427 // The 2 is website_type_id (yes, small hard-coding cheat)
1428 [$siblingKey, 'url', $linkedInTypeID],
1429 [$siblingKey, 'phone', $workID, $phoneTypeID],
1430 [$siblingKey, 'phone_ext', $workID, $phoneTypeID],
1431 [$employeeKey, 'organization_name'],
1432 [$employeeKey, 'url', $mainWebsiteTypeID],
1433 [$employeeKey, 'email', $homeID],
1434 [$employeeKey, 'do_not_import'],
1435 [$employeeKey, 'street_address', $homeID],
1436 [$employeeKey, 'supplemental_address_1', $homeID],
1437 [$employeeKey, 'do_not_import'],
1438 // Second website, different type.
1439 [$employeeKey, 'url', $linkedInTypeID],
1442 $this->validateCSV($csv, $mapper);
1444 $this->importCSV($csv, $mapper);
1445 $contacts = $this->getImportedContacts();
1446 $this->assertCount(4, $contacts);
1447 $this->assertCount(1, $contacts['Susie Jones']['phone']);
1448 $this->assertEquals('123', $contacts['Susie Jones']['phone'][0]['phone_ext']);
1449 $this->assertCount(2, $contacts['Mum Jones']['phone']);
1450 $this->assertCount(1, $contacts['sis@example.com']['phone']);
1451 $this->assertCount(0, $contacts['Soccer Superstars']['phone']);
1452 $this->assertCount(1, $contacts['Susie Jones']['website']);
1453 $this->assertCount(1, $contacts['Mum Jones']['website']);
1454 $this->assertCount(0, $contacts['sis@example.com']['website']);
1455 $this->assertCount(2, $contacts['Soccer Superstars']['website']);
1456 $this->assertCount(1, $contacts['Susie Jones']['email']);
1457 $this->assertEquals('Regards', $contacts['Susie Jones']['email'][0]['signature_text']);
1458 $this->assertCount(1, $contacts['Mum Jones']['email']);
1459 $this->assertCount(1, $contacts['sis@example.com']['email']);
1460 $this->assertCount(1, $contacts['Soccer Superstars']['email']);
1461 $this->assertCount(1, $contacts['Susie Jones']['im']);
1462 $this->assertCount(1, $contacts['Mum Jones']['im']);
1463 $this->assertCount(0, $contacts['sis@example.com']['im']);
1464 $this->assertCount(0, $contacts['Soccer Superstars']['im']);
1465 $this->assertCount(1, $contacts['Susie Jones']['address']);
1466 $this->assertCount(1, $contacts['Mum Jones']['address']);
1467 $this->assertCount(1, $contacts['sis@example.com']['address']);
1468 $this->assertCount(1, $contacts['Soccer Superstars']['address']);
1469 $this->assertCount(1, $contacts['Susie Jones']['openid']);
1473 * Test that setting duplicate action to fill doesn't blow away data
1474 * that exists, but does fill in where it's empty.
1478 public function testImportFill(): void
{
1479 // Create a custom field group for testing.
1480 $this->createCustomGroup([
1481 'title' => 'importFillGroup',
1482 'extends' => 'Individual',
1483 'is_active' => TRUE,
1485 $customGroupID = $this->ids
['CustomGroup']['importFillGroup'];
1487 // Add two custom fields.
1489 'custom_group_id' => $customGroupID,
1490 'label' => 'importFillField1',
1491 'html_type' => 'Select',
1492 'data_type' => 'String',
1493 'option_values' => [
1498 $result = $this->callAPISuccess('custom_field', 'create', $api_params);
1499 $customField1 = $result['id'];
1502 'custom_group_id' => $customGroupID,
1503 'label' => 'importFillField2',
1504 'html_type' => 'Select',
1505 'data_type' => 'String',
1506 'option_values' => [
1511 $result = $this->callAPISuccess('custom_field', 'create', $api_params);
1512 $customField2 = $result['id'];
1514 // Now set up values.
1515 $original_gender = 'Male';
1516 $original_custom1 = 'foo';
1517 $original_email = 'test-import-fill@example.org';
1519 $import_gender = 'Female';
1520 $import_custom1 = 'bar';
1521 $import_job_title = 'Chief data importer';
1522 $import_custom2 = 'baz';
1524 // Create contact with both one known core field and one custom
1527 'contact_type' => 'Individual',
1528 'email' => $original_email,
1529 'gender' => $original_gender,
1530 'custom_' . $customField1 => $original_custom1,
1532 $result = $this->callAPISuccess('contact', 'create', $api_params);
1533 $contact_id = $result['id'];
1537 'email' => $original_email,
1538 'gender_id' => $import_gender,
1539 'custom_' . $customField1 => $import_custom1,
1540 'job_title' => $import_job_title,
1541 'custom_' . $customField2 => $import_custom2,
1544 $this->runImport($import, CRM_Import_Parser
::DUPLICATE_FILL
, CRM_Import_Parser
::VALID
);
1547 'gender' => $original_gender,
1548 'custom_' . $customField1 => $original_custom1,
1549 'job_title' => $import_job_title,
1550 'custom_' . $customField2 => $import_custom2,
1554 'id' => $contact_id,
1557 'custom_' . $customField1,
1559 'custom_' . $customField2,
1562 $result = civicrm_api3('Contact', 'get', $params);
1563 $values = array_pop($result['values']);
1564 foreach ($expected as $field => $expected_value) {
1565 if (!isset($values[$field])) {
1566 $given_value = NULL;
1569 $given_value = $values[$field];
1573 // job_title: Chief Data Importer
1574 // importFillField1: foo
1575 // importFillField2: baz
1576 $this->assertEquals($expected_value, $given_value, "$field properly handled during Fill import");
1581 * CRM-19888 default country should be used if ambiguous.
1583 * @throws \API_Exception
1584 * @throws \CRM_Core_Exception
1585 * @throws \CiviCRM_API3_Exception
1587 public function testImportAmbiguousStateCountry(): void
{
1588 $this->callAPISuccess('Setting', 'create', ['defaultContactCountry' => 1228]);
1589 $countries = CRM_Core_PseudoConstant
::country(FALSE, FALSE);
1590 $this->callAPISuccess('Setting', 'create', ['countryLimit' => [array_search('United States', $countries, TRUE), array_search('Guyana', $countries, TRUE), array_search('Netherlands', $countries, TRUE)]]);
1591 $this->callAPISuccess('Setting', 'create', ['provinceLimit' => [array_search('United States', $countries, TRUE), array_search('Guyana', $countries, TRUE), array_search('Netherlands', $countries, TRUE)]]);
1592 [$contactValues] = $this->setUpBaseContact();
1594 // Set up the field mapping - this looks like an array per mapping as saved in
1595 // civicrm_mapping_field - eg ['name' => 'street_address', 'location_type_id' => 1],
1597 foreach (array_keys($contactValues) as $fieldName) {
1598 $fieldMapping[] = ['name' => $fieldName];
1602 'street_address' => 'PO Box 2716',
1604 'state_province' => 'UT',
1605 'postal_code' => 84049,
1606 'country' => 'United States',
1609 $homeLocationTypeID = CRM_Core_PseudoConstant
::getKey('CRM_Core_BAO_Address', 'location_type_id', 'Home');
1610 $workLocationTypeID = CRM_Core_PseudoConstant
::getKey('CRM_Core_BAO_Address', 'location_type_id', 'Work');
1611 foreach ($addressValues as $field => $value) {
1612 $contactValues['home_' . $field] = $value;
1613 $contactValues['work_' . $field] = $value;
1614 $fieldMapping[] = ['name' => $field, 'location_type_id' => $homeLocationTypeID];
1615 $fieldMapping[] = ['name' => $field, 'location_type_id' => $workLocationTypeID];
1617 // The value is set to nothing to show it will be calculated.
1618 $contactValues['work_country'] = '';
1620 $this->runImport($contactValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
, $fieldMapping);
1621 $addresses = $this->callAPISuccess('Address', 'get', ['contact_id' => ['>' => 2], 'sequential' => 1]);
1622 $this->assertEquals(2, $addresses['count']);
1623 $this->assertEquals(array_search('United States', $countries, TRUE), $addresses['values'][0]['country_id']);
1624 $this->assertEquals(array_search('United States', $countries, TRUE), $addresses['values'][1]['country_id']);
1628 * Test importing fields with various options.
1630 * Ensure we can import multiple preferred_communication_methods, single
1631 * gender, and single preferred language using both labels and values.
1633 * @throws \API_Exception
1634 * @throws \CRM_Core_Exception
1635 * @throws \CiviCRM_API3_Exception
1637 public function testImportFieldsWithVariousOptions(): void
{
1638 $processor = new CRM_Import_ImportProcessor();
1639 $processor->setUserJobID($this->getUserJobID([
1640 'mapper' => [['first_name'], ['last_name'], ['preferred_communication_method'], ['gender_id'], ['preferred_language']],
1641 'onDuplicate' => CRM_Import_Parser
::DUPLICATE_NOCHECK
,
1643 $processor->setMappingFields(
1645 ['name' => 'first_name'],
1646 ['name' => 'last_name'],
1647 ['name' => 'preferred_communication_method'],
1648 ['name' => 'gender_id'],
1649 ['name' => 'preferred_language'],
1652 $importer = $processor->getImporterObject();
1653 $fields = ['Ima', 'Texter', 'SMS,Phone', 'Female', 'Danish'];
1654 $importer->import($fields);
1655 $contact = $this->callAPISuccessGetSingle('Contact', ['last_name' => 'Texter']);
1657 $this->assertEquals([4, 1], $contact['preferred_communication_method'], 'Import multiple preferred communication methods using labels.');
1658 $this->assertEquals(1, $contact['gender_id'], 'Import gender with label.');
1659 $this->assertEquals('da_DK', $contact['preferred_language'], 'Import preferred language with label.');
1660 $this->callAPISuccess('Contact', 'delete', ['id' => $contact['id']]);
1662 $importer = $processor->getImporterObject();
1663 $fields = ['Ima', 'Texter', '4,1', '1', 'da_DK'];
1664 $importer->import($fields);
1665 $contact = $this->callAPISuccessGetSingle('Contact', ['last_name' => 'Texter']);
1667 $this->assertEquals([4, 1], $contact['preferred_communication_method'], 'Import multiple preferred communication methods using values.');
1668 $this->assertEquals(1, $contact['gender_id'], 'Import gender with id.');
1669 $this->assertEquals('da_DK', $contact['preferred_language'], 'Import preferred language with value.');
1673 * Run the import parser.
1675 * @param array $originalValues
1677 * @param int $onDuplicateAction
1678 * @param int $expectedResult
1679 * @param array|null $fieldMapping
1680 * Array of field mappings in the format used in civicrm_mapping_field.
1681 * @param array|null $fields
1682 * Array of field names. Will be calculated from $originalValues if not passed in, but
1683 * that method does not cope with duplicates.
1684 * @param int|null $ruleGroupId
1685 * To test against a specific dedupe rule group, pass its ID as this argument.
1687 * @throws \API_Exception
1688 * @throws \CRM_Core_Exception
1689 * @throws \CiviCRM_API3_Exception
1691 protected function runImport(array $originalValues, $onDuplicateAction, $expectedResult, $fieldMapping = [], $fields = NULL, int $ruleGroupId = NULL): void
{
1692 $values = array_values($originalValues);
1693 // Stand in for row number.
1696 if ($fieldMapping) {
1698 foreach ($fieldMapping as $mappedField) {
1699 $fields[] = $mappedField['name'];
1701 $mapper = $this->getMapperFromFieldMappingFormat($fieldMapping);
1705 $fields = array_keys($originalValues);
1708 foreach ($fields as $field) {
1711 in_array($field, ['phone', 'email'], TRUE) ?
'Primary' : NULL,
1712 $field === 'phone' ?
1 : NULL,
1716 $this->userJobID
= $this->getUserJobID(['mapper' => $mapper, 'onDuplicate' => $onDuplicateAction, 'dedupe_rule_id' => $ruleGroupId]);
1717 $parser = new CRM_Contact_Import_Parser_Contact();
1718 $parser->setUserJobID($this->userJobID
);
1719 $parser->_dedupeRuleGroupID
= $ruleGroupId;
1722 $result = $parser->import($values);
1723 $dataSource = $this->getDataSource();
1724 if ($result === FALSE && $expectedResult !== FALSE) {
1725 // Import is moving away from returning a status - this is a better way to check
1726 $this->assertGreaterThan(0, $dataSource->getRowCount([$expectedResult]));
1729 $this->assertEquals($expectedResult, $result, 'Return code from parser import was not as expected');
1733 * @param string $csv
1734 * @param array $mapper Mapping as entered on MapField form.
1735 * e.g [['first_name']['email', 1]].
1736 * {@see \CRM_Contact_Import_Parser_Contact::getMappingFieldFromMapperInput}
1737 * @param array $submittedValues
1740 * @throws \API_Exception
1741 * @throws \Civi\API\Exception\UnauthorizedException
1743 protected function getDataSourceAndParser(string $csv, array $mapper, array $submittedValues): array {
1744 $userJobID = $this->getUserJobID(array_merge([
1745 'uploadFile' => ['name' => __DIR__
. '/../Form/data/' . $csv],
1746 'skipColumnHeader' => TRUE,
1747 'fieldSeparator' => ',',
1748 'onDuplicate' => CRM_Import_Parser
::DUPLICATE_SKIP
,
1749 'contactType' => CRM_Import_Parser
::CONTACT_INDIVIDUAL
,
1750 'mapper' => $mapper,
1751 'dataSource' => 'CRM_Import_DataSource_CSV',
1752 ], $submittedValues));
1754 $dataSource = new CRM_Import_DataSource_CSV($userJobID);
1755 $parser = new CRM_Contact_Import_Parser_Contact();
1756 $parser->setUserJobID($userJobID);
1758 return [$dataSource, $parser];
1762 * @param int $contactID
1764 * @throws \API_Exception
1766 protected function addChild(int $contactID): void
{
1767 $relatedContactID = $this->individualCreate();
1768 $relationshipTypeID = RelationshipType
::create()->setValues([
1769 'name_a_b' => 'Dad to',
1770 'name_b_a' => 'Sleep destroyer of',
1771 'contact_type_a' => 'Individual',
1772 'contact_type_b' => 'Individual',
1773 'contact_sub_type_a' => 'Parent',
1774 ])->execute()->first()['id'];
1775 Relationship
::create()->setValues([
1776 'relationship_type_id' => $relationshipTypeID,
1777 'contact_id_a' => $contactID,
1778 'contact_id_b' => $relatedContactID,
1780 $this->relationships
['Dad to'] = $relationshipTypeID . '_a_b';
1785 * @throws \API_Exception
1786 * @throws \Civi\API\Exception\UnauthorizedException
1788 private function getRelationships(): array {
1789 if (empty($this->relationships
)) {
1790 $this->relationships
= (array) RelationshipType
::get()
1791 ->addSelect('name_a_b', 'id')
1793 ->indexBy('name_a_b');
1795 return $this->relationships
;
1799 * Get the mapper array from the field mapping array format.
1801 * The fieldMapping format is the same as the civicrm_mapping_field
1802 * table and is readable - eg ['name' => 'street_address', 'location_type_id' => 1].
1804 * The mapper format is converted to the array that would be submitted by the form
1805 * and is keyed by row number with the meaning of the fields depending on
1808 * @param array $fieldMapping
1812 protected function getMapperFromFieldMappingFormat($fieldMapping): array {
1814 foreach ($fieldMapping as $mapping) {
1816 if (!empty($mapping['relationship_type_id'])) {
1817 $mappedRow[] = $mapping['relationship_type_id'] . $mapping['relationship_direction'];
1819 $mappedRow[] = $mapping['name'];
1820 if (!empty($mapping['location_type_id'])) {
1821 $mappedRow[] = $mapping['location_type_id'];
1823 elseif (in_array($mapping['name'], ['email', 'phone'], TRUE)) {
1824 // Lets make it easy on test writers by assuming primary if not specified.
1825 $mappedRow[] = 'Primary';
1827 if (!empty($mapping['im_provider_id'])) {
1828 $mappedRow[] = $mapping['im_provider_id'];
1830 if (!empty($mapping['phone_type_id'])) {
1831 $mappedRow[] = $mapping['phone_type_id'];
1833 if (!empty($mapping['website_type_id'])) {
1834 $mappedRow[] = $mapping['website_type_id'];
1836 $mapper[] = $mappedRow;
1842 * Get a suitable mapper for the array with location defaults.
1844 * This function is designed for when 'good assumptions' are required rather
1845 * than careful mapping.
1847 * @param array $contactValues
1848 * @param string|int $defaultLocationType
1852 protected function getFieldMappingFromInput(array $contactValues, $defaultLocationType = 'Primary'): array {
1854 foreach (array_keys($contactValues) as $fieldName) {
1855 $mapping = ['name' => $fieldName];
1856 $addressFields = $this->callAPISuccess('Address', 'getfields', [])['values'];
1857 unset($addressFields['contact_id'], $addressFields['id'], $addressFields['location_type_id']);
1858 $locationFields = array_merge(['email', 'phone', 'im', 'openid'], array_keys($addressFields));
1859 if (in_array($fieldName, $locationFields, TRUE)) {
1860 $mapping['location_type_id'] = $defaultLocationType;
1862 if ($fieldName === 'phone') {
1863 $mapping['phone_type_id'] = 1;
1865 $mapper[] = $mapping;
1871 * Test mapping fields within the Parser class.
1873 * @throws \API_Exception
1874 * @throws \Civi\API\Exception\UnauthorizedException
1876 public function testMapFields(): void
{
1877 $parser = new CRM_Contact_Import_Parser_Contact(
1878 // Array of field names
1879 ['first_name', 'phone', NULL, 'im', NULL],
1880 // Array of location types, ie columns 2 & 4 have types.
1881 [NULL, 1, NULL, 1, NULL],
1882 // Array of phone types
1883 [NULL, 1, NULL, NULL, NULL],
1884 // Array of im provider types
1885 [NULL, NULL, NULL, 1, NULL],
1886 // Array of filled in relationship values.
1887 [NULL, NULL, '5_a_b', NULL, '5_a_b'],
1888 // Array of the contact type to map to - note this can be determined from ^^
1889 [NULL, NULL, 'Organization', NULL, 'Organization'],
1890 // Related contact field names
1891 [NULL, NULL, 'url', NULL, 'phone'],
1892 // Related contact location types
1893 [NULL, NULL, NULL, NULL, 1],
1894 // Related contact phone types
1895 [NULL, NULL, NULL, NULL, 1],
1896 // Related contact im provider types
1897 [NULL, NULL, NULL, NULL, NULL],
1899 [NULL, NULL, NULL, NULL, NULL],
1900 // Related contact website types
1901 [NULL, NULL, 1, NULL, NULL]
1903 $parser->setUserJobID($this->getUserJobID([
1907 ['5_a_b', 'url', 1],
1909 ['5_a_b', 'phone', 1, 1],
1913 $params = $parser->getMappedRow(
1914 ['Bob', '123', 'https://example.org', 'my-handle', '456']
1916 $this->assertEquals([
1917 'first_name' => 'Bob',
1921 'location_type_id' => 1,
1922 'phone_type_id' => 1,
1927 'contact_type' => 'Organization',
1928 'contact_sub_type' => NULL,
1930 'https://example.org' => [
1931 'url' => 'https://example.org',
1932 'website_type_id' => 1,
1938 'location_type_id' => 1,
1939 'phone_type_id' => 1,
1946 'name' => 'my-handle',
1947 'location_type_id' => 1,
1951 'contact_type' => 'Individual',
1956 * Test that import parser will not match the imported primary to
1957 * an existing contact via the related contacts fields.
1959 * Currently fails because CRM_Dedupe_Finder::formatParams($input, $contactType);
1960 * called in getDuplicateContacts flattens the contact array adding the
1961 * related contacts values to the primary contact.
1963 * https://github.com/civicrm/civicrm-core/blob/ca13ec46eae2042604e4e106c6cb3dc0439db3e2/CRM/Dedupe/Finder.php#L238
1965 * @throws \API_Exception
1966 * @throws \CRM_Core_Exception
1967 * @throws \CiviCRM_API3_Exception
1968 * @throws \Civi\API\Exception\UnauthorizedException
1970 public function testImportParserDoesNotMatchPrimaryToRelated(): void
{
1971 $this->individualCreate([
1972 'first_name' => 'Bob',
1973 'last_name' => 'Dobbs',
1974 'email' => 'tim.cook@apple.com',
1982 $values = ['Alok', 'Patel', 'tim.cook@apple.com', 1];
1984 $userJobID = $this->getUserJobID([
1985 'mapper' => $mapper,
1986 'onDuplicate' => CRM_Import_Parser
::DUPLICATE_UPDATE
,
1989 $parser = new CRM_Contact_Import_Parser_Contact();
1990 $parser->setUserJobID($userJobID);
1992 $parser->import($values);
1993 $this->callAPISuccessGetSingle('Contact', [
1994 'first_name' => 'Bob',
1995 'last_name' => 'Dobbs',
1996 'email' => 'tim.cook@apple.com',
1998 $contact = $this->callAPISuccessGetSingle('Contact', ['first_name' => 'Alok', 'last_name' => 'Patel']);
1999 $this->assertEmpty($contact['email']);
2003 * Set up the underlying contact.
2005 * @param array $params
2006 * Optional extra parameters to set.
2009 * @throws \CRM_Core_Exception
2011 protected function setUpBaseContact($params = []) {
2012 $originalValues = array_merge([
2013 'first_name' => 'Bill',
2014 'last_name' => 'Gates',
2015 'email' => 'bill.gates@microsoft.com',
2016 'nick_name' => 'Billy-boy',
2018 $this->runImport($originalValues, CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::VALID
);
2019 $result = $this->callAPISuccessGetSingle('Contact', $originalValues);
2020 return [$originalValues, $result];
2025 * @throws \API_Exception
2026 * @throws \Civi\API\Exception\UnauthorizedException
2028 protected function getUserJobID($submittedValues = []) {
2029 $userJobID = UserJob
::create()->setValues([
2031 'submitted_values' => array_merge([
2032 'contactType' => CRM_Import_Parser
::CONTACT_INDIVIDUAL
,
2033 'contactSubType' => '',
2034 'doGeocodeAddress' => 0,
2036 'dataSource' => 'CRM_Import_DataSource_SQL',
2037 'sqlQuery' => 'SELECT first_name FROM civicrm_contact',
2038 'onDuplicate' => CRM_Import_Parser
::DUPLICATE_SKIP
,
2039 'dedupe_rule_id' => NULL,
2040 'dateFormats' => CRM_Core_Form_Date
::DATE_yyyy_mm_dd
,
2041 ], $submittedValues),
2043 'status_id:name' => 'draft',
2044 'job_type' => 'contact_import',
2045 ])->execute()->first()['id'];
2046 if ($submittedValues['dataSource'] ??
NULL === 'CRM_Import_DataSource') {
2047 $dataSource = new CRM_Import_DataSource_CSV($userJobID);
2050 $dataSource = new CRM_Import_DataSource_SQL($userJobID);
2052 $dataSource->initialize();
2057 * Test geocode validation.
2059 * @throws \API_Exception
2060 * @throws \CRM_Core_Exception
2062 public function testImportGeocodes(): void
{
2069 $csv = 'individual_geocode.csv';
2070 $this->validateMultiRowCsv($csv, $mapper, 'GeoCode2');
2074 * Validate the csv file values.
2076 * @param string $csv Name of csv file.
2077 * @param array $mapper Mapping as entered on MapField form.
2078 * e.g [['first_name']['email', 1]].
2079 * {@see \CRM_Contact_Import_Parser_Contact::getMappingFieldFromMapperInput}
2080 * @param array $submittedValues
2081 * Any submitted values overrides.
2083 * @throws \API_Exception
2085 protected function validateCSV(string $csv, array $mapper, array $submittedValues = []): void
{
2086 [$dataSource, $parser] = $this->getDataSourceAndParser($csv, $mapper, $submittedValues);
2087 $parser->validateValues(array_values($dataSource->getRow()));
2091 * Import the csv file values.
2093 * This function uses a flow that mimics the UI flow.
2095 * @param string $csv Name of csv file.
2096 * @param array $mapper Mapping as entered on MapField form.
2097 * e.g [['first_name']['email', 1]].
2098 * {@see \CRM_Contact_Import_Parser_Contact::getMappingFieldFromMapperInput}
2099 * @param array $submittedValues
2101 protected function importCSV(string $csv, array $mapper, array $submittedValues = []): void
{
2102 $submittedValues = array_merge([
2103 'uploadFile' => ['name' => __DIR__
. '/../Form/data/' . $csv],
2104 'skipColumnHeader' => TRUE,
2105 'fieldSeparator' => ',',
2106 'contactType' => CRM_Import_Parser
::CONTACT_INDIVIDUAL
,
2107 'mapper' => $mapper,
2108 'dataSource' => 'CRM_Import_DataSource_CSV',
2109 'file' => ['name' => $csv],
2110 'dateFormats' => CRM_Core_Form_Date
::DATE_yyyy_mm_dd
,
2111 'onDuplicate' => CRM_Import_Parser
::DUPLICATE_UPDATE
,
2113 ], $submittedValues);
2114 $form = $this->getFormObject('CRM_Contact_Import_Form_DataSource', $submittedValues);
2115 $values = $_SESSION['_' . $form->controller
->_name
. '_container']['values'];
2118 $form->postProcess();
2119 $this->userJobID
= $form->getUserJobID();
2121 // This gets reset in DataSource so re-do....
2122 $_SESSION['_' . $form->controller
->_name
. '_container']['values'] = $values;
2124 /* @var CRM_Contact_Import_Form_MapField $form */
2125 $form = $this->getFormObject('CRM_Contact_Import_Form_MapField', $submittedValues);
2127 $form->setUserJobID($this->userJobID
);
2129 $form->postProcess();
2130 /* @var CRM_Contact_Import_Form_MapField $form */
2131 $form = $this->getFormObject('CRM_Contact_Import_Form_Preview', $submittedValues);
2132 $form->setUserJobID($this->userJobID
);
2136 $form->postProcess();
2138 catch (CRM_Core_Exception_PrematureExitException
$e) {
2139 $queue = Civi
::queue('user_job_' . $this->userJobID
);
2140 $runner = new CRM_Queue_Runner([
2142 'errorMode' => CRM_Queue_Runner
::ERROR_ABORT
,
2149 * Validate a csv with multiple rows in it.
2151 * @param string $csv
2152 * @param array $mapper Mapping as entered on MapField form.
2153 * e.g [['first_name']['email', 1]].
2154 * @param string $field
2155 * Name of the field whose data should be output in the error message.
2156 * @param array $submittedValues
2157 * Values submitted in the form process.
2159 * @throws \API_Exception
2160 * @throws \CRM_Core_Exception
2161 * @throws \Civi\API\Exception\UnauthorizedException
2163 private function validateMultiRowCsv(string $csv, array $mapper, string $field, $submittedValues = []): void
{
2164 /* @var CRM_Import_DataSource_CSV $dataSource */
2165 /* @var \CRM_Contact_Import_Parser_Contact $parser */
2166 [$dataSource, $parser] = $this->getDataSourceAndParser($csv, $mapper, $submittedValues);
2167 while ($values = $dataSource->getRow()) {
2169 $parser->validateValues(array_values($values));
2170 if ($values['expected'] !== 'Valid') {
2171 $this->fail($values[$field] . ' should not have been valid');
2174 catch (CRM_Core_Exception
$e) {
2175 if ($values['expected'] !== 'Invalid') {
2176 $this->fail($values[$field] . ' should have been valid');
2180 UserJob
::delete()->addWhere('id', '=', $parser->getUserJobID())->execute();
2184 * Get the contacts we imported (Susie Jones & family).
2187 * @throws \API_Exception
2189 public function getImportedContacts(): array {
2190 return (array) Contact
::get()
2191 ->addWhere('display_name', 'IN', [
2195 'Soccer Superstars',
2197 ->addChain('phone', Phone
::get()->addWhere('contact_id', '=', '$id'))
2198 ->addChain('address', Address
::get()->addWhere('contact_id', '=', '$id'))
2199 ->addChain('website', Website
::get()->addWhere('contact_id', '=', '$id'))
2200 ->addChain('im', IM
::get()->addWhere('contact_id', '=', '$id'))
2201 ->addChain('email', Email
::get()->addWhere('contact_id', '=', '$id'))
2202 ->addChain('openid', OpenID
::get()->addWhere('contact_id', '=', '$id'))
2203 ->execute()->indexBy('display_name');
2207 * Test that import parser will not throw error if Related Contact is not found via passed in External ID.
2209 * If the organization is present it will create it - otherwise fail without error.
2211 * @dataProvider getBooleanDataProvider
2213 * @throws \API_Exception
2214 * @throws \CRM_Core_Exception
2215 * @throws \CiviCRM_API3_Exception
2217 public function testImportParserWithExternalIdForRelationship(bool $isOrganizationProvided): void
{
2218 $contactImportValues = [
2219 'first_name' => 'Alok',
2220 'last_name' => 'Patel',
2221 'Employee of' => 'related external identifier',
2222 'organization_name' => $isOrganizationProvided ?
'Big shop' : '',
2228 ['5_a_b', 'external_identifier'],
2229 ['5_a_b', 'organization_name'],
2232 $values = array_values($contactImportValues);
2233 $userJobID = $this->getUserJobID([
2234 'mapper' => $mapper,
2237 $parser = new CRM_Contact_Import_Parser_Contact();
2238 $parser->setUserJobID($userJobID);
2241 $parser->import($values);
2242 $this->callAPISuccessGetCount('Contact', ['organization_name' => 'Big shop'], $isOrganizationProvided ?
2 : 0);