Commit | Line | Data |
---|---|---|
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 | 21 | class 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 | } |