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