Merge pull request #24117 from civicrm/5.52
[civicrm-core.git] / tests / phpunit / CRM / Member / Import / Parser / MembershipTest.php
1 <?php
2
3 /**
4 * File for the Membership import class
5 *
6 * (PHP 5)
7 *
8 * @package CiviCRM
9 *
10 * This file is part of CiviCRM
11 *
12 * CiviCRM is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU Affero General Public License
14 * as published by the Free Software Foundation; either version 3 of
15 * the License, or (at your option) any later version.
16 *
17 * CiviCRM is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU Affero General Public License for more details.
21 *
22 * You should have received a copy of the GNU Affero General Public
23 * License along with this program. If not, see
24 * <http://www.gnu.org/licenses/>.
25 */
26
27 use Civi\Api4\UserJob;
28
29 /**
30 * @package CiviCRM
31 * @group headless
32 */
33 class CRM_Member_Import_Parser_MembershipTest extends CiviUnitTestCase {
34 use CRMTraits_Custom_CustomDataTrait;
35
36 /**
37 * @var int
38 */
39 protected $userJobID;
40
41 /**
42 * Membership type name used in test function.
43 *
44 * @var string
45 */
46 protected $_membershipTypeName = NULL;
47
48 /**
49 * Membership type id used in test function.
50 *
51 * @var string
52 */
53 protected $_membershipTypeID;
54
55 protected $entity = 'Membership';
56
57 /**
58 * Set up for test.
59 *
60 * @throws \CRM_Core_Exception
61 * @throws \CiviCRM_API3_Exception
62 */
63 public function setUp(): void {
64 parent::setUp();
65
66 $params = [
67 'contact_type_a' => 'Individual',
68 'contact_type_b' => 'Organization',
69 'name_a_b' => 'Test Employee of',
70 'name_b_a' => 'Test Employer of',
71 ];
72 $this->_relationshipTypeId = $this->relationshipTypeCreate($params);
73 $this->_orgContactID = $this->organizationCreate();
74 $this->_financialTypeId = 1;
75 $this->restoreMembershipTypes();
76 $this->_membershipTypeName = 'Mickey Mouse Club Member';
77 $params = [
78 'name' => $this->_membershipTypeName,
79 'description' => NULL,
80 'minimum_fee' => 10,
81 'duration_unit' => 'year',
82 'member_of_contact_id' => $this->_orgContactID,
83 'period_type' => 'fixed',
84 'duration_interval' => 1,
85 'financial_type_id' => $this->_financialTypeId,
86 'relationship_type_id' => $this->_relationshipTypeId,
87 'visibility' => 'Public',
88 'is_active' => 1,
89 'fixed_period_start_day' => 101,
90 'fixed_period_rollover_day' => 1231,
91 ];
92
93 $membershipType = CRM_Member_BAO_MembershipType::add($params);
94 $this->_membershipTypeID = $membershipType->id;
95
96 $this->_mebershipStatusID = $this->membershipStatusCreate('test status');
97 }
98
99 /**
100 * Tears down the fixture, for example, closes a network connection.
101 * This method is called after a test is executed.
102 *
103 * @throws \CRM_Core_Exception
104 */
105 public function tearDown(): void {
106 $tablesToTruncate = [
107 'civicrm_membership',
108 'civicrm_membership_log',
109 'civicrm_contribution',
110 'civicrm_membership_payment',
111 'civicrm_contact',
112 'civicrm_email',
113 'civicrm_user_job',
114 'civicrm_queue',
115 'civicrm_queue_item',
116 ];
117 $this->relationshipTypeDelete($this->_relationshipTypeId);
118 $this->membershipTypeDelete(['id' => $this->_membershipTypeID]);
119 $this->membershipStatusDelete($this->_mebershipStatusID);
120 $this->quickCleanup($tablesToTruncate, TRUE);
121 parent::tearDown();
122 }
123
124 /**
125 * Test Import.
126 */
127 public function testImport(): void {
128 $this->individualCreate();
129 $contact2Params = [
130 'first_name' => 'Anthonita',
131 'middle_name' => 'J.',
132 'last_name' => 'Anderson',
133 'prefix_id' => 3,
134 'suffix_id' => 3,
135 'email' => 'b@c.com',
136 'contact_type' => 'Individual',
137 ];
138
139 $this->individualCreate($contact2Params);
140 $year = date('Y') - 1;
141 $startDate2 = $year . '-10-09';
142 $joinDate2 = $year . '-10-10';
143 $params = [
144 [
145 'anthony_anderson@civicrm.org',
146 $this->_membershipTypeID,
147 date('Y-m-d'),
148 date('Y-m-d'),
149 ],
150 [
151 $contact2Params['email'],
152 $this->_membershipTypeName,
153 $startDate2,
154 $joinDate2,
155 ],
156 ];
157
158 $importObject = $this->createImportObject(['email', 'membership_type_id', 'membership_start_date', 'membership_join_date']);
159 foreach ($params as $values) {
160 $this->assertEquals(CRM_Import_Parser::VALID, $importObject->import($values), $values[0]);
161 }
162 $result = $this->callAPISuccess('membership', 'get', ['sequential' => 1])['values'];
163 $this->assertCount(2, $result);
164 $this->assertEquals($startDate2, $result[1]['start_date']);
165 $this->assertEquals($joinDate2, $result[1]['join_date']);
166 }
167
168 /**
169 * Test overriding a membership but not providing status.
170 */
171 public function testImportOverriddenMembershipButWithoutStatus(): void {
172 $this->individualCreate(['email' => 'anthony_anderson2@civicrm.org']);
173 $membershipImporter = new CRM_Member_Import_Parser_Membership();
174 $membershipImporter->setUserJobID($this->getUserJobID([
175 'mapper' => [['email'], ['membership_type_id'], ['membership_start_date'], ['member_is_override']],
176 'onDuplicate' => CRM_Import_Parser::DUPLICATE_UPDATE,
177 ]));
178 $membershipImporter->init();
179
180 $importValues = [
181 'anthony_anderson2@civicrm.org',
182 $this->_membershipTypeID,
183 date('Y-m-d'),
184 TRUE,
185 ];
186 try {
187 $membershipImporter->validateValues($importValues);
188 $this->fail('validation error expected.');
189 }
190 catch (CRM_Core_Exception $e) {
191 $this->assertStringContainsString('Required parameter missing: Status', $e->getMessage());
192 return;
193 }
194
195 }
196
197 /**
198 * Test that the passed in status is respected.
199 */
200 public function testImportOverriddenMembershipWithStatus(): void {
201 $this->individualCreate(['email' => 'anthony_anderson3@civicrm.org']);
202 $membershipImporter = $this->createImportObject([
203 'email',
204 'membership_type_id',
205 'membership_start_date',
206 'member_is_override',
207 'status_id',
208 ]);
209
210 $importValues = [
211 'anthony_anderson3@civicrm.org',
212 $this->_membershipTypeID,
213 date('Y-m-d'),
214 TRUE,
215 'New',
216 ];
217
218 $importResponse = $membershipImporter->import($importValues);
219 $this->assertEquals(CRM_Import_Parser::VALID, $importResponse);
220 }
221
222 public function testImportOverriddenMembershipWithValidOverrideEndDate(): void {
223 $this->individualCreate(['email' => 'anthony_anderson4@civicrm.org']);
224 $membershipImporter = new CRM_Member_Import_Parser_Membership();
225 $membershipImporter->setUserJobID($this->getUserJobID([
226 'mapper' => [['email'], ['membership_type_id'], ['membership_start_date'], ['member_is_override'], ['status_id'], ['status_override_end_date']],
227 ]));
228 $membershipImporter->init();
229
230 $importValues = [
231 'anthony_anderson4@civicrm.org',
232 $this->_membershipTypeID,
233 date('Y-m-d'),
234 TRUE,
235 'New',
236 date('Y-m-d'),
237 ];
238
239 $importResponse = $membershipImporter->import($importValues);
240 $this->assertEquals(CRM_Import_Parser::VALID, $importResponse);
241 }
242
243 public function testImportOverriddenMembershipWithInvalidOverrideEndDate(): void {
244 $this->individualCreate(['email' => 'anthony_anderson5@civicrm.org']);
245 $this->userJobID = $this->getUserJobID([
246 'mapper' => [['email'], ['membership_type_id'], ['membership_start_date'], ['member_is_override'], ['status_id'], ['status_override_end_date']],
247 'onDuplicate' => CRM_Import_Parser::DUPLICATE_UPDATE,
248 ]);
249 $membershipImporter = new CRM_Member_Import_Parser_Membership();
250 $membershipImporter->setUserJobID($this->userJobID);
251 $membershipImporter->init();
252
253 $importValues = [
254 'anthony_anderson5@civicrm.org',
255 'General',
256 date('Y-m-d'),
257 1,
258 $this->_mebershipStatusID,
259 'abc',
260 ];
261 try {
262 $membershipImporter->validateValues($importValues);
263 }
264 catch (CRM_Core_Exception $e) {
265 $this->assertEquals('Invalid value for field(s) : Status Override End Date', $e->getMessage());
266 return;
267 }
268 $this->fail('Exception expected');
269
270 }
271
272 /**
273 * Test that memberships can still be imported if the status is renamed.
274 *
275 */
276 public function testImportMembershipWithRenamedStatus(): void {
277 $this->individualCreate(['email' => 'anthony_anderson3@civicrm.org']);
278
279 $this->callAPISuccess('MembershipStatus', 'get', [
280 'name' => 'New',
281 'api.MembershipStatus.create' => [
282 'label' => 'New-renamed',
283 ],
284 ]);
285 $membershipImporter = $this->createImportObject([
286 'email',
287 'membership_type_id',
288 'membership_start_date',
289 'member_is_override',
290 'status_id',
291 ]);
292
293 $importValues = [
294 'anthony_anderson3@civicrm.org',
295 $this->_membershipTypeID,
296 date('Y-m-d'),
297 TRUE,
298 'New-renamed',
299 ];
300
301 $importResponse = $membershipImporter->import($importValues);
302 $this->assertEquals(CRM_Import_Parser::VALID, $importResponse);
303 $createdStatusID = $this->callAPISuccessGetValue('Membership', ['return' => 'status_id']);
304 $this->assertEquals(CRM_Core_PseudoConstant::getKey('CRM_Member_BAO_Membership', 'status_id', 'New'), $createdStatusID);
305 $this->callAPISuccess('MembershipStatus', 'get', [
306 'name' => 'New',
307 'api.MembershipStatus.create' => [
308 'label' => 'New',
309 ],
310 ]);
311 }
312
313 /**
314 * Create an import object.
315 *
316 * @param array $fields
317 *
318 * @return \CRM_Member_Import_Parser_Membership
319 */
320 protected function createImportObject(array $fields): \CRM_Member_Import_Parser_Membership {
321 $fieldMapper = [];
322 $mapper = [];
323 foreach ($fields as $index => $field) {
324 $fieldMapper['mapper[' . $index . '][0]'] = $field;
325 $mapper[] = [$field];
326 }
327
328 $membershipImporter = new CRM_Member_Import_Parser_Membership($fieldMapper);
329 $membershipImporter->setUserJobID($this->getUserJobID(['mapper' => $mapper]));
330 $membershipImporter->init();
331 $membershipImporter->_contactType = 'Individual';
332 return $membershipImporter;
333 }
334
335 /**
336 * @param array $submittedValues
337 *
338 * @return int
339 */
340 protected function getUserJobID(array $submittedValues = []): int {
341 $userJobID = UserJob::create()->setValues([
342 'metadata' => [
343 'submitted_values' => array_merge([
344 'contactType' => CRM_Import_Parser::CONTACT_INDIVIDUAL,
345 'contactSubType' => '',
346 'dataSource' => 'CRM_Import_DataSource_SQL',
347 'sqlQuery' => 'SELECT first_name FROM civicrm_contact',
348 'onDuplicate' => CRM_Import_Parser::DUPLICATE_SKIP,
349 'dedupe_rule_id' => NULL,
350 'dateFormats' => CRM_Core_Form_Date::DATE_yyyy_mm_dd,
351 ], $submittedValues),
352 ],
353 'status_id:name' => 'draft',
354 'job_type' => 'contact_import',
355 ])->execute()->first()['id'];
356 if ($submittedValues['dataSource'] ?? NULL === 'CRM_Import_DataSource') {
357 $dataSource = new CRM_Import_DataSource_CSV($userJobID);
358 }
359 else {
360 $dataSource = new CRM_Import_DataSource_SQL($userJobID);
361 }
362 $dataSource->initialize();
363 return $userJobID;
364 }
365
366 /**
367 * Test importing to a custom field.
368 *
369 * @throws \API_Exception
370 * @throws \CRM_Core_Exception|\CiviCRM_API3_Exception
371 */
372 public function testImportCustomData(): void {
373 $donaldDuckID = $this->individualCreate(['first_name' => 'Donald', 'last_name' => 'Duck']);
374 $this->createCustomGroupWithFieldsOfAllTypes(['extends' => 'Membership']);
375 $membershipImporter = $this->createImportObject([
376 'membership_contact_id',
377 'membership_type_id',
378 'membership_start_date',
379 $this->getCustomFieldName('text'),
380 $this->getCustomFieldName('select_string'),
381 ]);
382 $importValues = [
383 $donaldDuckID,
384 $this->_membershipTypeID,
385 date('Y-m-d'),
386 'blah',
387 'Red',
388 ];
389
390 $importResponse = $membershipImporter->import($importValues);
391 $this->assertEquals(CRM_Import_Parser::VALID, $importResponse);
392 $membership = $this->callAPISuccessGetSingle('Membership', []);
393 $this->assertEquals('blah', $membership[$this->getCustomFieldName('text')]);
394 $this->assertEquals('R', $membership[$this->getCustomFieldName('select_string')]);
395 }
396
397 /**
398 * Test the full form-flow import.
399 */
400 public function testImportCSV() :void {
401 $this->importCSV('memberships_invalid.csv', [
402 ['name' => 'membership_contact_id'],
403 ['name' => 'membership_source'],
404 ['name' => 'membership_type_id'],
405 ['name' => 'membership_start_date'],
406 ['name' => 'do_not_import'],
407 ]);
408 $dataSource = new CRM_Import_DataSource_CSV($this->userJobID);
409 $row = $dataSource->getRow();
410 $this->assertEquals('ERROR', $row['_status']);
411 $this->assertEquals('Invalid value for field(s) : Membership Type', $row['_status_message']);
412 }
413
414 /**
415 * Test the full form-flow import.
416 */
417 public function testImportTSV() :void {
418 $this->individualCreate(['email' => 'member@example.com']);
419 $this->importCSV('memberships_valid.tsv', [
420 ['name' => 'email'],
421 ['name' => 'membership_source'],
422 ['name' => 'membership_type_id'],
423 ['name' => 'membership_start_date'],
424 ['name' => 'do_not_import'],
425 ], ['fieldSeparator' => 'tab']);
426 $dataSource = new CRM_Import_DataSource_CSV($this->userJobID);
427 $row = $dataSource->getRow();
428 $this->assertEquals('IMPORTED', $row['_status']);
429 $this->callAPISuccessGetSingle('Membership', []);
430 }
431
432 /**
433 * Test dates are parsed.
434 */
435 public function testUpdateWithCustomDates(): void {
436 $this->createCustomGroupWithFieldOfType([], 'date');
437 $contactID = $this->individualCreate(['external_identifier' => 'ext-1']);
438 $this->callAPISuccess('Membership', 'create', [
439 'contact_id' => $contactID,
440 'membership_type_id' => 'General',
441 'start_date' => '2020-10-01',
442 ]);
443 $mapping = [
444 ['name' => 'membership_id'],
445 ['name' => 'membership_source'],
446 ['name' => 'membership_type_id'],
447 ['name' => 'membership_start_date'],
448 ['name' => $this->getCustomFieldName('date')],
449 ];
450 $this->importCSV('memberships_update_custom_date.csv', $mapping, ['dateFormats' => 32]);
451 $membership = $this->callAPISuccessGetSingle('Membership', []);
452 $this->assertEquals('2021-03-23', $membership['start_date']);
453 $this->assertEquals('2019-03-23 00:00:00', $membership[$this->getCustomFieldName('date')]);
454 }
455
456 /**
457 * Import the csv file values.
458 *
459 * This function uses a flow that mimics the UI flow.
460 *
461 * @param string $csv Name of csv file.
462 * @param array $fieldMappings
463 * @param array $submittedValues
464 */
465 protected function importCSV(string $csv, array $fieldMappings, array $submittedValues = []): void {
466 $submittedValues = array_merge([
467 'uploadFile' => ['name' => __DIR__ . '/data/' . $csv],
468 'skipColumnHeader' => TRUE,
469 'fieldSeparator' => ',',
470 'contactType' => CRM_Import_Parser::CONTACT_INDIVIDUAL,
471 'mapper' => $this->getMapperFromFieldMappings($fieldMappings),
472 'dataSource' => 'CRM_Import_DataSource_CSV',
473 'file' => ['name' => $csv],
474 'dateFormats' => CRM_Core_Form_Date::DATE_yyyy_mm_dd,
475 'onDuplicate' => CRM_Import_Parser::DUPLICATE_UPDATE,
476 'groups' => [],
477 ], $submittedValues);
478 $form = $this->getFormObject('CRM_Member_Import_Form_DataSource', $submittedValues);
479 $values = $_SESSION['_' . $form->controller->_name . '_container']['values'];
480 $form->buildForm();
481 $form->postProcess();
482 // This gets reset in DataSource so re-do....
483 $_SESSION['_' . $form->controller->_name . '_container']['values'] = $values;
484
485 $this->userJobID = $form->getUserJobID();
486 $form = $this->getFormObject('CRM_Member_Import_Form_MapField', $submittedValues);
487 $form->setUserJobID($this->userJobID);
488 $form->buildForm();
489 $form->postProcess();
490 /* @var CRM_Member_Import_Form_MapField $form */
491 $form = $this->getFormObject('CRM_Member_Import_Form_Preview', $submittedValues);
492 $form->setUserJobID($this->userJobID);
493 $form->buildForm();
494 try {
495 $form->postProcess();
496 }
497 catch (CRM_Core_Exception_PrematureExitException $e) {
498 $queue = Civi::queue('user_job_' . $this->userJobID);
499 $runner = new CRM_Queue_Runner([
500 'queue' => $queue,
501 'errorMode' => CRM_Queue_Runner::ERROR_ABORT,
502 ]);
503 $runner->runAll();
504 }
505 }
506
507 /**
508 * @param array $mappings
509 *
510 * @return array
511 */
512 protected function getMapperFromFieldMappings(array $mappings): array {
513 $mapper = [];
514 foreach ($mappings as $mapping) {
515 $fieldInput = [$mapping['name']];
516 $mapper[] = $fieldInput;
517 }
518 return $mapper;
519 }
520
521 }