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_Import_Parser
{
23 protected $_mapperKeys;
26 * Array of metadata for all available fields.
30 protected $fieldMetadata = [];
33 * Array of successfully imported membership id's
37 protected $_newMemberships;
48 * Separator being used
51 protected $_separator;
54 * Total number of lines in file
57 protected $_lineCount;
64 public function __construct($mapperKeys = []) {
65 parent
::__construct();
66 $this->_mapperKeys
= $mapperKeys;
70 * @param string $fileName
71 * @param string $separator
73 * @param bool $skipColumnHeader
75 * @param int $contactType
76 * @param int $onDuplicate
77 * @param int $statusID
86 $skipColumnHeader = FALSE,
87 $mode = self
::MODE_PREVIEW
,
88 $contactType = self
::CONTACT_INDIVIDUAL
,
89 $onDuplicate = self
::DUPLICATE_SKIP
,
92 $this->_contactType
= $this->getContactType();
95 $this->_lineCount
= 0;
96 $this->_invalidRowCount
= $this->_validCount
= 0;
97 $this->_totalCount
= 0;
100 $this->_warnings
= [];
102 $this->progressImport($statusID);
103 $startTimestamp = $currTimestamp = $prevTimestamp = CRM_Utils_Time
::time();
105 $dataSource = $this->getDataSourceObject();
106 $totalRowCount = $dataSource->getRowCount(['new']);
107 $dataSource->setStatuses(['new']);
108 while ($row = $dataSource->getRow()) {
109 $values = array_values($row);
110 if ($mode == self
::MODE_IMPORT
) {
111 $this->import($values);
112 if ($statusID && (($this->_lineCount %
50) == 0)) {
113 $prevTimestamp = $this->progressImport($statusID, FALSE, $startTimestamp, $prevTimestamp, $totalRowCount);
120 * Given a list of the importable field keys that the user has selected
121 * set the active fields array to this list
123 * @param array $fieldKeys mapped array of values
127 public function setActiveFields($fieldKeys) {
128 $this->_activeFieldCount
= count($fieldKeys);
129 foreach ($fieldKeys as $key) {
130 if (empty($this->_fields
[$key])) {
131 $this->_activeFields
[] = new CRM_Member_Import_Field('', ts('- do not import -'));
134 $this->_activeFields
[] = clone($this->_fields
[$key]);
140 * Format the field values for input to the api.
143 * (reference ) associative array of name/value pairs
145 public function getParams() {
146 $this->getSubmittedValue('mapper');
148 for ($i = 0; $i < $this->_activeFieldCount
; $i++
) {
149 if (isset($this->_activeFields
[$i]->_value
)
150 && !isset($params[$this->_activeFields
[$i]->_name
])
151 && !isset($this->_activeFields
[$i]->_related
)
154 $params[$this->_activeFields
[$i]->_name
] = $this->_activeFields
[$i]->_value
;
161 * @param string $name
164 * @param string $headerPattern
165 * @param string $dataPattern
167 public function addField($name, $title, $type = CRM_Utils_Type
::T_INT
, $headerPattern = '//', $dataPattern = '//') {
169 $this->_fields
['doNotImport'] = new CRM_Member_Import_Field($name, $title, $type, $headerPattern, $dataPattern);
173 //$tempField = CRM_Contact_BAO_Contact::importableFields('Individual', null );
174 $tempField = CRM_Contact_BAO_Contact
::importableFields('All', NULL);
175 if (!array_key_exists($name, $tempField)) {
176 $this->_fields
[$name] = new CRM_Member_Import_Field($name, $title, $type, $headerPattern, $dataPattern);
179 $this->_fields
[$name] = new CRM_Contact_Import_Field($name, $title, $type, $headerPattern, $dataPattern,
180 CRM_Utils_Array
::value('hasLocationType', $tempField[$name])
187 * Store parser values.
189 * @param CRM_Core_Session $store
195 public function set($store, $mode = self
::MODE_SUMMARY
) {
196 $store->set('lineCount', $this->_lineCount
);
197 $store->set('validRowCount', $this->_validCount
);
198 $store->set('invalidRowCount', $this->_invalidRowCount
);
200 if ($this->_invalidRowCount
) {
201 $store->set('errorsFileName', $this->_errorFileName
);
204 if ($mode == self
::MODE_IMPORT
) {
205 $store->set('duplicateRowCount', $this->_duplicateCount
);
206 if ($this->_duplicateCount
) {
207 $store->set('duplicatesFileName', $this->_duplicateFileName
);
213 * Export data to a CSV file.
215 * @param string $fileName
216 * @param array $header
221 public static function exportCSV($fileName, $header, $data) {
223 $fd = fopen($fileName, 'w');
225 foreach ($header as $key => $value) {
226 $header[$key] = "\"$value\"";
228 $config = CRM_Core_Config
::singleton();
229 $output[] = implode($config->fieldSeparator
, $header);
231 foreach ($data as $datum) {
232 foreach ($datum as $key => $value) {
233 if (is_array($value)) {
234 foreach ($value[0] as $k1 => $v1) {
235 if ($k1 == 'location_type_id') {
242 $datum[$key] = "\"$value\"";
245 $output[] = implode($config->fieldSeparator
, $datum);
247 fwrite($fd, implode("\n", $output));
252 * The initializer code, called before the processing
256 public function init() {
257 // Force re-load of user job.
258 unset($this->userJob
);
259 $this->setFieldMetadata();
261 $this->_newMemberships
= [];
263 $this->setActiveFields($this->_mapperKeys
);
267 * Validate the values.
269 * @param array $values
270 * The array of values belonging to this line.
272 public function validateValues($values): void
{
273 $params = $this->getMappedRow($values);
275 foreach ($params as $key => $value) {
276 $errors = array_merge($this->getInvalidValues($value, $key), $errors);
279 if (empty($params['membership_type_id'])) {
280 $errors[] = ts('Missing required fields');
284 //To check whether start date or join date is provided
285 if (empty($params['start_date']) && empty($params['join_date'])) {
286 $errors[] = 'Membership Start Date is required to create a memberships.';
289 throw new CRM_Core_Exception('Invalid value for field(s) : ' . implode(',', $errors));
294 * Handle the values in import mode.
296 * @param array $values
297 * The array of values belonging to this line.
299 * @return int|void|null
300 * the result of this processing - which is ignored
302 public function import($values) {
303 $onDuplicate = $this->getSubmittedValue('onDuplicate');
304 $rowNumber = (int) ($values[array_key_last($values)]);
306 $params = $this->getMappedRow($values);
308 //assign join date equal to start date if join date is not provided
309 if (empty($params['join_date']) && !empty($params['start_date'])) {
310 $params['join_date'] = $params['start_date'];
313 $formatted = $params;
314 // don't add to recent items, CRM-4399
315 $formatted['skipRecentView'] = TRUE;
318 foreach ($params as $key => $field) {
319 // ignore empty values or empty arrays etc
320 if (CRM_Utils_System
::isNull($field)) {
324 $formatValues[$key] = $field;
327 //format params to meet api v2 requirements.
328 //@todo find a way to test removing this formatting
329 $formatError = $this->membership_format_params($formatValues, $formatted, TRUE);
331 if ($onDuplicate != CRM_Import_Parser
::DUPLICATE_UPDATE
) {
332 $formatted['custom'] = CRM_Core_BAO_CustomField
::postProcess($formatted,
338 //fix for CRM-2219 Update Membership
339 // onDuplicate == CRM_Import_Parser::DUPLICATE_UPDATE
340 if (!empty($formatted['is_override']) && empty($formatted['status_id'])) {
341 throw new CRM_Core_Exception('Required parameter missing: Status', CRM_Import_Parser
::ERROR
);
344 if (!empty($formatValues['membership_id'])) {
345 $dao = new CRM_Member_BAO_Membership();
346 $dao->id
= $formatValues['membership_id'];
347 $dates = ['join_date', 'start_date', 'end_date'];
348 foreach ($dates as $v) {
349 if (empty($formatted[$v])) {
350 $formatted[$v] = CRM_Core_DAO
::getFieldValue('CRM_Member_DAO_Membership', $formatValues['membership_id'], $v);
354 $formatted['custom'] = CRM_Core_BAO_CustomField
::postProcess($formatted,
355 $formatValues['membership_id'],
358 if ($dao->find(TRUE)) {
359 if (empty($params['line_item']) && !empty($formatted['membership_type_id'])) {
360 CRM_Price_BAO_LineItem
::getLineItemArray($formatted, NULL, 'membership', $formatted['membership_type_id']);
363 $newMembership = civicrm_api3('Membership', 'create', $formatted);
364 $this->_newMemberships
[] = $newMembership['id'];
365 $this->setImportStatus($rowNumber, 'IMPORTED', 'Required parameter missing: Status');
366 return CRM_Import_Parser
::VALID
;
368 throw new CRM_Core_Exception('Matching Membership record not found for Membership ID ' . $formatValues['membership_id'] . '. Row was skipped.', CRM_Import_Parser
::ERROR
);
373 $startDate = $formatted['start_date'];
374 $endDate = $formatted['end_date'] ??
NULL;
375 $joinDate = $formatted['join_date'];
377 if (empty($formatValues['id']) && empty($formatValues['contact_id'])) {
378 $error = $this->checkContactDuplicate($formatValues);
380 if (CRM_Core_Error
::isAPIError($error, CRM_Core_ERROR
::DUPLICATE_CONTACT
)) {
381 $matchedIDs = explode(',', $error['error_message']['params'][0]);
382 if (count($matchedIDs) > 1) {
383 throw new CRM_Core_Exception('Multiple matching contact records detected for this row. The membership was not imported', CRM_Import_Parser
::ERROR
);
386 $cid = $matchedIDs[0];
387 $formatted['contact_id'] = $cid;
390 $calcDates = CRM_Member_BAO_MembershipType
::getDatesForMembershipType($formatted['membership_type_id'],
395 self
::formattedDates($calcDates, $formatted);
397 //fix for CRM-3570, exclude the statuses those having is_admin = 1
398 //now user can import is_admin if is override is true.
399 $excludeIsAdmin = FALSE;
400 if (empty($formatted['is_override'])) {
401 $formatted['exclude_is_admin'] = $excludeIsAdmin = TRUE;
403 $calcStatus = CRM_Member_BAO_MembershipStatus
::getMembershipStatusByDate($startDate,
408 $formatted['membership_type_id'],
412 if (empty($formatted['status_id'])) {
413 $formatted['status_id'] = $calcStatus['id'];
415 elseif (empty($formatted['is_override'])) {
416 if (empty($calcStatus)) {
417 throw new CRM_Core_Exception('Status in import row (' . $formatValues['status_id'] . ') does not match calculated status based on your configured Membership Status Rules. Record was not imported.', CRM_Import_Parser
::ERROR
);
419 if ($formatted['status_id'] != $calcStatus['id']) {
420 //Status Hold" is either NOT mapped or is FALSE
421 throw new CRM_Core_Exception('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.', CRM_Import_Parser
::ERROR
);
425 $newMembership = civicrm_api3('membership', 'create', $formatted);
427 $this->_newMemberships
[] = $newMembership['id'];
428 $this->setImportStatus($rowNumber, 'IMPORTED', '');
429 return CRM_Import_Parser
::VALID
;
433 // Using new Dedupe rule.
435 'contact_type' => $this->_contactType
,
436 'used' => 'Unsupervised',
438 $fieldsArray = CRM_Dedupe_BAO_DedupeRule
::dedupeRuleFields($ruleParams);
441 foreach ($fieldsArray as $value) {
442 if (array_key_exists(trim($value), $params)) {
443 $paramValue = $params[trim($value)];
444 if (is_array($paramValue)) {
445 $disp .= $params[trim($value)][0][trim($value)] . " ";
448 $disp .= $params[trim($value)] . " ";
453 if (!empty($params['external_identifier'])) {
455 $disp .= "AND {$params['external_identifier']}";
458 $disp = $params['external_identifier'];
461 throw new CRM_Core_Exception('No matching Contact found for (' . $disp . ')', CRM_Import_Parser
::ERROR
);
465 if (!empty($formatValues['external_identifier'])) {
466 $checkCid = new CRM_Contact_DAO_Contact();
467 $checkCid->external_identifier
= $formatValues['external_identifier'];
468 $checkCid->find(TRUE);
469 if ($checkCid->id
!= $formatted['contact_id']) {
470 throw new CRM_Core_Exception('Mismatch of External ID:' . $formatValues['external_identifier'] . ' and Contact Id:' . $formatted['contact_id'], CRM_Import_Parser
::ERROR
);
475 $calcDates = CRM_Member_BAO_MembershipType
::getDatesForMembershipType($formatted['membership_type_id'],
480 self
::formattedDates($calcDates, $formatted);
481 //end of date calculation part
483 //fix for CRM-3570, exclude the statuses those having is_admin = 1
484 //now user can import is_admin if is override is true.
485 $excludeIsAdmin = FALSE;
486 if (empty($formatted['is_override'])) {
487 $formatted['exclude_is_admin'] = $excludeIsAdmin = TRUE;
489 $calcStatus = CRM_Member_BAO_MembershipStatus
::getMembershipStatusByDate($startDate,
494 $formatted['membership_type_id'],
497 if (empty($formatted['status_id'])) {
498 $formatted['status_id'] = $calcStatus['id'] ??
NULL;
500 elseif (empty($formatted['is_override'])) {
501 if (empty($calcStatus)) {
502 throw new CRM_Core_Exception('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.', CRM_Import_Parser
::ERROR
);
504 if ($formatted['status_id'] != $calcStatus['id']) {
505 //Status Hold" is either NOT mapped or is FALSE
506 throw new CRM_Core_Exception($rowNumber, 'ERROR', '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.', CRM_Import_Parser
::ERROR
);
510 $newMembership = civicrm_api3('membership', 'create', $formatted);
512 $this->_newMemberships
[] = $newMembership['id'];
513 $this->setImportStatus($rowNumber, 'IMPORTED', '');
514 return CRM_Import_Parser
::VALID
;
517 catch (CRM_Core_Exception
$e) {
518 $this->setImportStatus($rowNumber, 'ERROR', $e->getMessage());
519 return CRM_Import_Parser
::ERROR
;
521 catch (CiviCRM_API3_Exception
$e) {
522 $this->setImportStatus($rowNumber, 'ERROR', $e->getMessage());
523 return CRM_Import_Parser
::ERROR
;
528 * Get the array of successfully imported membership id's
532 public function &getImportedMemberships() {
533 return $this->_newMemberships
;
537 * to calculate join, start and end dates
539 * @param array $calcDates
540 * Array of dates returned by getDatesForMembershipType().
545 public function formattedDates($calcDates, &$formatted) {
552 foreach ($dates as $d) {
553 if (isset($formatted[$d]) &&
554 !CRM_Utils_System
::isNull($formatted[$d])
556 $formatted[$d] = CRM_Utils_Date
::isoToMysql($formatted[$d]);
558 elseif (isset($calcDates[$d])) {
559 $formatted[$d] = CRM_Utils_Date
::isoToMysql($calcDates[$d]);
565 * @deprecated - this function formats params according to v2 standards but
566 * need to be sure about the impact of not calling it so retaining on the import class
567 * take the input parameter list as specified in the data model and
568 * convert it into the same format that we use in QF and BAO object
570 * @param array $params
571 * Associative array of property name/value.
572 * pairs to insert in new contact.
573 * @param array $values
574 * The reformatted properties that we can use internally.
576 * @param array|bool $create Is the formatted Values array going to
577 * be used for CRM_Member_BAO_Membership:create()
580 * @return array|error
582 public function membership_format_params($params, &$values, $create = FALSE) {
583 require_once 'api/v3/utils.php';
584 $fields = CRM_Member_DAO_Membership
::fields();
585 _civicrm_api3_store_values($fields, $params, $values);
587 $customFields = CRM_Core_BAO_CustomField
::getFields('Membership');
589 foreach ($params as $key => $value) {
593 if (!CRM_Utils_Rule
::integer($value)) {
594 throw new Exception("contact_id not valid: $value");
596 $dao = new CRM_Core_DAO();
598 $svq = $dao->singleValueQuery("SELECT id FROM civicrm_contact WHERE id = $value",
602 throw new Exception("Invalid Contact ID: There is no contact record with contact_id = $value.");
604 $values[$key] = $value;
616 * Set field metadata.
618 protected function setFieldMetadata(): void
{
619 if (empty($this->importableFieldsMetadata
)) {
620 $metadata = CRM_Member_BAO_Membership
::importableFields($this->getContactType(), FALSE);
622 foreach ($metadata as $name => $field) {
623 // @todo - we don't really need to do all this.... fieldMetadata is just fine to use as is.
624 $field['type'] = CRM_Utils_Array
::value('type', $field, CRM_Utils_Type
::T_INT
);
625 $field['dataPattern'] = CRM_Utils_Array
::value('dataPattern', $field, '//');
626 $field['headerPattern'] = CRM_Utils_Array
::value('headerPattern', $field, '//');
627 $this->addField($name, $field['title'], $field['type'], $field['headerPattern'], $field['dataPattern']);
629 // We are consolidating on `importableFieldsMetadata` - but both still used.
630 $this->importableFieldsMetadata
= $this->fieldMetadata
= $metadata;
635 * Get the metadata field for which importable fields does not key the actual field name.
639 protected function getOddlyMappedMetadataFields(): array {
640 $uniqueNames = ['membership_id', 'membership_contact_id'];
642 foreach ($uniqueNames as $name) {
643 $fields[$this->importableFieldsMetadata
[$name]['name']] = $name;
645 // Include the parent fields as they could be present if required for matching ...in theory.
646 return array_merge($fields, parent
::getOddlyMappedMetadataFields());