4 * File for the Membership import class
10 * This file is part of CiviCRM
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.
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.
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/>.
27 use Civi\Api4\UserJob
;
33 class CRM_Member_Import_Parser_MembershipTest
extends CiviUnitTestCase
{
34 use CRMTraits_Custom_CustomDataTrait
;
42 * Membership type name used in test function.
46 protected $_membershipTypeName = NULL;
49 * Membership type id used in test function.
53 protected $_membershipTypeID;
55 protected $entity = 'Membership';
60 * @throws \CRM_Core_Exception
61 * @throws \CiviCRM_API3_Exception
63 public function setUp(): void
{
67 'contact_type_a' => 'Individual',
68 'contact_type_b' => 'Organization',
69 'name_a_b' => 'Test Employee of',
70 'name_b_a' => 'Test Employer of',
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';
78 'name' => $this->_membershipTypeName
,
79 'description' => NULL,
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',
89 'fixed_period_start_day' => 101,
90 'fixed_period_rollover_day' => 1231,
93 $membershipType = CRM_Member_BAO_MembershipType
::add($params);
94 $this->_membershipTypeID
= $membershipType->id
;
96 $this->_mebershipStatusID
= $this->membershipStatusCreate('test status');
100 * Tears down the fixture, for example, closes a network connection.
101 * This method is called after a test is executed.
103 * @throws \CRM_Core_Exception
105 public function tearDown(): void
{
106 $tablesToTruncate = [
107 'civicrm_membership',
108 'civicrm_membership_log',
109 'civicrm_contribution',
110 'civicrm_membership_payment',
115 'civicrm_queue_item',
117 $this->relationshipTypeDelete($this->_relationshipTypeId
);
118 $this->membershipTypeDelete(['id' => $this->_membershipTypeID
]);
119 $this->membershipStatusDelete($this->_mebershipStatusID
);
120 $this->quickCleanup($tablesToTruncate, TRUE);
127 public function testImport(): void
{
128 $this->individualCreate();
130 'first_name' => 'Anthonita',
131 'middle_name' => 'J.',
132 'last_name' => 'Anderson',
135 'email' => 'b@c.com',
136 'contact_type' => 'Individual',
139 $this->individualCreate($contact2Params);
140 $year = date('Y') - 1;
141 $startDate2 = $year . '-10-09';
142 $joinDate2 = $year . '-10-10';
145 'anthony_anderson@civicrm.org',
146 $this->_membershipTypeID
,
151 $contact2Params['email'],
152 $this->_membershipTypeName
,
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]);
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']);
169 * Test overriding a membership but not providing status.
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
,
178 $membershipImporter->init();
181 'anthony_anderson2@civicrm.org',
182 $this->_membershipTypeID
,
187 $membershipImporter->validateValues($importValues);
188 $this->fail('validation error expected.');
190 catch (CRM_Core_Exception
$e) {
191 $this->assertStringContainsString('Required parameter missing: Status', $e->getMessage());
198 * Test that the passed in status is respected.
200 public function testImportOverriddenMembershipWithStatus(): void
{
201 $this->individualCreate(['email' => 'anthony_anderson3@civicrm.org']);
202 $membershipImporter = $this->createImportObject([
204 'membership_type_id',
205 'membership_start_date',
206 'member_is_override',
211 'anthony_anderson3@civicrm.org',
212 $this->_membershipTypeID
,
218 $importResponse = $membershipImporter->import($importValues);
219 $this->assertEquals(CRM_Import_Parser
::VALID
, $importResponse);
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']],
228 $membershipImporter->init();
231 'anthony_anderson4@civicrm.org',
232 $this->_membershipTypeID
,
239 $importResponse = $membershipImporter->import($importValues);
240 $this->assertEquals(CRM_Import_Parser
::VALID
, $importResponse);
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
,
249 $membershipImporter = new CRM_Member_Import_Parser_Membership();
250 $membershipImporter->setUserJobID($this->userJobID
);
251 $membershipImporter->init();
254 'anthony_anderson5@civicrm.org',
258 $this->_mebershipStatusID
,
262 $membershipImporter->validateValues($importValues);
264 catch (CRM_Core_Exception
$e) {
265 $this->assertEquals('Invalid value for field(s) : Status Override End Date', $e->getMessage());
268 $this->fail('Exception expected');
273 * Test that memberships can still be imported if the status is renamed.
276 public function testImportMembershipWithRenamedStatus(): void
{
277 $this->individualCreate(['email' => 'anthony_anderson3@civicrm.org']);
279 $this->callAPISuccess('MembershipStatus', 'get', [
281 'api.MembershipStatus.create' => [
282 'label' => 'New-renamed',
285 $membershipImporter = $this->createImportObject([
287 'membership_type_id',
288 'membership_start_date',
289 'member_is_override',
294 'anthony_anderson3@civicrm.org',
295 $this->_membershipTypeID
,
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', [
307 'api.MembershipStatus.create' => [
314 * Create an import object.
316 * @param array $fields
318 * @return \CRM_Member_Import_Parser_Membership
320 protected function createImportObject(array $fields): \CRM_Member_Import_Parser_Membership
{
323 foreach ($fields as $index => $field) {
324 $fieldMapper['mapper[' . $index . '][0]'] = $field;
325 $mapper[] = [$field];
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;
336 * @param array $submittedValues
340 protected function getUserJobID(array $submittedValues = []): int {
341 $userJobID = UserJob
::create()->setValues([
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),
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);
360 $dataSource = new CRM_Import_DataSource_SQL($userJobID);
362 $dataSource->initialize();
367 * Test importing to a custom field.
369 * @throws \API_Exception
370 * @throws \CRM_Core_Exception|\CiviCRM_API3_Exception
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'),
384 $this->_membershipTypeID
,
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')]);
398 * Test the full form-flow import.
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'],
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']);
415 * Test the full form-flow import.
417 public function testImportTSV() :void
{
418 $this->individualCreate(['email' => 'member@example.com']);
419 $this->importCSV('memberships_valid.tsv', [
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', []);
433 * Test dates are parsed.
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',
444 ['name' => 'membership_id'],
445 ['name' => 'membership_source'],
446 ['name' => 'membership_type_id'],
447 ['name' => 'membership_start_date'],
448 ['name' => $this->getCustomFieldName('date')],
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')]);
457 * Import the csv file values.
459 * This function uses a flow that mimics the UI flow.
461 * @param string $csv Name of csv file.
462 * @param array $fieldMappings
463 * @param array $submittedValues
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
,
477 ], $submittedValues);
478 $form = $this->getFormObject('CRM_Member_Import_Form_DataSource', $submittedValues);
479 $values = $_SESSION['_' . $form->controller
->_name
. '_container']['values'];
481 $form->postProcess();
482 // This gets reset in DataSource so re-do....
483 $_SESSION['_' . $form->controller
->_name
. '_container']['values'] = $values;
485 $this->userJobID
= $form->getUserJobID();
486 $form = $this->getFormObject('CRM_Member_Import_Form_MapField', $submittedValues);
487 $form->setUserJobID($this->userJobID
);
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
);
495 $form->postProcess();
497 catch (CRM_Core_Exception_PrematureExitException
$e) {
498 $queue = Civi
::queue('user_job_' . $this->userJobID
);
499 $runner = new CRM_Queue_Runner([
501 'errorMode' => CRM_Queue_Runner
::ERROR_ABORT
,
508 * @param array $mappings
512 protected function getMapperFromFieldMappings(array $mappings): array {
514 foreach ($mappings as $mapping) {
515 $fieldInput = [$mapping['name']];
516 $mapper[] = $fieldInput;