3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 +--------------------------------------------------------------------+
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
19 * class to parse membership csv files
21 class CRM_Member_Import_Parser_Membership
extends CRM_Member_Import_Parser
{
23 protected $_mapperKeys;
25 private $_membershipTypeIndex;
26 private $_membershipStatusIndex;
29 * Array of metadata for all available fields.
33 protected $fieldMetadata = [];
36 * Array of successfully imported membership id's
40 protected $_newMemberships;
47 public function __construct($mapperKeys) {
48 parent
::__construct();
49 $this->_mapperKeys
= $mapperKeys;
53 * The initializer code, called before the processing
57 public function init() {
58 $this->fieldMetadata
= CRM_Member_BAO_Membership
::importableFields($this->_contactType
, FALSE);
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']);
68 $this->_newMemberships
= [];
70 $this->setActiveFields($this->_mapperKeys
);
72 // FIXME: we should do this in one place together with Form/MapField.php
73 $this->_membershipTypeIndex
= -1;
74 $this->_membershipStatusIndex
= -1;
77 foreach ($this->_mapperKeys
as $key) {
80 case 'membership_type_id':
81 $this->_membershipTypeIndex
= $index;
85 $this->_membershipStatusIndex
= $index;
93 * Handle the values in mapField mode.
95 * @param array $values
96 * The array of values belonging to this line.
100 public function mapField(&$values) {
101 return CRM_Import_Parser
::VALID
;
105 * Handle the values in preview mode.
107 * @param array $values
108 * The array of values belonging to this line.
111 * the result of this processing
113 public function preview(&$values) {
114 return $this->summary($values);
118 * Handle the values in summary mode.
120 * @param array $values
121 * The array of values belonging to this line.
124 * the result of this processing
126 public function summary(&$values) {
127 $erroneousField = NULL;
128 $this->setActiveFieldValues($values, $erroneousField);
130 $errorRequired = FALSE;
132 if ($this->_membershipTypeIndex
< 0) {
133 $errorRequired = TRUE;
136 $errorRequired = !CRM_Utils_Array
::value($this->_membershipTypeIndex
, $values);
139 if ($errorRequired) {
140 array_unshift($values, ts('Missing required fields'));
141 return CRM_Import_Parser
::ERROR
;
144 $params = $this->getActiveFieldParams();
145 $errorMessage = NULL;
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);
154 $session = CRM_Core_Session
::singleton();
155 $dateType = $session->get('dateTypes');
156 foreach ($params as $key => $val) {
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);
167 CRM_Contact_Import_Parser_Contact
::addToErrorMsg('Member Since', $errorMessage);
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);
178 CRM_Contact_Import_Parser_Contact
::addToErrorMsg('Start Date', $errorMessage);
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);
189 CRM_Contact_Import_Parser_Contact
::addToErrorMsg('End date', $errorMessage);
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);
200 CRM_Contact_Import_Parser_Contact
::addToErrorMsg('Status Override End Date', $errorMessage);
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)
210 CRM_Contact_Import_Parser_Contact
::addToErrorMsg('Membership Type', $errorMessage);
215 if (!empty($val) && !$this->parsePseudoConstantField($val, $this->fieldMetadata
[$key])) {
216 CRM_Contact_Import_Parser_Contact
::addToErrorMsg('Membership Status', $errorMessage);
221 if (!CRM_Utils_Rule
::email($val)) {
222 CRM_Contact_Import_Parser_Contact
::addToErrorMsg('Email Address', $errorMessage);
227 //date-Format part ends
229 $params['contact_type'] = 'Membership';
231 //checking error in custom data
232 CRM_Contact_Import_Parser_Contact
::isErrorInCustomData($params, $errorMessage);
235 $tempMsg = "Invalid value for field(s) : $errorMessage";
236 array_unshift($values, $tempMsg);
237 $errorMessage = NULL;
238 return CRM_Import_Parser
::ERROR
;
241 return CRM_Import_Parser
::VALID
;
245 * Handle the values in import mode.
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.
253 * the result of this processing
255 public function import($onDuplicate, &$values) {
257 // first make sure this is a valid line
258 $response = $this->summary($values);
259 if ($response != CRM_Import_Parser
::VALID
) {
263 $params = $this->getActiveFieldParams();
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'];
270 $session = CRM_Core_Session
::singleton();
271 $dateType = CRM_Core_Session
::singleton()->get('dateTypes');
273 $customDataType = !empty($params['contact_type']) ?
$params['contact_type'] : 'Membership';
274 $customFields = CRM_Core_BAO_CustomField
::getFields($customDataType);
276 // don't add to recent items, CRM-4399
277 $formatted['skipRecentView'] = TRUE;
279 'membership_join_date' => ts('Member Since'),
280 'membership_start_date' => ts('Start Date'),
281 'membership_end_date' => ts('End Date'),
283 foreach ($params as $key => $val) {
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);
295 CRM_Contact_Import_Parser_Contact
::addToErrorMsg($dateLabels[$key], $errorMessage);
299 case 'membership_type_id':
300 if (!is_numeric($val)) {
301 unset($params['membership_type_id']);
302 $params['membership_type'] = $val;
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]);
311 case 'member_is_override':
312 $params[$key] = CRM_Utils_String
::strtobool($val);
315 if ($customFieldID = CRM_Core_BAO_CustomField
::getKeyID($key)) {
316 if ($customFields[$customFieldID]['data_type'] == 'Date') {
317 CRM_Contact_Import_Parser_Contact
::formatCustomDate($params, $formatted, $dateType, $key);
318 unset($params[$key]);
320 elseif ($customFields[$customFieldID]['data_type'] == 'Boolean') {
321 $params[$key] = CRM_Utils_String
::strtoboolstr($val);
326 //date-Format part ends
329 foreach ($params as $key => $field) {
330 if ($field == NULL ||
$field === '') {
334 $formatValues[$key] = $field;
337 //format params to meet api v2 requirements.
338 //@todo find a way to test removing this formatting
339 $formatError = $this->membership_format_params($formatValues, $formatted, TRUE);
341 if ($onDuplicate != CRM_Import_Parser
::DUPLICATE_UPDATE
) {
342 $formatted['custom'] = CRM_Core_BAO_CustomField
::postProcess($formatted,
348 //fix for CRM-2219 Update Membership
349 // onDuplicate == CRM_Import_Parser::DUPLICATE_UPDATE
350 if (!empty($formatted['member_is_override']) && empty($formatted['status_id'])) {
351 array_unshift($values, 'Required parameter missing: Status');
352 return CRM_Import_Parser
::ERROR
;
355 if (!empty($formatValues['membership_id'])) {
356 $dao = new CRM_Member_BAO_Membership();
357 $dao->id
= $formatValues['membership_id'];
358 $dates = ['join_date', 'start_date', 'end_date'];
359 foreach ($dates as $v) {
360 if (empty($formatted[$v])) {
361 $formatted[$v] = CRM_Core_DAO
::getFieldValue('CRM_Member_DAO_Membership', $formatValues['membership_id'], $v);
365 $formatted['custom'] = CRM_Core_BAO_CustomField
::postProcess($formatted,
366 $formatValues['membership_id'],
369 if ($dao->find(TRUE)) {
370 if (empty($params['line_item']) && !empty($formatted['membership_type_id'])) {
371 CRM_Price_BAO_LineItem
::getLineItemArray($formatted, NULL, 'membership', $formatted['membership_type_id']);
374 // @todo stop passing $ids array (and put details in $formatted if required)
376 'membership' => $formatValues['membership_id'],
377 'userId' => $session->get('userID'),
379 $newMembership = CRM_Member_BAO_Membership
::create($formatted, $ids, TRUE);
380 if (civicrm_error($newMembership)) {
381 array_unshift($values, $newMembership['is_error'] . ' for Membership ID ' . $formatValues['membership_id'] . '. Row was skipped.');
382 return CRM_Import_Parser
::ERROR
;
385 $this->_newMemberships
[] = $newMembership->id
;
386 return CRM_Import_Parser
::VALID
;
390 array_unshift($values, 'Matching Membership record not found for Membership ID ' . $formatValues['membership_id'] . '. Row was skipped.');
391 return CRM_Import_Parser
::ERROR
;
397 $startDate = CRM_Utils_Date
::customFormat(CRM_Utils_Array
::value('start_date', $formatted), '%Y-%m-%d');
398 $endDate = CRM_Utils_Date
::customFormat(CRM_Utils_Array
::value('end_date', $formatted), '%Y-%m-%d');
399 $joinDate = CRM_Utils_Date
::customFormat(CRM_Utils_Array
::value('join_date', $formatted), '%Y-%m-%d');
401 if (!$this->isContactIDColumnPresent()) {
402 $error = $this->checkContactDuplicate($formatValues);
404 if (CRM_Core_Error
::isAPIError($error, CRM_Core_ERROR
::DUPLICATE_CONTACT
)) {
405 $matchedIDs = explode(',', $error['error_message']['params'][0]);
406 if (count($matchedIDs) > 1) {
407 array_unshift($values, 'Multiple matching contact records detected for this row. The membership was not imported');
408 return CRM_Import_Parser
::ERROR
;
411 $cid = $matchedIDs[0];
412 $formatted['contact_id'] = $cid;
415 $calcDates = CRM_Member_BAO_MembershipType
::getDatesForMembershipType($formatted['membership_type_id'],
420 self
::formattedDates($calcDates, $formatted);
422 //fix for CRM-3570, exclude the statuses those having is_admin = 1
423 //now user can import is_admin if is override is true.
424 $excludeIsAdmin = FALSE;
425 if (empty($formatted['member_is_override'])) {
426 $formatted['exclude_is_admin'] = $excludeIsAdmin = TRUE;
428 $calcStatus = CRM_Member_BAO_MembershipStatus
::getMembershipStatusByDate($startDate,
433 $formatted['membership_type_id'],
437 if (empty($formatted['status_id'])) {
438 $formatted['status_id'] = $calcStatus['id'];
440 elseif (empty($formatted['member_is_override'])) {
441 if (empty($calcStatus)) {
442 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.');
443 return CRM_Import_Parser
::ERROR
;
445 elseif ($formatted['status_id'] != $calcStatus['id']) {
446 //Status Hold" is either NOT mapped or is FALSE
447 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.');
448 return CRM_Import_Parser
::ERROR
;
452 $newMembership = civicrm_api3('membership', 'create', $formatted);
454 $this->_newMemberships
[] = $newMembership['id'];
455 return CRM_Import_Parser
::VALID
;
459 // Using new Dedupe rule.
461 'contact_type' => $this->_contactType
,
462 'used' => 'Unsupervised',
464 $fieldsArray = CRM_Dedupe_BAO_Rule
::dedupeRuleFields($ruleParams);
467 foreach ($fieldsArray as $value) {
468 if (array_key_exists(trim($value), $params)) {
469 $paramValue = $params[trim($value)];
470 if (is_array($paramValue)) {
471 $disp .= $params[trim($value)][0][trim($value)] . " ";
474 $disp .= $params[trim($value)] . " ";
479 if (!empty($params['external_identifier'])) {
481 $disp .= "AND {$params['external_identifier']}";
484 $disp = $params['external_identifier'];
488 array_unshift($values, 'No matching Contact found for (' . $disp . ')');
489 return CRM_Import_Parser
::ERROR
;
493 if (!empty($formatValues['external_identifier'])) {
494 $checkCid = new CRM_Contact_DAO_Contact();
495 $checkCid->external_identifier
= $formatValues['external_identifier'];
496 $checkCid->find(TRUE);
497 if ($checkCid->id
!= $formatted['contact_id']) {
498 array_unshift($values, 'Mismatch of External ID:' . $formatValues['external_identifier'] . ' and Contact Id:' . $formatted['contact_id']);
499 return CRM_Import_Parser
::ERROR
;
504 $calcDates = CRM_Member_BAO_MembershipType
::getDatesForMembershipType($formatted['membership_type_id'],
509 self
::formattedDates($calcDates, $formatted);
510 //end of date calculation part
512 //fix for CRM-3570, exclude the statuses those having is_admin = 1
513 //now user can import is_admin if is override is true.
514 $excludeIsAdmin = FALSE;
515 if (empty($formatted['member_is_override'])) {
516 $formatted['exclude_is_admin'] = $excludeIsAdmin = TRUE;
518 $calcStatus = CRM_Member_BAO_MembershipStatus
::getMembershipStatusByDate($startDate,
523 $formatted['membership_type_id'],
526 if (empty($formatted['status_id'])) {
527 $formatted['status_id'] = $calcStatus['id'] ??
NULL;
529 elseif (empty($formatted['member_is_override'])) {
530 if (empty($calcStatus)) {
531 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.');
532 return CRM_Import_Parser
::ERROR
;
534 elseif ($formatted['status_id'] != $calcStatus['id']) {
535 //Status Hold" is either NOT mapped or is FALSE
536 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.');
537 return CRM_Import_Parser
::ERROR
;
541 $newMembership = civicrm_api3('membership', 'create', $formatted);
543 $this->_newMemberships
[] = $newMembership['id'];
544 return CRM_Import_Parser
::VALID
;
547 catch (Exception
$e) {
548 array_unshift($values, $e->getMessage());
549 return CRM_Import_Parser
::ERROR
;
554 * Get the array of successfully imported membership id's
558 public function &getImportedMemberships() {
559 return $this->_newMemberships
;
563 * The initializer code, called before the processing
567 public function fini() {
571 * to calculate join, start and end dates
573 * @param array $calcDates
574 * Array of dates returned by getDatesForMembershipType().
579 public function formattedDates($calcDates, &$formatted) {
586 foreach ($dates as $d) {
587 if (isset($formatted[$d]) &&
588 !CRM_Utils_System
::isNull($formatted[$d])
590 $formatted[$d] = CRM_Utils_Date
::isoToMysql($formatted[$d]);
592 elseif (isset($calcDates[$d])) {
593 $formatted[$d] = CRM_Utils_Date
::isoToMysql($calcDates[$d]);
599 * @deprecated - this function formats params according to v2 standards but
600 * need to be sure about the impact of not calling it so retaining on the import class
601 * take the input parameter list as specified in the data model and
602 * convert it into the same format that we use in QF and BAO object
604 * @param array $params
605 * Associative array of property name/value.
606 * pairs to insert in new contact.
607 * @param array $values
608 * The reformatted properties that we can use internally.
610 * @param array|bool $create Is the formatted Values array going to
611 * be used for CRM_Member_BAO_Membership:create()
614 * @return array|error
616 public function membership_format_params($params, &$values, $create = FALSE) {
617 require_once 'api/v3/utils.php';
618 $fields = CRM_Member_DAO_Membership
::fields();
619 _civicrm_api3_store_values($fields, $params, $values);
621 $customFields = CRM_Core_BAO_CustomField
::getFields('Membership');
623 foreach ($params as $key => $value) {
624 // ignore empty values or empty arrays etc
625 if (CRM_Utils_System
::isNull($value)) {
629 //Handling Custom Data
630 if ($customFieldID = CRM_Core_BAO_CustomField
::getKeyID($key)) {
631 $values[$key] = $value;
632 $type = $customFields[$customFieldID]['html_type'];
633 if (CRM_Core_BAO_CustomField
::isSerialized($customFields[$customFieldID])) {
634 $values[$key] = self
::unserializeCustomValue($customFieldID, $value, $type);
639 case 'membership_contact_id':
640 if (!CRM_Utils_Rule
::integer($value)) {
641 throw new Exception("contact_id not valid: $value");
643 $dao = new CRM_Core_DAO();
645 $svq = $dao->singleValueQuery("SELECT id FROM civicrm_contact WHERE id = $value",
649 throw new Exception("Invalid Contact ID: There is no contact record with contact_id = $value.");
651 $values['contact_id'] = $values['membership_contact_id'];
652 unset($values['membership_contact_id']);
655 case 'membership_type_id':
656 if (!array_key_exists($value, CRM_Member_PseudoConstant
::membershipType())) {
657 throw new Exception('Invalid Membership Type Id');
659 $values[$key] = $value;
662 case 'membership_type':
663 $membershipTypeId = CRM_Utils_Array
::key(ucfirst($value),
664 CRM_Member_PseudoConstant
::membershipType()
666 if ($membershipTypeId) {
667 if (!empty($values['membership_type_id']) &&
668 $membershipTypeId != $values['membership_type_id']
670 throw new Exception('Mismatched membership Type and Membership Type Id');
674 throw new Exception('Invalid Membership Type');
676 $values['membership_type_id'] = $membershipTypeId;
684 _civicrm_api3_custom_format_params($params, $values, 'Membership');
687 // CRM_Member_BAO_Membership::create() handles membership_start_date, membership_join_date,
688 // membership_end_date and membership_source. So, if $values contains
689 // membership_start_date, membership_end_date, membership_join_date or membership_source,
690 // convert it to start_date, end_date, join_date or source
692 'membership_join_date' => 'join_date',
693 'membership_start_date' => 'start_date',
694 'membership_end_date' => 'end_date',
695 'membership_source' => 'source',
698 foreach ($changes as $orgVal => $changeVal) {
699 if (isset($values[$orgVal])) {
700 $values[$changeVal] = $values[$orgVal];
701 unset($values[$orgVal]);
710 * Is the contact ID mapped.
714 protected function isContactIDColumnPresent(): bool {
715 return in_array('membership_contact_id', $this->_mapperKeys
, TRUE);