dev/core#1806 Fix issue with importing radio custom data field using the option label
[civicrm-core.git] / CRM / Contribute / Import / Parser / Contribution.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 contribution csv files.
20 */
21 class CRM_Contribute_Import_Parser_Contribution extends CRM_Contribute_Import_Parser {
22
23 protected $_mapperKeys;
24
25 private $_contactIdIndex;
26 private $_totalAmountIndex;
27 private $_contributionTypeIndex;
28
29 protected $_mapperSoftCredit;
30 //protected $_mapperPhoneType;
31
32 /**
33 * Array of successfully imported contribution id's
34 *
35 * @var array
36 */
37 protected $_newContributions;
38
39 /**
40 * Class constructor.
41 *
42 * @param $mapperKeys
43 * @param array $mapperSoftCredit
44 * @param null $mapperPhoneType
45 * @param array $mapperSoftCreditType
46 */
47 public function __construct(&$mapperKeys, $mapperSoftCredit = [], $mapperPhoneType = NULL, $mapperSoftCreditType = []) {
48 parent::__construct();
49 $this->_mapperKeys = &$mapperKeys;
50 $this->_mapperSoftCredit = &$mapperSoftCredit;
51 $this->_mapperSoftCreditType = &$mapperSoftCreditType;
52 }
53
54 /**
55 * The initializer code, called before the processing
56 */
57 public function init() {
58 $fields = CRM_Contribute_BAO_Contribution::importableFields($this->_contactType, FALSE);
59
60 $fields = array_merge($fields,
61 [
62 'soft_credit' => [
63 'title' => ts('Soft Credit'),
64 'softCredit' => TRUE,
65 'headerPattern' => '/Soft Credit/i',
66 ],
67 ]
68 );
69
70 // add pledge fields only if its is enabled
71 if (CRM_Core_Permission::access('CiviPledge')) {
72 $pledgeFields = [
73 'pledge_payment' => [
74 'title' => ts('Pledge Payment'),
75 'headerPattern' => '/Pledge Payment/i',
76 ],
77 'pledge_id' => [
78 'title' => ts('Pledge ID'),
79 'headerPattern' => '/Pledge ID/i',
80 ],
81 ];
82
83 $fields = array_merge($fields, $pledgeFields);
84 }
85 foreach ($fields as $name => $field) {
86 $field['type'] = CRM_Utils_Array::value('type', $field, CRM_Utils_Type::T_INT);
87 $field['dataPattern'] = CRM_Utils_Array::value('dataPattern', $field, '//');
88 $field['headerPattern'] = CRM_Utils_Array::value('headerPattern', $field, '//');
89 $this->addField($name, $field['title'], $field['type'], $field['headerPattern'], $field['dataPattern']);
90 }
91
92 $this->_newContributions = [];
93
94 $this->setActiveFields($this->_mapperKeys);
95 $this->setActiveFieldSoftCredit($this->_mapperSoftCredit);
96 $this->setActiveFieldSoftCreditType($this->_mapperSoftCreditType);
97
98 // FIXME: we should do this in one place together with Form/MapField.php
99 $this->_contactIdIndex = -1;
100 $this->_totalAmountIndex = -1;
101 $this->_contributionTypeIndex = -1;
102
103 $index = 0;
104 foreach ($this->_mapperKeys as $key) {
105 switch ($key) {
106 case 'contribution_contact_id':
107 $this->_contactIdIndex = $index;
108 break;
109
110 case 'total_amount':
111 $this->_totalAmountIndex = $index;
112 break;
113
114 case 'financial_type':
115 $this->_contributionTypeIndex = $index;
116 break;
117 }
118 $index++;
119 }
120 }
121
122 /**
123 * Handle the values in mapField mode.
124 *
125 * @param array $values
126 * The array of values belonging to this line.
127 *
128 * @return bool
129 */
130 public function mapField(&$values) {
131 return CRM_Import_Parser::VALID;
132 }
133
134 /**
135 * Handle the values in preview mode.
136 *
137 * @param array $values
138 * The array of values belonging to this line.
139 *
140 * @return bool
141 * the result of this processing
142 */
143 public function preview(&$values) {
144 return $this->summary($values);
145 }
146
147 /**
148 * Handle the values in summary mode.
149 *
150 * @param array $values
151 * The array of values belonging to this line.
152 *
153 * @return bool
154 * the result of this processing
155 */
156 public function summary(&$values) {
157 $erroneousField = NULL;
158 $response = $this->setActiveFieldValues($values, $erroneousField);
159
160 $params = &$this->getActiveFieldParams();
161 $errorMessage = NULL;
162
163 //for date-Formats
164 $errorMessage = implode('; ', $this->formatDateFields($params));
165 //date-Format part ends
166
167 $params['contact_type'] = 'Contribution';
168
169 //checking error in custom data
170 CRM_Contact_Import_Parser_Contact::isErrorInCustomData($params, $errorMessage);
171
172 if ($errorMessage) {
173 $tempMsg = "Invalid value for field(s) : $errorMessage";
174 array_unshift($values, $tempMsg);
175 $errorMessage = NULL;
176 return CRM_Import_Parser::ERROR;
177 }
178
179 return CRM_Import_Parser::VALID;
180 }
181
182 /**
183 * Handle the values in import mode.
184 *
185 * @param int $onDuplicate
186 * The code for what action to take on duplicates.
187 * @param array $values
188 * The array of values belonging to this line.
189 *
190 * @return bool
191 * the result of this processing
192 */
193 public function import($onDuplicate, &$values) {
194 // first make sure this is a valid line
195 $response = $this->summary($values);
196 if ($response != CRM_Import_Parser::VALID) {
197 return $response;
198 }
199
200 $params = &$this->getActiveFieldParams();
201 $formatted = ['version' => 3, 'skipRecentView' => TRUE, 'skipCleanMoney' => FALSE];
202
203 //CRM-10994
204 if (isset($params['total_amount']) && $params['total_amount'] == 0) {
205 $params['total_amount'] = '0.00';
206 }
207 $this->formatInput($params, $formatted);
208
209 static $indieFields = NULL;
210 if ($indieFields == NULL) {
211 $tempIndieFields = CRM_Contribute_DAO_Contribution::import();
212 $indieFields = $tempIndieFields;
213 }
214
215 $paramValues = [];
216 foreach ($params as $key => $field) {
217 if ($field == NULL || $field === '') {
218 continue;
219 }
220 $paramValues[$key] = $field;
221 }
222
223 //import contribution record according to select contact type
224 if ($onDuplicate == CRM_Import_Parser::DUPLICATE_SKIP &&
225 (!empty($paramValues['contribution_contact_id']) || !empty($paramValues['external_identifier']))
226 ) {
227 $paramValues['contact_type'] = $this->_contactType;
228 }
229 elseif ($onDuplicate == CRM_Import_Parser::DUPLICATE_UPDATE &&
230 (!empty($paramValues['contribution_id']) || !empty($values['trxn_id']) || !empty($paramValues['invoice_id']))
231 ) {
232 $paramValues['contact_type'] = $this->_contactType;
233 }
234 elseif (!empty($params['soft_credit'])) {
235 $paramValues['contact_type'] = $this->_contactType;
236 }
237 elseif (!empty($paramValues['pledge_payment'])) {
238 $paramValues['contact_type'] = $this->_contactType;
239 }
240
241 //need to pass $onDuplicate to check import mode.
242 if (!empty($paramValues['pledge_payment'])) {
243 $paramValues['onDuplicate'] = $onDuplicate;
244 }
245 $formatError = $this->deprecatedFormatParams($paramValues, $formatted, TRUE, $onDuplicate);
246
247 if ($formatError) {
248 array_unshift($values, $formatError['error_message']);
249 if (CRM_Utils_Array::value('error_data', $formatError) == 'soft_credit') {
250 return CRM_Contribute_Import_Parser::SOFT_CREDIT_ERROR;
251 }
252 elseif (CRM_Utils_Array::value('error_data', $formatError) == 'pledge_payment') {
253 return CRM_Contribute_Import_Parser::PLEDGE_PAYMENT_ERROR;
254 }
255 return CRM_Import_Parser::ERROR;
256 }
257
258 if ($onDuplicate != CRM_Import_Parser::DUPLICATE_UPDATE) {
259 $formatted['custom'] = CRM_Core_BAO_CustomField::postProcess($formatted,
260 NULL,
261 'Contribution'
262 );
263 }
264 else {
265 //fix for CRM-2219 - Update Contribution
266 // onDuplicate == CRM_Import_Parser::DUPLICATE_UPDATE
267 if (!empty($paramValues['invoice_id']) || !empty($paramValues['trxn_id']) || !empty($paramValues['contribution_id'])) {
268 $dupeIds = [
269 'id' => $paramValues['contribution_id'] ?? NULL,
270 'trxn_id' => $paramValues['trxn_id'] ?? NULL,
271 'invoice_id' => $paramValues['invoice_id'] ?? NULL,
272 ];
273
274 $ids['contribution'] = CRM_Contribute_BAO_Contribution::checkDuplicateIds($dupeIds);
275
276 if ($ids['contribution']) {
277 $formatted['id'] = $ids['contribution'];
278 $formatted['custom'] = CRM_Core_BAO_CustomField::postProcess($formatted,
279 $formatted['id'],
280 'Contribution'
281 );
282 //process note
283 if (!empty($paramValues['note'])) {
284 $noteID = [];
285 $contactID = CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_Contribution', $ids['contribution'], 'contact_id');
286 $daoNote = new CRM_Core_BAO_Note();
287 $daoNote->entity_table = 'civicrm_contribution';
288 $daoNote->entity_id = $ids['contribution'];
289 if ($daoNote->find(TRUE)) {
290 $noteID['id'] = $daoNote->id;
291 }
292
293 $noteParams = [
294 'entity_table' => 'civicrm_contribution',
295 'note' => $paramValues['note'],
296 'entity_id' => $ids['contribution'],
297 'contact_id' => $contactID,
298 ];
299 CRM_Core_BAO_Note::add($noteParams, $noteID);
300 unset($formatted['note']);
301 }
302
303 //need to check existing soft credit contribution, CRM-3968
304 if (!empty($formatted['soft_credit'])) {
305 $dupeSoftCredit = [
306 'contact_id' => $formatted['soft_credit'],
307 'contribution_id' => $ids['contribution'],
308 ];
309
310 //Delete all existing soft Contribution from contribution_soft table for pcp_id is_null
311 $existingSoftCredit = CRM_Contribute_BAO_ContributionSoft::getSoftContribution($dupeSoftCredit['contribution_id']);
312 if (isset($existingSoftCredit['soft_credit']) && !empty($existingSoftCredit['soft_credit'])) {
313 foreach ($existingSoftCredit['soft_credit'] as $key => $existingSoftCreditValues) {
314 if (!empty($existingSoftCreditValues['soft_credit_id'])) {
315 civicrm_api3('ContributionSoft', 'delete', [
316 'id' => $existingSoftCreditValues['soft_credit_id'],
317 'pcp_id' => NULL,
318 ]);
319 }
320 }
321 }
322 }
323
324 $formatted['id'] = $ids['contribution'];
325 $newContribution = CRM_Contribute_BAO_Contribution::create($formatted);
326 $this->_newContributions[] = $newContribution->id;
327
328 //return soft valid since we need to show how soft credits were added
329 if (!empty($formatted['soft_credit'])) {
330 return CRM_Contribute_Import_Parser::SOFT_CREDIT;
331 }
332
333 // process pledge payment assoc w/ the contribution
334 return self::processPledgePayments($formatted);
335
336 return CRM_Import_Parser::VALID;
337 }
338 else {
339 $labels = [
340 'id' => 'Contribution ID',
341 'trxn_id' => 'Transaction ID',
342 'invoice_id' => 'Invoice ID',
343 ];
344 foreach ($dupeIds as $k => $v) {
345 if ($v) {
346 $errorMsg[] = "$labels[$k] $v";
347 }
348 }
349 $errorMsg = implode(' AND ', $errorMsg);
350 array_unshift($values, 'Matching Contribution record not found for ' . $errorMsg . '. Row was skipped.');
351 return CRM_Import_Parser::ERROR;
352 }
353 }
354 }
355
356 if ($this->_contactIdIndex < 0) {
357 // set the contact type if its not set
358 if (!isset($paramValues['contact_type'])) {
359 $paramValues['contact_type'] = $this->_contactType;
360 }
361
362 $error = $this->checkContactDuplicate($paramValues);
363
364 if (CRM_Core_Error::isAPIError($error, CRM_Core_ERROR::DUPLICATE_CONTACT)) {
365 $matchedIDs = explode(',', $error['error_message']['params'][0]);
366 if (count($matchedIDs) > 1) {
367 array_unshift($values, 'Multiple matching contact records detected for this row. The contribution was not imported');
368 return CRM_Import_Parser::ERROR;
369 }
370 else {
371 $cid = $matchedIDs[0];
372 $formatted['contact_id'] = $cid;
373
374 $newContribution = civicrm_api('contribution', 'create', $formatted);
375 if (civicrm_error($newContribution)) {
376 if (is_array($newContribution['error_message'])) {
377 array_unshift($values, $newContribution['error_message']['message']);
378 if ($newContribution['error_message']['params'][0]) {
379 return CRM_Import_Parser::DUPLICATE;
380 }
381 }
382 else {
383 array_unshift($values, $newContribution['error_message']);
384 return CRM_Import_Parser::ERROR;
385 }
386 }
387
388 $this->_newContributions[] = $newContribution['id'];
389 $formatted['contribution_id'] = $newContribution['id'];
390
391 //return soft valid since we need to show how soft credits were added
392 if (!empty($formatted['soft_credit'])) {
393 return CRM_Contribute_Import_Parser::SOFT_CREDIT;
394 }
395
396 // process pledge payment assoc w/ the contribution
397 return self::processPledgePayments($formatted);
398
399 return CRM_Import_Parser::VALID;
400 }
401 }
402 else {
403 // Using new Dedupe rule.
404 $ruleParams = [
405 'contact_type' => $this->_contactType,
406 'used' => 'Unsupervised',
407 ];
408 $fieldsArray = CRM_Dedupe_BAO_Rule::dedupeRuleFields($ruleParams);
409 $disp = NULL;
410 foreach ($fieldsArray as $value) {
411 if (array_key_exists(trim($value), $params)) {
412 $paramValue = $params[trim($value)];
413 if (is_array($paramValue)) {
414 $disp .= $params[trim($value)][0][trim($value)] . " ";
415 }
416 else {
417 $disp .= $params[trim($value)] . " ";
418 }
419 }
420 }
421
422 if (!empty($params['external_identifier'])) {
423 if ($disp) {
424 $disp .= "AND {$params['external_identifier']}";
425 }
426 else {
427 $disp = $params['external_identifier'];
428 }
429 }
430
431 array_unshift($values, 'No matching Contact found for (' . $disp . ')');
432 return CRM_Import_Parser::ERROR;
433 }
434 }
435 else {
436 if (!empty($paramValues['external_identifier'])) {
437 $checkCid = new CRM_Contact_DAO_Contact();
438 $checkCid->external_identifier = $paramValues['external_identifier'];
439 $checkCid->find(TRUE);
440 if ($checkCid->id != $formatted['contact_id']) {
441 array_unshift($values, 'Mismatch of External ID:' . $paramValues['external_identifier'] . ' and Contact Id:' . $formatted['contact_id']);
442 return CRM_Import_Parser::ERROR;
443 }
444 }
445 $newContribution = civicrm_api('contribution', 'create', $formatted);
446 if (civicrm_error($newContribution)) {
447 if (is_array($newContribution['error_message'])) {
448 array_unshift($values, $newContribution['error_message']['message']);
449 if ($newContribution['error_message']['params'][0]) {
450 return CRM_Import_Parser::DUPLICATE;
451 }
452 }
453 else {
454 array_unshift($values, $newContribution['error_message']);
455 return CRM_Import_Parser::ERROR;
456 }
457 }
458
459 $this->_newContributions[] = $newContribution['id'];
460 $formatted['contribution_id'] = $newContribution['id'];
461
462 //return soft valid since we need to show how soft credits were added
463 if (!empty($formatted['soft_credit'])) {
464 return CRM_Contribute_Import_Parser::SOFT_CREDIT;
465 }
466
467 // process pledge payment assoc w/ the contribution
468 return self::processPledgePayments($formatted);
469
470 return CRM_Import_Parser::VALID;
471 }
472 }
473
474 /**
475 * Process pledge payments.
476 *
477 * @param array $formatted
478 *
479 * @return int
480 */
481 public function processPledgePayments(&$formatted) {
482 if (!empty($formatted['pledge_payment_id']) && !empty($formatted['pledge_id'])) {
483 //get completed status
484 $completeStatusID = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
485
486 //need to update payment record to map contribution_id
487 CRM_Core_DAO::setFieldValue('CRM_Pledge_DAO_PledgePayment', $formatted['pledge_payment_id'],
488 'contribution_id', $formatted['contribution_id']
489 );
490
491 CRM_Pledge_BAO_PledgePayment::updatePledgePaymentStatus($formatted['pledge_id'],
492 [$formatted['pledge_payment_id']],
493 $completeStatusID,
494 NULL,
495 $formatted['total_amount']
496 );
497
498 return CRM_Contribute_Import_Parser::PLEDGE_PAYMENT;
499 }
500 }
501
502 /**
503 * Get the array of successfully imported contribution id's
504 *
505 * @return array
506 */
507 public function &getImportedContributions() {
508 return $this->_newContributions;
509 }
510
511 /**
512 * The initializer code, called before the processing.
513 */
514 public function fini() {
515 }
516
517 /**
518 * Format date fields from input to mysql.
519 *
520 * @param array $params
521 *
522 * @return array
523 * Error messages, if any.
524 */
525 public function formatDateFields(&$params) {
526 $errorMessage = [];
527 $dateType = CRM_Core_Session::singleton()->get('dateTypes');
528 foreach ($params as $key => $val) {
529 if ($val) {
530 switch ($key) {
531 case 'receive_date':
532 if ($dateValue = CRM_Utils_Date::formatDate($params[$key], $dateType)) {
533 $params[$key] = $dateValue;
534 }
535 else {
536 $errorMessage[] = ts('Receive Date');
537 }
538 break;
539
540 case 'cancel_date':
541 if ($dateValue = CRM_Utils_Date::formatDate($params[$key], $dateType)) {
542 $params[$key] = $dateValue;
543 }
544 else {
545 $errorMessage[] = ts('Cancel Date');
546 }
547 break;
548
549 case 'receipt_date':
550 if ($dateValue = CRM_Utils_Date::formatDate($params[$key], $dateType)) {
551 $params[$key] = $dateValue;
552 }
553 else {
554 $errorMessage[] = ts('Receipt date');
555 }
556 break;
557
558 case 'thankyou_date':
559 if ($dateValue = CRM_Utils_Date::formatDate($params[$key], $dateType)) {
560 $params[$key] = $dateValue;
561 }
562 else {
563 $errorMessage[] = ts('Thankyou Date');
564 }
565 break;
566 }
567 }
568 }
569 return $errorMessage;
570 }
571
572 /**
573 * Format input params to suit api handling.
574 *
575 * Over time all the parts of deprecatedFormatParams
576 * and all the parts of the import function on this class that relate to
577 * reformatting input should be moved here and tests should be added in
578 * CRM_Contribute_Import_Parser_ContributionTest.
579 *
580 * @param array $params
581 * @param array $formatted
582 */
583 public function formatInput(&$params, &$formatted = []) {
584 $dateType = CRM_Core_Session::singleton()->get('dateTypes');
585 $customDataType = !empty($params['contact_type']) ? $params['contact_type'] : 'Contribution';
586 $customFields = CRM_Core_BAO_CustomField::getFields($customDataType);
587 // @todo call formatDateFields & move custom data handling there.
588 // Also note error handling for dates is currently in deprecatedFormatParams
589 // we should use the error handling in formatDateFields.
590 foreach ($params as $key => $val) {
591 // @todo - call formatDateFields instead.
592 if ($val) {
593 switch ($key) {
594 case 'receive_date':
595 case 'cancel_date':
596 case 'receipt_date':
597 case 'thankyou_date':
598 $params[$key] = CRM_Utils_Date::formatDate($params[$key], $dateType);
599 break;
600
601 case 'pledge_payment':
602 $params[$key] = CRM_Utils_String::strtobool($val);
603 break;
604
605 }
606 if ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) {
607 if ($customFields[$customFieldID]['data_type'] == 'Date') {
608 CRM_Contact_Import_Parser_Contact::formatCustomDate($params, $formatted, $dateType, $key);
609 unset($params[$key]);
610 }
611 elseif ($customFields[$customFieldID]['data_type'] == 'Boolean') {
612 $params[$key] = CRM_Utils_String::strtoboolstr($val);
613 }
614 }
615 }
616 }
617 }
618
619 /**
620 * take the input parameter list as specified in the data model and
621 * convert it into the same format that we use in QF and BAO object
622 *
623 * @param array $params
624 * Associative array of property name/value.
625 * pairs to insert in new contact.
626 * @param array $values
627 * The reformatted properties that we can use internally.
628 * '
629 *
630 * @param bool $create
631 * @param null $onDuplicate
632 *
633 * @return array|CRM_Error
634 */
635 private function deprecatedFormatParams($params, &$values, $create = FALSE, $onDuplicate = NULL) {
636 require_once 'CRM/Utils/DeprecatedUtils.php';
637 // copy all the contribution fields as is
638 require_once 'api/v3/utils.php';
639 $fields = CRM_Core_DAO::getExportableFieldsWithPseudoConstants('CRM_Contribute_BAO_Contribution');
640
641 _civicrm_api3_store_values($fields, $params, $values);
642
643 $customFields = CRM_Core_BAO_CustomField::getFields('Contribution', FALSE, FALSE, NULL, NULL, FALSE, FALSE, FALSE);
644
645 foreach ($params as $key => $value) {
646 // ignore empty values or empty arrays etc
647 if (CRM_Utils_System::isNull($value)) {
648 continue;
649 }
650
651 // Handling Custom Data
652 if ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) {
653 $values[$key] = $value;
654 $type = $customFields[$customFieldID]['html_type'];
655 if (CRM_Core_BAO_CustomField::isSerialized($customFields[$customFieldID])) {
656 $values[$key] = self::unserializeCustomValue($customFieldID, $value, $type);
657 }
658 elseif ($type == 'Select' || $type == 'Radio' ||
659 ($type == 'Autocomplete-Select' &&
660 $customFields[$customFieldID]['data_type'] == 'String'
661 )
662 ) {
663 $customOption = CRM_Core_BAO_CustomOption::getCustomOption($customFieldID, TRUE);
664 foreach ($customOption as $customFldID => $customValue) {
665 $val = $customValue['value'] ?? NULL;
666 $label = $customValue['label'] ?? NULL;
667 $label = strtolower($label);
668 $value = strtolower(trim($value));
669 if (($value == $label) || ($value == strtolower($val))) {
670 $values[$key] = $val;
671 }
672 }
673 }
674 continue;
675 }
676
677 switch ($key) {
678 case 'contribution_contact_id':
679 if (!CRM_Utils_Rule::integer($value)) {
680 return civicrm_api3_create_error("contact_id not valid: $value");
681 }
682 $dao = new CRM_Core_DAO();
683 $qParams = [];
684 $svq = $dao->singleValueQuery("SELECT is_deleted FROM civicrm_contact WHERE id = $value",
685 $qParams
686 );
687 if (!isset($svq)) {
688 return civicrm_api3_create_error("Invalid Contact ID: There is no contact record with contact_id = $value.");
689 }
690 elseif ($svq == 1) {
691 return civicrm_api3_create_error("Invalid Contact ID: contact_id $value is a soft-deleted contact.");
692 }
693
694 $values['contact_id'] = $values['contribution_contact_id'];
695 unset($values['contribution_contact_id']);
696 break;
697
698 case 'contact_type':
699 // import contribution record according to select contact type
700 require_once 'CRM/Contact/DAO/Contact.php';
701 $contactType = new CRM_Contact_DAO_Contact();
702 $contactId = $params['contribution_contact_id'] ?? NULL;
703 $externalId = $params['external_identifier'] ?? NULL;
704 $email = $params['email'] ?? NULL;
705 //when insert mode check contact id or external identifier
706 if ($contactId || $externalId) {
707 $contactType->id = $contactId;
708 $contactType->external_identifier = $externalId;
709 if ($contactType->find(TRUE)) {
710 if ($params['contact_type'] != $contactType->contact_type) {
711 return civicrm_api3_create_error("Contact Type is wrong: $contactType->contact_type");
712 }
713 }
714 }
715 elseif ($email) {
716 if (!CRM_Utils_Rule::email($email)) {
717 return civicrm_api3_create_error("Invalid email address $email provided. Row was skipped");
718 }
719
720 // get the contact id from duplicate contact rule, if more than one contact is returned
721 // we should return error, since current interface allows only one-one mapping
722 $emailParams = ['email' => $email, 'contact_type' => $params['contact_type']];
723 $checkDedupe = _civicrm_api3_deprecated_duplicate_formatted_contact($emailParams);
724 if (!$checkDedupe['is_error']) {
725 return civicrm_api3_create_error("Invalid email address(doesn't exist) $email. Row was skipped");
726 }
727 else {
728 $matchingContactIds = explode(',', $checkDedupe['error_message']['params'][0]);
729 if (count($matchingContactIds) > 1) {
730 return civicrm_api3_create_error("Invalid email address(duplicate) $email. Row was skipped");
731 }
732 elseif (count($matchingContactIds) == 1) {
733 $params['contribution_contact_id'] = $matchingContactIds[0];
734 }
735 }
736 }
737 elseif (!empty($params['contribution_id']) || !empty($params['trxn_id']) || !empty($params['invoice_id'])) {
738 // when update mode check contribution id or trxn id or
739 // invoice id
740 $contactId = new CRM_Contribute_DAO_Contribution();
741 if (!empty($params['contribution_id'])) {
742 $contactId->id = $params['contribution_id'];
743 }
744 elseif (!empty($params['trxn_id'])) {
745 $contactId->trxn_id = $params['trxn_id'];
746 }
747 elseif (!empty($params['invoice_id'])) {
748 $contactId->invoice_id = $params['invoice_id'];
749 }
750 if ($contactId->find(TRUE)) {
751 $contactType->id = $contactId->contact_id;
752 if ($contactType->find(TRUE)) {
753 if ($params['contact_type'] != $contactType->contact_type) {
754 return civicrm_api3_create_error("Contact Type is wrong: $contactType->contact_type");
755 }
756 }
757 }
758 }
759 else {
760 if ($onDuplicate == CRM_Import_Parser::DUPLICATE_UPDATE) {
761 return civicrm_api3_create_error("Empty Contribution and Invoice and Transaction ID. Row was skipped.");
762 }
763 }
764 break;
765
766 case 'receive_date':
767 case 'cancel_date':
768 case 'receipt_date':
769 case 'thankyou_date':
770 if (!CRM_Utils_Rule::dateTime($value)) {
771 return civicrm_api3_create_error("$key not a valid date: $value");
772 }
773 break;
774
775 case 'non_deductible_amount':
776 case 'total_amount':
777 case 'fee_amount':
778 case 'net_amount':
779 // @todo add test like testPaymentTypeLabel & remove these lines as we can anticipate error will still be caught & handled.
780 if (!CRM_Utils_Rule::money($value)) {
781 return civicrm_api3_create_error("$key not a valid amount: $value");
782 }
783 break;
784
785 case 'currency':
786 if (!CRM_Utils_Rule::currencyCode($value)) {
787 return civicrm_api3_create_error("currency not a valid code: $value");
788 }
789 break;
790
791 case 'financial_type':
792 // @todo add test like testPaymentTypeLabel & remove these lines in favour of 'default' part of switch.
793 require_once 'CRM/Contribute/PseudoConstant.php';
794 $contriTypes = CRM_Contribute_PseudoConstant::financialType();
795 foreach ($contriTypes as $val => $type) {
796 if (strtolower($value) == strtolower($type)) {
797 $values['financial_type_id'] = $val;
798 break;
799 }
800 }
801 if (empty($values['financial_type_id'])) {
802 return civicrm_api3_create_error("Financial Type is not valid: $value");
803 }
804 break;
805
806 case 'soft_credit':
807 // import contribution record according to select contact type
808 // validate contact id and external identifier.
809 $value[$key] = $mismatchContactType = $softCreditContactIds = '';
810 if (isset($params[$key]) && is_array($params[$key])) {
811 foreach ($params[$key] as $softKey => $softParam) {
812 $contactId = $softParam['contact_id'] ?? NULL;
813 $externalId = $softParam['external_identifier'] ?? NULL;
814 $email = $softParam['email'] ?? NULL;
815 if ($contactId || $externalId) {
816 require_once 'CRM/Contact/DAO/Contact.php';
817 $contact = new CRM_Contact_DAO_Contact();
818 $contact->id = $contactId;
819 $contact->external_identifier = $externalId;
820 $errorMsg = NULL;
821 if (!$contact->find(TRUE)) {
822 $field = $contactId ? ts('Contact ID') : ts('External ID');
823 $errorMsg = ts("Soft Credit %1 - %2 doesn't exist. Row was skipped.",
824 [1 => $field, 2 => $contactId ? $contactId : $externalId]);
825 }
826
827 if ($errorMsg) {
828 return civicrm_api3_create_error($errorMsg);
829 }
830
831 // finally get soft credit contact id.
832 $values[$key][$softKey] = $softParam;
833 $values[$key][$softKey]['contact_id'] = $contact->id;
834 }
835 elseif ($email) {
836 if (!CRM_Utils_Rule::email($email)) {
837 return civicrm_api3_create_error("Invalid email address $email provided for Soft Credit. Row was skipped");
838 }
839
840 // get the contact id from duplicate contact rule, if more than one contact is returned
841 // we should return error, since current interface allows only one-one mapping
842 $emailParams = ['email' => $email, 'contact_type' => $params['contact_type']];
843 $checkDedupe = _civicrm_api3_deprecated_duplicate_formatted_contact($emailParams);
844 if (!$checkDedupe['is_error']) {
845 return civicrm_api3_create_error("Invalid email address(doesn't exist) $email for Soft Credit. Row was skipped");
846 }
847 else {
848 $matchingContactIds = explode(',', $checkDedupe['error_message']['params'][0]);
849 if (count($matchingContactIds) > 1) {
850 return civicrm_api3_create_error("Invalid email address(duplicate) $email for Soft Credit. Row was skipped");
851 }
852 elseif (count($matchingContactIds) == 1) {
853 $contactId = $matchingContactIds[0];
854 unset($softParam['email']);
855 $values[$key][$softKey] = $softParam + ['contact_id' => $contactId];
856 }
857 }
858 }
859 }
860 }
861 break;
862
863 case 'pledge_payment':
864 case 'pledge_id':
865
866 // giving respect to pledge_payment flag.
867 if (empty($params['pledge_payment'])) {
868 break;
869 }
870
871 // get total amount of from import fields
872 $totalAmount = $params['total_amount'] ?? NULL;
873
874 $onDuplicate = $params['onDuplicate'] ?? NULL;
875
876 // we need to get contact id $contributionContactID to
877 // retrieve pledge details as well as to validate pledge ID
878
879 // first need to check for update mode
880 if ($onDuplicate == CRM_Import_Parser::DUPLICATE_UPDATE &&
881 ($params['contribution_id'] || $params['trxn_id'] || $params['invoice_id'])
882 ) {
883 $contribution = new CRM_Contribute_DAO_Contribution();
884 if ($params['contribution_id']) {
885 $contribution->id = $params['contribution_id'];
886 }
887 elseif ($params['trxn_id']) {
888 $contribution->trxn_id = $params['trxn_id'];
889 }
890 elseif ($params['invoice_id']) {
891 $contribution->invoice_id = $params['invoice_id'];
892 }
893
894 if ($contribution->find(TRUE)) {
895 $contributionContactID = $contribution->contact_id;
896 if (!$totalAmount) {
897 $totalAmount = $contribution->total_amount;
898 }
899 }
900 else {
901 return civicrm_api3_create_error('No match found for specified contact in pledge payment data. Row was skipped.');
902 }
903 }
904 else {
905 // first get the contact id for given contribution record.
906 if (!empty($params['contribution_contact_id'])) {
907 $contributionContactID = $params['contribution_contact_id'];
908 }
909 elseif (!empty($params['external_identifier'])) {
910 require_once 'CRM/Contact/DAO/Contact.php';
911 $contact = new CRM_Contact_DAO_Contact();
912 $contact->external_identifier = $params['external_identifier'];
913 if ($contact->find(TRUE)) {
914 $contributionContactID = $params['contribution_contact_id'] = $values['contribution_contact_id'] = $contact->id;
915 }
916 else {
917 return civicrm_api3_create_error('No match found for specified contact in pledge payment data. Row was skipped.');
918 }
919 }
920 else {
921 // we need to get contribution contact using de dupe
922 $error = _civicrm_api3_deprecated_check_contact_dedupe($params);
923
924 if (isset($error['error_message']['params'][0])) {
925 $matchedIDs = explode(',', $error['error_message']['params'][0]);
926
927 // check if only one contact is found
928 if (count($matchedIDs) > 1) {
929 return civicrm_api3_create_error($error['error_message']['message']);
930 }
931 else {
932 $contributionContactID = $params['contribution_contact_id'] = $values['contribution_contact_id'] = $matchedIDs[0];
933 }
934 }
935 else {
936 return civicrm_api3_create_error('No match found for specified contact in contribution data. Row was skipped.');
937 }
938 }
939 }
940
941 if (!empty($params['pledge_id'])) {
942 if (CRM_Core_DAO::getFieldValue('CRM_Pledge_DAO_Pledge', $params['pledge_id'], 'contact_id') != $contributionContactID) {
943 return civicrm_api3_create_error('Invalid Pledge ID provided. Contribution row was skipped.');
944 }
945 $values['pledge_id'] = $params['pledge_id'];
946 }
947 else {
948 // check if there are any pledge related to this contact, with payments pending or in progress
949 require_once 'CRM/Pledge/BAO/Pledge.php';
950 $pledgeDetails = CRM_Pledge_BAO_Pledge::getContactPledges($contributionContactID);
951
952 if (empty($pledgeDetails)) {
953 return civicrm_api3_create_error('No open pledges found for this contact. Contribution row was skipped.');
954 }
955 elseif (count($pledgeDetails) > 1) {
956 return civicrm_api3_create_error('This contact has more than one open pledge. Unable to determine which pledge to apply the contribution to. Contribution row was skipped.');
957 }
958
959 // this mean we have only one pending / in progress pledge
960 $values['pledge_id'] = $pledgeDetails[0];
961 }
962
963 // we need to check if oldest payment amount equal to contribution amount
964 require_once 'CRM/Pledge/BAO/PledgePayment.php';
965 $pledgePaymentDetails = CRM_Pledge_BAO_PledgePayment::getOldestPledgePayment($values['pledge_id']);
966
967 if ($pledgePaymentDetails['amount'] == $totalAmount) {
968 $values['pledge_payment_id'] = $pledgePaymentDetails['id'];
969 }
970 else {
971 return civicrm_api3_create_error('Contribution and Pledge Payment amount mismatch for this record. Contribution row was skipped.');
972 }
973 break;
974
975 default:
976 // Hande name or label for fields with options.
977 if (isset($fields[$key]) &&
978 // Yay - just for a surprise we are inconsistent on whether we pass the pseudofield (payment_instrument)
979 // or the field name (contribution_status_id)
980 (!empty($fields[$key]['is_pseudofield_for']) || !empty($fields[$key]['pseudoconstant']))
981 ) {
982 $realField = $fields[$key]['is_pseudofield_for'] ?? $key;
983 $realFieldSpec = $fields[$realField];
984 $values[$key] = $this->parsePseudoConstantField($value, $realFieldSpec);
985 }
986 break;
987 }
988 }
989
990 if (array_key_exists('note', $params)) {
991 $values['note'] = $params['note'];
992 }
993
994 if ($create) {
995 // CRM_Contribute_BAO_Contribution::add() handles contribution_source
996 // So, if $values contains contribution_source, convert it to source
997 $changes = ['contribution_source' => 'source'];
998
999 foreach ($changes as $orgVal => $changeVal) {
1000 if (isset($values[$orgVal])) {
1001 $values[$changeVal] = $values[$orgVal];
1002 unset($values[$orgVal]);
1003 }
1004 }
1005 }
1006
1007 return NULL;
1008 }
1009
1010 }