ActionSchedule - Set locale during mail hooks / delivery / etc
[civicrm-core.git] / CRM / Contribute / BAO / Contribution.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
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 |
9 +--------------------------------------------------------------------+
10 */
11
12 use Civi\Api4\Activity;
13 use Civi\Api4\ActivityContact;
14 use Civi\Api4\Contribution;
15 use Civi\Api4\ContributionRecur;
16 use Civi\Api4\LineItem;
17 use Civi\Api4\ContributionSoft;
18 use Civi\Api4\PaymentProcessor;
19 use Civi\Api4\PledgePayment;
20
21 /**
22 *
23 * @package CRM
24 * @copyright CiviCRM LLC https://civicrm.org/licensing
25 */
26 class CRM_Contribute_BAO_Contribution extends CRM_Contribute_DAO_Contribution {
27
28 /**
29 * Static field for all the contribution information that we can potentially import
30 *
31 * @var array
32 */
33 public static $_importableFields = NULL;
34
35 /**
36 * Static field for all the contribution information that we can potentially export
37 *
38 * @var array
39 */
40 public static $_exportableFields = NULL;
41
42 /**
43 * Static field to hold financial trxn id's.
44 *
45 * @var array
46 */
47 public static $_trxnIDs = NULL;
48
49 /**
50 * Field for all the objects related to this contribution.
51 *
52 * This is used from
53 * 1) deprecated function transitionComponents
54 * 2) function to send contribution receipts _assignMessageVariablesToTemplate
55 * 3) some invoice code that is copied from 2
56 * 4) odds & sods that need to be investigated and fixed.
57 *
58 * However, it is no longer used by completeOrder.
59 *
60 * @var \CRM_Member_BAO_Membership|\CRM_Event_BAO_Participant[]
61 *
62 * @deprecated
63 */
64 public $_relatedObjects = [];
65
66 /**
67 * Field for the component - either 'event' (participant) or 'contribute'
68 * (any item related to a contribution page e.g. membership, pledge, contribution)
69 * This is used for composing messages because they have dependency on the
70 * contribution_page or event page - although over time we may eliminate that
71 *
72 * @var string
73 * "contribution"\"event"
74 */
75 public $_component = NULL;
76
77 /**
78 * Possibly obsolete variable.
79 *
80 * If you use it please explain why it is set in the create function here.
81 *
82 * @var string
83 */
84 public $trxn_result_code;
85
86 /**
87 * Class constructor.
88 */
89 public function __construct() {
90 parent::__construct();
91 }
92
93 /**
94 * Takes an associative array and creates a contribution object.
95 *
96 * the function extract all the params it needs to initialize the create a
97 * contribution object. the params array could contain additional unused name/value
98 * pairs
99 *
100 * @param array $params
101 * (reference ) an assoc array of name/value pairs.
102 *
103 * @return \CRM_Contribute_BAO_Contribution
104 * @throws \CRM_Core_Exception
105 * @throws \CiviCRM_API3_Exception
106 */
107 public static function add(&$params) {
108 if (empty($params)) {
109 return NULL;
110 }
111
112 $contributionID = $params['id'] ?? NULL;
113 $action = $contributionID ? 'edit' : 'create';
114 $duplicates = [];
115 if (self::checkDuplicate($params, $duplicates, $contributionID)) {
116 $message = ts("Duplicate error - existing contribution record(s) have a matching Transaction ID or Invoice ID. Contribution record ID(s) are: %1", [1 => implode(', ', $duplicates)]);
117 throw new CRM_Core_Exception($message);
118 }
119
120 //set defaults in create mode
121 if (!$contributionID) {
122 CRM_Core_DAO::setCreateDefaults($params, self::getDefaults());
123 if (empty($params['invoice_number']) && CRM_Invoicing_Utils::isInvoicingEnabled()) {
124 $nextContributionID = CRM_Core_DAO::singleValueQuery("SELECT COALESCE(MAX(id) + 1, 1) FROM civicrm_contribution");
125 $params['invoice_number'] = self::getInvoiceNumber($nextContributionID);
126 }
127 }
128
129 $contributionStatusID = $params['contribution_status_id'] ?? NULL;
130 if (CRM_Core_PseudoConstant::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', (int) $contributionStatusID) === 'Partially paid' && empty($params['is_post_payment_create'])) {
131 CRM_Core_Error::deprecatedFunctionWarning('Setting status to partially paid other than by using Payment.create is deprecated and unreliable');
132 }
133 if (!$contributionStatusID) {
134 // Since the fee amount is expecting this (later on) ensure it is always set.
135 // It would only not be set for an update where it is unchanged.
136 $params['contribution_status_id'] = civicrm_api3('Contribution', 'getvalue', [
137 'id' => $contributionID,
138 'return' => 'contribution_status_id',
139 ]);
140 }
141 $contributionStatus = CRM_Core_PseudoConstant::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', (int) $params['contribution_status_id']);
142
143 if (!$contributionID
144 && !empty($params['membership_id'])
145 && Civi::settings()->get('deferred_revenue_enabled')
146 ) {
147 $memberStartDate = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_Membership', $params['membership_id'], 'start_date');
148 if ($memberStartDate) {
149 $params['revenue_recognition_date'] = date('Ymd', strtotime($memberStartDate));
150 }
151 }
152 self::calculateMissingAmountParams($params, $contributionID);
153
154 if (!empty($params['payment_instrument_id'])) {
155 $paymentInstruments = CRM_Contribute_PseudoConstant::paymentInstrument('name');
156 if ($params['payment_instrument_id'] != array_search('Check', $paymentInstruments)) {
157 $params['check_number'] = 'null';
158 }
159 }
160
161 $setPrevContribution = TRUE;
162 if ($contributionID && $setPrevContribution) {
163 $params['prevContribution'] = self::getOriginalContribution($contributionID);
164 }
165 $previousContributionStatus = ($contributionID && !empty($params['prevContribution'])) ? CRM_Core_PseudoConstant::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', (int) $params['prevContribution']->contribution_status_id) : NULL;
166
167 if ($contributionID && !empty($params['revenue_recognition_date'])
168 && !($previousContributionStatus === 'Pending')
169 && !self::allowUpdateRevenueRecognitionDate($contributionID)
170 ) {
171 unset($params['revenue_recognition_date']);
172 }
173
174 // Get Line Items if we don't have them already.
175 if (empty($params['line_item'])) {
176 CRM_Price_BAO_LineItem::getLineItemArray($params, $contributionID ? [$contributionID] : NULL);
177 }
178
179 // We should really ALWAYS calculate tax amount off the line items.
180 // In order to be a bit cautious we are just messaging rather than
181 // overwriting in cases where we were not previously setting it here.
182 $taxAmount = $lineTotal = 0;
183 foreach ($params['line_item'] ?? [] as $lineItems) {
184 foreach ($lineItems as $lineItem) {
185 $taxAmount += (float) ($lineItem['tax_amount'] ?? 0);
186 $lineTotal += (float) ($lineItem['line_total'] ?? 0);
187 }
188 }
189 if (($params['tax_amount'] ?? '') === 'null') {
190 CRM_Core_Error::deprecatedWarning('tax_amount should be not passed in (preferable) or a float');
191 }
192 if (!isset($params['tax_amount']) && $setPrevContribution && (isset($params['total_amount']) ||
193 isset($params['financial_type_id']))) {
194 $params['tax_amount'] = $taxAmount;
195 $params['total_amount'] = $taxAmount + $lineTotal;
196 }
197 if (isset($params['tax_amount']) && empty($params['skipLineItem'])
198 && !CRM_Utils_Money::equals($params['tax_amount'], $taxAmount, ($params['currency'] ?? Civi::settings()->get('defaultCurrency')))
199 ) {
200 CRM_Core_Error::deprecatedWarning('passing in incorrect tax amounts is deprecated');
201 }
202
203 CRM_Utils_Hook::pre($action, 'Contribution', $contributionID, $params);
204
205 $contribution = new CRM_Contribute_BAO_Contribution();
206 $contribution->copyValues($params);
207
208 $contribution->id = $contributionID;
209
210 if (empty($contribution->id)) {
211 // (only) on 'create', make sure that a valid currency is set (CRM-16845)
212 if (!CRM_Utils_Rule::currencyCode($contribution->currency)) {
213 $contribution->currency = CRM_Core_Config::singleton()->defaultCurrency;
214 }
215 }
216
217 $result = $contribution->save();
218
219 // Add financial_trxn details as part of fix for CRM-4724
220 $contribution->trxn_result_code = $params['trxn_result_code'] ?? NULL;
221 $contribution->payment_processor = $params['payment_processor'] ?? NULL;
222
223 //add Account details
224 $params['contribution'] = $contribution;
225 if (empty($params['is_post_payment_create'])) {
226 // If this is being called from the Payment.create api/ BAO then that Entity
227 // takes responsibility for the financial transactions. In fact calling Payment.create
228 // to add payments & having it call completetransaction and / or contribution.create
229 // to update related entities is the preferred flow.
230 // Note that leveraging this parameter for any other code flow is not supported and
231 // is likely to break in future and / or cause serious problems in your data.
232 // https://github.com/civicrm/civicrm-core/pull/14673
233 self::recordFinancialAccounts($params);
234 }
235
236 if (self::isUpdateToRecurringContribution($params)) {
237 CRM_Contribute_BAO_ContributionRecur::updateOnNewPayment(
238 (!empty($params['contribution_recur_id']) ? $params['contribution_recur_id'] : $params['prevContribution']->contribution_recur_id),
239 $contributionStatus,
240 $params['receive_date'] ?? 'now'
241 );
242 }
243
244 $params['contribution_id'] = $contribution->id;
245
246 if (!empty($params['custom']) &&
247 is_array($params['custom'])
248 ) {
249 CRM_Core_BAO_CustomValueTable::store($params['custom'], 'civicrm_contribution', $contribution->id, $action);
250 }
251
252 CRM_Contact_BAO_GroupContactCache::opportunisticCacheFlush();
253
254 CRM_Utils_Hook::post($action, 'Contribution', $contribution->id, $contribution);
255 return $result;
256 }
257
258 /**
259 * Is this contribution updating an existing recurring contribution.
260 *
261 * We need to upd the status of the linked recurring contribution if we have a new payment against it, or the initial
262 * pending payment is being confirmed (or failing).
263 *
264 * @param array $params
265 *
266 * @return bool
267 */
268 public static function isUpdateToRecurringContribution($params) {
269 if (!empty($params['contribution_recur_id']) && empty($params['id'])) {
270 return TRUE;
271 }
272 if (empty($params['prevContribution']) || empty($params['contribution_status_id'])) {
273 return FALSE;
274 }
275 if (empty($params['contribution_recur_id']) && empty($params['prevContribution']->contribution_recur_id)) {
276 return FALSE;
277 }
278 $contributionStatus = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name');
279 if ($params['prevContribution']->contribution_status_id == array_search('Pending', $contributionStatus)) {
280 return TRUE;
281 }
282 return FALSE;
283 }
284
285 /**
286 * Get defaults for new entity.
287 *
288 * @return array
289 */
290 public static function getDefaults() {
291 return [
292 'payment_instrument_id' => key(CRM_Core_OptionGroup::values('payment_instrument',
293 FALSE, FALSE, FALSE, 'AND is_default = 1')
294 ),
295 'contribution_status_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'),
296 'receive_date' => date('Y-m-d H:i:s'),
297 ];
298 }
299
300 /**
301 * Fetch the object and store the values in the values array.
302 *
303 * @param array $params
304 * Input parameters to find object.
305 * @param array $values
306 * Output values of the object.
307 * @param array $ids
308 * The array that holds all the db ids.
309 *
310 * @return CRM_Contribute_BAO_Contribution|null
311 * The found object or null
312 */
313 public static function getValues($params, &$values = [], &$ids = []) {
314 if (empty($params)) {
315 return NULL;
316 }
317 $contribution = new CRM_Contribute_BAO_Contribution();
318
319 $contribution->copyValues($params);
320
321 if ($contribution->find(TRUE)) {
322 $ids['contribution'] = $contribution->id;
323
324 CRM_Core_DAO::storeValues($contribution, $values);
325
326 return $contribution;
327 }
328 // return by reference
329 $null = NULL;
330 return $null;
331 }
332
333 /**
334 * Get the values and resolve the most common mappings.
335 *
336 * Since contribution status is resolved in almost every function that calls getValues it makes
337 * sense to have an extra function to resolve it rather than repeat the code.
338 *
339 * Think carefully before adding more mappings to be resolved as there could be performance implications
340 * if this function starts to be called from more iterative functions.
341 *
342 * @param array $params
343 * Input parameters to find object.
344 *
345 * @return array
346 * Array of the found contribution.
347 * @throws CRM_Core_Exception
348 */
349 public static function getValuesWithMappings($params) {
350 $values = $ids = [];
351 $contribution = self::getValues($params, $values, $ids);
352 if (is_null($contribution)) {
353 throw new CRM_Core_Exception('No contribution found');
354 }
355 $values['contribution_status'] = CRM_Core_PseudoConstant::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', $values['contribution_status_id']);
356 return $values;
357 }
358
359 /**
360 * Calculate net_amount & fee_amount if they are not set.
361 *
362 * Net amount should be total - fee.
363 * This should only be called for new contributions.
364 *
365 * @param array $params
366 * Params for a new contribution before they are saved.
367 * @param int|null $contributionID
368 * Contribution ID if we are dealing with an update.
369 *
370 * @throws \CiviCRM_API3_Exception
371 */
372 public static function calculateMissingAmountParams(&$params, $contributionID) {
373 if (!$contributionID && (!isset($params['fee_amount']) || $params['fee_amount'] === '')) {
374 if (isset($params['total_amount']) && isset($params['net_amount'])) {
375 $params['fee_amount'] = $params['total_amount'] - $params['net_amount'];
376 }
377 else {
378 $params['fee_amount'] = 0;
379 }
380 }
381 if (!isset($params['net_amount'])) {
382 if (!$contributionID) {
383 $params['net_amount'] = $params['total_amount'] - $params['fee_amount'];
384 }
385 else {
386 if (isset($params['fee_amount']) || isset($params['total_amount'])) {
387 // We have an existing contribution and fee_amount or total_amount has been passed in but not net_amount.
388 // net_amount may need adjusting.
389 $contribution = civicrm_api3('Contribution', 'getsingle', [
390 'id' => $contributionID,
391 'return' => ['total_amount', 'net_amount', 'fee_amount'],
392 ]);
393 $totalAmount = (isset($params['total_amount']) ? (float) $params['total_amount'] : (float) CRM_Utils_Array::value('total_amount', $contribution));
394 $feeAmount = (isset($params['fee_amount']) ? (float) $params['fee_amount'] : (float) CRM_Utils_Array::value('fee_amount', $contribution));
395 $params['net_amount'] = $totalAmount - $feeAmount;
396 }
397 }
398 }
399 }
400
401 /**
402 * @param $params
403 * @param $billingLocationTypeID
404 *
405 * @return array
406 */
407 protected static function getBillingAddressParams($params, $billingLocationTypeID) {
408 $hasBillingField = FALSE;
409 $billingFields = [
410 'street_address',
411 'city',
412 'state_province_id',
413 'postal_code',
414 'country_id',
415 ];
416
417 //build address array
418 $addressParams = [];
419 $addressParams['location_type_id'] = $billingLocationTypeID;
420 $addressParams['is_billing'] = 1;
421
422 $billingFirstName = $params['billing_first_name'] ?? NULL;
423 $billingMiddleName = $params['billing_middle_name'] ?? NULL;
424 $billingLastName = $params['billing_last_name'] ?? NULL;
425 $addressParams['address_name'] = "{$billingFirstName}" . CRM_Core_DAO::VALUE_SEPARATOR . "{$billingMiddleName}" . CRM_Core_DAO::VALUE_SEPARATOR . "{$billingLastName}";
426
427 foreach ($billingFields as $value) {
428 $addressParams[$value] = $params["billing_{$value}-{$billingLocationTypeID}"] ?? NULL;
429 if (!empty($addressParams[$value])) {
430 $hasBillingField = TRUE;
431 }
432 }
433 return [$hasBillingField, $addressParams];
434 }
435
436 /**
437 * Get address params ready to be passed to the payment processor.
438 *
439 * We need address params in a couple of formats. For the payment processor we wan state_province_id-5.
440 * To create an address we need state_province_id.
441 *
442 * @param array $params
443 * @param int $billingLocationTypeID
444 *
445 * @return array
446 */
447 public static function getPaymentProcessorReadyAddressParams($params, $billingLocationTypeID) {
448 [$hasBillingField, $addressParams] = self::getBillingAddressParams($params, $billingLocationTypeID);
449 foreach ($addressParams as $name => $field) {
450 if (substr($name, 0, 8) == 'billing_') {
451 $addressParams[substr($name, 9)] = $addressParams[$field];
452 }
453 }
454 return [$hasBillingField, $addressParams];
455 }
456
457 /**
458 * Get the number of terms for this contribution for a given membership type
459 * based on querying the line item table and relevant price field values
460 * Note that any one contribution should only be able to have one line item relating to a particular membership
461 * type
462 *
463 * @param int $membershipTypeID
464 *
465 * @param int $contributionID
466 *
467 * @return int
468 */
469 public static function getNumTermsByContributionAndMembershipType($membershipTypeID, $contributionID) {
470 $numTerms = CRM_Core_DAO::singleValueQuery("
471 SELECT v.membership_num_terms FROM civicrm_line_item li
472 LEFT JOIN civicrm_price_field_value v ON li.price_field_value_id = v.id
473 WHERE contribution_id = %1 AND membership_type_id = %2",
474 [1 => [$contributionID, 'Integer'], 2 => [$membershipTypeID, 'Integer']]
475 );
476 // default of 1 is precautionary
477 return empty($numTerms) ? 1 : $numTerms;
478 }
479
480 /**
481 * Takes an associative array and creates a contribution object.
482 *
483 * @param array $params
484 * (reference ) an assoc array of name/value pairs.
485 *
486 * @return CRM_Contribute_BAO_Contribution
487 *
488 * @throws \API_Exception
489 * @throws \CRM_Core_Exception
490 * @throws \CiviCRM_API3_Exception
491 */
492 public static function create(&$params) {
493
494 $transaction = new CRM_Core_Transaction();
495
496 try {
497 $contribution = self::add($params);
498 }
499 catch (CRM_Core_Exception $e) {
500 $transaction->rollback();
501 throw $e;
502 }
503
504 $params['contribution_id'] = $contribution->id;
505 $session = CRM_Core_Session::singleton();
506
507 if (!empty($params['note'])) {
508 $noteParams = [
509 'entity_table' => 'civicrm_contribution',
510 'note' => $params['note'],
511 'entity_id' => $contribution->id,
512 'contact_id' => $session->get('userID'),
513 ];
514 if (!$noteParams['contact_id']) {
515 $noteParams['contact_id'] = $params['contact_id'];
516 }
517 CRM_Core_BAO_Note::add($noteParams);
518 }
519
520 CRM_Contribute_BAO_ContributionSoft::processSoftContribution($params, $contribution);
521
522 if (!empty($params['id']) && !empty($params['contribution_status_id'])
523 ) {
524 self::disconnectPledgePaymentsIfCancelled((int) $params['id'], CRM_Core_PseudoConstant::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', $params['contribution_status_id']));
525 }
526 $transaction->commit();
527
528 if (empty($contribution->contact_id)) {
529 $contribution->find(TRUE);
530 }
531
532 $isCompleted = ('Completed' === CRM_Core_PseudoConstant::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', $contribution->contribution_status_id));
533 if (!empty($params['on_behalf'])
534 || $isCompleted
535 ) {
536 $existingActivity = Activity::get(FALSE)->setWhere([
537 ['source_record_id', '=', $contribution->id],
538 ['activity_type_id:name', '=', 'Contribution'],
539 ])->execute()->first();
540
541 $campaignParams = isset($params['campaign_id']) ? ['campaign_id' => ($params['campaign_id'] ?? NULL)] : [];
542 $activityParams = array_merge([
543 'activity_type_id:name' => 'Contribution',
544 'source_record_id' => $contribution->id,
545 'activity_date_time' => $contribution->receive_date,
546 'is_test' => (bool) $contribution->is_test,
547 'status_id:name' => $isCompleted ? 'Completed' : 'Scheduled',
548 'skipRecentView' => TRUE,
549 'subject' => CRM_Activity_BAO_Activity::getActivitySubject($contribution),
550 'id' => $existingActivity['id'] ?? NULL,
551 ], $campaignParams);
552 if (!$activityParams['id']) {
553 $activityParams['source_contact_id'] = (int) ($params['source_contact_id'] ?? (CRM_Core_Session::getLoggedInContactID() ?: $contribution->contact_id));
554 $activityParams['target_contact_id'] = ($activityParams['source_contact_id'] === (int) $contribution->contact_id) ? [] : [$contribution->contact_id];
555 }
556 else {
557 [$sourceContactId, $targetContactId] = self::getActivitySourceAndTarget($activityParams['id']);
558
559 if (empty($targetContactId) && $sourceContactId != $contribution->contact_id) {
560 // If no target contact exists and the source contact is not equal to
561 // the contribution contact, update the source contact
562 $activityParams['source_contact_id'] = $contribution->contact_id;
563 }
564 elseif (isset($targetContactId) && $targetContactId != $contribution->contact_id) {
565 // If a target contact exists and it is not equal to the contribution
566 // contact, update the target contact
567 $activityParams['target_contact_id'] = [$contribution->contact_id];
568 }
569 }
570 Activity::save(FALSE)->addRecord($activityParams)->execute();
571 }
572
573 // do not add to recent items for import, CRM-4399
574 if (empty($params['skipRecentView'])) {
575 $url = CRM_Utils_System::url('civicrm/contact/view/contribution',
576 "action=view&reset=1&id={$contribution->id}&cid={$contribution->contact_id}&context=home"
577 );
578 // in some update cases we need to get extra fields - ie an update that doesn't pass in all these params
579 $titleFields = [
580 'contact_id',
581 'total_amount',
582 'currency',
583 'financial_type_id',
584 ];
585 $retrieveRequired = 0;
586 foreach ($titleFields as $titleField) {
587 if (!isset($contribution->$titleField)) {
588 $retrieveRequired = 1;
589 break;
590 }
591 }
592 if ($retrieveRequired == 1) {
593 $contribution->find(TRUE);
594 }
595 $financialType = CRM_Contribute_PseudoConstant::financialType($contribution->financial_type_id);
596 $title = CRM_Contact_BAO_Contact::displayName($contribution->contact_id) . ' - (' . CRM_Utils_Money::format($contribution->total_amount, $contribution->currency) . ' ' . ' - ' . $financialType . ')';
597
598 $recentOther = [];
599 if (CRM_Core_Permission::checkActionPermission('CiviContribute', CRM_Core_Action::UPDATE)) {
600 $recentOther['editUrl'] = CRM_Utils_System::url('civicrm/contact/view/contribution',
601 "action=update&reset=1&id={$contribution->id}&cid={$contribution->contact_id}&context=home"
602 );
603 }
604
605 if (CRM_Core_Permission::checkActionPermission('CiviContribute', CRM_Core_Action::DELETE)) {
606 $recentOther['deleteUrl'] = CRM_Utils_System::url('civicrm/contact/view/contribution',
607 "action=delete&reset=1&id={$contribution->id}&cid={$contribution->contact_id}&context=home"
608 );
609 }
610
611 // add the recently created Contribution
612 CRM_Utils_Recent::add($title,
613 $url,
614 $contribution->id,
615 'Contribution',
616 $contribution->contact_id,
617 NULL,
618 $recentOther
619 );
620 }
621
622 return $contribution;
623 }
624
625 /**
626 * Get the values for pseudoconstants for name->value and reverse.
627 *
628 * @param array $defaults
629 * (reference) the default values, some of which need to be resolved.
630 * @param bool $reverse
631 * True if we want to resolve the values in the reverse direction (value -> name).
632 */
633 public static function resolveDefaults(&$defaults, $reverse = FALSE) {
634 self::lookupValue($defaults, 'financial_type', CRM_Contribute_PseudoConstant::financialType(), $reverse);
635 self::lookupValue($defaults, 'payment_instrument', CRM_Contribute_PseudoConstant::paymentInstrument(), $reverse);
636 self::lookupValue($defaults, 'contribution_status', CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'label'), $reverse);
637 self::lookupValue($defaults, 'pcp', CRM_Contribute_PseudoConstant::pcPage(), $reverse);
638 }
639
640 /**
641 * Convert associative array names to values and vice-versa.
642 *
643 * This function is used by both the web form layer and the api. Note that
644 * the api needs the name => value conversion, also the view layer typically
645 * requires value => name conversion
646 *
647 * @param array $defaults
648 * @param string $property
649 * @param array $lookup
650 * @param bool $reverse
651 *
652 * @return bool
653 */
654 public static function lookupValue(&$defaults, $property, &$lookup, $reverse) {
655 $id = $property . '_id';
656
657 $src = $reverse ? $property : $id;
658 $dst = $reverse ? $id : $property;
659
660 if (!array_key_exists($src, $defaults)) {
661 return FALSE;
662 }
663
664 $look = $reverse ? array_flip($lookup) : $lookup;
665
666 if (is_array($look)) {
667 if (!array_key_exists($defaults[$src], $look)) {
668 return FALSE;
669 }
670 }
671 $defaults[$dst] = $look[$defaults[$src]];
672 return TRUE;
673 }
674
675 /**
676 * Retrieve DB object based on input parameters.
677 *
678 * It also stores all the retrieved values in the default array.
679 *
680 * @param array $params
681 * (reference ) an assoc array of name/value pairs.
682 * @param array $defaults
683 * (reference ) an assoc array to hold the name / value pairs.
684 * in a hierarchical manner
685 * @param array $ids
686 * (reference) the array that holds all the db ids.
687 *
688 * @return CRM_Contribute_BAO_Contribution
689 */
690 public static function retrieve(&$params, &$defaults = [], &$ids = []) {
691 $contribution = CRM_Contribute_BAO_Contribution::getValues($params, $defaults, $ids);
692 return $contribution;
693 }
694
695 /**
696 * Combine all the importable fields from the lower levels object.
697 *
698 * The ordering is important, since currently we do not have a weight
699 * scheme. Adding weight is super important and should be done in the
700 * next week or so, before this can be called complete.
701 *
702 * @param string $contactType
703 * @param bool $status
704 *
705 * @return array
706 * array of importable Fields
707 */
708 public static function &importableFields($contactType = 'Individual', $status = TRUE) {
709 if (!self::$_importableFields) {
710 if (!self::$_importableFields) {
711 self::$_importableFields = [];
712 }
713
714 if (!$status) {
715 $fields = ['' => ['title' => ts('- do not import -')]];
716 }
717 else {
718 $fields = ['' => ['title' => ts('- Contribution Fields -')]];
719 }
720
721 $note = CRM_Core_DAO_Note::import();
722 $tmpFields = CRM_Contribute_DAO_Contribution::import();
723 unset($tmpFields['option_value']);
724 $optionFields = CRM_Core_OptionValue::getFields($mode = 'contribute');
725 $contactFields = CRM_Contact_BAO_Contact::importableFields($contactType, NULL);
726
727 // Using new Dedupe rule.
728 $ruleParams = [
729 'contact_type' => $contactType,
730 'used' => 'Unsupervised',
731 ];
732 $fieldsArray = CRM_Dedupe_BAO_DedupeRule::dedupeRuleFields($ruleParams);
733 $tmpContactField = [];
734 if (is_array($fieldsArray)) {
735 foreach ($fieldsArray as $value) {
736 //skip if there is no dupe rule
737 if ($value == 'none') {
738 continue;
739 }
740 $customFieldId = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField',
741 $value,
742 'id',
743 'column_name'
744 );
745 $value = $customFieldId ? 'custom_' . $customFieldId : $value;
746 $tmpContactField[trim($value)] = $contactFields[trim($value)];
747 if (!$status) {
748 $title = $tmpContactField[trim($value)]['title'] . ' ' . ts('(match to contact)');
749 }
750 else {
751 $title = $tmpContactField[trim($value)]['title'];
752 }
753 $tmpContactField[trim($value)]['title'] = $title;
754 }
755 }
756
757 $tmpContactField['external_identifier'] = $contactFields['external_identifier'];
758 $tmpContactField['external_identifier']['title'] = $contactFields['external_identifier']['title'] . ' ' . ts('(match to contact)');
759 $tmpFields['contribution_contact_id']['title'] = $tmpFields['contribution_contact_id']['title'] . ' ' . ts('(match to contact)');
760 $fields = array_merge($fields, $tmpContactField);
761 $fields = array_merge($fields, $tmpFields);
762 $fields = array_merge($fields, $note);
763 $fields = array_merge($fields, $optionFields);
764 $fields = array_merge($fields, CRM_Financial_DAO_FinancialType::export());
765 $fields = array_merge($fields, CRM_Core_BAO_CustomField::getFieldsForImport('Contribution'));
766 self::$_importableFields = $fields;
767 }
768 return self::$_importableFields;
769 }
770
771 /**
772 * Combine all the exportable fields from the lower level objects.
773 *
774 * @param bool $checkPermission
775 *
776 * @return array
777 * array of exportable Fields
778 */
779 public static function &exportableFields($checkPermission = TRUE) {
780 if (!self::$_exportableFields) {
781 if (!self::$_exportableFields) {
782 self::$_exportableFields = [];
783 }
784
785 $fields = CRM_Contribute_DAO_Contribution::export();
786 if (CRM_Contribute_BAO_Query::isSiteHasProducts()) {
787 $fields = array_merge(
788 $fields,
789 CRM_Contribute_DAO_Product::export(),
790 CRM_Contribute_DAO_ContributionProduct::export(),
791 // CRM-16713 - contribution search by Premiums on 'Find Contribution' form.
792 [
793 'contribution_product_id' => [
794 'title' => ts('Premium'),
795 'name' => 'contribution_product_id',
796 'where' => 'civicrm_product.id',
797 'data_type' => CRM_Utils_Type::T_INT,
798 ],
799 ]
800 );
801 }
802
803 $financialAccount = CRM_Financial_DAO_FinancialAccount::export();
804
805 $contributionPage = [
806 'contribution_page' => [
807 'title' => ts('Contribution Page'),
808 'name' => 'contribution_page',
809 'where' => 'civicrm_contribution_page.title',
810 'data_type' => CRM_Utils_Type::T_STRING,
811 ],
812 ];
813
814 $contributionNote = [
815 'contribution_note' => [
816 'title' => ts('Contribution Note'),
817 'name' => 'contribution_note',
818 'data_type' => CRM_Utils_Type::T_TEXT,
819 ],
820 ];
821
822 $extraFields = [
823 'contribution_batch' => [
824 'title' => ts('Batch Name'),
825 ],
826 ];
827
828 // CRM-17787
829 $campaignTitle = [
830 'contribution_campaign_title' => [
831 'title' => ts('Campaign Title'),
832 'name' => 'campaign_title',
833 'where' => 'civicrm_campaign.title',
834 'data_type' => CRM_Utils_Type::T_STRING,
835 ],
836 ];
837 $softCreditFields = [
838 'contribution_soft_credit_name' => [
839 'name' => 'contribution_soft_credit_name',
840 'title' => ts('Soft Credit For'),
841 'where' => 'civicrm_contact_d.display_name',
842 'data_type' => CRM_Utils_Type::T_STRING,
843 ],
844 'contribution_soft_credit_amount' => [
845 'name' => 'contribution_soft_credit_amount',
846 'title' => ts('Soft Credit Amount'),
847 'where' => 'civicrm_contribution_soft.amount',
848 'data_type' => CRM_Utils_Type::T_MONEY,
849 ],
850 'contribution_soft_credit_type' => [
851 'name' => 'contribution_soft_credit_type',
852 'title' => ts('Soft Credit Type'),
853 'where' => 'contribution_softcredit_type.label',
854 'data_type' => CRM_Utils_Type::T_STRING,
855 ],
856 'contribution_soft_credit_contribution_id' => [
857 'name' => 'contribution_soft_credit_contribution_id',
858 'title' => ts('Soft Credit For Contribution ID'),
859 'where' => 'civicrm_contribution_soft.contribution_id',
860 'data_type' => CRM_Utils_Type::T_INT,
861 ],
862 'contribution_soft_credit_contact_id' => [
863 'name' => 'contribution_soft_credit_contact_id',
864 'title' => ts('Soft Credit For Contact ID'),
865 'where' => 'civicrm_contact_d.id',
866 'data_type' => CRM_Utils_Type::T_INT,
867 ],
868 ];
869
870 $fields = array_merge($fields, $contributionPage,
871 $contributionNote, $extraFields, $softCreditFields, $financialAccount, $campaignTitle,
872 CRM_Core_BAO_CustomField::getFieldsForImport('Contribution', FALSE, FALSE, FALSE, $checkPermission)
873 );
874
875 self::$_exportableFields = $fields;
876 }
877
878 return self::$_exportableFields;
879 }
880
881 /**
882 * Record an activity when a payment is received.
883 *
884 * @todo this is intended to be moved to payment BAO class as a protected function
885 * on that class. Currently being cleaned up. The addActivityForPayment doesn't really
886 * merit it's own function as it makes the code less rather than more readable.
887 *
888 * @param int $contributionId
889 * @param int $participantId
890 * @param string $totalAmount
891 * @param string $currency
892 * @param string $trxnDate
893 *
894 * @throws \CRM_Core_Exception
895 * @throws \CiviCRM_API3_Exception
896 */
897 public static function recordPaymentActivity($contributionId, $participantId, $totalAmount, $currency, $trxnDate) {
898 $activityType = ($totalAmount < 0) ? 'Refund' : 'Payment';
899
900 if ($participantId) {
901 $inputParams['id'] = $participantId;
902 $values = [];
903 $ids = [];
904 $entityObj = CRM_Event_BAO_Participant::getValues($inputParams, $values, $ids);
905 $entityObj = $entityObj[$participantId];
906 $title = CRM_Core_DAO::getFieldValue('CRM_Event_BAO_Event', $entityObj->event_id, 'title');
907 }
908 else {
909 $entityObj = new CRM_Contribute_BAO_Contribution();
910 $entityObj->id = $contributionId;
911 $entityObj->find(TRUE);
912 $title = ts('Contribution');
913 }
914 // @todo per block above this is not a logical splitting off of functionality.
915 self::addActivityForPayment($entityObj->contact_id, $activityType, $title, $contributionId, $totalAmount, $currency, $trxnDate);
916 }
917
918 /**
919 * Get the value for the To Financial Account.
920 *
921 * @param $contribution
922 * @param $params
923 *
924 * @return int
925 */
926 public static function getToFinancialAccount($contribution, $params) {
927 if (!empty($params['payment_processor_id'])) {
928 return CRM_Contribute_PseudoConstant::getRelationalFinancialAccount($params['payment_processor_id'], NULL, 'civicrm_payment_processor');
929 }
930 if (!empty($params['payment_instrument_id'])) {
931 return CRM_Financial_BAO_FinancialTypeAccount::getInstrumentFinancialAccount($contribution['payment_instrument_id']);
932 }
933 else {
934 $relationTypeId = key(CRM_Core_PseudoConstant::accountOptionValues('financial_account_type', NULL, " AND v.name LIKE 'Asset' "));
935 $queryParams = [1 => [$relationTypeId, 'Integer']];
936 return CRM_Core_DAO::singleValueQuery("SELECT id FROM civicrm_financial_account WHERE is_default = 1 AND financial_account_type_id = %1", $queryParams);
937 }
938 }
939
940 /**
941 * Get memberships related to the contribution.
942 *
943 * @param int $contributionID
944 *
945 * @return array
946 * @throws \API_Exception
947 */
948 protected static function getRelatedMemberships(int $contributionID): array {
949 $membershipIDs = array_keys((array) LineItem::get(FALSE)
950 ->addWhere('contribution_id', '=', $contributionID)
951 ->addWhere('entity_table', '=', 'civicrm_membership')
952 ->addSelect('entity_id')
953 ->execute()->indexBy('entity_id'));
954
955 $doubleCheckParams = [
956 'return' => 'membership_id',
957 'contribution_id' => $contributionID,
958 ];
959 if (!empty($membershipIDs)) {
960 $doubleCheckParams['membership_id'] = ['NOT IN' => $membershipIDs];
961 }
962 $membershipPayments = civicrm_api3('MembershipPayment', 'get', $doubleCheckParams)['values'];
963 if (!empty($membershipPayments)) {
964 $membershipIDs = [];
965 CRM_Core_Error::deprecatedWarning('Not having valid line items for membership payments is invalid.');
966 foreach ($membershipPayments as $membershipPayment) {
967 $membershipIDs[] = $membershipPayment['membership_id'];
968 }
969 }
970 if (empty($membershipIDs)) {
971 return [];
972 }
973 // We could combine this with the MembershipPayment.get - we'd
974 // need to re-wrangle the params (here or in the calling function)
975 // as they would then me membership.contact_id, membership.is_test etc
976 return civicrm_api3('Membership', 'get', [
977 'id' => ['IN' => $membershipIDs],
978 'return' => ['id', 'contact_id', 'membership_type_id', 'is_test', 'status_id', 'end_date'],
979 ])['values'];
980 }
981
982 /**
983 * Do any accounting updates required as a result of a contribution status change.
984 *
985 * Currently we have a bit of a roundabout where adding a payment results in this being called &
986 * this may attempt to add a payment. We need to resolve that....
987 *
988 * The 'right' way to add payments or refunds is through the Payment.create api. That api
989 * then updates the contribution but this process should not also record another financial trxn.
990 * Currently we have weak detection fot that scenario & where it is detected the first returned
991 * value is FALSE - meaning 'do not continue'.
992 *
993 * We should also look at the fact that the calling function - updateFinancialAccounts
994 * bunches together some disparate processes rather than having separate appropriate
995 * functions.
996 *
997 * @param array $params
998 *
999 * @return bool
1000 * Return indicates whether the updateFinancialAccounts function should continue.
1001 */
1002 private static function updateFinancialAccountsOnContributionStatusChange(&$params) {
1003 $previousContributionStatus = CRM_Contribute_PseudoConstant::contributionStatus($params['prevContribution']->contribution_status_id, 'name');
1004 $currentContributionStatus = CRM_Core_PseudoConstant::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', $params['contribution']->contribution_status_id);
1005
1006 if ((($previousContributionStatus === 'Partially paid' && $currentContributionStatus === 'Completed')
1007 || ($previousContributionStatus === 'Pending refund' && $currentContributionStatus === 'Completed')
1008 // This concept of pay_later as different to any other sort of pending is deprecated & it's unclear
1009 // why it is here or where it is handled instead.
1010 || ($previousContributionStatus === 'Pending' && $params['prevContribution']->is_pay_later == TRUE
1011 && $currentContributionStatus === 'Partially paid'))
1012 ) {
1013 return FALSE;
1014 }
1015
1016 if (self::isContributionUpdateARefund($params['prevContribution']->contribution_status_id, $params['contribution']->contribution_status_id)) {
1017 // @todo we should stop passing $params by reference - splitting this out would be a step towards that.
1018 $params['trxnParams']['total_amount'] = -$params['total_amount'];
1019 }
1020 elseif (($previousContributionStatus === 'Pending'
1021 && $params['prevContribution']->is_pay_later) || $previousContributionStatus === 'In Progress'
1022 ) {
1023 $financialTypeID = !empty($params['financial_type_id']) ? $params['financial_type_id'] : $params['prevContribution']->financial_type_id;
1024 $arAccountId = CRM_Contribute_PseudoConstant::getRelationalFinancialAccount($financialTypeID, 'Accounts Receivable Account is');
1025
1026 if ($currentContributionStatus === 'Cancelled') {
1027 // @todo we should stop passing $params by reference - splitting this out would be a step towards that.
1028 $params['trxnParams']['to_financial_account_id'] = $arAccountId;
1029 $params['trxnParams']['total_amount'] = -$params['total_amount'];
1030 }
1031 else {
1032 // @todo we should stop passing $params by reference - splitting this out would be a step towards that.
1033 $params['trxnParams']['from_financial_account_id'] = $arAccountId;
1034 }
1035 }
1036
1037 if (($previousContributionStatus === 'Pending'
1038 || $previousContributionStatus === 'In Progress')
1039 && ($currentContributionStatus === 'Completed')
1040 ) {
1041 if (empty($params['line_item'])) {
1042 //CRM-15296
1043 //@todo - check with Joe regarding this situation - payment processors create pending transactions with no line items
1044 // when creating recurring membership payment - there are 2 lines to comment out in contributionPageTest if fixed
1045 // & this can be removed
1046 return FALSE;
1047 }
1048 // @todo we should stop passing $params by reference - splitting this out would be a step towards that.
1049 // This is an update so original currency if none passed in.
1050 $params['trxnParams']['currency'] = CRM_Utils_Array::value('currency', $params, $params['prevContribution']->currency);
1051
1052 self::recordAlwaysAccountsReceivable($params['trxnParams'], $params);
1053 $trxn = CRM_Core_BAO_FinancialTrxn::create($params['trxnParams']);
1054 // @todo we should stop passing $params by reference - splitting this out would be a step towards that.
1055 $params['entity_id'] = self::$_trxnIDs[] = $trxn->id;
1056
1057 $sql = "SELECT id, amount FROM civicrm_financial_item WHERE entity_id = %1 and entity_table = 'civicrm_line_item'";
1058
1059 $entityParams = [
1060 'entity_table' => 'civicrm_financial_item',
1061 ];
1062 foreach ($params['line_item'] as $fieldId => $fields) {
1063 foreach ($fields as $fieldValueId => $lineItemDetails) {
1064 self::updateFinancialItemForLineItemToPaid($lineItemDetails['id']);
1065 $fparams = [
1066 1 => [$lineItemDetails['id'], 'Integer'],
1067 ];
1068 $financialItem = CRM_Core_DAO::executeQuery($sql, $fparams);
1069 while ($financialItem->fetch()) {
1070 $entityParams['entity_id'] = $financialItem->id;
1071 $entityParams['amount'] = $financialItem->amount;
1072 foreach (self::$_trxnIDs as $tID) {
1073 $entityParams['financial_trxn_id'] = $tID;
1074 CRM_Financial_BAO_FinancialItem::createEntityTrxn($entityParams);
1075 }
1076 }
1077 }
1078 }
1079 return FALSE;
1080 }
1081 return TRUE;
1082 }
1083
1084 /**
1085 * It is possible to override the membership id that is updated from the payment processor.
1086 *
1087 * Historically Paypal does this & it still does if it determines data is messed up - see
1088 * https://lab.civicrm.org/dev/membership/issues/13
1089 *
1090 * Read the comment block on repeattransaction for more information
1091 * about how things should work.
1092 *
1093 * @param int $contributionID
1094 * @param array $input
1095 *
1096 * @throws \CiviCRM_API3_Exception
1097 */
1098 protected static function handleMembershipIDOverride($contributionID, $input) {
1099 if (!empty($input['membership_id'])) {
1100 Civi::log()->debug('The related membership id has been overridden - this may impact data - see https://github.com/civicrm/civicrm-core/pull/15053');
1101 civicrm_api3('MembershipPayment', 'create', ['contribution_id' => $contributionID, 'membership_id' => $input['membership_id']]);
1102 }
1103 }
1104
1105 /**
1106 * Update all financial items related to the line item tto have a status of paid.
1107 *
1108 * @param int $lineItemID
1109 */
1110 private static function updateFinancialItemForLineItemToPaid($lineItemID) {
1111 $fparams = [
1112 1 => [
1113 CRM_Core_PseudoConstant::getKey('CRM_Financial_BAO_FinancialItem', 'status_id', 'Paid'),
1114 'Integer',
1115 ],
1116 2 => [$lineItemID, 'Integer'],
1117 ];
1118 $query = "UPDATE civicrm_financial_item SET status_id = %1 WHERE entity_id = %2 and entity_table = 'civicrm_line_item'";
1119 CRM_Core_DAO::executeQuery($query, $fparams);
1120 }
1121
1122 /**
1123 * Create the financial items for the line.
1124 *
1125 * @param array $params
1126 * @param string $context
1127 * @param array $fields
1128 * @param array $previousLineItems
1129 * @param array $inputParams
1130 * @param bool $isARefund
1131 * @param array $trxnIds
1132 * @param int $fieldId
1133 *
1134 * @return array
1135 */
1136 private static function createFinancialItemsForLine($params, $context, $fields, array $previousLineItems, array $inputParams, bool $isARefund, $trxnIds, $fieldId): array {
1137 foreach ($fields as $fieldValueId => $lineItemDetails) {
1138 $prevFinancialItem = CRM_Financial_BAO_FinancialItem::getPreviousFinancialItem($lineItemDetails['id']);
1139 $receiveDate = CRM_Utils_Date::isoToMysql($params['prevContribution']->receive_date);
1140 if ($params['contribution']->receive_date) {
1141 $receiveDate = CRM_Utils_Date::isoToMysql($params['contribution']->receive_date);
1142 }
1143
1144 $financialAccount = CRM_Contribute_BAO_FinancialProcessor::getFinancialAccountForStatusChangeTrxn($params, CRM_Utils_Array::value('financial_account_id', $prevFinancialItem));
1145
1146 $currency = $params['prevContribution']->currency;
1147 if ($params['contribution']->currency) {
1148 $currency = $params['contribution']->currency;
1149 }
1150 $previousLineItemTotal = CRM_Utils_Array::value('line_total', CRM_Utils_Array::value($fieldValueId, $previousLineItems), 0);
1151 $itemParams = [
1152 'transaction_date' => $receiveDate,
1153 'contact_id' => $params['prevContribution']->contact_id,
1154 'currency' => $currency,
1155 'amount' => self::getFinancialItemAmountFromParams($inputParams, $context, $lineItemDetails, $isARefund, $previousLineItemTotal),
1156 'description' => $prevFinancialItem['description'] ?? NULL,
1157 'status_id' => $prevFinancialItem['status_id'],
1158 'financial_account_id' => $financialAccount,
1159 'entity_table' => 'civicrm_line_item',
1160 'entity_id' => $lineItemDetails['id'],
1161 ];
1162 $financialItem = CRM_Financial_BAO_FinancialItem::create($itemParams, NULL, $trxnIds);
1163 $params['line_item'][$fieldId][$fieldValueId]['deferred_line_total'] = $itemParams['amount'];
1164 $params['line_item'][$fieldId][$fieldValueId]['financial_item_id'] = $financialItem->id;
1165
1166 if (($lineItemDetails['tax_amount'] && $lineItemDetails['tax_amount'] !== 'null') || ($context === 'changeFinancialType')) {
1167 $taxAmount = (float) $lineItemDetails['tax_amount'];
1168 if ($context === 'changeFinancialType' && $lineItemDetails['tax_amount'] === 'null') {
1169 // reverse the Sale Tax amount if there is no tax rate associated with new Financial Type
1170 $taxAmount = CRM_Utils_Array::value('tax_amount', CRM_Utils_Array::value($fieldValueId, $previousLineItems), 0);
1171 }
1172 elseif ($previousLineItemTotal != $lineItemDetails['line_total']) {
1173 $taxAmount -= CRM_Utils_Array::value('tax_amount', CRM_Utils_Array::value($fieldValueId, $previousLineItems), 0);
1174 }
1175 if ($taxAmount != 0) {
1176 $itemParams['amount'] = self::getMultiplier($params['contribution']->contribution_status_id, $context) * $taxAmount;
1177 $itemParams['description'] = CRM_Invoicing_Utils::getTaxTerm();
1178 if ($lineItemDetails['financial_type_id']) {
1179 $itemParams['financial_account_id'] = CRM_Financial_BAO_FinancialAccount::getSalesTaxFinancialAccount($lineItemDetails['financial_type_id']);
1180 }
1181 CRM_Financial_BAO_FinancialItem::create($itemParams, NULL, $trxnIds);
1182 }
1183 }
1184 }
1185 return $params;
1186 }
1187
1188 /**
1189 * Does this contributtion status update represent a refund.
1190 *
1191 * @param int $previousContributionStatusID
1192 * @param int $currentContributionStatusID
1193 *
1194 * @return bool
1195 */
1196 private static function isContributionUpdateARefund($previousContributionStatusID, $currentContributionStatusID): bool {
1197 if ('Completed' !== CRM_Core_PseudoConstant::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', $previousContributionStatusID)) {
1198 return FALSE;
1199 }
1200 return self::isContributionStatusNegative($currentContributionStatusID);
1201 }
1202
1203 /**
1204 * Get transaction information about the contribution.
1205 *
1206 * @param int $contributionId
1207 * @param int $financialTypeID
1208 *
1209 * @return mixed
1210 */
1211 protected static function getContributionTransactionInformation($contributionId, int $financialTypeID) {
1212 $rows = [];
1213 $feeFinancialAccount = CRM_Contribute_PseudoConstant::getRelationalFinancialAccount($financialTypeID, 'Expense Account is');
1214
1215 // Need to exclude fee trxn rows so filter out rows where TO FINANCIAL ACCOUNT is expense account
1216 $sql = "
1217 SELECT GROUP_CONCAT(fa.`name`) as financial_account,
1218 ft.total_amount,
1219 ft.payment_instrument_id,
1220 ft.trxn_date, ft.trxn_id, ft.status_id, ft.check_number, ft.currency, ft.pan_truncation, ft.card_type_id, ft.id
1221
1222 FROM civicrm_contribution con
1223 LEFT JOIN civicrm_entity_financial_trxn eft ON (eft.entity_id = con.id AND eft.entity_table = 'civicrm_contribution')
1224 INNER JOIN civicrm_financial_trxn ft ON ft.id = eft.financial_trxn_id
1225 AND ft.to_financial_account_id != %2
1226 LEFT JOIN civicrm_entity_financial_trxn ef ON (ef.financial_trxn_id = ft.id AND ef.entity_table = 'civicrm_financial_item')
1227 LEFT JOIN civicrm_financial_item fi ON fi.id = ef.entity_id
1228 LEFT JOIN civicrm_financial_account fa ON fa.id = fi.financial_account_id
1229
1230 WHERE con.id = %1 AND ft.is_payment = 1
1231 GROUP BY ft.id";
1232 $queryParams = [
1233 1 => [$contributionId, 'Integer'],
1234 2 => [$feeFinancialAccount, 'Integer'],
1235 ];
1236 $resultDAO = CRM_Core_DAO::executeQuery($sql, $queryParams);
1237 $statuses = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'label');
1238
1239 while ($resultDAO->fetch()) {
1240 $paidByLabel = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_FinancialTrxn', 'payment_instrument_id', $resultDAO->payment_instrument_id);
1241 $paidByName = CRM_Core_PseudoConstant::getName('CRM_Core_BAO_FinancialTrxn', 'payment_instrument_id', $resultDAO->payment_instrument_id);
1242 if ($resultDAO->card_type_id) {
1243 $creditCardType = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_FinancialTrxn', 'card_type_id', $resultDAO->card_type_id);
1244 $pantruncation = '';
1245 if ($resultDAO->pan_truncation) {
1246 $pantruncation = ": {$resultDAO->pan_truncation}";
1247 }
1248 $paidByLabel .= " ({$creditCardType}{$pantruncation})";
1249 }
1250
1251 // show payment edit link only for payments done via backoffice form
1252 $paymentEditLink = '';
1253 if (empty($resultDAO->payment_processor_id) && CRM_Core_Permission::check('edit contributions')) {
1254 $links = [
1255 CRM_Core_Action::UPDATE => [
1256 'name' => ts('Edit Payment'),
1257 'icon' => 'fa-pencil',
1258 'url' => 'civicrm/payment/edit',
1259 'class' => 'medium-popup',
1260 'qs' => "reset=1&id=%%id%%&contribution_id=%%contribution_id%%",
1261 'title' => ts('Edit Payment'),
1262 ],
1263 ];
1264 $paymentEditLink = CRM_Core_Action::formLink(
1265 $links,
1266 CRM_Core_Action::mask([CRM_Core_Permission::EDIT]),
1267 [
1268 'id' => $resultDAO->id,
1269 'contribution_id' => $contributionId,
1270 ],
1271 ts('more'),
1272 FALSE,
1273 'Payment.edit.action',
1274 'Payment',
1275 $resultDAO->id,
1276 'icon'
1277 );
1278 }
1279
1280 $val = [
1281 'id' => $resultDAO->id,
1282 'total_amount' => $resultDAO->total_amount,
1283 'financial_type' => $resultDAO->financial_account,
1284 'payment_instrument' => $paidByLabel,
1285 'receive_date' => $resultDAO->trxn_date,
1286 'trxn_id' => $resultDAO->trxn_id,
1287 'status' => $statuses[$resultDAO->status_id],
1288 'currency' => $resultDAO->currency,
1289 'action' => $paymentEditLink,
1290 ];
1291 if ($paidByName === 'Check') {
1292 $val['check_number'] = $resultDAO->check_number;
1293 }
1294 $rows[] = $val;
1295 }
1296 return $rows;
1297 }
1298
1299 /**
1300 * Should an email receipt be sent for this contribution on completion.
1301 *
1302 * @param array $input
1303 * @param int $contributionID
1304 * @param int $recurringContributionID
1305 *
1306 * @return bool
1307 * @throws \API_Exception
1308 */
1309 protected static function isEmailReceipt(array $input, int $contributionID, $recurringContributionID): bool {
1310 if (isset($input['is_email_receipt'])) {
1311 return (bool) $input['is_email_receipt'];
1312 }
1313 if ($recurringContributionID) {
1314 //CRM-13273 - is_email_receipt setting on recurring contribution should take precedence over contribution page setting
1315 // but CRM-16124 if $input['is_email_receipt'] is set then that should not be overridden.
1316 // dev/core#1245 this maybe not the desired effect because the default value for is_email_receipt is set to 0 rather than 1 in
1317 // Instance that had the table added via an upgrade in 4.1
1318 // see also https://github.com/civicrm/civicrm-svn/commit/7f39befd60bc735408d7866b02b3ac7fff1d4eea#diff-9ad8e290180451a2d6eacbd3d1ca7966R354
1319 // https://lab.civicrm.org/dev/core/issues/1245
1320 return (bool) ContributionRecur::get(FALSE)->addWhere('id', '=', $recurringContributionID)->addSelect('is_email_receipt')->execute()->first()['is_email_receipt'];
1321 }
1322 $contributionPage = Contribution::get(FALSE)
1323 ->addSelect('contribution_page_id.is_email_receipt')
1324 ->addWhere('contribution_page_id', 'IS NOT NULL')
1325 ->addWhere('id', '=', $contributionID)
1326 ->execute()->first();
1327
1328 if (!empty($contributionPage)) {
1329 return (bool) $contributionPage['contribution_page_id.is_email_receipt'];
1330 }
1331 // This would be the case for backoffice (where is_email_receipt is not passed in) or events, where Event::sendMail will filter
1332 // again anyway.
1333 return TRUE;
1334 }
1335
1336 /**
1337 * Disconnect pledge payments from cancelled or failed contributions.
1338 *
1339 * If the contribution has been cancelled or has failed check to
1340 * see if it is linked to a pledge and unlink it.
1341 *
1342 * @param int $pledgePaymentID
1343 * @param string $contributionStatus
1344 *
1345 * @throws \API_Exception
1346 * @throws \Civi\API\Exception\UnauthorizedException
1347 */
1348 protected static function disconnectPledgePaymentsIfCancelled(int $pledgePaymentID, $contributionStatus): void {
1349 if (!in_array($contributionStatus, ['Failed', 'Cancelled'], TRUE)) {
1350 return;
1351 }
1352 // Check first since just doing an update could be locking under load.
1353 $pledgePayment = PledgePayment::get(FALSE)
1354 ->addWhere('contribution_id', '=', $pledgePaymentID)
1355 ->setSelect(['id', 'pledge_id', 'scheduled_date', 'scheduled_amount'])
1356 ->execute()
1357 ->first();
1358 if (!empty($pledgePayment)) {
1359 PledgePayment::update(FALSE)->setValues([
1360 'contribution_id' => NULL,
1361 'actual_amount' => NULL,
1362 'status_id:name' => 'Pending',
1363 // We need to set these fields for now because the PledgePayment::create
1364 // function doesn't handled updates well at the moment. Test cover
1365 // in testCancelOrderWithPledge.
1366 'scheduled_date' => $pledgePayment['scheduled_date'],
1367 'installment_amount' => $pledgePayment['scheduled_amount'],
1368 'installments' => 1,
1369 'pledge_id' => $pledgePayment['pledge_id'],
1370 ])->addWhere('id', '=', $pledgePayment['id'])->execute();
1371 }
1372 }
1373
1374 /**
1375 * @inheritDoc
1376 */
1377 public function addSelectWhereClause() {
1378 $whereClauses = parent::addSelectWhereClause();
1379 if ($whereClauses !== []) {
1380 // In this case permisssions have been applied & we assume the
1381 // financialaclreport is applying these
1382 // https://github.com/JMAConsulting/biz.jmaconsulting.financialaclreport/blob/master/financialaclreport.php#L107
1383 return $whereClauses;
1384 }
1385
1386 if (!CRM_Financial_BAO_FinancialType::isACLFinancialTypeStatus()) {
1387 return $whereClauses;
1388 }
1389 $types = CRM_Financial_BAO_FinancialType::getAllEnabledAvailableFinancialTypes();
1390 if (empty($types)) {
1391 $whereClauses['financial_type_id'] = 'IN (0)';
1392 }
1393 else {
1394 $whereClauses['financial_type_id'] = [
1395 'IN (' . implode(',', array_keys($types)) . ')',
1396 ];
1397 }
1398 return $whereClauses;
1399 }
1400
1401 /**
1402 * @param null $status
1403 * @param null $startDate
1404 * @param null $endDate
1405 *
1406 * @return array|null
1407 */
1408 public static function getTotalAmountAndCount($status = NULL, $startDate = NULL, $endDate = NULL) {
1409 $where = [];
1410 switch ($status) {
1411 case 'Valid':
1412 $where[] = 'contribution_status_id = 1';
1413 break;
1414
1415 case 'Cancelled':
1416 $where[] = 'contribution_status_id = 3';
1417 break;
1418 }
1419
1420 if ($startDate) {
1421 $where[] = "receive_date >= '" . CRM_Utils_Type::escape($startDate, 'Timestamp') . "'";
1422 }
1423 if ($endDate) {
1424 $where[] = "receive_date <= '" . CRM_Utils_Type::escape($endDate, 'Timestamp') . "'";
1425 }
1426 $financialTypeACLJoin = '';
1427 if (CRM_Financial_BAO_FinancialType::isACLFinancialTypeStatus()) {
1428 $financialTypeACLJoin = " LEFT JOIN civicrm_line_item i ON (i.contribution_id = c.id AND i.entity_table = 'civicrm_contribution') ";
1429 $financialTypes = CRM_Contribute_PseudoConstant::financialType();
1430 CRM_Financial_BAO_FinancialType::getAvailableFinancialTypes($financialTypes);
1431 if ($financialTypes) {
1432 $where[] = "c.financial_type_id IN (" . implode(',', array_keys($financialTypes)) . ")";
1433 $where[] = "i.financial_type_id IN (" . implode(',', array_keys($financialTypes)) . ")";
1434 }
1435 else {
1436 $where[] = "c.financial_type_id IN (0)";
1437 }
1438 }
1439
1440 $whereCond = implode(' AND ', $where);
1441
1442 $query = "
1443 SELECT sum( total_amount ) as total_amount,
1444 count( c.id ) as total_count,
1445 currency
1446 FROM civicrm_contribution c
1447 INNER JOIN civicrm_contact contact ON ( contact.id = c.contact_id )
1448 $financialTypeACLJoin
1449 WHERE $whereCond
1450 AND ( is_test = 0 OR is_test IS NULL )
1451 AND contact.is_deleted = 0
1452 GROUP BY currency
1453 ";
1454
1455 $dao = CRM_Core_DAO::executeQuery($query);
1456 $amount = [];
1457 $count = 0;
1458 while ($dao->fetch()) {
1459 $count += $dao->total_count;
1460 $amount[] = CRM_Utils_Money::format($dao->total_amount, $dao->currency);
1461 }
1462 if ($count) {
1463 return [
1464 'amount' => implode(', ', $amount),
1465 'count' => $count,
1466 ];
1467 }
1468 return NULL;
1469 }
1470
1471 /**
1472 * Delete the indirect records associated with this contribution first.
1473 *
1474 * @param int $id
1475 *
1476 * @return mixed|null
1477 * $results no of deleted Contribution on success, false otherwise
1478 */
1479 public static function deleteContribution($id) {
1480 CRM_Utils_Hook::pre('delete', 'Contribution', $id);
1481
1482 $transaction = new CRM_Core_Transaction();
1483
1484 $results = NULL;
1485 //delete activity record
1486 $params = [
1487 'source_record_id' => $id,
1488 // activity type id for contribution
1489 'activity_type_id' => 6,
1490 ];
1491
1492 CRM_Activity_BAO_Activity::deleteActivity($params);
1493
1494 //delete billing address if exists for this contribution.
1495 self::deleteAddress($id);
1496
1497 //update pledge and pledge payment, CRM-3961
1498 CRM_Pledge_BAO_PledgePayment::resetPledgePayment($id);
1499
1500 // remove entry from civicrm_price_set_entity, CRM-5095
1501 if (CRM_Price_BAO_PriceSet::getFor('civicrm_contribution', $id)) {
1502 CRM_Price_BAO_PriceSet::removeFrom('civicrm_contribution', $id);
1503 }
1504 // cleanup line items.
1505 $participantId = CRM_Core_DAO::getFieldValue('CRM_Event_DAO_ParticipantPayment', $id, 'participant_id', 'contribution_id');
1506
1507 // delete any related entity_financial_trxn, financial_trxn and financial_item records.
1508 CRM_Core_BAO_FinancialTrxn::deleteFinancialTrxn($id);
1509
1510 if ($participantId) {
1511 CRM_Price_BAO_LineItem::deleteLineItems($participantId, 'civicrm_participant');
1512 }
1513 else {
1514 CRM_Price_BAO_LineItem::deleteLineItems($id, 'civicrm_contribution');
1515 }
1516
1517 //delete note.
1518 $note = CRM_Core_BAO_Note::getNote($id, 'civicrm_contribution');
1519 $noteId = key($note);
1520 if ($noteId) {
1521 CRM_Core_BAO_Note::del($noteId, FALSE);
1522 }
1523
1524 $dao = new CRM_Contribute_DAO_Contribution();
1525 $dao->id = $id;
1526
1527 $results = $dao->delete();
1528
1529 $transaction->commit();
1530
1531 CRM_Utils_Hook::post('delete', 'Contribution', $dao->id, $dao);
1532
1533 // delete the recently created Contribution
1534 $contributionRecent = [
1535 'id' => $id,
1536 'type' => 'Contribution',
1537 ];
1538 CRM_Utils_Recent::del($contributionRecent);
1539
1540 return $results;
1541 }
1542
1543 /**
1544 * React to a financial transaction (payment) failure.
1545 *
1546 * Prior to CRM-16417 these were simply removed from the database but it has been agreed that seeing attempted
1547 * payments is important for forensic and outreach reasons.
1548 *
1549 * @param int $contributionID
1550 * @param int $contactID
1551 * @param string $message
1552 *
1553 * @throws \CiviCRM_API3_Exception
1554 */
1555 public static function failPayment($contributionID, $contactID, $message) {
1556 civicrm_api3('activity', 'create', [
1557 'activity_type_id' => 'Failed Payment',
1558 'details' => $message,
1559 'subject' => ts('Payment failed at payment processor'),
1560 'source_record_id' => $contributionID,
1561 'source_contact_id' => CRM_Core_Session::getLoggedInContactID() ? CRM_Core_Session::getLoggedInContactID() : $contactID,
1562 ]);
1563
1564 // CRM-20336 Make sure that the contribution status is Failed, not Pending.
1565 civicrm_api3('contribution', 'create', [
1566 'id' => $contributionID,
1567 'contribution_status_id' => 'Failed',
1568 ]);
1569 }
1570
1571 /**
1572 * Check if there is a contribution with the same trxn_id or invoice_id.
1573 *
1574 * @param array $input
1575 * An assoc array of name/value pairs.
1576 * @param array $duplicates
1577 * (reference) store ids of duplicate contribs.
1578 * @param int $id
1579 *
1580 * @return bool
1581 * true if duplicate, false otherwise
1582 */
1583 public static function checkDuplicate($input, &$duplicates, $id = NULL) {
1584 if (!$id) {
1585 $id = $input['id'] ?? NULL;
1586 }
1587 $trxn_id = $input['trxn_id'] ?? NULL;
1588 $invoice_id = $input['invoice_id'] ?? NULL;
1589
1590 $clause = [];
1591 $input = [];
1592
1593 if ($trxn_id) {
1594 $clause[] = 'trxn_id = %1';
1595 $input[1] = [$trxn_id, 'String'];
1596 }
1597
1598 if ($invoice_id) {
1599 $clause[] = "invoice_id = %2";
1600 $input[2] = [$invoice_id, 'String'];
1601 }
1602
1603 if (empty($clause)) {
1604 return FALSE;
1605 }
1606
1607 $clause = implode(' OR ', $clause);
1608 if ($id) {
1609 $clause = "( $clause ) AND id != %3";
1610 $input[3] = [$id, 'Integer'];
1611 }
1612
1613 $query = "SELECT id FROM civicrm_contribution WHERE $clause";
1614 $dao = CRM_Core_DAO::executeQuery($query, $input);
1615 $result = FALSE;
1616 while ($dao->fetch()) {
1617 $duplicates[] = $dao->id;
1618 $result = TRUE;
1619 }
1620 return $result;
1621 }
1622
1623 /**
1624 * Takes an associative array and creates a contribution_product object.
1625 *
1626 * the function extract all the params it needs to initialize the create a
1627 * contribution_product object. the params array could contain additional unused name/value
1628 * pairs
1629 *
1630 * @param array $params
1631 * (reference) an assoc array of name/value pairs.
1632 *
1633 * @return CRM_Contribute_DAO_ContributionProduct
1634 */
1635 public static function addPremium(&$params) {
1636 $contributionProduct = new CRM_Contribute_DAO_ContributionProduct();
1637 $contributionProduct->copyValues($params);
1638 return $contributionProduct->save();
1639 }
1640
1641 /**
1642 * Get list of contribution fields for profile.
1643 * For now we only allow custom contribution fields to be in
1644 * profile
1645 *
1646 * @param bool $addExtraFields
1647 * True if special fields needs to be added.
1648 *
1649 * @return array
1650 * the list of contribution fields
1651 */
1652 public static function getContributionFields($addExtraFields = TRUE) {
1653 $contributionFields = CRM_Contribute_DAO_Contribution::export();
1654 // @todo remove this - this line was added because payment_instrument_id was not
1655 // set to exportable - but now it is.
1656 $contributionFields = array_merge($contributionFields, CRM_Core_OptionValue::getFields($mode = 'contribute'));
1657
1658 if ($addExtraFields) {
1659 $contributionFields = array_merge($contributionFields, self::getSpecialContributionFields());
1660 }
1661
1662 $contributionFields = array_merge($contributionFields, CRM_Financial_DAO_FinancialType::export());
1663
1664 foreach ($contributionFields as $key => $var) {
1665 if ($key === 'contribution_contact_id') {
1666 continue;
1667 }
1668 elseif ($key === 'contribution_campaign_id') {
1669 $var['title'] = ts('Campaign');
1670 }
1671 $fields[$key] = $var;
1672 }
1673
1674 $fields = array_merge($fields, CRM_Core_BAO_CustomField::getFieldsForImport('Contribution'));
1675 return $fields;
1676 }
1677
1678 /**
1679 * Add extra fields specific to contribution.
1680 */
1681 public static function getSpecialContributionFields() {
1682 $extraFields = [
1683 'contribution_soft_credit_name' => [
1684 'name' => 'contribution_soft_credit_name',
1685 'title' => ts('Soft Credit Name'),
1686 'headerPattern' => '/^soft_credit_name$/i',
1687 'where' => 'civicrm_contact_d.display_name',
1688 ],
1689 'contribution_soft_credit_email' => [
1690 'name' => 'contribution_soft_credit_email',
1691 'title' => ts('Soft Credit Email'),
1692 'headerPattern' => '/^soft_credit_email$/i',
1693 'where' => 'soft_email.email',
1694 ],
1695 'contribution_soft_credit_phone' => [
1696 'name' => 'contribution_soft_credit_phone',
1697 'title' => ts('Soft Credit Phone'),
1698 'headerPattern' => '/^soft_credit_phone$/i',
1699 'where' => 'soft_phone.phone',
1700 ],
1701 'contribution_soft_credit_contact_id' => [
1702 'name' => 'contribution_soft_credit_contact_id',
1703 'title' => ts('Soft Credit Contact ID'),
1704 'headerPattern' => '/^soft_credit_contact_id$/i',
1705 'where' => 'civicrm_contribution_soft.contact_id',
1706 ],
1707 'contribution_pcp_title' => [
1708 'name' => 'contribution_pcp_title',
1709 'title' => ts('Personal Campaign Page Title'),
1710 'headerPattern' => '/^contribution_pcp_title$/i',
1711 'where' => 'contribution_pcp.title',
1712 ],
1713 ];
1714
1715 return $extraFields;
1716 }
1717
1718 /**
1719 * @param int $pageID
1720 *
1721 * @return array
1722 */
1723 public static function getCurrentandGoalAmount($pageID) {
1724 $query = "
1725 SELECT p.goal_amount as goal, sum( c.total_amount ) as total
1726 FROM civicrm_contribution_page p,
1727 civicrm_contribution c
1728 WHERE p.id = c.contribution_page_id
1729 AND p.id = %1
1730 AND c.cancel_date is null
1731 GROUP BY p.id
1732 ";
1733
1734 $config = CRM_Core_Config::singleton();
1735 $params = [1 => [$pageID, 'Integer']];
1736 $dao = CRM_Core_DAO::executeQuery($query, $params);
1737
1738 if ($dao->fetch()) {
1739 return [$dao->goal, $dao->total];
1740 }
1741 else {
1742 return [NULL, NULL];
1743 }
1744 }
1745
1746 /**
1747 * Get list of contributions which credit the passed in contact ID.
1748 *
1749 * The returned array provides details about the original contribution & donor.
1750 *
1751 * @param int $honorId
1752 * In Honor of Contact ID.
1753 *
1754 * @return array
1755 * list of contribution fields
1756 * @todo - this is a confusing function called from one place. It has a test. It would be
1757 * nice to deprecate it.
1758 *
1759 */
1760 public static function getHonorContacts($honorId) {
1761 $params = [];
1762 $honorDAO = new CRM_Contribute_DAO_ContributionSoft();
1763 $honorDAO->contact_id = $honorId;
1764 $honorDAO->find();
1765
1766 $type = CRM_Contribute_PseudoConstant::financialType();
1767
1768 while ($honorDAO->fetch()) {
1769 $contributionDAO = new CRM_Contribute_DAO_Contribution();
1770 $contributionDAO->id = $honorDAO->contribution_id;
1771
1772 if ($contributionDAO->find(TRUE)) {
1773 $params[$contributionDAO->id]['honor_type'] = CRM_Core_PseudoConstant::getLabel('CRM_Contribute_BAO_ContributionSoft', 'soft_credit_type_id', $honorDAO->soft_credit_type_id);
1774 $params[$contributionDAO->id]['honorId'] = $contributionDAO->contact_id;
1775 $params[$contributionDAO->id]['display_name'] = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $contributionDAO->contact_id, 'display_name');
1776 $params[$contributionDAO->id]['type'] = $type[$contributionDAO->financial_type_id];
1777 $params[$contributionDAO->id]['type_id'] = $contributionDAO->financial_type_id;
1778 $params[$contributionDAO->id]['amount'] = CRM_Utils_Money::format($contributionDAO->total_amount, $contributionDAO->currency);
1779 $params[$contributionDAO->id]['source'] = $contributionDAO->source;
1780 $params[$contributionDAO->id]['receive_date'] = $contributionDAO->receive_date;
1781 $params[$contributionDAO->id]['contribution_status'] = CRM_Contribute_PseudoConstant::contributionStatus($contributionDAO->contribution_status_id, 'label');
1782 }
1783 }
1784
1785 return $params;
1786 }
1787
1788 /**
1789 * Get the sort name of a contact for a particular contribution.
1790 *
1791 * @param int $id
1792 * Id of the contribution.
1793 *
1794 * @return null|string
1795 * sort name of the contact if found
1796 */
1797 public static function sortName($id) {
1798 $id = CRM_Utils_Type::escape($id, 'Integer');
1799
1800 $query = "
1801 SELECT civicrm_contact.sort_name
1802 FROM civicrm_contribution, civicrm_contact
1803 WHERE civicrm_contribution.contact_id = civicrm_contact.id
1804 AND civicrm_contribution.id = {$id}
1805 ";
1806 return CRM_Core_DAO::singleValueQuery($query);
1807 }
1808
1809 /**
1810 * Generate summary of amount received in the current fiscal year to date from the contact or contacts.
1811 *
1812 * @param int|array $contactIDs
1813 *
1814 * @return array
1815 */
1816 public static function annual($contactIDs) {
1817 if (!is_array($contactIDs)) {
1818 // In practice I can't fine any evidence that this function is ever called with
1819 // anything other than a single contact id, but left like this due to .... fear.
1820 $contactIDs = explode(',', $contactIDs);
1821 }
1822
1823 $query = self::getAnnualQuery($contactIDs);
1824 $dao = CRM_Core_DAO::executeQuery($query);
1825 $count = 0;
1826 $amount = $average = [];
1827 while ($dao->fetch()) {
1828 if ($dao->count > 0 && $dao->amount > 0) {
1829 $count += $dao->count;
1830 $amount[] = CRM_Utils_Money::format($dao->amount, $dao->currency);
1831 $average[] = CRM_Utils_Money::format($dao->average, $dao->currency);
1832 }
1833 }
1834 if ($count > 0) {
1835 return [
1836 $count,
1837 implode(',&nbsp;', $amount),
1838 implode(',&nbsp;', $average),
1839 ];
1840 }
1841 return [0, 0, 0];
1842 }
1843
1844 /**
1845 * Check if there is a contribution with the params passed in.
1846 *
1847 * Used for trxn_id,invoice_id and contribution_id
1848 *
1849 * @param array $params
1850 * An assoc array of name/value pairs.
1851 *
1852 * @return array
1853 * contribution id if success else NULL
1854 */
1855 public static function checkDuplicateIds($params) {
1856 $dao = new CRM_Contribute_DAO_Contribution();
1857
1858 $clause = [];
1859 $input = [];
1860 foreach ($params as $k => $v) {
1861 if ($v) {
1862 $clause[] = "$k = '$v'";
1863 }
1864 }
1865 $clause = implode(' AND ', $clause);
1866 $query = "SELECT id FROM civicrm_contribution WHERE $clause";
1867 $dao = CRM_Core_DAO::executeQuery($query, $input);
1868
1869 while ($dao->fetch()) {
1870 $result = $dao->id;
1871 return $result;
1872 }
1873 return NULL;
1874 }
1875
1876 /**
1877 * Get the contribution details for component export.
1878 *
1879 * @param int $exportMode
1880 * Export mode.
1881 * @param array $componentIds
1882 * Component ids.
1883 *
1884 * @return array
1885 * associated array
1886 */
1887 public static function getContributionDetails($exportMode, $componentIds) {
1888 $paymentDetails = [];
1889 $componentClause = ' IN ( ' . implode(',', $componentIds) . ' ) ';
1890
1891 if ($exportMode == CRM_Export_Form_Select::EVENT_EXPORT) {
1892 $componentSelect = " civicrm_participant_payment.participant_id id";
1893 $additionalClause = "
1894 INNER JOIN civicrm_participant_payment ON (civicrm_contribution.id = civicrm_participant_payment.contribution_id
1895 AND civicrm_participant_payment.participant_id {$componentClause} )
1896 ";
1897 }
1898 elseif ($exportMode == CRM_Export_Form_Select::MEMBER_EXPORT) {
1899 $componentSelect = " civicrm_membership_payment.membership_id id";
1900 $additionalClause = "
1901 INNER JOIN civicrm_membership_payment ON (civicrm_contribution.id = civicrm_membership_payment.contribution_id
1902 AND civicrm_membership_payment.membership_id {$componentClause} )
1903 ";
1904 }
1905 elseif ($exportMode == CRM_Export_Form_Select::PLEDGE_EXPORT) {
1906 $componentSelect = " civicrm_pledge_payment.id id";
1907 $additionalClause = "
1908 INNER JOIN civicrm_pledge_payment ON (civicrm_contribution.id = civicrm_pledge_payment.contribution_id
1909 AND civicrm_pledge_payment.pledge_id {$componentClause} )
1910 ";
1911 }
1912
1913 $query = " SELECT total_amount, contribution_status.name as status_id, contribution_status.label as status, payment_instrument.name as payment_instrument, receive_date,
1914 trxn_id, {$componentSelect}
1915 FROM civicrm_contribution
1916 LEFT JOIN civicrm_option_group option_group_payment_instrument ON ( option_group_payment_instrument.name = 'payment_instrument')
1917 LEFT JOIN civicrm_option_value payment_instrument ON (civicrm_contribution.payment_instrument_id = payment_instrument.value
1918 AND option_group_payment_instrument.id = payment_instrument.option_group_id )
1919 LEFT JOIN civicrm_option_group option_group_contribution_status ON (option_group_contribution_status.name = 'contribution_status')
1920 LEFT JOIN civicrm_option_value contribution_status ON (civicrm_contribution.contribution_status_id = contribution_status.value
1921 AND option_group_contribution_status.id = contribution_status.option_group_id )
1922 {$additionalClause}
1923 ";
1924
1925 $dao = CRM_Core_DAO::executeQuery($query);
1926
1927 while ($dao->fetch()) {
1928 $paymentDetails[$dao->id] = [
1929 'total_amount' => $dao->total_amount,
1930 'contribution_status' => $dao->status,
1931 'receive_date' => $dao->receive_date,
1932 'pay_instru' => $dao->payment_instrument,
1933 'trxn_id' => $dao->trxn_id,
1934 ];
1935 }
1936
1937 return $paymentDetails;
1938 }
1939
1940 /**
1941 * Create address associated with contribution record.
1942 *
1943 * As long as there is one or more billing field in the parameters we will create the address.
1944 *
1945 * (historically the decision to create or not was based on the payment 'type' but these lines are greyer than once
1946 * thought).
1947 *
1948 * @param array $params
1949 * @param int $billingLocationTypeID
1950 *
1951 * @return int
1952 * address id
1953 */
1954 public static function createAddress($params, $billingLocationTypeID) {
1955 [$hasBillingField, $addressParams] = self::getBillingAddressParams($params, $billingLocationTypeID);
1956 if ($hasBillingField) {
1957 $address = CRM_Core_BAO_Address::add($addressParams, FALSE);
1958 return $address->id;
1959 }
1960 return NULL;
1961
1962 }
1963
1964 /**
1965 * Delete billing address record related contribution.
1966 *
1967 * @param int $contributionId
1968 * @param int $contactId
1969 */
1970 public static function deleteAddress($contributionId = NULL, $contactId = NULL) {
1971 $clauses = [];
1972 $contactJoin = NULL;
1973
1974 if ($contributionId) {
1975 $clauses[] = "cc.id = {$contributionId}";
1976 }
1977
1978 if ($contactId) {
1979 $clauses[] = "cco.id = {$contactId}";
1980 $contactJoin = "INNER JOIN civicrm_contact cco ON cc.contact_id = cco.id";
1981 }
1982
1983 if (empty($clauses)) {
1984 throw new CRM_Core_Exception('No Where clauses defined when deleting address');
1985 }
1986
1987 $condition = implode(' OR ', $clauses);
1988
1989 $query = "
1990 SELECT ca.id
1991 FROM civicrm_address ca
1992 INNER JOIN civicrm_contribution cc ON cc.address_id = ca.id
1993 $contactJoin
1994 WHERE $condition
1995 ";
1996 $dao = CRM_Core_DAO::executeQuery($query);
1997
1998 while ($dao->fetch()) {
1999 $params = ['id' => $dao->id];
2000 CRM_Core_BAO_Block::blockDelete('Address', $params);
2001 }
2002 }
2003
2004 /**
2005 * This function check online pending contribution associated w/
2006 * Online Event Registration or Online Membership signup.
2007 *
2008 * @param int $componentId
2009 * Participant/membership id.
2010 * @param string $componentName
2011 * Event/Membership.
2012 *
2013 * @return int
2014 * pending contribution id.
2015 */
2016 public static function checkOnlinePendingContribution($componentId, $componentName) {
2017 $contributionId = NULL;
2018 if (!$componentId ||
2019 !in_array($componentName, ['Event', 'Membership'])
2020 ) {
2021 return $contributionId;
2022 }
2023
2024 if ($componentName === 'Event') {
2025 $idName = 'participant_id';
2026 $componentTable = 'civicrm_participant';
2027 $paymentTable = 'civicrm_participant_payment';
2028 $source = ts('Online Event Registration');
2029 }
2030
2031 if ($componentName === 'Membership') {
2032 $idName = 'membership_id';
2033 $componentTable = 'civicrm_membership';
2034 $paymentTable = 'civicrm_membership_payment';
2035 $source = ts('Online Contribution');
2036 }
2037
2038 $pendingStatusId = array_search('Pending', CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name'));
2039
2040 $query = "
2041 SELECT component.id as {$idName},
2042 componentPayment.contribution_id as contribution_id,
2043 contribution.source source,
2044 contribution.contribution_status_id as contribution_status_id,
2045 contribution.is_pay_later as is_pay_later
2046 FROM $componentTable component
2047 LEFT JOIN $paymentTable componentPayment ON ( componentPayment.{$idName} = component.id )
2048 LEFT JOIN civicrm_contribution contribution ON ( componentPayment.contribution_id = contribution.id )
2049 WHERE component.id = {$componentId}";
2050
2051 $dao = CRM_Core_DAO::executeQuery($query);
2052
2053 while ($dao->fetch()) {
2054 if ($dao->contribution_id &&
2055 $dao->is_pay_later &&
2056 $dao->contribution_status_id == $pendingStatusId &&
2057 strpos($dao->source, $source) !== FALSE
2058 ) {
2059 $contributionId = $dao->contribution_id;
2060 }
2061 }
2062
2063 return $contributionId;
2064 }
2065
2066 /**
2067 * Update contribution as well as related objects.
2068 *
2069 * This function by-passes hooks - to address this - don't use this function.
2070 *
2071 * @param array $params
2072 *
2073 * @throws CRM_Core_Exception
2074 * @throws \CiviCRM_API3_Exception
2075 * @deprecated
2076 *
2077 * Use api contribute.completetransaction
2078 * For failures use failPayment (preferably exposing by api in the process).
2079 *
2080 */
2081 public static function transitionComponents($params) {
2082 $contributionStatus = CRM_Core_PseudoConstant::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', $params['contribution_status_id']);
2083 $previousStatus = CRM_Core_PseudoConstant::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', $params['previous_contribution_status_id']);
2084 // @todo fix the one place that calls this function to use Payment.create
2085 // remove this.
2086 // get minimum required values.
2087 $contactId = NULL;
2088 $componentId = NULL;
2089 $componentName = NULL;
2090 $contributionId = $params['contribution_id'];
2091 $contributionStatusId = $params['contribution_status_id'];
2092
2093 // if we already processed contribution object pass previous status id.
2094 $previousContriStatusId = $params['previous_contribution_status_id'];
2095
2096 // we process only ( Completed, Cancelled, or Failed ) contributions.
2097 if (!$contributionId || $contributionStatus !== 'Completed') {
2098 return;
2099 }
2100
2101 if (!$componentName || !$componentId) {
2102 // get the related component details.
2103 $componentDetails = self::getComponentDetails($contributionId);
2104 }
2105 else {
2106 $componentDetails['contact_id'] = $contactId;
2107 $componentDetails['component'] = $componentName;
2108
2109 if ($componentName === 'event') {
2110 $componentDetails['participant'] = $componentId;
2111 }
2112 else {
2113 $componentDetails['membership'] = $componentId;
2114 }
2115 }
2116
2117 if (!empty($componentDetails['contact_id'])) {
2118 $componentDetails['contact_id'] = CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_Contribution',
2119 $contributionId,
2120 'contact_id'
2121 );
2122 }
2123
2124 // do check for required ids.
2125 if (empty($componentDetails['membership']) && empty($componentDetails['participant']) && empty($componentDetails['pledge_payment']) || empty($componentDetails['contact_id'])) {
2126 return;
2127 }
2128
2129 $input = $ids = [];
2130
2131 $input['component'] = $componentDetails['component'] ?? NULL;
2132 $ids['contribution'] = $contributionId;
2133 $ids['contact'] = $componentDetails['contact_id'] ?? NULL;
2134 $ids['membership'] = $componentDetails['membership'] ?? NULL;
2135 $ids['participant'] = $componentDetails['participant'] ?? NULL;
2136 $ids['event'] = $componentDetails['event'] ?? NULL;
2137 $ids['pledge_payment'] = $componentDetails['pledge_payment'] ?? NULL;
2138 $ids['contributionRecur'] = NULL;
2139 $ids['contributionPage'] = NULL;
2140
2141 $contribution = new CRM_Contribute_BAO_Contribution();
2142 $contribution->id = $ids['contribution'];
2143 $contribution->find();
2144
2145 $contribution->loadRelatedObjects($input, $ids);
2146
2147 $memberships = $contribution->_relatedObjects['membership'] ?? [];
2148 $participant = $contribution->_relatedObjects['participant'] ?? [];
2149 $pledgePayment = $contribution->_relatedObjects['pledge_payment'] ?? [];
2150
2151 $pledgeID = $oldStatus = NULL;
2152 $pledgePaymentIDs = [];
2153 if ($pledgePayment) {
2154 foreach ($pledgePayment as $key => $object) {
2155 $pledgePaymentIDs[] = $object->id;
2156 }
2157 $pledgeID = $pledgePayment[0]->pledge_id;
2158 }
2159
2160 $membershipStatuses = CRM_Member_PseudoConstant::membershipStatus();
2161
2162 if ($participant) {
2163 $participantStatuses = CRM_Event_PseudoConstant::participantStatus();
2164 $oldStatus = CRM_Core_DAO::getFieldValue('CRM_Event_DAO_Participant',
2165 $participant->id,
2166 'status_id'
2167 );
2168 }
2169 if ($contributionStatus === 'Completed') {
2170
2171 // only pending contribution related object processed.
2172 if ($previousContriStatusId &&
2173 !in_array($previousStatus, [
2174 'Pending',
2175 'Partially paid',
2176 ])
2177 ) {
2178 // this is case when we already processed contribution object.
2179 return;
2180 }
2181 elseif (!$previousContriStatusId &&
2182 !in_array($contributionStatus, [
2183 'Pending',
2184 'Partially paid',
2185 ])
2186 ) {
2187 // this is case when we are going to process contribution object later.
2188 return;
2189 }
2190
2191 if (is_array($memberships)) {
2192 foreach ($memberships as $membership) {
2193 if ($membership) {
2194 $format = '%Y%m%d';
2195
2196 //CRM-4523
2197 $currentMembership = CRM_Member_BAO_Membership::getContactMembership($membership->contact_id,
2198 $membership->membership_type_id,
2199 $membership->is_test, $membership->id
2200 );
2201
2202 // CRM-8141 update the membership type with the value recorded in log when membership created/renewed
2203 // this picks up membership type changes during renewals
2204 $sql = "
2205 SELECT membership_type_id
2206 FROM civicrm_membership_log
2207 WHERE membership_id=$membership->id
2208 ORDER BY id DESC
2209 LIMIT 1;";
2210 $dao = CRM_Core_DAO::executeQuery($sql);
2211 if ($dao->fetch()) {
2212 if (!empty($dao->membership_type_id)) {
2213 $membership->membership_type_id = $dao->membership_type_id;
2214 $membership->save();
2215 }
2216 }
2217 // else fall back to using current membership type
2218 // Figure out number of terms
2219 $numterms = 1;
2220 $lineitems = CRM_Price_BAO_LineItem::getLineItemsByContributionID($contributionId);
2221 foreach ($lineitems as $lineitem) {
2222 if ($membership->membership_type_id == ($lineitem['membership_type_id'] ?? NULL)) {
2223 $numterms = $lineitem['membership_num_terms'] ?? NULL;
2224
2225 // in case membership_num_terms comes through as null or zero
2226 $numterms = $numterms >= 1 ? $numterms : 1;
2227 break;
2228 }
2229 }
2230
2231 // CRM-15735-to update the membership status as per the contribution receive date
2232 $joinDate = NULL;
2233 $oldStatus = $membership->status_id;
2234 if (!empty($params['receive_date'])) {
2235 $joinDate = $params['receive_date'];
2236 $status = CRM_Member_BAO_MembershipStatus::getMembershipStatusByDate($membership->start_date,
2237 $membership->end_date,
2238 $membership->join_date,
2239 $params['receive_date'],
2240 FALSE,
2241 $membership->membership_type_id,
2242 (array) $membership
2243 );
2244 $membership->status_id = CRM_Utils_Array::value('id', $status, $membership->status_id);
2245 $membership->save();
2246 }
2247
2248 if ($currentMembership) {
2249 CRM_Member_BAO_Membership::fixMembershipStatusBeforeRenew($currentMembership, NULL);
2250 $dates = CRM_Member_BAO_MembershipType::getRenewalDatesForMembershipType($membership->id, NULL, NULL, $numterms);
2251 $dates['join_date'] = CRM_Utils_Date::customFormat($currentMembership['join_date'], $format);
2252 }
2253 else {
2254 $dates = CRM_Member_BAO_MembershipType::getDatesForMembershipType($membership->membership_type_id, $joinDate, NULL, NULL, $numterms);
2255 }
2256
2257 //get the status for membership.
2258 $calcStatus = CRM_Member_BAO_MembershipStatus::getMembershipStatusByDate($dates['start_date'],
2259 $dates['end_date'],
2260 $dates['join_date'],
2261 'now',
2262 TRUE,
2263 $membership->membership_type_id,
2264 (array) $membership
2265 );
2266
2267 $formattedParams = [
2268 'status_id' => CRM_Utils_Array::value('id', $calcStatus,
2269 array_search('Current', $membershipStatuses)
2270 ),
2271 'join_date' => CRM_Utils_Date::customFormat($dates['join_date'], $format),
2272 'start_date' => CRM_Utils_Date::customFormat($dates['start_date'], $format),
2273 'end_date' => CRM_Utils_Date::customFormat($dates['end_date'], $format),
2274 ];
2275
2276 CRM_Utils_Hook::pre('edit', 'Membership', $membership->id, $formattedParams);
2277
2278 $membership->copyValues($formattedParams);
2279 $membership->save();
2280
2281 //updating the membership log
2282 $membershipLog = $formattedParams;
2283 $logStartDate = CRM_Utils_Date::customFormat($dates['log_start_date'] ?? NULL, $format);
2284 $logStartDate = ($logStartDate) ? CRM_Utils_Date::isoToMysql($logStartDate) : $formattedParams['start_date'];
2285
2286 $membershipLog['start_date'] = $logStartDate;
2287 $membershipLog['membership_id'] = $membership->id;
2288 $membershipLog['modified_id'] = $membership->contact_id;
2289 $membershipLog['modified_date'] = date('Ymd');
2290 $membershipLog['membership_type_id'] = $membership->membership_type_id;
2291
2292 CRM_Member_BAO_MembershipLog::add($membershipLog);
2293
2294 //update related Memberships.
2295 CRM_Member_BAO_Membership::updateRelatedMemberships($membership->id, $formattedParams);
2296
2297 foreach (['Membership Signup', 'Membership Renewal'] as $activityType) {
2298 $scheduledActivityID = CRM_Utils_Array::value('id',
2299 civicrm_api3('Activity', 'Get',
2300 [
2301 'source_record_id' => $membership->id,
2302 'activity_type_id' => $activityType,
2303 'status_id' => 'Scheduled',
2304 'options' => [
2305 'limit' => 1,
2306 'sort' => 'id DESC',
2307 ],
2308 ]
2309 )
2310 );
2311 // 1. Update Schedule Membership Signup/Renewal activity to completed on successful payment of pending membership
2312 // 2. OR Create renewal activity scheduled if its membership renewal will be paid later
2313 if ($scheduledActivityID) {
2314 CRM_Activity_BAO_Activity::addActivity($membership, $activityType, $membership->contact_id, ['id' => $scheduledActivityID]);
2315 break;
2316 }
2317 }
2318
2319 // track membership status change if any
2320 if (!empty($oldStatus) && $membership->status_id != $oldStatus) {
2321 $allStatus = CRM_Member_BAO_Membership::buildOptions('status_id', 'get');
2322 CRM_Activity_BAO_Activity::addActivity($membership,
2323 'Change Membership Status',
2324 NULL,
2325 [
2326 'subject' => "Status changed from {$allStatus[$oldStatus]} to {$allStatus[$membership->status_id]}",
2327 'source_contact_id' => $membershipLog['modified_id'],
2328 'priority_id' => 'Normal',
2329 ]
2330 );
2331 }
2332
2333 CRM_Utils_Hook::post('edit', 'Membership', $membership->id, $membership);
2334 }
2335 }
2336 }
2337
2338 if ($participant) {
2339 $updatedStatusId = array_search('Registered', $participantStatuses);
2340 CRM_Event_BAO_Participant::updateParticipantStatus($participant->id, $oldStatus, $updatedStatusId, TRUE);
2341 }
2342
2343 if ($pledgePayment) {
2344 CRM_Pledge_BAO_PledgePayment::updatePledgePaymentStatus($pledgeID, $pledgePaymentIDs, $contributionStatusId);
2345 }
2346 }
2347
2348 }
2349
2350 /**
2351 * Returns all contribution related object ids.
2352 *
2353 * @param $contributionId
2354 *
2355 * @return array
2356 */
2357 public static function getComponentDetails($contributionId) {
2358 $componentDetails = $pledgePayment = [];
2359 if (!$contributionId) {
2360 return $componentDetails;
2361 }
2362
2363 $query = "
2364 SELECT c.id as contribution_id,
2365 c.contact_id as contact_id,
2366 c.contribution_recur_id,
2367 mp.membership_id as membership_id,
2368 m.membership_type_id as membership_type_id,
2369 pp.participant_id as participant_id,
2370 p.event_id as event_id,
2371 pgp.id as pledge_payment_id
2372 FROM civicrm_contribution c
2373 LEFT JOIN civicrm_membership_payment mp ON mp.contribution_id = c.id
2374 LEFT JOIN civicrm_participant_payment pp ON pp.contribution_id = c.id
2375 LEFT JOIN civicrm_participant p ON pp.participant_id = p.id
2376 LEFT JOIN civicrm_membership m ON m.id = mp.membership_id
2377 LEFT JOIN civicrm_pledge_payment pgp ON pgp.contribution_id = c.id
2378 WHERE c.id = $contributionId";
2379
2380 $dao = CRM_Core_DAO::executeQuery($query);
2381 $componentDetails = [];
2382
2383 while ($dao->fetch()) {
2384 $componentDetails['component'] = $dao->participant_id ? 'event' : 'contribute';
2385 $componentDetails['contact_id'] = $dao->contact_id;
2386 if ($dao->event_id) {
2387 $componentDetails['event'] = $dao->event_id;
2388 }
2389 if ($dao->participant_id) {
2390 $componentDetails['participant'] = $dao->participant_id;
2391 }
2392 if ($dao->membership_id) {
2393 if (!isset($componentDetails['membership'])) {
2394 $componentDetails['membership'] = $componentDetails['membership_type'] = [];
2395 }
2396 $componentDetails['membership'][] = $dao->membership_id;
2397 $componentDetails['membership_type'][] = $dao->membership_type_id;
2398 }
2399 if ($dao->pledge_payment_id) {
2400 $pledgePayment[] = $dao->pledge_payment_id;
2401 }
2402 if ($dao->contribution_recur_id) {
2403 $componentDetails['contributionRecur'] = $dao->contribution_recur_id;
2404 }
2405 }
2406
2407 if ($pledgePayment) {
2408 $componentDetails['pledge_payment'] = $pledgePayment;
2409 }
2410
2411 return $componentDetails;
2412 }
2413
2414 /**
2415 * @param int $contactId
2416 * @param bool $includeSoftCredit
2417 *
2418 * @return null|string
2419 */
2420 public static function contributionCount($contactId, $includeSoftCredit = TRUE) {
2421 if (!$contactId) {
2422 return 0;
2423 }
2424 $financialTypes = CRM_Financial_BAO_FinancialType::getAllAvailableFinancialTypes();
2425 $additionalWhere = " AND contribution.financial_type_id IN (0)";
2426 $liWhere = " AND i.financial_type_id IN (0)";
2427 if (!empty($financialTypes)) {
2428 $additionalWhere = " AND contribution.financial_type_id IN (" . implode(',', array_keys($financialTypes)) . ")";
2429 $liWhere = " AND i.financial_type_id NOT IN (" . implode(',', array_keys($financialTypes)) . ")";
2430 }
2431 $contactContributionsSQL = "
2432 SELECT contribution.id AS id
2433 FROM civicrm_contribution contribution
2434 LEFT JOIN civicrm_line_item i ON i.contribution_id = contribution.id AND i.entity_table = 'civicrm_contribution' $liWhere
2435 WHERE contribution.is_test = 0 AND contribution.is_template != '1' AND contribution.contact_id = {$contactId}
2436 $additionalWhere
2437 AND i.id IS NULL";
2438
2439 $contactSoftCreditContributionsSQL = "
2440 SELECT contribution.id
2441 FROM civicrm_contribution contribution INNER JOIN civicrm_contribution_soft softContribution
2442 ON ( contribution.id = softContribution.contribution_id )
2443 WHERE contribution.is_test = 0 AND contribution.is_template != '1' AND softContribution.contact_id = {$contactId} ";
2444 $query = "SELECT count( x.id ) count FROM ( ";
2445 $query .= $contactContributionsSQL;
2446
2447 if ($includeSoftCredit) {
2448 $query .= " UNION ";
2449 $query .= $contactSoftCreditContributionsSQL;
2450 }
2451
2452 $query .= ") x";
2453
2454 return CRM_Core_DAO::singleValueQuery($query);
2455 }
2456
2457 /**
2458 * Repeat a transaction as part of a recurring series.
2459 *
2460 * The ideal flow is
2461 * 1) Processor calls contribution.repeattransaction with contribution_status_id = Pending
2462 * 2) The repeattransaction loads the 'template contribution' and calls a hook to allow altering of it .
2463 * 3) Repeat transaction calls order.create to create the pending contribution with correct line items
2464 * and associated entities.
2465 * 4) The calling code calls Payment.create which in turn calls CompleteOrder (if completing)
2466 * which updates the various entities and sends appropriate emails.
2467 *
2468 * Gaps in the above (
2469 *
2470 * @param array $input
2471 *
2472 * @param array $contributionParams
2473 *
2474 * @return bool|array
2475 * @throws \API_Exception
2476 * @throws \CiviCRM_API3_Exception
2477 * @throws \Civi\API\Exception\UnauthorizedException
2478 * @todo
2479 * 1) many processors still call repeattransaction with contribution_status_id = Completed
2480 * 2) repeattransaction code is current munged into completeTransaction code for historical bad coding reasons
2481 * 3) Repeat transaction duplicates rather than calls Order.create
2482 * 4) Use of payment.create still limited - completetransaction is more common.
2483 * 6) the determination of the membership to be linked is tricksy. The prioritised method is
2484 * to load the membership(s) referred to via line items in the template transactions. Any other
2485 * method is likely to lead to incorrect line items & related entities being created (as the line_item
2486 * link is a required part of 'correct data'). However there are 3 other methods to determine it
2487 * - membership_payment record
2488 * - civicrm_membership.contribution_recur_id
2489 * - input override.
2490 * Passing in an input override WILL ensure the membership is extended to prevent regressions
2491 * of historical processors since this has been handled 'forever' - specifically for paypal.
2492 * albeit by an even nastier mechanism than the current input override.
2493 * The count is out on how correct related entities wind up in this case.
2494 */
2495 protected static function repeatTransaction(array $input, array $contributionParams) {
2496 $templateContribution = CRM_Contribute_BAO_ContributionRecur::getTemplateContribution(
2497 (int) $contributionParams['contribution_recur_id'],
2498 array_filter([
2499 'total_amount' => $input['total_amount'] ?? NULL,
2500 'financial_type_id' => $input['financial_type_id'] ?? NULL,
2501 'campaign_id' => $input['campaign_id'] ?? NULL,
2502 // array_filter with strlen filters out NULL, '' and FALSE but not 0.
2503 ], 'strlen')
2504 );
2505 $input['line_item'] = $contributionParams['line_item'] = $templateContribution['line_item'];
2506 $contributionParams['status_id'] = 'Pending';
2507
2508 foreach (['contact_id', 'campaign_id', 'financial_type_id', 'currency', 'source', 'amount_level', 'address_id', 'on_behalf', 'source_contact_id', 'tax_amount', 'contribution_page_id', 'total_amount'] as $fieldName) {
2509 if (isset($templateContribution[$fieldName])) {
2510 $contributionParams[$fieldName] = $templateContribution[$fieldName];
2511 }
2512 }
2513
2514 $contributionParams['source'] = $contributionParams['source'] ?? ts('Recurring contribution');
2515
2516 $createContribution = civicrm_api3('Contribution', 'create', $contributionParams);
2517 $temporaryObject = new CRM_Contribute_BAO_Contribution();
2518 $temporaryObject->copyCustomFields($templateContribution['id'], $createContribution['id']);
2519 self::handleMembershipIDOverride($createContribution['id'], $input);
2520 // Add new soft credit against current $contribution.
2521 CRM_Contribute_BAO_ContributionRecur::addrecurSoftCredit($contributionParams['contribution_recur_id'], $createContribution['id']);
2522 return $createContribution;
2523 }
2524
2525 /**
2526 * Get individual id for onbehalf contribution.
2527 *
2528 * @param int $contributionId
2529 * Contribution id.
2530 * @param int $contributorId
2531 * Contributor id.
2532 *
2533 * @return array
2534 * containing organization id and individual id
2535 */
2536 public static function getOnbehalfIds($contributionId, $contributorId = NULL) {
2537
2538 $ids = [];
2539
2540 if (!$contributionId) {
2541 return $ids;
2542 }
2543
2544 // fetch contributor id if null
2545 if (!$contributorId) {
2546 $contributorId = CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_Contribution',
2547 $contributionId, 'contact_id'
2548 );
2549 }
2550
2551 $activityTypeIds = CRM_Core_PseudoConstant::activityType(TRUE, FALSE, FALSE, 'name');
2552 $activityTypeId = array_search('Contribution', $activityTypeIds);
2553
2554 if ($activityTypeId && $contributorId) {
2555 $activityQuery = "
2556 SELECT civicrm_activity_contact.contact_id
2557 FROM civicrm_activity_contact
2558 INNER JOIN civicrm_activity ON civicrm_activity_contact.activity_id = civicrm_activity.id
2559 WHERE civicrm_activity.activity_type_id = %1
2560 AND civicrm_activity.source_record_id = %2
2561 AND civicrm_activity_contact.record_type_id = %3
2562 ";
2563
2564 $activityContacts = CRM_Activity_BAO_ActivityContact::buildOptions('record_type_id', 'validate');
2565 $sourceID = CRM_Utils_Array::key('Activity Source', $activityContacts);
2566
2567 $params = [
2568 1 => [$activityTypeId, 'Integer'],
2569 2 => [$contributionId, 'Integer'],
2570 3 => [$sourceID, 'Integer'],
2571 ];
2572
2573 $sourceContactId = CRM_Core_DAO::singleValueQuery($activityQuery, $params);
2574
2575 // for on behalf contribution source is individual and contributor is organization
2576 if ($sourceContactId && $sourceContactId != $contributorId) {
2577 $relationshipTypeIds = CRM_Core_PseudoConstant::relationshipType('name');
2578 // get rel type id for employee of relation
2579 foreach ($relationshipTypeIds as $id => $typeVals) {
2580 if ($typeVals['name_a_b'] == 'Employee of') {
2581 $relationshipTypeId = $id;
2582 break;
2583 }
2584 }
2585
2586 $rel = new CRM_Contact_DAO_Relationship();
2587 $rel->relationship_type_id = $relationshipTypeId;
2588 $rel->contact_id_a = $sourceContactId;
2589 $rel->contact_id_b = $contributorId;
2590 if ($rel->find(TRUE)) {
2591 $ids['individual_id'] = $rel->contact_id_a;
2592 $ids['organization_id'] = $rel->contact_id_b;
2593 }
2594 }
2595 }
2596
2597 return $ids;
2598 }
2599
2600 /**
2601 * @return array
2602 */
2603 public static function getContributionDates() {
2604 $config = CRM_Core_Config::singleton();
2605 $currentMonth = date('m');
2606 $currentDay = date('d');
2607 if ((int ) $config->fiscalYearStart['M'] > $currentMonth ||
2608 ((int ) $config->fiscalYearStart['M'] == $currentMonth &&
2609 (int ) $config->fiscalYearStart['d'] > $currentDay
2610 )
2611 ) {
2612 $year = date('Y') - 1;
2613 }
2614 else {
2615 $year = date('Y');
2616 }
2617 $year = ['Y' => $year];
2618 $yearDate = $config->fiscalYearStart;
2619 $yearDate = array_merge($year, $yearDate);
2620 $yearDate = CRM_Utils_Date::format($yearDate);
2621
2622 $monthDate = date('Ym') . '01';
2623
2624 $now = date('Ymd');
2625
2626 return [
2627 'now' => $now,
2628 'yearDate' => $yearDate,
2629 'monthDate' => $monthDate,
2630 ];
2631 }
2632
2633 /**
2634 * Load objects relations to contribution object.
2635 * Objects are stored in the $_relatedObjects property
2636 * In the first instance we are just moving functionality from BASEIpn -
2637 *
2638 * @see http://issues.civicrm.org/jira/browse/CRM-9996
2639 *
2640 * Note that the unit test for the BaseIPN class tests this function
2641 *
2642 * @param array $input
2643 * Input as delivered from Payment Processor.
2644 * @param array $ids
2645 * Ids as Loaded by Payment Processor.
2646 * @param bool $loadAll
2647 * Load all related objects - even where id not passed in? (allows API to call this).
2648 *
2649 * @return bool
2650 * @throws CRM_Core_Exception
2651 */
2652 public function loadRelatedObjects($input, &$ids, $loadAll = FALSE) {
2653 // @todo deprecate this function - the steps should be
2654 // 1) add additional functions like 'getRelatedMemberships'
2655 // 2) switch all calls that refer to ->_relatedObjects to
2656 // using the helper functions
2657 // 3) make ->_relatedObjects noisy in some way (deprecation won't work for properties - hmm
2658 // 4) make ->_relatedObjects protected
2659 // 5) hone up the individual functions to not use rely on this having been called
2660 // 6) deprecate like mad
2661 if ($loadAll) {
2662 $ids = array_merge($this->getComponentDetails($this->id), $ids);
2663 if (empty($ids['contact']) && isset($this->contact_id)) {
2664 $ids['contact'] = $this->contact_id;
2665 }
2666 }
2667 if (empty($this->_component)) {
2668 if (!empty($ids['event'])) {
2669 $this->_component = 'event';
2670 }
2671 else {
2672 $this->_component = strtolower(CRM_Utils_Array::value('component', $input, 'contribute'));
2673 }
2674 }
2675
2676 // If the object is not fully populated then make sure it is - this is a more about legacy paths & cautious
2677 // refactoring than anything else, and has unit test coverage.
2678 if (empty($this->financial_type_id)) {
2679 $this->find(TRUE);
2680 }
2681
2682 $paymentProcessorID = CRM_Utils_Array::value('payment_processor_id', $input, CRM_Utils_Array::value(
2683 'paymentProcessor',
2684 $ids
2685 ));
2686
2687 if (!isset($input['payment_processor_id']) && !$paymentProcessorID && $this->contribution_page_id) {
2688 $paymentProcessorID = CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_ContributionPage',
2689 $this->contribution_page_id,
2690 'payment_processor'
2691 );
2692 if ($paymentProcessorID) {
2693 $intentionalEnotice = $CRM16923AnUnreliableMethodHasBeenUserToDeterminePaymentProcessorFromContributionPage;
2694 }
2695 }
2696
2697 $ids['contributionType'] = $this->financial_type_id;
2698 $ids['financialType'] = $this->financial_type_id;
2699 if ($this->contribution_page_id) {
2700 $ids['contributionPage'] = $this->contribution_page_id;
2701 }
2702
2703 $this->loadRelatedEntitiesByID($ids);
2704
2705 if (!empty($ids['contributionRecur']) && !$paymentProcessorID) {
2706 $paymentProcessorID = $this->_relatedObjects['contributionRecur']->payment_processor_id;
2707 }
2708
2709 if (!empty($ids['pledge_payment'])) {
2710 foreach ($ids['pledge_payment'] as $key => $paymentID) {
2711 if (empty($paymentID)) {
2712 continue;
2713 }
2714 $payment = new CRM_Pledge_BAO_PledgePayment();
2715 $payment->id = $paymentID;
2716 if (!$payment->find(TRUE)) {
2717 throw new CRM_Core_Exception("Could not find pledge payment record: " . $paymentID);
2718 }
2719 $this->_relatedObjects['pledge_payment'][] = $payment;
2720 }
2721 }
2722
2723 // These are probably no longer accessed from anywhere
2724 // @todo remove this line, after ensuring not used.
2725 $ids = $this->loadRelatedMembershipObjects($ids);
2726
2727 if ($this->_component != 'contribute') {
2728 // we are in event mode
2729 // make sure event exists and is valid
2730 $event = new CRM_Event_BAO_Event();
2731 $event->id = $ids['event'];
2732 if ($ids['event'] &&
2733 !$event->find(TRUE)
2734 ) {
2735 throw new CRM_Core_Exception("Could not find event: " . $ids['event']);
2736 }
2737
2738 $this->_relatedObjects['event'] = &$event;
2739
2740 $participant = new CRM_Event_BAO_Participant();
2741 $participant->id = $ids['participant'];
2742 if ($ids['participant'] &&
2743 !$participant->find(TRUE)
2744 ) {
2745 throw new CRM_Core_Exception("Could not find participant: " . $ids['participant']);
2746 }
2747 $participant->register_date = CRM_Utils_Date::isoToMysql($participant->register_date);
2748
2749 $this->_relatedObjects['participant'] = &$participant;
2750
2751 // get the payment processor id from event - this is inaccurate see CRM-16923
2752 // in future we should look at throwing an exception here rather than an dubious guess.
2753 if (!$paymentProcessorID) {
2754 $paymentProcessorID = $this->_relatedObjects['event']->payment_processor;
2755 if ($paymentProcessorID) {
2756 $intentionalEnotice = $CRM16923AnUnreliableMethodHasBeenUserToDeterminePaymentProcessorFromEvent;
2757 }
2758 }
2759 }
2760
2761 $relatedContact = CRM_Contribute_BAO_Contribution::getOnbehalfIds($this->id);
2762 if (!empty($relatedContact['individual_id'])) {
2763 $ids['related_contact'] = $relatedContact['individual_id'];
2764 }
2765
2766 if ($paymentProcessorID) {
2767 $paymentProcessor = CRM_Financial_BAO_PaymentProcessor::getPayment($paymentProcessorID,
2768 $this->is_test ? 'test' : 'live'
2769 );
2770 $ids['paymentProcessor'] = $paymentProcessorID;
2771 $this->_relatedObjects['paymentProcessor'] = $paymentProcessor;
2772 }
2773
2774 // Add contribution id to $ids. CRM-20401
2775 $ids['contribution'] = $this->id;
2776 return TRUE;
2777 }
2778
2779 /**
2780 * Create array of message information - ie. return html version, txt version, to field
2781 *
2782 * @param array $input
2783 * Incoming information.
2784 * - is_recur - should this be treated as recurring (not sure why you wouldn't
2785 * just check presence of recur object but maintaining legacy approach
2786 * to be careful)
2787 * @param array $ids
2788 * IDs of related objects.
2789 * @param array $values
2790 * Any values that may have already been compiled by calling process.
2791 * This is augmented by values 'gathered' by gatherMessageValues
2792 * @param bool $returnMessageText
2793 * Distinguishes between whether to send message or return.
2794 * message text. We are working towards this function ALWAYS returning message text & calling
2795 * function doing emails / pdfs with it
2796 *
2797 * @return array
2798 * messages
2799 * @throws Exception
2800 */
2801 public function composeMessageArray(&$input, &$ids, &$values, $returnMessageText = TRUE) {
2802 $this->loadRelatedObjects($input, $ids, TRUE);
2803
2804 if (empty($this->_component)) {
2805 $this->_component = $input['component'] ?? NULL;
2806 }
2807
2808 //not really sure what params might be passed in but lets merge em into values
2809 $values = array_merge($this->_gatherMessageValues($input, $values, $ids), $values);
2810 $values['is_email_receipt'] = !$returnMessageText;
2811 foreach (['receipt_date', 'cc_receipt', 'bcc_receipt', 'receipt_from_name', 'receipt_from_email', 'receipt_text', 'pay_later_receipt'] as $fld) {
2812 if (!empty($input[$fld])) {
2813 $values[$fld] = $input[$fld];
2814 }
2815 }
2816
2817 $template = $this->_assignMessageVariablesToTemplate($values, $input, $returnMessageText);
2818 //what does recur 'mean here - to do with payment processor return functionality but
2819 // what is the importance
2820 if (!empty($this->contribution_recur_id) && !empty($this->_relatedObjects['paymentProcessor'])) {
2821 $paymentObject = Civi\Payment\System::singleton()->getByProcessor($this->_relatedObjects['paymentProcessor']);
2822
2823 $entityID = $entity = NULL;
2824 if (isset($ids['contribution'])) {
2825 $entity = 'contribution';
2826 $entityID = $ids['contribution'];
2827 }
2828 if (!empty($ids['membership'])) {
2829 //not sure whether is is possible for this not to be an array - load related contacts loads an array but this code was expecting a string
2830 // the addition of the casting is in case it could get here & be a string. Added in 4.6 - maybe remove later? This AuthorizeNetIPN & PaypalIPN tests hit this
2831 // line having loaded an array
2832 $ids['membership'] = (array) $ids['membership'];
2833 $entity = 'membership';
2834 $entityID = $ids['membership'][0];
2835 }
2836
2837 $template->assign('cancelSubscriptionUrl', $paymentObject->subscriptionURL($entityID, $entity, 'cancel'));
2838 $template->assign('updateSubscriptionBillingUrl', $paymentObject->subscriptionURL($entityID, $entity, 'billing'));
2839 $template->assign('updateSubscriptionUrl', $paymentObject->subscriptionURL($entityID, $entity, 'update'));
2840
2841 if ($this->_relatedObjects['paymentProcessor']['billing_mode'] & CRM_Core_Payment::BILLING_MODE_FORM) {
2842 //direct mode showing billing block, so use directIPN for temporary
2843 $template->assign('contributeMode', 'directIPN');
2844 }
2845 }
2846 // todo remove strtolower - check consistency
2847 if (strtolower($this->_component) === 'event') {
2848 $eventParams = ['id' => $this->_relatedObjects['participant']->event_id];
2849 $values['event'] = [];
2850
2851 CRM_Event_BAO_Event::retrieve($eventParams, $values['event']);
2852
2853 //get location details
2854 $locationParams = [
2855 'entity_id' => $this->_relatedObjects['participant']->event_id,
2856 'entity_table' => 'civicrm_event',
2857 ];
2858 $values['location'] = CRM_Core_BAO_Location::getValues($locationParams);
2859
2860 $ufJoinParams = [
2861 'entity_table' => 'civicrm_event',
2862 'entity_id' => $ids['event'],
2863 'module' => 'CiviEvent',
2864 ];
2865
2866 [$custom_pre_id, $custom_post_ids] = CRM_Core_BAO_UFJoin::getUFGroupIds($ufJoinParams);
2867
2868 $values['custom_pre_id'] = $custom_pre_id;
2869 $values['custom_post_id'] = $custom_post_ids;
2870 //for tasks 'Change Participant Status' and 'Update multiple Contributions' case
2871 //and cases involving status updation through ipn
2872 // whatever that means!
2873 // total_amount appears to be the preferred input param & it is unclear why we support amount here
2874 // perhaps we should throw an e-notice if amount is set & force total_amount?
2875 if (!empty($input['amount'])) {
2876 $values['totalAmount'] = $input['amount'];
2877 }
2878 // @todo set this in is_email_receipt, based on $this->_relatedObjects.
2879 if ($values['event']['is_email_confirm']) {
2880 $values['is_email_receipt'] = 1;
2881 }
2882
2883 if (!empty($ids['contribution'])) {
2884 $values['contributionId'] = $ids['contribution'];
2885 }
2886
2887 return CRM_Event_BAO_Event::sendMail($ids['contact'], $values,
2888 $this->_relatedObjects['participant']->id, $this->is_test, $returnMessageText
2889 );
2890 }
2891 else {
2892 $values['contribution_id'] = $this->id;
2893 if (!empty($ids['related_contact'])) {
2894 $values['related_contact'] = $ids['related_contact'];
2895 if (isset($ids['onbehalf_dupe_alert'])) {
2896 $values['onbehalf_dupe_alert'] = $ids['onbehalf_dupe_alert'];
2897 }
2898 $entityBlock = [
2899 'contact_id' => $ids['contact'],
2900 'location_type_id' => CRM_Core_DAO::getFieldValue('CRM_Core_DAO_LocationType',
2901 'Home', 'id', 'name'
2902 ),
2903 ];
2904 $address = CRM_Core_BAO_Address::getValues($entityBlock);
2905 $template->assign('onBehalfAddress', $address[$entityBlock['location_type_id']]['display'] ?? NULL);
2906 }
2907 $isTest = FALSE;
2908 if ($this->is_test) {
2909 $isTest = TRUE;
2910 }
2911 if (!empty($this->_relatedObjects['membership'])) {
2912 foreach ($this->_relatedObjects['membership'] as $membership) {
2913 if ($membership->id) {
2914 $values['membership_id'] = $membership->id;
2915 $values['isMembership'] = TRUE;
2916 $values['membership_assign'] = TRUE;
2917
2918 // need to set the membership values here
2919 $template->assign('membership_name',
2920 CRM_Member_PseudoConstant::membershipType($membership->membership_type_id)
2921 );
2922 $template->assign('mem_start_date', $membership->start_date);
2923 $template->assign('mem_join_date', $membership->join_date);
2924 $template->assign('mem_end_date', $membership->end_date);
2925 $membership_status = CRM_Member_PseudoConstant::membershipStatus($membership->status_id, NULL, 'label');
2926 $template->assign('mem_status', $membership_status);
2927 if ($membership_status === 'Pending' && $membership->is_pay_later == 1) {
2928 $values['is_pay_later'] = 1;
2929 }
2930 // Pass amount to floatval as string '0.00' is considered a
2931 // valid amount and includes Fee section in the mail.
2932 if (isset($values['amount'])) {
2933 $values['amount'] = floatval($values['amount']);
2934 }
2935
2936 if (!empty($this->contribution_recur_id) && $paymentObject) {
2937 $url = $paymentObject->subscriptionURL($membership->id, 'membership', 'cancel');
2938 $template->assign('cancelSubscriptionUrl', $url);
2939 $url = $paymentObject->subscriptionURL($membership->id, 'membership', 'billing');
2940 $template->assign('updateSubscriptionBillingUrl', $url);
2941 $url = $paymentObject->subscriptionURL($entityID, $entity, 'update');
2942 $template->assign('updateSubscriptionUrl', $url);
2943 }
2944
2945 $result = CRM_Contribute_BAO_ContributionPage::sendMail($ids['contact'], $values, $isTest, $returnMessageText);
2946
2947 return $result;
2948 // otherwise if its about sending emails, continue sending without return, as we
2949 // don't want to exit the loop.
2950 }
2951 }
2952 }
2953 else {
2954 return CRM_Contribute_BAO_ContributionPage::sendMail($ids['contact'], $values, $isTest, $returnMessageText);
2955 }
2956 }
2957 }
2958
2959 /**
2960 * Gather values for contribution mail - this function has been created
2961 * as part of CRM-9996 refactoring as a step towards simplifying the composeMessage function
2962 * Values related to the contribution in question are gathered
2963 *
2964 * @param array $input
2965 * Input into function (probably from payment processor).
2966 * @param array $values
2967 * @param array $ids
2968 * The set of ids related to the input.
2969 *
2970 * @return array
2971 * @throws \CRM_Core_Exception
2972 */
2973 public function _gatherMessageValues($input, &$values, $ids = []) {
2974 // set display address of contributor
2975 $values['billingName'] = '';
2976 if ($this->address_id) {
2977 $addressDetails = CRM_Core_BAO_Address::getValues(['id' => $this->address_id], FALSE, 'id');
2978 $addressDetails = reset($addressDetails);
2979 $values['billingName'] = $addressDetails['name'] ?? '';
2980 }
2981 // Else we assign the billing address of the contribution contact.
2982 else {
2983 $addressDetails = (array) CRM_Core_BAO_Address::getValues(['contact_id' => $this->contact_id, 'is_billing' => 1]);
2984 $addressDetails = reset($addressDetails);
2985 }
2986 $values['address'] = $addressDetails['display'] ?? '';
2987
2988 if ($this->_component === 'contribute') {
2989 //get soft contributions
2990 $softContributions = CRM_Contribute_BAO_ContributionSoft::getSoftContribution($this->id, TRUE);
2991 if (!empty($softContributions)) {
2992 // For pcp soft credit, there is no 'soft_credit' member it comes
2993 // back in different array members, but shortly after returning from
2994 // this function it calls _assignMessageVariablesToTemplate which does
2995 // its own lookup of any pcp soft credit, so we can skip it here.
2996 $values['softContributions'] = $softContributions['soft_credit'] ?? NULL;
2997 }
2998 if (isset($this->contribution_page_id)) {
2999 // This is a call we want to use less, in favour of loading related objects.
3000 $values = $this->addContributionPageValuesToValuesHeavyHandedly($values);
3001 if ($this->contribution_page_id) {
3002 // This is precautionary as there are some legacy flows, but it should really be
3003 // loaded by now.
3004 if (!isset($this->_relatedObjects['contributionPage'])) {
3005 $this->loadRelatedEntitiesByID(['contributionPage' => $this->contribution_page_id]);
3006 }
3007 CRM_Contribute_BAO_Contribution_Utils::overrideDefaultCurrency($values);
3008 }
3009 }
3010 // no contribution page -probably back office
3011 else {
3012 // Handle re-print receipt for offline contributions (call from PDF.php - no contribution_page_id)
3013 $values['title'] = 'Contribution';
3014 }
3015 // set lineItem for contribution
3016 if ($this->id) {
3017 $lineItems = CRM_Price_BAO_LineItem::getLineItemsByContributionID($this->id);
3018 if (!empty($lineItems)) {
3019 $firstLineItem = reset($lineItems);
3020 $priceSet = [];
3021 if (!empty($firstLineItem['price_set_id'])) {
3022 $priceSet = civicrm_api3('PriceSet', 'getsingle', [
3023 'id' => $firstLineItem['price_set_id'],
3024 'return' => 'is_quick_config, id',
3025 ]);
3026 $values['priceSetID'] = $priceSet['id'];
3027 }
3028 foreach ($lineItems as &$eachItem) {
3029 if ($eachItem['entity_table'] === 'civicrm_membership') {
3030 $membership = reset(civicrm_api3('Membership', 'get', [
3031 'id' => $eachItem['entity_id'],
3032 'return' => ['join_date', 'start_date', 'end_date'],
3033 ])['values']);
3034 if ($membership) {
3035 $eachItem['join_date'] = CRM_Utils_Date::customFormat($membership['join_date']);
3036 $eachItem['start_date'] = CRM_Utils_Date::customFormat($membership['start_date']);
3037 $eachItem['end_date'] = CRM_Utils_Date::customFormat($membership['end_date']);
3038 }
3039 }
3040 // This is actually used in conjunction with is_quick_config in the template & we should deprecate it.
3041 // However, that does create upgrade pain so would be better to be phased in.
3042 $values['useForMember'] = empty($priceSet['is_quick_config']);
3043 }
3044 $values['lineItem'][0] = $lineItems;
3045 }
3046 }
3047
3048 $relatedContact = CRM_Contribute_BAO_Contribution::getOnbehalfIds(
3049 $this->id,
3050 $this->contact_id
3051 );
3052 // if this is onbehalf of contribution then set related contact
3053 if (!empty($relatedContact['individual_id'])) {
3054 $values['related_contact'] = $ids['related_contact'] = $relatedContact['individual_id'];
3055 }
3056 }
3057 else {
3058 $values = array_merge($values, $this->loadEventMessageTemplateParams((int) $ids['event'], (int) $this->_relatedObjects['participant']->id, $this->id));
3059 }
3060
3061 $groupTree = CRM_Core_BAO_CustomGroup::getTree('Contribution', NULL, $this->id);
3062
3063 $customGroup = [];
3064 foreach ($groupTree as $key => $group) {
3065 if ($key === 'info') {
3066 continue;
3067 }
3068
3069 foreach ($group['fields'] as $k => $customField) {
3070 $groupLabel = $group['title'];
3071 if (!empty($customField['customValue'])) {
3072 foreach ($customField['customValue'] as $customFieldValues) {
3073 $customGroup[$groupLabel][$customField['label']] = $customFieldValues['data'] ?? NULL;
3074 }
3075 }
3076 }
3077 }
3078 $values['customGroup'] = $customGroup;
3079
3080 $values['is_pay_later'] = $this->is_pay_later;
3081
3082 return $values;
3083 }
3084
3085 /**
3086 * Assign message variables to template but try to break the habit.
3087 *
3088 * In order to get away from leaky variables it is better to ensure variables are set in values and assign them
3089 * from the send function. Otherwise smarty variables can leak if this is called more than once - e.g. processing
3090 * multiple recurring payments for processors like IATS that use tokens.
3091 *
3092 * Apply variables for message to smarty template - this function is part of analysing what is in the huge
3093 * function & breaking it down into manageable chunks. Eventually it will be refactored into something else
3094 * Note we send directly from this function in some cases because it is only partly refactored.
3095 *
3096 * Don't call this function directly as the signature will change.
3097 *
3098 * @param $values
3099 * @param $input
3100 * @param bool $returnMessageText
3101 *
3102 * @return mixed
3103 */
3104 public function _assignMessageVariablesToTemplate(&$values, $input, $returnMessageText = TRUE) {
3105 // @todo - this should have a better separation of concerns - ie.
3106 // gatherMessageValues should build an array of values to be assigned to the template
3107 // and this function should assign them (assigning null if not set).
3108 // the way the pcpParams & honor Params section works is a baby-step towards this.
3109 $template = CRM_Core_Smarty::singleton();
3110 $template->assign('first_name', $this->_relatedObjects['contact']->first_name);
3111 $template->assign('last_name', $this->_relatedObjects['contact']->last_name);
3112 $template->assign('displayName', $this->_relatedObjects['contact']->display_name);
3113 $template->assign('billingName', $values['billingName']);
3114
3115 // For some unit tests contribution cannot contain paymentProcessor information
3116 $billingMode = empty($this->_relatedObjects['paymentProcessor']) ? CRM_Core_Payment::BILLING_MODE_NOTIFY : $this->_relatedObjects['paymentProcessor']['billing_mode'];
3117 $template->assign('contributeMode', CRM_Core_SelectValues::contributeMode()[$billingMode] ?? NULL);
3118
3119 //assign honor information to receipt message
3120 $softRecord = CRM_Contribute_BAO_ContributionSoft::getSoftContribution($this->id);
3121
3122 $honorParams = [
3123 'soft_credit_type' => NULL,
3124 'honor_block_is_active' => NULL,
3125 ];
3126 if (isset($softRecord['soft_credit'])) {
3127 //if id of contribution page is present
3128 if (!empty($values['id'])) {
3129 $values['honor'] = [
3130 'honor_profile_values' => [],
3131 'honor_profile_id' => CRM_Core_DAO::getFieldValue('CRM_Core_DAO_UFJoin', $values['id'], 'uf_group_id', 'entity_id'),
3132 'honor_id' => $softRecord['soft_credit'][1]['contact_id'],
3133 ];
3134
3135 $honorParams['soft_credit_type'] = $softRecord['soft_credit'][1]['soft_credit_type_label'];
3136 $honorParams['honor_block_is_active'] = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_UFJoin', $values['id'], 'is_active', 'entity_id');
3137 }
3138 else {
3139 //offline contribution
3140 $softCreditTypes = $softCredits = [];
3141 foreach ($softRecord['soft_credit'] as $key => $softCredit) {
3142 $softCreditTypes[$key] = $softCredit['soft_credit_type_label'];
3143 $softCredits[$key] = [
3144 'Name' => $softCredit['contact_name'],
3145 'Amount' => CRM_Utils_Money::format($softCredit['amount'], $softCredit['currency']),
3146 ];
3147 }
3148 $template->assign('softCreditTypes', $softCreditTypes);
3149 $template->assign('softCredits', $softCredits);
3150 }
3151 }
3152
3153 $dao = new CRM_Contribute_DAO_ContributionProduct();
3154 $dao->contribution_id = $this->id;
3155 if ($dao->find(TRUE)) {
3156 $premiumId = $dao->product_id;
3157 $template->assign('option', $dao->product_option);
3158
3159 $productDAO = new CRM_Contribute_DAO_Product();
3160 $productDAO->id = $premiumId;
3161 $productDAO->find(TRUE);
3162 $template->assign('selectPremium', TRUE);
3163 $template->assign('product_name', $productDAO->name);
3164 $template->assign('price', $productDAO->price);
3165 $template->assign('sku', $productDAO->sku);
3166 }
3167 $template->assign('title', $values['title'] ?? NULL);
3168 $values['amount'] = CRM_Utils_Array::value('total_amount', $input, (CRM_Utils_Array::value('amount', $input)), NULL);
3169 if (!$values['amount'] && isset($this->total_amount)) {
3170 $values['amount'] = $this->total_amount;
3171 }
3172
3173 $pcpParams = [
3174 'pcpBlock' => NULL,
3175 'pcp_display_in_roll' => NULL,
3176 'pcp_roll_nickname' => NULL,
3177 'pcp_personal_note' => NULL,
3178 'title' => NULL,
3179 ];
3180
3181 if (strtolower($this->_component) == 'contribute') {
3182 //PCP Info
3183 $softDAO = new CRM_Contribute_DAO_ContributionSoft();
3184 $softDAO->contribution_id = $this->id;
3185 if ($softDAO->find(TRUE)) {
3186 $pcpParams['pcpBlock'] = TRUE;
3187 $pcpParams['pcp_display_in_roll'] = $softDAO->pcp_display_in_roll;
3188 $pcpParams['pcp_roll_nickname'] = $softDAO->pcp_roll_nickname;
3189 $pcpParams['pcp_personal_note'] = $softDAO->pcp_personal_note;
3190
3191 //assign the pcp page title for email subject
3192 $pcpDAO = new CRM_PCP_DAO_PCP();
3193 $pcpDAO->id = $softDAO->pcp_id;
3194 if ($pcpDAO->find(TRUE)) {
3195 $pcpParams['title'] = $pcpDAO->title;
3196 }
3197 }
3198 }
3199 foreach (array_merge($honorParams, $pcpParams) as $templateKey => $templateValue) {
3200 $template->assign($templateKey, $templateValue);
3201 }
3202
3203 if ($this->financial_type_id) {
3204 $values['financial_type_id'] = $this->financial_type_id;
3205 }
3206
3207 $template->assign('trxn_id', $this->trxn_id);
3208 $template->assign('receive_date',
3209 CRM_Utils_Date::processDate($this->receive_date)
3210 );
3211 $values['receipt_date'] = (empty($this->receipt_date) ? NULL : $this->receipt_date);
3212 $template->assign('action', $this->is_test ? 1024 : 1);
3213 $template->assign('receipt_text', $values['receipt_text'] ?? NULL);
3214 $template->assign('is_monetary', 1);
3215 $template->assign('is_recur', !empty($this->contribution_recur_id));
3216 $template->assign('currency', $this->currency);
3217 $template->assign('address', CRM_Utils_Address::format($input));
3218 if (!empty($values['customGroup'])) {
3219 $template->assign('customGroup', $values['customGroup']);
3220 }
3221 if (!empty($values['softContributions'])) {
3222 $template->assign('softContributions', $values['softContributions']);
3223 }
3224 if ($this->_component == 'event') {
3225 $template->assign('title', $values['event']['title']);
3226 $participantRoles = CRM_Event_PseudoConstant::participantRole();
3227 $viewRoles = [];
3228 foreach (explode(CRM_Core_DAO::VALUE_SEPARATOR, $this->_relatedObjects['participant']->role_id) as $k => $v) {
3229 $viewRoles[] = $participantRoles[$v];
3230 }
3231 $values['event']['participant_role'] = implode(', ', $viewRoles);
3232 $template->assign('event', $values['event']);
3233 $template->assign('participant', $values['participant']);
3234 $template->assign('location', $values['location']);
3235 $template->assign('customPre', $values['custom_pre_id']);
3236 $template->assign('customPost', $values['custom_post_id']);
3237
3238 $isTest = FALSE;
3239 if ($this->_relatedObjects['participant']->is_test) {
3240 $isTest = TRUE;
3241 }
3242
3243 $values['params'] = [];
3244 //to get email of primary participant.
3245 $primaryEmail = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_Email', $this->_relatedObjects['participant']->contact_id, 'email', 'contact_id');
3246 $primaryAmount[] = [
3247 'label' => $this->_relatedObjects['participant']->fee_level . ' - ' . $primaryEmail,
3248 'amount' => $this->_relatedObjects['participant']->fee_amount,
3249 ];
3250 //build an array of cId/pId of participants
3251 $additionalIDs = CRM_Event_BAO_Event::buildCustomProfile($this->_relatedObjects['participant']->id, NULL, $this->_relatedObjects['contact']->id, $isTest, TRUE);
3252 unset($additionalIDs[$this->_relatedObjects['participant']->id]);
3253 //send receipt to additional participant if exists
3254 if (count($additionalIDs)) {
3255 $template->assign('isPrimary', 0);
3256 $template->assign('customProfile', NULL);
3257 //set additionalParticipant true
3258 $values['params']['additionalParticipant'] = TRUE;
3259 foreach ($additionalIDs as $pId => $cId) {
3260 $amount = [];
3261 //to change the status pending to completed
3262 $additional = new CRM_Event_DAO_Participant();
3263 $additional->id = $pId;
3264 $additional->contact_id = $cId;
3265 $additional->find(TRUE);
3266 $additional->register_date = $this->_relatedObjects['participant']->register_date;
3267 $additional->status_id = 1;
3268 $additionalParticipantInfo = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_Email', $additional->contact_id, 'email', 'contact_id');
3269 //if additional participant dont have email
3270 //use display name.
3271 if (!$additionalParticipantInfo) {
3272 $additionalParticipantInfo = CRM_Contact_BAO_Contact::displayName($additional->contact_id);
3273 }
3274 $amount[0] = [
3275 'label' => $additional->fee_level,
3276 'amount' => $additional->fee_amount,
3277 ];
3278 $primaryAmount[] = [
3279 'label' => $additional->fee_level . ' - ' . $additionalParticipantInfo,
3280 'amount' => $additional->fee_amount,
3281 ];
3282 $additional->save();
3283 $template->assign('amount', $amount);
3284 CRM_Event_BAO_Event::sendMail($cId, $values, $pId, $isTest, $returnMessageText);
3285 }
3286 }
3287
3288 //build an array of custom profile and assigning it to template
3289 $customProfile = CRM_Event_BAO_Event::buildCustomProfile($this->_relatedObjects['participant']->id, $values, NULL, $isTest);
3290
3291 if (count($customProfile)) {
3292 $template->assign('customProfile', $customProfile);
3293 }
3294
3295 // for primary contact
3296 $values['params']['additionalParticipant'] = FALSE;
3297 $template->assign('isPrimary', 1);
3298 $template->assign('amount', $primaryAmount);
3299 $template->assign('register_date', CRM_Utils_Date::isoToMysql($this->_relatedObjects['participant']->register_date));
3300 if ($this->payment_instrument_id) {
3301 $paymentInstrument = CRM_Contribute_PseudoConstant::paymentInstrument();
3302 $template->assign('paidBy', $paymentInstrument[$this->payment_instrument_id]);
3303 }
3304 // carry paylater, since we did not created billing,
3305 // so need to pull email from primary location, CRM-4395
3306 $values['params']['is_pay_later'] = $this->_relatedObjects['participant']->is_pay_later;
3307 }
3308 return $template;
3309 }
3310
3311 /**
3312 * Check whether payment processor supports
3313 * cancellation of contribution subscription
3314 *
3315 * @param int $contributionId
3316 * Contribution id.
3317 *
3318 * @param bool $isNotCancelled
3319 *
3320 * @return bool
3321 */
3322 public static function isCancelSubscriptionSupported($contributionId, $isNotCancelled = TRUE) {
3323 $cacheKeyString = "$contributionId";
3324 $cacheKeyString .= $isNotCancelled ? '_1' : '_0';
3325
3326 static $supportsCancel = [];
3327
3328 if (!array_key_exists($cacheKeyString, $supportsCancel)) {
3329 $supportsCancel[$cacheKeyString] = FALSE;
3330 $isCancelled = FALSE;
3331
3332 if ($isNotCancelled) {
3333 $isCancelled = self::isSubscriptionCancelled($contributionId);
3334 }
3335
3336 $paymentObject = CRM_Financial_BAO_PaymentProcessor::getProcessorForEntity($contributionId, 'contribute', 'obj');
3337 if (!empty($paymentObject)) {
3338 $supportsCancel[$cacheKeyString] = $paymentObject->supports('cancelRecurring') && !$isCancelled;
3339 }
3340 }
3341 return $supportsCancel[$cacheKeyString];
3342 }
3343
3344 /**
3345 * Check whether subscription is already cancelled.
3346 *
3347 * @param int $contributionId
3348 * Contribution id.
3349 *
3350 * @return string
3351 * contribution status
3352 */
3353 public static function isSubscriptionCancelled($contributionId) {
3354 $sql = "
3355 SELECT cr.contribution_status_id
3356 FROM civicrm_contribution_recur cr
3357 LEFT JOIN civicrm_contribution con ON ( cr.id = con.contribution_recur_id )
3358 WHERE con.id = %1 LIMIT 1";
3359 $params = [1 => [$contributionId, 'Integer']];
3360 $statusId = CRM_Core_DAO::singleValueQuery($sql, $params);
3361 $status = CRM_Contribute_PseudoConstant::contributionStatus($statusId, 'name');
3362 if ($status == 'Cancelled') {
3363 return TRUE;
3364 }
3365 return FALSE;
3366 }
3367
3368 /**
3369 * Create all financial accounts entry.
3370 *
3371 * @param array $params
3372 * Contribution object, line item array and params for trxn.
3373 *
3374 *
3375 * @return null|\CRM_Core_BAO_FinancialTrxn
3376 */
3377 public static function recordFinancialAccounts(&$params) {
3378 $skipRecords = $return = FALSE;
3379 $isUpdate = !empty($params['prevContribution']);
3380
3381 $additionalParticipantId = [];
3382 $contributionStatuses = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name');
3383 $contributionStatus = empty($params['contribution_status_id']) ? NULL : $contributionStatuses[$params['contribution_status_id']];
3384
3385 if (CRM_Utils_Array::value('contribution_mode', $params) == 'participant') {
3386 $entityId = $params['participant_id'];
3387 $entityTable = 'civicrm_participant';
3388 $additionalParticipantId = CRM_Event_BAO_Participant::getAdditionalParticipantIds($entityId);
3389 }
3390 elseif (!empty($params['membership_id'])) {
3391 //so far $params['membership_id'] should only be set coming in from membershipBAO::create so the situation where multiple memberships
3392 // are created off one contribution should be handled elsewhere
3393 $entityId = $params['membership_id'];
3394 $entityTable = 'civicrm_membership';
3395 }
3396 else {
3397 $entityId = $params['contribution']->id;
3398 $entityTable = 'civicrm_contribution';
3399 }
3400
3401 $entityID[] = $entityId;
3402 if (!empty($additionalParticipantId)) {
3403 $entityID += $additionalParticipantId;
3404 // build line item array if necessary
3405 if ($additionalParticipantId) {
3406 CRM_Price_BAO_LineItem::getLineItemArray($params, $entityID, str_replace('civicrm_', '', $entityTable));
3407 }
3408 }
3409 // prevContribution appears to mean - original contribution object- ie copy of contribution from before the update started that is being updated
3410 if (empty($params['prevContribution'])) {
3411 $entityID = NULL;
3412 }
3413
3414 $statusId = $params['contribution']->contribution_status_id;
3415
3416 if ($contributionStatus != 'Failed' &&
3417 !($contributionStatus == 'Pending' && !$params['contribution']->is_pay_later)
3418 ) {
3419 $skipRecords = TRUE;
3420 $pendingStatus = [
3421 'Pending',
3422 'In Progress',
3423 ];
3424 if (in_array($contributionStatus, $pendingStatus)) {
3425 $params['to_financial_account_id'] = CRM_Financial_BAO_FinancialAccount::getFinancialAccountForFinancialTypeByRelationship(
3426 $params['financial_type_id'],
3427 'Accounts Receivable Account is'
3428 );
3429 }
3430 elseif (!empty($params['payment_processor'])) {
3431 $params['to_financial_account_id'] = CRM_Contribute_PseudoConstant::getRelationalFinancialAccount($params['payment_processor'], NULL, 'civicrm_payment_processor');
3432 $params['payment_instrument_id'] = civicrm_api3('PaymentProcessor', 'getvalue', [
3433 'id' => $params['payment_processor'],
3434 'return' => 'payment_instrument_id',
3435 ]);
3436 }
3437 elseif (!empty($params['payment_instrument_id'])) {
3438 $params['to_financial_account_id'] = CRM_Financial_BAO_FinancialTypeAccount::getInstrumentFinancialAccount($params['payment_instrument_id']);
3439 }
3440 // dev/financial#160 - If this is a contribution update, also check for an existing payment_instrument_id.
3441 elseif ($isUpdate && $params['prevContribution']->payment_instrument_id) {
3442 $params['to_financial_account_id'] = CRM_Financial_BAO_FinancialTypeAccount::getInstrumentFinancialAccount((int) $params['prevContribution']->payment_instrument_id);
3443 }
3444 else {
3445 $relationTypeId = key(CRM_Core_PseudoConstant::accountOptionValues('financial_account_type', NULL, " AND v.name LIKE 'Asset' "));
3446 $queryParams = [1 => [$relationTypeId, 'Integer']];
3447 $params['to_financial_account_id'] = CRM_Core_DAO::singleValueQuery("SELECT id FROM civicrm_financial_account WHERE is_default = 1 AND financial_account_type_id = %1", $queryParams);
3448 }
3449
3450 $totalAmount = $params['total_amount'] ?? NULL;
3451 if (!isset($totalAmount) && !empty($params['prevContribution'])) {
3452 $totalAmount = $params['total_amount'] = $params['prevContribution']->total_amount;
3453 }
3454 //build financial transaction params
3455 $trxnParams = [
3456 'contribution_id' => $params['contribution']->id,
3457 'to_financial_account_id' => $params['to_financial_account_id'],
3458 'trxn_date' => !empty($params['contribution']->receive_date) ? $params['contribution']->receive_date : date('YmdHis'),
3459 'total_amount' => $totalAmount,
3460 'fee_amount' => $params['fee_amount'] ?? NULL,
3461 'net_amount' => CRM_Utils_Array::value('net_amount', $params, $totalAmount),
3462 'currency' => $params['contribution']->currency,
3463 'trxn_id' => $params['contribution']->trxn_id,
3464 // @todo - this is getting the status id from the contribution - that is BAD - ie the contribution could be partially
3465 // paid but each payment is completed. The work around is to pass in the status_id in the trxn_params but
3466 // this should really default to completed (after discussion).
3467 'status_id' => $statusId,
3468 'payment_instrument_id' => CRM_Utils_Array::value('payment_instrument_id', $params, $params['contribution']->payment_instrument_id),
3469 'check_number' => $params['check_number'] ?? NULL,
3470 'pan_truncation' => $params['pan_truncation'] ?? NULL,
3471 'card_type_id' => $params['card_type_id'] ?? NULL,
3472 ];
3473 if ($contributionStatus == 'Refunded' || $contributionStatus == 'Chargeback' || $contributionStatus == 'Cancelled') {
3474 $trxnParams['trxn_date'] = !empty($params['contribution']->cancel_date) ? $params['contribution']->cancel_date : date('YmdHis');
3475 if (isset($params['refund_trxn_id'])) {
3476 // CRM-17751 allow a separate trxn_id for the refund to be passed in via api & form.
3477 $trxnParams['trxn_id'] = $params['refund_trxn_id'];
3478 }
3479 }
3480 //CRM-16259, set is_payment flag for non pending status
3481 if (!in_array($contributionStatus, $pendingStatus)) {
3482 $trxnParams['is_payment'] = 1;
3483 }
3484 if (!empty($params['payment_processor'])) {
3485 $trxnParams['payment_processor_id'] = $params['payment_processor'];
3486 }
3487
3488 if (empty($trxnParams['payment_processor_id'])) {
3489 unset($trxnParams['payment_processor_id']);
3490 }
3491
3492 $params['trxnParams'] = $trxnParams;
3493
3494 if ($isUpdate) {
3495 $updated = FALSE;
3496 $params['trxnParams']['total_amount'] = $trxnParams['total_amount'] = $params['total_amount'] = $params['prevContribution']->total_amount;
3497 $params['trxnParams']['fee_amount'] = $params['prevContribution']->fee_amount;
3498 $params['trxnParams']['net_amount'] = $params['prevContribution']->net_amount;
3499 if (!isset($params['trxnParams']['trxn_id'])) {
3500 // Actually I have no idea why we are overwriting any values from the previous contribution.
3501 // (filling makes sense to me). However, only protecting this value as I really really know we
3502 // don't want this one overwritten.
3503 // CRM-17751.
3504 $params['trxnParams']['trxn_id'] = $params['prevContribution']->trxn_id;
3505 }
3506 $params['trxnParams']['status_id'] = $params['prevContribution']->contribution_status_id;
3507
3508 if (!(($params['prevContribution']->contribution_status_id == array_search('Pending', $contributionStatuses)
3509 || $params['prevContribution']->contribution_status_id == array_search('In Progress', $contributionStatuses))
3510 && $params['contribution']->contribution_status_id == array_search('Completed', $contributionStatuses))
3511 ) {
3512 $params['trxnParams']['payment_instrument_id'] = $params['prevContribution']->payment_instrument_id;
3513 $params['trxnParams']['check_number'] = $params['prevContribution']->check_number;
3514 }
3515
3516 //if financial type is changed
3517 if (!empty($params['financial_type_id']) &&
3518 $params['contribution']->financial_type_id != $params['prevContribution']->financial_type_id
3519 ) {
3520 $accountRelationship = 'Income Account is';
3521 if (!empty($params['revenue_recognition_date']) || $params['prevContribution']->revenue_recognition_date) {
3522 $accountRelationship = 'Deferred Revenue Account is';
3523 }
3524 $oldFinancialAccount = CRM_Financial_BAO_FinancialAccount::getFinancialAccountForFinancialTypeByRelationship($params['prevContribution']->financial_type_id, $accountRelationship);
3525 $newFinancialAccount = CRM_Financial_BAO_FinancialAccount::getFinancialAccountForFinancialTypeByRelationship($params['financial_type_id'], $accountRelationship);
3526 if ($oldFinancialAccount != $newFinancialAccount) {
3527 $params['total_amount'] = 0;
3528 // If we have a fee amount set reverse this as well.
3529 if (isset($params['fee_amount'])) {
3530 $params['trxnParams']['fee_amount'] = 0 - $params['fee_amount'];
3531 }
3532 if (in_array($params['contribution']->contribution_status_id, $pendingStatus)) {
3533 $params['trxnParams']['to_financial_account_id'] = CRM_Financial_BAO_FinancialAccount::getFinancialAccountForFinancialTypeByRelationship(
3534 $params['prevContribution']->financial_type_id, $accountRelationship);
3535 }
3536 else {
3537 $lastFinancialTrxnId = CRM_Core_BAO_FinancialTrxn::getFinancialTrxnId($params['prevContribution']->id, 'DESC');
3538 if (!empty($lastFinancialTrxnId['financialTrxnId'])) {
3539 $params['trxnParams']['to_financial_account_id'] = CRM_Core_DAO::getFieldValue('CRM_Financial_DAO_FinancialTrxn', $lastFinancialTrxnId['financialTrxnId'], 'to_financial_account_id');
3540 }
3541 }
3542 self::updateFinancialAccounts($params, 'changeFinancialType');
3543 $params['skipLineItem'] = FALSE;
3544 foreach ($params['line_item'] as &$lineItems) {
3545 foreach ($lineItems as &$line) {
3546 $line['financial_type_id'] = $params['financial_type_id'];
3547 }
3548 }
3549 CRM_Core_BAO_FinancialTrxn::createDeferredTrxn(CRM_Utils_Array::value('line_item', $params), $params['contribution'], TRUE, 'changeFinancialType');
3550 /* $params['trxnParams']['to_financial_account_id'] = $trxnParams['to_financial_account_id']; */
3551 $params['financial_account_id'] = $newFinancialAccount;
3552 $params['total_amount'] = $params['trxnParams']['total_amount'] = $params['trxnParams']['net_amount'] = $trxnParams['total_amount'];
3553 // Set the transaction fee amount back to the original value for creating the new positive financial trxn.
3554 if (isset($params['fee_amount'])) {
3555 $params['trxnParams']['fee_amount'] = $params['fee_amount'];
3556 }
3557 self::updateFinancialAccounts($params);
3558 CRM_Core_BAO_FinancialTrxn::createDeferredTrxn(CRM_Utils_Array::value('line_item', $params), $params['contribution'], TRUE);
3559 $params['trxnParams']['to_financial_account_id'] = $trxnParams['to_financial_account_id'];
3560 $updated = TRUE;
3561 $params['deferred_financial_account_id'] = $newFinancialAccount;
3562 }
3563 }
3564
3565 //Update contribution status
3566 $params['trxnParams']['status_id'] = $params['contribution']->contribution_status_id;
3567 if (!isset($params['refund_trxn_id'])) {
3568 // CRM-17751 This has previously been deliberately set. No explanation as to why one variant
3569 // gets preference over another so I am only 'protecting' a very specific tested flow
3570 // and letting natural justice take care of the rest.
3571 $params['trxnParams']['trxn_id'] = $params['contribution']->trxn_id;
3572 }
3573 if (!empty($params['contribution_status_id']) &&
3574 $params['prevContribution']->contribution_status_id != $params['contribution']->contribution_status_id
3575 ) {
3576 //Update Financial Records
3577 $callUpdateFinancialAccounts = self::updateFinancialAccountsOnContributionStatusChange($params);
3578 if ($callUpdateFinancialAccounts) {
3579 self::updateFinancialAccounts($params, 'changedStatus');
3580 CRM_Core_BAO_FinancialTrxn::createDeferredTrxn(CRM_Utils_Array::value('line_item', $params), $params['contribution'], TRUE, 'changedStatus');
3581 }
3582 $updated = TRUE;
3583 }
3584
3585 // change Payment Instrument for a Completed contribution
3586 // first handle special case when contribution is changed from Pending to Completed status when initial payment
3587 // instrument is null and now new payment instrument is added along with the payment
3588 if (!$params['contribution']->payment_instrument_id) {
3589 $params['contribution']->find(TRUE);
3590 }
3591 $params['trxnParams']['payment_instrument_id'] = $params['contribution']->payment_instrument_id;
3592 $params['trxnParams']['check_number'] = $params['check_number'] ?? NULL;
3593
3594 if (self::isPaymentInstrumentChange($params, $pendingStatus)) {
3595 $updated = CRM_Core_BAO_FinancialTrxn::updateFinancialAccountsOnPaymentInstrumentChange($params);
3596 }
3597
3598 //if Change contribution amount
3599 $params['trxnParams']['fee_amount'] = $params['fee_amount'] ?? NULL;
3600 $params['trxnParams']['net_amount'] = $params['net_amount'] ?? NULL;
3601 $params['trxnParams']['total_amount'] = $trxnParams['total_amount'] = $params['total_amount'] = $totalAmount;
3602 $params['trxnParams']['trxn_id'] = $params['contribution']->trxn_id;
3603 if (isset($totalAmount) &&
3604 $totalAmount != $params['prevContribution']->total_amount
3605 ) {
3606 //Update Financial Records
3607 $params['trxnParams']['from_financial_account_id'] = NULL;
3608 self::updateFinancialAccounts($params, 'changedAmount');
3609 CRM_Core_BAO_FinancialTrxn::createDeferredTrxn(CRM_Utils_Array::value('line_item', $params), $params['contribution'], TRUE, 'changedAmount');
3610 $updated = TRUE;
3611 }
3612
3613 if (!$updated) {
3614 // Looks like we might have a data correction update.
3615 // This would be a case where a transaction id has been entered but it is incorrect &
3616 // the person goes back in & fixes it, as opposed to a new transaction.
3617 // Currently the UI doesn't support multiple refunds against a single transaction & we are only supporting
3618 // the data fix scenario.
3619 // CRM-17751.
3620 if (isset($params['refund_trxn_id'])) {
3621 $refundIDs = CRM_Core_BAO_FinancialTrxn::getRefundTransactionIDs($params['id']);
3622 if (!empty($refundIDs['financialTrxnId']) && $refundIDs['trxn_id'] != $params['refund_trxn_id']) {
3623 civicrm_api3('FinancialTrxn', 'create', [
3624 'id' => $refundIDs['financialTrxnId'],
3625 'trxn_id' => $params['refund_trxn_id'],
3626 ]);
3627 }
3628 }
3629 $cardType = $params['card_type_id'] ?? NULL;
3630 $panTruncation = $params['pan_truncation'] ?? NULL;
3631 CRM_Core_BAO_FinancialTrxn::updateCreditCardDetails($params['contribution']->id, $panTruncation, $cardType);
3632 }
3633 }
3634
3635 else {
3636 // records finanical trxn and entity financial trxn
3637 // also make it available as return value
3638 self::recordAlwaysAccountsReceivable($trxnParams, $params);
3639 $trxnParams['pan_truncation'] = $params['pan_truncation'] ?? NULL;
3640 $trxnParams['card_type_id'] = $params['card_type_id'] ?? NULL;
3641 $return = $financialTxn = CRM_Core_BAO_FinancialTrxn::create($trxnParams);
3642 $params['entity_id'] = $financialTxn->id;
3643 self::$_trxnIDs[] = $financialTxn->id;
3644 }
3645 }
3646 // record line items and financial items
3647 if (empty($params['skipLineItem'])) {
3648 CRM_Price_BAO_LineItem::processPriceSet($entityId, CRM_Utils_Array::value('line_item', $params), $params['contribution'], $entityTable, $isUpdate);
3649 }
3650
3651 // create batch entry if batch_id is passed and
3652 // ensure no batch entry is been made on 'Pending' or 'Failed' contribution, CRM-16611
3653 if (!empty($params['batch_id']) && !empty($financialTxn)) {
3654 $entityParams = [
3655 'batch_id' => $params['batch_id'],
3656 'entity_table' => 'civicrm_financial_trxn',
3657 'entity_id' => $financialTxn->id,
3658 ];
3659 CRM_Batch_BAO_EntityBatch::create($entityParams);
3660 }
3661
3662 // when a fee is charged
3663 if (!empty($params['fee_amount']) && (empty($params['prevContribution']) || $params['contribution']->fee_amount != $params['prevContribution']->fee_amount) && $skipRecords) {
3664 CRM_Core_BAO_FinancialTrxn::recordFees($params);
3665 }
3666
3667 if (!empty($params['prevContribution']) && $entityTable == 'civicrm_participant'
3668 && $params['prevContribution']->contribution_status_id != $params['contribution']->contribution_status_id
3669 ) {
3670 $eventID = CRM_Core_DAO::getFieldValue('CRM_Event_DAO_Participant', $entityId, 'event_id');
3671 $feeLevel[] = str_replace('\ 1', '', $params['prevContribution']->amount_level);
3672 CRM_Event_BAO_Participant::createDiscountTrxn($eventID, $params, $feeLevel);
3673 }
3674 unset($params['line_item']);
3675 self::$_trxnIDs = NULL;
3676 return $return;
3677 }
3678
3679 /**
3680 * Update all financial accounts entry.
3681 *
3682 * @param array $params
3683 * Contribution object, line item array and params for trxn.
3684 *
3685 * @param string $context
3686 * Update scenarios.
3687 *
3688 * @todo stop passing $params by reference. It is unclear the purpose of doing this &
3689 * adds unpredictability.
3690 *
3691 */
3692 public static function updateFinancialAccounts(&$params, $context = NULL) {
3693 $trxnID = NULL;
3694 $inputParams = $params;
3695 $isARefund = self::isContributionUpdateARefund($params['prevContribution']->contribution_status_id, $params['contribution']->contribution_status_id);
3696
3697 if ($context == 'changedAmount' || $context == 'changeFinancialType') {
3698 // @todo we should stop passing $params by reference - splitting this out would be a step towards that.
3699 $params['trxnParams']['total_amount'] = $params['trxnParams']['net_amount'] = ($params['total_amount'] - $params['prevContribution']->total_amount);
3700 }
3701
3702 $trxn = CRM_Core_BAO_FinancialTrxn::create($params['trxnParams']);
3703 // @todo we should stop passing $params by reference - splitting this out would be a step towards that.
3704 $params['entity_id'] = $trxn->id;
3705
3706 $itemParams['entity_table'] = 'civicrm_line_item';
3707 $trxnIds['id'] = $params['entity_id'];
3708 $previousLineItems = CRM_Price_BAO_LineItem::getLineItemsByContributionID($params['contribution']->id);
3709 foreach ($params['line_item'] as $fieldId => $fields) {
3710 $params = self::createFinancialItemsForLine($params, $context, $fields, $previousLineItems, $inputParams, $isARefund, $trxnIds, $fieldId);
3711 }
3712 }
3713
3714 /**
3715 * Is this contribution status a reversal.
3716 *
3717 * If so we would expect to record a negative value in the financial_trxn table.
3718 *
3719 * @param int $status_id
3720 *
3721 * @return bool
3722 */
3723 public static function isContributionStatusNegative($status_id) {
3724 $reversalStatuses = ['Cancelled', 'Chargeback', 'Refunded'];
3725 return in_array(CRM_Core_PseudoConstant::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', $status_id), $reversalStatuses, TRUE);
3726 }
3727
3728 /**
3729 * Check status validation on update of a contribution.
3730 *
3731 * @param array $values
3732 * Previous form values before submit.
3733 *
3734 * @param array $fields
3735 * The input form values.
3736 *
3737 * @param array $errors
3738 * List of errors.
3739 *
3740 * @return bool
3741 */
3742 public static function checkStatusValidation($values, &$fields, &$errors) {
3743 if (CRM_Utils_System::isNull($values) && !empty($fields['id'])) {
3744 $values['contribution_status_id'] = CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_Contribution', $fields['id'], 'contribution_status_id');
3745 if ($values['contribution_status_id'] == $fields['contribution_status_id']) {
3746 return FALSE;
3747 }
3748 }
3749 $contributionStatuses = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name');
3750 $checkStatus = [
3751 'Cancelled' => ['Completed', 'Refunded'],
3752 'Completed' => ['Cancelled', 'Refunded', 'Chargeback'],
3753 'Pending' => ['Cancelled', 'Completed', 'Failed', 'Partially paid'],
3754 'In Progress' => ['Cancelled', 'Completed', 'Failed'],
3755 'Refunded' => ['Cancelled', 'Completed'],
3756 'Partially paid' => ['Completed'],
3757 'Pending refund' => ['Completed', 'Refunded'],
3758 'Failed' => ['Pending'],
3759 ];
3760
3761 if (!in_array($contributionStatuses[$fields['contribution_status_id']],
3762 CRM_Utils_Array::value($contributionStatuses[$values['contribution_status_id']], $checkStatus, []))
3763 ) {
3764 $errors['contribution_status_id'] = ts("Cannot change contribution status from %1 to %2.", [
3765 1 => $contributionStatuses[$values['contribution_status_id']],
3766 2 => $contributionStatuses[$fields['contribution_status_id']],
3767 ]);
3768 }
3769 }
3770
3771 /**
3772 * Delete contribution of contact.
3773 *
3774 * @see https://issues.civicrm.org/jira/browse/CRM-12155
3775 *
3776 * @param int $contactId
3777 * Contact id.
3778 *
3779 */
3780 public static function deleteContactContribution($contactId) {
3781 $contribution = new CRM_Contribute_DAO_Contribution();
3782 $contribution->contact_id = $contactId;
3783 $contribution->find();
3784 while ($contribution->fetch()) {
3785 self::deleteContribution($contribution->id);
3786 }
3787 }
3788
3789 /**
3790 * Get options for a given contribution field.
3791 *
3792 * @param string $fieldName
3793 * @param string $context see CRM_Core_DAO::buildOptionsContext.
3794 * @param array $props whatever is known about this dao object.
3795 *
3796 * @return array|bool
3797 * @see CRM_Core_DAO::buildOptions
3798 *
3799 */
3800 public static function buildOptions($fieldName, $context = NULL, $props = []) {
3801 $className = __CLASS__;
3802 $params = [];
3803 if (isset($props['orderColumn'])) {
3804 $params['orderColumn'] = $props['orderColumn'];
3805 }
3806 switch ($fieldName) {
3807 // This field is not part of this object but the api supports it
3808 case 'payment_processor':
3809 $className = 'CRM_Contribute_BAO_ContributionPage';
3810 // Filter results by contribution page
3811 if (!empty($props['contribution_page_id'])) {
3812 $page = civicrm_api('contribution_page', 'getsingle', [
3813 'version' => 3,
3814 'id' => ($props['contribution_page_id']),
3815 ]);
3816 $types = (array) CRM_Utils_Array::value('payment_processor', $page, 0);
3817 $params['condition'] = 'id IN (' . implode(',', $types) . ')';
3818 }
3819 break;
3820
3821 // CRM-13981 This field was combined with soft_credits in 4.5 but the api still supports it
3822 case 'honor_type_id':
3823 $className = 'CRM_Contribute_BAO_ContributionSoft';
3824 $fieldName = 'soft_credit_type_id';
3825 $params['condition'] = "v.name IN ('in_honor_of','in_memory_of')";
3826 break;
3827
3828 case 'contribution_status_id':
3829 if ($context !== 'validate') {
3830 $params['condition'] = "v.name <> 'Template'";
3831 }
3832 }
3833 return CRM_Core_PseudoConstant::get($className, $fieldName, $params, $context);
3834 }
3835
3836 /**
3837 * Validate financial type.
3838 *
3839 * @see https://issues.civicrm.org/jira/browse/CRM-13231
3840 *
3841 * @param int $financialTypeId
3842 * Financial Type id.
3843 *
3844 * @param string $relationName
3845 *
3846 * @return array|bool
3847 */
3848 public static function validateFinancialType($financialTypeId, $relationName = 'Expense Account is') {
3849 $financialAccount = CRM_Contribute_PseudoConstant::getRelationalFinancialAccount($financialTypeId, $relationName);
3850
3851 if (!$financialAccount) {
3852 return CRM_Contribute_PseudoConstant::financialType($financialTypeId);
3853 }
3854 return FALSE;
3855 }
3856
3857 /**
3858 * @param int $targetCid
3859 * @param $activityType
3860 * @param string $title
3861 * @param int $contributionId
3862 * @param string $totalAmount
3863 * @param string $currency
3864 * @param string $trxn_date
3865 *
3866 * @throws \CRM_Core_Exception
3867 * @throws \CiviCRM_API3_Exception
3868 */
3869 public static function addActivityForPayment($targetCid, $activityType, $title, $contributionId, $totalAmount, $currency, $trxn_date) {
3870 $paymentAmount = CRM_Utils_Money::format($totalAmount, $currency);
3871 $subject = "{$paymentAmount} - Offline {$activityType} for {$title}";
3872 $date = CRM_Utils_Date::isoToMysql($trxn_date);
3873 // source record id would be the contribution id
3874 $srcRecId = $contributionId;
3875
3876 // activity params
3877 $activityParams = [
3878 'source_contact_id' => $targetCid,
3879 'source_record_id' => $srcRecId,
3880 'activity_type_id' => CRM_Core_PseudoConstant::getKey('CRM_Activity_BAO_Activity', 'activity_type_id', $activityType),
3881 'subject' => $subject,
3882 'activity_date_time' => $date,
3883 'status_id' => CRM_Core_PseudoConstant::getKey('CRM_Activity_BAO_Activity', 'activity_status_id', 'Completed'),
3884 'skipRecentView' => TRUE,
3885 ];
3886
3887 // create activity with target contacts
3888 $session = CRM_Core_Session::singleton();
3889 $id = $session->get('userID');
3890 if ($id) {
3891 $activityParams['source_contact_id'] = $id;
3892 $activityParams['target_contact_id'][] = $targetCid;
3893 }
3894 civicrm_api3('Activity', 'create', $activityParams);
3895 }
3896
3897 /**
3898 * Get list of payments displayed by Contribute_Page_PaymentInfo.
3899 *
3900 * @param int $id
3901 * @param string $component
3902 * @param bool $getTrxnInfo
3903 *
3904 * @return mixed
3905 *
3906 * @throws \CRM_Core_Exception
3907 * @throws \CiviCRM_API3_Exception
3908 */
3909 public static function getPaymentInfo($id, $component = 'contribution', $getTrxnInfo = FALSE) {
3910 // @todo deprecate passing in component - always call with contribution.
3911 if ($component == 'event') {
3912 $contributionId = CRM_Core_DAO::getFieldValue('CRM_Event_BAO_ParticipantPayment', $id, 'contribution_id', 'participant_id');
3913
3914 if (!$contributionId) {
3915 if ($primaryParticipantId = CRM_Core_DAO::getFieldValue('CRM_Event_BAO_Participant', $id, 'registered_by_id')) {
3916 $contributionId = CRM_Core_DAO::getFieldValue('CRM_Event_BAO_ParticipantPayment', $primaryParticipantId, 'contribution_id', 'participant_id');
3917 $id = $primaryParticipantId;
3918 }
3919 if (!$contributionId) {
3920 return;
3921 }
3922 }
3923 }
3924 elseif ($component == 'membership') {
3925 $contributionId = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_MembershipPayment', $id, 'contribution_id', 'membership_id');
3926 }
3927 else {
3928 $contributionId = $id;
3929 }
3930
3931 // The balance used to be calculated this way - we really want to remove this 'oldCalculation'
3932 // but need to unpick the whole trxn_id it's returning first.
3933 $oldCalculation = CRM_Core_BAO_FinancialTrxn::getBalanceTrxnAmt($contributionId);
3934 $baseTrxnId = !empty($oldCalculation['trxn_id']) ? $oldCalculation['trxn_id'] : NULL;
3935 if (!$baseTrxnId) {
3936 $baseTrxnId = CRM_Core_BAO_FinancialTrxn::getFinancialTrxnId($contributionId);
3937 $baseTrxnId = $baseTrxnId['financialTrxnId'];
3938 }
3939 $total = CRM_Price_BAO_LineItem::getLineTotal($contributionId);
3940
3941 $paymentBalance = CRM_Contribute_BAO_Contribution::getContributionBalance($contributionId, $total);
3942
3943 $contribution = civicrm_api3('Contribution', 'getsingle', [
3944 'id' => $contributionId,
3945 'return' => [
3946 'currency',
3947 'is_pay_later',
3948 'contribution_status_id',
3949 'financial_type_id',
3950 ],
3951 ]);
3952
3953 $info['payLater'] = $contribution['is_pay_later'];
3954 $info['contribution_status'] = $contribution['contribution_status'];
3955 $info['currency'] = $contribution['currency'];
3956
3957 $info['total'] = $total;
3958 $info['paid'] = $total - $paymentBalance;
3959 $info['balance'] = $paymentBalance;
3960 $info['id'] = $id;
3961 $info['component'] = $component;
3962 if ($getTrxnInfo && $baseTrxnId) {
3963 $info['transaction'] = self::getContributionTransactionInformation($contributionId, $contribution['financial_type_id']);
3964 }
3965
3966 $info['payment_links'] = self::getContributionPaymentLinks($id, $paymentBalance, $info['contribution_status']);
3967 return $info;
3968 }
3969
3970 /**
3971 * Get the outstanding balance on a contribution.
3972 *
3973 * @param int $contributionId
3974 * @param float $contributionTotal
3975 * Optional amount to override the saved amount paid (e.g if calculating what it WILL be).
3976 *
3977 * @return float
3978 * @throws \CRM_Core_Exception
3979 */
3980 public static function getContributionBalance($contributionId, $contributionTotal = NULL) {
3981 if ($contributionTotal === NULL) {
3982 $contributionTotal = CRM_Price_BAO_LineItem::getLineTotal($contributionId);
3983 }
3984
3985 return (float) CRM_Utils_Money::subtractCurrencies(
3986 $contributionTotal,
3987 CRM_Core_BAO_FinancialTrxn::getTotalPayments($contributionId, TRUE),
3988 CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_Contribution', $contributionId, 'currency')
3989 );
3990 }
3991
3992 /**
3993 * Check financial type validation on update of a contribution.
3994 *
3995 * @param int $financialTypeId
3996 * Value of latest Financial Type.
3997 *
3998 * @param int $contributionId
3999 * Contribution Id.
4000 *
4001 * @param array $errors
4002 * List of errors.
4003 *
4004 * @return void
4005 */
4006 public static function checkFinancialTypeChange($financialTypeId, $contributionId, &$errors) {
4007 if (!empty($financialTypeId)) {
4008 $oldFinancialTypeId = CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_Contribution', $contributionId, 'financial_type_id');
4009 if ($oldFinancialTypeId == $financialTypeId) {
4010 return;
4011 }
4012 }
4013 $sql = 'SELECT financial_type_id FROM civicrm_line_item WHERE contribution_id = %1 GROUP BY financial_type_id;';
4014 $params = [
4015 '1' => [$contributionId, 'Integer'],
4016 ];
4017 $result = CRM_Core_DAO::executeQuery($sql, $params);
4018 if ($result->N > 1) {
4019 $errors['financial_type_id'] = ts('One or more line items have a different financial type than the contribution. Editing the financial type is not yet supported in this situation.');
4020 }
4021 }
4022
4023 /**
4024 * Update related pledge payment payments.
4025 *
4026 * This function has been refactored out of the back office contribution form and may
4027 * still overlap with other functions.
4028 *
4029 * @param string $action
4030 * @param int $pledgePaymentID
4031 * @param int $contributionID
4032 * @param bool $adjustTotalAmount
4033 * @param float $total_amount
4034 * @param float $original_total_amount
4035 * @param int $contribution_status_id
4036 * @param int $original_contribution_status_id
4037 */
4038 public static function updateRelatedPledge(
4039 $action,
4040 $pledgePaymentID,
4041 $contributionID,
4042 $adjustTotalAmount,
4043 $total_amount,
4044 $original_total_amount,
4045 $contribution_status_id,
4046 $original_contribution_status_id
4047 ) {
4048 if (!$pledgePaymentID && $action & CRM_Core_Action::ADD && !$contributionID) {
4049 return;
4050 }
4051
4052 if ($pledgePaymentID) {
4053 //store contribution id in payment record.
4054 CRM_Core_DAO::setFieldValue('CRM_Pledge_DAO_PledgePayment', $pledgePaymentID, 'contribution_id', $contributionID);
4055 }
4056 else {
4057 $pledgePaymentID = CRM_Core_DAO::getFieldValue('CRM_Pledge_DAO_PledgePayment',
4058 $contributionID,
4059 'id',
4060 'contribution_id'
4061 );
4062 }
4063
4064 if (!$pledgePaymentID) {
4065 return;
4066 }
4067 $pledgeID = CRM_Core_DAO::getFieldValue('CRM_Pledge_DAO_PledgePayment',
4068 $contributionID,
4069 'pledge_id',
4070 'contribution_id'
4071 );
4072
4073 $updatePledgePaymentStatus = FALSE;
4074
4075 // If either the status or the amount has changed we update the pledge status.
4076 if ($action & CRM_Core_Action::ADD) {
4077 $updatePledgePaymentStatus = TRUE;
4078 }
4079 elseif ($action & CRM_Core_Action::UPDATE && (($original_contribution_status_id != $contribution_status_id) ||
4080 ($original_total_amount != $total_amount))
4081 ) {
4082 $updatePledgePaymentStatus = TRUE;
4083 }
4084
4085 if ($updatePledgePaymentStatus) {
4086 CRM_Pledge_BAO_PledgePayment::updatePledgePaymentStatus($pledgeID,
4087 [$pledgePaymentID],
4088 $contribution_status_id,
4089 NULL,
4090 $total_amount,
4091 $adjustTotalAmount
4092 );
4093 }
4094 }
4095
4096 /**
4097 * Is there only one line item attached to the contribution.
4098 *
4099 * @param int $id
4100 * Contribution ID.
4101 *
4102 * @return bool
4103 * @throws \CiviCRM_API3_Exception
4104 */
4105 public static function isSingleLineItem($id) {
4106 $lineItemCount = civicrm_api3('LineItem', 'getcount', ['contribution_id' => $id]);
4107 return ($lineItemCount == 1);
4108 }
4109
4110 /**
4111 * Complete an order.
4112 *
4113 * Do not call this directly - use the contribution.completetransaction api as this function is being refactored.
4114 *
4115 * Currently overloaded to complete a transaction & repeat a transaction - fix!
4116 *
4117 * Moving it out of the BaseIPN class is just the first step.
4118 *
4119 * @param array $input
4120 * @param int $recurringContributionID
4121 * @param int|null $contributionID
4122 * @param bool $isPostPaymentCreate
4123 * Is this being called from the payment.create api. If so the api has taken care of financial entities.
4124 * Note that our goal is that this would only ever be called from payment.create and never handle financials (only
4125 * transitioning related elements).
4126 *
4127 * @return array
4128 * @throws \API_Exception
4129 * @throws \CRM_Core_Exception
4130 * @throws \CiviCRM_API3_Exception
4131 */
4132 public static function completeOrder($input, $recurringContributionID, $contributionID, $isPostPaymentCreate = FALSE) {
4133 $transaction = new CRM_Core_Transaction();
4134
4135 $inputContributionWhiteList = [
4136 'fee_amount',
4137 'net_amount',
4138 'trxn_id',
4139 'check_number',
4140 'payment_instrument_id',
4141 'is_test',
4142 'campaign_id',
4143 'receive_date',
4144 'receipt_date',
4145 'contribution_status_id',
4146 'card_type_id',
4147 'pan_truncation',
4148 'financial_type_id',
4149 ];
4150
4151 $paymentProcessorId = $input['payment_processor_id'] ?? NULL;
4152
4153 $completedContributionStatusID = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
4154
4155 $contributionParams = array_merge([
4156 'contribution_status_id' => $completedContributionStatusID,
4157 ], array_intersect_key($input, array_fill_keys($inputContributionWhiteList, 1)
4158 ));
4159
4160 $contributionParams['payment_processor'] = $paymentProcessorId;
4161
4162 if (empty($contributionParams['payment_instrument_id']) && $paymentProcessorId) {
4163 $contributionParams['payment_instrument_id'] = PaymentProcessor::get(FALSE)->addWhere('id', '=', $paymentProcessorId)->addSelect('payment_instrument_id')->execute()->first()['payment_instrument_id'];
4164 }
4165
4166 if ($recurringContributionID) {
4167 $contributionParams['contribution_recur_id'] = $recurringContributionID;
4168 }
4169
4170 if (!$contributionID) {
4171 $contributionResult = self::repeatTransaction($input, $contributionParams);
4172 $contributionID = $contributionResult['id'];
4173 }
4174
4175 if ($contributionParams['contribution_status_id'] === $completedContributionStatusID) {
4176 self::updateMembershipBasedOnCompletionOfContribution(
4177 $contributionID,
4178 $input['trxn_date'] ?? date('YmdHis')
4179 );
4180 }
4181
4182 $participantPayments = civicrm_api3('ParticipantPayment', 'get', ['contribution_id' => $contributionID, 'return' => 'participant_id', 'sequential' => 1])['values'];
4183 if (!empty($participantPayments) && empty($input['IAmAHorribleNastyBeyondExcusableHackInTheCRMEventFORMTaskClassThatNeedsToBERemoved'])) {
4184 foreach ($participantPayments as $participantPayment) {
4185 $participantParams['id'] = $participantPayment['participant_id'];
4186 $participantParams['status_id'] = 'Registered';
4187 civicrm_api3('Participant', 'create', $participantParams);
4188 }
4189 }
4190
4191 $contributionParams['id'] = $contributionID;
4192 $contributionParams['is_post_payment_create'] = $isPostPaymentCreate;
4193
4194 if (empty($contributionResult)) {
4195 $contributionResult = civicrm_api3('Contribution', 'create', $contributionParams);
4196 }
4197
4198 $transaction->commit();
4199 \Civi::log()->info("Contribution {$contributionParams['id']} updated successfully");
4200
4201 $contributionSoft = ContributionSoft::get(FALSE)
4202 ->addWhere('contribution_id', '=', $contributionID)
4203 ->addWhere('pcp_id', '>', 0)
4204 ->addSelect('*')
4205 ->execute()->first();
4206 if (!empty($contributionSoft)) {
4207 CRM_Contribute_BAO_ContributionSoft::pcpNotifyOwner($contributionID, $contributionSoft);
4208 }
4209 // @todo - check if Contribution::create does this, test, remove.
4210 CRM_Contribute_BAO_ContributionRecur::updateRecurLinkedPledge($contributionID, $recurringContributionID,
4211 $contributionParams['contribution_status_id'], $input['amount']);
4212
4213 if (self::isEmailReceipt($input, $contributionID, $recurringContributionID)) {
4214 civicrm_api3('Contribution', 'sendconfirmation', [
4215 'id' => $contributionID,
4216 'payment_processor_id' => $paymentProcessorId,
4217 ]);
4218 \Civi::log()->info("Contribution {$contributionParams['id']} Receipt sent");
4219 }
4220
4221 return $contributionResult;
4222 }
4223
4224 /**
4225 * Send receipt from contribution.
4226 *
4227 * Do not call this directly - it is being refactored. use contribution.sendmessage api call.
4228 *
4229 * Note that the compose message part has been moved to contribution
4230 * In general LoadObjects is called first to get the objects but the composeMessageArray function now calls it.
4231 *
4232 * @param array $input
4233 * Incoming data from Payment processor.
4234 * @param array $ids
4235 * Related object IDs.
4236 * @param int $contributionID
4237 * @param bool $returnMessageText
4238 * Should text be returned instead of sent. This.
4239 * is because the function is also used to generate pdfs
4240 *
4241 * @return array
4242 * @throws \CRM_Core_Exception
4243 * @throws \CiviCRM_API3_Exception
4244 * @throws \Exception
4245 */
4246 public static function sendMail($input, $ids, $contributionID, $returnMessageText = FALSE) {
4247 $values = [];
4248 $contribution = new CRM_Contribute_BAO_Contribution();
4249 $contribution->id = $contributionID;
4250 if (!$contribution->find(TRUE)) {
4251 throw new CRM_Core_Exception('Contribution does not exist');
4252 }
4253 // set receipt from e-mail and name in value
4254 if (!$returnMessageText) {
4255 [$values['receipt_from_name'], $values['receipt_from_email']] = self::generateFromEmailAndName($input, $contribution);
4256 }
4257 $values['contribution_status'] = CRM_Core_PseudoConstant::getLabel('CRM_Contribute_BAO_Contribution', 'contribution_status_id', $contribution->contribution_status_id);
4258 $return = $contribution->composeMessageArray($input, $ids, $values, $returnMessageText);
4259 if ((!isset($input['receipt_update']) || $input['receipt_update']) && empty($contribution->receipt_date)) {
4260 civicrm_api3('Contribution', 'create', [
4261 'receipt_date' => 'now',
4262 'id' => $contribution->id,
4263 ]);
4264 }
4265 return $return;
4266 }
4267
4268 /**
4269 * Generate From email and from name in an array values
4270 *
4271 * @param array $input
4272 * @param \CRM_Contribute_BAO_Contribution $contribution
4273 *
4274 * @return array
4275 */
4276 public static function generateFromEmailAndName($input, $contribution) {
4277 // Use input value if supplied.
4278 if (!empty($input['receipt_from_email'])) {
4279 return [
4280 CRM_Utils_Array::value('receipt_from_name', $input, ''),
4281 $input['receipt_from_email'],
4282 ];
4283 }
4284 // if we are still empty see if we can use anything from a contribution page.
4285 $pageValues = [];
4286 if (!empty($contribution->contribution_page_id)) {
4287 $pageValues = civicrm_api3('ContributionPage', 'getsingle', ['id' => $contribution->contribution_page_id]);
4288 }
4289 // if we are still empty see if we can use anything from a contribution page.
4290 if (!empty($pageValues['receipt_from_email'])) {
4291 return [
4292 CRM_Utils_Array::value('receipt_from_name', $pageValues),
4293 $pageValues['receipt_from_email'],
4294 ];
4295 }
4296 // If we are still empty fall back to the domain or logged in user information.
4297 return CRM_Core_BAO_Domain::getDefaultReceiptFrom();
4298 }
4299
4300 /**
4301 * Load related memberships.
4302 *
4303 * @param array $ids
4304 *
4305 * @return array $ids
4306 *
4307 * @throws Exception
4308 * @deprecated
4309 *
4310 * Note that in theory it should be possible to retrieve these from the line_item table
4311 * with the membership_payment table being deprecated. Attempting to do this here causes tests to fail
4312 * as it seems the api is not correctly linking the line items when the contribution is created in the flow
4313 * where the contribution is created in the API, followed by the membership (using the api) followed by the membership
4314 * payment. The membership payment BAO does have code to address this but it doesn't appear to be working.
4315 *
4316 * I don't know if it never worked or broke as a result of https://issues.civicrm.org/jira/browse/CRM-14918.
4317 *
4318 */
4319 public function loadRelatedMembershipObjects($ids = []) {
4320 $query = "
4321 SELECT membership_id
4322 FROM civicrm_membership_payment
4323 WHERE contribution_id = %1 ";
4324 $params = [1 => [$this->id, 'Integer']];
4325 $ids['membership'] = (array) CRM_Utils_Array::value('membership', $ids, []);
4326
4327 $dao = CRM_Core_DAO::executeQuery($query, $params);
4328 while ($dao->fetch()) {
4329 if ($dao->membership_id && !in_array($dao->membership_id, $ids['membership'])) {
4330 $ids['membership'][$dao->membership_id] = $dao->membership_id;
4331 }
4332 }
4333
4334 if (array_key_exists('membership', $ids) && is_array($ids['membership'])) {
4335 foreach ($ids['membership'] as $id) {
4336 if (!empty($id)) {
4337 $membership = new CRM_Member_BAO_Membership();
4338 $membership->id = $id;
4339 if (!$membership->find(TRUE)) {
4340 throw new Exception("Could not find membership record: $id");
4341 }
4342 $membership->join_date = CRM_Utils_Date::isoToMysql($membership->join_date);
4343 $membership->start_date = CRM_Utils_Date::isoToMysql($membership->start_date);
4344 $membership->end_date = CRM_Utils_Date::isoToMysql($membership->end_date);
4345 $this->_relatedObjects['membership'][$membership->id . '_' . $membership->membership_type_id] = $membership;
4346
4347 }
4348 }
4349 }
4350 return $ids;
4351 }
4352
4353 /**
4354 * Function use to store line item proportionally in in entity financial trxn table
4355 *
4356 * @param array $trxnParams
4357 *
4358 * @param int $trxnId
4359 *
4360 * @param float $contributionTotalAmount
4361 *
4362 * @throws \CiviCRM_API3_Exception
4363 */
4364 public static function assignProportionalLineItems($trxnParams, $trxnId, $contributionTotalAmount) {
4365 $lineItems = CRM_Price_BAO_LineItem::getLineItemsByContributionID($trxnParams['contribution_id']);
4366 if (!empty($lineItems)) {
4367 // get financial item
4368 [$ftIds, $taxItems] = self::getLastFinancialItemIds($trxnParams['contribution_id']);
4369 $entityParams = [
4370 'contribution_total_amount' => $contributionTotalAmount,
4371 'trxn_total_amount' => $trxnParams['total_amount'],
4372 'trxn_id' => $trxnId,
4373 ];
4374 self::createProportionalFinancialEntries($entityParams, $lineItems, $ftIds, $taxItems);
4375 }
4376 }
4377
4378 /**
4379 * ContributionPage values were being imposed onto values.
4380 *
4381 * I have made this explicit and removed the couple (is_recur, is_pay_later) we
4382 * REALLY didn't want superimposed. The rest are left there in their overkill out
4383 * of cautiousness.
4384 *
4385 * The rationale for making this explicit is that it was a case of carefully set values being
4386 * seemingly randonly overwritten without much care. In general I think array randomly setting
4387 * variables en mass is risky.
4388 *
4389 * @param array $values
4390 *
4391 * @return array
4392 */
4393 protected function addContributionPageValuesToValuesHeavyHandedly(&$values) {
4394 $contributionPageValues = [];
4395 CRM_Contribute_BAO_ContributionPage::setValues(
4396 $this->contribution_page_id,
4397 $contributionPageValues
4398 );
4399 $valuesToCopy = [
4400 // These are the values that I believe to be useful.
4401 'id',
4402 'title',
4403 'pay_later_receipt',
4404 'pay_later_text',
4405 'receipt_from_email',
4406 'receipt_from_name',
4407 'receipt_text',
4408 'custom_pre_id',
4409 'custom_post_id',
4410 'honoree_profile_id',
4411 'onbehalf_profile_id',
4412 'honor_block_is_active',
4413 // Kinda might be - but would be on the contribution...
4414 'campaign_id',
4415 'currency',
4416 // Included for 'fear of regression' but can't justify any use for these....
4417 'intro_text',
4418 'payment_processor',
4419 'financial_type_id',
4420 'amount_block_is_active',
4421 'bcc_receipt',
4422 'cc_receipt',
4423 'created_date',
4424 'created_id',
4425 'default_amount_id',
4426 'end_date',
4427 'footer_text',
4428 'goal_amount',
4429 'initial_amount_help_text',
4430 'initial_amount_label',
4431 'intro_text',
4432 'is_allow_other_amount',
4433 'is_billing_required',
4434 'is_confirm_enabled',
4435 'is_credit_card_only',
4436 'is_monetary',
4437 'is_partial_payment',
4438 'is_recur_installments',
4439 'is_recur_interval',
4440 'is_share',
4441 'max_amount',
4442 'min_amount',
4443 'min_initial_amount',
4444 'recur_frequency_unit',
4445 'start_date',
4446 'thankyou_footer',
4447 'thankyou_text',
4448 'thankyou_title',
4449
4450 ];
4451 foreach ($valuesToCopy as $valueToCopy) {
4452 if (isset($contributionPageValues[$valueToCopy])) {
4453 if ($valueToCopy === 'title') {
4454 $values[$valueToCopy] = CRM_Contribute_BAO_Contribution_Utils::getContributionPageTitle($this->contribution_page_id);
4455 }
4456 else {
4457 $values[$valueToCopy] = $contributionPageValues[$valueToCopy];
4458 }
4459 }
4460 }
4461 return $values;
4462 }
4463
4464 /**
4465 * Get values of CiviContribute Settings
4466 * and check if its enabled or not.
4467 * Note: The CiviContribute settings are stored as single entry in civicrm_setting
4468 * in serialized form. Usually this should be stored as flat settings for each form fields
4469 * as per CiviCRM standards. Since this would take more effort to change the current behaviour of CiviContribute
4470 * settings we will live with an inconsistency because it's too hard to change for now.
4471 * https://github.com/civicrm/civicrm-core/pull/8562#issuecomment-227874245
4472 *
4473 *
4474 * @param string $name
4475 *
4476 * @return string
4477 *
4478 */
4479 public static function checkContributeSettings($name) {
4480 $contributeSettings = Civi::settings()->get('contribution_invoice_settings');
4481 return $contributeSettings[$name] ?? NULL;
4482 }
4483
4484 /**
4485 * Get the contribution as it is in the database before being updated.
4486 *
4487 * @param int $contributionID
4488 *
4489 * @return \CRM_Contribute_BAO_Contribution|null
4490 */
4491 private static function getOriginalContribution($contributionID) {
4492 return self::getValues(['id' => $contributionID]);
4493 }
4494
4495 /**
4496 * Get the amount for the financial item row.
4497 *
4498 * Helper function to start to break down recordFinancialTransactions for readability.
4499 *
4500 * The logic is more historical than .. logical. Paths other than the deprecated one are tested.
4501 *
4502 * Codewise, several somewhat disimmilar things have been squished into recordFinancialAccounts
4503 * for historical reasons. Going forwards we can hope to add tests & improve readibility
4504 * of that function
4505 *
4506 * @param array $params
4507 * Params as passed to contribution.create
4508 *
4509 * @param string $context
4510 * changeFinancialType| changedAmount
4511 * @param array $lineItemDetails
4512 * Line items.
4513 * @param bool $isARefund
4514 * Is this a refund / negative transaction.
4515 * @param int $previousLineItemTotal
4516 *
4517 * @return float
4518 * @todo move recordFinancialAccounts & helper functions to their own class?
4519 *
4520 */
4521 protected static function getFinancialItemAmountFromParams($params, $context, $lineItemDetails, $isARefund, $previousLineItemTotal) {
4522 if ($context == 'changedAmount') {
4523 $lineTotal = $lineItemDetails['line_total'];
4524 if ($lineTotal != $previousLineItemTotal) {
4525 $lineTotal -= $previousLineItemTotal;
4526 }
4527 return $lineTotal;
4528 }
4529 elseif ($context == 'changeFinancialType') {
4530 return -$lineItemDetails['line_total'];
4531 }
4532 elseif ($context == 'changedStatus') {
4533 $cancelledTaxAmount = 0;
4534 if ($isARefund) {
4535 $cancelledTaxAmount = CRM_Utils_Array::value('tax_amount', $lineItemDetails, '0.00');
4536 }
4537 return self::getMultiplier($params['contribution']->contribution_status_id, $context) * ((float) $lineItemDetails['line_total'] + (float) $cancelledTaxAmount);
4538 }
4539 elseif ($context === NULL) {
4540 // erm, yes because? but, hey, it's tested.
4541 return $lineItemDetails['line_total'];
4542 }
4543 else {
4544 return self::getMultiplier($params['contribution']->contribution_status_id, $context) * ((float) $lineItemDetails['line_total']);
4545 }
4546 }
4547
4548 /**
4549 * Get the multiplier for adjusting rows.
4550 *
4551 * If we are dealing with a refund or cancellation then it will be a negative
4552 * amount to reflect the negative transaction.
4553 *
4554 * If we are changing Financial Type it will be a negative amount to
4555 * adjust down the old type.
4556 *
4557 * @param int $contribution_status_id
4558 * @param string $context
4559 *
4560 * @return int
4561 */
4562 protected static function getMultiplier($contribution_status_id, $context) {
4563 if ($context == 'changeFinancialType' || self::isContributionStatusNegative($contribution_status_id)) {
4564 return -1;
4565 }
4566 return 1;
4567 }
4568
4569 /**
4570 * Does this transaction reflect a payment instrument change.
4571 *
4572 * @param array $params
4573 * @param array $pendingStatuses
4574 *
4575 * @return bool
4576 */
4577 protected static function isPaymentInstrumentChange(&$params, $pendingStatuses) {
4578 $contributionStatus = CRM_Core_PseudoConstant::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', $params['contribution']->contribution_status_id);
4579
4580 if (array_key_exists('payment_instrument_id', $params)) {
4581 if (CRM_Utils_System::isNull($params['prevContribution']->payment_instrument_id) &&
4582 !CRM_Utils_System::isNull($params['payment_instrument_id'])
4583 ) {
4584 //check if status is changed from Pending to Completed
4585 // do not update payment instrument changes for Pending to Completed
4586 if (!($contributionStatus == 'Completed' &&
4587 in_array($params['prevContribution']->contribution_status_id, $pendingStatuses))
4588 ) {
4589 return TRUE;
4590 }
4591 }
4592 elseif ((!CRM_Utils_System::isNull($params['payment_instrument_id']) &&
4593 !CRM_Utils_System::isNull($params['prevContribution']->payment_instrument_id)) &&
4594 $params['payment_instrument_id'] != $params['prevContribution']->payment_instrument_id
4595 ) {
4596 return TRUE;
4597 }
4598 elseif (!CRM_Utils_System::isNull($params['contribution']->check_number) &&
4599 $params['contribution']->check_number != $params['prevContribution']->check_number
4600 ) {
4601 // another special case when check number is changed, create new financial records
4602 // create financial trxn with negative amount
4603 return TRUE;
4604 }
4605 }
4606 return FALSE;
4607 }
4608
4609 /**
4610 * Update the memberships associated with a contribution if it has been completed.
4611 *
4612 * Note that the way in which $memberships are loaded as objects is pretty messy & I think we could just
4613 * load them in this function. Code clean up would compensate for any minor performance implication.
4614 *
4615 * @param int $contributionID
4616 * @param string $changeDate
4617 *
4618 * @throws \CRM_Core_Exception
4619 * @throws \CiviCRM_API3_Exception
4620 */
4621 public static function updateMembershipBasedOnCompletionOfContribution($contributionID, $changeDate) {
4622 $memberships = self::getRelatedMemberships((int) $contributionID);
4623 foreach ($memberships as $membership) {
4624 $membershipParams = [
4625 'id' => $membership['id'],
4626 'contact_id' => $membership['contact_id'],
4627 'is_test' => $membership['is_test'],
4628 'membership_type_id' => $membership['membership_type_id'],
4629 'membership_activity_status' => 'Completed',
4630 ];
4631
4632 $currentMembership = CRM_Member_BAO_Membership::getContactMembership($membershipParams['contact_id'],
4633 $membershipParams['membership_type_id'],
4634 $membershipParams['is_test'],
4635 $membershipParams['id']
4636 );
4637
4638 // CRM-8141 update the membership type with the value recorded in log when membership created/renewed
4639 // this picks up membership type changes during renewals
4640 // @todo this is almost certainly an obsolete sql call, the pre-change
4641 // membership is accessible via $this->_relatedObjects
4642 $sql = "
4643 SELECT membership_type_id
4644 FROM civicrm_membership_log
4645 WHERE membership_id={$membershipParams['id']}
4646 ORDER BY id DESC
4647 LIMIT 1;";
4648 $dao = CRM_Core_DAO::executeQuery($sql);
4649 if ($dao->fetch()) {
4650 if (!empty($dao->membership_type_id)) {
4651 $membershipParams['membership_type_id'] = $dao->membership_type_id;
4652 }
4653 }
4654 if (empty($membership['end_date']) || (int) $membership['status_id'] !== CRM_Core_PseudoConstant::getKey('CRM_Member_BAO_Membership', 'status_id', 'Pending')) {
4655 // Passing num_terms to the api triggers date calculations, but for pending memberships these may be already calculated.
4656 // sigh - they should be consistent but removing the end date check causes test failures & maybe UI too?
4657 // The api assumes num_terms is a special sauce for 'is_renewal' so we need to not pass it when updating a pending to completed.
4658 // ... except testCompleteTransactionMembershipPriceSetTwoTerms hits this line so the above is obviously not true....
4659 // @todo once apiv4 ships with core switch to that & find sanity.
4660 $membershipParams['num_terms'] = self::getNumTermsByContributionAndMembershipType(
4661 $membershipParams['membership_type_id'],
4662 $contributionID
4663 );
4664 }
4665 // @todo remove all this stuff in favour of letting the api call further down handle in
4666 // (it is a duplication of what the api does).
4667 $dates = array_fill_keys([
4668 'join_date',
4669 'start_date',
4670 'end_date',
4671 ], NULL);
4672 if ($currentMembership) {
4673 /*
4674 * Fixed FOR CRM-4433
4675 * In BAO/Membership.php(renewMembership function), we skip the extend membership date and status
4676 * when Contribution mode is notify and membership is for renewal )
4677 */
4678 // Test cover for this is in testRepeattransactionRenewMembershipOldMembership
4679 // Be afraid.
4680 CRM_Member_BAO_Membership::fixMembershipStatusBeforeRenew($currentMembership, $changeDate);
4681
4682 // @todo - we should pass membership_type_id instead of null here but not
4683 // adding as not sure of testing
4684 $dates = CRM_Member_BAO_MembershipType::getRenewalDatesForMembershipType($membershipParams['id'],
4685 $changeDate, NULL, $membershipParams['num_terms']
4686 );
4687 $dates['join_date'] = $currentMembership['join_date'];
4688 }
4689 if ('Pending' === CRM_Core_PseudoConstant::getName('CRM_Member_BAO_Membership', 'status_id', $membership['status_id'])) {
4690 $membershipParams['skipStatusCal'] = '';
4691 }
4692 else {
4693 //get the status for membership.
4694 $calcStatus = CRM_Member_BAO_MembershipStatus::getMembershipStatusByDate($dates['start_date'],
4695 $dates['end_date'],
4696 $dates['join_date'],
4697 'now',
4698 TRUE,
4699 $membershipParams['membership_type_id'],
4700 $membershipParams
4701 );
4702
4703 unset($dates['end_date']);
4704 $membershipParams['status_id'] = CRM_Utils_Array::value('id', $calcStatus, 'New');
4705 }
4706 //we might be renewing membership,
4707 //so make status override false.
4708 $membershipParams['is_override'] = FALSE;
4709 $membershipParams['status_override_end_date'] = 'null';
4710 civicrm_api3('Membership', 'create', $membershipParams);
4711 }
4712 }
4713
4714 /**
4715 * Get payment links as they relate to a contribution.
4716 *
4717 * If a payment can be made then include a payment link & if a refund is appropriate
4718 * then a refund link.
4719 *
4720 * @param int $id
4721 * @param float $balance
4722 * @param string $contributionStatus
4723 *
4724 * @return array
4725 * $actionLinks Links array containing:
4726 * -url
4727 * -title
4728 */
4729 protected static function getContributionPaymentLinks($id, $balance, $contributionStatus) {
4730 if ($contributionStatus === 'Failed' || !CRM_Core_Permission::check('edit contributions')) {
4731 // In general the balance is the best way to determine if a payment can be added or not,
4732 // but not for Failed contributions, where we don't accept additional payments at the moment.
4733 // (in some cases the contribution is 'Pending' and only the payment is failed. In those we
4734 // do accept more payments agains them.
4735 return [];
4736 }
4737 $actionLinks = [];
4738 $actionLinks[] = [
4739 'url' => CRM_Utils_System::url('civicrm/payment', [
4740 'action' => 'add',
4741 'reset' => 1,
4742 'id' => $id,
4743 'is_refund' => 0,
4744 ]),
4745 'title' => ts('Record Payment'),
4746 ];
4747
4748 if (CRM_Core_Config::isEnabledBackOfficeCreditCardPayments()) {
4749 $actionLinks[] = [
4750 'url' => CRM_Utils_System::url('civicrm/payment', [
4751 'action' => 'add',
4752 'reset' => 1,
4753 'is_refund' => 0,
4754 'id' => $id,
4755 'mode' => 'live',
4756 ]),
4757 'title' => ts('Submit Credit Card payment'),
4758 ];
4759 }
4760 $actionLinks[] = [
4761 'url' => CRM_Utils_System::url('civicrm/payment', [
4762 'action' => 'add',
4763 'reset' => 1,
4764 'id' => $id,
4765 'is_refund' => 1,
4766 ]),
4767 'title' => ts('Record Refund'),
4768 ];
4769 return $actionLinks;
4770 }
4771
4772 /**
4773 * Get a query to determine the amount donated by the contact/s in the current financial year.
4774 *
4775 * @param array $contactIDs
4776 *
4777 * @return string
4778 */
4779 public static function getAnnualQuery($contactIDs) {
4780 $contactIDs = implode(',', $contactIDs);
4781 $config = CRM_Core_Config::singleton();
4782 $currentMonth = date('m');
4783 $currentDay = date('d');
4784 if (
4785 (int) $config->fiscalYearStart['M'] > $currentMonth ||
4786 (
4787 (int) $config->fiscalYearStart['M'] == $currentMonth &&
4788 (int) $config->fiscalYearStart['d'] > $currentDay
4789 )
4790 ) {
4791 $year = date('Y') - 1;
4792 }
4793 else {
4794 $year = date('Y');
4795 }
4796 $nextYear = $year + 1;
4797
4798 if ($config->fiscalYearStart) {
4799 $newFiscalYearStart = $config->fiscalYearStart;
4800 if ($newFiscalYearStart['M'] < 10) {
4801 // This is just a clumsy way of adding padding.
4802 // @todo next round look for a nicer way.
4803 $newFiscalYearStart['M'] = '0' . $newFiscalYearStart['M'];
4804 }
4805 if ($newFiscalYearStart['d'] < 10) {
4806 // This is just a clumsy way of adding padding.
4807 // @todo next round look for a nicer way.
4808 $newFiscalYearStart['d'] = '0' . $newFiscalYearStart['d'];
4809 }
4810 $config->fiscalYearStart = $newFiscalYearStart;
4811 $monthDay = $config->fiscalYearStart['M'] . $config->fiscalYearStart['d'];
4812 }
4813 else {
4814 // First of January.
4815 $monthDay = '0101';
4816 }
4817 $startDate = "$year$monthDay";
4818 $endDate = "$nextYear$monthDay";
4819
4820 $whereClauses = [
4821 'contact_id' => 'IN (' . $contactIDs . ')',
4822 'is_test' => ' = 0',
4823 'receive_date' => ['>=' . $startDate, '< ' . $endDate],
4824 ];
4825 $havingClause = 'contribution_status_id = ' . (int) CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
4826 CRM_Financial_BAO_FinancialType::addACLClausesToWhereClauses($whereClauses);
4827
4828 $clauses = [];
4829 foreach ($whereClauses as $key => $clause) {
4830 $clauses[] = 'b.' . $key . " " . implode(' AND b.' . $key, (array) $clause);
4831 }
4832 $whereClauseString = implode(' AND ', $clauses);
4833
4834 // See https://github.com/civicrm/civicrm-core/pull/13512 for discussion of how
4835 // this group by + having on contribution_status_id improves performance
4836 $query = "
4837 SELECT COUNT(*) as count,
4838 SUM(total_amount) as amount,
4839 AVG(total_amount) as average,
4840 currency
4841 FROM civicrm_contribution b
4842 WHERE " . $whereClauseString . "
4843 GROUP BY currency, contribution_status_id
4844 HAVING $havingClause
4845 ";
4846 return $query;
4847 }
4848
4849 /**
4850 * Assign Test Value.
4851 *
4852 * @param string $fieldName
4853 * @param array $fieldDef
4854 * @param int $counter
4855 */
4856 protected function assignTestValue($fieldName, &$fieldDef, $counter) {
4857 if ($fieldName === 'tax_amount') {
4858 $this->{$fieldName} = "0.00";
4859 }
4860 elseif ($fieldName === 'net_amount') {
4861 $this->{$fieldName} = '2.00';
4862 }
4863 elseif ($fieldName === 'total_amount') {
4864 $this->{$fieldName} = "3.00";
4865 }
4866 elseif ($fieldName === 'fee_amount') {
4867 $this->{$fieldName} = '1.00';
4868 }
4869 else {
4870 parent::assignTestValues($fieldName, $fieldDef, $counter);
4871 }
4872 }
4873
4874 /**
4875 * Check if contribution has participant/membership payment.
4876 *
4877 * @param int $contributionId
4878 * Contribution ID
4879 *
4880 * @return bool
4881 */
4882 public static function allowUpdateRevenueRecognitionDate($contributionId) {
4883 // get line item for contribution
4884 $lineItems = CRM_Price_BAO_LineItem::getLineItemsByContributionID($contributionId);
4885 // check if line item is for membership or participant
4886 foreach ($lineItems as $items) {
4887 if ($items['entity_table'] == 'civicrm_participant') {
4888 $flag = FALSE;
4889 break;
4890 }
4891 elseif ($items['entity_table'] == 'civicrm_membership') {
4892 $flag = FALSE;
4893 }
4894 else {
4895 $flag = TRUE;
4896 break;
4897 }
4898 }
4899 return $flag;
4900 }
4901
4902 /**
4903 * Create Accounts Receivable financial trxn entry for Completed Contribution.
4904 *
4905 * @param array $trxnParams
4906 * Financial trxn params
4907 * @param array $contributionParams
4908 * Contribution Params
4909 *
4910 * @return null
4911 */
4912 public static function recordAlwaysAccountsReceivable(&$trxnParams, $contributionParams) {
4913 if (!Civi::settings()->get('always_post_to_accounts_receivable')) {
4914 return NULL;
4915 }
4916 $statusId = $contributionParams['contribution']->contribution_status_id;
4917 $contributionStatuses = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name');
4918 $contributionStatus = empty($statusId) ? NULL : $contributionStatuses[$statusId];
4919 $previousContributionStatus = empty($contributionParams['prevContribution']) ? NULL : $contributionStatuses[$contributionParams['prevContribution']->contribution_status_id];
4920 // Return if contribution status is not completed.
4921 if (!($contributionStatus == 'Completed' && (empty($previousContributionStatus)
4922 || (!empty($previousContributionStatus) && $previousContributionStatus == 'Pending'
4923 && $contributionParams['prevContribution']->is_pay_later == 0
4924 )))
4925 ) {
4926 return NULL;
4927 }
4928
4929 $params = $trxnParams;
4930 $financialTypeID = !empty($contributionParams['financial_type_id']) ? $contributionParams['financial_type_id'] : $contributionParams['prevContribution']->financial_type_id;
4931 $arAccountId = CRM_Contribute_PseudoConstant::getRelationalFinancialAccount($financialTypeID, 'Accounts Receivable Account is');
4932 $params['to_financial_account_id'] = $arAccountId;
4933 $params['status_id'] = array_search('Pending', $contributionStatuses);
4934 $params['is_payment'] = FALSE;
4935 $trxn = CRM_Core_BAO_FinancialTrxn::create($params);
4936 self::$_trxnIDs[] = $trxn->id;
4937 $trxnParams['from_financial_account_id'] = $params['to_financial_account_id'];
4938 }
4939
4940 /**
4941 * Calculate financial item amount when contribution is updated.
4942 *
4943 * @param array $params
4944 * contribution params
4945 * @param array $amountParams
4946 *
4947 * @param string $context
4948 *
4949 * @return float
4950 */
4951 public static function calculateFinancialItemAmount($params, $amountParams, $context) {
4952 if (!empty($params['is_quick_config'])) {
4953 $amount = $amountParams['item_amount'];
4954 if (!$amount) {
4955 $amount = $params['total_amount'];
4956 if ($context === NULL) {
4957 $amount -= CRM_Utils_Array::value('tax_amount', $params, 0);
4958 }
4959 }
4960 }
4961 else {
4962 $amount = $amountParams['line_total'];
4963 if ($context == 'changedAmount') {
4964 $amount -= $amountParams['previous_line_total'];
4965 }
4966 $amount *= $amountParams['diff'];
4967 }
4968 return $amount;
4969 }
4970
4971 /**
4972 * Retrieve Sales Tax Financial Accounts.
4973 *
4974 *
4975 * @return array
4976 *
4977 */
4978 public static function getSalesTaxFinancialAccounts() {
4979 $query = "SELECT cfa.id FROM civicrm_entity_financial_account ce
4980 INNER JOIN civicrm_financial_account cfa ON ce.financial_account_id = cfa.id
4981 WHERE `entity_table` = 'civicrm_financial_type' AND cfa.is_tax = 1 AND ce.account_relationship = %1 GROUP BY cfa.id";
4982 $accountRel = key(CRM_Core_PseudoConstant::accountOptionValues('account_relationship', NULL, " AND v.name LIKE 'Sales Tax Account is' "));
4983 $queryParams = [1 => [$accountRel, 'Integer']];
4984 $dao = CRM_Core_DAO::executeQuery($query, $queryParams);
4985 $financialAccount = [];
4986 while ($dao->fetch()) {
4987 $financialAccount[(int) $dao->id] = (int) $dao->id;
4988 }
4989 return $financialAccount;
4990 }
4991
4992 /**
4993 * Create tax entry in civicrm_entity_financial_trxn table.
4994 *
4995 * @param array $entityParams
4996 *
4997 * @param array $eftParams
4998 *
4999 * @throws \CiviCRM_API3_Exception
5000 */
5001 public static function createProportionalEntry($entityParams, $eftParams) {
5002 $paid = 0;
5003 if ($entityParams['contribution_total_amount'] != 0) {
5004 $paid = $entityParams['line_item_amount'] * ($entityParams['trxn_total_amount'] / $entityParams['contribution_total_amount']);
5005 }
5006 // Record Entity Financial Trxn; CRM-20145
5007 $eftParams['amount'] = $paid;
5008 civicrm_api3('EntityFinancialTrxn', 'create', $eftParams);
5009 }
5010
5011 /**
5012 * Create array of last financial item id's.
5013 *
5014 * @param int $contributionId
5015 *
5016 * @return array
5017 */
5018 public static function getLastFinancialItemIds($contributionId) {
5019 $sql = "SELECT fi.id, li.price_field_value_id, li.tax_amount, fi.financial_account_id
5020 FROM civicrm_financial_item fi
5021 INNER JOIN civicrm_line_item li ON li.id = fi.entity_id and fi.entity_table = 'civicrm_line_item'
5022 WHERE li.contribution_id = %1";
5023 $dao = CRM_Core_DAO::executeQuery($sql, [
5024 1 => [
5025 $contributionId,
5026 'Integer',
5027 ],
5028 ]);
5029 $ftIds = $taxItems = [];
5030 $salesTaxFinancialAccount = self::getSalesTaxFinancialAccounts();
5031 while ($dao->fetch()) {
5032 /* if sales tax item*/
5033 if (in_array($dao->financial_account_id, $salesTaxFinancialAccount)) {
5034 $taxItems[$dao->price_field_value_id] = [
5035 'financial_item_id' => $dao->id,
5036 'amount' => $dao->tax_amount,
5037 ];
5038 }
5039 else {
5040 $ftIds[$dao->price_field_value_id] = $dao->id;
5041 }
5042 }
5043 return [$ftIds, $taxItems];
5044 }
5045
5046 /**
5047 * Create proportional entries in civicrm_entity_financial_trxn.
5048 *
5049 * @param array $entityParams
5050 *
5051 * @param array $lineItems
5052 *
5053 * @param array $ftIds
5054 *
5055 * @param array $taxItems
5056 *
5057 * @throws \CiviCRM_API3_Exception
5058 */
5059 public static function createProportionalFinancialEntries($entityParams, $lineItems, $ftIds, $taxItems) {
5060 $eftParams = [
5061 'entity_table' => 'civicrm_financial_item',
5062 'financial_trxn_id' => $entityParams['trxn_id'],
5063 ];
5064 foreach ($lineItems as $key => $value) {
5065 if ($value['qty'] == 0) {
5066 continue;
5067 }
5068 $eftParams['entity_id'] = $ftIds[$value['price_field_value_id']];
5069 $entityParams['line_item_amount'] = $value['line_total'];
5070 self::createProportionalEntry($entityParams, $eftParams);
5071 if (array_key_exists($value['price_field_value_id'], $taxItems)) {
5072 $entityParams['line_item_amount'] = $taxItems[$value['price_field_value_id']]['amount'];
5073 $eftParams['entity_id'] = $taxItems[$value['price_field_value_id']]['financial_item_id'];
5074 self::createProportionalEntry($entityParams, $eftParams);
5075 }
5076 }
5077 }
5078
5079 /**
5080 * Load entities related to the contribution into $this->_relatedObjects.
5081 *
5082 * @param array $ids
5083 *
5084 * @throws \CRM_Core_Exception
5085 */
5086 protected function loadRelatedEntitiesByID($ids) {
5087 $entities = [
5088 'contact' => 'CRM_Contact_BAO_Contact',
5089 'contributionRecur' => 'CRM_Contribute_BAO_ContributionRecur',
5090 'contributionType' => 'CRM_Financial_BAO_FinancialType',
5091 'financialType' => 'CRM_Financial_BAO_FinancialType',
5092 'contributionPage' => 'CRM_Contribute_BAO_ContributionPage',
5093 ];
5094 foreach ($entities as $entity => $bao) {
5095 if (!empty($ids[$entity])) {
5096 $this->_relatedObjects[$entity] = new $bao();
5097 $this->_relatedObjects[$entity]->id = $ids[$entity];
5098 if (!$this->_relatedObjects[$entity]->find(TRUE)) {
5099 throw new CRM_Core_Exception($entity . ' could not be loaded');
5100 }
5101 }
5102 }
5103 }
5104
5105 /**
5106 * Function to replace contribution tokens.
5107 *
5108 * @param array $contributionIds
5109 *
5110 * @param string $subject
5111 *
5112 * @param array $subjectToken
5113 *
5114 * @param string $text
5115 *
5116 * @param string $html
5117 *
5118 * @param array $messageToken
5119 *
5120 * @param bool $escapeSmarty
5121 *
5122 * @return array
5123 * @throws \CiviCRM_API3_Exception
5124 */
5125 public static function replaceContributionTokens(
5126 $contributionIds,
5127 $subject,
5128 $subjectToken,
5129 $text,
5130 $html,
5131 $messageToken,
5132 $escapeSmarty
5133 ) {
5134 if (empty($contributionIds)) {
5135 return [];
5136 }
5137 $contributionDetails = [];
5138 foreach ($contributionIds as $id) {
5139 $result = self::getContributionTokenValues($id, $messageToken);
5140 $contributionDetails[$result['values'][$result['id']]['contact_id']]['subject'] = CRM_Utils_Token::replaceContributionTokens($subject, $result, FALSE, $subjectToken, FALSE, $escapeSmarty);
5141 $contributionDetails[$result['values'][$result['id']]['contact_id']]['text'] = CRM_Utils_Token::replaceContributionTokens($text, $result, FALSE, $messageToken, FALSE, $escapeSmarty);
5142 $contributionDetails[$result['values'][$result['id']]['contact_id']]['html'] = CRM_Utils_Token::replaceContributionTokens($html, $result, FALSE, $messageToken, FALSE, $escapeSmarty);
5143 }
5144 return $contributionDetails;
5145 }
5146
5147 /**
5148 * Get the contribution fields for $id and display labels where
5149 * appropriate (if the token is present).
5150 *
5151 * @param int $id
5152 * @param array $messageToken
5153 *
5154 * @return array
5155 * @throws \CRM_Core_Exception
5156 */
5157 public static function getContributionTokenValues($id, $messageToken) {
5158 if (empty($id)) {
5159 return [];
5160 }
5161 $result = civicrm_api3('Contribution', 'get', ['id' => $id]);
5162 // lab.c.o mail#46 - show labels, not values, for custom fields with option values.
5163 foreach ($result['values'][$id] as $fieldName => $fieldValue) {
5164 if (strpos($fieldName, 'custom_') === 0 && array_search($fieldName, $messageToken['contribution']) !== FALSE) {
5165 $result['values'][$id][$fieldName] = CRM_Core_BAO_CustomField::displayValue($result['values'][$id][$fieldName], $fieldName);
5166 }
5167 }
5168 if (!empty($messageToken['contribution'])) {
5169 $processor = new CRM_Contribute_Tokens();
5170 $pseudoFields = array_keys($processor->getPseudoTokens());
5171 foreach ($pseudoFields as $pseudoField) {
5172 $split = explode(':', $pseudoField);
5173 $result['values'][$id][$pseudoField] = $processor->getPseudoValue($split[0], $split[1], $result['values'][$id][$split[0]] ?? '');
5174 }
5175 }
5176 return $result;
5177 }
5178
5179 /**
5180 * Get invoice_number for contribution.
5181 *
5182 * @param int $contributionID
5183 *
5184 * @return string
5185 */
5186 public static function getInvoiceNumber($contributionID) {
5187 if ($invoicePrefix = self::checkContributeSettings('invoice_prefix')) {
5188 return $invoicePrefix . $contributionID;
5189 }
5190
5191 return NULL;
5192 }
5193
5194 /**
5195 * Load the values needed for the event message.
5196 *
5197 * @param int $eventID
5198 * @param int $participantID
5199 * @param int|null $contributionID
5200 *
5201 * @return array
5202 * @throws \CRM_Core_Exception
5203 */
5204 protected function loadEventMessageTemplateParams(int $eventID, int $participantID, $contributionID): array {
5205
5206 $eventParams = [
5207 'id' => $eventID,
5208 ];
5209 $values = ['event' => []];
5210
5211 CRM_Event_BAO_Event::retrieve($eventParams, $values['event']);
5212 // add custom fields for event
5213 $eventGroupTree = CRM_Core_BAO_CustomGroup::getTree('Event', NULL, $eventID);
5214
5215 $eventCustomGroup = [];
5216 foreach ($eventGroupTree as $key => $group) {
5217 if ($key === 'info') {
5218 continue;
5219 }
5220
5221 foreach ($group['fields'] as $k => $customField) {
5222 $groupLabel = $group['title'];
5223 if (!empty($customField['customValue'])) {
5224 foreach ($customField['customValue'] as $customFieldValues) {
5225 $eventCustomGroup[$groupLabel][$customField['label']] = $customFieldValues['data'] ?? NULL;
5226 }
5227 }
5228 }
5229 }
5230 $values['event']['customGroup'] = $eventCustomGroup;
5231
5232 //get participant details
5233 $participantParams = [
5234 'id' => $participantID,
5235 ];
5236
5237 $values['participant'] = [];
5238
5239 CRM_Event_BAO_Participant::getValues($participantParams, $values['participant'], $participantIds);
5240 // add custom fields for event
5241 $participantGroupTree = CRM_Core_BAO_CustomGroup::getTree('Participant', NULL, $participantID);
5242 $participantCustomGroup = [];
5243 foreach ($participantGroupTree as $key => $group) {
5244 if ($key === 'info') {
5245 continue;
5246 }
5247
5248 foreach ($group['fields'] as $k => $customField) {
5249 $groupLabel = $group['title'];
5250 if (!empty($customField['customValue'])) {
5251 foreach ($customField['customValue'] as $customFieldValues) {
5252 $participantCustomGroup[$groupLabel][$customField['label']] = $customFieldValues['data'] ?? NULL;
5253 }
5254 }
5255 }
5256 }
5257 $values['participant']['customGroup'] = $participantCustomGroup;
5258
5259 //get location details
5260 $locationParams = [
5261 'entity_id' => $eventID,
5262 'entity_table' => 'civicrm_event',
5263 ];
5264 $values['location'] = CRM_Core_BAO_Location::getValues($locationParams);
5265
5266 $ufJoinParams = [
5267 'entity_table' => 'civicrm_event',
5268 'entity_id' => $eventID,
5269 'module' => 'CiviEvent',
5270 ];
5271
5272 [$custom_pre_id, $custom_post_ids] = CRM_Core_BAO_UFJoin::getUFGroupIds($ufJoinParams);
5273
5274 $values['custom_pre_id'] = $custom_pre_id;
5275 $values['custom_post_id'] = $custom_post_ids;
5276
5277 // set lineItem for event contribution
5278 if ($contributionID) {
5279 $participantIds = CRM_Event_BAO_Participant::getParticipantIds($contributionID);
5280 if (!empty($participantIds)) {
5281 foreach ($participantIds as $pIDs) {
5282 $lineItem = CRM_Price_BAO_LineItem::getLineItems($pIDs);
5283 if (!CRM_Utils_System::isNull($lineItem)) {
5284 $values['lineItem'][] = $lineItem;
5285 }
5286 }
5287 }
5288 }
5289 return $values;
5290 }
5291
5292 /**
5293 * Get the activity source and target contacts linked to a contribution
5294 *
5295 * @param $activityId
5296 *
5297 * @return array
5298 */
5299 private static function getActivitySourceAndTarget($activityId): array {
5300 $activityContactQuery = ActivityContact::get(FALSE)->setWhere([
5301 ['activity_id', '=', $activityId],
5302 ['record_type_id:name', 'IN', ['Activity Source', 'Activity Targets']],
5303 ])->execute();
5304
5305 $sourceContactKey = CRM_Core_PseudoConstant::getKey('CRM_Activity_BAO_ActivityContact', 'record_type_id', 'Activity Source');
5306 $targetContactKey = CRM_Core_PseudoConstant::getKey('CRM_Activity_BAO_ActivityContact', 'record_type_id', 'Activity Targets');
5307
5308 $sourceContactId = NULL;
5309 $targetContactId = NULL;
5310
5311 for ($i = 0; $i < $activityContactQuery->count(); $i++) {
5312 $record = $activityContactQuery->itemAt($i);
5313
5314 if ($record['record_type_id'] === $sourceContactKey) {
5315 $sourceContactId = $record['contact_id'];
5316 }
5317
5318 if ($record['record_type_id'] === $targetContactKey) {
5319 $targetContactId = $record['contact_id'];
5320 }
5321 }
5322
5323 return [$sourceContactId, $targetContactId];
5324 }
5325
5326 }