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