Commit | Line | Data |
---|---|---|
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 |
18 | use Civi\Api4\Contact; |
19 | use Civi\Api4\Email; | |
20 | ||
6a488035 | 21 | /** |
74ab7ba8 | 22 | * Class to parse contribution csv files. |
6a488035 | 23 | */ |
8dc9763a | 24 | class 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 | ||
8dc9763a | 113 | /** |
01c21f7e | 114 | * Get the field mappings for the import. |
8dc9763a | 115 | * |
01c21f7e EM |
116 | * This is the same format as saved in civicrm_mapping_field except |
117 | * that location_type_id = 'Primary' rather than empty where relevant. | |
118 | * Also 'im_provider_id' is mapped to the 'real' field name 'provider_id' | |
8dc9763a EM |
119 | * |
120 | * @return array | |
01c21f7e | 121 | * @throws \API_Exception |
8dc9763a | 122 | */ |
01c21f7e EM |
123 | protected function getFieldMappings(): array { |
124 | $mappedFields = []; | |
125 | foreach ($this->getSubmittedValue('mapper') as $i => $mapperRow) { | |
126 | $mappedField = $this->getMappingFieldFromMapperInput($mapperRow, 0, $i); | |
127 | // Just for clarity since 0 is a pseudo-value | |
128 | unset($mappedField['mapping_id']); | |
129 | $mappedFields[] = $mappedField; | |
8dc9763a | 130 | } |
01c21f7e | 131 | return $mappedFields; |
8dc9763a EM |
132 | } |
133 | ||
b7dde70c EM |
134 | /** |
135 | * Get the required fields. | |
136 | * | |
137 | * @return array | |
138 | */ | |
139 | public function getRequiredFields(): array { | |
140 | return ['id' => ts('Contribution ID'), ['financial_type_id' => ts('Financial Type'), 'total_amount' => ts('Total Amount')]]; | |
141 | } | |
142 | ||
288db2d2 EM |
143 | /** |
144 | * Transform the input parameters into the form handled by the input routine. | |
145 | * | |
146 | * @param array $values | |
147 | * Input parameters as they come in from the datasource | |
148 | * eg. ['Bob', 'Smith', 'bob@example.org', '123-456'] | |
149 | * | |
150 | * @return array | |
151 | * Parameters mapped to CiviCRM fields based on the mapping. eg. | |
152 | * [ | |
153 | * 'total_amount' => '1230.99', | |
154 | * 'financial_type_id' => 1, | |
155 | * 'external_identifier' => 'abcd', | |
156 | * 'soft_credit' => [3 => ['external_identifier' => '123', 'soft_credit_type_id' => 1]] | |
157 | * | |
158 | * @throws \API_Exception | |
159 | */ | |
160 | public function getMappedRow(array $values): array { | |
161 | $params = []; | |
162 | foreach ($this->getFieldMappings() as $i => $mappedField) { | |
66673bfe | 163 | if ($mappedField['name'] === 'do_not_import' || !$mappedField['name']) { |
4b58c5c4 EM |
164 | continue; |
165 | } | |
288db2d2 EM |
166 | if (!empty($mappedField['soft_credit_match_field'])) { |
167 | $params['soft_credit'][$i] = ['soft_credit_type_id' => $mappedField['soft_credit_type_id'], $mappedField['soft_credit_match_field'] => $values[$i]]; | |
168 | } | |
169 | else { | |
1c82489b | 170 | $params[$this->getFieldMetadata($mappedField['name'])['name']] = $this->getTransformedFieldValue($mappedField['name'], $values[$i]); |
288db2d2 EM |
171 | } |
172 | } | |
173 | return $params; | |
174 | } | |
175 | ||
8dc9763a EM |
176 | /** |
177 | * @param string $name | |
178 | * @param $title | |
179 | * @param int $type | |
180 | * @param string $headerPattern | |
181 | * @param string $dataPattern | |
182 | */ | |
183 | public function addField($name, $title, $type = CRM_Utils_Type::T_INT, $headerPattern = '//', $dataPattern = '//') { | |
184 | if (empty($name)) { | |
185 | $this->_fields['doNotImport'] = new CRM_Contribute_Import_Field($name, $title, $type, $headerPattern, $dataPattern); | |
186 | } | |
187 | else { | |
188 | $tempField = CRM_Contact_BAO_Contact::importableFields('All', NULL); | |
189 | if (!array_key_exists($name, $tempField)) { | |
190 | $this->_fields[$name] = new CRM_Contribute_Import_Field($name, $title, $type, $headerPattern, $dataPattern); | |
191 | } | |
192 | else { | |
193 | $this->_fields[$name] = new CRM_Contact_Import_Field($name, $title, $type, $headerPattern, $dataPattern, | |
194 | CRM_Utils_Array::value('hasLocationType', $tempField[$name]) | |
195 | ); | |
196 | } | |
197 | } | |
198 | } | |
199 | ||
6a488035 | 200 | /** |
100fef9d | 201 | * The initializer code, called before the processing |
6a488035 | 202 | */ |
00be9182 | 203 | public function init() { |
603928dc EM |
204 | // Force re-load of user job. |
205 | unset($this->userJob); | |
73edfc10 EM |
206 | $this->setFieldMetadata(); |
207 | foreach ($this->getImportableFieldsMetadata() as $name => $field) { | |
6a488035 TO |
208 | $this->addField($name, $field['title'], $field['type'], $field['headerPattern'], $field['dataPattern']); |
209 | } | |
6a488035 TO |
210 | } |
211 | ||
73edfc10 EM |
212 | /** |
213 | * Set field metadata. | |
214 | */ | |
215 | protected function setFieldMetadata() { | |
216 | if (empty($this->importableFieldsMetadata)) { | |
603928dc | 217 | $fields = CRM_Contribute_BAO_Contribution::importableFields($this->getContactType(), FALSE); |
73edfc10 EM |
218 | |
219 | $fields = array_merge($fields, | |
220 | [ | |
221 | 'soft_credit' => [ | |
222 | 'title' => ts('Soft Credit'), | |
223 | 'softCredit' => TRUE, | |
224 | 'headerPattern' => '/Soft Credit/i', | |
603928dc | 225 | 'options' => FALSE, |
66673bfe | 226 | 'type' => CRM_Utils_Type::T_STRING, |
73edfc10 EM |
227 | ], |
228 | ] | |
229 | ); | |
230 | ||
231 | // add pledge fields only if its is enabled | |
232 | if (CRM_Core_Permission::access('CiviPledge')) { | |
233 | $pledgeFields = [ | |
73edfc10 EM |
234 | 'pledge_id' => [ |
235 | 'title' => ts('Pledge ID'), | |
236 | 'headerPattern' => '/Pledge ID/i', | |
b7dde70c EM |
237 | 'name' => 'pledge_id', |
238 | 'entity' => 'Pledge', | |
239 | 'type' => CRM_Utils_Type::T_INT, | |
240 | 'options' => FALSE, | |
73edfc10 EM |
241 | ], |
242 | ]; | |
243 | ||
244 | $fields = array_merge($fields, $pledgeFields); | |
245 | } | |
246 | foreach ($fields as $name => $field) { | |
247 | $fields[$name] = array_merge([ | |
248 | 'type' => CRM_Utils_Type::T_INT, | |
249 | 'dataPattern' => '//', | |
250 | 'headerPattern' => '//', | |
251 | ], $field); | |
252 | } | |
253 | $this->importableFieldsMetadata = $fields; | |
254 | } | |
255 | } | |
256 | ||
6a488035 | 257 | /** |
fe482240 | 258 | * Handle the values in import mode. |
6a488035 | 259 | * |
014c4014 TO |
260 | * @param array $values |
261 | * The array of values belonging to this line. | |
6a488035 | 262 | */ |
b7dde70c | 263 | public function import($values): void { |
8daa4f36 | 264 | $rowNumber = (int) ($values[array_key_last($values)]); |
145374a4 | 265 | try { |
145374a4 EM |
266 | $params = $this->getMappedRow($values); |
267 | $formatted = array_merge(['version' => 3, 'skipRecentView' => TRUE, 'skipCleanMoney' => TRUE, 'contribution_id' => $params['id'] ?? NULL], $params); | |
268 | //CRM-10994 | |
269 | if (isset($params['total_amount']) && $params['total_amount'] == 0) { | |
270 | $params['total_amount'] = '0.00'; | |
271 | } | |
272 | $this->formatInput($params, $formatted); | |
6a488035 | 273 | |
145374a4 EM |
274 | $paramValues = []; |
275 | foreach ($params as $key => $field) { | |
276 | if ($field == NULL || $field === '') { | |
277 | continue; | |
278 | } | |
279 | $paramValues[$key] = $field; | |
6a488035 | 280 | } |
6a488035 | 281 | |
145374a4 | 282 | //import contribution record according to select contact type |
63bf48e9 | 283 | if ($this->isSkipDuplicates() && |
145374a4 EM |
284 | (!empty($paramValues['contribution_contact_id']) || !empty($paramValues['external_identifier'])) |
285 | ) { | |
603928dc | 286 | $paramValues['contact_type'] = $this->getContactType(); |
145374a4 | 287 | } |
63bf48e9 | 288 | elseif ($this->isUpdateExisting() && |
145374a4 EM |
289 | (!empty($paramValues['contribution_id']) || !empty($values['trxn_id']) || !empty($paramValues['invoice_id'])) |
290 | ) { | |
603928dc | 291 | $paramValues['contact_type'] = $this->getContactType(); |
145374a4 EM |
292 | } |
293 | elseif (!empty($paramValues['pledge_payment'])) { | |
603928dc | 294 | $paramValues['contact_type'] = $this->getContactType(); |
145374a4 | 295 | } |
6a488035 | 296 | |
63bf48e9 | 297 | $formatError = $this->deprecatedFormatParams($paramValues, $formatted); |
6a488035 | 298 | |
145374a4 | 299 | if ($formatError) { |
145374a4 | 300 | if (CRM_Utils_Array::value('error_data', $formatError) == 'soft_credit') { |
b7dde70c | 301 | throw new CRM_Core_Exception('', self::SOFT_CREDIT_ERROR); |
145374a4 EM |
302 | } |
303 | if (CRM_Utils_Array::value('error_data', $formatError) == 'pledge_payment') { | |
b7dde70c | 304 | throw new CRM_Core_Exception('', self::PLEDGE_PAYMENT_ERROR); |
145374a4 | 305 | } |
01f12b3d | 306 | throw new CRM_Core_Exception('', CRM_Import_Parser::ERROR); |
6a488035 | 307 | } |
6a488035 | 308 | |
63bf48e9 | 309 | if ($this->isUpdateExisting()) { |
145374a4 EM |
310 | //fix for CRM-2219 - Update Contribution |
311 | // onDuplicate == CRM_Import_Parser::DUPLICATE_UPDATE | |
312 | if (!empty($paramValues['invoice_id']) || !empty($paramValues['trxn_id']) || !empty($paramValues['contribution_id'])) { | |
313 | $dupeIds = [ | |
314 | 'id' => $paramValues['contribution_id'] ?? NULL, | |
315 | 'trxn_id' => $paramValues['trxn_id'] ?? NULL, | |
316 | 'invoice_id' => $paramValues['invoice_id'] ?? NULL, | |
317 | ]; | |
318 | $ids['contribution'] = CRM_Contribute_BAO_Contribution::checkDuplicateIds($dupeIds); | |
319 | ||
320 | if ($ids['contribution']) { | |
321 | $formatted['id'] = $ids['contribution']; | |
322 | //process note | |
323 | if (!empty($paramValues['note'])) { | |
324 | $noteID = []; | |
325 | $contactID = CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_Contribution', $ids['contribution'], 'contact_id'); | |
326 | $daoNote = new CRM_Core_BAO_Note(); | |
327 | $daoNote->entity_table = 'civicrm_contribution'; | |
328 | $daoNote->entity_id = $ids['contribution']; | |
329 | if ($daoNote->find(TRUE)) { | |
330 | $noteID['id'] = $daoNote->id; | |
331 | } | |
6a488035 | 332 | |
145374a4 EM |
333 | $noteParams = [ |
334 | 'entity_table' => 'civicrm_contribution', | |
335 | 'note' => $paramValues['note'], | |
336 | 'entity_id' => $ids['contribution'], | |
337 | 'contact_id' => $contactID, | |
338 | ]; | |
339 | CRM_Core_BAO_Note::add($noteParams, $noteID); | |
340 | unset($formatted['note']); | |
341 | } | |
6a488035 | 342 | |
145374a4 EM |
343 | //need to check existing soft credit contribution, CRM-3968 |
344 | if (!empty($formatted['soft_credit'])) { | |
345 | $dupeSoftCredit = [ | |
346 | 'contact_id' => $formatted['soft_credit'], | |
347 | 'contribution_id' => $ids['contribution'], | |
348 | ]; | |
349 | ||
350 | //Delete all existing soft Contribution from contribution_soft table for pcp_id is_null | |
351 | $existingSoftCredit = CRM_Contribute_BAO_ContributionSoft::getSoftContribution($dupeSoftCredit['contribution_id']); | |
352 | if (isset($existingSoftCredit['soft_credit']) && !empty($existingSoftCredit['soft_credit'])) { | |
353 | foreach ($existingSoftCredit['soft_credit'] as $key => $existingSoftCreditValues) { | |
354 | if (!empty($existingSoftCreditValues['soft_credit_id'])) { | |
355 | civicrm_api3('ContributionSoft', 'delete', [ | |
356 | 'id' => $existingSoftCreditValues['soft_credit_id'], | |
357 | 'pcp_id' => NULL, | |
358 | ]); | |
359 | } | |
1221efe9 | 360 | } |
361 | } | |
6a488035 | 362 | } |
145374a4 EM |
363 | |
364 | $formatted['id'] = $ids['contribution']; | |
365 | ||
366 | $newContribution = civicrm_api3('contribution', 'create', $formatted); | |
367 | $this->_newContributions[] = $newContribution['id']; | |
368 | ||
369 | //return soft valid since we need to show how soft credits were added | |
370 | if (!empty($formatted['soft_credit'])) { | |
b7dde70c EM |
371 | $this->setImportStatus($rowNumber, $this->getStatus(self::SOFT_CREDIT)); |
372 | return; | |
145374a4 EM |
373 | } |
374 | ||
375 | // process pledge payment assoc w/ the contribution | |
b7dde70c EM |
376 | $this->processPledgePayments($formatted); |
377 | $this->setImportStatus($rowNumber, $this->getStatus(self::PLEDGE_PAYMENT)); | |
378 | return; | |
145374a4 EM |
379 | } |
380 | $labels = [ | |
381 | 'id' => 'Contribution ID', | |
382 | 'trxn_id' => 'Transaction ID', | |
383 | 'invoice_id' => 'Invoice ID', | |
384 | ]; | |
385 | foreach ($dupeIds as $k => $v) { | |
386 | if ($v) { | |
387 | $errorMsg[] = "$labels[$k] $v"; | |
388 | } | |
6a488035 | 389 | } |
145374a4 | 390 | $errorMsg = implode(' AND ', $errorMsg); |
01f12b3d | 391 | throw new CRM_Core_Exception('Matching Contribution record not found for ' . $errorMsg . '. Row was skipped.', CRM_Import_Parser::ERROR); |
145374a4 EM |
392 | } |
393 | } | |
6a488035 | 394 | |
145374a4 EM |
395 | if (empty($formatted['contact_id'])) { |
396 | ||
397 | $error = $this->checkContactDuplicate($paramValues); | |
398 | ||
399 | if (CRM_Core_Error::isAPIError($error, CRM_Core_ERROR::DUPLICATE_CONTACT)) { | |
400 | $matchedIDs = explode(',', $error['error_message']['params'][0]); | |
401 | if (count($matchedIDs) > 1) { | |
01f12b3d | 402 | throw new CRM_Core_Exception('Multiple matching contact records detected for this row. The contribution was not imported', CRM_Import_Parser::ERROR); |
145374a4 EM |
403 | } |
404 | $cid = $matchedIDs[0]; | |
405 | $formatted['contact_id'] = $cid; | |
406 | ||
407 | $newContribution = civicrm_api('contribution', 'create', $formatted); | |
408 | if (civicrm_error($newContribution)) { | |
409 | if (is_array($newContribution['error_message'])) { | |
145374a4 | 410 | if ($newContribution['error_message']['params'][0]) { |
63bf48e9 | 411 | throw new CRM_Core_Exception($newContribution['error_message']['message'], CRM_Import_Parser::DUPLICATE); |
145374a4 EM |
412 | } |
413 | } | |
414 | else { | |
01f12b3d | 415 | throw new CRM_Core_Exception($newContribution['error_message'], CRM_Import_Parser::ERROR); |
145374a4 EM |
416 | } |
417 | } | |
f6fc1b15 | 418 | |
f6fc1b15 | 419 | $this->_newContributions[] = $newContribution['id']; |
145374a4 | 420 | $formatted['contribution_id'] = $newContribution['id']; |
6a488035 TO |
421 | |
422 | //return soft valid since we need to show how soft credits were added | |
1221efe9 | 423 | if (!empty($formatted['soft_credit'])) { |
b7dde70c EM |
424 | $this->setImportStatus($rowNumber, $this->getStatus(self::SOFT_CREDIT)); |
425 | return; | |
6a488035 TO |
426 | } |
427 | ||
b7dde70c EM |
428 | $this->processPledgePayments($formatted); |
429 | $this->setImportStatus($rowNumber, $this->getStatus(self::PLEDGE_PAYMENT)); | |
430 | return; | |
6a488035 | 431 | } |
6a488035 | 432 | |
145374a4 EM |
433 | // Using new Dedupe rule. |
434 | $ruleParams = [ | |
603928dc | 435 | 'contact_type' => $this->getContactType(), |
145374a4 EM |
436 | 'used' => 'Unsupervised', |
437 | ]; | |
438 | $fieldsArray = CRM_Dedupe_BAO_DedupeRule::dedupeRuleFields($ruleParams); | |
439 | $disp = NULL; | |
440 | foreach ($fieldsArray as $value) { | |
441 | if (array_key_exists(trim($value), $params)) { | |
442 | $paramValue = $params[trim($value)]; | |
443 | if (is_array($paramValue)) { | |
444 | $disp .= $params[trim($value)][0][trim($value)] . " "; | |
445 | } | |
446 | else { | |
447 | $disp .= $params[trim($value)] . " "; | |
6a488035 | 448 | } |
6a488035 | 449 | } |
69f296a4 | 450 | } |
6a488035 | 451 | |
145374a4 EM |
452 | if (!empty($params['external_identifier'])) { |
453 | if ($disp) { | |
454 | $disp .= "AND {$params['external_identifier']}"; | |
6a488035 TO |
455 | } |
456 | else { | |
145374a4 | 457 | $disp = $params['external_identifier']; |
6a488035 TO |
458 | } |
459 | } | |
145374a4 | 460 | $errorMessage = 'No matching Contact found for (' . $disp . ')'; |
01f12b3d | 461 | throw new CRM_Core_Exception($errorMessage, CRM_Import_Parser::ERROR); |
6a488035 | 462 | } |
69f296a4 | 463 | |
145374a4 EM |
464 | if (!empty($paramValues['external_identifier'])) { |
465 | $checkCid = new CRM_Contact_DAO_Contact(); | |
466 | $checkCid->external_identifier = $paramValues['external_identifier']; | |
467 | $checkCid->find(TRUE); | |
468 | if ($checkCid->id != $formatted['contact_id']) { | |
469 | $errorMessage = 'Mismatch of External ID:' . $paramValues['external_identifier'] . ' and Contact Id:' . $formatted['contact_id']; | |
01f12b3d | 470 | throw new CRM_Core_Exception($errorMessage, CRM_Import_Parser::ERROR); |
145374a4 EM |
471 | } |
472 | } | |
473 | $newContribution = civicrm_api('contribution', 'create', $formatted); | |
474 | if (civicrm_error($newContribution)) { | |
475 | if (is_array($newContribution['error_message'])) { | |
145374a4 | 476 | if ($newContribution['error_message']['params'][0]) { |
63bf48e9 | 477 | throw new CRM_Core_Exception('', CRM_Import_Parser::DUPLICATE); |
145374a4 | 478 | } |
6a488035 TO |
479 | } |
480 | else { | |
01f12b3d | 481 | throw new CRM_Core_Exception($newContribution['error_message'], CRM_Import_Parser::ERROR); |
6a488035 TO |
482 | } |
483 | } | |
6a488035 | 484 | |
145374a4 EM |
485 | $this->_newContributions[] = $newContribution['id']; |
486 | $formatted['contribution_id'] = $newContribution['id']; | |
6a488035 | 487 | |
145374a4 EM |
488 | //return soft valid since we need to show how soft credits were added |
489 | if (!empty($formatted['soft_credit'])) { | |
b7dde70c EM |
490 | $this->setImportStatus($rowNumber, $this->getStatus(self::SOFT_CREDIT), ''); |
491 | return; | |
145374a4 | 492 | } |
6a488035 | 493 | |
145374a4 | 494 | // process pledge payment assoc w/ the contribution |
b7dde70c EM |
495 | $this->processPledgePayments($formatted); |
496 | $this->setImportStatus($rowNumber, $this->getStatus(self::PLEDGE_PAYMENT)); | |
497 | return; | |
498 | ||
145374a4 EM |
499 | } |
500 | catch (CRM_Core_Exception $e) { | |
b7dde70c | 501 | $this->setImportStatus($rowNumber, $this->getStatus($e->getErrorCode()), $e->getMessage()); |
6a488035 TO |
502 | } |
503 | } | |
504 | ||
b7dde70c EM |
505 | /** |
506 | * Get the status to record. | |
507 | * | |
508 | * @param int|null $code | |
509 | * | |
510 | * @return string | |
511 | */ | |
512 | protected function getStatus(?int $code): string { | |
513 | $errorMapping = [ | |
514 | self::SOFT_CREDIT_ERROR => 'soft_credit_error', | |
515 | self::PLEDGE_PAYMENT_ERROR => 'pledge_payment_error', | |
516 | self::SOFT_CREDIT => 'soft_credit_imported', | |
517 | self::PLEDGE_PAYMENT => 'pledge_payment_imported', | |
518 | CRM_Import_Parser::DUPLICATE => 'DUPLICATE', | |
519 | CRM_Import_Parser::VALID => 'IMPORTED', | |
520 | ]; | |
521 | return $errorMapping[$code] ?? 'ERROR'; | |
522 | } | |
523 | ||
6a488035 | 524 | /** |
74ab7ba8 EM |
525 | * Process pledge payments. |
526 | * | |
527 | * @param array $formatted | |
6a488035 | 528 | */ |
672b72ea | 529 | private function processPledgePayments(array $formatted) { |
8cc574cf | 530 | if (!empty($formatted['pledge_payment_id']) && !empty($formatted['pledge_id'])) { |
6a488035 | 531 | //get completed status |
593dbb07 | 532 | $completeStatusID = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'); |
6a488035 TO |
533 | |
534 | //need to update payment record to map contribution_id | |
535 | CRM_Core_DAO::setFieldValue('CRM_Pledge_DAO_PledgePayment', $formatted['pledge_payment_id'], | |
536 | 'contribution_id', $formatted['contribution_id'] | |
537 | ); | |
538 | ||
539 | CRM_Pledge_BAO_PledgePayment::updatePledgePaymentStatus($formatted['pledge_id'], | |
be2fb01f | 540 | [$formatted['pledge_payment_id']], |
6a488035 TO |
541 | $completeStatusID, |
542 | NULL, | |
543 | $formatted['total_amount'] | |
544 | ); | |
6a488035 TO |
545 | } |
546 | } | |
547 | ||
548 | /** | |
ceb10dc7 | 549 | * Get the array of successfully imported contribution id's |
6a488035 TO |
550 | * |
551 | * @return array | |
6a488035 | 552 | */ |
00be9182 | 553 | public function &getImportedContributions() { |
6a488035 TO |
554 | return $this->_newContributions; |
555 | } | |
556 | ||
1004b689 | 557 | /** |
558 | * Format input params to suit api handling. | |
559 | * | |
4fc2ce46 | 560 | * Over time all the parts of deprecatedFormatParams |
1004b689 | 561 | * and all the parts of the import function on this class that relate to |
562 | * reformatting input should be moved here and tests should be added in | |
563 | * CRM_Contribute_Import_Parser_ContributionTest. | |
564 | * | |
565 | * @param array $params | |
fed96c11 | 566 | * @param array $formatted |
1004b689 | 567 | */ |
fed96c11 | 568 | public function formatInput(&$params, &$formatted = []) { |
1004b689 | 569 | foreach ($params as $key => $val) { |
570 | // @todo - call formatDateFields instead. | |
571 | if ($val) { | |
572 | switch ($key) { | |
1004b689 | 573 | |
574 | case 'pledge_payment': | |
575 | $params[$key] = CRM_Utils_String::strtobool($val); | |
576 | break; | |
577 | ||
578 | } | |
1004b689 | 579 | } |
580 | } | |
581 | } | |
582 | ||
4fc2ce46 | 583 | /** |
584 | * take the input parameter list as specified in the data model and | |
585 | * convert it into the same format that we use in QF and BAO object | |
586 | * | |
587 | * @param array $params | |
5e21e0f3 BT |
588 | * Associative array of property name/value |
589 | * pairs to insert in new contact. | |
4fc2ce46 | 590 | * @param array $values |
591 | * The reformatted properties that we can use internally. | |
4fc2ce46 | 592 | * @param bool $create |
4fc2ce46 | 593 | * |
594 | * @return array|CRM_Error | |
01c21f7e | 595 | * @throws \CRM_Core_Exception |
4fc2ce46 | 596 | */ |
63bf48e9 | 597 | private function deprecatedFormatParams($params, &$values, $create = FALSE) { |
4fc2ce46 | 598 | require_once 'CRM/Utils/DeprecatedUtils.php'; |
599 | // copy all the contribution fields as is | |
600 | require_once 'api/v3/utils.php'; | |
4fc2ce46 | 601 | |
602 | foreach ($params as $key => $value) { | |
603 | // ignore empty values or empty arrays etc | |
604 | if (CRM_Utils_System::isNull($value)) { | |
605 | continue; | |
606 | } | |
607 | ||
4fc2ce46 | 608 | switch ($key) { |
01c21f7e | 609 | case 'contact_id': |
4fc2ce46 | 610 | if (!CRM_Utils_Rule::integer($value)) { |
611 | return civicrm_api3_create_error("contact_id not valid: $value"); | |
612 | } | |
613 | $dao = new CRM_Core_DAO(); | |
614 | $qParams = []; | |
615 | $svq = $dao->singleValueQuery("SELECT is_deleted FROM civicrm_contact WHERE id = $value", | |
616 | $qParams | |
617 | ); | |
618 | if (!isset($svq)) { | |
619 | return civicrm_api3_create_error("Invalid Contact ID: There is no contact record with contact_id = $value."); | |
620 | } | |
621 | elseif ($svq == 1) { | |
622 | return civicrm_api3_create_error("Invalid Contact ID: contact_id $value is a soft-deleted contact."); | |
623 | } | |
01c21f7e | 624 | $values['contact_id'] = $value; |
4fc2ce46 | 625 | break; |
626 | ||
627 | case 'contact_type': | |
628 | // import contribution record according to select contact type | |
629 | require_once 'CRM/Contact/DAO/Contact.php'; | |
630 | $contactType = new CRM_Contact_DAO_Contact(); | |
9c1bc317 CW |
631 | $contactId = $params['contribution_contact_id'] ?? NULL; |
632 | $externalId = $params['external_identifier'] ?? NULL; | |
633 | $email = $params['email'] ?? NULL; | |
4fc2ce46 | 634 | //when insert mode check contact id or external identifier |
635 | if ($contactId || $externalId) { | |
636 | $contactType->id = $contactId; | |
637 | $contactType->external_identifier = $externalId; | |
638 | if ($contactType->find(TRUE)) { | |
639 | if ($params['contact_type'] != $contactType->contact_type) { | |
640 | return civicrm_api3_create_error("Contact Type is wrong: $contactType->contact_type"); | |
641 | } | |
642 | } | |
643 | } | |
644 | elseif ($email) { | |
645 | if (!CRM_Utils_Rule::email($email)) { | |
646 | return civicrm_api3_create_error("Invalid email address $email provided. Row was skipped"); | |
647 | } | |
648 | ||
649 | // get the contact id from duplicate contact rule, if more than one contact is returned | |
650 | // we should return error, since current interface allows only one-one mapping | |
69f296a4 | 651 | $emailParams = [ |
652 | 'email' => $email, | |
653 | 'contact_type' => $params['contact_type'], | |
654 | ]; | |
4fc2ce46 | 655 | $checkDedupe = _civicrm_api3_deprecated_duplicate_formatted_contact($emailParams); |
656 | if (!$checkDedupe['is_error']) { | |
657 | return civicrm_api3_create_error("Invalid email address(doesn't exist) $email. Row was skipped"); | |
658 | } | |
69f296a4 | 659 | $matchingContactIds = explode(',', $checkDedupe['error_message']['params'][0]); |
660 | if (count($matchingContactIds) > 1) { | |
661 | return civicrm_api3_create_error("Invalid email address(duplicate) $email. Row was skipped"); | |
662 | } | |
663 | if (count($matchingContactIds) == 1) { | |
664 | $params['contribution_contact_id'] = $matchingContactIds[0]; | |
4fc2ce46 | 665 | } |
666 | } | |
667 | elseif (!empty($params['contribution_id']) || !empty($params['trxn_id']) || !empty($params['invoice_id'])) { | |
668 | // when update mode check contribution id or trxn id or | |
669 | // invoice id | |
670 | $contactId = new CRM_Contribute_DAO_Contribution(); | |
671 | if (!empty($params['contribution_id'])) { | |
672 | $contactId->id = $params['contribution_id']; | |
673 | } | |
674 | elseif (!empty($params['trxn_id'])) { | |
675 | $contactId->trxn_id = $params['trxn_id']; | |
676 | } | |
677 | elseif (!empty($params['invoice_id'])) { | |
678 | $contactId->invoice_id = $params['invoice_id']; | |
679 | } | |
680 | if ($contactId->find(TRUE)) { | |
681 | $contactType->id = $contactId->contact_id; | |
682 | if ($contactType->find(TRUE)) { | |
683 | if ($params['contact_type'] != $contactType->contact_type) { | |
684 | return civicrm_api3_create_error("Contact Type is wrong: $contactType->contact_type"); | |
685 | } | |
686 | } | |
687 | } | |
688 | } | |
689 | else { | |
63bf48e9 | 690 | if ($this->isUpdateExisting()) { |
4fc2ce46 | 691 | return civicrm_api3_create_error("Empty Contribution and Invoice and Transaction ID. Row was skipped."); |
692 | } | |
693 | } | |
694 | break; | |
695 | ||
4fc2ce46 | 696 | case 'soft_credit': |
697 | // import contribution record according to select contact type | |
698 | // validate contact id and external identifier. | |
01c21f7e EM |
699 | foreach ($value as $softKey => $softParam) { |
700 | $values['soft_credit'][$softKey] = [ | |
701 | 'contact_id' => $this->lookupMatchingContact($softParam), | |
702 | 'soft_credit_type_id' => $softParam['soft_credit_type_id'], | |
703 | ]; | |
4fc2ce46 | 704 | } |
705 | break; | |
706 | ||
4fc2ce46 | 707 | case 'pledge_id': |
4fc2ce46 | 708 | // get total amount of from import fields |
9c1bc317 | 709 | $totalAmount = $params['total_amount'] ?? NULL; |
4fc2ce46 | 710 | // we need to get contact id $contributionContactID to |
711 | // retrieve pledge details as well as to validate pledge ID | |
712 | ||
713 | // first need to check for update mode | |
63bf48e9 | 714 | if ($this->isUpdateExisting() && |
b7dde70c | 715 | ($params['id'] || $params['trxn_id'] || $params['invoice_id']) |
4fc2ce46 | 716 | ) { |
717 | $contribution = new CRM_Contribute_DAO_Contribution(); | |
718 | if ($params['contribution_id']) { | |
719 | $contribution->id = $params['contribution_id']; | |
720 | } | |
721 | elseif ($params['trxn_id']) { | |
722 | $contribution->trxn_id = $params['trxn_id']; | |
723 | } | |
724 | elseif ($params['invoice_id']) { | |
725 | $contribution->invoice_id = $params['invoice_id']; | |
726 | } | |
727 | ||
728 | if ($contribution->find(TRUE)) { | |
729 | $contributionContactID = $contribution->contact_id; | |
730 | if (!$totalAmount) { | |
731 | $totalAmount = $contribution->total_amount; | |
732 | } | |
733 | } | |
734 | else { | |
b7dde70c | 735 | throw new CRM_Core_Exception('No match found for specified contact in pledge payment data. Row was skipped.'); |
4fc2ce46 | 736 | } |
737 | } | |
738 | else { | |
739 | // first get the contact id for given contribution record. | |
740 | if (!empty($params['contribution_contact_id'])) { | |
741 | $contributionContactID = $params['contribution_contact_id']; | |
742 | } | |
743 | elseif (!empty($params['external_identifier'])) { | |
744 | require_once 'CRM/Contact/DAO/Contact.php'; | |
745 | $contact = new CRM_Contact_DAO_Contact(); | |
746 | $contact->external_identifier = $params['external_identifier']; | |
747 | if ($contact->find(TRUE)) { | |
748 | $contributionContactID = $params['contribution_contact_id'] = $values['contribution_contact_id'] = $contact->id; | |
749 | } | |
750 | else { | |
751 | return civicrm_api3_create_error('No match found for specified contact in pledge payment data. Row was skipped.'); | |
752 | } | |
753 | } | |
754 | else { | |
755 | // we need to get contribution contact using de dupe | |
95519b12 | 756 | $error = $this->checkContactDuplicate($params); |
4fc2ce46 | 757 | |
758 | if (isset($error['error_message']['params'][0])) { | |
759 | $matchedIDs = explode(',', $error['error_message']['params'][0]); | |
760 | ||
761 | // check if only one contact is found | |
762 | if (count($matchedIDs) > 1) { | |
763 | return civicrm_api3_create_error($error['error_message']['message']); | |
764 | } | |
69f296a4 | 765 | $contributionContactID = $params['contribution_contact_id'] = $values['contribution_contact_id'] = $matchedIDs[0]; |
4fc2ce46 | 766 | } |
767 | else { | |
768 | return civicrm_api3_create_error('No match found for specified contact in contribution data. Row was skipped.'); | |
769 | } | |
770 | } | |
771 | } | |
772 | ||
773 | if (!empty($params['pledge_id'])) { | |
774 | if (CRM_Core_DAO::getFieldValue('CRM_Pledge_DAO_Pledge', $params['pledge_id'], 'contact_id') != $contributionContactID) { | |
775 | return civicrm_api3_create_error('Invalid Pledge ID provided. Contribution row was skipped.'); | |
776 | } | |
777 | $values['pledge_id'] = $params['pledge_id']; | |
778 | } | |
779 | else { | |
780 | // check if there are any pledge related to this contact, with payments pending or in progress | |
781 | require_once 'CRM/Pledge/BAO/Pledge.php'; | |
782 | $pledgeDetails = CRM_Pledge_BAO_Pledge::getContactPledges($contributionContactID); | |
783 | ||
784 | if (empty($pledgeDetails)) { | |
785 | return civicrm_api3_create_error('No open pledges found for this contact. Contribution row was skipped.'); | |
786 | } | |
69f296a4 | 787 | if (count($pledgeDetails) > 1) { |
4fc2ce46 | 788 | 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.'); |
789 | } | |
790 | ||
791 | // this mean we have only one pending / in progress pledge | |
792 | $values['pledge_id'] = $pledgeDetails[0]; | |
793 | } | |
794 | ||
795 | // we need to check if oldest payment amount equal to contribution amount | |
796 | require_once 'CRM/Pledge/BAO/PledgePayment.php'; | |
797 | $pledgePaymentDetails = CRM_Pledge_BAO_PledgePayment::getOldestPledgePayment($values['pledge_id']); | |
798 | ||
799 | if ($pledgePaymentDetails['amount'] == $totalAmount) { | |
800 | $values['pledge_payment_id'] = $pledgePaymentDetails['id']; | |
801 | } | |
802 | else { | |
803 | return civicrm_api3_create_error('Contribution and Pledge Payment amount mismatch for this record. Contribution row was skipped.'); | |
804 | } | |
805 | break; | |
806 | ||
cb9cdda8 KJ |
807 | case 'contribution_campaign_id': |
808 | if (empty(CRM_Core_DAO::getFieldValue('CRM_Campaign_DAO_Campaign', $params['contribution_campaign_id']))) { | |
809 | return civicrm_api3_create_error('Invalid Campaign ID provided. Contribution row was skipped.'); | |
810 | } | |
811 | $values['contribution_campaign_id'] = $params['contribution_campaign_id']; | |
812 | break; | |
813 | ||
4fc2ce46 | 814 | } |
815 | } | |
816 | ||
817 | if (array_key_exists('note', $params)) { | |
818 | $values['note'] = $params['note']; | |
819 | } | |
820 | ||
821 | if ($create) { | |
822 | // CRM_Contribute_BAO_Contribution::add() handles contribution_source | |
823 | // So, if $values contains contribution_source, convert it to source | |
824 | $changes = ['contribution_source' => 'source']; | |
825 | ||
826 | foreach ($changes as $orgVal => $changeVal) { | |
827 | if (isset($values[$orgVal])) { | |
828 | $values[$changeVal] = $values[$orgVal]; | |
829 | unset($values[$orgVal]); | |
830 | } | |
831 | } | |
832 | } | |
833 | ||
834 | return NULL; | |
835 | } | |
836 | ||
73edfc10 EM |
837 | /** |
838 | * Get the civicrm_mapping_field appropriate layout for the mapper input. | |
839 | * | |
840 | * The input looks something like ['street_address', 1] | |
841 | * and would be mapped to ['name' => 'street_address', 'location_type_id' => | |
842 | * 1] | |
843 | * | |
844 | * @param array $fieldMapping | |
845 | * @param int $mappingID | |
846 | * @param int $columnNumber | |
847 | * | |
848 | * @return array | |
849 | * @throws \API_Exception | |
850 | */ | |
851 | public function getMappingFieldFromMapperInput(array $fieldMapping, int $mappingID, int $columnNumber): array { | |
73edfc10 EM |
852 | return [ |
853 | 'name' => $fieldMapping[0], | |
854 | 'mapping_id' => $mappingID, | |
855 | 'column_number' => $columnNumber, | |
856 | // The name of the field to match the soft credit on is (crazily) | |
857 | // stored in 'contact_type' | |
858 | 'contact_type' => $fieldMapping[1] ?? NULL, | |
859 | // We also store the field in a sensible key, even if it isn't saved sensibly. | |
860 | 'soft_credit_match_field' => $fieldMapping[1] ?? NULL, | |
861 | // This field is actually not saved at all :-( It is lost each time. | |
862 | 'soft_credit_type_id' => $fieldMapping[2] ?? NULL, | |
863 | ]; | |
864 | } | |
865 | ||
01c21f7e EM |
866 | /** |
867 | * Lookup matching contact. | |
868 | * | |
869 | * This looks up the matching contact from the contact id, external identifier | |
870 | * or email. For the email a straight email search is done - this is equivalent | |
871 | * to what happens on a dedupe rule lookup when the only field is 'email' - but | |
872 | * we can't be sure the rule is 'just email' - and we are not collecting the | |
873 | * fields for any other lookup in the case of soft credits (if we | |
874 | * extend this function to main-contact-lookup we can handle full dedupe | |
875 | * lookups - but note the error messages will need tweaking. | |
876 | * | |
877 | * @param array $params | |
878 | * | |
879 | * @return int | |
880 | * Contact ID | |
881 | * | |
882 | * @throws \API_Exception | |
883 | * @throws \CRM_Core_Exception | |
884 | */ | |
885 | private function lookupMatchingContact(array $params): int { | |
886 | $lookupField = !empty($params['contact_id']) ? 'contact_id' : (!empty($params['external_identifier']) ? 'external_identifier' : 'email'); | |
887 | if (empty($params['email'])) { | |
888 | $contact = Contact::get(FALSE)->addSelect('id') | |
8daa4f36 | 889 | ->addWhere($lookupField === 'contact_id' ? 'id' : $lookupField, '=', $params[$lookupField]) |
01c21f7e EM |
890 | ->execute(); |
891 | if (count($contact) !== 1) { | |
892 | throw new CRM_Core_Exception(ts("Soft Credit %1 - %2 doesn't exist. Row was skipped.", | |
893 | [ | |
894 | 1 => $this->getFieldMetadata($lookupField), | |
895 | 2 => $params['contact_id'] ?? $params['external_identifier'], | |
896 | ])); | |
897 | } | |
898 | return $contact->first()['id']; | |
899 | } | |
900 | ||
901 | if (!CRM_Utils_Rule::email($params['email'])) { | |
902 | throw new CRM_Core_Exception(ts('Invalid email address %1 provided for Soft Credit. Row was skipped'), [1 => $params['email']]); | |
903 | } | |
904 | $emails = Email::get(FALSE) | |
905 | ->addWhere('contact_id.is_deleted', '=', 0) | |
906 | ->addWhere('contact_id.contact_type', '=', $this->getContactType()) | |
907 | ->addWhere('email', '=', $params['email']) | |
908 | ->addSelect('contact_id')->execute(); | |
909 | if (count($emails) === 0) { | |
910 | throw new CRM_Core_Exception(ts("Invalid email address(doesn't exist) %1 for Soft Credit. Row was skipped", [1 => $params['email']])); | |
911 | } | |
912 | if (count($emails) > 1) { | |
913 | throw new CRM_Core_Exception(ts('Invalid email address(duplicate) %1 for Soft Credit. Row was skipped', [1 => $params['email']])); | |
914 | } | |
915 | return $emails->first()['contact_id']; | |
916 | } | |
917 | ||
8bf4c85b EM |
918 | /** |
919 | * @param array $mappedField | |
920 | * Field detail as would be saved in field_mapping table | |
921 | * or as returned from getMappingFieldFromMapperInput | |
922 | * | |
923 | * @return string | |
924 | * @throws \API_Exception | |
925 | */ | |
926 | public function getMappedFieldLabel(array $mappedField): string { | |
927 | if (empty($this->importableFieldsMetadata)) { | |
928 | $this->setFieldMetadata(); | |
929 | } | |
66673bfe EM |
930 | if ($mappedField['name'] === '') { |
931 | return ''; | |
932 | } | |
8bf4c85b EM |
933 | $title = []; |
934 | $title[] = $this->getFieldMetadata($mappedField['name'])['title']; | |
935 | if ($mappedField['soft_credit_match_field']) { | |
936 | $title[] = $this->getFieldMetadata($mappedField['soft_credit_match_field'])['title']; | |
937 | } | |
938 | if ($mappedField['soft_credit_type_id']) { | |
939 | $title[] = CRM_Core_PseudoConstant::getLabel('CRM_Contribute_BAO_ContributionSoft', 'soft_credit_type_id', $mappedField['soft_credit_type_id']); | |
940 | } | |
941 | ||
942 | return implode(' - ', $title); | |
943 | } | |
944 | ||
8daa4f36 EM |
945 | /** |
946 | * Get the metadata field for which importable fields does not key the actual field name. | |
947 | * | |
948 | * @return string[] | |
949 | */ | |
950 | protected function getOddlyMappedMetadataFields(): array { | |
951 | $uniqueNames = ['contribution_id', 'contribution_contact_id', 'contribution_cancel_date', 'contribution_source', 'contribution_check_number']; | |
952 | $fields = []; | |
953 | foreach ($uniqueNames as $name) { | |
954 | $fields[$this->importableFieldsMetadata[$name]['name']] = $name; | |
955 | } | |
956 | // Include the parent fields as they could be present if required for matching ...in theory. | |
957 | return array_merge($fields, parent::getOddlyMappedMetadataFields()); | |
958 | } | |
959 | ||
6a488035 | 960 | } |