Fix Parser classes to use ClassScanner
[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_Import_Parser {
22
23 /**
24 * Array of metadata for all available fields.
25 *
26 * @var array
27 */
28 protected $fieldMetadata = [];
29
30 /**
31 * Array of successfully imported membership id's
32 *
33 * @var array
34 */
35 protected $_newMemberships;
36
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
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
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
103 /**
104 * Validate the values.
105 *
106 * @param array $values
107 * The array of values belonging to this line.
108 *
109 * @throws \CRM_Core_Exception
110 */
111 public function validateValues($values): void {
112 $params = $this->getMappedRow($values);
113 $errors = [];
114 foreach ($params as $key => $value) {
115 $errors = array_merge($this->getInvalidValues($value, $key), $errors);
116 }
117
118 if (empty($params['membership_type_id'])) {
119 $errors[] = ts('Missing required fields');
120 return;
121 }
122
123 //To check whether start date or join date is provided
124 if (empty($params['start_date']) && empty($params['join_date'])) {
125 $errors[] = 'Membership Start Date is required to create a memberships.';
126 }
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 }
131 if ($errors) {
132 throw new CRM_Core_Exception('Invalid value for field(s) : ' . implode(',', $errors));
133 }
134 }
135
136 /**
137 * Handle the values in import mode.
138 *
139 * @param array $values
140 * The array of values belonging to this line.
141 *
142 * @return int|void|null
143 * the result of this processing - which is ignored
144 */
145 public function import($values) {
146 $rowNumber = (int) ($values[array_key_last($values)]);
147 try {
148 $params = $this->getMappedRow($values);
149
150 //assign join date equal to start date if join date is not provided
151 if (empty($params['join_date']) && !empty($params['start_date'])) {
152 $params['join_date'] = $params['start_date'];
153 }
154
155 $formatted = $params;
156 // don't add to recent items, CRM-4399
157 $formatted['skipRecentView'] = TRUE;
158
159 $formatValues = [];
160 foreach ($params as $key => $field) {
161 // ignore empty values or empty arrays etc
162 if (CRM_Utils_System::isNull($field)) {
163 continue;
164 }
165
166 $formatValues[$key] = $field;
167 }
168
169 //format params to meet api v2 requirements.
170 //@todo find a way to test removing this formatting
171 $this->membership_format_params($formatValues, $formatted, TRUE);
172
173 if (!$this->isUpdateExisting()) {
174 $formatted['custom'] = CRM_Core_BAO_CustomField::postProcess($formatted,
175 NULL,
176 'Membership'
177 );
178 }
179 else {
180
181 if (!empty($formatValues['membership_id'])) {
182 $dao = new CRM_Member_BAO_Membership();
183 $dao->id = $formatValues['membership_id'];
184 $dates = ['join_date', 'start_date', 'end_date'];
185 foreach ($dates as $v) {
186 if (empty($formatted[$v])) {
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,
192 $formatValues['membership_id'],
193 'Membership'
194 );
195 if ($dao->find(TRUE)) {
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 }
199
200 $newMembership = civicrm_api3('Membership', 'create', $formatted);
201 $this->_newMemberships[] = $newMembership['id'];
202 $this->setImportStatus($rowNumber, 'IMPORTED', 'Required parameter missing: Status');
203 return CRM_Import_Parser::VALID;
204 }
205 throw new CRM_Core_Exception('Matching Membership record not found for Membership ID ' . $formatValues['membership_id'] . '. Row was skipped.', CRM_Import_Parser::ERROR);
206 }
207 }
208
209 //Format dates
210 $startDate = $formatted['start_date'];
211 $endDate = $formatted['end_date'] ?? NULL;
212 $joinDate = $formatted['join_date'];
213
214 if (empty($formatValues['id']) && empty($formatValues['contact_id'])) {
215 $error = $this->checkContactDuplicate($formatValues);
216
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) {
220 throw new CRM_Core_Exception('Multiple matching contact records detected for this row. The membership was not imported', CRM_Import_Parser::ERROR);
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 );
232 $this->formattedDates($calcDates, $formatted);
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;
237 if (empty($formatted['is_override'])) {
238 $formatted['exclude_is_admin'] = $excludeIsAdmin = TRUE;
239 }
240 $calcStatus = CRM_Member_BAO_MembershipStatus::getMembershipStatusByDate($startDate,
241 $endDate,
242 $joinDate,
243 'now',
244 $excludeIsAdmin,
245 $formatted['membership_type_id'],
246 $formatted
247 );
248
249 if (empty($formatted['status_id'])) {
250 $formatted['status_id'] = $calcStatus['id'];
251 }
252 elseif (empty($formatted['is_override'])) {
253 if (empty($calcStatus)) {
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);
255 }
256 if ($formatted['status_id'] != $calcStatus['id']) {
257 //Status Hold" is either NOT mapped or is FALSE
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);
259 }
260 }
261
262 $newMembership = civicrm_api3('membership', 'create', $formatted);
263
264 $this->_newMemberships[] = $newMembership['id'];
265 $this->setImportStatus($rowNumber, 'IMPORTED', '');
266 return CRM_Import_Parser::VALID;
267 }
268 }
269 else {
270 // Using new Dedupe rule.
271 $ruleParams = [
272 'contact_type' => $this->getContactType(),
273 'used' => 'Unsupervised',
274 ];
275 $fieldsArray = CRM_Dedupe_BAO_DedupeRule::dedupeRuleFields($ruleParams);
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 }
288 }
289
290 if (!empty($params['external_identifier'])) {
291 if ($disp) {
292 $disp .= "AND {$params['external_identifier']}";
293 }
294 else {
295 $disp = $params['external_identifier'];
296 }
297 }
298 throw new CRM_Core_Exception('No matching Contact found for (' . $disp . ')', CRM_Import_Parser::ERROR);
299 }
300 }
301 else {
302 if (!empty($formatValues['external_identifier'])) {
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']) {
307 throw new CRM_Core_Exception('Mismatch of External ID:' . $formatValues['external_identifier'] . ' and Contact Id:' . $formatted['contact_id'], CRM_Import_Parser::ERROR);
308 }
309 }
310
311 //to calculate dates
312 $calcDates = CRM_Member_BAO_MembershipType::getDatesForMembershipType($formatted['membership_type_id'],
313 $joinDate,
314 $startDate,
315 $endDate
316 );
317 $this->formattedDates($calcDates, $formatted);
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;
323 if (empty($formatted['is_override'])) {
324 $formatted['exclude_is_admin'] = $excludeIsAdmin = TRUE;
325 }
326 $calcStatus = CRM_Member_BAO_MembershipStatus::getMembershipStatusByDate($startDate,
327 $endDate,
328 $joinDate,
329 'now',
330 $excludeIsAdmin,
331 $formatted['membership_type_id'],
332 $formatted
333 );
334 if (empty($formatted['status_id'])) {
335 $formatted['status_id'] = $calcStatus['id'] ?? NULL;
336 }
337 elseif (empty($formatted['is_override'])) {
338 if (empty($calcStatus)) {
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);
340 }
341 if ($formatted['status_id'] != $calcStatus['id']) {
342 //Status Hold" is either NOT mapped or is FALSE
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);
344 }
345 }
346
347 $newMembership = civicrm_api3('membership', 'create', $formatted);
348 $this->setImportStatus($rowNumber, 'IMPORTED', '', $newMembership['id']);
349 return CRM_Import_Parser::VALID;
350 }
351 }
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) {
357 $this->setImportStatus($rowNumber, 'ERROR', $e->getMessage());
358 return CRM_Import_Parser::ERROR;
359 }
360 }
361
362 /**
363 * to calculate join, start and end dates
364 *
365 * @param array $calcDates
366 * Array of dates returned by getDatesForMembershipType().
367 *
368 * @param $formatted
369 *
370 */
371 public function formattedDates($calcDates, &$formatted) {
372 $dates = [
373 'join_date',
374 'start_date',
375 'end_date',
376 ];
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 }
389
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 *
396 * @param array $params
397 * Associative array of property name/value.
398 * pairs to insert in new contact.
399 * @param array $values
400 * The reformatted properties that we can use internally.
401 *
402 * @param array|bool $create Is the formatted Values array going to
403 * be used for CRM_Member_BAO_Membership:create()
404 *
405 * @throws Exception
406 * @return array|error
407 */
408 public function membership_format_params($params, &$values, $create = FALSE) {
409 require_once 'api/v3/utils.php';
410 $fields = CRM_Member_DAO_Membership::fields();
411 _civicrm_api3_store_values($fields, $params, $values);
412
413 foreach ($params as $key => $value) {
414
415 switch ($key) {
416 case 'contact_id':
417 if (!CRM_Utils_Rule::integer($value)) {
418 throw new Exception("contact_id not valid: $value");
419 }
420 $dao = new CRM_Core_DAO();
421 $qParams = [];
422 $svq = $dao->singleValueQuery("SELECT id FROM civicrm_contact WHERE id = $value",
423 $qParams
424 );
425 if (!$svq) {
426 throw new Exception("Invalid Contact ID: There is no contact record with contact_id = $value.");
427 }
428 $values[$key] = $value;
429 break;
430
431 default:
432 break;
433 }
434 }
435
436 return NULL;
437 }
438
439 /**
440 * Set field metadata.
441 */
442 protected function setFieldMetadata(): void {
443 if (empty($this->importableFieldsMetadata)) {
444 $metadata = CRM_Member_BAO_Membership::importableFields($this->getContactType(), FALSE);
445
446 foreach ($metadata as $name => $field) {
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
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
473 }