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