Cleanup templates & form variables, following contact pattern
[civicrm-core.git] / CRM / Contribute / Import / Parser / Contribution.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 */
17
18 use Civi\Api4\Contact;
19 use Civi\Api4\Email;
20
21 /**
22 * Class to parse contribution csv files.
23 */
24 class CRM_Contribute_Import_Parser_Contribution extends CRM_Import_Parser {
25
26 protected $_mapperKeys;
27
28 /**
29 * Array of successfully imported contribution id's
30 *
31 * @var array
32 */
33 protected $_newContributions;
34
35 /**
36 * Class constructor.
37 *
38 * @param $mapperKeys
39 */
40 public function __construct($mapperKeys = []) {
41 parent::__construct();
42 $this->_mapperKeys = $mapperKeys;
43 }
44
45 /**
46 * Contribution-specific result codes
47 * @see CRM_Import_Parser result code constants
48 */
49 const SOFT_CREDIT = 512, SOFT_CREDIT_ERROR = 1024, PLEDGE_PAYMENT = 2048, PLEDGE_PAYMENT_ERROR = 4096;
50
51 /**
52 * Separator being used
53 * @var string
54 */
55 protected $_separator;
56
57 /**
58 * Total number of lines in file
59 * @var int
60 */
61 protected $_lineCount;
62
63 /**
64 * Running total number of valid soft credit rows
65 * @var int
66 */
67 protected $_validSoftCreditRowCount;
68
69 /**
70 * Running total number of invalid soft credit rows
71 * @var int
72 */
73 protected $_invalidSoftCreditRowCount;
74
75 /**
76 * Running total number of valid pledge payment rows
77 * @var int
78 */
79 protected $_validPledgePaymentRowCount;
80
81 /**
82 * Running total number of invalid pledge payment rows
83 * @var int
84 */
85 protected $_invalidPledgePaymentRowCount;
86
87 /**
88 * Array of pledge payment error lines, bounded by MAX_ERROR
89 * @var array
90 */
91 protected $_pledgePaymentErrors;
92
93 /**
94 * Array of pledge payment error lines, bounded by MAX_ERROR
95 * @var array
96 */
97 protected $_softCreditErrors;
98
99 /**
100 * Filename of pledge payment error data
101 *
102 * @var string
103 */
104 protected $_pledgePaymentErrorsFileName;
105
106 /**
107 * Filename of soft credit error data
108 *
109 * @var string
110 */
111 protected $_softCreditErrorsFileName;
112
113 /**
114 * Whether the file has a column header or not
115 *
116 * @var bool
117 */
118 protected $_haveColumnHeader;
119
120 /**
121 * @param string $fileName
122 * @param string $separator
123 * @param $mapper
124 * @param bool $skipColumnHeader
125 * @param int $mode
126 * @param int $contactType
127 * @param int $onDuplicate
128 * @param int $statusID
129 *
130 * @return mixed
131 * @throws Exception
132 */
133 public function run(
134 $fileName,
135 $separator,
136 $mapper,
137 $skipColumnHeader = FALSE,
138 $mode = self::MODE_PREVIEW,
139 $contactType = self::CONTACT_INDIVIDUAL,
140 $onDuplicate = self::DUPLICATE_SKIP,
141 $statusID = NULL
142 ) {
143 // Since $this->_contactType is still being called directly do a get call
144 // here to make sure it is instantiated.
145 $this->getContactType();
146
147 $this->init();
148
149 $this->_haveColumnHeader = $skipColumnHeader;
150
151 $this->_lineCount = $this->_validSoftCreditRowCount = $this->_validPledgePaymentRowCount = 0;
152 $this->_invalidRowCount = $this->_validCount = $this->_invalidSoftCreditRowCount = $this->_invalidPledgePaymentRowCount = 0;
153 $this->_totalCount = 0;
154
155 $this->_errors = [];
156 $this->_warnings = [];
157 $this->_pledgePaymentErrors = [];
158 $this->_softCreditErrors = [];
159 if ($statusID) {
160 $this->progressImport($statusID);
161 $startTimestamp = $currTimestamp = $prevTimestamp = time();
162 }
163
164 if ($mode == self::MODE_MAPFIELD) {
165 $this->_rows = [];
166 }
167 else {
168 $this->_activeFieldCount = count($this->_activeFields);
169 }
170
171 $dataSource = $this->getDataSourceObject();
172 $totalRowCount = $dataSource->getRowCount(['new']);
173 $dataSource->setStatuses(['new']);
174
175 while ($row = $dataSource->getRow()) {
176 $values = array_values($row);
177 $this->_lineCount++;
178
179 $this->_totalCount++;
180
181 if ($mode == self::MODE_MAPFIELD) {
182 $returnCode = CRM_Import_Parser::VALID;
183 }
184 // Note that import summary appears to be unused
185 elseif ($mode == self::MODE_PREVIEW || $mode == self::MODE_SUMMARY) {
186 $returnCode = $this->summary($values);
187 }
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);
192 }
193 }
194 else {
195 $returnCode = self::ERROR;
196 }
197
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));
204 }
205 }
206
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));
213 }
214 }
215
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));
222 }
223 }
224
225 if ($returnCode == self::ERROR) {
226 $this->_invalidRowCount++;
227 $recordNumber = $this->_lineCount;
228 if ($this->_haveColumnHeader) {
229 $recordNumber--;
230 }
231 array_unshift($values, $recordNumber);
232 $this->_errors[] = $values;
233 }
234
235 if ($returnCode == self::PLEDGE_PAYMENT_ERROR) {
236 $this->_invalidPledgePaymentRowCount++;
237 $recordNumber = $this->_lineCount;
238 if ($this->_haveColumnHeader) {
239 $recordNumber--;
240 }
241 array_unshift($values, $recordNumber);
242 $this->_pledgePaymentErrors[] = $values;
243 }
244
245 if ($returnCode == self::SOFT_CREDIT_ERROR) {
246 $this->_invalidSoftCreditRowCount++;
247 $recordNumber = $this->_lineCount;
248 if ($this->_haveColumnHeader) {
249 $recordNumber--;
250 }
251 array_unshift($values, $recordNumber);
252 $this->_softCreditErrors[] = $values;
253 }
254
255 if ($returnCode == self::DUPLICATE) {
256 $this->_duplicateCount++;
257 $recordNumber = $this->_lineCount;
258 if ($this->_haveColumnHeader) {
259 $recordNumber--;
260 }
261 array_unshift($values, $recordNumber);
262 $this->_duplicates[] = $values;
263 if ($onDuplicate != self::DUPLICATE_SKIP) {
264 $this->_validCount++;
265 }
266 }
267 }
268
269 if ($mode == self::MODE_PREVIEW || $mode == self::MODE_IMPORT) {
270 $customHeaders = $mapper;
271
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];
276 }
277 }
278 if ($this->_invalidRowCount) {
279 // removed view url for invlaid contacts
280 $headers = array_merge([
281 ts('Line Number'),
282 ts('Reason'),
283 ], $customHeaders);
284 $this->_errorFileName = self::errorFileName(self::ERROR);
285 self::exportCSV($this->_errorFileName, $headers, $this->_errors);
286 }
287
288 if ($this->_invalidPledgePaymentRowCount) {
289 // removed view url for invlaid contacts
290 $headers = array_merge([
291 ts('Line Number'),
292 ts('Reason'),
293 ], $customHeaders);
294 $this->_pledgePaymentErrorsFileName = self::errorFileName(self::PLEDGE_PAYMENT_ERROR);
295 self::exportCSV($this->_pledgePaymentErrorsFileName, $headers, $this->_pledgePaymentErrors);
296 }
297
298 if ($this->_invalidSoftCreditRowCount) {
299 // removed view url for invlaid contacts
300 $headers = array_merge([
301 ts('Line Number'),
302 ts('Reason'),
303 ], $customHeaders);
304 $this->_softCreditErrorsFileName = self::errorFileName(self::SOFT_CREDIT_ERROR);
305 self::exportCSV($this->_softCreditErrorsFileName, $headers, $this->_softCreditErrors);
306 }
307
308 if ($this->_duplicateCount) {
309 $headers = array_merge([
310 ts('Line Number'),
311 ts('View Contribution URL'),
312 ], $customHeaders);
313
314 $this->_duplicateFileName = self::errorFileName(self::DUPLICATE);
315 self::exportCSV($this->_duplicateFileName, $headers, $this->_duplicates);
316 }
317 }
318 }
319
320 /**
321 * Given a list of the importable field keys that the user has selected
322 * set the active fields array to this list
323 *
324 * @param array $fieldKeys mapped array of values
325 */
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 -'));
331 }
332 else {
333 $this->_activeFields[] = clone($this->_fields[$key]);
334 }
335 }
336 }
337
338 /**
339 * Get the field mappings for the import.
340 *
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'
344 *
345 * @return array
346 * @throws \API_Exception
347 */
348 protected function getFieldMappings(): array {
349 $mappedFields = [];
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;
355 }
356 return $mappedFields;
357 }
358
359 /**
360 * Transform the input parameters into the form handled by the input routine.
361 *
362 * @param array $values
363 * Input parameters as they come in from the datasource
364 * eg. ['Bob', 'Smith', 'bob@example.org', '123-456']
365 *
366 * @return array
367 * Parameters mapped to CiviCRM fields based on the mapping. eg.
368 * [
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]]
373 *
374 * @throws \API_Exception
375 */
376 public function getMappedRow(array $values): array {
377 $params = [];
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]];
381 }
382 else {
383 $params[$this->getFieldMetadata($mappedField['name'])['name']] = $values[$i];
384 }
385 }
386 return $params;
387 }
388
389 /**
390 * @param string $name
391 * @param $title
392 * @param int $type
393 * @param string $headerPattern
394 * @param string $dataPattern
395 */
396 public function addField($name, $title, $type = CRM_Utils_Type::T_INT, $headerPattern = '//', $dataPattern = '//') {
397 if (empty($name)) {
398 $this->_fields['doNotImport'] = new CRM_Contribute_Import_Field($name, $title, $type, $headerPattern, $dataPattern);
399 }
400 else {
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);
404 }
405 else {
406 $this->_fields[$name] = new CRM_Contact_Import_Field($name, $title, $type, $headerPattern, $dataPattern,
407 CRM_Utils_Array::value('hasLocationType', $tempField[$name])
408 );
409 }
410 }
411 }
412
413 /**
414 * Store parser values.
415 *
416 * @param CRM_Core_Session $store
417 *
418 * @param int $mode
419 */
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);
424
425 $store->set('invalidSoftCreditRowCount', $this->_invalidSoftCreditRowCount);
426 $store->set('validSoftCreditRowCount', $this->_validSoftCreditRowCount);
427 $store->set('invalidPledgePaymentRowCount', $this->_invalidPledgePaymentRowCount);
428 $store->set('validPledgePaymentRowCount', $this->_validPledgePaymentRowCount);
429
430 switch ($this->_contactType) {
431 case 'Individual':
432 $store->set('contactType', CRM_Import_Parser::CONTACT_INDIVIDUAL);
433 break;
434
435 case 'Household':
436 $store->set('contactType', CRM_Import_Parser::CONTACT_HOUSEHOLD);
437 break;
438
439 case 'Organization':
440 $store->set('contactType', CRM_Import_Parser::CONTACT_ORGANIZATION);
441 }
442
443 if ($this->_invalidRowCount) {
444 $store->set('errorsFileName', $this->_errorFileName);
445 }
446 if (isset($this->_rows) && !empty($this->_rows)) {
447 $store->set('dataValues', $this->_rows);
448 }
449
450 if ($this->_invalidPledgePaymentRowCount) {
451 $store->set('pledgePaymentErrorsFileName', $this->_pledgePaymentErrorsFileName);
452 }
453
454 if ($this->_invalidSoftCreditRowCount) {
455 $store->set('softCreditErrorsFileName', $this->_softCreditErrorsFileName);
456 }
457
458 if ($mode == self::MODE_IMPORT) {
459 $store->set('duplicateRowCount', $this->_duplicateCount);
460 if ($this->_duplicateCount) {
461 $store->set('duplicatesFileName', $this->_duplicateFileName);
462 }
463 }
464 }
465
466 /**
467 * Export data to a CSV file.
468 *
469 * @param string $fileName
470 * @param array $header
471 * @param array $data
472 */
473 public static function exportCSV($fileName, $header, $data) {
474 $output = [];
475 $fd = fopen($fileName, 'w');
476
477 foreach ($header as $key => $value) {
478 $header[$key] = "\"$value\"";
479 }
480 $config = CRM_Core_Config::singleton();
481 $output[] = implode($config->fieldSeparator, $header);
482
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') {
488 continue;
489 }
490 $datum[$k1] = $v1;
491 }
492 }
493 else {
494 $datum[$key] = "\"$value\"";
495 }
496 }
497 $output[] = implode($config->fieldSeparator, $datum);
498 }
499 fwrite($fd, implode("\n", $output));
500 fclose($fd);
501 }
502
503 /**
504 * Determines the file extension based on error code.
505 *
506 * @param int $type
507 * Error code constant.
508 *
509 * @return string
510 */
511 public static function errorFileName($type) {
512 $fileName = NULL;
513 if (empty($type)) {
514 return $fileName;
515 }
516
517 $config = CRM_Core_Config::singleton();
518 $fileName = $config->uploadDir . "sqlImport";
519
520 switch ($type) {
521 case self::SOFT_CREDIT_ERROR:
522 $fileName .= '.softCreditErrors';
523 break;
524
525 case self::PLEDGE_PAYMENT_ERROR:
526 $fileName .= '.pledgePaymentErrors';
527 break;
528
529 default:
530 $fileName = parent::errorFileName($type);
531 break;
532 }
533
534 return $fileName;
535 }
536
537 /**
538 * Determines the file name based on error code.
539 *
540 * @param int $type
541 * Error code constant.
542 *
543 * @return string
544 */
545 public static function saveFileName($type) {
546 $fileName = NULL;
547 if (empty($type)) {
548 return $fileName;
549 }
550
551 switch ($type) {
552 case self::SOFT_CREDIT_ERROR:
553 $fileName = 'Import_Soft_Credit_Errors.csv';
554 break;
555
556 case self::PLEDGE_PAYMENT_ERROR:
557 $fileName = 'Import_Pledge_Payment_Errors.csv';
558 break;
559
560 default:
561 $fileName = parent::saveFileName($type);
562 break;
563 }
564
565 return $fileName;
566 }
567
568 /**
569 * The initializer code, called before the processing
570 */
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']);
575 }
576
577 $this->_newContributions = [];
578
579 $this->setActiveFields($this->_mapperKeys);
580 }
581
582 /**
583 * Set field metadata.
584 */
585 protected function setFieldMetadata() {
586 if (empty($this->importableFieldsMetadata)) {
587 $fields = CRM_Contribute_BAO_Contribution::importableFields($this->_contactType, FALSE);
588
589 $fields = array_merge($fields,
590 [
591 'soft_credit' => [
592 'title' => ts('Soft Credit'),
593 'softCredit' => TRUE,
594 'headerPattern' => '/Soft Credit/i',
595 ],
596 ]
597 );
598
599 // add pledge fields only if its is enabled
600 if (CRM_Core_Permission::access('CiviPledge')) {
601 $pledgeFields = [
602 'pledge_payment' => [
603 'title' => ts('Pledge Payment'),
604 'headerPattern' => '/Pledge Payment/i',
605 ],
606 'pledge_id' => [
607 'title' => ts('Pledge ID'),
608 'headerPattern' => '/Pledge ID/i',
609 ],
610 ];
611
612 $fields = array_merge($fields, $pledgeFields);
613 }
614 foreach ($fields as $name => $field) {
615 $fields[$name] = array_merge([
616 'type' => CRM_Utils_Type::T_INT,
617 'dataPattern' => '//',
618 'headerPattern' => '//',
619 ], $field);
620 }
621 $this->importableFieldsMetadata = $fields;
622 }
623 }
624
625 /**
626 * Handle the values in summary mode.
627 *
628 * @param array $values
629 * The array of values belonging to this line.
630 *
631 * @return int
632 * CRM_Import_Parser::VALID or CRM_Import_Parser::ERROR
633 */
634 public function summary(&$values) {
635 $rowNumber = (int) ($values[array_key_last($values)]);
636 $params = $this->getMappedRow($values);
637
638 //for date-Formats
639 $errorMessage = implode('; ', $this->formatDateFields($params));
640 //date-Format part ends
641
642 $params['contact_type'] = 'Contribution';
643
644 //checking error in custom data
645 $this->isErrorInCustomData($params, $errorMessage);
646
647 if ($errorMessage) {
648 $tempMsg = "Invalid value for field(s) : $errorMessage";
649 array_unshift($values, $tempMsg);
650 $errorMessage = NULL;
651 $this->setImportStatus($rowNumber, 'ERROR', $tempMsg);
652 return CRM_Import_Parser::ERROR;
653 }
654
655 return CRM_Import_Parser::VALID;
656 }
657
658 /**
659 * Handle the values in import mode.
660 *
661 * @param int $onDuplicate
662 * The code for what action to take on duplicates.
663 * @param array $values
664 * The array of values belonging to this line.
665 *
666 * @return int
667 * the result of this processing - one of
668 * - CRM_Import_Parser::VALID
669 * - CRM_Import_Parser::ERROR
670 * - CRM_Import_Parser::SOFT_CREDIT_ERROR
671 * - CRM_Import_Parser::PLEDGE_PAYMENT_ERROR
672 * - CRM_Import_Parser::DUPLICATE
673 * - CRM_Import_Parser::SOFT_CREDIT (successful creation)
674 * - CRM_Import_Parser::PLEDGE_PAYMENT (successful creation)
675 */
676 public function import($onDuplicate, &$values) {
677 $rowNumber = (int) ($values[array_key_last($values)]);
678 // first make sure this is a valid line
679 $response = $this->summary($values);
680 if ($response != CRM_Import_Parser::VALID) {
681 return CRM_Import_Parser::ERROR;
682 }
683
684 $params = $this->getMappedRow($values);
685 $formatted = ['version' => 3, 'skipRecentView' => TRUE, 'skipCleanMoney' => FALSE, 'contribution_id' => $params['id'] ?? NULL];
686 //CRM-10994
687 if (isset($params['total_amount']) && $params['total_amount'] == 0) {
688 $params['total_amount'] = '0.00';
689 }
690 $this->formatInput($params, $formatted);
691
692 $paramValues = [];
693 foreach ($params as $key => $field) {
694 if ($field == NULL || $field === '') {
695 continue;
696 }
697 $paramValues[$key] = $field;
698 }
699
700 //import contribution record according to select contact type
701 if ($onDuplicate == CRM_Import_Parser::DUPLICATE_SKIP &&
702 (!empty($paramValues['contribution_contact_id']) || !empty($paramValues['external_identifier']))
703 ) {
704 $paramValues['contact_type'] = $this->_contactType;
705 }
706 elseif ($onDuplicate == CRM_Import_Parser::DUPLICATE_UPDATE &&
707 (!empty($paramValues['contribution_id']) || !empty($values['trxn_id']) || !empty($paramValues['invoice_id']))
708 ) {
709 $paramValues['contact_type'] = $this->_contactType;
710 }
711 elseif (!empty($paramValues['pledge_payment'])) {
712 $paramValues['contact_type'] = $this->_contactType;
713 }
714
715 //need to pass $onDuplicate to check import mode.
716 if (!empty($paramValues['pledge_payment'])) {
717 $paramValues['onDuplicate'] = $onDuplicate;
718 }
719 try {
720 $formatError = $this->deprecatedFormatParams($paramValues, $formatted, TRUE, $onDuplicate);
721 }
722 catch (CRM_Core_Exception $e) {
723 array_unshift($values, $e->getMessage());
724 $errorMapping = ['soft_credit' => self::SOFT_CREDIT_ERROR, 'pledge_payment' => self::PLEDGE_PAYMENT_ERROR];
725 $this->setImportStatus($rowNumber, $errorMapping[$e->getErrorCode()] ?? CRM_Import_Parser::ERROR, $e->getMessage());
726 return $errorMapping[$e->getErrorCode()] ?? CRM_Import_Parser::ERROR;
727 }
728
729 if ($formatError) {
730 array_unshift($values, $formatError['error_message']);
731 if (CRM_Utils_Array::value('error_data', $formatError) == 'soft_credit') {
732 return self::SOFT_CREDIT_ERROR;
733 }
734 if (CRM_Utils_Array::value('error_data', $formatError) == 'pledge_payment') {
735 return self::PLEDGE_PAYMENT_ERROR;
736 }
737 $this->setImportStatus($rowNumber, 'ERROR', '');
738 return CRM_Import_Parser::ERROR;
739 }
740
741 if ($onDuplicate == CRM_Import_Parser::DUPLICATE_UPDATE) {
742 //fix for CRM-2219 - Update Contribution
743 // onDuplicate == CRM_Import_Parser::DUPLICATE_UPDATE
744 if (!empty($paramValues['invoice_id']) || !empty($paramValues['trxn_id']) || !empty($paramValues['contribution_id'])) {
745 $dupeIds = [
746 'id' => $paramValues['contribution_id'] ?? NULL,
747 'trxn_id' => $paramValues['trxn_id'] ?? NULL,
748 'invoice_id' => $paramValues['invoice_id'] ?? NULL,
749 ];
750 $ids['contribution'] = CRM_Contribute_BAO_Contribution::checkDuplicateIds($dupeIds);
751
752 if ($ids['contribution']) {
753 $formatted['id'] = $ids['contribution'];
754 //process note
755 if (!empty($paramValues['note'])) {
756 $noteID = [];
757 $contactID = CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_Contribution', $ids['contribution'], 'contact_id');
758 $daoNote = new CRM_Core_BAO_Note();
759 $daoNote->entity_table = 'civicrm_contribution';
760 $daoNote->entity_id = $ids['contribution'];
761 if ($daoNote->find(TRUE)) {
762 $noteID['id'] = $daoNote->id;
763 }
764
765 $noteParams = [
766 'entity_table' => 'civicrm_contribution',
767 'note' => $paramValues['note'],
768 'entity_id' => $ids['contribution'],
769 'contact_id' => $contactID,
770 ];
771 CRM_Core_BAO_Note::add($noteParams, $noteID);
772 unset($formatted['note']);
773 }
774
775 //need to check existing soft credit contribution, CRM-3968
776 if (!empty($formatted['soft_credit'])) {
777 $dupeSoftCredit = [
778 'contact_id' => $formatted['soft_credit'],
779 'contribution_id' => $ids['contribution'],
780 ];
781
782 //Delete all existing soft Contribution from contribution_soft table for pcp_id is_null
783 $existingSoftCredit = CRM_Contribute_BAO_ContributionSoft::getSoftContribution($dupeSoftCredit['contribution_id']);
784 if (isset($existingSoftCredit['soft_credit']) && !empty($existingSoftCredit['soft_credit'])) {
785 foreach ($existingSoftCredit['soft_credit'] as $key => $existingSoftCreditValues) {
786 if (!empty($existingSoftCreditValues['soft_credit_id'])) {
787 civicrm_api3('ContributionSoft', 'delete', [
788 'id' => $existingSoftCreditValues['soft_credit_id'],
789 'pcp_id' => NULL,
790 ]);
791 }
792 }
793 }
794 }
795
796 $formatted['id'] = $ids['contribution'];
797
798 $newContribution = civicrm_api3('contribution', 'create', $formatted);
799 $this->_newContributions[] = $newContribution['id'];
800
801 //return soft valid since we need to show how soft credits were added
802 if (!empty($formatted['soft_credit'])) {
803 return self::SOFT_CREDIT;
804 }
805
806 // process pledge payment assoc w/ the contribution
807 return $this->processPledgePayments($formatted);
808 }
809 $labels = [
810 'id' => 'Contribution ID',
811 'trxn_id' => 'Transaction ID',
812 'invoice_id' => 'Invoice ID',
813 ];
814 foreach ($dupeIds as $k => $v) {
815 if ($v) {
816 $errorMsg[] = "$labels[$k] $v";
817 }
818 }
819 $errorMsg = implode(' AND ', $errorMsg);
820 array_unshift($values, 'Matching Contribution record not found for ' . $errorMsg . '. Row was skipped.');
821 $this->setImportStatus($rowNumber, 'ERROR', 'Matching Contribution record not found for ' . $errorMsg . '. Row was skipped.');
822 return CRM_Import_Parser::ERROR;
823 }
824 }
825
826 if (empty($formatted['contact_id'])) {
827
828 $error = $this->checkContactDuplicate($paramValues);
829
830 if (CRM_Core_Error::isAPIError($error, CRM_Core_ERROR::DUPLICATE_CONTACT)) {
831 $matchedIDs = explode(',', $error['error_message']['params'][0]);
832 if (count($matchedIDs) > 1) {
833 array_unshift($values, 'Multiple matching contact records detected for this row. The contribution was not imported');
834 $this->setImportStatus($rowNumber, 'ERROR', 'Multiple matching contact records detected for this row. The contribution was not imported');
835 return CRM_Import_Parser::ERROR;
836 }
837 $cid = $matchedIDs[0];
838 $formatted['contact_id'] = $cid;
839
840 $newContribution = civicrm_api('contribution', 'create', $formatted);
841 if (civicrm_error($newContribution)) {
842 if (is_array($newContribution['error_message'])) {
843 array_unshift($values, $newContribution['error_message']['message']);
844 if ($newContribution['error_message']['params'][0]) {
845 $this->setImportStatus($rowNumber, 'DUPLICATE', $newContribution['error_message']['message']);
846 return CRM_Import_Parser::DUPLICATE;
847 }
848 }
849 else {
850 array_unshift($values, $newContribution['error_message']);
851 $this->setImportStatus($rowNumber, 'ERROR', $newContribution['error_message']);
852 return CRM_Import_Parser::ERROR;
853 }
854 }
855
856 $this->_newContributions[] = $newContribution['id'];
857 $formatted['contribution_id'] = $newContribution['id'];
858
859 //return soft valid since we need to show how soft credits were added
860 if (!empty($formatted['soft_credit'])) {
861 return self::SOFT_CREDIT;
862 }
863
864 // process pledge payment assoc w/ the contribution
865 return $this->processPledgePayments($formatted);
866 }
867
868 // Using new Dedupe rule.
869 $ruleParams = [
870 'contact_type' => $this->_contactType,
871 'used' => 'Unsupervised',
872 ];
873 $fieldsArray = CRM_Dedupe_BAO_DedupeRule::dedupeRuleFields($ruleParams);
874 $disp = NULL;
875 foreach ($fieldsArray as $value) {
876 if (array_key_exists(trim($value), $params)) {
877 $paramValue = $params[trim($value)];
878 if (is_array($paramValue)) {
879 $disp .= $params[trim($value)][0][trim($value)] . " ";
880 }
881 else {
882 $disp .= $params[trim($value)] . " ";
883 }
884 }
885 }
886
887 if (!empty($params['external_identifier'])) {
888 if ($disp) {
889 $disp .= "AND {$params['external_identifier']}";
890 }
891 else {
892 $disp = $params['external_identifier'];
893 }
894 }
895 $errorMessage = 'No matching Contact found for (' . $disp . ')';
896 $this->setImportStatus($rowNumber, 'ERROR', $errorMessage);
897 array_unshift($values, $errorMessage);
898 return CRM_Import_Parser::ERROR;
899 }
900
901 if (!empty($paramValues['external_identifier'])) {
902 $checkCid = new CRM_Contact_DAO_Contact();
903 $checkCid->external_identifier = $paramValues['external_identifier'];
904 $checkCid->find(TRUE);
905 if ($checkCid->id != $formatted['contact_id']) {
906 $errorMessage = 'Mismatch of External ID:' . $paramValues['external_identifier'] . ' and Contact Id:' . $formatted['contact_id'];
907 array_unshift($values, $errorMessage);
908 $this->setImportStatus($rowNumber, 'ERROR', $errorMessage);
909 return CRM_Import_Parser::ERROR;
910 }
911 }
912 $newContribution = civicrm_api('contribution', 'create', $formatted);
913 if (civicrm_error($newContribution)) {
914 if (is_array($newContribution['error_message'])) {
915 array_unshift($values, $newContribution['error_message']['message']);
916 if ($newContribution['error_message']['params'][0]) {
917 $this->setImportStatus($rowNumber, 'DUPLICATE', '');
918 return CRM_Import_Parser::DUPLICATE;
919 }
920 }
921 else {
922 array_unshift($values, $newContribution['error_message']);
923 $this->setImportStatus($rowNumber, 'ERROR', $newContribution['error_message']);
924 return CRM_Import_Parser::ERROR;
925 }
926 }
927
928 $this->_newContributions[] = $newContribution['id'];
929 $formatted['contribution_id'] = $newContribution['id'];
930
931 //return soft valid since we need to show how soft credits were added
932 if (!empty($formatted['soft_credit'])) {
933 return self::SOFT_CREDIT;
934 }
935
936 // process pledge payment assoc w/ the contribution
937 return $this->processPledgePayments($formatted);
938 }
939
940 /**
941 * Process pledge payments.
942 *
943 * @param array $formatted
944 *
945 * @return int
946 */
947 private function processPledgePayments(array $formatted) {
948 if (!empty($formatted['pledge_payment_id']) && !empty($formatted['pledge_id'])) {
949 //get completed status
950 $completeStatusID = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
951
952 //need to update payment record to map contribution_id
953 CRM_Core_DAO::setFieldValue('CRM_Pledge_DAO_PledgePayment', $formatted['pledge_payment_id'],
954 'contribution_id', $formatted['contribution_id']
955 );
956
957 CRM_Pledge_BAO_PledgePayment::updatePledgePaymentStatus($formatted['pledge_id'],
958 [$formatted['pledge_payment_id']],
959 $completeStatusID,
960 NULL,
961 $formatted['total_amount']
962 );
963
964 return self::PLEDGE_PAYMENT;
965 }
966 }
967
968 /**
969 * Get the array of successfully imported contribution id's
970 *
971 * @return array
972 */
973 public function &getImportedContributions() {
974 return $this->_newContributions;
975 }
976
977 /**
978 * Format date fields from input to mysql.
979 *
980 * @param array $params
981 *
982 * @return array
983 * Error messages, if any.
984 */
985 public function formatDateFields(&$params) {
986 $errorMessage = [];
987 $dateType = CRM_Core_Session::singleton()->get('dateTypes');
988 foreach ($params as $key => $val) {
989 if ($val) {
990 switch ($key) {
991 case 'receive_date':
992 if ($dateValue = CRM_Utils_Date::formatDate($params[$key], $dateType)) {
993 $params[$key] = $dateValue;
994 }
995 else {
996 $errorMessage[] = ts('Receive Date');
997 }
998 break;
999
1000 case 'cancel_date':
1001 if ($dateValue = CRM_Utils_Date::formatDate($params[$key], $dateType)) {
1002 $params[$key] = $dateValue;
1003 }
1004 else {
1005 $errorMessage[] = ts('Cancel Date');
1006 }
1007 break;
1008
1009 case 'receipt_date':
1010 if ($dateValue = CRM_Utils_Date::formatDate($params[$key], $dateType)) {
1011 $params[$key] = $dateValue;
1012 }
1013 else {
1014 $errorMessage[] = ts('Receipt date');
1015 }
1016 break;
1017
1018 case 'thankyou_date':
1019 if ($dateValue = CRM_Utils_Date::formatDate($params[$key], $dateType)) {
1020 $params[$key] = $dateValue;
1021 }
1022 else {
1023 $errorMessage[] = ts('Thankyou Date');
1024 }
1025 break;
1026 }
1027 }
1028 }
1029 return $errorMessage;
1030 }
1031
1032 /**
1033 * Format input params to suit api handling.
1034 *
1035 * Over time all the parts of deprecatedFormatParams
1036 * and all the parts of the import function on this class that relate to
1037 * reformatting input should be moved here and tests should be added in
1038 * CRM_Contribute_Import_Parser_ContributionTest.
1039 *
1040 * @param array $params
1041 * @param array $formatted
1042 */
1043 public function formatInput(&$params, &$formatted = []) {
1044 $dateType = CRM_Core_Session::singleton()->get('dateTypes');
1045 $customDataType = !empty($params['contact_type']) ? $params['contact_type'] : 'Contribution';
1046 $customFields = CRM_Core_BAO_CustomField::getFields($customDataType);
1047 // @todo call formatDateFields & move custom data handling there.
1048 // Also note error handling for dates is currently in deprecatedFormatParams
1049 // we should use the error handling in formatDateFields.
1050 foreach ($params as $key => $val) {
1051 // @todo - call formatDateFields instead.
1052 if ($val) {
1053 switch ($key) {
1054 case 'receive_date':
1055 case 'cancel_date':
1056 case 'receipt_date':
1057 case 'thankyou_date':
1058 $params[$key] = CRM_Utils_Date::formatDate($params[$key], $dateType);
1059 break;
1060
1061 case 'pledge_payment':
1062 $params[$key] = CRM_Utils_String::strtobool($val);
1063 break;
1064
1065 }
1066 if ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) {
1067 if ($customFields[$customFieldID]['data_type'] == 'Date') {
1068 CRM_Contact_Import_Parser_Contact::formatCustomDate($params, $formatted, $dateType, $key);
1069 unset($params[$key]);
1070 }
1071 elseif ($customFields[$customFieldID]['data_type'] == 'Boolean') {
1072 $params[$key] = CRM_Utils_String::strtoboolstr($val);
1073 }
1074 }
1075 }
1076 }
1077 }
1078
1079 /**
1080 * take the input parameter list as specified in the data model and
1081 * convert it into the same format that we use in QF and BAO object
1082 *
1083 * @param array $params
1084 * Associative array of property name/value
1085 * pairs to insert in new contact.
1086 * @param array $values
1087 * The reformatted properties that we can use internally.
1088 * @param bool $create
1089 * @param int $onDuplicate
1090 *
1091 * @return array|CRM_Error
1092 * @throws \CRM_Core_Exception
1093 */
1094 private function deprecatedFormatParams($params, &$values, $create = FALSE, $onDuplicate = NULL) {
1095 require_once 'CRM/Utils/DeprecatedUtils.php';
1096 // copy all the contribution fields as is
1097 require_once 'api/v3/utils.php';
1098 $fields = CRM_Core_DAO::getExportableFieldsWithPseudoConstants('CRM_Contribute_BAO_Contribution');
1099
1100 _civicrm_api3_store_values($fields, $params, $values);
1101
1102 $customFields = CRM_Core_BAO_CustomField::getFields('Contribution', FALSE, FALSE, NULL, NULL, FALSE, FALSE, FALSE);
1103
1104 foreach ($params as $key => $value) {
1105 // ignore empty values or empty arrays etc
1106 if (CRM_Utils_System::isNull($value)) {
1107 continue;
1108 }
1109
1110 // Handling Custom Data
1111 if ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) {
1112 $values[$key] = $value;
1113 $type = $customFields[$customFieldID]['html_type'];
1114 if (CRM_Core_BAO_CustomField::isSerialized($customFields[$customFieldID])) {
1115 $values[$key] = self::unserializeCustomValue($customFieldID, $value, $type);
1116 }
1117 elseif ($type == 'Select' || $type == 'Radio' ||
1118 ($type == 'Autocomplete-Select' &&
1119 $customFields[$customFieldID]['data_type'] == 'String'
1120 )
1121 ) {
1122 $customOption = CRM_Core_BAO_CustomOption::getCustomOption($customFieldID, TRUE);
1123 foreach ($customOption as $customFldID => $customValue) {
1124 $val = $customValue['value'] ?? NULL;
1125 $label = $customValue['label'] ?? NULL;
1126 $label = strtolower($label);
1127 $value = strtolower(trim($value));
1128 if (($value == $label) || ($value == strtolower($val))) {
1129 $values[$key] = $val;
1130 }
1131 }
1132 }
1133 continue;
1134 }
1135
1136 switch ($key) {
1137 case 'contact_id':
1138 if (!CRM_Utils_Rule::integer($value)) {
1139 return civicrm_api3_create_error("contact_id not valid: $value");
1140 }
1141 $dao = new CRM_Core_DAO();
1142 $qParams = [];
1143 $svq = $dao->singleValueQuery("SELECT is_deleted FROM civicrm_contact WHERE id = $value",
1144 $qParams
1145 );
1146 if (!isset($svq)) {
1147 return civicrm_api3_create_error("Invalid Contact ID: There is no contact record with contact_id = $value.");
1148 }
1149 elseif ($svq == 1) {
1150 return civicrm_api3_create_error("Invalid Contact ID: contact_id $value is a soft-deleted contact.");
1151 }
1152 $values['contact_id'] = $value;
1153 break;
1154
1155 case 'contact_type':
1156 // import contribution record according to select contact type
1157 require_once 'CRM/Contact/DAO/Contact.php';
1158 $contactType = new CRM_Contact_DAO_Contact();
1159 $contactId = $params['contribution_contact_id'] ?? NULL;
1160 $externalId = $params['external_identifier'] ?? NULL;
1161 $email = $params['email'] ?? NULL;
1162 //when insert mode check contact id or external identifier
1163 if ($contactId || $externalId) {
1164 $contactType->id = $contactId;
1165 $contactType->external_identifier = $externalId;
1166 if ($contactType->find(TRUE)) {
1167 if ($params['contact_type'] != $contactType->contact_type) {
1168 return civicrm_api3_create_error("Contact Type is wrong: $contactType->contact_type");
1169 }
1170 }
1171 }
1172 elseif ($email) {
1173 if (!CRM_Utils_Rule::email($email)) {
1174 return civicrm_api3_create_error("Invalid email address $email provided. Row was skipped");
1175 }
1176
1177 // get the contact id from duplicate contact rule, if more than one contact is returned
1178 // we should return error, since current interface allows only one-one mapping
1179 $emailParams = [
1180 'email' => $email,
1181 'contact_type' => $params['contact_type'],
1182 ];
1183 $checkDedupe = _civicrm_api3_deprecated_duplicate_formatted_contact($emailParams);
1184 if (!$checkDedupe['is_error']) {
1185 return civicrm_api3_create_error("Invalid email address(doesn't exist) $email. Row was skipped");
1186 }
1187 $matchingContactIds = explode(',', $checkDedupe['error_message']['params'][0]);
1188 if (count($matchingContactIds) > 1) {
1189 return civicrm_api3_create_error("Invalid email address(duplicate) $email. Row was skipped");
1190 }
1191 if (count($matchingContactIds) == 1) {
1192 $params['contribution_contact_id'] = $matchingContactIds[0];
1193 }
1194 }
1195 elseif (!empty($params['contribution_id']) || !empty($params['trxn_id']) || !empty($params['invoice_id'])) {
1196 // when update mode check contribution id or trxn id or
1197 // invoice id
1198 $contactId = new CRM_Contribute_DAO_Contribution();
1199 if (!empty($params['contribution_id'])) {
1200 $contactId->id = $params['contribution_id'];
1201 }
1202 elseif (!empty($params['trxn_id'])) {
1203 $contactId->trxn_id = $params['trxn_id'];
1204 }
1205 elseif (!empty($params['invoice_id'])) {
1206 $contactId->invoice_id = $params['invoice_id'];
1207 }
1208 if ($contactId->find(TRUE)) {
1209 $contactType->id = $contactId->contact_id;
1210 if ($contactType->find(TRUE)) {
1211 if ($params['contact_type'] != $contactType->contact_type) {
1212 return civicrm_api3_create_error("Contact Type is wrong: $contactType->contact_type");
1213 }
1214 }
1215 }
1216 }
1217 else {
1218 if ($onDuplicate == CRM_Import_Parser::DUPLICATE_UPDATE) {
1219 return civicrm_api3_create_error("Empty Contribution and Invoice and Transaction ID. Row was skipped.");
1220 }
1221 }
1222 break;
1223
1224 case 'receive_date':
1225 case 'cancel_date':
1226 case 'receipt_date':
1227 case 'thankyou_date':
1228 if (!CRM_Utils_Rule::dateTime($value)) {
1229 return civicrm_api3_create_error("$key not a valid date: $value");
1230 }
1231 break;
1232
1233 case 'non_deductible_amount':
1234 case 'total_amount':
1235 case 'fee_amount':
1236 case 'net_amount':
1237 // @todo add test like testPaymentTypeLabel & remove these lines as we can anticipate error will still be caught & handled.
1238 if (!CRM_Utils_Rule::money($value)) {
1239 return civicrm_api3_create_error("$key not a valid amount: $value");
1240 }
1241 break;
1242
1243 case 'currency':
1244 if (!CRM_Utils_Rule::currencyCode($value)) {
1245 return civicrm_api3_create_error("currency not a valid code: $value");
1246 }
1247 break;
1248
1249 case 'soft_credit':
1250 // import contribution record according to select contact type
1251 // validate contact id and external identifier.
1252 foreach ($value as $softKey => $softParam) {
1253 $values['soft_credit'][$softKey] = [
1254 'contact_id' => $this->lookupMatchingContact($softParam),
1255 'soft_credit_type_id' => $softParam['soft_credit_type_id'],
1256 ];
1257 }
1258 break;
1259
1260 case 'pledge_payment':
1261 case 'pledge_id':
1262
1263 // giving respect to pledge_payment flag.
1264 if (empty($params['pledge_payment'])) {
1265 break;
1266 }
1267
1268 // get total amount of from import fields
1269 $totalAmount = $params['total_amount'] ?? NULL;
1270
1271 $onDuplicate = $params['onDuplicate'] ?? NULL;
1272
1273 // we need to get contact id $contributionContactID to
1274 // retrieve pledge details as well as to validate pledge ID
1275
1276 // first need to check for update mode
1277 if ($onDuplicate == CRM_Import_Parser::DUPLICATE_UPDATE &&
1278 ($params['contribution_id'] || $params['trxn_id'] || $params['invoice_id'])
1279 ) {
1280 $contribution = new CRM_Contribute_DAO_Contribution();
1281 if ($params['contribution_id']) {
1282 $contribution->id = $params['contribution_id'];
1283 }
1284 elseif ($params['trxn_id']) {
1285 $contribution->trxn_id = $params['trxn_id'];
1286 }
1287 elseif ($params['invoice_id']) {
1288 $contribution->invoice_id = $params['invoice_id'];
1289 }
1290
1291 if ($contribution->find(TRUE)) {
1292 $contributionContactID = $contribution->contact_id;
1293 if (!$totalAmount) {
1294 $totalAmount = $contribution->total_amount;
1295 }
1296 }
1297 else {
1298 return civicrm_api3_create_error('No match found for specified contact in pledge payment data. Row was skipped.');
1299 }
1300 }
1301 else {
1302 // first get the contact id for given contribution record.
1303 if (!empty($params['contribution_contact_id'])) {
1304 $contributionContactID = $params['contribution_contact_id'];
1305 }
1306 elseif (!empty($params['external_identifier'])) {
1307 require_once 'CRM/Contact/DAO/Contact.php';
1308 $contact = new CRM_Contact_DAO_Contact();
1309 $contact->external_identifier = $params['external_identifier'];
1310 if ($contact->find(TRUE)) {
1311 $contributionContactID = $params['contribution_contact_id'] = $values['contribution_contact_id'] = $contact->id;
1312 }
1313 else {
1314 return civicrm_api3_create_error('No match found for specified contact in pledge payment data. Row was skipped.');
1315 }
1316 }
1317 else {
1318 // we need to get contribution contact using de dupe
1319 $error = $this->checkContactDuplicate($params);
1320
1321 if (isset($error['error_message']['params'][0])) {
1322 $matchedIDs = explode(',', $error['error_message']['params'][0]);
1323
1324 // check if only one contact is found
1325 if (count($matchedIDs) > 1) {
1326 return civicrm_api3_create_error($error['error_message']['message']);
1327 }
1328 $contributionContactID = $params['contribution_contact_id'] = $values['contribution_contact_id'] = $matchedIDs[0];
1329 }
1330 else {
1331 return civicrm_api3_create_error('No match found for specified contact in contribution data. Row was skipped.');
1332 }
1333 }
1334 }
1335
1336 if (!empty($params['pledge_id'])) {
1337 if (CRM_Core_DAO::getFieldValue('CRM_Pledge_DAO_Pledge', $params['pledge_id'], 'contact_id') != $contributionContactID) {
1338 return civicrm_api3_create_error('Invalid Pledge ID provided. Contribution row was skipped.');
1339 }
1340 $values['pledge_id'] = $params['pledge_id'];
1341 }
1342 else {
1343 // check if there are any pledge related to this contact, with payments pending or in progress
1344 require_once 'CRM/Pledge/BAO/Pledge.php';
1345 $pledgeDetails = CRM_Pledge_BAO_Pledge::getContactPledges($contributionContactID);
1346
1347 if (empty($pledgeDetails)) {
1348 return civicrm_api3_create_error('No open pledges found for this contact. Contribution row was skipped.');
1349 }
1350 if (count($pledgeDetails) > 1) {
1351 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.');
1352 }
1353
1354 // this mean we have only one pending / in progress pledge
1355 $values['pledge_id'] = $pledgeDetails[0];
1356 }
1357
1358 // we need to check if oldest payment amount equal to contribution amount
1359 require_once 'CRM/Pledge/BAO/PledgePayment.php';
1360 $pledgePaymentDetails = CRM_Pledge_BAO_PledgePayment::getOldestPledgePayment($values['pledge_id']);
1361
1362 if ($pledgePaymentDetails['amount'] == $totalAmount) {
1363 $values['pledge_payment_id'] = $pledgePaymentDetails['id'];
1364 }
1365 else {
1366 return civicrm_api3_create_error('Contribution and Pledge Payment amount mismatch for this record. Contribution row was skipped.');
1367 }
1368 break;
1369
1370 case 'contribution_campaign_id':
1371 if (empty(CRM_Core_DAO::getFieldValue('CRM_Campaign_DAO_Campaign', $params['contribution_campaign_id']))) {
1372 return civicrm_api3_create_error('Invalid Campaign ID provided. Contribution row was skipped.');
1373 }
1374 $values['contribution_campaign_id'] = $params['contribution_campaign_id'];
1375 break;
1376
1377 default:
1378 // Hande name or label for fields with options.
1379 if (isset($fields[$key]) &&
1380 // Yay - just for a surprise we are inconsistent on whether we pass the pseudofield (payment_instrument)
1381 // or the field name (contribution_status_id)
1382 // @todo - payment_instrument is goneburger - now payment_instrument_id - how
1383 // can we simplify.
1384 (!empty($fields[$key]['is_pseudofield_for']) || !empty($fields[$key]['pseudoconstant']))
1385 ) {
1386 $realField = $fields[$key]['is_pseudofield_for'] ?? $key;
1387 $realFieldSpec = $fields[$realField];
1388 $values[$key] = $this->parsePseudoConstantField($value, $realFieldSpec);
1389 }
1390 break;
1391 }
1392 }
1393
1394 if (array_key_exists('note', $params)) {
1395 $values['note'] = $params['note'];
1396 }
1397
1398 if ($create) {
1399 // CRM_Contribute_BAO_Contribution::add() handles contribution_source
1400 // So, if $values contains contribution_source, convert it to source
1401 $changes = ['contribution_source' => 'source'];
1402
1403 foreach ($changes as $orgVal => $changeVal) {
1404 if (isset($values[$orgVal])) {
1405 $values[$changeVal] = $values[$orgVal];
1406 unset($values[$orgVal]);
1407 }
1408 }
1409 }
1410
1411 return NULL;
1412 }
1413
1414 /**
1415 * Get the civicrm_mapping_field appropriate layout for the mapper input.
1416 *
1417 * The input looks something like ['street_address', 1]
1418 * and would be mapped to ['name' => 'street_address', 'location_type_id' =>
1419 * 1]
1420 *
1421 * @param array $fieldMapping
1422 * @param int $mappingID
1423 * @param int $columnNumber
1424 *
1425 * @return array
1426 * @throws \API_Exception
1427 */
1428 public function getMappingFieldFromMapperInput(array $fieldMapping, int $mappingID, int $columnNumber): array {
1429 return [
1430 'name' => $fieldMapping[0],
1431 'mapping_id' => $mappingID,
1432 'column_number' => $columnNumber,
1433 // The name of the field to match the soft credit on is (crazily)
1434 // stored in 'contact_type'
1435 'contact_type' => $fieldMapping[1] ?? NULL,
1436 // We also store the field in a sensible key, even if it isn't saved sensibly.
1437 'soft_credit_match_field' => $fieldMapping[1] ?? NULL,
1438 // This field is actually not saved at all :-( It is lost each time.
1439 'soft_credit_type_id' => $fieldMapping[2] ?? NULL,
1440 ];
1441 }
1442
1443 /**
1444 * Lookup matching contact.
1445 *
1446 * This looks up the matching contact from the contact id, external identifier
1447 * or email. For the email a straight email search is done - this is equivalent
1448 * to what happens on a dedupe rule lookup when the only field is 'email' - but
1449 * we can't be sure the rule is 'just email' - and we are not collecting the
1450 * fields for any other lookup in the case of soft credits (if we
1451 * extend this function to main-contact-lookup we can handle full dedupe
1452 * lookups - but note the error messages will need tweaking.
1453 *
1454 * @param array $params
1455 *
1456 * @return int
1457 * Contact ID
1458 *
1459 * @throws \API_Exception
1460 * @throws \CRM_Core_Exception
1461 */
1462 private function lookupMatchingContact(array $params): int {
1463 $lookupField = !empty($params['contact_id']) ? 'contact_id' : (!empty($params['external_identifier']) ? 'external_identifier' : 'email');
1464 if (empty($params['email'])) {
1465 $contact = Contact::get(FALSE)->addSelect('id')
1466 ->addWhere($lookupField === 'contact_id' ? 'id' : $lookupField, '=', $params[$lookupField])
1467 ->execute();
1468 if (count($contact) !== 1) {
1469 throw new CRM_Core_Exception(ts("Soft Credit %1 - %2 doesn't exist. Row was skipped.",
1470 [
1471 1 => $this->getFieldMetadata($lookupField),
1472 2 => $params['contact_id'] ?? $params['external_identifier'],
1473 ]));
1474 }
1475 return $contact->first()['id'];
1476 }
1477
1478 if (!CRM_Utils_Rule::email($params['email'])) {
1479 throw new CRM_Core_Exception(ts('Invalid email address %1 provided for Soft Credit. Row was skipped'), [1 => $params['email']]);
1480 }
1481 $emails = Email::get(FALSE)
1482 ->addWhere('contact_id.is_deleted', '=', 0)
1483 ->addWhere('contact_id.contact_type', '=', $this->getContactType())
1484 ->addWhere('email', '=', $params['email'])
1485 ->addSelect('contact_id')->execute();
1486 if (count($emails) === 0) {
1487 throw new CRM_Core_Exception(ts("Invalid email address(doesn't exist) %1 for Soft Credit. Row was skipped", [1 => $params['email']]));
1488 }
1489 if (count($emails) > 1) {
1490 throw new CRM_Core_Exception(ts('Invalid email address(duplicate) %1 for Soft Credit. Row was skipped', [1 => $params['email']]));
1491 }
1492 return $emails->first()['contact_id'];
1493 }
1494
1495 /**
1496 * @param array $mappedField
1497 * Field detail as would be saved in field_mapping table
1498 * or as returned from getMappingFieldFromMapperInput
1499 *
1500 * @return string
1501 * @throws \API_Exception
1502 */
1503 public function getMappedFieldLabel(array $mappedField): string {
1504 if (empty($this->importableFieldsMetadata)) {
1505 $this->setFieldMetadata();
1506 }
1507 $title = [];
1508 $title[] = $this->getFieldMetadata($mappedField['name'])['title'];
1509 if ($mappedField['soft_credit_match_field']) {
1510 $title[] = $this->getFieldMetadata($mappedField['soft_credit_match_field'])['title'];
1511 }
1512 if ($mappedField['soft_credit_type_id']) {
1513 $title[] = CRM_Core_PseudoConstant::getLabel('CRM_Contribute_BAO_ContributionSoft', 'soft_credit_type_id', $mappedField['soft_credit_type_id']);
1514 }
1515
1516 return implode(' - ', $title);
1517 }
1518
1519 /**
1520 * Get the metadata field for which importable fields does not key the actual field name.
1521 *
1522 * @return string[]
1523 */
1524 protected function getOddlyMappedMetadataFields(): array {
1525 $uniqueNames = ['contribution_id', 'contribution_contact_id', 'contribution_cancel_date', 'contribution_source', 'contribution_check_number'];
1526 $fields = [];
1527 foreach ($uniqueNames as $name) {
1528 $fields[$this->importableFieldsMetadata[$name]['name']] = $name;
1529 }
1530 // Include the parent fields as they could be present if required for matching ...in theory.
1531 return array_merge($fields, parent::getOddlyMappedMetadataFields());
1532 }
1533
1534 }