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