Add location import testing
[civicrm-core.git] / tests / phpunit / CRM / Contact / Import / Parser / ContactTest.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This code is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 * @file
14 * File for the CRM_Contact_Imports_Parser_ContactTest class.
15 */
16
17 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;
23
24 /**
25 * Test contact import parser.
26 *
27 * @package CiviCRM
28 * @group headless
29 */
30 class CRM_Contact_Import_Parser_ContactTest extends CiviUnitTestCase {
31 use CRMTraits_Custom_CustomDataTrait;
32
33 /**
34 * Main entity for the class.
35 *
36 * @var string
37 */
38 protected $entity = 'Contact';
39
40 /**
41 * Tear down after test.
42 */
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();
47 parent::tearDown();
48 }
49
50 /**
51 * Test that import parser will add contact with employee of relationship.
52 *
53 * @throws \API_Exception
54 * @throws \CRM_Core_Exception
55 * @throws \CiviCRM_API3_Exception
56 * @throws \Civi\API\Exception\UnauthorizedException
57 */
58 public function testImportParserWithEmployeeOfRelationship(): void {
59 $this->organizationCreate([
60 'organization_name' => 'Agileware',
61 'legal_name' => 'Agileware',
62 ]);
63 $contactImportValues = [
64 'first_name' => 'Alok',
65 'last_name' => 'Patel',
66 'Employee of' => 'Agileware',
67 ];
68
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']],
73 ]);
74
75 $parser = new CRM_Contact_Import_Parser_Contact($fields);
76 $parser->setUserJobID($userJobID);
77 $parser->_onDuplicate = CRM_Import_Parser::DUPLICATE_UPDATE;
78 $parser->init();
79
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',
85 ]);
86 }
87
88 /**
89 * Test that import parser will not fail when same external_identifier found
90 * of deleted contact.
91 *
92 * @throws \API_Exception
93 * @throws \CRM_Core_Exception
94 * @throws \CiviCRM_API3_Exception
95 */
96 public function testImportParserWithDeletedContactExternalIdentifier(): void {
97 $contactId = $this->individualCreate([
98 'external_identifier' => 'ext-1',
99 ]);
100 $this->callAPISuccess('Contact', 'delete', ['id' => $contactId]);
101 [$originalValues, $result] = $this->setUpBaseContact([
102 'external_identifier' => 'ext-1',
103 ]);
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);
109 }
110
111 /**
112 * Test import parser will update based on a rule match.
113 *
114 * In this case the contact has no external identifier.
115 *
116 * @throws \API_Exception
117 * @throws \CRM_Core_Exception
118 * @throws \CiviCRM_API3_Exception
119 */
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);
127 }
128
129 /**
130 * Test import parser will update based on a custom rule match.
131 *
132 * In this case the contact has no external identifier.
133 *
134 * @throws \API_Exception
135 * @throws \CRM_Core_Exception
136 * @throws \CiviCRM_API3_Exception
137 */
138 public function testImportParserWithUpdateWithCustomRule(): void {
139 $this->createCustomGroupWithFieldsOfAllTypes();
140
141 $ruleGroup = $this->callAPISuccess('RuleGroup', 'create', [
142 'contact_type' => 'Individual',
143 'threshold' => 10,
144 'used' => 'General',
145 'name' => 'TestRule',
146 'title' => 'TestRule',
147 'is_reserved' => 0,
148 ]);
149 $this->callAPISuccess('Rule', 'create', [
150 'dedupe_rule_group_id' => $ruleGroup['id'],
151 'rule_table' => $this->getCustomGroupTable(),
152 'rule_weight' => 10,
153 'rule_field' => $this->getCustomFieldColumnName('text'),
154 ]);
155
156 $extra = [
157 $this->getCustomFieldName('select_string') => 'Yellow',
158 $this->getCustomFieldName('text') => 'Duplicate',
159 ];
160
161 [$originalValues, $result] = $this->setUpBaseContact($extra);
162
163 $contactValues = [
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',
170 ];
171
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);
176
177 $foundDupes = CRM_Dedupe_Finder::dupes($ruleGroup['id']);
178 $this->assertCount(0, $foundDupes);
179 }
180
181 /**
182 * Test import parser will update based on a custom rule match.
183 *
184 * In this case the contact has no external identifier.
185 *
186 * @throws \API_Exception
187 * @throws \CRM_Core_Exception
188 * @throws \CiviCRM_API3_Exception
189 */
190 public function testImportParserWithUpdateWithCustomRuleNoExternalIDMatch(): void {
191 $this->createCustomGroupWithFieldsOfAllTypes();
192
193 $ruleGroup = $this->callAPISuccess('RuleGroup', 'create', [
194 'contact_type' => 'Individual',
195 'threshold' => 10,
196 'used' => 'General',
197 'name' => 'TestRule',
198 'title' => 'TestRule',
199 'is_reserved' => 0,
200 ]);
201 $this->callAPISuccess('Rule', 'create', [
202 'dedupe_rule_group_id' => $ruleGroup['id'],
203 'rule_table' => $this->getCustomGroupTable(),
204 'rule_weight' => 10,
205 'rule_field' => $this->getCustomFieldColumnName('text'),
206 ]);
207
208 $extra = [
209 $this->getCustomFieldName('select_string') => 'Yellow',
210 $this->getCustomFieldName('text') => 'Duplicate',
211 'external_identifier' => 'ext-2',
212 ];
213
214 [$originalValues, $result] = $this->setUpBaseContact($extra);
215
216 $contactValues = [
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',
224 ];
225
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);
230
231 $foundDupes = CRM_Dedupe_Finder::dupes($ruleGroup['id']);
232 $this->assertCount(0, $foundDupes);
233 }
234
235 /**
236 * Test import parser will update contacts with an external identifier.
237 *
238 * This is the basic test where the identifier matches the import parameters.
239 *
240 * @throws \Exception
241 */
242 public function testImportParserWithUpdateWithExternalIdentifier(): void {
243 [$originalValues, $result] = $this->setUpBaseContact(['external_identifier' => 'windows']);
244
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']);
247
248 $originalValues['nick_name'] = 'Old Bill';
249 $this->runImport($originalValues, CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::VALID);
250 $originalValues['id'] = $result['id'];
251
252 $this->assertEquals('Old Bill', $this->callAPISuccessGetValue('Contact', ['id' => $result['id'], 'return' => 'nick_name']));
253 $this->callAPISuccessGetSingle('Contact', $originalValues);
254 }
255
256 /**
257 * Test updating an existing contact with external_identifier match but subtype mismatch.
258 *
259 * The subtype is updated, as there is no conflicting contact data.
260 *
261 * @throws \Exception
262 */
263 public function testImportParserWithUpdateWithExternalIdentifierSubtypeChange(): void {
264 $contactID = $this->individualCreate(['external_identifier' => 'billy', 'first_name' => 'William', 'contact_sub_type' => 'Parent']);
265 $this->runImport([
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']);
275 }
276
277 /**
278 * Test updating an existing contact with external_identifier match but subtype mismatch.
279 *
280 * The subtype is not updated, as there is conflicting contact data.
281 *
282 * @throws \Exception
283 */
284 public function testImportParserUpdateWithExternalIdentifierSubtypeChangeFail(): void {
285 $contactID = $this->individualCreate(['external_identifier' => 'billy', 'first_name' => 'William', 'contact_sub_type' => 'Parent']);
286 $this->addChild($contactID);
287
288 $this->runImport([
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']);
296 }
297
298 /**
299 * Test updating an existing contact with external_identifier match but subtype mismatch.
300 *
301 * @throws \Exception
302 */
303 public function testImportParserWithUpdateWithTypeMismatch(): void {
304 $contactID = $this->organizationCreate(['external_identifier' => 'billy']);
305 $this->runImport([
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']);
313
314 $this->runImport([
315 'id' => $contactID,
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']);
322
323 }
324
325 /**
326 * Test import parser will fallback to external identifier.
327 *
328 * In this case no primary match exists (e.g the details are not supplied) so it falls back on external identifier.
329 *
330 * @see https://issues.civicrm.org/jira/browse/CRM-17275
331 *
332 * @throws \Exception
333 */
334 public function testImportParserWithUpdateWithExternalIdentifierButNoPrimaryMatch(): void {
335 [$originalValues, $result] = $this->setUpBaseContact([
336 'external_identifier' => 'windows',
337 'email' => NULL,
338 ]);
339
340 $this->assertEquals('windows', $result['external_identifier']);
341
342 $originalValues['nick_name'] = 'Old Bill';
343 $this->runImport($originalValues, CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::VALID);
344 $originalValues['id'] = $result['id'];
345
346 $this->assertEquals('Old Bill', $this->callAPISuccessGetValue('Contact', ['id' => $result['id'], 'return' => 'nick_name']));
347 $this->callAPISuccessGetSingle('Contact', $originalValues);
348 }
349
350 /**
351 * Test import parser will fallback to external identifier.
352 *
353 * In this case no primary match exists (e.g the details are not supplied) so it falls back on external identifier.
354 *
355 * @see https://issues.civicrm.org/jira/browse/CRM-17275
356 *
357 * @throws \Exception
358 */
359 public function testImportParserWithUpdateWithContactID(): void {
360 [$originalValues, $result] = $this->setUpBaseContact([
361 'external_identifier' => '',
362 'email' => NULL,
363 ]);
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);
372 }
373
374 /**
375 * Test that the import parser adds the external identifier where none is set.
376 *
377 * @throws \Exception
378 */
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);
387 }
388
389 /**
390 * Test that the import parser changes the external identifier when there is a dedupe match.
391 *
392 * @throws \Exception
393 */
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);
403 }
404
405 /**
406 * Test that the import parser adds the address to the right location.
407 *
408 * @throws \API_Exception
409 * @throws \CRM_Core_Exception
410 * @throws \CiviCRM_API3_Exception
411 */
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']);
421
422 $phone = $this->callAPISuccessGetSingle('Phone', ['phone' => '911']);
423 $this->assertEquals(2, $phone['location_type_id']);
424
425 $contact = $this->callAPISuccessGetSingle('Contact', $contactValues);
426 $this->callAPISuccess('Contact', 'delete', ['id' => $contact['id']]);
427 }
428
429 /**
430 * Test that the not-really-encouraged way of creating locations via contact.create doesn't mess up primaries.
431 */
432 public function testContactLocationBlockHandling() {
433 $id = $this->individualCreate([
434 'phone' => [
435 1 => [
436 'location_type_id' => 1,
437 'phone' => '987654321',
438 ],
439 2 => [
440 'location_type_id' => 2,
441 'phone' => '456-7890',
442 ],
443 ],
444 'im' => [
445 1 => [
446 'location_type_id' => 1,
447 'name' => 'bob',
448 ],
449 2 => [
450 'location_type_id' => 2,
451 'name' => 'fred',
452 ],
453 ],
454 'openid' => [
455 1 => [
456 'location_type_id' => 1,
457 'openid' => 'bob',
458 ],
459 2 => [
460 'location_type_id' => 2,
461 'openid' => 'fred',
462 ],
463 ],
464 'email' => [
465 1 => [
466 'location_type_id' => 1,
467 'email' => 'bob@example.com',
468 ],
469 2 => [
470 'location_type_id' => 2,
471 'email' => 'fred@example.com',
472 ],
473 ],
474 ]);
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);
483
484 $this->assertLocationValidity();
485 $this->callAPISuccess('Contact', 'create', [
486 'id' => $id,
487 // This is secret code for 'delete this phone'.
488 'updateBlankLocInfo' => TRUE,
489 'phone' => [
490 1 => [
491 'id' => key($phones),
492 ],
493 ],
494 'email' => [
495 1 => [
496 'id' => key($emails),
497 ],
498 ],
499 'im' => [
500 1 => [
501 'id' => key($ims),
502 ],
503 ],
504 'openid' => [
505 1 => [
506 'id' => key($openIDs),
507 ],
508 ],
509 ]);
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);
515 }
516
517 /**
518 * Test that the import parser adds the address to the primary location.
519 *
520 * @throws \Exception
521 */
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']);
532
533 $phone = $this->callAPISuccessGetSingle('Phone', ['phone' => '12334']);
534 $this->assertEquals(1, $phone['location_type_id']);
535
536 $this->callAPISuccessGetSingle('Email', ['email' => 'bill.gates@microsoft.com']);
537
538 $contact = $this->callAPISuccessGetSingle('Contact', $contactValues);
539 $this->callAPISuccess('Contact', 'delete', ['id' => $contact['id']]);
540 }
541
542 /**
543 * Test that address location type id is ignored for dedupe purposes on import.
544 *
545 * @throws \Exception
546 */
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',
554 'rule_weight' => 4,
555 ]);
556 $this->callAPISuccess('Rule', 'create', [
557 'dedupe_rule_group_id' => $rgid,
558 'rule_field' => 'street_address',
559 'rule_table' => 'civicrm_address',
560 'rule_weight' => 4,
561 ]);
562 // Create a contact with an address of location_type_id 1.
563 $contact1Params = [
564 'contact_type' => 'Individual',
565 'first_name' => 'Original',
566 'last_name' => 'Smith',
567 ];
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',
573 ]);
574
575 $contactValues = [
576 'first_name' => 'New',
577 'last_name' => 'Smith',
578 'street_address' => 'Big Mansion',
579 ];
580
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']]);
588 }
589
590 /**
591 * Test that address custom fields can be imported
592 * FIXME: Api4
593 *
594 * @throws \CRM_Core_Exception
595 */
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']]);
606 }
607
608 /**
609 * Test gender works when you specify the label.
610 *
611 * There is an expectation that you can import by label here.
612 *
613 * @throws \CRM_Core_Exception
614 */
615 public function testGenderLabel() {
616 $contactValues = [
617 'first_name' => 'Bill',
618 'last_name' => 'Gates',
619 'email' => 'bill.gates@microsoft.com',
620 'nick_name' => 'Billy-boy',
621 'gender_id' => 'Female',
622 ];
623 $this->runImport($contactValues, CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::VALID, [NULL, NULL, 'Primary', NULL, NULL]);
624 $this->callAPISuccessGetSingle('Contact', $contactValues);
625 }
626
627 /**
628 * Test prefix & suffix work when you specify the label.
629 *
630 * There is an expectation that you can import by label here.
631 *
632 * @throws \API_Exception
633 * @throws \CRM_Core_Exception
634 * @throws \CiviCRM_API3_Exception
635 */
636 public function testPrefixLabel(): void {
637 $this->callAPISuccess('OptionValue', 'create', ['option_group_id' => 'individual_prefix', 'name' => 'new_one', 'label' => 'special', 'value' => 70]);
638 $mapping = [
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],
644 ];
645 $mapperInput = [['first_name'], ['last_name'], ['email', CRM_Core_PseudoConstant::getKey('CRM_Core_BAO_Email', 'location_type_id', 'Home')], ['prefix_id'], ['suffix_id']];
646
647 $processor = new CRM_Import_ImportProcessor();
648 $processor->setMappingFields($mapping);
649 $userJobID = $this->getUserJobID(['mapper' => $mapperInput]);
650 $processor->setUserJobID($userJobID);
651 $importer = $processor->getImporterObject();
652
653 $contactValues = [
654 'Bill',
655 'Gates',
656 'bill.gates@microsoft.com',
657 'special',
658 'III',
659 ];
660 $importer->import(CRM_Import_Parser::DUPLICATE_NOCHECK, $contactValues);
661
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']);
664 }
665
666 /**
667 * Test that labels work for importing custom data.
668 *
669 * @throws \API_Exception
670 * @throws \CRM_Core_Exception
671 * @throws \CiviCRM_API3_Exception
672 */
673 public function testCustomDataLabel(): void {
674 $this->createCustomGroupWithFieldOfType([], 'select');
675 $contactValues = [
676 'first_name' => 'Bill',
677 'last_name' => 'Gates',
678 'email' => 'bill.gates@microsoft.com',
679 'nick_name' => 'Billy-boy',
680 $this->getCustomFieldName('select') => 'Yellow',
681 ];
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')]);
685 }
686
687 /**
688 * Test that names work for importing custom data.
689 *
690 * @throws \CRM_Core_Exception
691 */
692 public function testCustomDataName() {
693 $this->createCustomGroupWithFieldOfType([], 'select');
694 $contactValues = [
695 'first_name' => 'Bill',
696 'last_name' => 'Gates',
697 'email' => 'bill.gates@microsoft.com',
698 'nick_name' => 'Billy-boy',
699 $this->getCustomFieldName('select') => 'Y',
700 ];
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')]);
704 }
705
706 /**
707 * Test importing in the Preferred Language Field
708 *
709 * @throws \CRM_Core_Exception
710 */
711 public function testPreferredLanguageImport() {
712 $contactValues = [
713 'first_name' => 'Bill',
714 'last_name' => 'Gates',
715 'email' => 'bill.gates@microsoft.com',
716 'nick_name' => 'Billy-boy',
717 'preferred_language' => 'English (Australia)',
718 ];
719 $this->runImport($contactValues, CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::VALID, [NULL, NULL, 'Primary', NULL, NULL]);
720 }
721
722 /**
723 * Test that the import parser adds the address to the primary location.
724 *
725 * @throws \Exception
726 */
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']]);
738 }
739
740 /**
741 * Test that the import parser adds the address to the primary location.
742 *
743 * @throws \Exception
744 */
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';
755 $fields[] = 'phone';
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]);
759
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']);
763
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']);
767
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']);
775
776 $this->callAPISuccess('Contact', 'delete', ['id' => $contact['id']]);
777 }
778
779 /**
780 * Test importing 2 phones of different types.
781 *
782 * @throws \API_Exception
783 * @throws \CRM_Core_Exception
784 * @throws \CiviCRM_API3_Exception
785 */
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]],
790 ]));
791 $processor->setMappingFields(
792 [
793 ['name' => 'first_name'],
794 ['name' => 'last_name'],
795 ['name' => 'email'],
796 ['name' => 'phone', 'location_type_id' => 1, 'phone_type_id' => 2],
797 ['name' => 'phone', 'location_type_id' => 1, 'phone_type_id' => 1],
798 ]
799 );
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);
806 }
807
808 /**
809 * Test that the import parser adds the address to the primary location.
810 *
811 * @throws \Exception
812 */
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';
823 $fields[] = 'phone';
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'];
827
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']);
831
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']);
835
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']);
843
844 $this->callAPISuccess('Contact', 'delete', ['id' => $contact['id']]);
845 }
846
847 /**
848 * Test that the import parser updates the address on the existing primary location.
849 *
850 * @throws \Exception
851 */
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);
872 }
873
874 /**
875 * Test the determination of whether a custom field is valid.
876 */
877 public function testCustomFieldValidation(): void {
878 $errorMessage = '';
879 $customGroup = $this->customGroupCreate([
880 'extends' => 'Contact',
881 'title' => 'ABC',
882 ]);
883 $customField = $this->customFieldOptionValueCreate($customGroup, 'fieldABC', ['html_type' => 'Select', 'serialize' => 1]);
884 $params = [
885 'custom_' . $customField['id'] => 'Label1|Label2',
886 ];
887 CRM_Contact_Import_Parser_Contact::isErrorInCustomData($params, $errorMessage);
888 $this->assertEquals(NULL, $errorMessage);
889 }
890
891 /**
892 * Test the import validation.
893 *
894 * @dataProvider validateDataProvider
895 *
896 * @param string $csv
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
902 *
903 *
904 * @throws \API_Exception
905 */
906 public function testValidation(string $csv, array $mapper, string $expectedError = '', $submittedValues = []): void {
907 try {
908 $this->validateCSV($csv, $mapper, $submittedValues);
909 }
910 catch (CRM_Core_Exception $e) {
911 $this->assertSame($expectedError, $e->getMessage());
912 return;
913 }
914 if ($expectedError) {
915 $this->fail('expected error :' . $expectedError);
916 }
917 }
918
919 /**
920 * Get combinations to test for validation.
921 *
922 * @return array[]
923 */
924 public function validateDataProvider(): array {
925 return [
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',
930 ],
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' => '',
935 ],
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',
940 ],
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',
945 ],
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',
950 ],
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',
956 ],
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],
963 ],
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],
970 ],
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],
977 ],
978 ];
979 }
980
981 /**
982 * Test the import.
983 *
984 * @dataProvider importDataProvider
985 *
986 * @throws \API_Exception
987 * @throws \CRM_Core_Exception
988 */
989 public function testImport($csv, $mapper, $expectedError, $expectedOutcomes = []): void {
990 try {
991 $this->importCSV($csv, $mapper);
992 }
993 catch (CRM_Core_Exception $e) {
994 $this->assertSame($expectedError, $e->getMessage());
995 return;
996 }
997 if ($expectedError) {
998 $this->fail('expected error :' . $expectedError);
999 }
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);
1003 }
1004 }
1005
1006 /**
1007 * Get combinations to test for validation.
1008 *
1009 * @return array[]
1010 */
1011 public function importDataProvider(): array {
1012 return [
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],
1018 ],
1019 ];
1020 }
1021
1022 /**
1023 * Test the handling of validation when importing genders.
1024 *
1025 * If it's not gonna import it should fail at the validation stage...
1026 *
1027 * @throws \API_Exception
1028 * @throws \CRM_Core_Exception
1029 */
1030 public function testImportGenders(): void {
1031 $mapper = [
1032 ['first_name'],
1033 ['last_name'],
1034 ['gender_id'],
1035 ['1_a_b', 'first_name'],
1036 ['1_a_b', 'last_name'],
1037 ['1_a_b', 'gender_id'],
1038 ['do_not_import'],
1039 ];
1040 $csv = 'individual_genders.csv';
1041 $this->validateMultiRowCsv($csv, $mapper, 'gender');
1042
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']);
1049 }
1050 $this->assertCount(8, $contacts);
1051 }
1052
1053 /**
1054 * Test date validation.
1055 *
1056 * @dataProvider dateDataProvider
1057 *
1058 * @param string $csv
1059 * @param int $dateType
1060 *
1061 * @throws \API_Exception
1062 * @throws \CRM_Core_Exception
1063 */
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'];
1069 $mapper = [
1070 ['first_name'],
1071 ['last_name'],
1072 ['birth_date'],
1073 ['deceased_date'],
1074 ['custom_' . $contactCustomFieldID],
1075 ['custom_' . $addressCustomFieldID, 1],
1076 ['street_address', 1],
1077 ['do_not_import'],
1078 ];
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]);
1082 $fields = [
1083 'contact_id.birth_date',
1084 'contact_id.deceased_date',
1085 'contact_id.is_deceased',
1086 'contact_id.custom_' . $contactCustomFieldID,
1087 $addressCustomFieldID,
1088 ];
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]);
1094 }
1095 else {
1096 $this->assertEquals('2008-09-01', $contact[$field]);
1097 }
1098 }
1099 }
1100 }
1101
1102 /**
1103 * @throws \API_Exception
1104 */
1105 public function testImportContactSubTypes(): void {
1106 ContactType::create()->setValues([
1107 'name' => 'baby',
1108 'label' => 'Infant',
1109 'parent_id:name' => 'Individual',
1110 ])->execute();
1111 $mapper = [
1112 ['first_name'],
1113 ['last_name'],
1114 ['5_a_b', 'organization_name'],
1115 ['contact_sub_type'],
1116 ['5_a_b', 'contact_sub_type'],
1117 ];
1118 $csv = 'individual_contact_sub_types.csv';
1119 $field = 'contact_sub_type';
1120
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']);
1128 }
1129 $this->assertCount(3, $contacts);
1130 }
1131
1132 /**
1133 * Data provider for date tests.
1134 *
1135 * @return array[]
1136 */
1137 public function dateDataProvider(): array {
1138 return [
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],
1145 ];
1146 }
1147
1148 /**
1149 * Test location importing, including for related contacts.
1150 *
1151 * @throws \CRM_Core_Exception
1152 * @throws \API_Exception
1153 */
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', [
1157 'Child of',
1158 'Sibling of',
1159 'Employee of',
1160 ])->execute()->indexBy('name_a_b');
1161
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'];
1173 $mapper = [
1174 ['first_name'],
1175 ['last_name'],
1176 ['birth_date'],
1177 ['street_address', $homeID],
1178 ['city', $homeID],
1179 ['postal_code', $homeID],
1180 ['country', $homeID],
1181 ['state_province', $homeID],
1182 // No location type ID means 'Primary'
1183 ['email'],
1184 ['signature_text'],
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],
1224 ];
1225 $this->validateCSV($csv, $mapper);
1226 }
1227
1228 /**
1229 * Test that setting duplicate action to fill doesn't blow away data
1230 * that exists, but does fill in where it's empty.
1231 *
1232 * @throw \Exception
1233 */
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,
1240 ]);
1241 $customGroupID = $this->ids['CustomGroup']['importFillGroup'];
1242
1243 // Add two custom fields.
1244 $api_params = [
1245 'custom_group_id' => $customGroupID,
1246 'label' => 'importFillField1',
1247 'html_type' => 'Select',
1248 'data_type' => 'String',
1249 'option_values' => [
1250 'foo' => 'Foo',
1251 'bar' => 'Bar',
1252 ],
1253 ];
1254 $result = $this->callAPISuccess('custom_field', 'create', $api_params);
1255 $customField1 = $result['id'];
1256
1257 $api_params = [
1258 'custom_group_id' => $customGroupID,
1259 'label' => 'importFillField2',
1260 'html_type' => 'Select',
1261 'data_type' => 'String',
1262 'option_values' => [
1263 'baz' => 'Baz',
1264 'boo' => 'Boo',
1265 ],
1266 ];
1267 $result = $this->callAPISuccess('custom_field', 'create', $api_params);
1268 $customField2 = $result['id'];
1269
1270 // Now set up values.
1271 $original_gender = 'Male';
1272 $original_custom1 = 'foo';
1273 $original_email = 'test-import-fill@example.org';
1274
1275 $import_gender = 'Female';
1276 $import_custom1 = 'bar';
1277 $import_job_title = 'Chief data importer';
1278 $import_custom2 = 'baz';
1279
1280 // Create contact with both one known core field and one custom
1281 // field filled in.
1282 $api_params = [
1283 'contact_type' => 'Individual',
1284 'email' => $original_email,
1285 'gender' => $original_gender,
1286 'custom_' . $customField1 => $original_custom1,
1287 ];
1288 $result = $this->callAPISuccess('contact', 'create', $api_params);
1289 $contact_id = $result['id'];
1290
1291 // Run an import.
1292 $import = [
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,
1298 ];
1299
1300 $this->runImport($import, CRM_Import_Parser::DUPLICATE_FILL, CRM_Import_Parser::VALID);
1301
1302 $expected = [
1303 'gender' => $original_gender,
1304 'custom_' . $customField1 => $original_custom1,
1305 'job_title' => $import_job_title,
1306 'custom_' . $customField2 => $import_custom2,
1307 ];
1308
1309 $params = [
1310 'id' => $contact_id,
1311 'return' => [
1312 'gender',
1313 'custom_' . $customField1,
1314 'job_title',
1315 'custom_' . $customField2,
1316 ],
1317 ];
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;
1323 }
1324 else {
1325 $given_value = $values[$field];
1326 }
1327 // We expect:
1328 // gender: Male
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");
1333 }
1334 }
1335
1336 /**
1337 * CRM-19888 default country should be used if ambigous.
1338 *
1339 * @throws \CRM_Core_Exception
1340 */
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);
1349 $addressValues = [
1350 'street_address' => 'PO Box 2716',
1351 'city' => 'Midway',
1352 'state_province' => 'UT',
1353 'postal_code' => 84049,
1354 'country' => 'United States',
1355 ];
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);
1363 $fields[] = $field;
1364 $fields[] = $field;
1365 }
1366 $contactValues['work_country'] = '';
1367
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']);
1373 }
1374
1375 /**
1376 * Test importing fields with various options.
1377 *
1378 * Ensure we can import multiple preferred_communication_methods, single
1379 * gender, and single preferred language using both labels and values.
1380 *
1381 * @throws \API_Exception
1382 * @throws \CRM_Core_Exception
1383 * @throws \CiviCRM_API3_Exception
1384 */
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']],
1389 ]));
1390 $processor->setMappingFields(
1391 [
1392 ['name' => 'first_name'],
1393 ['name' => 'last_name'],
1394 ['name' => 'preferred_communication_method'],
1395 ['name' => 'gender_id'],
1396 ['name' => 'preferred_language'],
1397 ]
1398 );
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']);
1403
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.");
1407
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']);
1412
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.");
1416 }
1417
1418 /**
1419 * Run the import parser.
1420 *
1421 * @param array $originalValues
1422 *
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.
1432 *
1433 * @throws \API_Exception
1434 * @throws \CRM_Core_Exception
1435 * @throws \CiviCRM_API3_Exception
1436 */
1437 protected function runImport(array $originalValues, $onDuplicateAction, $expectedResult, $mapperLocType = [], $fields = NULL, int $ruleGroupId = NULL): void {
1438 if (!$fields) {
1439 $fields = array_keys($originalValues);
1440 }
1441 $values = array_values($originalValues);
1442 $mapper = [];
1443 foreach ($fields as $index => $field) {
1444 $mapper[] = [$field, $mapperLocType[$index] ?? NULL, $field === 'phone' ? 1 : NULL];
1445 }
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;
1450 $parser->init();
1451 $this->assertEquals($expectedResult, $parser->import($onDuplicateAction, $values), 'Return code from parser import was not as expected');
1452 }
1453
1454 /**
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
1460 *
1461 * @return array
1462 * @throws \API_Exception
1463 * @throws \Civi\API\Exception\UnauthorizedException
1464 */
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));
1475
1476 $dataSource = new CRM_Import_DataSource_CSV($userJobID);
1477 $parser = new CRM_Contact_Import_Parser_Contact();
1478 $parser->setUserJobID($userJobID);
1479 $parser->init();
1480 return [$dataSource, $parser];
1481 }
1482
1483 /**
1484 * @param int $contactID
1485 *
1486 * @throws \API_Exception
1487 * @throws \Civi\API\Exception\UnauthorizedException
1488 */
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,
1502 ])->execute();
1503 }
1504
1505 /**
1506 * @param array $fields Array of fields to be imported
1507 * @param array $allfields Array of all fields which can be part of import
1508 */
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;
1514 }
1515 }
1516 }
1517
1518 /**
1519 * Test mapping fields within the Parser class.
1520 *
1521 * @throws \API_Exception
1522 * @throws \Civi\API\Exception\UnauthorizedException
1523 */
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],
1546 // Website types
1547 [NULL, NULL, NULL, NULL, NULL],
1548 // Related contact website types
1549 [NULL, NULL, 1, NULL, NULL]
1550 );
1551 $parser->setUserJobID($this->getUserJobID([
1552 'mapper' => [
1553 ['first_name'],
1554 ['phone', 1, 1],
1555 ['5_a_b', 'url', 1],
1556 ['im', 1, 1],
1557 ['5_a_b', 'phone', 1, 1],
1558 ],
1559 ]));
1560 $parser->init();
1561 $params = $parser->getMappedRow(
1562 ['Bob', '123', 'https://example.org', 'my-handle', '456']
1563 );
1564 $this->assertEquals([
1565 'first_name' => 'Bob',
1566 'phone' => [
1567 [
1568 'phone' => '123',
1569 'location_type_id' => 1,
1570 'phone_type_id' => 1,
1571 ],
1572 ],
1573 '5_a_b' => [
1574 'contact_type' => 'Organization',
1575 'url' =>
1576 [
1577
1578 [
1579 'url' => 'https://example.org',
1580 'website_type_id' => 1,
1581 ],
1582 ],
1583 'phone' =>
1584 [
1585 [
1586 'phone' => '456',
1587 'location_type_id' => 1,
1588 'phone_type_id' => 1,
1589 ],
1590 ],
1591 ],
1592 'im' =>
1593 [
1594
1595 [
1596 'im' => 'my-handle',
1597 'location_type_id' => 1,
1598 'provider_id' => 1,
1599 ],
1600 ],
1601 'contact_type' => 'Individual',
1602 ], $params);
1603 }
1604
1605 /**
1606 * Set up the underlying contact.
1607 *
1608 * @param array $params
1609 * Optional extra parameters to set.
1610 *
1611 * @return array
1612 * @throws \CRM_Core_Exception
1613 */
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',
1620 ], $params);
1621 $this->runImport($originalValues, CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::VALID);
1622 $result = $this->callAPISuccessGetSingle('Contact', $originalValues);
1623 return [$originalValues, $result];
1624 }
1625
1626 /**
1627 * @return mixed
1628 * @throws \API_Exception
1629 * @throws \Civi\API\Exception\UnauthorizedException
1630 */
1631 protected function getUserJobID($submittedValues = []) {
1632 $userJobID = UserJob::create()->setValues([
1633 'metadata' => [
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),
1644 ],
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);
1650 }
1651 else {
1652 $dataSource = new CRM_Import_DataSource_SQL($userJobID);
1653 }
1654 $dataSource->initialize();
1655 return $userJobID;
1656 }
1657
1658 /**
1659 * Validate the csv file values.
1660 *
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.
1667 *
1668 * @throws \API_Exception
1669 */
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()));
1673 }
1674
1675 /**
1676 * Import the csv file values.
1677 *
1678 * This function uses a flow that mimics the UI flow.
1679 *
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
1685 */
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,
1697 'groups' => [],
1698 ], $submittedValues);
1699 $form = $this->getFormObject('CRM_Contact_Import_Form_DataSource', $submittedValues);
1700 $form->buildForm();
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);
1706 $form->buildForm();
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);
1711 $form->buildForm();
1712 $form->postProcess();
1713 }
1714
1715 /**
1716 * Validate a csv with multiple rows in it.
1717 *
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.
1725 *
1726 * @throws \API_Exception
1727 * @throws \CRM_Core_Exception
1728 * @throws \Civi\API\Exception\UnauthorizedException
1729 */
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()) {
1735 try {
1736 $parser->validateValues(array_values($values));
1737 if ($values['expected'] !== 'Valid') {
1738 $this->fail($values[$field] . ' should not have been valid');
1739 }
1740 }
1741 catch (CRM_Core_Exception $e) {
1742 if ($values['expected'] !== 'Invalid') {
1743 $this->fail($values[$field] . ' should have been valid');
1744 }
1745 }
1746 }
1747 }
1748
1749 }