Merge pull request #13689 from eileenmcnaughton/no_record_payment
[civicrm-core.git] / CRM / Contribute / Import / Parser / Contribution.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2019 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26 */
27
28 /**
29 *
30 * @package CRM
31 * @copyright CiviCRM LLC (c) 2004-2019
32 */
33
34 /**
35 * Class to parse contribution csv files.
36 */
37 class CRM_Contribute_Import_Parser_Contribution extends CRM_Contribute_Import_Parser {
38
39 protected $_mapperKeys;
40
41 private $_contactIdIndex;
42 private $_totalAmountIndex;
43 private $_contributionTypeIndex;
44
45 protected $_mapperSoftCredit;
46 //protected $_mapperPhoneType;
47
48 /**
49 * Array of successfully imported contribution id's
50 *
51 * @var array
52 */
53 protected $_newContributions;
54
55 /**
56 * Class constructor.
57 *
58 * @param $mapperKeys
59 * @param null $mapperSoftCredit
60 * @param null $mapperPhoneType
61 * @param null $mapperSoftCreditType
62 */
63 public function __construct(&$mapperKeys, $mapperSoftCredit = NULL, $mapperPhoneType = NULL, $mapperSoftCreditType = NULL) {
64 parent::__construct();
65 $this->_mapperKeys = &$mapperKeys;
66 $this->_mapperSoftCredit = &$mapperSoftCredit;
67 $this->_mapperSoftCreditType = &$mapperSoftCreditType;
68 }
69
70 /**
71 * The initializer code, called before the processing
72 */
73 public function init() {
74 $fields = CRM_Contribute_BAO_Contribution::importableFields($this->_contactType, FALSE);
75
76 $fields = array_merge($fields,
77 [
78 'soft_credit' => [
79 'title' => ts('Soft Credit'),
80 'softCredit' => TRUE,
81 'headerPattern' => '/Soft Credit/i',
82 ],
83 ]
84 );
85
86 // add pledge fields only if its is enabled
87 if (CRM_Core_Permission::access('CiviPledge')) {
88 $pledgeFields = [
89 'pledge_payment' => [
90 'title' => ts('Pledge Payment'),
91 'headerPattern' => '/Pledge Payment/i',
92 ],
93 'pledge_id' => [
94 'title' => ts('Pledge ID'),
95 'headerPattern' => '/Pledge ID/i',
96 ],
97 ];
98
99 $fields = array_merge($fields, $pledgeFields);
100 }
101 foreach ($fields as $name => $field) {
102 $field['type'] = CRM_Utils_Array::value('type', $field, CRM_Utils_Type::T_INT);
103 $field['dataPattern'] = CRM_Utils_Array::value('dataPattern', $field, '//');
104 $field['headerPattern'] = CRM_Utils_Array::value('headerPattern', $field, '//');
105 $this->addField($name, $field['title'], $field['type'], $field['headerPattern'], $field['dataPattern']);
106 }
107
108 $this->_newContributions = [];
109
110 $this->setActiveFields($this->_mapperKeys);
111 $this->setActiveFieldSoftCredit($this->_mapperSoftCredit);
112 $this->setActiveFieldSoftCreditType($this->_mapperSoftCreditType);
113
114 // FIXME: we should do this in one place together with Form/MapField.php
115 $this->_contactIdIndex = -1;
116 $this->_totalAmountIndex = -1;
117 $this->_contributionTypeIndex = -1;
118
119 $index = 0;
120 foreach ($this->_mapperKeys as $key) {
121 switch ($key) {
122 case 'contribution_contact_id':
123 $this->_contactIdIndex = $index;
124 break;
125
126 case 'total_amount':
127 $this->_totalAmountIndex = $index;
128 break;
129
130 case 'financial_type':
131 $this->_contributionTypeIndex = $index;
132 break;
133 }
134 $index++;
135 }
136 }
137
138 /**
139 * Handle the values in mapField mode.
140 *
141 * @param array $values
142 * The array of values belonging to this line.
143 *
144 * @return bool
145 */
146 public function mapField(&$values) {
147 return CRM_Import_Parser::VALID;
148 }
149
150 /**
151 * Handle the values in preview mode.
152 *
153 * @param array $values
154 * The array of values belonging to this line.
155 *
156 * @return bool
157 * the result of this processing
158 */
159 public function preview(&$values) {
160 return $this->summary($values);
161 }
162
163 /**
164 * Handle the values in summary mode.
165 *
166 * @param array $values
167 * The array of values belonging to this line.
168 *
169 * @return bool
170 * the result of this processing
171 */
172 public function summary(&$values) {
173 $erroneousField = NULL;
174 $response = $this->setActiveFieldValues($values, $erroneousField);
175
176 $params = &$this->getActiveFieldParams();
177 $errorMessage = NULL;
178
179 //for date-Formats
180 $errorMessage = implode('; ', $this->formatDateFields($params));
181 //date-Format part ends
182
183 $params['contact_type'] = 'Contribution';
184
185 //checking error in custom data
186 CRM_Contact_Import_Parser_Contact::isErrorInCustomData($params, $errorMessage);
187
188 if ($errorMessage) {
189 $tempMsg = "Invalid value for field(s) : $errorMessage";
190 array_unshift($values, $tempMsg);
191 $errorMessage = NULL;
192 return CRM_Import_Parser::ERROR;
193 }
194
195 return CRM_Import_Parser::VALID;
196 }
197
198 /**
199 * Handle the values in import mode.
200 *
201 * @param int $onDuplicate
202 * The code for what action to take on duplicates.
203 * @param array $values
204 * The array of values belonging to this line.
205 *
206 * @return bool
207 * the result of this processing
208 */
209 public function import($onDuplicate, &$values) {
210 // first make sure this is a valid line
211 $response = $this->summary($values);
212 if ($response != CRM_Import_Parser::VALID) {
213 return $response;
214 }
215
216 $params = &$this->getActiveFieldParams();
217 $formatted = ['version' => 3, 'skipRecentView' => TRUE, 'skipCleanMoney' => FALSE];
218
219 //CRM-10994
220 if (isset($params['total_amount']) && $params['total_amount'] == 0) {
221 $params['total_amount'] = '0.00';
222 }
223 $this->formatInput($params);
224
225 static $indieFields = NULL;
226 if ($indieFields == NULL) {
227 $tempIndieFields = CRM_Contribute_DAO_Contribution::import();
228 $indieFields = $tempIndieFields;
229 }
230
231 $paramValues = [];
232 foreach ($params as $key => $field) {
233 if ($field == NULL || $field === '') {
234 continue;
235 }
236 $paramValues[$key] = $field;
237 }
238
239 //import contribution record according to select contact type
240 if ($onDuplicate == CRM_Import_Parser::DUPLICATE_SKIP &&
241 (!empty($paramValues['contribution_contact_id']) || !empty($paramValues['external_identifier']))
242 ) {
243 $paramValues['contact_type'] = $this->_contactType;
244 }
245 elseif ($onDuplicate == CRM_Import_Parser::DUPLICATE_UPDATE &&
246 (!empty($paramValues['contribution_id']) || !empty($values['trxn_id']) || !empty($paramValues['invoice_id']))
247 ) {
248 $paramValues['contact_type'] = $this->_contactType;
249 }
250 elseif (!empty($params['soft_credit'])) {
251 $paramValues['contact_type'] = $this->_contactType;
252 }
253 elseif (!empty($paramValues['pledge_payment'])) {
254 $paramValues['contact_type'] = $this->_contactType;
255 }
256
257 //need to pass $onDuplicate to check import mode.
258 if (!empty($paramValues['pledge_payment'])) {
259 $paramValues['onDuplicate'] = $onDuplicate;
260 }
261 require_once 'CRM/Utils/DeprecatedUtils.php';
262 $formatError = _civicrm_api3_deprecated_formatted_param($paramValues, $formatted, TRUE, $onDuplicate);
263
264 if ($formatError) {
265 array_unshift($values, $formatError['error_message']);
266 if (CRM_Utils_Array::value('error_data', $formatError) == 'soft_credit') {
267 return CRM_Contribute_Import_Parser::SOFT_CREDIT_ERROR;
268 }
269 elseif (CRM_Utils_Array::value('error_data', $formatError) == 'pledge_payment') {
270 return CRM_Contribute_Import_Parser::PLEDGE_PAYMENT_ERROR;
271 }
272 return CRM_Import_Parser::ERROR;
273 }
274
275 if ($onDuplicate != CRM_Import_Parser::DUPLICATE_UPDATE) {
276 $formatted['custom'] = CRM_Core_BAO_CustomField::postProcess($formatted,
277 NULL,
278 'Contribution'
279 );
280 }
281 else {
282 //fix for CRM-2219 - Update Contribution
283 // onDuplicate == CRM_Import_Parser::DUPLICATE_UPDATE
284 if (!empty($paramValues['invoice_id']) || !empty($paramValues['trxn_id']) || !empty($paramValues['contribution_id'])) {
285 $dupeIds = [
286 'id' => CRM_Utils_Array::value('contribution_id', $paramValues),
287 'trxn_id' => CRM_Utils_Array::value('trxn_id', $paramValues),
288 'invoice_id' => CRM_Utils_Array::value('invoice_id', $paramValues),
289 ];
290
291 $ids['contribution'] = CRM_Contribute_BAO_Contribution::checkDuplicateIds($dupeIds);
292
293 if ($ids['contribution']) {
294 $formatted['id'] = $ids['contribution'];
295 $formatted['custom'] = CRM_Core_BAO_CustomField::postProcess($formatted,
296 $formatted['id'],
297 'Contribution'
298 );
299 //process note
300 if (!empty($paramValues['note'])) {
301 $noteID = [];
302 $contactID = CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_Contribution', $ids['contribution'], 'contact_id');
303 $daoNote = new CRM_Core_BAO_Note();
304 $daoNote->entity_table = 'civicrm_contribution';
305 $daoNote->entity_id = $ids['contribution'];
306 if ($daoNote->find(TRUE)) {
307 $noteID['id'] = $daoNote->id;
308 }
309
310 $noteParams = [
311 'entity_table' => 'civicrm_contribution',
312 'note' => $paramValues['note'],
313 'entity_id' => $ids['contribution'],
314 'contact_id' => $contactID,
315 ];
316 CRM_Core_BAO_Note::add($noteParams, $noteID);
317 unset($formatted['note']);
318 }
319
320 //need to check existing soft credit contribution, CRM-3968
321 if (!empty($formatted['soft_credit'])) {
322 $dupeSoftCredit = [
323 'contact_id' => $formatted['soft_credit'],
324 'contribution_id' => $ids['contribution'],
325 ];
326
327 //Delete all existing soft Contribution from contribution_soft table for pcp_id is_null
328 $existingSoftCredit = CRM_Contribute_BAO_ContributionSoft::getSoftContribution($dupeSoftCredit['contribution_id']);
329 if (isset($existingSoftCredit['soft_credit']) && !empty($existingSoftCredit['soft_credit'])) {
330 foreach ($existingSoftCredit['soft_credit'] as $key => $existingSoftCreditValues) {
331 if (!empty($existingSoftCreditValues['soft_credit_id'])) {
332 civicrm_api3('ContributionSoft', 'delete', [
333 'id' => $existingSoftCreditValues['soft_credit_id'],
334 'pcp_id' => NULL,
335 ]);
336 }
337 }
338 }
339 }
340
341 $newContribution = CRM_Contribute_BAO_Contribution::create($formatted, $ids);
342 $this->_newContributions[] = $newContribution->id;
343
344 //return soft valid since we need to show how soft credits were added
345 if (!empty($formatted['soft_credit'])) {
346 return CRM_Contribute_Import_Parser::SOFT_CREDIT;
347 }
348
349 // process pledge payment assoc w/ the contribution
350 return self::processPledgePayments($formatted);
351
352 return CRM_Import_Parser::VALID;
353 }
354 else {
355 $labels = [
356 'id' => 'Contribution ID',
357 'trxn_id' => 'Transaction ID',
358 'invoice_id' => 'Invoice ID',
359 ];
360 foreach ($dupeIds as $k => $v) {
361 if ($v) {
362 $errorMsg[] = "$labels[$k] $v";
363 }
364 }
365 $errorMsg = implode(' AND ', $errorMsg);
366 array_unshift($values, 'Matching Contribution record not found for ' . $errorMsg . '. Row was skipped.');
367 return CRM_Import_Parser::ERROR;
368 }
369 }
370 }
371
372 if ($this->_contactIdIndex < 0) {
373 // set the contact type if its not set
374 if (!isset($paramValues['contact_type'])) {
375 $paramValues['contact_type'] = $this->_contactType;
376 }
377
378 $error = $this->checkContactDuplicate($paramValues);
379
380 if (CRM_Core_Error::isAPIError($error, CRM_Core_ERROR::DUPLICATE_CONTACT)) {
381 $matchedIDs = explode(',', $error['error_message']['params'][0]);
382 if (count($matchedIDs) > 1) {
383 array_unshift($values, 'Multiple matching contact records detected for this row. The contribution was not imported');
384 return CRM_Import_Parser::ERROR;
385 }
386 else {
387 $cid = $matchedIDs[0];
388 $formatted['contact_id'] = $cid;
389
390 $newContribution = civicrm_api('contribution', 'create', $formatted);
391 if (civicrm_error($newContribution)) {
392 if (is_array($newContribution['error_message'])) {
393 array_unshift($values, $newContribution['error_message']['message']);
394 if ($newContribution['error_message']['params'][0]) {
395 return CRM_Import_Parser::DUPLICATE;
396 }
397 }
398 else {
399 array_unshift($values, $newContribution['error_message']);
400 return CRM_Import_Parser::ERROR;
401 }
402 }
403
404 $this->_newContributions[] = $newContribution['id'];
405 $formatted['contribution_id'] = $newContribution['id'];
406
407 //return soft valid since we need to show how soft credits were added
408 if (!empty($formatted['soft_credit'])) {
409 return CRM_Contribute_Import_Parser::SOFT_CREDIT;
410 }
411
412 // process pledge payment assoc w/ the contribution
413 return self::processPledgePayments($formatted);
414
415 return CRM_Import_Parser::VALID;
416 }
417 }
418 else {
419 // Using new Dedupe rule.
420 $ruleParams = [
421 'contact_type' => $this->_contactType,
422 'used' => 'Unsupervised',
423 ];
424 $fieldsArray = CRM_Dedupe_BAO_Rule::dedupeRuleFields($ruleParams);
425 $disp = NULL;
426 foreach ($fieldsArray as $value) {
427 if (array_key_exists(trim($value), $params)) {
428 $paramValue = $params[trim($value)];
429 if (is_array($paramValue)) {
430 $disp .= $params[trim($value)][0][trim($value)] . " ";
431 }
432 else {
433 $disp .= $params[trim($value)] . " ";
434 }
435 }
436 }
437
438 if (!empty($params['external_identifier'])) {
439 if ($disp) {
440 $disp .= "AND {$params['external_identifier']}";
441 }
442 else {
443 $disp = $params['external_identifier'];
444 }
445 }
446
447 array_unshift($values, 'No matching Contact found for (' . $disp . ')');
448 return CRM_Import_Parser::ERROR;
449 }
450 }
451 else {
452 if (!empty($paramValues['external_identifier'])) {
453 $checkCid = new CRM_Contact_DAO_Contact();
454 $checkCid->external_identifier = $paramValues['external_identifier'];
455 $checkCid->find(TRUE);
456 if ($checkCid->id != $formatted['contact_id']) {
457 array_unshift($values, 'Mismatch of External ID:' . $paramValues['external_identifier'] . ' and Contact Id:' . $formatted['contact_id']);
458 return CRM_Import_Parser::ERROR;
459 }
460 }
461 $newContribution = civicrm_api('contribution', 'create', $formatted);
462 if (civicrm_error($newContribution)) {
463 if (is_array($newContribution['error_message'])) {
464 array_unshift($values, $newContribution['error_message']['message']);
465 if ($newContribution['error_message']['params'][0]) {
466 return CRM_Import_Parser::DUPLICATE;
467 }
468 }
469 else {
470 array_unshift($values, $newContribution['error_message']);
471 return CRM_Import_Parser::ERROR;
472 }
473 }
474
475 $this->_newContributions[] = $newContribution['id'];
476 $formatted['contribution_id'] = $newContribution['id'];
477
478 //return soft valid since we need to show how soft credits were added
479 if (!empty($formatted['soft_credit'])) {
480 return CRM_Contribute_Import_Parser::SOFT_CREDIT;
481 }
482
483 // process pledge payment assoc w/ the contribution
484 return self::processPledgePayments($formatted);
485
486 return CRM_Import_Parser::VALID;
487 }
488 }
489
490 /**
491 * Process pledge payments.
492 *
493 * @param array $formatted
494 *
495 * @return int
496 */
497 public function processPledgePayments(&$formatted) {
498 if (!empty($formatted['pledge_payment_id']) && !empty($formatted['pledge_id'])) {
499 //get completed status
500 $completeStatusID = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
501
502 //need to update payment record to map contribution_id
503 CRM_Core_DAO::setFieldValue('CRM_Pledge_DAO_PledgePayment', $formatted['pledge_payment_id'],
504 'contribution_id', $formatted['contribution_id']
505 );
506
507 CRM_Pledge_BAO_PledgePayment::updatePledgePaymentStatus($formatted['pledge_id'],
508 [$formatted['pledge_payment_id']],
509 $completeStatusID,
510 NULL,
511 $formatted['total_amount']
512 );
513
514 return CRM_Contribute_Import_Parser::PLEDGE_PAYMENT;
515 }
516 }
517
518 /**
519 * Get the array of successfully imported contribution id's
520 *
521 * @return array
522 */
523 public function &getImportedContributions() {
524 return $this->_newContributions;
525 }
526
527 /**
528 * The initializer code, called before the processing.
529 */
530 public function fini() {
531 }
532
533 /**
534 * Format date fields from input to mysql.
535 *
536 * @param array $params
537 *
538 * @return array
539 * Error messages, if any.
540 */
541 public function formatDateFields(&$params) {
542 $errorMessage = [];
543 $dateType = CRM_Core_Session::singleton()->get('dateTypes');
544 foreach ($params as $key => $val) {
545 if ($val) {
546 switch ($key) {
547 case 'receive_date':
548 if ($dateValue = CRM_Utils_Date::formatDate($params[$key], $dateType)) {
549 $params[$key] = $dateValue;
550 }
551 else {
552 $errorMessage[] = ts('Receive Date');
553 }
554 break;
555
556 case 'cancel_date':
557 if ($dateValue = CRM_Utils_Date::formatDate($params[$key], $dateType)) {
558 $params[$key] = $dateValue;
559 }
560 else {
561 $errorMessage[] = ts('Cancel Date');
562 }
563 break;
564
565 case 'receipt_date':
566 if ($dateValue = CRM_Utils_Date::formatDate($params[$key], $dateType)) {
567 $params[$key] = $dateValue;
568 }
569 else {
570 $errorMessage[] = ts('Receipt date');
571 }
572 break;
573
574 case 'thankyou_date':
575 if ($dateValue = CRM_Utils_Date::formatDate($params[$key], $dateType)) {
576 $params[$key] = $dateValue;
577 }
578 else {
579 $errorMessage[] = ts('Thankyou Date');
580 }
581 break;
582 }
583 }
584 }
585 return $errorMessage;
586 }
587
588 /**
589 * Format input params to suit api handling.
590 *
591 * Over time all the parts of _civicrm_api3_deprecated_formatted_param
592 * and all the parts of the import function on this class that relate to
593 * reformatting input should be moved here and tests should be added in
594 * CRM_Contribute_Import_Parser_ContributionTest.
595 *
596 * @param array $params
597 */
598 public function formatInput(&$params) {
599 $dateType = CRM_Core_Session::singleton()->get('dateTypes');
600 $customDataType = !empty($params['contact_type']) ? $params['contact_type'] : 'Contribution';
601 $customFields = CRM_Core_BAO_CustomField::getFields($customDataType);
602 // @todo call formatDateFields & move custom data handling there.
603 // Also note error handling for dates is currently in _civicrm_api3_deprecated_formatted_param
604 // we should use the error handling in formatDateFields.
605 foreach ($params as $key => $val) {
606 // @todo - call formatDateFields instead.
607 if ($val) {
608 switch ($key) {
609 case 'receive_date':
610 case 'cancel_date':
611 case 'receipt_date':
612 case 'thankyou_date':
613 $params[$key] = CRM_Utils_Date::formatDate($params[$key], $dateType);
614 break;
615
616 case 'pledge_payment':
617 $params[$key] = CRM_Utils_String::strtobool($val);
618 break;
619
620 }
621 if ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) {
622 if ($customFields[$customFieldID]['data_type'] == 'Date') {
623 CRM_Contact_Import_Parser_Contact::formatCustomDate($params, $params, $dateType, $key);
624 unset($params[$key]);
625 }
626 elseif ($customFields[$customFieldID]['data_type'] == 'Boolean') {
627 $params[$key] = CRM_Utils_String::strtoboolstr($val);
628 }
629 }
630 }
631 }
632 }
633
634 }