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