Merge pull request #18745 from seamuslee001/backdrop_session
[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_Member_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 * Class constructor.
44 *
45 * @param $mapperKeys
46 */
47 public function __construct($mapperKeys) {
48 parent::__construct();
49 $this->_mapperKeys = $mapperKeys;
50 }
51
52 /**
53 * The initializer code, called before the processing
54 *
55 * @return void
56 */
57 public function init() {
58 $this->fieldMetadata = CRM_Member_BAO_Membership::importableFields($this->_contactType, FALSE);
59
60 foreach ($this->fieldMetadata as $name => $field) {
61 // @todo - we don't really need to do all this.... fieldMetadata is just fine to use as is.
62 $field['type'] = CRM_Utils_Array::value('type', $field, CRM_Utils_Type::T_INT);
63 $field['dataPattern'] = CRM_Utils_Array::value('dataPattern', $field, '//');
64 $field['headerPattern'] = CRM_Utils_Array::value('headerPattern', $field, '//');
65 $this->addField($name, $field['title'], $field['type'], $field['headerPattern'], $field['dataPattern']);
66 }
67
68 $this->_newMemberships = [];
69
70 $this->setActiveFields($this->_mapperKeys);
71
72 // FIXME: we should do this in one place together with Form/MapField.php
73 $this->_membershipTypeIndex = -1;
74 $this->_membershipStatusIndex = -1;
75
76 $index = 0;
77 foreach ($this->_mapperKeys as $key) {
78 switch ($key) {
79
80 case 'membership_type_id':
81 $this->_membershipTypeIndex = $index;
82 break;
83
84 case 'status_id':
85 $this->_membershipStatusIndex = $index;
86 break;
87 }
88 $index++;
89 }
90 }
91
92 /**
93 * Handle the values in mapField mode.
94 *
95 * @param array $values
96 * The array of values belonging to this line.
97 *
98 * @return bool
99 */
100 public function mapField(&$values) {
101 return CRM_Import_Parser::VALID;
102 }
103
104 /**
105 * Handle the values in preview mode.
106 *
107 * @param array $values
108 * The array of values belonging to this line.
109 *
110 * @return bool
111 * the result of this processing
112 */
113 public function preview(&$values) {
114 return $this->summary($values);
115 }
116
117 /**
118 * Handle the values in summary mode.
119 *
120 * @param array $values
121 * The array of values belonging to this line.
122 *
123 * @return bool
124 * the result of this processing
125 */
126 public function summary(&$values) {
127 $erroneousField = NULL;
128 $this->setActiveFieldValues($values, $erroneousField);
129
130 $errorRequired = FALSE;
131
132 if ($this->_membershipTypeIndex < 0) {
133 $errorRequired = TRUE;
134 }
135 else {
136 $errorRequired = !CRM_Utils_Array::value($this->_membershipTypeIndex, $values);
137 }
138
139 if ($errorRequired) {
140 array_unshift($values, ts('Missing required fields'));
141 return CRM_Import_Parser::ERROR;
142 }
143
144 $params = $this->getActiveFieldParams();
145 $errorMessage = NULL;
146
147 //To check whether start date or join date is provided
148 if (empty($params['membership_start_date']) && empty($params['membership_join_date'])) {
149 $errorMessage = 'Membership Start Date is required to create a memberships.';
150 CRM_Contact_Import_Parser_Contact::addToErrorMsg('Start Date', $errorMessage);
151 }
152
153 //for date-Formats
154 $session = CRM_Core_Session::singleton();
155 $dateType = $session->get('dateTypes');
156 foreach ($params as $key => $val) {
157
158 if ($val) {
159 switch ($key) {
160 case 'membership_join_date':
161 if (CRM_Utils_Date::convertToDefaultDate($params, $dateType, $key)) {
162 if (!CRM_Utils_Rule::date($params[$key])) {
163 CRM_Contact_Import_Parser_Contact::addToErrorMsg('Member Since', $errorMessage);
164 }
165 }
166 else {
167 CRM_Contact_Import_Parser_Contact::addToErrorMsg('Member Since', $errorMessage);
168 }
169 break;
170
171 case 'membership_start_date':
172 if (CRM_Utils_Date::convertToDefaultDate($params, $dateType, $key)) {
173 if (!CRM_Utils_Rule::date($params[$key])) {
174 CRM_Contact_Import_Parser_Contact::addToErrorMsg('Start Date', $errorMessage);
175 }
176 }
177 else {
178 CRM_Contact_Import_Parser_Contact::addToErrorMsg('Start Date', $errorMessage);
179 }
180 break;
181
182 case 'membership_end_date':
183 if (CRM_Utils_Date::convertToDefaultDate($params, $dateType, $key)) {
184 if (!CRM_Utils_Rule::date($params[$key])) {
185 CRM_Contact_Import_Parser_Contact::addToErrorMsg('End date', $errorMessage);
186 }
187 }
188 else {
189 CRM_Contact_Import_Parser_Contact::addToErrorMsg('End date', $errorMessage);
190 }
191 break;
192
193 case 'status_override_end_date':
194 if (CRM_Utils_Date::convertToDefaultDate($params, $dateType, $key)) {
195 if (!CRM_Utils_Rule::date($params[$key])) {
196 CRM_Contact_Import_Parser_Contact::addToErrorMsg('Status Override End Date', $errorMessage);
197 }
198 }
199 else {
200 CRM_Contact_Import_Parser_Contact::addToErrorMsg('Status Override End Date', $errorMessage);
201 }
202 break;
203
204 case 'membership_type_id':
205 // @todo - squish into membership status - can use same lines here too.
206 $membershipTypes = CRM_Member_PseudoConstant::membershipType();
207 if (!CRM_Utils_Array::crmInArray($val, $membershipTypes) &&
208 !array_key_exists($val, $membershipTypes)
209 ) {
210 CRM_Contact_Import_Parser_Contact::addToErrorMsg('Membership Type', $errorMessage);
211 }
212 break;
213
214 case 'status_id':
215 if (!empty($val) && !$this->parsePseudoConstantField($val, $this->fieldMetadata[$key])) {
216 CRM_Contact_Import_Parser_Contact::addToErrorMsg('Membership Status', $errorMessage);
217 }
218 break;
219
220 case 'email':
221 if (!CRM_Utils_Rule::email($val)) {
222 CRM_Contact_Import_Parser_Contact::addToErrorMsg('Email Address', $errorMessage);
223 }
224 }
225 }
226 }
227 //date-Format part ends
228
229 $params['contact_type'] = 'Membership';
230
231 //checking error in custom data
232 CRM_Contact_Import_Parser_Contact::isErrorInCustomData($params, $errorMessage);
233
234 if ($errorMessage) {
235 $tempMsg = "Invalid value for field(s) : $errorMessage";
236 array_unshift($values, $tempMsg);
237 $errorMessage = NULL;
238 return CRM_Import_Parser::ERROR;
239 }
240
241 return CRM_Import_Parser::VALID;
242 }
243
244 /**
245 * Handle the values in import mode.
246 *
247 * @param int $onDuplicate
248 * The code for what action to take on duplicates.
249 * @param array $values
250 * The array of values belonging to this line.
251 *
252 * @return bool
253 * the result of this processing
254 */
255 public function import($onDuplicate, &$values) {
256 try {
257 // first make sure this is a valid line
258 $response = $this->summary($values);
259 if ($response != CRM_Import_Parser::VALID) {
260 return $response;
261 }
262
263 $params = $this->getActiveFieldParams();
264
265 //assign join date equal to start date if join date is not provided
266 if (empty($params['membership_join_date']) && !empty($params['membership_start_date'])) {
267 $params['membership_join_date'] = $params['membership_start_date'];
268 }
269
270 $session = CRM_Core_Session::singleton();
271 $dateType = CRM_Core_Session::singleton()->get('dateTypes');
272 $formatted = [];
273 $customDataType = !empty($params['contact_type']) ? $params['contact_type'] : 'Membership';
274 $customFields = CRM_Core_BAO_CustomField::getFields($customDataType);
275
276 // don't add to recent items, CRM-4399
277 $formatted['skipRecentView'] = TRUE;
278 $dateLabels = [
279 'membership_join_date' => ts('Member Since'),
280 'membership_start_date' => ts('Start Date'),
281 'membership_end_date' => ts('End Date'),
282 ];
283 foreach ($params as $key => $val) {
284 if ($val) {
285 switch ($key) {
286 case 'membership_join_date':
287 case 'membership_start_date':
288 case 'membership_end_date':
289 if (CRM_Utils_Date::convertToDefaultDate($params, $dateType, $key)) {
290 if (!CRM_Utils_Rule::date($params[$key])) {
291 CRM_Contact_Import_Parser_Contact::addToErrorMsg($dateLabels[$key], $errorMessage);
292 }
293 }
294 else {
295 CRM_Contact_Import_Parser_Contact::addToErrorMsg($dateLabels[$key], $errorMessage);
296 }
297 break;
298
299 case 'membership_type_id':
300 if (!is_numeric($val)) {
301 unset($params['membership_type_id']);
302 $params['membership_type'] = $val;
303 }
304 break;
305
306 case 'status_id':
307 // @todo - we can do this based on the presence of 'pseudoconstant' in the metadata rather than field specific.
308 $params[$key] = $this->parsePseudoConstantField($val, $this->fieldMetadata[$key]);
309 break;
310
311 }
312 if ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) {
313 if ($customFields[$customFieldID]['data_type'] == 'Date') {
314 CRM_Contact_Import_Parser_Contact::formatCustomDate($params, $formatted, $dateType, $key);
315 unset($params[$key]);
316 }
317 elseif ($customFields[$customFieldID]['data_type'] == 'Boolean') {
318 $params[$key] = CRM_Utils_String::strtoboolstr($val);
319 }
320 }
321 }
322 }
323 //date-Format part ends
324
325 $formatValues = [];
326 foreach ($params as $key => $field) {
327 if ($field == NULL || $field === '') {
328 continue;
329 }
330
331 $formatValues[$key] = $field;
332 }
333
334 //format params to meet api v2 requirements.
335 //@todo find a way to test removing this formatting
336 $formatError = $this->membership_format_params($formatValues, $formatted, TRUE);
337
338 if ($onDuplicate != CRM_Import_Parser::DUPLICATE_UPDATE) {
339 $formatted['custom'] = CRM_Core_BAO_CustomField::postProcess($formatted,
340 NULL,
341 'Membership'
342 );
343 }
344 else {
345 //fix for CRM-2219 Update Membership
346 // onDuplicate == CRM_Import_Parser::DUPLICATE_UPDATE
347 if (!empty($formatted['member_is_override']) && empty($formatted['status_id'])) {
348 array_unshift($values, 'Required parameter missing: Status');
349 return CRM_Import_Parser::ERROR;
350 }
351
352 if (!empty($formatValues['membership_id'])) {
353 $dao = new CRM_Member_BAO_Membership();
354 $dao->id = $formatValues['membership_id'];
355 $dates = ['join_date', 'start_date', 'end_date'];
356 foreach ($dates as $v) {
357 if (empty($formatted[$v])) {
358 $formatted[$v] = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_Membership', $formatValues['membership_id'], $v);
359 }
360 }
361
362 $formatted['custom'] = CRM_Core_BAO_CustomField::postProcess($formatted,
363 $formatValues['membership_id'],
364 'Membership'
365 );
366 if ($dao->find(TRUE)) {
367 if (empty($params['line_item']) && !empty($formatted['membership_type_id'])) {
368 CRM_Price_BAO_LineItem::getLineItemArray($formatted, NULL, 'membership', $formatted['membership_type_id']);
369 }
370
371 $newMembership = civicrm_api3('Membership', 'create', $formatted);
372 $this->_newMemberships[] = $newMembership['id'];
373 return CRM_Import_Parser::VALID;
374 }
375 else {
376 array_unshift($values, 'Matching Membership record not found for Membership ID ' . $formatValues['membership_id'] . '. Row was skipped.');
377 return CRM_Import_Parser::ERROR;
378 }
379 }
380 }
381
382 //Format dates
383 $startDate = CRM_Utils_Date::customFormat(CRM_Utils_Array::value('start_date', $formatted), '%Y-%m-%d');
384 $endDate = CRM_Utils_Date::customFormat(CRM_Utils_Array::value('end_date', $formatted), '%Y-%m-%d');
385 $joinDate = CRM_Utils_Date::customFormat(CRM_Utils_Array::value('join_date', $formatted), '%Y-%m-%d');
386
387 if (!$this->isContactIDColumnPresent()) {
388 $error = $this->checkContactDuplicate($formatValues);
389
390 if (CRM_Core_Error::isAPIError($error, CRM_Core_ERROR::DUPLICATE_CONTACT)) {
391 $matchedIDs = explode(',', $error['error_message']['params'][0]);
392 if (count($matchedIDs) > 1) {
393 array_unshift($values, 'Multiple matching contact records detected for this row. The membership was not imported');
394 return CRM_Import_Parser::ERROR;
395 }
396 else {
397 $cid = $matchedIDs[0];
398 $formatted['contact_id'] = $cid;
399
400 //fix for CRM-1924
401 $calcDates = CRM_Member_BAO_MembershipType::getDatesForMembershipType($formatted['membership_type_id'],
402 $joinDate,
403 $startDate,
404 $endDate
405 );
406 self::formattedDates($calcDates, $formatted);
407
408 //fix for CRM-3570, exclude the statuses those having is_admin = 1
409 //now user can import is_admin if is override is true.
410 $excludeIsAdmin = FALSE;
411 if (empty($formatted['member_is_override'])) {
412 $formatted['exclude_is_admin'] = $excludeIsAdmin = TRUE;
413 }
414 $calcStatus = CRM_Member_BAO_MembershipStatus::getMembershipStatusByDate($startDate,
415 $endDate,
416 $joinDate,
417 'now',
418 $excludeIsAdmin,
419 $formatted['membership_type_id'],
420 $formatted
421 );
422
423 if (empty($formatted['status_id'])) {
424 $formatted['status_id'] = $calcStatus['id'];
425 }
426 elseif (empty($formatted['member_is_override'])) {
427 if (empty($calcStatus)) {
428 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.');
429 return CRM_Import_Parser::ERROR;
430 }
431 elseif ($formatted['status_id'] != $calcStatus['id']) {
432 //Status Hold" is either NOT mapped or is FALSE
433 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.');
434 return CRM_Import_Parser::ERROR;
435 }
436 }
437
438 $newMembership = civicrm_api3('membership', 'create', $formatted);
439
440 $this->_newMemberships[] = $newMembership['id'];
441 return CRM_Import_Parser::VALID;
442 }
443 }
444 else {
445 // Using new Dedupe rule.
446 $ruleParams = [
447 'contact_type' => $this->_contactType,
448 'used' => 'Unsupervised',
449 ];
450 $fieldsArray = CRM_Dedupe_BAO_Rule::dedupeRuleFields($ruleParams);
451 $disp = '';
452
453 foreach ($fieldsArray as $value) {
454 if (array_key_exists(trim($value), $params)) {
455 $paramValue = $params[trim($value)];
456 if (is_array($paramValue)) {
457 $disp .= $params[trim($value)][0][trim($value)] . " ";
458 }
459 else {
460 $disp .= $params[trim($value)] . " ";
461 }
462 }
463 }
464
465 if (!empty($params['external_identifier'])) {
466 if ($disp) {
467 $disp .= "AND {$params['external_identifier']}";
468 }
469 else {
470 $disp = $params['external_identifier'];
471 }
472 }
473
474 array_unshift($values, 'No matching Contact found for (' . $disp . ')');
475 return CRM_Import_Parser::ERROR;
476 }
477 }
478 else {
479 if (!empty($formatValues['external_identifier'])) {
480 $checkCid = new CRM_Contact_DAO_Contact();
481 $checkCid->external_identifier = $formatValues['external_identifier'];
482 $checkCid->find(TRUE);
483 if ($checkCid->id != $formatted['contact_id']) {
484 array_unshift($values, 'Mismatch of External ID:' . $formatValues['external_identifier'] . ' and Contact Id:' . $formatted['contact_id']);
485 return CRM_Import_Parser::ERROR;
486 }
487 }
488
489 //to calculate dates
490 $calcDates = CRM_Member_BAO_MembershipType::getDatesForMembershipType($formatted['membership_type_id'],
491 $joinDate,
492 $startDate,
493 $endDate
494 );
495 self::formattedDates($calcDates, $formatted);
496 //end of date calculation part
497
498 //fix for CRM-3570, exclude the statuses those having is_admin = 1
499 //now user can import is_admin if is override is true.
500 $excludeIsAdmin = FALSE;
501 if (empty($formatted['member_is_override'])) {
502 $formatted['exclude_is_admin'] = $excludeIsAdmin = TRUE;
503 }
504 $calcStatus = CRM_Member_BAO_MembershipStatus::getMembershipStatusByDate($startDate,
505 $endDate,
506 $joinDate,
507 'now',
508 $excludeIsAdmin,
509 $formatted['membership_type_id'],
510 $formatted
511 );
512 if (empty($formatted['status_id'])) {
513 $formatted['status_id'] = $calcStatus['id'] ?? NULL;
514 }
515 elseif (empty($formatted['member_is_override'])) {
516 if (empty($calcStatus)) {
517 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.');
518 return CRM_Import_Parser::ERROR;
519 }
520 elseif ($formatted['status_id'] != $calcStatus['id']) {
521 //Status Hold" is either NOT mapped or is FALSE
522 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.');
523 return CRM_Import_Parser::ERROR;
524 }
525 }
526
527 $newMembership = civicrm_api3('membership', 'create', $formatted);
528
529 $this->_newMemberships[] = $newMembership['id'];
530 return CRM_Import_Parser::VALID;
531 }
532 }
533 catch (Exception $e) {
534 array_unshift($values, $e->getMessage());
535 return CRM_Import_Parser::ERROR;
536 }
537 }
538
539 /**
540 * Get the array of successfully imported membership id's
541 *
542 * @return array
543 */
544 public function &getImportedMemberships() {
545 return $this->_newMemberships;
546 }
547
548 /**
549 * The initializer code, called before the processing
550 *
551 * @return void
552 */
553 public function fini() {
554 }
555
556 /**
557 * to calculate join, start and end dates
558 *
559 * @param array $calcDates
560 * Array of dates returned by getDatesForMembershipType().
561 *
562 * @param $formatted
563 *
564 */
565 public function formattedDates($calcDates, &$formatted) {
566 $dates = [
567 'join_date',
568 'start_date',
569 'end_date',
570 ];
571
572 foreach ($dates as $d) {
573 if (isset($formatted[$d]) &&
574 !CRM_Utils_System::isNull($formatted[$d])
575 ) {
576 $formatted[$d] = CRM_Utils_Date::isoToMysql($formatted[$d]);
577 }
578 elseif (isset($calcDates[$d])) {
579 $formatted[$d] = CRM_Utils_Date::isoToMysql($calcDates[$d]);
580 }
581 }
582 }
583
584 /**
585 * @deprecated - this function formats params according to v2 standards but
586 * need to be sure about the impact of not calling it so retaining on the import class
587 * take the input parameter list as specified in the data model and
588 * convert it into the same format that we use in QF and BAO object
589 *
590 * @param array $params
591 * Associative array of property name/value.
592 * pairs to insert in new contact.
593 * @param array $values
594 * The reformatted properties that we can use internally.
595 *
596 * @param array|bool $create Is the formatted Values array going to
597 * be used for CRM_Member_BAO_Membership:create()
598 *
599 * @throws Exception
600 * @return array|error
601 */
602 public function membership_format_params($params, &$values, $create = FALSE) {
603 require_once 'api/v3/utils.php';
604 $fields = CRM_Member_DAO_Membership::fields();
605 _civicrm_api3_store_values($fields, $params, $values);
606
607 $customFields = CRM_Core_BAO_CustomField::getFields('Membership');
608
609 foreach ($params as $key => $value) {
610 // ignore empty values or empty arrays etc
611 if (CRM_Utils_System::isNull($value)) {
612 continue;
613 }
614
615 //Handling Custom Data
616 if ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) {
617 $values[$key] = $value;
618 $type = $customFields[$customFieldID]['html_type'];
619 if (CRM_Core_BAO_CustomField::isSerialized($customFields[$customFieldID])) {
620 $values[$key] = self::unserializeCustomValue($customFieldID, $value, $type);
621 }
622 }
623
624 switch ($key) {
625 case 'membership_contact_id':
626 if (!CRM_Utils_Rule::integer($value)) {
627 throw new Exception("contact_id not valid: $value");
628 }
629 $dao = new CRM_Core_DAO();
630 $qParams = [];
631 $svq = $dao->singleValueQuery("SELECT id FROM civicrm_contact WHERE id = $value",
632 $qParams
633 );
634 if (!$svq) {
635 throw new Exception("Invalid Contact ID: There is no contact record with contact_id = $value.");
636 }
637 $values['contact_id'] = $values['membership_contact_id'];
638 unset($values['membership_contact_id']);
639 break;
640
641 case 'membership_type_id':
642 if (!array_key_exists($value, CRM_Member_PseudoConstant::membershipType())) {
643 throw new Exception('Invalid Membership Type Id');
644 }
645 $values[$key] = $value;
646 break;
647
648 case 'membership_type':
649 $membershipTypeId = CRM_Utils_Array::key(ucfirst($value),
650 CRM_Member_PseudoConstant::membershipType()
651 );
652 if ($membershipTypeId) {
653 if (!empty($values['membership_type_id']) &&
654 $membershipTypeId != $values['membership_type_id']
655 ) {
656 throw new Exception('Mismatched membership Type and Membership Type Id');
657 }
658 }
659 else {
660 throw new Exception('Invalid Membership Type');
661 }
662 $values['membership_type_id'] = $membershipTypeId;
663 break;
664
665 default:
666 break;
667 }
668 }
669
670 _civicrm_api3_custom_format_params($params, $values, 'Membership');
671
672 if ($create) {
673 // CRM_Member_BAO_Membership::create() handles membership_start_date, membership_join_date,
674 // membership_end_date and membership_source. So, if $values contains
675 // membership_start_date, membership_end_date, membership_join_date or membership_source,
676 // convert it to start_date, end_date, join_date or source
677 $changes = [
678 'membership_join_date' => 'join_date',
679 'membership_start_date' => 'start_date',
680 'membership_end_date' => 'end_date',
681 'membership_source' => 'source',
682 ];
683
684 foreach ($changes as $orgVal => $changeVal) {
685 if (isset($values[$orgVal])) {
686 $values[$changeVal] = $values[$orgVal];
687 unset($values[$orgVal]);
688 }
689 }
690 }
691
692 return NULL;
693 }
694
695 /**
696 * Is the contact ID mapped.
697 *
698 * @return bool
699 */
700 protected function isContactIDColumnPresent(): bool {
701 return in_array('membership_contact_id', $this->_mapperKeys, TRUE);
702 }
703
704 }