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
18 use Civi\Api4\Contact
;
22 * Class to parse contribution csv files.
24 class CRM_Contribute_Import_Parser_Contribution
extends CRM_Import_Parser
{
26 protected $_mapperKeys;
29 * Array of successfully imported contribution id's
33 protected $_newContributions;
40 public function __construct($mapperKeys = []) {
41 parent
::__construct();
42 $this->_mapperKeys
= $mapperKeys;
46 * Contribution-specific result codes
47 * @see CRM_Import_Parser result code constants
49 const SOFT_CREDIT
= 512, SOFT_CREDIT_ERROR
= 1024, PLEDGE_PAYMENT
= 2048, PLEDGE_PAYMENT_ERROR
= 4096;
52 * Separator being used
55 protected $_separator;
58 * Total number of lines in file
61 protected $_lineCount;
64 * Running total number of valid soft credit rows
67 protected $_validSoftCreditRowCount;
70 * Running total number of invalid soft credit rows
73 protected $_invalidSoftCreditRowCount;
76 * Running total number of valid pledge payment rows
79 protected $_validPledgePaymentRowCount;
82 * Running total number of invalid pledge payment rows
85 protected $_invalidPledgePaymentRowCount;
88 * Array of pledge payment error lines, bounded by MAX_ERROR
91 protected $_pledgePaymentErrors;
94 * Array of pledge payment error lines, bounded by MAX_ERROR
97 protected $_softCreditErrors;
100 * Filename of pledge payment error data
104 protected $_pledgePaymentErrorsFileName;
107 * Filename of soft credit error data
111 protected $_softCreditErrorsFileName;
114 * Whether the file has a column header or not
118 protected $_haveColumnHeader;
121 * @param string $fileName
122 * @param string $separator
124 * @param bool $skipColumnHeader
126 * @param int $contactType
127 * @param int $onDuplicate
128 * @param int $statusID
137 $skipColumnHeader = FALSE,
138 $mode = self
::MODE_PREVIEW
,
139 $contactType = self
::CONTACT_INDIVIDUAL
,
140 $onDuplicate = self
::DUPLICATE_SKIP
,
143 // Since $this->_contactType is still being called directly do a get call
144 // here to make sure it is instantiated.
145 $this->getContactType();
149 $this->_haveColumnHeader
= $skipColumnHeader;
151 $this->_lineCount
= $this->_validSoftCreditRowCount
= $this->_validPledgePaymentRowCount
= 0;
152 $this->_invalidRowCount
= $this->_validCount
= $this->_invalidSoftCreditRowCount
= $this->_invalidPledgePaymentRowCount
= 0;
153 $this->_totalCount
= 0;
156 $this->_warnings
= [];
157 $this->_pledgePaymentErrors
= [];
158 $this->_softCreditErrors
= [];
160 $this->progressImport($statusID);
161 $startTimestamp = $currTimestamp = $prevTimestamp = time();
164 if ($mode == self
::MODE_MAPFIELD
) {
168 $this->_activeFieldCount
= count($this->_activeFields
);
171 $dataSource = $this->getDataSourceObject();
172 $totalRowCount = $dataSource->getRowCount(['new']);
173 $dataSource->setStatuses(['new']);
175 while ($row = $dataSource->getRow()) {
176 $values = array_values($row);
179 $this->_totalCount++
;
181 if ($mode == self
::MODE_MAPFIELD
) {
182 $returnCode = CRM_Import_Parser
::VALID
;
184 // Note that import summary appears to be unused
185 elseif ($mode == self
::MODE_PREVIEW ||
$mode == self
::MODE_SUMMARY
) {
186 $returnCode = $this->summary($values);
188 elseif ($mode == self
::MODE_IMPORT
) {
189 $returnCode = $this->import($onDuplicate, $values);
190 if ($statusID && (($this->_lineCount %
50) == 0)) {
191 $prevTimestamp = $this->progressImport($statusID, FALSE, $startTimestamp, $prevTimestamp, $totalRowCount);
195 $returnCode = self
::ERROR
;
198 // note that a line could be valid but still produce a warning
199 if ($returnCode == self
::VALID
) {
200 $this->_validCount++
;
201 if ($mode == self
::MODE_MAPFIELD
) {
202 $this->_rows
[] = $values;
203 $this->_activeFieldCount
= max($this->_activeFieldCount
, count($values));
207 if ($returnCode == self
::SOFT_CREDIT
) {
208 $this->_validSoftCreditRowCount++
;
209 $this->_validCount++
;
210 if ($mode == self
::MODE_MAPFIELD
) {
211 $this->_rows
[] = $values;
212 $this->_activeFieldCount
= max($this->_activeFieldCount
, count($values));
216 if ($returnCode == self
::PLEDGE_PAYMENT
) {
217 $this->_validPledgePaymentRowCount++
;
218 $this->_validCount++
;
219 if ($mode == self
::MODE_MAPFIELD
) {
220 $this->_rows
[] = $values;
221 $this->_activeFieldCount
= max($this->_activeFieldCount
, count($values));
225 if ($returnCode == self
::ERROR
) {
226 $this->_invalidRowCount++
;
227 $recordNumber = $this->_lineCount
;
228 if ($this->_haveColumnHeader
) {
231 array_unshift($values, $recordNumber);
232 $this->_errors
[] = $values;
235 if ($returnCode == self
::PLEDGE_PAYMENT_ERROR
) {
236 $this->_invalidPledgePaymentRowCount++
;
237 $recordNumber = $this->_lineCount
;
238 if ($this->_haveColumnHeader
) {
241 array_unshift($values, $recordNumber);
242 $this->_pledgePaymentErrors
[] = $values;
245 if ($returnCode == self
::SOFT_CREDIT_ERROR
) {
246 $this->_invalidSoftCreditRowCount++
;
247 $recordNumber = $this->_lineCount
;
248 if ($this->_haveColumnHeader
) {
251 array_unshift($values, $recordNumber);
252 $this->_softCreditErrors
[] = $values;
255 if ($returnCode == self
::DUPLICATE
) {
256 $this->_duplicateCount++
;
257 $recordNumber = $this->_lineCount
;
258 if ($this->_haveColumnHeader
) {
261 array_unshift($values, $recordNumber);
262 $this->_duplicates
[] = $values;
263 if ($onDuplicate != self
::DUPLICATE_SKIP
) {
264 $this->_validCount++
;
269 if ($mode == self
::MODE_PREVIEW ||
$mode == self
::MODE_IMPORT
) {
270 $customHeaders = $mapper;
272 $customfields = CRM_Core_BAO_CustomField
::getFields('Contribution');
273 foreach ($customHeaders as $key => $value) {
274 if ($id = CRM_Core_BAO_CustomField
::getKeyID($value)) {
275 $customHeaders[$key] = $customfields[$id][0];
278 if ($this->_invalidRowCount
) {
279 // removed view url for invlaid contacts
280 $headers = array_merge([
284 $this->_errorFileName
= self
::errorFileName(self
::ERROR
);
285 self
::exportCSV($this->_errorFileName
, $headers, $this->_errors
);
288 if ($this->_invalidPledgePaymentRowCount
) {
289 // removed view url for invlaid contacts
290 $headers = array_merge([
294 $this->_pledgePaymentErrorsFileName
= self
::errorFileName(self
::PLEDGE_PAYMENT_ERROR
);
295 self
::exportCSV($this->_pledgePaymentErrorsFileName
, $headers, $this->_pledgePaymentErrors
);
298 if ($this->_invalidSoftCreditRowCount
) {
299 // removed view url for invlaid contacts
300 $headers = array_merge([
304 $this->_softCreditErrorsFileName
= self
::errorFileName(self
::SOFT_CREDIT_ERROR
);
305 self
::exportCSV($this->_softCreditErrorsFileName
, $headers, $this->_softCreditErrors
);
308 if ($this->_duplicateCount
) {
309 $headers = array_merge([
311 ts('View Contribution URL'),
314 $this->_duplicateFileName
= self
::errorFileName(self
::DUPLICATE
);
315 self
::exportCSV($this->_duplicateFileName
, $headers, $this->_duplicates
);
321 * Given a list of the importable field keys that the user has selected
322 * set the active fields array to this list
324 * @param array $fieldKeys mapped array of values
326 public function setActiveFields($fieldKeys) {
327 $this->_activeFieldCount
= count($fieldKeys);
328 foreach ($fieldKeys as $key) {
329 if (empty($this->_fields
[$key])) {
330 $this->_activeFields
[] = new CRM_Contribute_Import_Field('', ts('- do not import -'));
333 $this->_activeFields
[] = clone($this->_fields
[$key]);
339 * Get the field mappings for the import.
341 * This is the same format as saved in civicrm_mapping_field except
342 * that location_type_id = 'Primary' rather than empty where relevant.
343 * Also 'im_provider_id' is mapped to the 'real' field name 'provider_id'
346 * @throws \API_Exception
348 protected function getFieldMappings(): array {
350 foreach ($this->getSubmittedValue('mapper') as $i => $mapperRow) {
351 $mappedField = $this->getMappingFieldFromMapperInput($mapperRow, 0, $i);
352 // Just for clarity since 0 is a pseudo-value
353 unset($mappedField['mapping_id']);
354 $mappedFields[] = $mappedField;
356 return $mappedFields;
360 * Transform the input parameters into the form handled by the input routine.
362 * @param array $values
363 * Input parameters as they come in from the datasource
364 * eg. ['Bob', 'Smith', 'bob@example.org', '123-456']
367 * Parameters mapped to CiviCRM fields based on the mapping. eg.
369 * 'total_amount' => '1230.99',
370 * 'financial_type_id' => 1,
371 * 'external_identifier' => 'abcd',
372 * 'soft_credit' => [3 => ['external_identifier' => '123', 'soft_credit_type_id' => 1]]
374 * @throws \API_Exception
376 public function getMappedRow(array $values): array {
378 foreach ($this->getFieldMappings() as $i => $mappedField) {
379 if (!empty($mappedField['soft_credit_match_field'])) {
380 $params['soft_credit'][$i] = ['soft_credit_type_id' => $mappedField['soft_credit_type_id'], $mappedField['soft_credit_match_field'] => $values[$i]];
383 $params[$this->getFieldMetadata($mappedField['name'])['name']] = $this->getTransformedFieldValue($mappedField['name'], $values[$i]);
390 * @param string $name
393 * @param string $headerPattern
394 * @param string $dataPattern
396 public function addField($name, $title, $type = CRM_Utils_Type
::T_INT
, $headerPattern = '//', $dataPattern = '//') {
398 $this->_fields
['doNotImport'] = new CRM_Contribute_Import_Field($name, $title, $type, $headerPattern, $dataPattern);
401 $tempField = CRM_Contact_BAO_Contact
::importableFields('All', NULL);
402 if (!array_key_exists($name, $tempField)) {
403 $this->_fields
[$name] = new CRM_Contribute_Import_Field($name, $title, $type, $headerPattern, $dataPattern);
406 $this->_fields
[$name] = new CRM_Contact_Import_Field($name, $title, $type, $headerPattern, $dataPattern,
407 CRM_Utils_Array
::value('hasLocationType', $tempField[$name])
414 * Store parser values.
416 * @param CRM_Core_Session $store
420 public function set($store, $mode = self
::MODE_SUMMARY
) {
421 $store->set('totalRowCount', $this->_totalCount
);
422 $store->set('validRowCount', $this->_validCount
);
423 $store->set('invalidRowCount', $this->_invalidRowCount
);
425 $store->set('invalidSoftCreditRowCount', $this->_invalidSoftCreditRowCount
);
426 $store->set('validSoftCreditRowCount', $this->_validSoftCreditRowCount
);
427 $store->set('invalidPledgePaymentRowCount', $this->_invalidPledgePaymentRowCount
);
428 $store->set('validPledgePaymentRowCount', $this->_validPledgePaymentRowCount
);
430 switch ($this->_contactType
) {
432 $store->set('contactType', CRM_Import_Parser
::CONTACT_INDIVIDUAL
);
436 $store->set('contactType', CRM_Import_Parser
::CONTACT_HOUSEHOLD
);
440 $store->set('contactType', CRM_Import_Parser
::CONTACT_ORGANIZATION
);
443 if ($this->_invalidRowCount
) {
444 $store->set('errorsFileName', $this->_errorFileName
);
446 if (isset($this->_rows
) && !empty($this->_rows
)) {
447 $store->set('dataValues', $this->_rows
);
450 if ($this->_invalidPledgePaymentRowCount
) {
451 $store->set('pledgePaymentErrorsFileName', $this->_pledgePaymentErrorsFileName
);
454 if ($this->_invalidSoftCreditRowCount
) {
455 $store->set('softCreditErrorsFileName', $this->_softCreditErrorsFileName
);
458 if ($mode == self
::MODE_IMPORT
) {
459 $store->set('duplicateRowCount', $this->_duplicateCount
);
460 if ($this->_duplicateCount
) {
461 $store->set('duplicatesFileName', $this->_duplicateFileName
);
467 * Export data to a CSV file.
469 * @param string $fileName
470 * @param array $header
473 public static function exportCSV($fileName, $header, $data) {
475 $fd = fopen($fileName, 'w');
477 foreach ($header as $key => $value) {
478 $header[$key] = "\"$value\"";
480 $config = CRM_Core_Config
::singleton();
481 $output[] = implode($config->fieldSeparator
, $header);
483 foreach ($data as $datum) {
484 foreach ($datum as $key => $value) {
485 if (isset($value[0]) && is_array($value)) {
486 foreach ($value[0] as $k1 => $v1) {
487 if ($k1 == 'location_type_id') {
494 $datum[$key] = "\"$value\"";
497 $output[] = implode($config->fieldSeparator
, $datum);
499 fwrite($fd, implode("\n", $output));
504 * Determines the file extension based on error code.
507 * Error code constant.
511 public static function errorFileName($type) {
517 $config = CRM_Core_Config
::singleton();
518 $fileName = $config->uploadDir
. "sqlImport";
521 case self
::SOFT_CREDIT_ERROR
:
522 $fileName .= '.softCreditErrors';
525 case self
::PLEDGE_PAYMENT_ERROR
:
526 $fileName .= '.pledgePaymentErrors';
530 $fileName = parent
::errorFileName($type);
538 * Determines the file name based on error code.
541 * Error code constant.
545 public static function saveFileName($type) {
552 case self
::SOFT_CREDIT_ERROR
:
553 $fileName = 'Import_Soft_Credit_Errors.csv';
556 case self
::PLEDGE_PAYMENT_ERROR
:
557 $fileName = 'Import_Pledge_Payment_Errors.csv';
561 $fileName = parent
::saveFileName($type);
569 * The initializer code, called before the processing
571 public function init() {
572 $this->setFieldMetadata();
573 foreach ($this->getImportableFieldsMetadata() as $name => $field) {
574 $this->addField($name, $field['title'], $field['type'], $field['headerPattern'], $field['dataPattern']);
577 $this->_newContributions
= [];
579 $this->setActiveFields($this->_mapperKeys
);
583 * Set field metadata.
585 protected function setFieldMetadata() {
586 if (empty($this->importableFieldsMetadata
)) {
587 $fields = CRM_Contribute_BAO_Contribution
::importableFields($this->_contactType
, FALSE);
589 $fields = array_merge($fields,
592 'title' => ts('Soft Credit'),
593 'softCredit' => TRUE,
594 'headerPattern' => '/Soft Credit/i',
599 // add pledge fields only if its is enabled
600 if (CRM_Core_Permission
::access('CiviPledge')) {
602 'pledge_payment' => [
603 'title' => ts('Pledge Payment'),
604 'headerPattern' => '/Pledge Payment/i',
607 'title' => ts('Pledge ID'),
608 'headerPattern' => '/Pledge ID/i',
612 $fields = array_merge($fields, $pledgeFields);
614 foreach ($fields as $name => $field) {
615 $fields[$name] = array_merge([
616 'type' => CRM_Utils_Type
::T_INT
,
617 'dataPattern' => '//',
618 'headerPattern' => '//',
621 $this->importableFieldsMetadata
= $fields;
626 * Handle the values in summary mode.
628 * @param array $values
629 * The array of values belonging to this line.
632 * CRM_Import_Parser::VALID or CRM_Import_Parser::ERROR
634 public function summary(&$values) {
635 $rowNumber = (int) ($values[array_key_last($values)]);
636 $params = $this->getMappedRow($values);
637 $errorMessage = implode(';', $this->getInvalidValues($params));
638 $params['contact_type'] = 'Contribution';
641 $tempMsg = "Invalid value for field(s) : $errorMessage";
642 array_unshift($values, $tempMsg);
643 $errorMessage = NULL;
644 $this->setImportStatus($rowNumber, 'ERROR', $tempMsg);
645 return CRM_Import_Parser
::ERROR
;
648 return CRM_Import_Parser
::VALID
;
652 * Handle the values in import mode.
654 * @param int $onDuplicate
655 * The code for what action to take on duplicates.
656 * @param array $values
657 * The array of values belonging to this line.
660 * the result of this processing - one of
661 * - CRM_Import_Parser::VALID
662 * - CRM_Import_Parser::ERROR
663 * - CRM_Import_Parser::SOFT_CREDIT_ERROR
664 * - CRM_Import_Parser::PLEDGE_PAYMENT_ERROR
665 * - CRM_Import_Parser::DUPLICATE
666 * - CRM_Import_Parser::SOFT_CREDIT (successful creation)
667 * - CRM_Import_Parser::PLEDGE_PAYMENT (successful creation)
669 public function import($onDuplicate, &$values) {
670 $rowNumber = (int) ($values[array_key_last($values)]);
671 // first make sure this is a valid line
672 $response = $this->summary($values);
673 if ($response != CRM_Import_Parser
::VALID
) {
674 return CRM_Import_Parser
::ERROR
;
677 $params = $this->getMappedRow($values);
678 $formatted = array_merge(['version' => 3, 'skipRecentView' => TRUE, 'skipCleanMoney' => FALSE, 'contribution_id' => $params['id'] ??
NULL], $params);
680 if (isset($params['total_amount']) && $params['total_amount'] == 0) {
681 $params['total_amount'] = '0.00';
683 $this->formatInput($params, $formatted);
686 foreach ($params as $key => $field) {
687 if ($field == NULL ||
$field === '') {
690 $paramValues[$key] = $field;
693 //import contribution record according to select contact type
694 if ($onDuplicate == CRM_Import_Parser
::DUPLICATE_SKIP
&&
695 (!empty($paramValues['contribution_contact_id']) ||
!empty($paramValues['external_identifier']))
697 $paramValues['contact_type'] = $this->_contactType
;
699 elseif ($onDuplicate == CRM_Import_Parser
::DUPLICATE_UPDATE
&&
700 (!empty($paramValues['contribution_id']) ||
!empty($values['trxn_id']) ||
!empty($paramValues['invoice_id']))
702 $paramValues['contact_type'] = $this->_contactType
;
704 elseif (!empty($paramValues['pledge_payment'])) {
705 $paramValues['contact_type'] = $this->_contactType
;
708 //need to pass $onDuplicate to check import mode.
709 if (!empty($paramValues['pledge_payment'])) {
710 $paramValues['onDuplicate'] = $onDuplicate;
713 $formatError = $this->deprecatedFormatParams($paramValues, $formatted, TRUE, $onDuplicate);
715 catch (CRM_Core_Exception
$e) {
716 array_unshift($values, $e->getMessage());
717 $errorMapping = ['soft_credit' => self
::SOFT_CREDIT_ERROR
, 'pledge_payment' => self
::PLEDGE_PAYMENT_ERROR
];
718 $this->setImportStatus($rowNumber, $errorMapping[$e->getErrorCode()] ?? CRM_Import_Parser
::ERROR
, $e->getMessage());
719 return $errorMapping[$e->getErrorCode()] ?? CRM_Import_Parser
::ERROR
;
723 array_unshift($values, $formatError['error_message']);
724 if (CRM_Utils_Array
::value('error_data', $formatError) == 'soft_credit') {
725 return self
::SOFT_CREDIT_ERROR
;
727 if (CRM_Utils_Array
::value('error_data', $formatError) == 'pledge_payment') {
728 return self
::PLEDGE_PAYMENT_ERROR
;
730 $this->setImportStatus($rowNumber, 'ERROR', '');
731 return CRM_Import_Parser
::ERROR
;
734 if ($onDuplicate == CRM_Import_Parser
::DUPLICATE_UPDATE
) {
735 //fix for CRM-2219 - Update Contribution
736 // onDuplicate == CRM_Import_Parser::DUPLICATE_UPDATE
737 if (!empty($paramValues['invoice_id']) ||
!empty($paramValues['trxn_id']) ||
!empty($paramValues['contribution_id'])) {
739 'id' => $paramValues['contribution_id'] ??
NULL,
740 'trxn_id' => $paramValues['trxn_id'] ??
NULL,
741 'invoice_id' => $paramValues['invoice_id'] ??
NULL,
743 $ids['contribution'] = CRM_Contribute_BAO_Contribution
::checkDuplicateIds($dupeIds);
745 if ($ids['contribution']) {
746 $formatted['id'] = $ids['contribution'];
748 if (!empty($paramValues['note'])) {
750 $contactID = CRM_Core_DAO
::getFieldValue('CRM_Contribute_DAO_Contribution', $ids['contribution'], 'contact_id');
751 $daoNote = new CRM_Core_BAO_Note();
752 $daoNote->entity_table
= 'civicrm_contribution';
753 $daoNote->entity_id
= $ids['contribution'];
754 if ($daoNote->find(TRUE)) {
755 $noteID['id'] = $daoNote->id
;
759 'entity_table' => 'civicrm_contribution',
760 'note' => $paramValues['note'],
761 'entity_id' => $ids['contribution'],
762 'contact_id' => $contactID,
764 CRM_Core_BAO_Note
::add($noteParams, $noteID);
765 unset($formatted['note']);
768 //need to check existing soft credit contribution, CRM-3968
769 if (!empty($formatted['soft_credit'])) {
771 'contact_id' => $formatted['soft_credit'],
772 'contribution_id' => $ids['contribution'],
775 //Delete all existing soft Contribution from contribution_soft table for pcp_id is_null
776 $existingSoftCredit = CRM_Contribute_BAO_ContributionSoft
::getSoftContribution($dupeSoftCredit['contribution_id']);
777 if (isset($existingSoftCredit['soft_credit']) && !empty($existingSoftCredit['soft_credit'])) {
778 foreach ($existingSoftCredit['soft_credit'] as $key => $existingSoftCreditValues) {
779 if (!empty($existingSoftCreditValues['soft_credit_id'])) {
780 civicrm_api3('ContributionSoft', 'delete', [
781 'id' => $existingSoftCreditValues['soft_credit_id'],
789 $formatted['id'] = $ids['contribution'];
791 $newContribution = civicrm_api3('contribution', 'create', $formatted);
792 $this->_newContributions
[] = $newContribution['id'];
794 //return soft valid since we need to show how soft credits were added
795 if (!empty($formatted['soft_credit'])) {
796 return self
::SOFT_CREDIT
;
799 // process pledge payment assoc w/ the contribution
800 return $this->processPledgePayments($formatted);
803 'id' => 'Contribution ID',
804 'trxn_id' => 'Transaction ID',
805 'invoice_id' => 'Invoice ID',
807 foreach ($dupeIds as $k => $v) {
809 $errorMsg[] = "$labels[$k] $v";
812 $errorMsg = implode(' AND ', $errorMsg);
813 array_unshift($values, 'Matching Contribution record not found for ' . $errorMsg . '. Row was skipped.');
814 $this->setImportStatus($rowNumber, 'ERROR', 'Matching Contribution record not found for ' . $errorMsg . '. Row was skipped.');
815 return CRM_Import_Parser
::ERROR
;
819 if (empty($formatted['contact_id'])) {
821 $error = $this->checkContactDuplicate($paramValues);
823 if (CRM_Core_Error
::isAPIError($error, CRM_Core_ERROR
::DUPLICATE_CONTACT
)) {
824 $matchedIDs = explode(',', $error['error_message']['params'][0]);
825 if (count($matchedIDs) > 1) {
826 array_unshift($values, 'Multiple matching contact records detected for this row. The contribution was not imported');
827 $this->setImportStatus($rowNumber, 'ERROR', 'Multiple matching contact records detected for this row. The contribution was not imported');
828 return CRM_Import_Parser
::ERROR
;
830 $cid = $matchedIDs[0];
831 $formatted['contact_id'] = $cid;
833 $newContribution = civicrm_api('contribution', 'create', $formatted);
834 if (civicrm_error($newContribution)) {
835 if (is_array($newContribution['error_message'])) {
836 array_unshift($values, $newContribution['error_message']['message']);
837 if ($newContribution['error_message']['params'][0]) {
838 $this->setImportStatus($rowNumber, 'DUPLICATE', $newContribution['error_message']['message']);
839 return CRM_Import_Parser
::DUPLICATE
;
843 array_unshift($values, $newContribution['error_message']);
844 $this->setImportStatus($rowNumber, 'ERROR', $newContribution['error_message']);
845 return CRM_Import_Parser
::ERROR
;
849 $this->_newContributions
[] = $newContribution['id'];
850 $formatted['contribution_id'] = $newContribution['id'];
852 //return soft valid since we need to show how soft credits were added
853 if (!empty($formatted['soft_credit'])) {
854 return self
::SOFT_CREDIT
;
857 // process pledge payment assoc w/ the contribution
858 return $this->processPledgePayments($formatted);
861 // Using new Dedupe rule.
863 'contact_type' => $this->_contactType
,
864 'used' => 'Unsupervised',
866 $fieldsArray = CRM_Dedupe_BAO_DedupeRule
::dedupeRuleFields($ruleParams);
868 foreach ($fieldsArray as $value) {
869 if (array_key_exists(trim($value), $params)) {
870 $paramValue = $params[trim($value)];
871 if (is_array($paramValue)) {
872 $disp .= $params[trim($value)][0][trim($value)] . " ";
875 $disp .= $params[trim($value)] . " ";
880 if (!empty($params['external_identifier'])) {
882 $disp .= "AND {$params['external_identifier']}";
885 $disp = $params['external_identifier'];
888 $errorMessage = 'No matching Contact found for (' . $disp . ')';
889 $this->setImportStatus($rowNumber, 'ERROR', $errorMessage);
890 array_unshift($values, $errorMessage);
891 return CRM_Import_Parser
::ERROR
;
894 if (!empty($paramValues['external_identifier'])) {
895 $checkCid = new CRM_Contact_DAO_Contact();
896 $checkCid->external_identifier
= $paramValues['external_identifier'];
897 $checkCid->find(TRUE);
898 if ($checkCid->id
!= $formatted['contact_id']) {
899 $errorMessage = 'Mismatch of External ID:' . $paramValues['external_identifier'] . ' and Contact Id:' . $formatted['contact_id'];
900 array_unshift($values, $errorMessage);
901 $this->setImportStatus($rowNumber, 'ERROR', $errorMessage);
902 return CRM_Import_Parser
::ERROR
;
905 $newContribution = civicrm_api('contribution', 'create', $formatted);
906 if (civicrm_error($newContribution)) {
907 if (is_array($newContribution['error_message'])) {
908 array_unshift($values, $newContribution['error_message']['message']);
909 if ($newContribution['error_message']['params'][0]) {
910 $this->setImportStatus($rowNumber, 'DUPLICATE', '');
911 return CRM_Import_Parser
::DUPLICATE
;
915 array_unshift($values, $newContribution['error_message']);
916 $this->setImportStatus($rowNumber, 'ERROR', $newContribution['error_message']);
917 return CRM_Import_Parser
::ERROR
;
921 $this->_newContributions
[] = $newContribution['id'];
922 $formatted['contribution_id'] = $newContribution['id'];
924 //return soft valid since we need to show how soft credits were added
925 if (!empty($formatted['soft_credit'])) {
926 return self
::SOFT_CREDIT
;
929 // process pledge payment assoc w/ the contribution
930 return $this->processPledgePayments($formatted);
934 * Process pledge payments.
936 * @param array $formatted
940 private function processPledgePayments(array $formatted) {
941 if (!empty($formatted['pledge_payment_id']) && !empty($formatted['pledge_id'])) {
942 //get completed status
943 $completeStatusID = CRM_Core_PseudoConstant
::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
945 //need to update payment record to map contribution_id
946 CRM_Core_DAO
::setFieldValue('CRM_Pledge_DAO_PledgePayment', $formatted['pledge_payment_id'],
947 'contribution_id', $formatted['contribution_id']
950 CRM_Pledge_BAO_PledgePayment
::updatePledgePaymentStatus($formatted['pledge_id'],
951 [$formatted['pledge_payment_id']],
954 $formatted['total_amount']
957 return self
::PLEDGE_PAYMENT
;
962 * Get the array of successfully imported contribution id's
966 public function &getImportedContributions() {
967 return $this->_newContributions
;
971 * Format date fields from input to mysql.
973 * @param array $params
976 * Error messages, if any.
978 public function formatDateFields(&$params) {
980 $dateType = CRM_Core_Session
::singleton()->get('dateTypes');
981 foreach ($params as $key => $val) {
985 if ($dateValue = CRM_Utils_Date
::formatDate($params[$key], $dateType)) {
986 $params[$key] = $dateValue;
989 $errorMessage[] = ts('Receive Date');
994 if ($dateValue = CRM_Utils_Date
::formatDate($params[$key], $dateType)) {
995 $params[$key] = $dateValue;
998 $errorMessage[] = ts('Cancel Date');
1002 case 'receipt_date':
1003 if ($dateValue = CRM_Utils_Date
::formatDate($params[$key], $dateType)) {
1004 $params[$key] = $dateValue;
1007 $errorMessage[] = ts('Receipt date');
1011 case 'thankyou_date':
1012 if ($dateValue = CRM_Utils_Date
::formatDate($params[$key], $dateType)) {
1013 $params[$key] = $dateValue;
1016 $errorMessage[] = ts('Thankyou Date');
1022 return $errorMessage;
1026 * Format input params to suit api handling.
1028 * Over time all the parts of deprecatedFormatParams
1029 * and all the parts of the import function on this class that relate to
1030 * reformatting input should be moved here and tests should be added in
1031 * CRM_Contribute_Import_Parser_ContributionTest.
1033 * @param array $params
1034 * @param array $formatted
1036 public function formatInput(&$params, &$formatted = []) {
1037 $dateType = CRM_Core_Session
::singleton()->get('dateTypes');
1038 $customDataType = !empty($params['contact_type']) ?
$params['contact_type'] : 'Contribution';
1039 $customFields = CRM_Core_BAO_CustomField
::getFields($customDataType);
1040 // @todo call formatDateFields & move custom data handling there.
1041 // Also note error handling for dates is currently in deprecatedFormatParams
1042 // we should use the error handling in formatDateFields.
1043 foreach ($params as $key => $val) {
1044 // @todo - call formatDateFields instead.
1047 case 'receive_date':
1049 case 'receipt_date':
1050 case 'thankyou_date':
1051 $params[$key] = CRM_Utils_Date
::formatDate($params[$key], $dateType);
1054 case 'pledge_payment':
1055 $params[$key] = CRM_Utils_String
::strtobool($val);
1059 if ($customFieldID = CRM_Core_BAO_CustomField
::getKeyID($key)) {
1060 if ($customFields[$customFieldID]['data_type'] == 'Date') {
1061 CRM_Contact_Import_Parser_Contact
::formatCustomDate($params, $formatted, $dateType, $key);
1062 unset($params[$key]);
1064 elseif ($customFields[$customFieldID]['data_type'] == 'Boolean') {
1065 $params[$key] = CRM_Utils_String
::strtoboolstr($val);
1073 * take the input parameter list as specified in the data model and
1074 * convert it into the same format that we use in QF and BAO object
1076 * @param array $params
1077 * Associative array of property name/value
1078 * pairs to insert in new contact.
1079 * @param array $values
1080 * The reformatted properties that we can use internally.
1081 * @param bool $create
1082 * @param int $onDuplicate
1084 * @return array|CRM_Error
1085 * @throws \CRM_Core_Exception
1087 private function deprecatedFormatParams($params, &$values, $create = FALSE, $onDuplicate = NULL) {
1088 require_once 'CRM/Utils/DeprecatedUtils.php';
1089 // copy all the contribution fields as is
1090 require_once 'api/v3/utils.php';
1092 foreach ($params as $key => $value) {
1093 // ignore empty values or empty arrays etc
1094 if (CRM_Utils_System
::isNull($value)) {
1100 if (!CRM_Utils_Rule
::integer($value)) {
1101 return civicrm_api3_create_error("contact_id not valid: $value");
1103 $dao = new CRM_Core_DAO();
1105 $svq = $dao->singleValueQuery("SELECT is_deleted FROM civicrm_contact WHERE id = $value",
1109 return civicrm_api3_create_error("Invalid Contact ID: There is no contact record with contact_id = $value.");
1111 elseif ($svq == 1) {
1112 return civicrm_api3_create_error("Invalid Contact ID: contact_id $value is a soft-deleted contact.");
1114 $values['contact_id'] = $value;
1117 case 'contact_type':
1118 // import contribution record according to select contact type
1119 require_once 'CRM/Contact/DAO/Contact.php';
1120 $contactType = new CRM_Contact_DAO_Contact();
1121 $contactId = $params['contribution_contact_id'] ??
NULL;
1122 $externalId = $params['external_identifier'] ??
NULL;
1123 $email = $params['email'] ??
NULL;
1124 //when insert mode check contact id or external identifier
1125 if ($contactId ||
$externalId) {
1126 $contactType->id
= $contactId;
1127 $contactType->external_identifier
= $externalId;
1128 if ($contactType->find(TRUE)) {
1129 if ($params['contact_type'] != $contactType->contact_type
) {
1130 return civicrm_api3_create_error("Contact Type is wrong: $contactType->contact_type");
1135 if (!CRM_Utils_Rule
::email($email)) {
1136 return civicrm_api3_create_error("Invalid email address $email provided. Row was skipped");
1139 // get the contact id from duplicate contact rule, if more than one contact is returned
1140 // we should return error, since current interface allows only one-one mapping
1143 'contact_type' => $params['contact_type'],
1145 $checkDedupe = _civicrm_api3_deprecated_duplicate_formatted_contact($emailParams);
1146 if (!$checkDedupe['is_error']) {
1147 return civicrm_api3_create_error("Invalid email address(doesn't exist) $email. Row was skipped");
1149 $matchingContactIds = explode(',', $checkDedupe['error_message']['params'][0]);
1150 if (count($matchingContactIds) > 1) {
1151 return civicrm_api3_create_error("Invalid email address(duplicate) $email. Row was skipped");
1153 if (count($matchingContactIds) == 1) {
1154 $params['contribution_contact_id'] = $matchingContactIds[0];
1157 elseif (!empty($params['contribution_id']) ||
!empty($params['trxn_id']) ||
!empty($params['invoice_id'])) {
1158 // when update mode check contribution id or trxn id or
1160 $contactId = new CRM_Contribute_DAO_Contribution();
1161 if (!empty($params['contribution_id'])) {
1162 $contactId->id
= $params['contribution_id'];
1164 elseif (!empty($params['trxn_id'])) {
1165 $contactId->trxn_id
= $params['trxn_id'];
1167 elseif (!empty($params['invoice_id'])) {
1168 $contactId->invoice_id
= $params['invoice_id'];
1170 if ($contactId->find(TRUE)) {
1171 $contactType->id
= $contactId->contact_id
;
1172 if ($contactType->find(TRUE)) {
1173 if ($params['contact_type'] != $contactType->contact_type
) {
1174 return civicrm_api3_create_error("Contact Type is wrong: $contactType->contact_type");
1180 if ($onDuplicate == CRM_Import_Parser
::DUPLICATE_UPDATE
) {
1181 return civicrm_api3_create_error("Empty Contribution and Invoice and Transaction ID. Row was skipped.");
1186 case 'non_deductible_amount':
1187 case 'total_amount':
1190 // @todo add test like testPaymentTypeLabel & remove these lines as we can anticipate error will still be caught & handled.
1191 if (!CRM_Utils_Rule
::money($value)) {
1192 return civicrm_api3_create_error("$key not a valid amount: $value");
1197 if (!CRM_Utils_Rule
::currencyCode($value)) {
1198 return civicrm_api3_create_error("currency not a valid code: $value");
1203 // import contribution record according to select contact type
1204 // validate contact id and external identifier.
1205 foreach ($value as $softKey => $softParam) {
1206 $values['soft_credit'][$softKey] = [
1207 'contact_id' => $this->lookupMatchingContact($softParam),
1208 'soft_credit_type_id' => $softParam['soft_credit_type_id'],
1213 case 'pledge_payment':
1216 // giving respect to pledge_payment flag.
1217 if (empty($params['pledge_payment'])) {
1221 // get total amount of from import fields
1222 $totalAmount = $params['total_amount'] ??
NULL;
1224 $onDuplicate = $params['onDuplicate'] ??
NULL;
1226 // we need to get contact id $contributionContactID to
1227 // retrieve pledge details as well as to validate pledge ID
1229 // first need to check for update mode
1230 if ($onDuplicate == CRM_Import_Parser
::DUPLICATE_UPDATE
&&
1231 ($params['contribution_id'] ||
$params['trxn_id'] ||
$params['invoice_id'])
1233 $contribution = new CRM_Contribute_DAO_Contribution();
1234 if ($params['contribution_id']) {
1235 $contribution->id
= $params['contribution_id'];
1237 elseif ($params['trxn_id']) {
1238 $contribution->trxn_id
= $params['trxn_id'];
1240 elseif ($params['invoice_id']) {
1241 $contribution->invoice_id
= $params['invoice_id'];
1244 if ($contribution->find(TRUE)) {
1245 $contributionContactID = $contribution->contact_id
;
1246 if (!$totalAmount) {
1247 $totalAmount = $contribution->total_amount
;
1251 return civicrm_api3_create_error('No match found for specified contact in pledge payment data. Row was skipped.');
1255 // first get the contact id for given contribution record.
1256 if (!empty($params['contribution_contact_id'])) {
1257 $contributionContactID = $params['contribution_contact_id'];
1259 elseif (!empty($params['external_identifier'])) {
1260 require_once 'CRM/Contact/DAO/Contact.php';
1261 $contact = new CRM_Contact_DAO_Contact();
1262 $contact->external_identifier
= $params['external_identifier'];
1263 if ($contact->find(TRUE)) {
1264 $contributionContactID = $params['contribution_contact_id'] = $values['contribution_contact_id'] = $contact->id
;
1267 return civicrm_api3_create_error('No match found for specified contact in pledge payment data. Row was skipped.');
1271 // we need to get contribution contact using de dupe
1272 $error = $this->checkContactDuplicate($params);
1274 if (isset($error['error_message']['params'][0])) {
1275 $matchedIDs = explode(',', $error['error_message']['params'][0]);
1277 // check if only one contact is found
1278 if (count($matchedIDs) > 1) {
1279 return civicrm_api3_create_error($error['error_message']['message']);
1281 $contributionContactID = $params['contribution_contact_id'] = $values['contribution_contact_id'] = $matchedIDs[0];
1284 return civicrm_api3_create_error('No match found for specified contact in contribution data. Row was skipped.');
1289 if (!empty($params['pledge_id'])) {
1290 if (CRM_Core_DAO
::getFieldValue('CRM_Pledge_DAO_Pledge', $params['pledge_id'], 'contact_id') != $contributionContactID) {
1291 return civicrm_api3_create_error('Invalid Pledge ID provided. Contribution row was skipped.');
1293 $values['pledge_id'] = $params['pledge_id'];
1296 // check if there are any pledge related to this contact, with payments pending or in progress
1297 require_once 'CRM/Pledge/BAO/Pledge.php';
1298 $pledgeDetails = CRM_Pledge_BAO_Pledge
::getContactPledges($contributionContactID);
1300 if (empty($pledgeDetails)) {
1301 return civicrm_api3_create_error('No open pledges found for this contact. Contribution row was skipped.');
1303 if (count($pledgeDetails) > 1) {
1304 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.');
1307 // this mean we have only one pending / in progress pledge
1308 $values['pledge_id'] = $pledgeDetails[0];
1311 // we need to check if oldest payment amount equal to contribution amount
1312 require_once 'CRM/Pledge/BAO/PledgePayment.php';
1313 $pledgePaymentDetails = CRM_Pledge_BAO_PledgePayment
::getOldestPledgePayment($values['pledge_id']);
1315 if ($pledgePaymentDetails['amount'] == $totalAmount) {
1316 $values['pledge_payment_id'] = $pledgePaymentDetails['id'];
1319 return civicrm_api3_create_error('Contribution and Pledge Payment amount mismatch for this record. Contribution row was skipped.');
1323 case 'contribution_campaign_id':
1324 if (empty(CRM_Core_DAO
::getFieldValue('CRM_Campaign_DAO_Campaign', $params['contribution_campaign_id']))) {
1325 return civicrm_api3_create_error('Invalid Campaign ID provided. Contribution row was skipped.');
1327 $values['contribution_campaign_id'] = $params['contribution_campaign_id'];
1333 if (array_key_exists('note', $params)) {
1334 $values['note'] = $params['note'];
1338 // CRM_Contribute_BAO_Contribution::add() handles contribution_source
1339 // So, if $values contains contribution_source, convert it to source
1340 $changes = ['contribution_source' => 'source'];
1342 foreach ($changes as $orgVal => $changeVal) {
1343 if (isset($values[$orgVal])) {
1344 $values[$changeVal] = $values[$orgVal];
1345 unset($values[$orgVal]);
1354 * Get the civicrm_mapping_field appropriate layout for the mapper input.
1356 * The input looks something like ['street_address', 1]
1357 * and would be mapped to ['name' => 'street_address', 'location_type_id' =>
1360 * @param array $fieldMapping
1361 * @param int $mappingID
1362 * @param int $columnNumber
1365 * @throws \API_Exception
1367 public function getMappingFieldFromMapperInput(array $fieldMapping, int $mappingID, int $columnNumber): array {
1369 'name' => $fieldMapping[0],
1370 'mapping_id' => $mappingID,
1371 'column_number' => $columnNumber,
1372 // The name of the field to match the soft credit on is (crazily)
1373 // stored in 'contact_type'
1374 'contact_type' => $fieldMapping[1] ??
NULL,
1375 // We also store the field in a sensible key, even if it isn't saved sensibly.
1376 'soft_credit_match_field' => $fieldMapping[1] ??
NULL,
1377 // This field is actually not saved at all :-( It is lost each time.
1378 'soft_credit_type_id' => $fieldMapping[2] ??
NULL,
1383 * Lookup matching contact.
1385 * This looks up the matching contact from the contact id, external identifier
1386 * or email. For the email a straight email search is done - this is equivalent
1387 * to what happens on a dedupe rule lookup when the only field is 'email' - but
1388 * we can't be sure the rule is 'just email' - and we are not collecting the
1389 * fields for any other lookup in the case of soft credits (if we
1390 * extend this function to main-contact-lookup we can handle full dedupe
1391 * lookups - but note the error messages will need tweaking.
1393 * @param array $params
1398 * @throws \API_Exception
1399 * @throws \CRM_Core_Exception
1401 private function lookupMatchingContact(array $params): int {
1402 $lookupField = !empty($params['contact_id']) ?
'contact_id' : (!empty($params['external_identifier']) ?
'external_identifier' : 'email');
1403 if (empty($params['email'])) {
1404 $contact = Contact
::get(FALSE)->addSelect('id')
1405 ->addWhere($lookupField === 'contact_id' ?
'id' : $lookupField, '=', $params[$lookupField])
1407 if (count($contact) !== 1) {
1408 throw new CRM_Core_Exception(ts("Soft Credit %1 - %2 doesn't exist. Row was skipped.",
1410 1 => $this->getFieldMetadata($lookupField),
1411 2 => $params['contact_id'] ??
$params['external_identifier'],
1414 return $contact->first()['id'];
1417 if (!CRM_Utils_Rule
::email($params['email'])) {
1418 throw new CRM_Core_Exception(ts('Invalid email address %1 provided for Soft Credit. Row was skipped'), [1 => $params['email']]);
1420 $emails = Email
::get(FALSE)
1421 ->addWhere('contact_id.is_deleted', '=', 0)
1422 ->addWhere('contact_id.contact_type', '=', $this->getContactType())
1423 ->addWhere('email', '=', $params['email'])
1424 ->addSelect('contact_id')->execute();
1425 if (count($emails) === 0) {
1426 throw new CRM_Core_Exception(ts("Invalid email address(doesn't exist) %1 for Soft Credit. Row was skipped", [1 => $params['email']]));
1428 if (count($emails) > 1) {
1429 throw new CRM_Core_Exception(ts('Invalid email address(duplicate) %1 for Soft Credit. Row was skipped', [1 => $params['email']]));
1431 return $emails->first()['contact_id'];
1435 * @param array $mappedField
1436 * Field detail as would be saved in field_mapping table
1437 * or as returned from getMappingFieldFromMapperInput
1440 * @throws \API_Exception
1442 public function getMappedFieldLabel(array $mappedField): string {
1443 if (empty($this->importableFieldsMetadata
)) {
1444 $this->setFieldMetadata();
1447 $title[] = $this->getFieldMetadata($mappedField['name'])['title'];
1448 if ($mappedField['soft_credit_match_field']) {
1449 $title[] = $this->getFieldMetadata($mappedField['soft_credit_match_field'])['title'];
1451 if ($mappedField['soft_credit_type_id']) {
1452 $title[] = CRM_Core_PseudoConstant
::getLabel('CRM_Contribute_BAO_ContributionSoft', 'soft_credit_type_id', $mappedField['soft_credit_type_id']);
1455 return implode(' - ', $title);
1459 * Get the metadata field for which importable fields does not key the actual field name.
1463 protected function getOddlyMappedMetadataFields(): array {
1464 $uniqueNames = ['contribution_id', 'contribution_contact_id', 'contribution_cancel_date', 'contribution_source', 'contribution_check_number'];
1466 foreach ($uniqueNames as $name) {
1467 $fields[$this->importableFieldsMetadata
[$name]['name']] = $name;
1469 // Include the parent fields as they could be present if required for matching ...in theory.
1470 return array_merge($fields, parent
::getOddlyMappedMetadataFields());