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