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 * Get information about the provided job.
48 * - id (generally the same as name)
51 * e.g. ['activity_import' => ['id' => 'activity_import', 'label' => ts('Activity Import'), 'name' => 'activity_import']]
55 public static function getUserJobInfo(): array {
57 'contribution_import' => [
58 'id' => 'contribution_import',
59 'name' => 'contribution_import',
60 'label' => ts('Contribution Import'),
66 * Contribution-specific result codes
67 * @see CRM_Import_Parser result code constants
69 const SOFT_CREDIT
= 512, SOFT_CREDIT_ERROR
= 1024, PLEDGE_PAYMENT
= 2048, PLEDGE_PAYMENT_ERROR
= 4096;
72 * Separator being used
75 protected $_separator;
78 * Total number of lines in file
81 protected $_lineCount;
84 * Running total number of valid soft credit rows
87 protected $_validSoftCreditRowCount;
90 * Running total number of invalid soft credit rows
93 protected $_invalidSoftCreditRowCount;
96 * Running total number of valid pledge payment rows
99 protected $_validPledgePaymentRowCount;
102 * Running total number of invalid pledge payment rows
105 protected $_invalidPledgePaymentRowCount;
108 * Array of pledge payment error lines, bounded by MAX_ERROR
111 protected $_pledgePaymentErrors;
114 * Array of pledge payment error lines, bounded by MAX_ERROR
117 protected $_softCreditErrors;
120 * Filename of pledge payment error data
124 protected $_pledgePaymentErrorsFileName;
127 * Filename of soft credit error data
131 protected $_softCreditErrorsFileName;
134 * Get the field mappings for the import.
136 * This is the same format as saved in civicrm_mapping_field except
137 * that location_type_id = 'Primary' rather than empty where relevant.
138 * Also 'im_provider_id' is mapped to the 'real' field name 'provider_id'
141 * @throws \API_Exception
143 protected function getFieldMappings(): array {
145 foreach ($this->getSubmittedValue('mapper') as $i => $mapperRow) {
146 $mappedField = $this->getMappingFieldFromMapperInput($mapperRow, 0, $i);
147 // Just for clarity since 0 is a pseudo-value
148 unset($mappedField['mapping_id']);
149 $mappedFields[] = $mappedField;
151 return $mappedFields;
155 * Get the required fields.
159 public function getRequiredFields(): array {
160 return ['id' => ts('Contribution ID'), ['financial_type_id' => ts('Financial Type'), 'total_amount' => ts('Total Amount')]];
164 * Transform the input parameters into the form handled by the input routine.
166 * @param array $values
167 * Input parameters as they come in from the datasource
168 * eg. ['Bob', 'Smith', 'bob@example.org', '123-456']
171 * Parameters mapped to CiviCRM fields based on the mapping. eg.
173 * 'total_amount' => '1230.99',
174 * 'financial_type_id' => 1,
175 * 'external_identifier' => 'abcd',
176 * 'soft_credit' => [3 => ['external_identifier' => '123', 'soft_credit_type_id' => 1]]
178 * @throws \API_Exception
180 public function getMappedRow(array $values): array {
182 foreach ($this->getFieldMappings() as $i => $mappedField) {
183 if ($mappedField['name'] === 'do_not_import' ||
!$mappedField['name']) {
186 if (!empty($mappedField['soft_credit_match_field'])) {
187 $params['soft_credit'][$i] = ['soft_credit_type_id' => $mappedField['soft_credit_type_id'], $mappedField['soft_credit_match_field'] => $values[$i]];
190 $params[$this->getFieldMetadata($mappedField['name'])['name']] = $this->getTransformedFieldValue($mappedField['name'], $values[$i]);
197 * @param string $name
200 * @param string $headerPattern
201 * @param string $dataPattern
203 public function addField($name, $title, $type = CRM_Utils_Type
::T_INT
, $headerPattern = '//', $dataPattern = '//') {
205 $this->_fields
['doNotImport'] = new CRM_Contribute_Import_Field($name, $title, $type, $headerPattern, $dataPattern);
208 $tempField = CRM_Contact_BAO_Contact
::importableFields('All', NULL);
209 if (!array_key_exists($name, $tempField)) {
210 $this->_fields
[$name] = new CRM_Contribute_Import_Field($name, $title, $type, $headerPattern, $dataPattern);
213 $this->_fields
[$name] = new CRM_Contact_Import_Field($name, $title, $type, $headerPattern, $dataPattern,
214 CRM_Utils_Array
::value('hasLocationType', $tempField[$name])
221 * The initializer code, called before the processing
223 public function init() {
224 // Force re-load of user job.
225 unset($this->userJob
);
226 $this->setFieldMetadata();
227 foreach ($this->getImportableFieldsMetadata() as $name => $field) {
228 $this->addField($name, $field['title'], $field['type'], $field['headerPattern'], $field['dataPattern']);
233 * Set field metadata.
235 protected function setFieldMetadata() {
236 if (empty($this->importableFieldsMetadata
)) {
237 $fields = CRM_Contribute_BAO_Contribution
::importableFields($this->getContactType(), FALSE);
239 $fields = array_merge($fields,
242 'title' => ts('Soft Credit'),
243 'softCredit' => TRUE,
244 'headerPattern' => '/Soft Credit/i',
246 'type' => CRM_Utils_Type
::T_STRING
,
251 // add pledge fields only if its is enabled
252 if (CRM_Core_Permission
::access('CiviPledge')) {
255 'title' => ts('Pledge ID'),
256 'headerPattern' => '/Pledge ID/i',
257 'name' => 'pledge_id',
258 'entity' => 'Pledge',
259 'type' => CRM_Utils_Type
::T_INT
,
264 $fields = array_merge($fields, $pledgeFields);
266 foreach ($fields as $name => $field) {
267 $fields[$name] = array_merge([
268 'type' => CRM_Utils_Type
::T_INT
,
269 'dataPattern' => '//',
270 'headerPattern' => '//',
273 $this->importableFieldsMetadata
= $fields;
278 * Handle the values in import mode.
280 * @param array $values
281 * The array of values belonging to this line.
283 public function import($values): void
{
284 $rowNumber = (int) ($values[array_key_last($values)]);
286 $params = $this->getMappedRow($values);
287 $formatted = array_merge(['version' => 3, 'skipRecentView' => TRUE, 'skipCleanMoney' => TRUE, 'contribution_id' => $params['id'] ??
NULL], $params);
289 if (isset($params['total_amount']) && $params['total_amount'] == 0) {
290 $params['total_amount'] = '0.00';
292 $this->formatInput($params, $formatted);
295 foreach ($params as $key => $field) {
296 if ($field == NULL ||
$field === '') {
299 $paramValues[$key] = $field;
302 //import contribution record according to select contact type
303 if ($this->isSkipDuplicates() &&
304 (!empty($paramValues['contribution_contact_id']) ||
!empty($paramValues['external_identifier']))
306 $paramValues['contact_type'] = $this->getContactType();
308 elseif ($this->isUpdateExisting() &&
309 (!empty($paramValues['contribution_id']) ||
!empty($values['trxn_id']) ||
!empty($paramValues['invoice_id']))
311 $paramValues['contact_type'] = $this->getContactType();
313 elseif (!empty($paramValues['pledge_payment'])) {
314 $paramValues['contact_type'] = $this->getContactType();
317 $formatError = $this->deprecatedFormatParams($paramValues, $formatted);
320 if (CRM_Utils_Array
::value('error_data', $formatError) == 'soft_credit') {
321 throw new CRM_Core_Exception('', self
::SOFT_CREDIT_ERROR
);
323 if (CRM_Utils_Array
::value('error_data', $formatError) == 'pledge_payment') {
324 throw new CRM_Core_Exception('', self
::PLEDGE_PAYMENT_ERROR
);
326 throw new CRM_Core_Exception('', CRM_Import_Parser
::ERROR
);
329 if ($this->isUpdateExisting()) {
330 //fix for CRM-2219 - Update Contribution
331 // onDuplicate == CRM_Import_Parser::DUPLICATE_UPDATE
332 if (!empty($paramValues['invoice_id']) ||
!empty($paramValues['trxn_id']) ||
!empty($paramValues['contribution_id'])) {
334 'id' => $paramValues['contribution_id'] ??
NULL,
335 'trxn_id' => $paramValues['trxn_id'] ??
NULL,
336 'invoice_id' => $paramValues['invoice_id'] ??
NULL,
338 $ids['contribution'] = CRM_Contribute_BAO_Contribution
::checkDuplicateIds($dupeIds);
340 if ($ids['contribution']) {
341 $formatted['id'] = $ids['contribution'];
343 if (!empty($paramValues['note'])) {
345 $contactID = CRM_Core_DAO
::getFieldValue('CRM_Contribute_DAO_Contribution', $ids['contribution'], 'contact_id');
346 $daoNote = new CRM_Core_BAO_Note();
347 $daoNote->entity_table
= 'civicrm_contribution';
348 $daoNote->entity_id
= $ids['contribution'];
349 if ($daoNote->find(TRUE)) {
350 $noteID['id'] = $daoNote->id
;
354 'entity_table' => 'civicrm_contribution',
355 'note' => $paramValues['note'],
356 'entity_id' => $ids['contribution'],
357 'contact_id' => $contactID,
359 CRM_Core_BAO_Note
::add($noteParams, $noteID);
360 unset($formatted['note']);
363 //need to check existing soft credit contribution, CRM-3968
364 if (!empty($formatted['soft_credit'])) {
366 'contact_id' => $formatted['soft_credit'],
367 'contribution_id' => $ids['contribution'],
370 //Delete all existing soft Contribution from contribution_soft table for pcp_id is_null
371 $existingSoftCredit = CRM_Contribute_BAO_ContributionSoft
::getSoftContribution($dupeSoftCredit['contribution_id']);
372 if (isset($existingSoftCredit['soft_credit']) && !empty($existingSoftCredit['soft_credit'])) {
373 foreach ($existingSoftCredit['soft_credit'] as $key => $existingSoftCreditValues) {
374 if (!empty($existingSoftCreditValues['soft_credit_id'])) {
375 civicrm_api3('ContributionSoft', 'delete', [
376 'id' => $existingSoftCreditValues['soft_credit_id'],
384 $formatted['id'] = $ids['contribution'];
386 $newContribution = civicrm_api3('contribution', 'create', $formatted);
387 $this->_newContributions
[] = $newContribution['id'];
389 //return soft valid since we need to show how soft credits were added
390 if (!empty($formatted['soft_credit'])) {
391 $this->setImportStatus($rowNumber, $this->getStatus(self
::SOFT_CREDIT
));
395 // process pledge payment assoc w/ the contribution
396 $this->processPledgePayments($formatted);
397 $this->setImportStatus($rowNumber, $this->getStatus(self
::PLEDGE_PAYMENT
));
401 'id' => 'Contribution ID',
402 'trxn_id' => 'Transaction ID',
403 'invoice_id' => 'Invoice ID',
405 foreach ($dupeIds as $k => $v) {
407 $errorMsg[] = "$labels[$k] $v";
410 $errorMsg = implode(' AND ', $errorMsg);
411 throw new CRM_Core_Exception('Matching Contribution record not found for ' . $errorMsg . '. Row was skipped.', CRM_Import_Parser
::ERROR
);
415 if (empty($formatted['contact_id'])) {
417 $error = $this->checkContactDuplicate($paramValues);
419 if (CRM_Core_Error
::isAPIError($error, CRM_Core_ERROR
::DUPLICATE_CONTACT
)) {
420 $matchedIDs = explode(',', $error['error_message']['params'][0]);
421 if (count($matchedIDs) > 1) {
422 throw new CRM_Core_Exception('Multiple matching contact records detected for this row. The contribution was not imported', CRM_Import_Parser
::ERROR
);
424 $cid = $matchedIDs[0];
425 $formatted['contact_id'] = $cid;
427 $newContribution = civicrm_api('contribution', 'create', $formatted);
428 if (civicrm_error($newContribution)) {
429 if (is_array($newContribution['error_message'])) {
430 if ($newContribution['error_message']['params'][0]) {
431 throw new CRM_Core_Exception($newContribution['error_message']['message'], CRM_Import_Parser
::DUPLICATE
);
435 throw new CRM_Core_Exception($newContribution['error_message'], CRM_Import_Parser
::ERROR
);
439 $this->_newContributions
[] = $newContribution['id'];
440 $formatted['contribution_id'] = $newContribution['id'];
442 //return soft valid since we need to show how soft credits were added
443 if (!empty($formatted['soft_credit'])) {
444 $this->setImportStatus($rowNumber, $this->getStatus(self
::SOFT_CREDIT
));
448 $this->processPledgePayments($formatted);
449 $this->setImportStatus($rowNumber, $this->getStatus(self
::PLEDGE_PAYMENT
));
453 // Using new Dedupe rule.
455 'contact_type' => $this->getContactType(),
456 'used' => 'Unsupervised',
458 $fieldsArray = CRM_Dedupe_BAO_DedupeRule
::dedupeRuleFields($ruleParams);
460 foreach ($fieldsArray as $value) {
461 if (array_key_exists(trim($value), $params)) {
462 $paramValue = $params[trim($value)];
463 if (is_array($paramValue)) {
464 $disp .= $params[trim($value)][0][trim($value)] . " ";
467 $disp .= $params[trim($value)] . " ";
472 if (!empty($params['external_identifier'])) {
474 $disp .= "AND {$params['external_identifier']}";
477 $disp = $params['external_identifier'];
480 $errorMessage = 'No matching Contact found for (' . $disp . ')';
481 throw new CRM_Core_Exception($errorMessage, CRM_Import_Parser
::ERROR
);
484 if (!empty($paramValues['external_identifier'])) {
485 $checkCid = new CRM_Contact_DAO_Contact();
486 $checkCid->external_identifier
= $paramValues['external_identifier'];
487 $checkCid->find(TRUE);
488 if ($checkCid->id
!= $formatted['contact_id']) {
489 $errorMessage = 'Mismatch of External ID:' . $paramValues['external_identifier'] . ' and Contact Id:' . $formatted['contact_id'];
490 throw new CRM_Core_Exception($errorMessage, CRM_Import_Parser
::ERROR
);
493 $newContribution = civicrm_api('contribution', 'create', $formatted);
494 if (civicrm_error($newContribution)) {
495 if (is_array($newContribution['error_message'])) {
496 if ($newContribution['error_message']['params'][0]) {
497 throw new CRM_Core_Exception('', CRM_Import_Parser
::DUPLICATE
);
501 throw new CRM_Core_Exception($newContribution['error_message'], CRM_Import_Parser
::ERROR
);
505 $this->_newContributions
[] = $newContribution['id'];
506 $formatted['contribution_id'] = $newContribution['id'];
508 //return soft valid since we need to show how soft credits were added
509 if (!empty($formatted['soft_credit'])) {
510 $this->setImportStatus($rowNumber, $this->getStatus(self
::SOFT_CREDIT
), '');
514 // process pledge payment assoc w/ the contribution
515 $this->processPledgePayments($formatted);
516 $this->setImportStatus($rowNumber, $this->getStatus(self
::PLEDGE_PAYMENT
));
520 catch (CRM_Core_Exception
$e) {
521 $this->setImportStatus($rowNumber, $this->getStatus($e->getErrorCode()), $e->getMessage());
526 * Get the status to record.
528 * @param int|null $code
532 protected function getStatus(?
int $code): string {
534 self
::SOFT_CREDIT_ERROR
=> 'soft_credit_error',
535 self
::PLEDGE_PAYMENT_ERROR
=> 'pledge_payment_error',
536 self
::SOFT_CREDIT
=> 'soft_credit_imported',
537 self
::PLEDGE_PAYMENT
=> 'pledge_payment_imported',
538 CRM_Import_Parser
::DUPLICATE
=> 'DUPLICATE',
539 CRM_Import_Parser
::VALID
=> 'IMPORTED',
541 return $errorMapping[$code] ??
'ERROR';
545 * Process pledge payments.
547 * @param array $formatted
549 private function processPledgePayments(array $formatted) {
550 if (!empty($formatted['pledge_payment_id']) && !empty($formatted['pledge_id'])) {
551 //get completed status
552 $completeStatusID = CRM_Core_PseudoConstant
::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
554 //need to update payment record to map contribution_id
555 CRM_Core_DAO
::setFieldValue('CRM_Pledge_DAO_PledgePayment', $formatted['pledge_payment_id'],
556 'contribution_id', $formatted['contribution_id']
559 CRM_Pledge_BAO_PledgePayment
::updatePledgePaymentStatus($formatted['pledge_id'],
560 [$formatted['pledge_payment_id']],
563 $formatted['total_amount']
569 * Get the array of successfully imported contribution id's
573 public function &getImportedContributions() {
574 return $this->_newContributions
;
578 * Format input params to suit api handling.
580 * Over time all the parts of deprecatedFormatParams
581 * and all the parts of the import function on this class that relate to
582 * reformatting input should be moved here and tests should be added in
583 * CRM_Contribute_Import_Parser_ContributionTest.
585 * @param array $params
586 * @param array $formatted
588 public function formatInput(&$params, &$formatted = []) {
589 foreach ($params as $key => $val) {
590 // @todo - call formatDateFields instead.
594 case 'pledge_payment':
595 $params[$key] = CRM_Utils_String
::strtobool($val);
604 * take the input parameter list as specified in the data model and
605 * convert it into the same format that we use in QF and BAO object
607 * @param array $params
608 * Associative array of property name/value
609 * pairs to insert in new contact.
610 * @param array $values
611 * The reformatted properties that we can use internally.
612 * @param bool $create
614 * @return array|CRM_Error
615 * @throws \CRM_Core_Exception
617 private function deprecatedFormatParams($params, &$values, $create = FALSE) {
618 require_once 'CRM/Utils/DeprecatedUtils.php';
619 // copy all the contribution fields as is
620 require_once 'api/v3/utils.php';
622 foreach ($params as $key => $value) {
623 // ignore empty values or empty arrays etc
624 if (CRM_Utils_System
::isNull($value)) {
630 if (!CRM_Utils_Rule
::integer($value)) {
631 return civicrm_api3_create_error("contact_id not valid: $value");
633 $dao = new CRM_Core_DAO();
635 $svq = $dao->singleValueQuery("SELECT is_deleted FROM civicrm_contact WHERE id = $value",
639 return civicrm_api3_create_error("Invalid Contact ID: There is no contact record with contact_id = $value.");
642 return civicrm_api3_create_error("Invalid Contact ID: contact_id $value is a soft-deleted contact.");
644 $values['contact_id'] = $value;
648 // import contribution record according to select contact type
649 require_once 'CRM/Contact/DAO/Contact.php';
650 $contactType = new CRM_Contact_DAO_Contact();
651 $contactId = $params['contribution_contact_id'] ??
NULL;
652 $externalId = $params['external_identifier'] ??
NULL;
653 $email = $params['email'] ??
NULL;
654 //when insert mode check contact id or external identifier
655 if ($contactId ||
$externalId) {
656 $contactType->id
= $contactId;
657 $contactType->external_identifier
= $externalId;
658 if ($contactType->find(TRUE)) {
659 if ($params['contact_type'] != $contactType->contact_type
) {
660 return civicrm_api3_create_error("Contact Type is wrong: $contactType->contact_type");
665 if (!CRM_Utils_Rule
::email($email)) {
666 return civicrm_api3_create_error("Invalid email address $email provided. Row was skipped");
669 // get the contact id from duplicate contact rule, if more than one contact is returned
670 // we should return error, since current interface allows only one-one mapping
673 'contact_type' => $params['contact_type'],
675 $checkDedupe = _civicrm_api3_deprecated_duplicate_formatted_contact($emailParams);
676 if (!$checkDedupe['is_error']) {
677 return civicrm_api3_create_error("Invalid email address(doesn't exist) $email. Row was skipped");
679 $matchingContactIds = explode(',', $checkDedupe['error_message']['params'][0]);
680 if (count($matchingContactIds) > 1) {
681 return civicrm_api3_create_error("Invalid email address(duplicate) $email. Row was skipped");
683 if (count($matchingContactIds) == 1) {
684 $params['contribution_contact_id'] = $matchingContactIds[0];
687 elseif (!empty($params['contribution_id']) ||
!empty($params['trxn_id']) ||
!empty($params['invoice_id'])) {
688 // when update mode check contribution id or trxn id or
690 $contactId = new CRM_Contribute_DAO_Contribution();
691 if (!empty($params['contribution_id'])) {
692 $contactId->id
= $params['contribution_id'];
694 elseif (!empty($params['trxn_id'])) {
695 $contactId->trxn_id
= $params['trxn_id'];
697 elseif (!empty($params['invoice_id'])) {
698 $contactId->invoice_id
= $params['invoice_id'];
700 if ($contactId->find(TRUE)) {
701 $contactType->id
= $contactId->contact_id
;
702 if ($contactType->find(TRUE)) {
703 if ($params['contact_type'] != $contactType->contact_type
) {
704 return civicrm_api3_create_error("Contact Type is wrong: $contactType->contact_type");
710 if ($this->isUpdateExisting()) {
711 return civicrm_api3_create_error("Empty Contribution and Invoice and Transaction ID. Row was skipped.");
717 // import contribution record according to select contact type
718 // validate contact id and external identifier.
719 foreach ($value as $softKey => $softParam) {
720 $values['soft_credit'][$softKey] = [
721 'contact_id' => $this->lookupMatchingContact($softParam),
722 'soft_credit_type_id' => $softParam['soft_credit_type_id'],
728 // get total amount of from import fields
729 $totalAmount = $params['total_amount'] ??
NULL;
730 // we need to get contact id $contributionContactID to
731 // retrieve pledge details as well as to validate pledge ID
733 // first need to check for update mode
734 if ($this->isUpdateExisting() &&
735 ($params['id'] ||
$params['trxn_id'] ||
$params['invoice_id'])
737 $contribution = new CRM_Contribute_DAO_Contribution();
738 if ($params['contribution_id']) {
739 $contribution->id
= $params['contribution_id'];
741 elseif ($params['trxn_id']) {
742 $contribution->trxn_id
= $params['trxn_id'];
744 elseif ($params['invoice_id']) {
745 $contribution->invoice_id
= $params['invoice_id'];
748 if ($contribution->find(TRUE)) {
749 $contributionContactID = $contribution->contact_id
;
751 $totalAmount = $contribution->total_amount
;
755 throw new CRM_Core_Exception('No match found for specified contact in pledge payment data. Row was skipped.');
759 // first get the contact id for given contribution record.
760 if (!empty($params['contribution_contact_id'])) {
761 $contributionContactID = $params['contribution_contact_id'];
763 elseif (!empty($params['external_identifier'])) {
764 require_once 'CRM/Contact/DAO/Contact.php';
765 $contact = new CRM_Contact_DAO_Contact();
766 $contact->external_identifier
= $params['external_identifier'];
767 if ($contact->find(TRUE)) {
768 $contributionContactID = $params['contribution_contact_id'] = $values['contribution_contact_id'] = $contact->id
;
771 return civicrm_api3_create_error('No match found for specified contact in pledge payment data. Row was skipped.');
775 // we need to get contribution contact using de dupe
776 $error = $this->checkContactDuplicate($params);
778 if (isset($error['error_message']['params'][0])) {
779 $matchedIDs = explode(',', $error['error_message']['params'][0]);
781 // check if only one contact is found
782 if (count($matchedIDs) > 1) {
783 return civicrm_api3_create_error($error['error_message']['message']);
785 $contributionContactID = $params['contribution_contact_id'] = $values['contribution_contact_id'] = $matchedIDs[0];
788 return civicrm_api3_create_error('No match found for specified contact in contribution data. Row was skipped.');
793 if (!empty($params['pledge_id'])) {
794 if (CRM_Core_DAO
::getFieldValue('CRM_Pledge_DAO_Pledge', $params['pledge_id'], 'contact_id') != $contributionContactID) {
795 return civicrm_api3_create_error('Invalid Pledge ID provided. Contribution row was skipped.');
797 $values['pledge_id'] = $params['pledge_id'];
800 // check if there are any pledge related to this contact, with payments pending or in progress
801 require_once 'CRM/Pledge/BAO/Pledge.php';
802 $pledgeDetails = CRM_Pledge_BAO_Pledge
::getContactPledges($contributionContactID);
804 if (empty($pledgeDetails)) {
805 return civicrm_api3_create_error('No open pledges found for this contact. Contribution row was skipped.');
807 if (count($pledgeDetails) > 1) {
808 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.');
811 // this mean we have only one pending / in progress pledge
812 $values['pledge_id'] = $pledgeDetails[0];
815 // we need to check if oldest payment amount equal to contribution amount
816 require_once 'CRM/Pledge/BAO/PledgePayment.php';
817 $pledgePaymentDetails = CRM_Pledge_BAO_PledgePayment
::getOldestPledgePayment($values['pledge_id']);
819 if ($pledgePaymentDetails['amount'] == $totalAmount) {
820 $values['pledge_payment_id'] = $pledgePaymentDetails['id'];
823 return civicrm_api3_create_error('Contribution and Pledge Payment amount mismatch for this record. Contribution row was skipped.');
827 case 'contribution_campaign_id':
828 if (empty(CRM_Core_DAO
::getFieldValue('CRM_Campaign_DAO_Campaign', $params['contribution_campaign_id']))) {
829 return civicrm_api3_create_error('Invalid Campaign ID provided. Contribution row was skipped.');
831 $values['contribution_campaign_id'] = $params['contribution_campaign_id'];
837 if (array_key_exists('note', $params)) {
838 $values['note'] = $params['note'];
842 // CRM_Contribute_BAO_Contribution::add() handles contribution_source
843 // So, if $values contains contribution_source, convert it to source
844 $changes = ['contribution_source' => 'source'];
846 foreach ($changes as $orgVal => $changeVal) {
847 if (isset($values[$orgVal])) {
848 $values[$changeVal] = $values[$orgVal];
849 unset($values[$orgVal]);
858 * Get the civicrm_mapping_field appropriate layout for the mapper input.
860 * The input looks something like ['street_address', 1]
861 * and would be mapped to ['name' => 'street_address', 'location_type_id' =>
864 * @param array $fieldMapping
865 * @param int $mappingID
866 * @param int $columnNumber
869 * @throws \API_Exception
871 public function getMappingFieldFromMapperInput(array $fieldMapping, int $mappingID, int $columnNumber): array {
873 'name' => $fieldMapping[0],
874 'mapping_id' => $mappingID,
875 'column_number' => $columnNumber,
876 // The name of the field to match the soft credit on is (crazily)
877 // stored in 'contact_type'
878 'contact_type' => $fieldMapping[1] ??
NULL,
879 // We also store the field in a sensible key, even if it isn't saved sensibly.
880 'soft_credit_match_field' => $fieldMapping[1] ??
NULL,
881 // This field is actually not saved at all :-( It is lost each time.
882 'soft_credit_type_id' => $fieldMapping[2] ??
NULL,
887 * Lookup matching contact.
889 * This looks up the matching contact from the contact id, external identifier
890 * or email. For the email a straight email search is done - this is equivalent
891 * to what happens on a dedupe rule lookup when the only field is 'email' - but
892 * we can't be sure the rule is 'just email' - and we are not collecting the
893 * fields for any other lookup in the case of soft credits (if we
894 * extend this function to main-contact-lookup we can handle full dedupe
895 * lookups - but note the error messages will need tweaking.
897 * @param array $params
902 * @throws \API_Exception
903 * @throws \CRM_Core_Exception
905 private function lookupMatchingContact(array $params): int {
906 $lookupField = !empty($params['contact_id']) ?
'contact_id' : (!empty($params['external_identifier']) ?
'external_identifier' : 'email');
907 if (empty($params['email'])) {
908 $contact = Contact
::get(FALSE)->addSelect('id')
909 ->addWhere($lookupField === 'contact_id' ?
'id' : $lookupField, '=', $params[$lookupField])
911 if (count($contact) !== 1) {
912 throw new CRM_Core_Exception(ts("Soft Credit %1 - %2 doesn't exist. Row was skipped.",
914 1 => $this->getFieldMetadata($lookupField),
915 2 => $params['contact_id'] ??
$params['external_identifier'],
918 return $contact->first()['id'];
921 if (!CRM_Utils_Rule
::email($params['email'])) {
922 throw new CRM_Core_Exception(ts('Invalid email address %1 provided for Soft Credit. Row was skipped'), [1 => $params['email']]);
924 $emails = Email
::get(FALSE)
925 ->addWhere('contact_id.is_deleted', '=', 0)
926 ->addWhere('contact_id.contact_type', '=', $this->getContactType())
927 ->addWhere('email', '=', $params['email'])
928 ->addSelect('contact_id')->execute();
929 if (count($emails) === 0) {
930 throw new CRM_Core_Exception(ts("Invalid email address(doesn't exist) %1 for Soft Credit. Row was skipped", [1 => $params['email']]));
932 if (count($emails) > 1) {
933 throw new CRM_Core_Exception(ts('Invalid email address(duplicate) %1 for Soft Credit. Row was skipped', [1 => $params['email']]));
935 return $emails->first()['contact_id'];
939 * @param array $mappedField
940 * Field detail as would be saved in field_mapping table
941 * or as returned from getMappingFieldFromMapperInput
944 * @throws \API_Exception
946 public function getMappedFieldLabel(array $mappedField): string {
947 if (empty($this->importableFieldsMetadata
)) {
948 $this->setFieldMetadata();
950 if ($mappedField['name'] === '') {
954 $title[] = $this->getFieldMetadata($mappedField['name'])['title'];
955 if ($mappedField['soft_credit_match_field']) {
956 $title[] = $this->getFieldMetadata($mappedField['soft_credit_match_field'])['title'];
958 if ($mappedField['soft_credit_type_id']) {
959 $title[] = CRM_Core_PseudoConstant
::getLabel('CRM_Contribute_BAO_ContributionSoft', 'soft_credit_type_id', $mappedField['soft_credit_type_id']);
962 return implode(' - ', $title);
966 * Get the metadata field for which importable fields does not key the actual field name.
970 protected function getOddlyMappedMetadataFields(): array {
971 $uniqueNames = ['contribution_id', 'contribution_contact_id', 'contribution_cancel_date', 'contribution_source', 'contribution_check_number'];
973 foreach ($uniqueNames as $name) {
974 $fields[$this->importableFieldsMetadata
[$name]['name']] = $name;
976 // Include the parent fields as they could be present if required for matching ...in theory.
977 return array_merge($fields, parent
::getOddlyMappedMetadataFields());