Fix Parser classes to use ClassScanner
[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 22
14b9e069 23 /**
24 * Array of metadata for all available fields.
25 *
26 * @var array
27 */
28 protected $fieldMetadata = [];
29
6a488035 30 /**
ceb10dc7 31 * Array of successfully imported membership id's
6a488035 32 *
971e129b 33 * @var array
6a488035
TO
34 */
35 protected $_newMemberships;
36
ca4caf13
EM
37 protected $_fileName;
38
39 /**
40 * Imported file size
41 * @var int
42 */
43 protected $_fileSize;
44
45 /**
46 * Separator being used
47 * @var string
48 */
49 protected $_separator;
50
51 /**
52 * Total number of lines in file
53 * @var int
54 */
55 protected $_lineCount;
56
3764aead
EM
57 /**
58 * Get information about the provided job.
59 * - name
60 * - id (generally the same as name)
61 * - label
62 *
63 * e.g. ['activity_import' => ['id' => 'activity_import', 'label' => ts('Activity Import'), 'name' => 'activity_import']]
64 *
65 * @return array
66 */
67 public static function getUserJobInfo(): array {
68 return [
69 'membership_import' => [
70 'id' => 'membership_import',
71 'name' => 'membership_import',
72 'label' => ts('Membership Import'),
73 ],
74 ];
75 }
76
ca4caf13
EM
77 /**
78 * @param string $name
79 * @param $title
80 * @param int $type
81 * @param string $headerPattern
82 * @param string $dataPattern
83 */
84 public function addField($name, $title, $type = CRM_Utils_Type::T_INT, $headerPattern = '//', $dataPattern = '//') {
85 if (empty($name)) {
86 $this->_fields['doNotImport'] = new CRM_Member_Import_Field($name, $title, $type, $headerPattern, $dataPattern);
87 }
88 else {
89
90 //$tempField = CRM_Contact_BAO_Contact::importableFields('Individual', null );
91 $tempField = CRM_Contact_BAO_Contact::importableFields('All', NULL);
92 if (!array_key_exists($name, $tempField)) {
93 $this->_fields[$name] = new CRM_Member_Import_Field($name, $title, $type, $headerPattern, $dataPattern);
94 }
95 else {
96 $this->_fields[$name] = new CRM_Contact_Import_Field($name, $title, $type, $headerPattern, $dataPattern,
97 CRM_Utils_Array::value('hasLocationType', $tempField[$name])
98 );
99 }
100 }
101 }
102
6a488035 103 /**
22f90136 104 * Validate the values.
6a488035 105 *
b2363ea8
TO
106 * @param array $values
107 * The array of values belonging to this line.
7cd963e4
EM
108 *
109 * @throws \CRM_Core_Exception
6a488035 110 */
79e1afb8 111 public function validateValues($values): void {
209cedd9 112 $params = $this->getMappedRow($values);
22f90136
EM
113 $errors = [];
114 foreach ($params as $key => $value) {
115 $errors = array_merge($this->getInvalidValues($value, $key), $errors);
116 }
1006edc9 117
209cedd9 118 if (empty($params['membership_type_id'])) {
22f90136 119 $errors[] = ts('Missing required fields');
79e1afb8 120 return;
6a488035 121 }
6a488035
TO
122
123 //To check whether start date or join date is provided
209cedd9 124 if (empty($params['start_date']) && empty($params['join_date'])) {
22f90136 125 $errors[] = 'Membership Start Date is required to create a memberships.';
6a488035 126 }
7cd963e4
EM
127 //fix for CRM-2219 Update Membership
128 if ($this->isUpdateExisting() && !empty($params['is_override']) && empty($params['status_id'])) {
129 $errors[] = 'Required parameter missing: Status';
130 }
22f90136
EM
131 if ($errors) {
132 throw new CRM_Core_Exception('Invalid value for field(s) : ' . implode(',', $errors));
6a488035 133 }
6a488035
TO
134 }
135
136 /**
fe482240 137 * Handle the values in import mode.
6a488035 138 *
b2363ea8
TO
139 * @param array $values
140 * The array of values belonging to this line.
6a488035 141 *
2d306c45
EM
142 * @return int|void|null
143 * the result of this processing - which is ignored
6a488035 144 */
2d306c45 145 public function import($values) {
22f90136 146 $rowNumber = (int) ($values[array_key_last($values)]);
92e4c2a5 147 try {
209cedd9 148 $params = $this->getMappedRow($values);
6a488035 149
4f7b71ab 150 //assign join date equal to start date if join date is not provided
209cedd9
EM
151 if (empty($params['join_date']) && !empty($params['start_date'])) {
152 $params['join_date'] = $params['start_date'];
4f7b71ab 153 }
6a488035 154
209cedd9 155 $formatted = $params;
4f7b71ab 156 // don't add to recent items, CRM-4399
157 $formatted['skipRecentView'] = TRUE;
6a488035 158
be2fb01f 159 $formatValues = [];
4f7b71ab 160 foreach ($params as $key => $field) {
19cea1b4 161 // ignore empty values or empty arrays etc
162 if (CRM_Utils_System::isNull($field)) {
4f7b71ab 163 continue;
164 }
6a488035 165
4f7b71ab 166 $formatValues[$key] = $field;
6a488035
TO
167 }
168
4f7b71ab 169 //format params to meet api v2 requirements.
170 //@todo find a way to test removing this formatting
7cd963e4 171 $this->membership_format_params($formatValues, $formatted, TRUE);
6a488035 172
7cd963e4 173 if (!$this->isUpdateExisting()) {
6a488035 174 $formatted['custom'] = CRM_Core_BAO_CustomField::postProcess($formatted,
4f7b71ab 175 NULL,
6a488035
TO
176 'Membership'
177 );
4f7b71ab 178 }
179 else {
6a488035 180
1a9d4317 181 if (!empty($formatValues['membership_id'])) {
353ffa53 182 $dao = new CRM_Member_BAO_Membership();
4f7b71ab 183 $dao->id = $formatValues['membership_id'];
be2fb01f 184 $dates = ['join_date', 'start_date', 'end_date'];
4f7b71ab 185 foreach ($dates as $v) {
82cc6775 186 if (empty($formatted[$v])) {
4f7b71ab 187 $formatted[$v] = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_Membership', $formatValues['membership_id'], $v);
188 }
189 }
190
191 $formatted['custom'] = CRM_Core_BAO_CustomField::postProcess($formatted,
4f7b71ab 192 $formatValues['membership_id'],
193 'Membership'
194 );
195 if ($dao->find(TRUE)) {
82cc6775
PN
196 if (empty($params['line_item']) && !empty($formatted['membership_type_id'])) {
197 CRM_Price_BAO_LineItem::getLineItemArray($formatted, NULL, 'membership', $formatted['membership_type_id']);
198 }
d75f2f47 199
f3a5127d 200 $newMembership = civicrm_api3('Membership', 'create', $formatted);
201 $this->_newMemberships[] = $newMembership['id'];
22f90136 202 $this->setImportStatus($rowNumber, 'IMPORTED', 'Required parameter missing: Status');
f3a5127d 203 return CRM_Import_Parser::VALID;
6a488035 204 }
2d306c45 205 throw new CRM_Core_Exception('Matching Membership record not found for Membership ID ' . $formatValues['membership_id'] . '. Row was skipped.', CRM_Import_Parser::ERROR);
6a488035 206 }
6a488035 207 }
6a488035 208
4f7b71ab 209 //Format dates
397ed960
EM
210 $startDate = $formatted['start_date'];
211 $endDate = $formatted['end_date'] ?? NULL;
212 $joinDate = $formatted['join_date'];
6a488035 213
397ed960 214 if (empty($formatValues['id']) && empty($formatValues['contact_id'])) {
56316747 215 $error = $this->checkContactDuplicate($formatValues);
6a488035 216
4f7b71ab 217 if (CRM_Core_Error::isAPIError($error, CRM_Core_ERROR::DUPLICATE_CONTACT)) {
218 $matchedIDs = explode(',', $error['error_message']['params'][0]);
219 if (count($matchedIDs) > 1) {
2d306c45 220 throw new CRM_Core_Exception('Multiple matching contact records detected for this row. The membership was not imported', CRM_Import_Parser::ERROR);
4f7b71ab 221 }
222 else {
223 $cid = $matchedIDs[0];
224 $formatted['contact_id'] = $cid;
225
226 //fix for CRM-1924
227 $calcDates = CRM_Member_BAO_MembershipType::getDatesForMembershipType($formatted['membership_type_id'],
228 $joinDate,
229 $startDate,
230 $endDate
231 );
ca43b565 232 $this->formattedDates($calcDates, $formatted);
4f7b71ab 233
234 //fix for CRM-3570, exclude the statuses those having is_admin = 1
235 //now user can import is_admin if is override is true.
236 $excludeIsAdmin = FALSE;
209cedd9 237 if (empty($formatted['is_override'])) {
4f7b71ab 238 $formatted['exclude_is_admin'] = $excludeIsAdmin = TRUE;
239 }
240 $calcStatus = CRM_Member_BAO_MembershipStatus::getMembershipStatusByDate($startDate,
241 $endDate,
242 $joinDate,
2cb64970 243 'now',
5f11bbcc
EM
244 $excludeIsAdmin,
245 $formatted['membership_type_id'],
246 $formatted
4f7b71ab 247 );
248
a7488080 249 if (empty($formatted['status_id'])) {
4f7b71ab 250 $formatted['status_id'] = $calcStatus['id'];
251 }
209cedd9 252 elseif (empty($formatted['is_override'])) {
4f7b71ab 253 if (empty($calcStatus)) {
2d306c45 254 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);
4f7b71ab 255 }
22f90136 256 if ($formatted['status_id'] != $calcStatus['id']) {
4f7b71ab 257 //Status Hold" is either NOT mapped or is FALSE
2d306c45 258 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);
4f7b71ab 259 }
260 }
261
262 $newMembership = civicrm_api3('membership', 'create', $formatted);
263
264 $this->_newMemberships[] = $newMembership['id'];
22f90136 265 $this->setImportStatus($rowNumber, 'IMPORTED', '');
4f7b71ab 266 return CRM_Import_Parser::VALID;
267 }
6a488035
TO
268 }
269 else {
4f7b71ab 270 // Using new Dedupe rule.
be2fb01f 271 $ruleParams = [
ca43b565 272 'contact_type' => $this->getContactType(),
353ffa53 273 'used' => 'Unsupervised',
be2fb01f 274 ];
61194d45 275 $fieldsArray = CRM_Dedupe_BAO_DedupeRule::dedupeRuleFields($ruleParams);
4f7b71ab 276 $disp = '';
277
278 foreach ($fieldsArray as $value) {
279 if (array_key_exists(trim($value), $params)) {
280 $paramValue = $params[trim($value)];
281 if (is_array($paramValue)) {
282 $disp .= $params[trim($value)][0][trim($value)] . " ";
283 }
284 else {
285 $disp .= $params[trim($value)] . " ";
286 }
287 }
6a488035 288 }
6a488035 289
a7488080 290 if (!empty($params['external_identifier'])) {
4f7b71ab 291 if ($disp) {
292 $disp .= "AND {$params['external_identifier']}";
6a488035 293 }
4f7b71ab 294 else {
295 $disp = $params['external_identifier'];
6a488035
TO
296 }
297 }
2d306c45 298 throw new CRM_Core_Exception('No matching Contact found for (' . $disp . ')', CRM_Import_Parser::ERROR);
6a488035
TO
299 }
300 }
301 else {
a7488080 302 if (!empty($formatValues['external_identifier'])) {
4f7b71ab 303 $checkCid = new CRM_Contact_DAO_Contact();
304 $checkCid->external_identifier = $formatValues['external_identifier'];
305 $checkCid->find(TRUE);
306 if ($checkCid->id != $formatted['contact_id']) {
2d306c45 307 throw new CRM_Core_Exception('Mismatch of External ID:' . $formatValues['external_identifier'] . ' and Contact Id:' . $formatted['contact_id'], CRM_Import_Parser::ERROR);
6a488035
TO
308 }
309 }
310
4f7b71ab 311 //to calculate dates
312 $calcDates = CRM_Member_BAO_MembershipType::getDatesForMembershipType($formatted['membership_type_id'],
313 $joinDate,
314 $startDate,
315 $endDate
316 );
ca43b565 317 $this->formattedDates($calcDates, $formatted);
4f7b71ab 318 //end of date calculation part
319
320 //fix for CRM-3570, exclude the statuses those having is_admin = 1
321 //now user can import is_admin if is override is true.
322 $excludeIsAdmin = FALSE;
209cedd9 323 if (empty($formatted['is_override'])) {
4f7b71ab 324 $formatted['exclude_is_admin'] = $excludeIsAdmin = TRUE;
325 }
326 $calcStatus = CRM_Member_BAO_MembershipStatus::getMembershipStatusByDate($startDate,
327 $endDate,
328 $joinDate,
2cb64970 329 'now',
5f11bbcc
EM
330 $excludeIsAdmin,
331 $formatted['membership_type_id'],
332 $formatted
4f7b71ab 333 );
a7488080 334 if (empty($formatted['status_id'])) {
9c1bc317 335 $formatted['status_id'] = $calcStatus['id'] ?? NULL;
4f7b71ab 336 }
209cedd9 337 elseif (empty($formatted['is_override'])) {
4f7b71ab 338 if (empty($calcStatus)) {
2d306c45 339 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);
6a488035 340 }
2d306c45 341 if ($formatted['status_id'] != $calcStatus['id']) {
4f7b71ab 342 //Status Hold" is either NOT mapped or is FALSE
2d306c45 343 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);
6a488035
TO
344 }
345 }
346
4f7b71ab 347 $newMembership = civicrm_api3('membership', 'create', $formatted);
ca43b565 348 $this->setImportStatus($rowNumber, 'IMPORTED', '', $newMembership['id']);
4f7b71ab 349 return CRM_Import_Parser::VALID;
6a488035 350 }
f719e41c 351 }
2d306c45
EM
352 catch (CRM_Core_Exception $e) {
353 $this->setImportStatus($rowNumber, 'ERROR', $e->getMessage());
354 return CRM_Import_Parser::ERROR;
355 }
356 catch (CiviCRM_API3_Exception $e) {
22f90136 357 $this->setImportStatus($rowNumber, 'ERROR', $e->getMessage());
f719e41c 358 return CRM_Import_Parser::ERROR;
359 }
6a488035
TO
360 }
361
6a488035
TO
362 /**
363 * to calculate join, start and end dates
364 *
b2363ea8
TO
365 * @param array $calcDates
366 * Array of dates returned by getDatesForMembershipType().
6a488035 367 *
2a6da8d7 368 * @param $formatted
6a488035 369 *
6a488035 370 */
00be9182 371 public function formattedDates($calcDates, &$formatted) {
be2fb01f 372 $dates = [
6a488035
TO
373 'join_date',
374 'start_date',
375 'end_date',
be2fb01f 376 ];
6a488035
TO
377
378 foreach ($dates as $d) {
379 if (isset($formatted[$d]) &&
380 !CRM_Utils_System::isNull($formatted[$d])
381 ) {
382 $formatted[$d] = CRM_Utils_Date::isoToMysql($formatted[$d]);
383 }
384 elseif (isset($calcDates[$d])) {
385 $formatted[$d] = CRM_Utils_Date::isoToMysql($calcDates[$d]);
386 }
387 }
388 }
77b97be7 389
3c15495c 390 /**
391 * @deprecated - this function formats params according to v2 standards but
392 * need to be sure about the impact of not calling it so retaining on the import class
393 * take the input parameter list as specified in the data model and
394 * convert it into the same format that we use in QF and BAO object
395 *
b2363ea8
TO
396 * @param array $params
397 * Associative array of property name/value.
3c15495c 398 * pairs to insert in new contact.
b2363ea8
TO
399 * @param array $values
400 * The reformatted properties that we can use internally.
3c15495c 401 *
77b97be7 402 * @param array|bool $create Is the formatted Values array going to
3c15495c 403 * be used for CRM_Member_BAO_Membership:create()
404 *
77b97be7 405 * @throws Exception
3c15495c 406 * @return array|error
3c15495c 407 */
00be9182 408 public function membership_format_params($params, &$values, $create = FALSE) {
3c15495c 409 require_once 'api/v3/utils.php';
410 $fields = CRM_Member_DAO_Membership::fields();
411 _civicrm_api3_store_values($fields, $params, $values);
412
3c15495c 413 foreach ($params as $key => $value) {
3c15495c 414
3c15495c 415 switch ($key) {
209cedd9 416 case 'contact_id':
3c15495c 417 if (!CRM_Utils_Rule::integer($value)) {
f719e41c 418 throw new Exception("contact_id not valid: $value");
3c15495c 419 }
353ffa53 420 $dao = new CRM_Core_DAO();
be2fb01f 421 $qParams = [];
353ffa53 422 $svq = $dao->singleValueQuery("SELECT id FROM civicrm_contact WHERE id = $value",
3c15495c 423 $qParams
424 );
425 if (!$svq) {
f719e41c 426 throw new Exception("Invalid Contact ID: There is no contact record with contact_id = $value.");
3c15495c 427 }
209cedd9 428 $values[$key] = $value;
3c15495c 429 break;
430
3c15495c 431 default:
432 break;
433 }
434 }
435
3c15495c 436 return NULL;
437 }
96025800 438
d8844db5
EM
439 /**
440 * Set field metadata.
441 */
442 protected function setFieldMetadata(): void {
443 if (empty($this->importableFieldsMetadata)) {
209cedd9 444 $metadata = CRM_Member_BAO_Membership::importableFields($this->getContactType(), FALSE);
d8844db5 445
209cedd9 446 foreach ($metadata as $name => $field) {
d8844db5
EM
447 // @todo - we don't really need to do all this.... fieldMetadata is just fine to use as is.
448 $field['type'] = CRM_Utils_Array::value('type', $field, CRM_Utils_Type::T_INT);
449 $field['dataPattern'] = CRM_Utils_Array::value('dataPattern', $field, '//');
450 $field['headerPattern'] = CRM_Utils_Array::value('headerPattern', $field, '//');
451 $this->addField($name, $field['title'], $field['type'], $field['headerPattern'], $field['dataPattern']);
452 }
453 // We are consolidating on `importableFieldsMetadata` - but both still used.
454 $this->importableFieldsMetadata = $this->fieldMetadata = $metadata;
455 }
456 }
457
22f90136
EM
458 /**
459 * Get the metadata field for which importable fields does not key the actual field name.
460 *
461 * @return string[]
462 */
463 protected function getOddlyMappedMetadataFields(): array {
464 $uniqueNames = ['membership_id', 'membership_contact_id'];
465 $fields = [];
466 foreach ($uniqueNames as $name) {
467 $fields[$this->importableFieldsMetadata[$name]['name']] = $name;
468 }
469 // Include the parent fields as they could be present if required for matching ...in theory.
470 return array_merge($fields, parent::getOddlyMappedMetadataFields());
471 }
472
6a488035 473}