[REF] Remove handling for non-existent 'savedMapping' field
[civicrm-core.git] / CRM / Member / Import / Parser / Membership.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work 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 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 */
17
18 /**
19 * class to parse membership csv files
20 */
21 class CRM_Member_Import_Parser_Membership extends CRM_Import_Parser {
22
23 protected $_mapperKeys;
24
25 private $_membershipTypeIndex;
26 private $_membershipStatusIndex;
27
28 /**
29 * Array of metadata for all available fields.
30 *
31 * @var array
32 */
33 protected $fieldMetadata = [];
34
35 /**
36 * Array of successfully imported membership id's
37 *
38 * @var array
39 */
40 protected $_newMemberships;
41
42
43 protected $_fileName;
44
45 /**
46 * Imported file size
47 * @var int
48 */
49 protected $_fileSize;
50
51 /**
52 * Separator being used
53 * @var string
54 */
55 protected $_separator;
56
57 /**
58 * Total number of lines in file
59 * @var int
60 */
61 protected $_lineCount;
62
63 /**
64 * Whether the file has a column header or not
65 *
66 * @var bool
67 */
68 protected $_haveColumnHeader;
69
70 /**
71 * Class constructor.
72 *
73 * @param $mapperKeys
74 */
75 public function __construct($mapperKeys = []) {
76 parent::__construct();
77 $this->_mapperKeys = $mapperKeys;
78 }
79
80 /**
81 * @param string $fileName
82 * @param string $separator
83 * @param $mapper
84 * @param bool $skipColumnHeader
85 * @param int $mode
86 * @param int $contactType
87 * @param int $onDuplicate
88 * @param int $statusID
89 * @param int $totalRowCount
90 *
91 * @return mixed
92 * @throws Exception
93 */
94 public function run(
95 $fileName,
96 $separator,
97 $mapper,
98 $skipColumnHeader = FALSE,
99 $mode = self::MODE_PREVIEW,
100 $contactType = self::CONTACT_INDIVIDUAL,
101 $onDuplicate = self::DUPLICATE_SKIP,
102 $statusID = NULL,
103 $totalRowCount = NULL
104 ) {
105 if (!is_array($fileName)) {
106 throw new CRM_Core_Exception('Unable to determine import file');
107 }
108 $fileName = $fileName['name'];
109
110 switch ($contactType) {
111 case self::CONTACT_INDIVIDUAL:
112 $this->_contactType = 'Individual';
113 break;
114
115 case self::CONTACT_HOUSEHOLD:
116 $this->_contactType = 'Household';
117 break;
118
119 case self::CONTACT_ORGANIZATION:
120 $this->_contactType = 'Organization';
121 }
122
123 $this->init();
124
125 $this->_haveColumnHeader = $skipColumnHeader;
126
127 $this->_separator = $separator;
128
129 $fd = fopen($fileName, "r");
130 if (!$fd) {
131 return FALSE;
132 }
133
134 $this->_lineCount = 0;
135 $this->_invalidRowCount = $this->_validCount = 0;
136 $this->_totalCount = 0;
137
138 $this->_errors = [];
139 $this->_warnings = [];
140
141 $this->_fileSize = number_format(filesize($fileName) / 1024.0, 2);
142
143 if ($mode == self::MODE_MAPFIELD) {
144 $this->_rows = [];
145 }
146 else {
147 $this->_activeFieldCount = count($this->_activeFields);
148 }
149 if ($statusID) {
150 $this->progressImport($statusID);
151 $startTimestamp = $currTimestamp = $prevTimestamp = CRM_Utils_Time::time();
152 }
153
154 while (!feof($fd)) {
155 $this->_lineCount++;
156
157 $values = fgetcsv($fd, 8192, $separator);
158 if (!$values) {
159 continue;
160 }
161
162 self::encloseScrub($values);
163
164 // skip column header if we're not in mapfield mode
165 if ($mode != self::MODE_MAPFIELD && $skipColumnHeader) {
166 $skipColumnHeader = FALSE;
167 continue;
168 }
169
170 /* trim whitespace around the values */
171 $empty = TRUE;
172 foreach ($values as $k => $v) {
173 $values[$k] = trim($v, " \t\r\n");
174 }
175 if (CRM_Utils_System::isNull($values)) {
176 continue;
177 }
178
179 $this->_totalCount++;
180
181 if ($mode == self::MODE_MAPFIELD) {
182 $returnCode = CRM_Import_Parser::VALID;
183 }
184 elseif ($mode == self::MODE_PREVIEW) {
185 $returnCode = $this->preview($values);
186 }
187 elseif ($mode == self::MODE_SUMMARY) {
188 $returnCode = $this->summary($values);
189 }
190 elseif ($mode == self::MODE_IMPORT) {
191 $returnCode = $this->import($onDuplicate, $values);
192 if ($statusID && (($this->_lineCount % 50) == 0)) {
193 $prevTimestamp = $this->progressImport($statusID, FALSE, $startTimestamp, $prevTimestamp, $totalRowCount);
194 }
195 }
196 else {
197 $returnCode = self::ERROR;
198 }
199
200 // note that a line could be valid but still produce a warning
201 if ($returnCode & self::VALID) {
202 $this->_validCount++;
203 if ($mode == self::MODE_MAPFIELD) {
204 $this->_rows[] = $values;
205 $this->_activeFieldCount = max($this->_activeFieldCount, count($values));
206 }
207 }
208
209 if ($returnCode & self::ERROR) {
210 $this->_invalidRowCount++;
211 $recordNumber = $this->_lineCount;
212 array_unshift($values, $recordNumber);
213 $this->_errors[] = $values;
214 }
215
216 if ($returnCode & self::DUPLICATE) {
217 $this->_duplicateCount++;
218 $recordNumber = $this->_lineCount;
219 array_unshift($values, $recordNumber);
220 $this->_duplicates[] = $values;
221 if ($onDuplicate != self::DUPLICATE_SKIP) {
222 $this->_validCount++;
223 }
224 }
225
226 // if we are done processing the maxNumber of lines, break
227 if ($this->_maxLinesToProcess > 0 && $this->_validCount >= $this->_maxLinesToProcess) {
228 break;
229 }
230 }
231
232 fclose($fd);
233
234 if ($mode == self::MODE_PREVIEW || $mode == self::MODE_IMPORT) {
235 $customHeaders = $mapper;
236
237 $customfields = CRM_Core_BAO_CustomField::getFields('Membership');
238 foreach ($customHeaders as $key => $value) {
239 if ($id = CRM_Core_BAO_CustomField::getKeyID($value)) {
240 $customHeaders[$key] = $customfields[$id][0];
241 }
242 }
243 if ($this->_invalidRowCount) {
244 // removed view url for invlaid contacts
245 $headers = array_merge([
246 ts('Line Number'),
247 ts('Reason'),
248 ], $customHeaders);
249 $this->_errorFileName = self::errorFileName(self::ERROR);
250
251 self::exportCSV($this->_errorFileName, $headers, $this->_errors);
252 }
253 if ($this->_duplicateCount) {
254 $headers = array_merge([
255 ts('Line Number'),
256 ts('View Membership URL'),
257 ], $customHeaders);
258
259 $this->_duplicateFileName = self::errorFileName(self::DUPLICATE);
260 self::exportCSV($this->_duplicateFileName, $headers, $this->_duplicates);
261 }
262 }
263 }
264
265 /**
266 * Given a list of the importable field keys that the user has selected
267 * set the active fields array to this list
268 *
269 * @param array $fieldKeys mapped array of values
270 *
271 * @return void
272 */
273 public function setActiveFields($fieldKeys) {
274 $this->_activeFieldCount = count($fieldKeys);
275 foreach ($fieldKeys as $key) {
276 if (empty($this->_fields[$key])) {
277 $this->_activeFields[] = new CRM_Member_Import_Field('', ts('- do not import -'));
278 }
279 else {
280 $this->_activeFields[] = clone($this->_fields[$key]);
281 }
282 }
283 }
284
285 /**
286 * Format the field values for input to the api.
287 *
288 * @return array
289 * (reference ) associative array of name/value pairs
290 */
291 public function &getActiveFieldParams() {
292 $params = [];
293 for ($i = 0; $i < $this->_activeFieldCount; $i++) {
294 if (isset($this->_activeFields[$i]->_value)
295 && !isset($params[$this->_activeFields[$i]->_name])
296 && !isset($this->_activeFields[$i]->_related)
297 ) {
298
299 $params[$this->_activeFields[$i]->_name] = $this->_activeFields[$i]->_value;
300 }
301 }
302 return $params;
303 }
304
305 /**
306 * @param string $name
307 * @param $title
308 * @param int $type
309 * @param string $headerPattern
310 * @param string $dataPattern
311 */
312 public function addField($name, $title, $type = CRM_Utils_Type::T_INT, $headerPattern = '//', $dataPattern = '//') {
313 if (empty($name)) {
314 $this->_fields['doNotImport'] = new CRM_Member_Import_Field($name, $title, $type, $headerPattern, $dataPattern);
315 }
316 else {
317
318 //$tempField = CRM_Contact_BAO_Contact::importableFields('Individual', null );
319 $tempField = CRM_Contact_BAO_Contact::importableFields('All', NULL);
320 if (!array_key_exists($name, $tempField)) {
321 $this->_fields[$name] = new CRM_Member_Import_Field($name, $title, $type, $headerPattern, $dataPattern);
322 }
323 else {
324 $this->_fields[$name] = new CRM_Contact_Import_Field($name, $title, $type, $headerPattern, $dataPattern,
325 CRM_Utils_Array::value('hasLocationType', $tempField[$name])
326 );
327 }
328 }
329 }
330
331 /**
332 * Store parser values.
333 *
334 * @param CRM_Core_Session $store
335 *
336 * @param int $mode
337 *
338 * @return void
339 */
340 public function set($store, $mode = self::MODE_SUMMARY) {
341 $store->set('fileSize', $this->_fileSize);
342 $store->set('lineCount', $this->_lineCount);
343 $store->set('separator', $this->_separator);
344 $store->set('fields', $this->getSelectValues());
345
346 $store->set('headerPatterns', $this->getHeaderPatterns());
347 $store->set('dataPatterns', $this->getDataPatterns());
348 $store->set('columnCount', $this->_activeFieldCount);
349
350 $store->set('totalRowCount', $this->_totalCount);
351 $store->set('validRowCount', $this->_validCount);
352 $store->set('invalidRowCount', $this->_invalidRowCount);
353
354 switch ($this->_contactType) {
355 case 'Individual':
356 $store->set('contactType', CRM_Import_Parser::CONTACT_INDIVIDUAL);
357 break;
358
359 case 'Household':
360 $store->set('contactType', CRM_Import_Parser::CONTACT_HOUSEHOLD);
361 break;
362
363 case 'Organization':
364 $store->set('contactType', CRM_Import_Parser::CONTACT_ORGANIZATION);
365 }
366
367 if ($this->_invalidRowCount) {
368 $store->set('errorsFileName', $this->_errorFileName);
369 }
370 if (isset($this->_rows) && !empty($this->_rows)) {
371 $store->set('dataValues', $this->_rows);
372 }
373
374 if ($mode == self::MODE_IMPORT) {
375 $store->set('duplicateRowCount', $this->_duplicateCount);
376 if ($this->_duplicateCount) {
377 $store->set('duplicatesFileName', $this->_duplicateFileName);
378 }
379 }
380 }
381
382 /**
383 * Export data to a CSV file.
384 *
385 * @param string $fileName
386 * @param array $header
387 * @param array $data
388 *
389 * @return void
390 */
391 public static function exportCSV($fileName, $header, $data) {
392 $output = [];
393 $fd = fopen($fileName, 'w');
394
395 foreach ($header as $key => $value) {
396 $header[$key] = "\"$value\"";
397 }
398 $config = CRM_Core_Config::singleton();
399 $output[] = implode($config->fieldSeparator, $header);
400
401 foreach ($data as $datum) {
402 foreach ($datum as $key => $value) {
403 if (is_array($value)) {
404 foreach ($value[0] as $k1 => $v1) {
405 if ($k1 == 'location_type_id') {
406 continue;
407 }
408 $datum[$k1] = $v1;
409 }
410 }
411 else {
412 $datum[$key] = "\"$value\"";
413 }
414 }
415 $output[] = implode($config->fieldSeparator, $datum);
416 }
417 fwrite($fd, implode("\n", $output));
418 fclose($fd);
419 }
420
421 /**
422 * The initializer code, called before the processing
423 *
424 * @return void
425 */
426 public function init() {
427 $this->fieldMetadata = CRM_Member_BAO_Membership::importableFields($this->_contactType, FALSE);
428
429 foreach ($this->fieldMetadata as $name => $field) {
430 // @todo - we don't really need to do all this.... fieldMetadata is just fine to use as is.
431 $field['type'] = CRM_Utils_Array::value('type', $field, CRM_Utils_Type::T_INT);
432 $field['dataPattern'] = CRM_Utils_Array::value('dataPattern', $field, '//');
433 $field['headerPattern'] = CRM_Utils_Array::value('headerPattern', $field, '//');
434 $this->addField($name, $field['title'], $field['type'], $field['headerPattern'], $field['dataPattern']);
435 }
436
437 $this->_newMemberships = [];
438
439 $this->setActiveFields($this->_mapperKeys);
440
441 // FIXME: we should do this in one place together with Form/MapField.php
442 $this->_membershipTypeIndex = -1;
443 $this->_membershipStatusIndex = -1;
444
445 $index = 0;
446 foreach ($this->_mapperKeys as $key) {
447 switch ($key) {
448
449 case 'membership_type_id':
450 $this->_membershipTypeIndex = $index;
451 break;
452
453 case 'status_id':
454 $this->_membershipStatusIndex = $index;
455 break;
456 }
457 $index++;
458 }
459 }
460
461 /**
462 * Handle the values in preview mode.
463 *
464 * @param array $values
465 * The array of values belonging to this line.
466 *
467 * @return bool
468 * the result of this processing
469 */
470 public function preview(&$values) {
471 return $this->summary($values);
472 }
473
474 /**
475 * Handle the values in summary mode.
476 *
477 * @param array $values
478 * The array of values belonging to this line.
479 *
480 * @return bool
481 * the result of this processing
482 */
483 public function summary(&$values) {
484
485 $this->setActiveFieldValues($values);
486
487 $errorRequired = FALSE;
488
489 if ($this->_membershipTypeIndex < 0) {
490 $errorRequired = TRUE;
491 }
492 else {
493 $errorRequired = !CRM_Utils_Array::value($this->_membershipTypeIndex, $values);
494 }
495
496 if ($errorRequired) {
497 array_unshift($values, ts('Missing required fields'));
498 return CRM_Import_Parser::ERROR;
499 }
500
501 $params = $this->getActiveFieldParams();
502 $errorMessage = NULL;
503
504 //To check whether start date or join date is provided
505 if (empty($params['membership_start_date']) && empty($params['membership_join_date'])) {
506 $errorMessage = 'Membership Start Date is required to create a memberships.';
507 CRM_Contact_Import_Parser_Contact::addToErrorMsg('Start Date', $errorMessage);
508 }
509
510 //for date-Formats
511 $session = CRM_Core_Session::singleton();
512 $dateType = $session->get('dateTypes');
513 foreach ($params as $key => $val) {
514
515 if ($val) {
516 switch ($key) {
517 case 'membership_join_date':
518 if (CRM_Utils_Date::convertToDefaultDate($params, $dateType, $key)) {
519 if (!CRM_Utils_Rule::date($params[$key])) {
520 CRM_Contact_Import_Parser_Contact::addToErrorMsg('Member Since', $errorMessage);
521 }
522 }
523 else {
524 CRM_Contact_Import_Parser_Contact::addToErrorMsg('Member Since', $errorMessage);
525 }
526 break;
527
528 case 'membership_start_date':
529 if (CRM_Utils_Date::convertToDefaultDate($params, $dateType, $key)) {
530 if (!CRM_Utils_Rule::date($params[$key])) {
531 CRM_Contact_Import_Parser_Contact::addToErrorMsg('Start Date', $errorMessage);
532 }
533 }
534 else {
535 CRM_Contact_Import_Parser_Contact::addToErrorMsg('Start Date', $errorMessage);
536 }
537 break;
538
539 case 'membership_end_date':
540 if (CRM_Utils_Date::convertToDefaultDate($params, $dateType, $key)) {
541 if (!CRM_Utils_Rule::date($params[$key])) {
542 CRM_Contact_Import_Parser_Contact::addToErrorMsg('End date', $errorMessage);
543 }
544 }
545 else {
546 CRM_Contact_Import_Parser_Contact::addToErrorMsg('End date', $errorMessage);
547 }
548 break;
549
550 case 'status_override_end_date':
551 if (CRM_Utils_Date::convertToDefaultDate($params, $dateType, $key)) {
552 if (!CRM_Utils_Rule::date($params[$key])) {
553 CRM_Contact_Import_Parser_Contact::addToErrorMsg('Status Override End Date', $errorMessage);
554 }
555 }
556 else {
557 CRM_Contact_Import_Parser_Contact::addToErrorMsg('Status Override End Date', $errorMessage);
558 }
559 break;
560
561 case 'membership_type_id':
562 // @todo - squish into membership status - can use same lines here too.
563 $membershipTypes = CRM_Member_PseudoConstant::membershipType();
564 if (!CRM_Utils_Array::crmInArray($val, $membershipTypes) &&
565 !array_key_exists($val, $membershipTypes)
566 ) {
567 CRM_Contact_Import_Parser_Contact::addToErrorMsg('Membership Type', $errorMessage);
568 }
569 break;
570
571 case 'status_id':
572 if (!empty($val) && !$this->parsePseudoConstantField($val, $this->fieldMetadata[$key])) {
573 CRM_Contact_Import_Parser_Contact::addToErrorMsg('Membership Status', $errorMessage);
574 }
575 break;
576
577 case 'email':
578 if (!CRM_Utils_Rule::email($val)) {
579 CRM_Contact_Import_Parser_Contact::addToErrorMsg('Email Address', $errorMessage);
580 }
581 }
582 }
583 }
584 //date-Format part ends
585
586 $params['contact_type'] = 'Membership';
587
588 //checking error in custom data
589 CRM_Contact_Import_Parser_Contact::isErrorInCustomData($params, $errorMessage);
590
591 if ($errorMessage) {
592 $tempMsg = "Invalid value for field(s) : $errorMessage";
593 array_unshift($values, $tempMsg);
594 $errorMessage = NULL;
595 return CRM_Import_Parser::ERROR;
596 }
597
598 return CRM_Import_Parser::VALID;
599 }
600
601 /**
602 * Handle the values in import mode.
603 *
604 * @param int $onDuplicate
605 * The code for what action to take on duplicates.
606 * @param array $values
607 * The array of values belonging to this line.
608 *
609 * @return bool
610 * the result of this processing
611 */
612 public function import($onDuplicate, &$values) {
613 try {
614 // first make sure this is a valid line
615 $response = $this->summary($values);
616 if ($response != CRM_Import_Parser::VALID) {
617 return $response;
618 }
619
620 $params = $this->getActiveFieldParams();
621
622 //assign join date equal to start date if join date is not provided
623 if (empty($params['membership_join_date']) && !empty($params['membership_start_date'])) {
624 $params['membership_join_date'] = $params['membership_start_date'];
625 }
626
627 $session = CRM_Core_Session::singleton();
628 $dateType = CRM_Core_Session::singleton()->get('dateTypes');
629 $formatted = [];
630 $customDataType = !empty($params['contact_type']) ? $params['contact_type'] : 'Membership';
631 $customFields = CRM_Core_BAO_CustomField::getFields($customDataType);
632
633 // don't add to recent items, CRM-4399
634 $formatted['skipRecentView'] = TRUE;
635 $dateLabels = [
636 'membership_join_date' => ts('Member Since'),
637 'membership_start_date' => ts('Start Date'),
638 'membership_end_date' => ts('End Date'),
639 ];
640 foreach ($params as $key => $val) {
641 if ($val) {
642 switch ($key) {
643 case 'membership_join_date':
644 case 'membership_start_date':
645 case 'membership_end_date':
646 if (CRM_Utils_Date::convertToDefaultDate($params, $dateType, $key)) {
647 if (!CRM_Utils_Rule::date($params[$key])) {
648 CRM_Contact_Import_Parser_Contact::addToErrorMsg($dateLabels[$key], $errorMessage);
649 }
650 }
651 else {
652 CRM_Contact_Import_Parser_Contact::addToErrorMsg($dateLabels[$key], $errorMessage);
653 }
654 break;
655
656 case 'membership_type_id':
657 if (!is_numeric($val)) {
658 unset($params['membership_type_id']);
659 $params['membership_type'] = $val;
660 }
661 break;
662
663 case 'status_id':
664 // @todo - we can do this based on the presence of 'pseudoconstant' in the metadata rather than field specific.
665 $params[$key] = $this->parsePseudoConstantField($val, $this->fieldMetadata[$key]);
666 break;
667
668 }
669 if ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) {
670 if ($customFields[$customFieldID]['data_type'] == 'Date') {
671 CRM_Contact_Import_Parser_Contact::formatCustomDate($params, $formatted, $dateType, $key);
672 unset($params[$key]);
673 }
674 elseif ($customFields[$customFieldID]['data_type'] == 'Boolean') {
675 $params[$key] = CRM_Utils_String::strtoboolstr($val);
676 }
677 }
678 }
679 }
680 //date-Format part ends
681
682 $formatValues = [];
683 foreach ($params as $key => $field) {
684 // ignore empty values or empty arrays etc
685 if (CRM_Utils_System::isNull($field)) {
686 continue;
687 }
688
689 $formatValues[$key] = $field;
690 }
691
692 //format params to meet api v2 requirements.
693 //@todo find a way to test removing this formatting
694 $formatError = $this->membership_format_params($formatValues, $formatted, TRUE);
695
696 if ($onDuplicate != CRM_Import_Parser::DUPLICATE_UPDATE) {
697 $formatted['custom'] = CRM_Core_BAO_CustomField::postProcess($formatted,
698 NULL,
699 'Membership'
700 );
701 }
702 else {
703 //fix for CRM-2219 Update Membership
704 // onDuplicate == CRM_Import_Parser::DUPLICATE_UPDATE
705 if (!empty($formatted['member_is_override']) && empty($formatted['status_id'])) {
706 array_unshift($values, 'Required parameter missing: Status');
707 return CRM_Import_Parser::ERROR;
708 }
709
710 if (!empty($formatValues['membership_id'])) {
711 $dao = new CRM_Member_BAO_Membership();
712 $dao->id = $formatValues['membership_id'];
713 $dates = ['join_date', 'start_date', 'end_date'];
714 foreach ($dates as $v) {
715 if (empty($formatted[$v])) {
716 $formatted[$v] = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_Membership', $formatValues['membership_id'], $v);
717 }
718 }
719
720 $formatted['custom'] = CRM_Core_BAO_CustomField::postProcess($formatted,
721 $formatValues['membership_id'],
722 'Membership'
723 );
724 if ($dao->find(TRUE)) {
725 if (empty($params['line_item']) && !empty($formatted['membership_type_id'])) {
726 CRM_Price_BAO_LineItem::getLineItemArray($formatted, NULL, 'membership', $formatted['membership_type_id']);
727 }
728
729 $newMembership = civicrm_api3('Membership', 'create', $formatted);
730 $this->_newMemberships[] = $newMembership['id'];
731 return CRM_Import_Parser::VALID;
732 }
733 else {
734 array_unshift($values, 'Matching Membership record not found for Membership ID ' . $formatValues['membership_id'] . '. Row was skipped.');
735 return CRM_Import_Parser::ERROR;
736 }
737 }
738 }
739
740 //Format dates
741 $startDate = CRM_Utils_Date::customFormat(CRM_Utils_Array::value('start_date', $formatted), '%Y-%m-%d');
742 $endDate = CRM_Utils_Date::customFormat(CRM_Utils_Array::value('end_date', $formatted), '%Y-%m-%d');
743 $joinDate = CRM_Utils_Date::customFormat(CRM_Utils_Array::value('join_date', $formatted), '%Y-%m-%d');
744
745 if (!$this->isContactIDColumnPresent()) {
746 $error = $this->checkContactDuplicate($formatValues);
747
748 if (CRM_Core_Error::isAPIError($error, CRM_Core_ERROR::DUPLICATE_CONTACT)) {
749 $matchedIDs = explode(',', $error['error_message']['params'][0]);
750 if (count($matchedIDs) > 1) {
751 array_unshift($values, 'Multiple matching contact records detected for this row. The membership was not imported');
752 return CRM_Import_Parser::ERROR;
753 }
754 else {
755 $cid = $matchedIDs[0];
756 $formatted['contact_id'] = $cid;
757
758 //fix for CRM-1924
759 $calcDates = CRM_Member_BAO_MembershipType::getDatesForMembershipType($formatted['membership_type_id'],
760 $joinDate,
761 $startDate,
762 $endDate
763 );
764 self::formattedDates($calcDates, $formatted);
765
766 //fix for CRM-3570, exclude the statuses those having is_admin = 1
767 //now user can import is_admin if is override is true.
768 $excludeIsAdmin = FALSE;
769 if (empty($formatted['member_is_override'])) {
770 $formatted['exclude_is_admin'] = $excludeIsAdmin = TRUE;
771 }
772 $calcStatus = CRM_Member_BAO_MembershipStatus::getMembershipStatusByDate($startDate,
773 $endDate,
774 $joinDate,
775 'now',
776 $excludeIsAdmin,
777 $formatted['membership_type_id'],
778 $formatted
779 );
780
781 if (empty($formatted['status_id'])) {
782 $formatted['status_id'] = $calcStatus['id'];
783 }
784 elseif (empty($formatted['member_is_override'])) {
785 if (empty($calcStatus)) {
786 array_unshift($values, 'Status in import row (' . $formatValues['status_id'] . ') does not match calculated status based on your configured Membership Status Rules. Record was not imported.');
787 return CRM_Import_Parser::ERROR;
788 }
789 elseif ($formatted['status_id'] != $calcStatus['id']) {
790 //Status Hold" is either NOT mapped or is FALSE
791 array_unshift($values, 'Status in import row (' . $formatValues['status_id'] . ') does not match calculated status based on your configured Membership Status Rules (' . $calcStatus['name'] . '). Record was not imported.');
792 return CRM_Import_Parser::ERROR;
793 }
794 }
795
796 $newMembership = civicrm_api3('membership', 'create', $formatted);
797
798 $this->_newMemberships[] = $newMembership['id'];
799 return CRM_Import_Parser::VALID;
800 }
801 }
802 else {
803 // Using new Dedupe rule.
804 $ruleParams = [
805 'contact_type' => $this->_contactType,
806 'used' => 'Unsupervised',
807 ];
808 $fieldsArray = CRM_Dedupe_BAO_DedupeRule::dedupeRuleFields($ruleParams);
809 $disp = '';
810
811 foreach ($fieldsArray as $value) {
812 if (array_key_exists(trim($value), $params)) {
813 $paramValue = $params[trim($value)];
814 if (is_array($paramValue)) {
815 $disp .= $params[trim($value)][0][trim($value)] . " ";
816 }
817 else {
818 $disp .= $params[trim($value)] . " ";
819 }
820 }
821 }
822
823 if (!empty($params['external_identifier'])) {
824 if ($disp) {
825 $disp .= "AND {$params['external_identifier']}";
826 }
827 else {
828 $disp = $params['external_identifier'];
829 }
830 }
831
832 array_unshift($values, 'No matching Contact found for (' . $disp . ')');
833 return CRM_Import_Parser::ERROR;
834 }
835 }
836 else {
837 if (!empty($formatValues['external_identifier'])) {
838 $checkCid = new CRM_Contact_DAO_Contact();
839 $checkCid->external_identifier = $formatValues['external_identifier'];
840 $checkCid->find(TRUE);
841 if ($checkCid->id != $formatted['contact_id']) {
842 array_unshift($values, 'Mismatch of External ID:' . $formatValues['external_identifier'] . ' and Contact Id:' . $formatted['contact_id']);
843 return CRM_Import_Parser::ERROR;
844 }
845 }
846
847 //to calculate dates
848 $calcDates = CRM_Member_BAO_MembershipType::getDatesForMembershipType($formatted['membership_type_id'],
849 $joinDate,
850 $startDate,
851 $endDate
852 );
853 self::formattedDates($calcDates, $formatted);
854 //end of date calculation part
855
856 //fix for CRM-3570, exclude the statuses those having is_admin = 1
857 //now user can import is_admin if is override is true.
858 $excludeIsAdmin = FALSE;
859 if (empty($formatted['member_is_override'])) {
860 $formatted['exclude_is_admin'] = $excludeIsAdmin = TRUE;
861 }
862 $calcStatus = CRM_Member_BAO_MembershipStatus::getMembershipStatusByDate($startDate,
863 $endDate,
864 $joinDate,
865 'now',
866 $excludeIsAdmin,
867 $formatted['membership_type_id'],
868 $formatted
869 );
870 if (empty($formatted['status_id'])) {
871 $formatted['status_id'] = $calcStatus['id'] ?? NULL;
872 }
873 elseif (empty($formatted['member_is_override'])) {
874 if (empty($calcStatus)) {
875 array_unshift($values, 'Status in import row (' . CRM_Utils_Array::value('status_id', $formatValues) . ') does not match calculated status based on your configured Membership Status Rules. Record was not imported.');
876 return CRM_Import_Parser::ERROR;
877 }
878 elseif ($formatted['status_id'] != $calcStatus['id']) {
879 //Status Hold" is either NOT mapped or is FALSE
880 array_unshift($values, 'Status in import row (' . CRM_Utils_Array::value('status_id', $formatValues) . ') does not match calculated status based on your configured Membership Status Rules (' . $calcStatus['name'] . '). Record was not imported.');
881 return CRM_Import_Parser::ERROR;
882 }
883 }
884
885 $newMembership = civicrm_api3('membership', 'create', $formatted);
886
887 $this->_newMemberships[] = $newMembership['id'];
888 return CRM_Import_Parser::VALID;
889 }
890 }
891 catch (Exception $e) {
892 array_unshift($values, $e->getMessage());
893 return CRM_Import_Parser::ERROR;
894 }
895 }
896
897 /**
898 * Get the array of successfully imported membership id's
899 *
900 * @return array
901 */
902 public function &getImportedMemberships() {
903 return $this->_newMemberships;
904 }
905
906 /**
907 * to calculate join, start and end dates
908 *
909 * @param array $calcDates
910 * Array of dates returned by getDatesForMembershipType().
911 *
912 * @param $formatted
913 *
914 */
915 public function formattedDates($calcDates, &$formatted) {
916 $dates = [
917 'join_date',
918 'start_date',
919 'end_date',
920 ];
921
922 foreach ($dates as $d) {
923 if (isset($formatted[$d]) &&
924 !CRM_Utils_System::isNull($formatted[$d])
925 ) {
926 $formatted[$d] = CRM_Utils_Date::isoToMysql($formatted[$d]);
927 }
928 elseif (isset($calcDates[$d])) {
929 $formatted[$d] = CRM_Utils_Date::isoToMysql($calcDates[$d]);
930 }
931 }
932 }
933
934 /**
935 * @deprecated - this function formats params according to v2 standards but
936 * need to be sure about the impact of not calling it so retaining on the import class
937 * take the input parameter list as specified in the data model and
938 * convert it into the same format that we use in QF and BAO object
939 *
940 * @param array $params
941 * Associative array of property name/value.
942 * pairs to insert in new contact.
943 * @param array $values
944 * The reformatted properties that we can use internally.
945 *
946 * @param array|bool $create Is the formatted Values array going to
947 * be used for CRM_Member_BAO_Membership:create()
948 *
949 * @throws Exception
950 * @return array|error
951 */
952 public function membership_format_params($params, &$values, $create = FALSE) {
953 require_once 'api/v3/utils.php';
954 $fields = CRM_Member_DAO_Membership::fields();
955 _civicrm_api3_store_values($fields, $params, $values);
956
957 $customFields = CRM_Core_BAO_CustomField::getFields('Membership');
958
959 foreach ($params as $key => $value) {
960
961 //Handling Custom Data
962 if ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) {
963 $values[$key] = $value;
964 $type = $customFields[$customFieldID]['html_type'];
965 if (CRM_Core_BAO_CustomField::isSerialized($customFields[$customFieldID])) {
966 $values[$key] = self::unserializeCustomValue($customFieldID, $value, $type);
967 }
968 }
969
970 switch ($key) {
971 case 'membership_contact_id':
972 if (!CRM_Utils_Rule::integer($value)) {
973 throw new Exception("contact_id not valid: $value");
974 }
975 $dao = new CRM_Core_DAO();
976 $qParams = [];
977 $svq = $dao->singleValueQuery("SELECT id FROM civicrm_contact WHERE id = $value",
978 $qParams
979 );
980 if (!$svq) {
981 throw new Exception("Invalid Contact ID: There is no contact record with contact_id = $value.");
982 }
983 $values['contact_id'] = $values['membership_contact_id'];
984 unset($values['membership_contact_id']);
985 break;
986
987 case 'membership_type_id':
988 if (!array_key_exists($value, CRM_Member_PseudoConstant::membershipType())) {
989 throw new Exception('Invalid Membership Type Id');
990 }
991 $values[$key] = $value;
992 break;
993
994 case 'membership_type':
995 $membershipTypeId = CRM_Utils_Array::key(ucfirst($value),
996 CRM_Member_PseudoConstant::membershipType()
997 );
998 if ($membershipTypeId) {
999 if (!empty($values['membership_type_id']) &&
1000 $membershipTypeId != $values['membership_type_id']
1001 ) {
1002 throw new Exception('Mismatched membership Type and Membership Type Id');
1003 }
1004 }
1005 else {
1006 throw new Exception('Invalid Membership Type');
1007 }
1008 $values['membership_type_id'] = $membershipTypeId;
1009 break;
1010
1011 default:
1012 break;
1013 }
1014 }
1015
1016 if ($create) {
1017 // CRM_Member_BAO_Membership::create() handles membership_start_date, membership_join_date,
1018 // membership_end_date and membership_source. So, if $values contains
1019 // membership_start_date, membership_end_date, membership_join_date or membership_source,
1020 // convert it to start_date, end_date, join_date or source
1021 $changes = [
1022 'membership_join_date' => 'join_date',
1023 'membership_start_date' => 'start_date',
1024 'membership_end_date' => 'end_date',
1025 'membership_source' => 'source',
1026 ];
1027
1028 foreach ($changes as $orgVal => $changeVal) {
1029 if (isset($values[$orgVal])) {
1030 $values[$changeVal] = $values[$orgVal];
1031 unset($values[$orgVal]);
1032 }
1033 }
1034 }
1035
1036 return NULL;
1037 }
1038
1039 /**
1040 * Is the contact ID mapped.
1041 *
1042 * @return bool
1043 */
1044 protected function isContactIDColumnPresent(): bool {
1045 return in_array('membership_contact_id', $this->_mapperKeys, TRUE);
1046 }
1047
1048 }