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 contribution csv files.
21 class CRM_Contribute_Import_Parser_Contribution
extends CRM_Import_Parser
{
23 protected $_mapperKeys;
25 private $_contactIdIndex;
27 protected $_mapperSoftCredit;
28 //protected $_mapperPhoneType;
31 * Array of successfully imported contribution id's
35 protected $_newContributions;
41 * @param array $mapperSoftCredit
42 * @param null $mapperPhoneType
43 * @param array $mapperSoftCreditType
45 public function __construct(&$mapperKeys = [], $mapperSoftCredit = [], $mapperPhoneType = NULL, $mapperSoftCreditType = []) {
46 parent
::__construct();
47 $this->_mapperKeys
= &$mapperKeys;
48 $this->_mapperSoftCredit
= &$mapperSoftCredit;
49 $this->_mapperSoftCreditType
= &$mapperSoftCreditType;
53 * Contribution-specific result codes
54 * @see CRM_Import_Parser result code constants
56 const SOFT_CREDIT
= 512, SOFT_CREDIT_ERROR
= 1024, PLEDGE_PAYMENT
= 2048, PLEDGE_PAYMENT_ERROR
= 4096;
70 * Separator being used
73 protected $_separator;
76 * Total number of lines in file
79 protected $_lineCount;
82 * Running total number of valid soft credit rows
85 protected $_validSoftCreditRowCount;
88 * Running total number of invalid soft credit rows
91 protected $_invalidSoftCreditRowCount;
94 * Running total number of valid pledge payment rows
97 protected $_validPledgePaymentRowCount;
100 * Running total number of invalid pledge payment rows
103 protected $_invalidPledgePaymentRowCount;
106 * Array of pledge payment error lines, bounded by MAX_ERROR
109 protected $_pledgePaymentErrors;
112 * Array of pledge payment error lines, bounded by MAX_ERROR
115 protected $_softCreditErrors;
118 * Filename of pledge payment error data
122 protected $_pledgePaymentErrorsFileName;
125 * Filename of soft credit error data
129 protected $_softCreditErrorsFileName;
132 * Whether the file has a column header or not
136 protected $_haveColumnHeader;
139 * @param string $fileName
140 * @param string $separator
142 * @param bool $skipColumnHeader
144 * @param int $contactType
145 * @param int $onDuplicate
146 * @param int $statusID
147 * @param int $totalRowCount
156 $skipColumnHeader = FALSE,
157 $mode = self
::MODE_PREVIEW
,
158 $contactType = self
::CONTACT_INDIVIDUAL
,
159 $onDuplicate = self
::DUPLICATE_SKIP
,
161 $totalRowCount = NULL
163 if (!is_array($fileName)) {
164 throw new CRM_Core_Exception('Unable to determine import file');
166 $fileName = $fileName['name'];
168 switch ($contactType) {
169 case self
::CONTACT_INDIVIDUAL
:
170 $this->_contactType
= 'Individual';
173 case self
::CONTACT_HOUSEHOLD
:
174 $this->_contactType
= 'Household';
177 case self
::CONTACT_ORGANIZATION
:
178 $this->_contactType
= 'Organization';
183 $this->_haveColumnHeader
= $skipColumnHeader;
185 $this->_separator
= $separator;
187 $fd = fopen($fileName, "r");
192 $this->_lineCount
= $this->_validSoftCreditRowCount
= $this->_validPledgePaymentRowCount
= 0;
193 $this->_invalidRowCount
= $this->_validCount
= $this->_invalidSoftCreditRowCount
= $this->_invalidPledgePaymentRowCount
= 0;
194 $this->_totalCount
= 0;
197 $this->_warnings
= [];
198 $this->_pledgePaymentErrors
= [];
199 $this->_softCreditErrors
= [];
201 $this->progressImport($statusID);
202 $startTimestamp = $currTimestamp = $prevTimestamp = time();
205 $this->_fileSize
= number_format(filesize($fileName) / 1024.0, 2);
207 if ($mode == self
::MODE_MAPFIELD
) {
211 $this->_activeFieldCount
= count($this->_activeFields
);
217 $values = fgetcsv($fd, 8192, $separator);
222 self
::encloseScrub($values);
224 // skip column header if we're not in mapfield mode
225 if ($mode != self
::MODE_MAPFIELD
&& $skipColumnHeader) {
226 $skipColumnHeader = FALSE;
230 /* trim whitespace around the values */
233 foreach ($values as $k => $v) {
234 $values[$k] = trim($v, " \t\r\n");
237 if (CRM_Utils_System
::isNull($values)) {
241 $this->_totalCount++
;
243 if ($mode == self
::MODE_MAPFIELD
) {
244 $returnCode = CRM_Import_Parser
::VALID
;
246 // Note that import summary appears to be unused
247 elseif ($mode == self
::MODE_PREVIEW ||
$mode == self
::MODE_SUMMARY
) {
248 $returnCode = $this->summary($values);
250 elseif ($mode == self
::MODE_IMPORT
) {
251 $returnCode = $this->import($onDuplicate, $values);
252 if ($statusID && (($this->_lineCount %
50) == 0)) {
253 $prevTimestamp = $this->progressImport($statusID, FALSE, $startTimestamp, $prevTimestamp, $totalRowCount);
257 $returnCode = self
::ERROR
;
260 // note that a line could be valid but still produce a warning
261 if ($returnCode == self
::VALID
) {
262 $this->_validCount++
;
263 if ($mode == self
::MODE_MAPFIELD
) {
264 $this->_rows
[] = $values;
265 $this->_activeFieldCount
= max($this->_activeFieldCount
, count($values));
269 if ($returnCode == self
::SOFT_CREDIT
) {
270 $this->_validSoftCreditRowCount++
;
271 $this->_validCount++
;
272 if ($mode == self
::MODE_MAPFIELD
) {
273 $this->_rows
[] = $values;
274 $this->_activeFieldCount
= max($this->_activeFieldCount
, count($values));
278 if ($returnCode == self
::PLEDGE_PAYMENT
) {
279 $this->_validPledgePaymentRowCount++
;
280 $this->_validCount++
;
281 if ($mode == self
::MODE_MAPFIELD
) {
282 $this->_rows
[] = $values;
283 $this->_activeFieldCount
= max($this->_activeFieldCount
, count($values));
287 if ($returnCode == self
::ERROR
) {
288 $this->_invalidRowCount++
;
289 $recordNumber = $this->_lineCount
;
290 if ($this->_haveColumnHeader
) {
293 array_unshift($values, $recordNumber);
294 $this->_errors
[] = $values;
297 if ($returnCode == self
::PLEDGE_PAYMENT_ERROR
) {
298 $this->_invalidPledgePaymentRowCount++
;
299 $recordNumber = $this->_lineCount
;
300 if ($this->_haveColumnHeader
) {
303 array_unshift($values, $recordNumber);
304 $this->_pledgePaymentErrors
[] = $values;
307 if ($returnCode == self
::SOFT_CREDIT_ERROR
) {
308 $this->_invalidSoftCreditRowCount++
;
309 $recordNumber = $this->_lineCount
;
310 if ($this->_haveColumnHeader
) {
313 array_unshift($values, $recordNumber);
314 $this->_softCreditErrors
[] = $values;
317 if ($returnCode == self
::DUPLICATE
) {
318 $this->_duplicateCount++
;
319 $recordNumber = $this->_lineCount
;
320 if ($this->_haveColumnHeader
) {
323 array_unshift($values, $recordNumber);
324 $this->_duplicates
[] = $values;
325 if ($onDuplicate != self
::DUPLICATE_SKIP
) {
326 $this->_validCount++
;
330 // if we are done processing the maxNumber of lines, break
331 if ($this->_maxLinesToProcess
> 0 && $this->_validCount
>= $this->_maxLinesToProcess
) {
338 if ($mode == self
::MODE_PREVIEW ||
$mode == self
::MODE_IMPORT
) {
339 $customHeaders = $mapper;
341 $customfields = CRM_Core_BAO_CustomField
::getFields('Contribution');
342 foreach ($customHeaders as $key => $value) {
343 if ($id = CRM_Core_BAO_CustomField
::getKeyID($value)) {
344 $customHeaders[$key] = $customfields[$id][0];
347 if ($this->_invalidRowCount
) {
348 // removed view url for invlaid contacts
349 $headers = array_merge([
353 $this->_errorFileName
= self
::errorFileName(self
::ERROR
);
354 self
::exportCSV($this->_errorFileName
, $headers, $this->_errors
);
357 if ($this->_invalidPledgePaymentRowCount
) {
358 // removed view url for invlaid contacts
359 $headers = array_merge([
363 $this->_pledgePaymentErrorsFileName
= self
::errorFileName(self
::PLEDGE_PAYMENT_ERROR
);
364 self
::exportCSV($this->_pledgePaymentErrorsFileName
, $headers, $this->_pledgePaymentErrors
);
367 if ($this->_invalidSoftCreditRowCount
) {
368 // removed view url for invlaid contacts
369 $headers = array_merge([
373 $this->_softCreditErrorsFileName
= self
::errorFileName(self
::SOFT_CREDIT_ERROR
);
374 self
::exportCSV($this->_softCreditErrorsFileName
, $headers, $this->_softCreditErrors
);
377 if ($this->_duplicateCount
) {
378 $headers = array_merge([
380 ts('View Contribution URL'),
383 $this->_duplicateFileName
= self
::errorFileName(self
::DUPLICATE
);
384 self
::exportCSV($this->_duplicateFileName
, $headers, $this->_duplicates
);
390 * Given a list of the importable field keys that the user has selected
391 * set the active fields array to this list
393 * @param array $fieldKeys mapped array of values
395 public function setActiveFields($fieldKeys) {
396 $this->_activeFieldCount
= count($fieldKeys);
397 foreach ($fieldKeys as $key) {
398 if (empty($this->_fields
[$key])) {
399 $this->_activeFields
[] = new CRM_Contribute_Import_Field('', ts('- do not import -'));
402 $this->_activeFields
[] = clone($this->_fields
[$key]);
408 * Store the soft credit field information.
410 * This was perhaps done this way on the believe that a lot of code pain
411 * was worth it to avoid negligible-cost array iterations. Perhaps we could prioritise
412 * readability & maintainability next since we can just work with functions to retrieve
413 * data from the metadata.
415 * @param array $elements
417 public function setActiveFieldSoftCredit($elements) {
418 foreach ((array) $elements as $i => $element) {
419 $this->_activeFields
[$i]->_softCreditField
= $element;
424 * Store the soft credit field type information.
426 * This was perhaps done this way on the believe that a lot of code pain
427 * was worth it to avoid negligible-cost array iterations. Perhaps we could prioritise
428 * readability & maintainability next since we can just work with functions to retrieve
429 * data from the metadata.
431 * @param array $elements
433 public function setActiveFieldSoftCreditType($elements) {
434 foreach ((array) $elements as $i => $element) {
435 $this->_activeFields
[$i]->_softCreditType
= $element;
440 * Format the field values for input to the api.
443 * (reference ) associative array of name/value pairs
445 public function &getActiveFieldParams() {
447 for ($i = 0; $i < $this->_activeFieldCount
; $i++
) {
448 if (isset($this->_activeFields
[$i]->_value
)) {
449 if (isset($this->_activeFields
[$i]->_softCreditField
)) {
450 if (!isset($params[$this->_activeFields
[$i]->_name
])) {
451 $params[$this->_activeFields
[$i]->_name
] = [];
453 $params[$this->_activeFields
[$i]->_name
][$i][$this->_activeFields
[$i]->_softCreditField
] = $this->_activeFields
[$i]->_value
;
454 if (isset($this->_activeFields
[$i]->_softCreditType
)) {
455 $params[$this->_activeFields
[$i]->_name
][$i]['soft_credit_type_id'] = $this->_activeFields
[$i]->_softCreditType
;
459 if (!isset($params[$this->_activeFields
[$i]->_name
])) {
460 if (!isset($this->_activeFields
[$i]->_softCreditField
)) {
461 $params[$this->_activeFields
[$i]->_name
] = $this->_activeFields
[$i]->_value
;
470 * @param string $name
473 * @param string $headerPattern
474 * @param string $dataPattern
476 public function addField($name, $title, $type = CRM_Utils_Type
::T_INT
, $headerPattern = '//', $dataPattern = '//') {
478 $this->_fields
['doNotImport'] = new CRM_Contribute_Import_Field($name, $title, $type, $headerPattern, $dataPattern);
481 $tempField = CRM_Contact_BAO_Contact
::importableFields('All', NULL);
482 if (!array_key_exists($name, $tempField)) {
483 $this->_fields
[$name] = new CRM_Contribute_Import_Field($name, $title, $type, $headerPattern, $dataPattern);
486 $this->_fields
[$name] = new CRM_Contact_Import_Field($name, $title, $type, $headerPattern, $dataPattern,
487 CRM_Utils_Array
::value('hasLocationType', $tempField[$name])
494 * Store parser values.
496 * @param CRM_Core_Session $store
500 public function set($store, $mode = self
::MODE_SUMMARY
) {
501 $store->set('fileSize', $this->_fileSize
);
502 $store->set('lineCount', $this->_lineCount
);
503 $store->set('separator', $this->_separator
);
504 $store->set('fields', $this->getSelectValues());
506 $store->set('headerPatterns', $this->getHeaderPatterns());
507 $store->set('dataPatterns', $this->getDataPatterns());
508 $store->set('columnCount', $this->_activeFieldCount
);
510 $store->set('totalRowCount', $this->_totalCount
);
511 $store->set('validRowCount', $this->_validCount
);
512 $store->set('invalidRowCount', $this->_invalidRowCount
);
513 $store->set('invalidSoftCreditRowCount', $this->_invalidSoftCreditRowCount
);
514 $store->set('validSoftCreditRowCount', $this->_validSoftCreditRowCount
);
515 $store->set('invalidPledgePaymentRowCount', $this->_invalidPledgePaymentRowCount
);
516 $store->set('validPledgePaymentRowCount', $this->_validPledgePaymentRowCount
);
518 switch ($this->_contactType
) {
520 $store->set('contactType', CRM_Import_Parser
::CONTACT_INDIVIDUAL
);
524 $store->set('contactType', CRM_Import_Parser
::CONTACT_HOUSEHOLD
);
528 $store->set('contactType', CRM_Import_Parser
::CONTACT_ORGANIZATION
);
531 if ($this->_invalidRowCount
) {
532 $store->set('errorsFileName', $this->_errorFileName
);
534 if (isset($this->_rows
) && !empty($this->_rows
)) {
535 $store->set('dataValues', $this->_rows
);
538 if ($this->_invalidPledgePaymentRowCount
) {
539 $store->set('pledgePaymentErrorsFileName', $this->_pledgePaymentErrorsFileName
);
542 if ($this->_invalidSoftCreditRowCount
) {
543 $store->set('softCreditErrorsFileName', $this->_softCreditErrorsFileName
);
546 if ($mode == self
::MODE_IMPORT
) {
547 $store->set('duplicateRowCount', $this->_duplicateCount
);
548 if ($this->_duplicateCount
) {
549 $store->set('duplicatesFileName', $this->_duplicateFileName
);
555 * Export data to a CSV file.
557 * @param string $fileName
558 * @param array $header
561 public static function exportCSV($fileName, $header, $data) {
563 $fd = fopen($fileName, 'w');
565 foreach ($header as $key => $value) {
566 $header[$key] = "\"$value\"";
568 $config = CRM_Core_Config
::singleton();
569 $output[] = implode($config->fieldSeparator
, $header);
571 foreach ($data as $datum) {
572 foreach ($datum as $key => $value) {
573 if (isset($value[0]) && is_array($value)) {
574 foreach ($value[0] as $k1 => $v1) {
575 if ($k1 == 'location_type_id') {
582 $datum[$key] = "\"$value\"";
585 $output[] = implode($config->fieldSeparator
, $datum);
587 fwrite($fd, implode("\n", $output));
592 * Determines the file extension based on error code.
595 * Error code constant.
599 public static function errorFileName($type) {
605 $config = CRM_Core_Config
::singleton();
606 $fileName = $config->uploadDir
. "sqlImport";
609 case self
::SOFT_CREDIT_ERROR
:
610 $fileName .= '.softCreditErrors';
613 case self
::PLEDGE_PAYMENT_ERROR
:
614 $fileName .= '.pledgePaymentErrors';
618 $fileName = parent
::errorFileName($type);
626 * Determines the file name based on error code.
629 * Error code constant.
633 public static function saveFileName($type) {
640 case self
::SOFT_CREDIT_ERROR
:
641 $fileName = 'Import_Soft_Credit_Errors.csv';
644 case self
::PLEDGE_PAYMENT_ERROR
:
645 $fileName = 'Import_Pledge_Payment_Errors.csv';
649 $fileName = parent
::saveFileName($type);
657 * The initializer code, called before the processing
659 public function init() {
660 $this->setFieldMetadata();
661 foreach ($this->getImportableFieldsMetadata() as $name => $field) {
662 $this->addField($name, $field['title'], $field['type'], $field['headerPattern'], $field['dataPattern']);
665 $this->_newContributions
= [];
667 $this->setActiveFields($this->_mapperKeys
);
668 $this->setActiveFieldSoftCredit($this->_mapperSoftCredit
);
669 $this->setActiveFieldSoftCreditType($this->_mapperSoftCreditType
);
671 // FIXME: we should do this in one place together with Form/MapField.php
672 $this->_contactIdIndex
= -1;
675 foreach ($this->_mapperKeys
as $key) {
677 case 'contribution_contact_id':
678 $this->_contactIdIndex
= $index;
687 * Set field metadata.
689 protected function setFieldMetadata() {
690 if (empty($this->importableFieldsMetadata
)) {
691 $fields = CRM_Contribute_BAO_Contribution
::importableFields($this->_contactType
, FALSE);
693 $fields = array_merge($fields,
696 'title' => ts('Soft Credit'),
697 'softCredit' => TRUE,
698 'headerPattern' => '/Soft Credit/i',
703 // add pledge fields only if its is enabled
704 if (CRM_Core_Permission
::access('CiviPledge')) {
706 'pledge_payment' => [
707 'title' => ts('Pledge Payment'),
708 'headerPattern' => '/Pledge Payment/i',
711 'title' => ts('Pledge ID'),
712 'headerPattern' => '/Pledge ID/i',
716 $fields = array_merge($fields, $pledgeFields);
718 foreach ($fields as $name => $field) {
719 $fields[$name] = array_merge([
720 'type' => CRM_Utils_Type
::T_INT
,
721 'dataPattern' => '//',
722 'headerPattern' => '//',
725 $this->importableFieldsMetadata
= $fields;
730 * Handle the values in summary mode.
732 * @param array $values
733 * The array of values belonging to this line.
736 * CRM_Import_Parser::VALID or CRM_Import_Parser::ERROR
738 public function summary(&$values) {
739 $this->setActiveFieldValues($values);
741 $params = $this->getActiveFieldParams();
744 $errorMessage = implode('; ', $this->formatDateFields($params));
745 //date-Format part ends
747 $params['contact_type'] = 'Contribution';
749 //checking error in custom data
750 CRM_Contact_Import_Parser_Contact
::isErrorInCustomData($params, $errorMessage);
753 $tempMsg = "Invalid value for field(s) : $errorMessage";
754 array_unshift($values, $tempMsg);
755 $errorMessage = NULL;
756 return CRM_Import_Parser
::ERROR
;
759 return CRM_Import_Parser
::VALID
;
763 * Handle the values in import mode.
765 * @param int $onDuplicate
766 * The code for what action to take on duplicates.
767 * @param array $values
768 * The array of values belonging to this line.
771 * the result of this processing - one of
772 * - CRM_Import_Parser::VALID
773 * - CRM_Import_Parser::ERROR
774 * - CRM_Import_Parser::SOFT_CREDIT_ERROR
775 * - CRM_Import_Parser::PLEDGE_PAYMENT_ERROR
776 * - CRM_Import_Parser::DUPLICATE
777 * - CRM_Import_Parser::SOFT_CREDIT (successful creation)
778 * - CRM_Import_Parser::PLEDGE_PAYMENT (successful creation)
780 public function import($onDuplicate, &$values) {
781 // first make sure this is a valid line
782 $response = $this->summary($values);
783 if ($response != CRM_Import_Parser
::VALID
) {
784 return CRM_Import_Parser
::ERROR
;
787 $params = &$this->getActiveFieldParams();
788 $formatted = ['version' => 3, 'skipRecentView' => TRUE, 'skipCleanMoney' => FALSE];
791 if (isset($params['total_amount']) && $params['total_amount'] == 0) {
792 $params['total_amount'] = '0.00';
794 $this->formatInput($params, $formatted);
797 foreach ($params as $key => $field) {
798 if ($field == NULL ||
$field === '') {
801 $paramValues[$key] = $field;
804 //import contribution record according to select contact type
805 if ($onDuplicate == CRM_Import_Parser
::DUPLICATE_SKIP
&&
806 (!empty($paramValues['contribution_contact_id']) ||
!empty($paramValues['external_identifier']))
808 $paramValues['contact_type'] = $this->_contactType
;
810 elseif ($onDuplicate == CRM_Import_Parser
::DUPLICATE_UPDATE
&&
811 (!empty($paramValues['contribution_id']) ||
!empty($values['trxn_id']) ||
!empty($paramValues['invoice_id']))
813 $paramValues['contact_type'] = $this->_contactType
;
815 elseif (!empty($params['soft_credit'])) {
816 $paramValues['contact_type'] = $this->_contactType
;
818 elseif (!empty($paramValues['pledge_payment'])) {
819 $paramValues['contact_type'] = $this->_contactType
;
822 //need to pass $onDuplicate to check import mode.
823 if (!empty($paramValues['pledge_payment'])) {
824 $paramValues['onDuplicate'] = $onDuplicate;
826 $formatError = $this->deprecatedFormatParams($paramValues, $formatted, TRUE, $onDuplicate);
829 array_unshift($values, $formatError['error_message']);
830 if (CRM_Utils_Array
::value('error_data', $formatError) == 'soft_credit') {
831 return self
::SOFT_CREDIT_ERROR
;
833 if (CRM_Utils_Array
::value('error_data', $formatError) == 'pledge_payment') {
834 return self
::PLEDGE_PAYMENT_ERROR
;
836 return CRM_Import_Parser
::ERROR
;
839 if ($onDuplicate == CRM_Import_Parser
::DUPLICATE_UPDATE
) {
840 //fix for CRM-2219 - Update Contribution
841 // onDuplicate == CRM_Import_Parser::DUPLICATE_UPDATE
842 if (!empty($paramValues['invoice_id']) ||
!empty($paramValues['trxn_id']) ||
!empty($paramValues['contribution_id'])) {
844 'id' => $paramValues['contribution_id'] ??
NULL,
845 'trxn_id' => $paramValues['trxn_id'] ??
NULL,
846 'invoice_id' => $paramValues['invoice_id'] ??
NULL,
848 $ids['contribution'] = CRM_Contribute_BAO_Contribution
::checkDuplicateIds($dupeIds);
850 if ($ids['contribution']) {
851 $formatted['id'] = $ids['contribution'];
853 if (!empty($paramValues['note'])) {
855 $contactID = CRM_Core_DAO
::getFieldValue('CRM_Contribute_DAO_Contribution', $ids['contribution'], 'contact_id');
856 $daoNote = new CRM_Core_BAO_Note();
857 $daoNote->entity_table
= 'civicrm_contribution';
858 $daoNote->entity_id
= $ids['contribution'];
859 if ($daoNote->find(TRUE)) {
860 $noteID['id'] = $daoNote->id
;
864 'entity_table' => 'civicrm_contribution',
865 'note' => $paramValues['note'],
866 'entity_id' => $ids['contribution'],
867 'contact_id' => $contactID,
869 CRM_Core_BAO_Note
::add($noteParams, $noteID);
870 unset($formatted['note']);
873 //need to check existing soft credit contribution, CRM-3968
874 if (!empty($formatted['soft_credit'])) {
876 'contact_id' => $formatted['soft_credit'],
877 'contribution_id' => $ids['contribution'],
880 //Delete all existing soft Contribution from contribution_soft table for pcp_id is_null
881 $existingSoftCredit = CRM_Contribute_BAO_ContributionSoft
::getSoftContribution($dupeSoftCredit['contribution_id']);
882 if (isset($existingSoftCredit['soft_credit']) && !empty($existingSoftCredit['soft_credit'])) {
883 foreach ($existingSoftCredit['soft_credit'] as $key => $existingSoftCreditValues) {
884 if (!empty($existingSoftCreditValues['soft_credit_id'])) {
885 civicrm_api3('ContributionSoft', 'delete', [
886 'id' => $existingSoftCreditValues['soft_credit_id'],
894 $formatted['id'] = $ids['contribution'];
896 $newContribution = civicrm_api3('contribution', 'create', $formatted);
897 $this->_newContributions
[] = $newContribution['id'];
899 //return soft valid since we need to show how soft credits were added
900 if (!empty($formatted['soft_credit'])) {
901 return self
::SOFT_CREDIT
;
904 // process pledge payment assoc w/ the contribution
905 return $this->processPledgePayments($formatted);
908 'id' => 'Contribution ID',
909 'trxn_id' => 'Transaction ID',
910 'invoice_id' => 'Invoice ID',
912 foreach ($dupeIds as $k => $v) {
914 $errorMsg[] = "$labels[$k] $v";
917 $errorMsg = implode(' AND ', $errorMsg);
918 array_unshift($values, 'Matching Contribution record not found for ' . $errorMsg . '. Row was skipped.');
919 return CRM_Import_Parser
::ERROR
;
923 if ($this->_contactIdIndex
< 0) {
925 $error = $this->checkContactDuplicate($paramValues);
927 if (CRM_Core_Error
::isAPIError($error, CRM_Core_ERROR
::DUPLICATE_CONTACT
)) {
928 $matchedIDs = explode(',', $error['error_message']['params'][0]);
929 if (count($matchedIDs) > 1) {
930 array_unshift($values, 'Multiple matching contact records detected for this row. The contribution was not imported');
931 return CRM_Import_Parser
::ERROR
;
933 $cid = $matchedIDs[0];
934 $formatted['contact_id'] = $cid;
936 $newContribution = civicrm_api('contribution', 'create', $formatted);
937 if (civicrm_error($newContribution)) {
938 if (is_array($newContribution['error_message'])) {
939 array_unshift($values, $newContribution['error_message']['message']);
940 if ($newContribution['error_message']['params'][0]) {
941 return CRM_Import_Parser
::DUPLICATE
;
945 array_unshift($values, $newContribution['error_message']);
946 return CRM_Import_Parser
::ERROR
;
950 $this->_newContributions
[] = $newContribution['id'];
951 $formatted['contribution_id'] = $newContribution['id'];
953 //return soft valid since we need to show how soft credits were added
954 if (!empty($formatted['soft_credit'])) {
955 return self
::SOFT_CREDIT
;
958 // process pledge payment assoc w/ the contribution
959 return $this->processPledgePayments($formatted);
962 // Using new Dedupe rule.
964 'contact_type' => $this->_contactType
,
965 'used' => 'Unsupervised',
967 $fieldsArray = CRM_Dedupe_BAO_DedupeRule
::dedupeRuleFields($ruleParams);
969 foreach ($fieldsArray as $value) {
970 if (array_key_exists(trim($value), $params)) {
971 $paramValue = $params[trim($value)];
972 if (is_array($paramValue)) {
973 $disp .= $params[trim($value)][0][trim($value)] . " ";
976 $disp .= $params[trim($value)] . " ";
981 if (!empty($params['external_identifier'])) {
983 $disp .= "AND {$params['external_identifier']}";
986 $disp = $params['external_identifier'];
990 array_unshift($values, 'No matching Contact found for (' . $disp . ')');
991 return CRM_Import_Parser
::ERROR
;
994 if (!empty($paramValues['external_identifier'])) {
995 $checkCid = new CRM_Contact_DAO_Contact();
996 $checkCid->external_identifier
= $paramValues['external_identifier'];
997 $checkCid->find(TRUE);
998 if ($checkCid->id
!= $formatted['contact_id']) {
999 array_unshift($values, 'Mismatch of External ID:' . $paramValues['external_identifier'] . ' and Contact Id:' . $formatted['contact_id']);
1000 return CRM_Import_Parser
::ERROR
;
1003 $newContribution = civicrm_api('contribution', 'create', $formatted);
1004 if (civicrm_error($newContribution)) {
1005 if (is_array($newContribution['error_message'])) {
1006 array_unshift($values, $newContribution['error_message']['message']);
1007 if ($newContribution['error_message']['params'][0]) {
1008 return CRM_Import_Parser
::DUPLICATE
;
1012 array_unshift($values, $newContribution['error_message']);
1013 return CRM_Import_Parser
::ERROR
;
1017 $this->_newContributions
[] = $newContribution['id'];
1018 $formatted['contribution_id'] = $newContribution['id'];
1020 //return soft valid since we need to show how soft credits were added
1021 if (!empty($formatted['soft_credit'])) {
1022 return self
::SOFT_CREDIT
;
1025 // process pledge payment assoc w/ the contribution
1026 return $this->processPledgePayments($formatted);
1030 * Process pledge payments.
1032 * @param array $formatted
1036 private function processPledgePayments(array $formatted) {
1037 if (!empty($formatted['pledge_payment_id']) && !empty($formatted['pledge_id'])) {
1038 //get completed status
1039 $completeStatusID = CRM_Core_PseudoConstant
::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
1041 //need to update payment record to map contribution_id
1042 CRM_Core_DAO
::setFieldValue('CRM_Pledge_DAO_PledgePayment', $formatted['pledge_payment_id'],
1043 'contribution_id', $formatted['contribution_id']
1046 CRM_Pledge_BAO_PledgePayment
::updatePledgePaymentStatus($formatted['pledge_id'],
1047 [$formatted['pledge_payment_id']],
1050 $formatted['total_amount']
1053 return self
::PLEDGE_PAYMENT
;
1058 * Get the array of successfully imported contribution id's
1062 public function &getImportedContributions() {
1063 return $this->_newContributions
;
1067 * Format date fields from input to mysql.
1069 * @param array $params
1072 * Error messages, if any.
1074 public function formatDateFields(&$params) {
1076 $dateType = CRM_Core_Session
::singleton()->get('dateTypes');
1077 foreach ($params as $key => $val) {
1080 case 'receive_date':
1081 if ($dateValue = CRM_Utils_Date
::formatDate($params[$key], $dateType)) {
1082 $params[$key] = $dateValue;
1085 $errorMessage[] = ts('Receive Date');
1090 if ($dateValue = CRM_Utils_Date
::formatDate($params[$key], $dateType)) {
1091 $params[$key] = $dateValue;
1094 $errorMessage[] = ts('Cancel Date');
1098 case 'receipt_date':
1099 if ($dateValue = CRM_Utils_Date
::formatDate($params[$key], $dateType)) {
1100 $params[$key] = $dateValue;
1103 $errorMessage[] = ts('Receipt date');
1107 case 'thankyou_date':
1108 if ($dateValue = CRM_Utils_Date
::formatDate($params[$key], $dateType)) {
1109 $params[$key] = $dateValue;
1112 $errorMessage[] = ts('Thankyou Date');
1118 return $errorMessage;
1122 * Format input params to suit api handling.
1124 * Over time all the parts of deprecatedFormatParams
1125 * and all the parts of the import function on this class that relate to
1126 * reformatting input should be moved here and tests should be added in
1127 * CRM_Contribute_Import_Parser_ContributionTest.
1129 * @param array $params
1130 * @param array $formatted
1132 public function formatInput(&$params, &$formatted = []) {
1133 $dateType = CRM_Core_Session
::singleton()->get('dateTypes');
1134 $customDataType = !empty($params['contact_type']) ?
$params['contact_type'] : 'Contribution';
1135 $customFields = CRM_Core_BAO_CustomField
::getFields($customDataType);
1136 // @todo call formatDateFields & move custom data handling there.
1137 // Also note error handling for dates is currently in deprecatedFormatParams
1138 // we should use the error handling in formatDateFields.
1139 foreach ($params as $key => $val) {
1140 // @todo - call formatDateFields instead.
1143 case 'receive_date':
1145 case 'receipt_date':
1146 case 'thankyou_date':
1147 $params[$key] = CRM_Utils_Date
::formatDate($params[$key], $dateType);
1150 case 'pledge_payment':
1151 $params[$key] = CRM_Utils_String
::strtobool($val);
1155 if ($customFieldID = CRM_Core_BAO_CustomField
::getKeyID($key)) {
1156 if ($customFields[$customFieldID]['data_type'] == 'Date') {
1157 CRM_Contact_Import_Parser_Contact
::formatCustomDate($params, $formatted, $dateType, $key);
1158 unset($params[$key]);
1160 elseif ($customFields[$customFieldID]['data_type'] == 'Boolean') {
1161 $params[$key] = CRM_Utils_String
::strtoboolstr($val);
1169 * take the input parameter list as specified in the data model and
1170 * convert it into the same format that we use in QF and BAO object
1172 * @param array $params
1173 * Associative array of property name/value
1174 * pairs to insert in new contact.
1175 * @param array $values
1176 * The reformatted properties that we can use internally.
1177 * @param bool $create
1178 * @param int $onDuplicate
1180 * @return array|CRM_Error
1182 private function deprecatedFormatParams($params, &$values, $create = FALSE, $onDuplicate = NULL) {
1183 require_once 'CRM/Utils/DeprecatedUtils.php';
1184 // copy all the contribution fields as is
1185 require_once 'api/v3/utils.php';
1186 $fields = CRM_Core_DAO
::getExportableFieldsWithPseudoConstants('CRM_Contribute_BAO_Contribution');
1188 _civicrm_api3_store_values($fields, $params, $values);
1190 $customFields = CRM_Core_BAO_CustomField
::getFields('Contribution', FALSE, FALSE, NULL, NULL, FALSE, FALSE, FALSE);
1192 foreach ($params as $key => $value) {
1193 // ignore empty values or empty arrays etc
1194 if (CRM_Utils_System
::isNull($value)) {
1198 // Handling Custom Data
1199 if ($customFieldID = CRM_Core_BAO_CustomField
::getKeyID($key)) {
1200 $values[$key] = $value;
1201 $type = $customFields[$customFieldID]['html_type'];
1202 if (CRM_Core_BAO_CustomField
::isSerialized($customFields[$customFieldID])) {
1203 $values[$key] = self
::unserializeCustomValue($customFieldID, $value, $type);
1205 elseif ($type == 'Select' ||
$type == 'Radio' ||
1206 ($type == 'Autocomplete-Select' &&
1207 $customFields[$customFieldID]['data_type'] == 'String'
1210 $customOption = CRM_Core_BAO_CustomOption
::getCustomOption($customFieldID, TRUE);
1211 foreach ($customOption as $customFldID => $customValue) {
1212 $val = $customValue['value'] ??
NULL;
1213 $label = $customValue['label'] ??
NULL;
1214 $label = strtolower($label);
1215 $value = strtolower(trim($value));
1216 if (($value == $label) ||
($value == strtolower($val))) {
1217 $values[$key] = $val;
1225 case 'contribution_contact_id':
1226 if (!CRM_Utils_Rule
::integer($value)) {
1227 return civicrm_api3_create_error("contact_id not valid: $value");
1229 $dao = new CRM_Core_DAO();
1231 $svq = $dao->singleValueQuery("SELECT is_deleted FROM civicrm_contact WHERE id = $value",
1235 return civicrm_api3_create_error("Invalid Contact ID: There is no contact record with contact_id = $value.");
1237 elseif ($svq == 1) {
1238 return civicrm_api3_create_error("Invalid Contact ID: contact_id $value is a soft-deleted contact.");
1241 $values['contact_id'] = $values['contribution_contact_id'];
1242 unset($values['contribution_contact_id']);
1245 case 'contact_type':
1246 // import contribution record according to select contact type
1247 require_once 'CRM/Contact/DAO/Contact.php';
1248 $contactType = new CRM_Contact_DAO_Contact();
1249 $contactId = $params['contribution_contact_id'] ??
NULL;
1250 $externalId = $params['external_identifier'] ??
NULL;
1251 $email = $params['email'] ??
NULL;
1252 //when insert mode check contact id or external identifier
1253 if ($contactId ||
$externalId) {
1254 $contactType->id
= $contactId;
1255 $contactType->external_identifier
= $externalId;
1256 if ($contactType->find(TRUE)) {
1257 if ($params['contact_type'] != $contactType->contact_type
) {
1258 return civicrm_api3_create_error("Contact Type is wrong: $contactType->contact_type");
1263 if (!CRM_Utils_Rule
::email($email)) {
1264 return civicrm_api3_create_error("Invalid email address $email provided. Row was skipped");
1267 // get the contact id from duplicate contact rule, if more than one contact is returned
1268 // we should return error, since current interface allows only one-one mapping
1271 'contact_type' => $params['contact_type'],
1273 $checkDedupe = _civicrm_api3_deprecated_duplicate_formatted_contact($emailParams);
1274 if (!$checkDedupe['is_error']) {
1275 return civicrm_api3_create_error("Invalid email address(doesn't exist) $email. Row was skipped");
1277 $matchingContactIds = explode(',', $checkDedupe['error_message']['params'][0]);
1278 if (count($matchingContactIds) > 1) {
1279 return civicrm_api3_create_error("Invalid email address(duplicate) $email. Row was skipped");
1281 if (count($matchingContactIds) == 1) {
1282 $params['contribution_contact_id'] = $matchingContactIds[0];
1285 elseif (!empty($params['contribution_id']) ||
!empty($params['trxn_id']) ||
!empty($params['invoice_id'])) {
1286 // when update mode check contribution id or trxn id or
1288 $contactId = new CRM_Contribute_DAO_Contribution();
1289 if (!empty($params['contribution_id'])) {
1290 $contactId->id
= $params['contribution_id'];
1292 elseif (!empty($params['trxn_id'])) {
1293 $contactId->trxn_id
= $params['trxn_id'];
1295 elseif (!empty($params['invoice_id'])) {
1296 $contactId->invoice_id
= $params['invoice_id'];
1298 if ($contactId->find(TRUE)) {
1299 $contactType->id
= $contactId->contact_id
;
1300 if ($contactType->find(TRUE)) {
1301 if ($params['contact_type'] != $contactType->contact_type
) {
1302 return civicrm_api3_create_error("Contact Type is wrong: $contactType->contact_type");
1308 if ($onDuplicate == CRM_Import_Parser
::DUPLICATE_UPDATE
) {
1309 return civicrm_api3_create_error("Empty Contribution and Invoice and Transaction ID. Row was skipped.");
1314 case 'receive_date':
1316 case 'receipt_date':
1317 case 'thankyou_date':
1318 if (!CRM_Utils_Rule
::dateTime($value)) {
1319 return civicrm_api3_create_error("$key not a valid date: $value");
1323 case 'non_deductible_amount':
1324 case 'total_amount':
1327 // @todo add test like testPaymentTypeLabel & remove these lines as we can anticipate error will still be caught & handled.
1328 if (!CRM_Utils_Rule
::money($value)) {
1329 return civicrm_api3_create_error("$key not a valid amount: $value");
1334 if (!CRM_Utils_Rule
::currencyCode($value)) {
1335 return civicrm_api3_create_error("currency not a valid code: $value");
1340 // import contribution record according to select contact type
1341 // validate contact id and external identifier.
1342 $value[$key] = $mismatchContactType = $softCreditContactIds = '';
1343 if (isset($params[$key]) && is_array($params[$key])) {
1344 foreach ($params[$key] as $softKey => $softParam) {
1345 $contactId = $softParam['contact_id'] ??
NULL;
1346 $externalId = $softParam['external_identifier'] ??
NULL;
1347 $email = $softParam['email'] ??
NULL;
1348 if ($contactId ||
$externalId) {
1349 require_once 'CRM/Contact/DAO/Contact.php';
1350 $contact = new CRM_Contact_DAO_Contact();
1351 $contact->id
= $contactId;
1352 $contact->external_identifier
= $externalId;
1354 if (!$contact->find(TRUE)) {
1355 $field = $contactId ?
ts('Contact ID') : ts('External ID');
1356 $errorMsg = ts("Soft Credit %1 - %2 doesn't exist. Row was skipped.",
1357 [1 => $field, 2 => $contactId ?
$contactId : $externalId]);
1361 return civicrm_api3_create_error($errorMsg);
1364 // finally get soft credit contact id.
1365 $values[$key][$softKey] = $softParam;
1366 $values[$key][$softKey]['contact_id'] = $contact->id
;
1369 if (!CRM_Utils_Rule
::email($email)) {
1370 return civicrm_api3_create_error("Invalid email address $email provided for Soft Credit. Row was skipped");
1373 // get the contact id from duplicate contact rule, if more than one contact is returned
1374 // we should return error, since current interface allows only one-one mapping
1377 'contact_type' => $params['contact_type'],
1379 $checkDedupe = _civicrm_api3_deprecated_duplicate_formatted_contact($emailParams);
1380 if (!$checkDedupe['is_error']) {
1381 return civicrm_api3_create_error("Invalid email address(doesn't exist) $email for Soft Credit. Row was skipped");
1383 $matchingContactIds = explode(',', $checkDedupe['error_message']['params'][0]);
1384 if (count($matchingContactIds) > 1) {
1385 return civicrm_api3_create_error("Invalid email address(duplicate) $email for Soft Credit. Row was skipped");
1387 if (count($matchingContactIds) == 1) {
1388 $contactId = $matchingContactIds[0];
1389 unset($softParam['email']);
1390 $values[$key][$softKey] = $softParam +
['contact_id' => $contactId];
1397 case 'pledge_payment':
1400 // giving respect to pledge_payment flag.
1401 if (empty($params['pledge_payment'])) {
1405 // get total amount of from import fields
1406 $totalAmount = $params['total_amount'] ??
NULL;
1408 $onDuplicate = $params['onDuplicate'] ??
NULL;
1410 // we need to get contact id $contributionContactID to
1411 // retrieve pledge details as well as to validate pledge ID
1413 // first need to check for update mode
1414 if ($onDuplicate == CRM_Import_Parser
::DUPLICATE_UPDATE
&&
1415 ($params['contribution_id'] ||
$params['trxn_id'] ||
$params['invoice_id'])
1417 $contribution = new CRM_Contribute_DAO_Contribution();
1418 if ($params['contribution_id']) {
1419 $contribution->id
= $params['contribution_id'];
1421 elseif ($params['trxn_id']) {
1422 $contribution->trxn_id
= $params['trxn_id'];
1424 elseif ($params['invoice_id']) {
1425 $contribution->invoice_id
= $params['invoice_id'];
1428 if ($contribution->find(TRUE)) {
1429 $contributionContactID = $contribution->contact_id
;
1430 if (!$totalAmount) {
1431 $totalAmount = $contribution->total_amount
;
1435 return civicrm_api3_create_error('No match found for specified contact in pledge payment data. Row was skipped.');
1439 // first get the contact id for given contribution record.
1440 if (!empty($params['contribution_contact_id'])) {
1441 $contributionContactID = $params['contribution_contact_id'];
1443 elseif (!empty($params['external_identifier'])) {
1444 require_once 'CRM/Contact/DAO/Contact.php';
1445 $contact = new CRM_Contact_DAO_Contact();
1446 $contact->external_identifier
= $params['external_identifier'];
1447 if ($contact->find(TRUE)) {
1448 $contributionContactID = $params['contribution_contact_id'] = $values['contribution_contact_id'] = $contact->id
;
1451 return civicrm_api3_create_error('No match found for specified contact in pledge payment data. Row was skipped.');
1455 // we need to get contribution contact using de dupe
1456 $error = $this->checkContactDuplicate($params);
1458 if (isset($error['error_message']['params'][0])) {
1459 $matchedIDs = explode(',', $error['error_message']['params'][0]);
1461 // check if only one contact is found
1462 if (count($matchedIDs) > 1) {
1463 return civicrm_api3_create_error($error['error_message']['message']);
1465 $contributionContactID = $params['contribution_contact_id'] = $values['contribution_contact_id'] = $matchedIDs[0];
1468 return civicrm_api3_create_error('No match found for specified contact in contribution data. Row was skipped.');
1473 if (!empty($params['pledge_id'])) {
1474 if (CRM_Core_DAO
::getFieldValue('CRM_Pledge_DAO_Pledge', $params['pledge_id'], 'contact_id') != $contributionContactID) {
1475 return civicrm_api3_create_error('Invalid Pledge ID provided. Contribution row was skipped.');
1477 $values['pledge_id'] = $params['pledge_id'];
1480 // check if there are any pledge related to this contact, with payments pending or in progress
1481 require_once 'CRM/Pledge/BAO/Pledge.php';
1482 $pledgeDetails = CRM_Pledge_BAO_Pledge
::getContactPledges($contributionContactID);
1484 if (empty($pledgeDetails)) {
1485 return civicrm_api3_create_error('No open pledges found for this contact. Contribution row was skipped.');
1487 if (count($pledgeDetails) > 1) {
1488 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.');
1491 // this mean we have only one pending / in progress pledge
1492 $values['pledge_id'] = $pledgeDetails[0];
1495 // we need to check if oldest payment amount equal to contribution amount
1496 require_once 'CRM/Pledge/BAO/PledgePayment.php';
1497 $pledgePaymentDetails = CRM_Pledge_BAO_PledgePayment
::getOldestPledgePayment($values['pledge_id']);
1499 if ($pledgePaymentDetails['amount'] == $totalAmount) {
1500 $values['pledge_payment_id'] = $pledgePaymentDetails['id'];
1503 return civicrm_api3_create_error('Contribution and Pledge Payment amount mismatch for this record. Contribution row was skipped.');
1507 case 'contribution_campaign_id':
1508 if (empty(CRM_Core_DAO
::getFieldValue('CRM_Campaign_DAO_Campaign', $params['contribution_campaign_id']))) {
1509 return civicrm_api3_create_error('Invalid Campaign ID provided. Contribution row was skipped.');
1511 $values['contribution_campaign_id'] = $params['contribution_campaign_id'];
1515 // Hande name or label for fields with options.
1516 if (isset($fields[$key]) &&
1517 // Yay - just for a surprise we are inconsistent on whether we pass the pseudofield (payment_instrument)
1518 // or the field name (contribution_status_id)
1519 // @todo - payment_instrument is goneburger - now payment_instrument_id - how
1521 (!empty($fields[$key]['is_pseudofield_for']) ||
!empty($fields[$key]['pseudoconstant']))
1523 $realField = $fields[$key]['is_pseudofield_for'] ??
$key;
1524 $realFieldSpec = $fields[$realField];
1525 $values[$key] = $this->parsePseudoConstantField($value, $realFieldSpec);
1531 if (array_key_exists('note', $params)) {
1532 $values['note'] = $params['note'];
1536 // CRM_Contribute_BAO_Contribution::add() handles contribution_source
1537 // So, if $values contains contribution_source, convert it to source
1538 $changes = ['contribution_source' => 'source'];
1540 foreach ($changes as $orgVal => $changeVal) {
1541 if (isset($values[$orgVal])) {
1542 $values[$changeVal] = $values[$orgVal];
1543 unset($values[$orgVal]);
1552 * Get the civicrm_mapping_field appropriate layout for the mapper input.
1554 * The input looks something like ['street_address', 1]
1555 * and would be mapped to ['name' => 'street_address', 'location_type_id' =>
1558 * @param array $fieldMapping
1559 * @param int $mappingID
1560 * @param int $columnNumber
1563 * @throws \API_Exception
1565 public function getMappingFieldFromMapperInput(array $fieldMapping, int $mappingID, int $columnNumber): array {
1566 $isRelationshipField = preg_match('/\d*_a_b|b_a$/', $fieldMapping[0]);
1567 $fieldName = $isRelationshipField ?
$fieldMapping[1] : $fieldMapping[0];
1569 'name' => $fieldMapping[0],
1570 'mapping_id' => $mappingID,
1571 'column_number' => $columnNumber,
1572 // The name of the field to match the soft credit on is (crazily)
1573 // stored in 'contact_type'
1574 'contact_type' => $fieldMapping[1] ??
NULL,
1575 // We also store the field in a sensible key, even if it isn't saved sensibly.
1576 'soft_credit_match_field' => $fieldMapping[1] ??
NULL,
1577 // This field is actually not saved at all :-( It is lost each time.
1578 'soft_credit_type_id' => $fieldMapping[2] ??
NULL,