Merge pull request #15588 from seamuslee001/ref_group_contact_cache_rebuild
[civicrm-core.git] / CRM / Financial / BAO / Payment.php
CommitLineData
0c9b306a 1<?php
2/*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2019 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26 */
27
28/**
29 *
30 * @package CRM
31 * @copyright CiviCRM LLC (c) 2004-2019
32 */
33
34/**
35 * This class contains payment related functions.
36 */
37class CRM_Financial_BAO_Payment {
38
39 /**
40 * Function to process additional payment for partial and refund contributions.
41 *
42 * This function is called via API payment.create function. All forms that add payments
43 * should use this.
44 *
45 * @param array $params
46 * - contribution_id
47 * - total_amount
48 * - line_item
49 *
50 * @return \CRM_Financial_DAO_FinancialTrxn
51 *
52 * @throws \API_Exception
53 * @throws \CRM_Core_Exception
6c434a1d 54 * @throws \CiviCRM_API3_Exception
0c9b306a 55 */
56 public static function create($params) {
57 $contribution = civicrm_api3('Contribution', 'getsingle', ['id' => $params['contribution_id']]);
58 $contributionStatus = CRM_Contribute_PseudoConstant::contributionStatus($contribution['contribution_status_id'], 'name');
7585038a 59 $isPaymentCompletesContribution = self::isPaymentCompletesContribution($params['contribution_id'], $params['total_amount']);
f5a468db 60 $lineItems = self::getPayableLineItems($params);
7585038a 61
a494d7a3 62 $whiteList = ['check_number', 'payment_processor_id', 'fee_amount', 'total_amount', 'contribution_id', 'net_amount', 'card_type_id', 'pan_truncation', 'trxn_result_code', 'payment_instrument_id', 'trxn_id'];
63 $paymentTrxnParams = array_intersect_key($params, array_fill_keys($whiteList, 1));
64 $paymentTrxnParams['is_payment'] = 1;
65 if (!empty($params['payment_processor'])) {
66 // I can't find evidence this is passed in - I was gonna just remove it but decided to deprecate as I see getToFinancialAccount
67 // also anticipates it.
68 CRM_Core_Error::deprecatedFunctionWarning('passing payment_processor is deprecated - use payment_processor_id');
69 $paymentTrxnParams['payment_processor_id'] = $params['payment_processor'];
70 }
71 if (isset($paymentTrxnParams['payment_processor_id']) && empty($paymentTrxnParams['payment_processor_id'])) {
72 // Don't pass 0 - ie the Pay Later processor as it is a pseudo-processor.
73 unset($paymentTrxnParams['payment_processor_id']);
74 }
75 if (empty($paymentTrxnParams['payment_instrument_id'])) {
76 if (!empty($params['payment_processor_id'])) {
77 $paymentTrxnParams['payment_instrument_id'] = civicrm_api3('PaymentProcessor', 'getvalue', ['return' => 'payment_instrument_id', 'id' => $paymentTrxnParams['payment_processor_id']]);
78 }
79 else {
80 // Fall back on the payment instrument already used - should we deprecate this?
81 $paymentTrxnParams['payment_instrument_id'] = $contribution['payment_instrument_id'];
82 }
83 }
84 if (empty($paymentTrxnParams['trxn_id']) && !empty($paymentTrxnParams['contribution_trxn_id'])) {
85 CRM_Core_Error::deprecatedFunctionWarning('contribution_trxn_id is deprecated - use trxn_id');
86 $paymentTrxnParams['trxn_id'] = $paymentTrxnParams['contribution_trxn_id'];
87 }
88
362f37fc 89 if ($params['total_amount'] > 0) {
f55dedff 90 $paymentTrxnParams['to_financial_account_id'] = CRM_Contribute_BAO_Contribution::getToFinancialAccount($contribution, $params);
91 $paymentTrxnParams['from_financial_account_id'] = CRM_Financial_BAO_FinancialAccount::getFinancialAccountForFinancialTypeByRelationship($contribution['financial_type_id'], 'Accounts Receivable Account is');
f55dedff 92 $paymentTrxnParams['trxn_date'] = CRM_Utils_Array::value('trxn_date', $params, CRM_Utils_Array::value('contribution_receive_date', $params, date('YmdHis')));
f55dedff 93 $paymentTrxnParams['currency'] = $contribution['currency'];
f55dedff 94 $paymentTrxnParams['status_id'] = CRM_Core_PseudoConstant::getKey('CRM_Core_BAO_FinancialTrxn', 'status_id', 'Completed');
74e4c3c2 95
f55dedff 96 $trxn = CRM_Core_BAO_FinancialTrxn::create($paymentTrxnParams);
88a20030 97
f5a468db 98 // @todo - this is pretty much the same as the next section now - just
99 // the getFinancialItem retrieval needs consolidating - one gets by line item, the other by
100 // price field id. One must be right the other wrong - which is which?
de6c59ca 101 if (!empty($params['line_item']) && !empty($trxn)) {
db77f7d3 102 foreach ($params['line_item'] as $values) {
103 foreach ($values as $id => $amount) {
104 $p = ['id' => $id];
105 $check = CRM_Price_BAO_LineItem::retrieve($p, $defaults);
106 if (empty($check)) {
107 throw new API_Exception('Please specify a valid Line Item.');
0c9b306a 108 }
db77f7d3 109 // get financial item
110 $sql = "SELECT fi.id
111 FROM civicrm_financial_item fi
112 INNER JOIN civicrm_line_item li ON li.id = fi.entity_id and fi.entity_table = 'civicrm_line_item'
113 WHERE li.contribution_id = %1 AND li.id = %2";
114 $sqlParams = [
115 1 => [$params['contribution_id'], 'Integer'],
116 2 => [$id, 'Integer'],
117 ];
118 $fid = CRM_Core_DAO::singleValueQuery($sql, $sqlParams);
119 // Record Entity Financial Trxn
120 $eftParams = [
121 'entity_table' => 'civicrm_financial_item',
122 'financial_trxn_id' => $trxn->id,
123 'amount' => $amount,
124 'entity_id' => $fid,
125 ];
126 civicrm_api3('EntityFinancialTrxn', 'create', $eftParams);
0c9b306a 127 }
128 }
db77f7d3 129 }
130 elseif (!empty($trxn)) {
6eb592cc 131 if (!empty($lineItems)) {
f5a468db 132 // @todo the difference between this and the above loop is that we are linking the
133 // financial trxn in the above loop to the last financial item that relates to the line item but
134 // here to the last financial item that relates to the price field value.
135 // Likely this difference is because one of them handles updated text fields correctly and the other
136 // doesn't - but which is which?
137 // Note that getPayableLineItems is probably the right place to determine this - see the todo there.
6eb592cc 138 list($ftIds, $taxItems) = CRM_Contribute_BAO_Contribution::getLastFinancialItemIds($params['contribution_id']);
f5a468db 139
4e52c6df 140 foreach ($lineItems as $key => $value) {
f5a468db 141 if ($value['qty'] == 0 || $value['allocation'] === (float) 0) {
4e52c6df 142 continue;
143 }
f5a468db 144 $eftParams = [
145 'entity_table' => 'civicrm_financial_item',
146 'financial_trxn_id' => $trxn->id,
147 'entity_id' => $ftIds[$value['price_field_value_id']],
148 'amount' => $value['allocation'],
149 ];
150
151 civicrm_api3('EntityFinancialTrxn', 'create', $eftParams);
152
4e52c6df 153 if (array_key_exists($value['price_field_value_id'], $taxItems)) {
f5a468db 154 // @todo - this is expected to be broken - it should be fixed to
155 // a) have the getPayableLineItems add the amount to allocate for tax
156 // b) call EntityFinancialTrxn directly - per above.
157 // - see https://github.com/civicrm/civicrm-core/pull/14763
158 $entityParams = [
159 'contribution_total_amount' => $contribution['total_amount'],
160 'trxn_total_amount' => $params['total_amount'],
161 'trxn_id' => $trxn->id,
162 'line_item_amount' => $taxItems[$value['price_field_value_id']]['amount'],
163 ];
4e52c6df 164 $eftParams['entity_id'] = $taxItems[$value['price_field_value_id']]['financial_item_id'];
165 CRM_Contribute_BAO_Contribution::createProportionalEntry($entityParams, $eftParams);
166 }
167 }
6eb592cc 168 }
0c9b306a 169 }
170 }
2561fc11 171 elseif ($params['total_amount'] < 0) {
172 $trxn = self::recordRefundPayment($params['contribution_id'], $params, FALSE);
df29ccff 173 if (!empty($params['cancelled_payment_id'])) {
174 // Do a direct reversal of any entity_financial_trxn records being cancelled.
175 $entityFinancialTrxns = civicrm_api3('EntityFinancialTrxn', 'get', [
176 'entity_table' => 'civicrm_financial_item',
177 'options' => ['limit' => 0],
178 'financial_trxn_id.id' => $params['cancelled_payment_id'],
179 ])['values'];
180 foreach ($entityFinancialTrxns as $entityFinancialTrxn) {
181 civicrm_api3('EntityFinancialTrxn', 'create', [
182 'entity_table' => 'civicrm_financial_item',
183 'entity_id' => $entityFinancialTrxn['entity_id'],
184 'amount' => -$entityFinancialTrxn['amount'],
185 'financial_trxn_id' => $trxn->id,
186 ]);
187 }
188 }
2561fc11 189 }
db77f7d3 190
5f5ed53a 191 if ($isPaymentCompletesContribution) {
6c434a1d 192 if ($contributionStatus == 'Pending refund') {
193 // Ideally we could still call completetransaction as non-payment related actions should
194 // be outside this class. However, for now we just update the contribution here.
195 // Unit test cover in CRM_Event_BAO_AdditionalPaymentTest::testTransactionInfo.
196 civicrm_api3('Contribution', 'create',
197 [
198 'id' => $contribution['id'],
199 'contribution_status_id' => 'Completed',
200 ]
201 );
202 }
203 else {
362f37fc 204 civicrm_api3('Contribution', 'completetransaction', [
205 'id' => $contribution['id'],
206 'is_post_payment_create' => TRUE,
bd981689 207 'is_email_receipt' => $params['is_send_contribution_notification'],
362f37fc 208 ]);
6c434a1d 209 // Get the trxn
210 $trxnId = CRM_Core_BAO_FinancialTrxn::getFinancialTrxnId($contribution['id'], 'DESC');
211 $ftParams = ['id' => $trxnId['financialTrxnId']];
f0da41da 212 $trxn = CRM_Core_BAO_FinancialTrxn::retrieve($ftParams);
6c434a1d 213 }
5f5ed53a 214 }
215 elseif ($contributionStatus === 'Pending') {
16b0233c 216 self::updateContributionStatus($contribution['id'], 'Partially Paid');
5f5ed53a 217 }
8d54448e 218 CRM_Contribute_BAO_Contribution::recordPaymentActivity($params['contribution_id'], CRM_Utils_Array::value('participant_id', $params), $params['total_amount'], $trxn->currency, $trxn->trxn_date);
0c9b306a 219 return $trxn;
220 }
221
a79d2ec2 222 /**
223 * Send an email confirming a payment that has been received.
224 *
225 * @param array $params
226 *
227 * @return array
f3e6da5e 228 *
229 * @throws \CiviCRM_API3_Exception
a79d2ec2 230 */
231 public static function sendConfirmation($params) {
232
233 $entities = self::loadRelatedEntities($params['id']);
be2fb01f 234 $sendTemplateParams = [
a79d2ec2 235 'groupName' => 'msg_tpl_workflow_contribution',
236 'valueName' => 'payment_or_refund_notification',
237 'PDFFilename' => ts('notification') . '.pdf',
238 'contactId' => $entities['contact']['id'],
239 'toName' => $entities['contact']['display_name'],
240 'toEmail' => $entities['contact']['email'],
241 'tplParams' => self::getConfirmationTemplateParameters($entities),
be2fb01f 242 ];
44a2f017 243 if (!empty($params['from']) && !empty($params['check_permissions'])) {
244 // Filter from against permitted emails.
245 $validEmails = self::getValidFromEmailsForPayment($entities['event']['id'] ?? NULL);
246 if (!isset($validEmails[$params['from']])) {
247 // Ignore unpermitted parameter.
248 unset($params['from']);
249 }
250 }
251 $sendTemplateParams['from'] = $params['from'] ?? key(CRM_Core_BAO_Email::domainEmails());
a79d2ec2 252 return CRM_Core_BAO_MessageTemplate::sendTemplate($sendTemplateParams);
253 }
254
44a2f017 255 /**
256 * Get valid from emails for payment.
257 *
258 * @param int $eventID
259 *
260 * @return array
261 */
262 public static function getValidFromEmailsForPayment($eventID = NULL) {
263 if ($eventID) {
264 $emails = CRM_Event_BAO_Event::getFromEmailIds($eventID);
265 }
266 else {
267 $emails['from_email_id'] = CRM_Core_BAO_Email::getFromEmail();
268 }
269 return $emails['from_email_id'];
270 }
271
a79d2ec2 272 /**
273 * Load entities related to the current payment id.
274 *
275 * This gives us all the data we need to send an email confirmation but avoiding
276 * getting anything not tested for the confirmations. We retrieve the 'full' event as
277 * it has been traditionally assigned in full.
278 *
279 * @param int $id
280 *
281 * @return array
282 * - contact = ['id' => x, 'display_name' => y, 'email' => z]
283 * - event = [.... full event details......]
284 * - contribution = ['id' => x],
285 * - payment = [payment info + payment summary info]
fdbe8ee1 286 * @throws \CiviCRM_API3_Exception
a79d2ec2 287 */
288 protected static function loadRelatedEntities($id) {
289 $entities = [];
290 $contributionID = (int) civicrm_api3('EntityFinancialTrxn', 'getvalue', [
291 'financial_trxn_id' => $id,
292 'entity_table' => 'civicrm_contribution',
293 'return' => 'entity_id',
294 ]);
295 $entities['contribution'] = ['id' => $contributionID];
296 $entities['payment'] = array_merge(civicrm_api3('FinancialTrxn', 'getsingle', ['id' => $id]),
297 CRM_Contribute_BAO_Contribution::getPaymentInfo($contributionID)
298 );
299
300 $contactID = self::getPaymentContactID($contributionID);
301 list($displayName, $email) = CRM_Contact_BAO_Contact_Location::getEmailDetails($contactID);
302 $entities['contact'] = ['id' => $contactID, 'display_name' => $displayName, 'email' => $email];
1e477c5b 303 $contact = civicrm_api3('Contact', 'getsingle', ['id' => $contactID, 'return' => 'email_greeting']);
304 $entities['contact']['email_greeting'] = $contact['email_greeting_display'];
a79d2ec2 305
306 $participantRecords = civicrm_api3('ParticipantPayment', 'get', [
307 'contribution_id' => $contributionID,
308 'api.Participant.get' => ['return' => 'event_id'],
309 'sequential' => 1,
310 ])['values'];
311 if (!empty($participantRecords)) {
312 $entities['event'] = civicrm_api3('Event', 'getsingle', ['id' => $participantRecords[0]['api.Participant.get']['values'][0]['event_id']]);
0fad34a0 313 if (!empty($entities['event']['is_show_location'])) {
314 $locationParams = [
315 'entity_id' => $entities['event']['id'],
316 'entity_table' => 'civicrm_event',
317 ];
318 $entities['location'] = CRM_Core_BAO_Location::getValues($locationParams, TRUE);
319 }
a79d2ec2 320 }
321
322 return $entities;
323 }
324
325 /**
326 * @param int $contributionID
327 *
328 * @return int
329 */
330 public static function getPaymentContactID($contributionID) {
331 $contribution = civicrm_api3('Contribution', 'getsingle', [
332 'id' => $contributionID ,
333 'return' => ['contact_id'],
334 ]);
335 return (int) $contribution['contact_id'];
336 }
7b966967 337
a79d2ec2 338 /**
339 * @param array $entities
340 * Related entities as an array keyed by the various entities.
341 *
342 * @return array
343 * Values required for the notification
344 * - contact_id
345 * - template_variables
346 * - event (DAO of event if relevant)
347 */
348 public static function getConfirmationTemplateParameters($entities) {
349 $templateVariables = [
350 'contactDisplayName' => $entities['contact']['display_name'],
1e477c5b 351 'emailGreeting' => $entities['contact']['email_greeting'],
a79d2ec2 352 'totalAmount' => $entities['payment']['total'],
353 'amountOwed' => $entities['payment']['balance'],
b5a442ed 354 'totalPaid' => $entities['payment']['paid'],
a79d2ec2 355 'paymentAmount' => $entities['payment']['total_amount'],
434546ac 356 'checkNumber' => CRM_Utils_Array::value('check_number', $entities['payment']),
357 'receive_date' => $entities['payment']['trxn_date'],
358 'paidBy' => CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_FinancialTrxn', 'payment_instrument_id', $entities['payment']['payment_instrument_id']),
0fad34a0 359 'isShowLocation' => (!empty($entities['event']) ? $entities['event']['is_show_location'] : FALSE),
360 'location' => CRM_Utils_Array::value('location', $entities),
361 'event' => CRM_Utils_Array::value('event', $entities),
362 'component' => (!empty($entities['event']) ? 'event' : 'contribution'),
b5a442ed 363 'isRefund' => $entities['payment']['total_amount'] < 0,
364 'isAmountzero' => $entities['payment']['total_amount'] === 0,
365 'refundAmount' => ($entities['payment']['total_amount'] < 0 ? $entities['payment']['total_amount'] : NULL),
00ef9b01 366 'paymentsComplete' => ($entities['payment']['balance'] == 0),
a79d2ec2 367 ];
a79d2ec2 368
369 return self::filterUntestedTemplateVariables($templateVariables);
370 }
371
372 /**
373 * Filter out any untested variables.
374 *
375 * This just serves to highlight if any variables are added without a unit test also being added.
376 *
377 * (if hit then add a unit test for the param & add to this array).
378 *
379 * @param array $params
380 *
381 * @return array
382 */
383 public static function filterUntestedTemplateVariables($params) {
384 $testedTemplateVariables = [
385 'contactDisplayName',
386 'totalAmount',
387 'amountOwed',
388 'paymentAmount',
389 'event',
390 'component',
434546ac 391 'checkNumber',
392 'receive_date',
393 'paidBy',
0fad34a0 394 'isShowLocation',
395 'location',
b5a442ed 396 'isRefund',
397 'isAmountzero',
398 'refundAmount',
399 'totalPaid',
00ef9b01 400 'paymentsComplete',
7b966967 401 'emailGreeting',
a79d2ec2 402 ];
565920da 403 // These are assigned by the payment form - they still 'get through' from the
404 // form for now without being in here but we should ideally load
405 // and assign. Note we should update the tpl to use {if $billingName}
406 // and ditch contributeMode - although it might need to be deprecated rather than removed.
a79d2ec2 407 $todoParams = [
a79d2ec2 408 'contributeMode',
a79d2ec2 409 'billingName',
410 'address',
411 'credit_card_type',
412 'credit_card_number',
413 'credit_card_exp_date',
a79d2ec2 414 ];
415 $filteredParams = [];
416 foreach ($testedTemplateVariables as $templateVariable) {
417 // This will cause an a-notice if any are NOT set - by design. Ensuring
418 // they are set prevents leakage.
419 $filteredParams[$templateVariable] = $params[$templateVariable];
420 }
421 return $filteredParams;
422 }
423
d3726f09 424 /**
425 * @param $contributionId
426 * @param $trxnData
427 * @param $updateStatus
428 * - deprecate this param
429 *
d3726f09 430 * @return CRM_Financial_DAO_FinancialTrxn
df29ccff 431 * @throws \CiviCRM_API3_Exception
d3726f09 432 */
957655fe 433 protected static function recordRefundPayment($contributionId, $trxnData, $updateStatus) {
9a2dce8d 434 list($contributionDAO, $params) = self::getContributionAndParamsInFormatForRecordFinancialTransaction($contributionId);
d3726f09 435
d3726f09 436 $params['payment_instrument_id'] = CRM_Utils_Array::value('payment_instrument_id', $trxnData, CRM_Utils_Array::value('payment_instrument_id', $params));
437
438 $paidStatus = CRM_Core_PseudoConstant::getKey('CRM_Financial_DAO_FinancialItem', 'status_id', 'Paid');
439 $arAccountId = CRM_Contribute_PseudoConstant::getRelationalFinancialAccount($contributionDAO->financial_type_id, 'Accounts Receivable Account is');
440 $completedStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
441
2561fc11 442 $trxnData['total_amount'] = $trxnData['net_amount'] = $trxnData['total_amount'];
d3726f09 443 $trxnData['from_financial_account_id'] = $arAccountId;
444 $trxnData['status_id'] = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Refunded');
445 // record the entry
446 $financialTrxn = CRM_Contribute_BAO_Contribution::recordFinancialAccounts($params, $trxnData);
447
448 // note : not using the self::add method,
449 // the reason because it performs 'status change' related code execution for financial records
450 // which in 'Pending Refund' => 'Completed' is not useful, instead specific financial record updates
451 // are coded below i.e. just updating financial_item status to 'Paid'
452 if ($updateStatus) {
453 CRM_Core_DAO::setFieldValue('CRM_Contribute_BAO_Contribution', $contributionId, 'contribution_status_id', $completedStatusId);
454 }
d3726f09 455 return $financialTrxn;
456 }
457
9a2dce8d 458 /**
459 * @param int $contributionId
460 * @param array $trxnData
461 * @param int $participantId
462 *
463 * @return \CRM_Core_BAO_FinancialTrxn
464 */
465 public static function recordPayment($contributionId, $trxnData, $participantId) {
466 list($contributionDAO, $params) = self::getContributionAndParamsInFormatForRecordFinancialTransaction($contributionId);
9a2dce8d 467
9a2dce8d 468 $trxnData['trxn_date'] = !empty($trxnData['trxn_date']) ? $trxnData['trxn_date'] : date('YmdHis');
469 $params['payment_instrument_id'] = CRM_Utils_Array::value('payment_instrument_id', $trxnData, CRM_Utils_Array::value('payment_instrument_id', $params));
470
471 $paidStatus = CRM_Core_PseudoConstant::getKey('CRM_Financial_DAO_FinancialItem', 'status_id', 'Paid');
472 $arAccountId = CRM_Contribute_PseudoConstant::getRelationalFinancialAccount($contributionDAO->financial_type_id, 'Accounts Receivable Account is');
473 $completedStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
474
475 $params['partial_payment_total'] = $contributionDAO->total_amount;
476 $params['partial_amount_to_pay'] = $trxnData['total_amount'];
477 $trxnData['net_amount'] = !empty($trxnData['net_amount']) ? $trxnData['net_amount'] : $trxnData['total_amount'];
478 $params['pan_truncation'] = CRM_Utils_Array::value('pan_truncation', $trxnData);
479 $params['card_type_id'] = CRM_Utils_Array::value('card_type_id', $trxnData);
480 $params['check_number'] = CRM_Utils_Array::value('check_number', $trxnData);
481
482 // record the entry
483 $financialTrxn = CRM_Contribute_BAO_Contribution::recordFinancialAccounts($params, $trxnData);
484 $toFinancialAccount = $arAccountId;
485 $trxnId = CRM_Core_BAO_FinancialTrxn::getBalanceTrxnAmt($contributionId, $contributionDAO->financial_type_id);
486 if (!empty($trxnId)) {
487 $trxnId = $trxnId['trxn_id'];
488 }
489 elseif (!empty($contributionDAO->payment_instrument_id)) {
490 $trxnId = CRM_Financial_BAO_FinancialTypeAccount::getInstrumentFinancialAccount($contributionDAO->payment_instrument_id);
491 }
492 else {
493 $relationTypeId = key(CRM_Core_PseudoConstant::accountOptionValues('financial_account_type', NULL, " AND v.name LIKE 'Asset' "));
494 $queryParams = [1 => [$relationTypeId, 'Integer']];
495 $trxnId = CRM_Core_DAO::singleValueQuery("SELECT id FROM civicrm_financial_account WHERE is_default = 1 AND financial_account_type_id = %1", $queryParams);
496 }
497
498 // update statuses
499 // criteria for updates contribution total_amount == financial_trxns of partial_payments
500 $sql = "SELECT SUM(ft.total_amount) as sum_of_payments, SUM(ft.net_amount) as net_amount_total
501FROM civicrm_financial_trxn ft
502LEFT JOIN civicrm_entity_financial_trxn eft
503 ON (ft.id = eft.financial_trxn_id)
504WHERE eft.entity_table = 'civicrm_contribution'
505 AND eft.entity_id = {$contributionId}
506 AND ft.to_financial_account_id != {$toFinancialAccount}
507 AND ft.status_id = {$completedStatusId}
508";
509 $query = CRM_Core_DAO::executeQuery($sql);
510 $query->fetch();
511 $sumOfPayments = $query->sum_of_payments;
512
513 // update statuses
514 if ($contributionDAO->total_amount == $sumOfPayments) {
515 // update contribution status and
516 // clean cancel info (if any) if prev. contribution was updated in case of 'Refunded' => 'Completed'
517 $contributionDAO->contribution_status_id = $completedStatusId;
518 $contributionDAO->cancel_date = 'null';
519 $contributionDAO->cancel_reason = NULL;
520 $netAmount = !empty($trxnData['net_amount']) ? NULL : $trxnData['total_amount'];
521 $contributionDAO->net_amount = $query->net_amount_total + $netAmount;
522 $contributionDAO->fee_amount = $contributionDAO->total_amount - $contributionDAO->net_amount;
523 $contributionDAO->save();
524
525 //Change status of financial record too
526 $financialTrxn->status_id = $completedStatusId;
527 $financialTrxn->save();
528
529 // note : not using the self::add method,
530 // the reason because it performs 'status change' related code execution for financial records
531 // which in 'Partial Paid' => 'Completed' is not useful, instead specific financial record updates
532 // are coded below i.e. just updating financial_item status to 'Paid'
533
54ec4839 534 if (!$participantId) {
535 $participantId = CRM_Core_DAO::getFieldValue('CRM_Event_DAO_ParticipantPayment', $contributionId, 'participant_id', 'contribution_id');
536 }
9a2dce8d 537 if ($participantId) {
538 // update participant status
539 $participantStatuses = CRM_Event_PseudoConstant::participantStatus();
540 $ids = CRM_Event_BAO_Participant::getParticipantIds($contributionId);
541 foreach ($ids as $val) {
542 $participantUpdate['id'] = $val;
543 $participantUpdate['status_id'] = array_search('Registered', $participantStatuses);
544 CRM_Event_BAO_Participant::add($participantUpdate);
545 }
546 }
547
548 // Remove this - completeOrder does it.
549 CRM_Contribute_BAO_Contribution::updateMembershipBasedOnCompletionOfContribution(
550 $contributionDAO,
551 $contributionId,
552 $trxnData['trxn_date']
553 );
554
555 // update financial item statuses
556 $baseTrxnId = CRM_Core_BAO_FinancialTrxn::getFinancialTrxnId($contributionId);
557 $sqlFinancialItemUpdate = "
558UPDATE civicrm_financial_item fi
559 LEFT JOIN civicrm_entity_financial_trxn eft
560 ON (eft.entity_id = fi.id AND eft.entity_table = 'civicrm_financial_item')
561SET status_id = {$paidStatus}
562WHERE eft.financial_trxn_id IN ({$trxnId}, {$baseTrxnId['financialTrxnId']})
563";
564 CRM_Core_DAO::executeQuery($sqlFinancialItemUpdate);
565 }
566 return $financialTrxn;
567 }
568
569 /**
570 * The recordFinancialTransactions function has capricious requirements for input parameters - load them.
571 *
572 * The function needs rework but for now we need to give it what it wants.
573 *
574 * @param int $contributionId
575 *
576 * @return array
577 */
578 protected static function getContributionAndParamsInFormatForRecordFinancialTransaction($contributionId) {
579 $getInfoOf['id'] = $contributionId;
580 $defaults = [];
db62d3a5 581 $contributionDAO = CRM_Contribute_BAO_Contribution::retrieve($getInfoOf, $defaults);
9a2dce8d 582
583 // build params for recording financial trxn entry
584 $params['contribution'] = $contributionDAO;
585 $params = array_merge($defaults, $params);
586 $params['skipLineItem'] = TRUE;
587 return [$contributionDAO, $params];
588 }
589
7585038a 590 /**
591 * Does this payment complete the contribution
592 *
593 * @param int $contributionID
594 * @param float $paymentAmount
595 *
596 * @return bool
597 */
598 protected static function isPaymentCompletesContribution($contributionID, $paymentAmount) {
599 $outstandingBalance = CRM_Contribute_BAO_Contribution::getContributionBalance($contributionID);
600 $cmp = bccomp($paymentAmount, $outstandingBalance, 5);
601 return ($cmp == 0 || $cmp == 1);
602 }
603
16b0233c 604 /**
605 * Update the status of the contribution.
606 *
607 * We pass the is_post_payment_create as we have already created the line items
608 *
609 * @param int $contributionID
610 * @param string $status
611 *
612 * @throws \CiviCRM_API3_Exception
613 */
614 private static function updateContributionStatus(int $contributionID, string $status) {
615 civicrm_api3('Contribution', 'create',
616 [
617 'id' => $contributionID,
618 'is_post_payment_create' => TRUE,
619 'contribution_status_id' => $status,
620 ]
621 );
622 }
623
f5a468db 624 /**
625 * Get the line items for the contribution.
626 *
627 * Retrieve the line items and wrangle the following
628 *
629 * - get the outstanding balance on a line item basis.
630 * - determine what amount is being paid on this line item - we get the total being paid
631 * for the whole contribution and determine the ratio of the balance that is being paid
632 * and then assign apply that ratio to each line item.
633 * - if overrides have been passed in we use those amounts instead.
634 *
635 * @param $params
636 *
637 * @return array
638 * @throws \CiviCRM_API3_Exception
639 */
640 protected static function getPayableLineItems($params): array {
641 $lineItems = CRM_Price_BAO_LineItem::getLineItemsByContributionID($params['contribution_id']);
642 $lineItemOverrides = CRM_Utils_Array::value('line_item', $params, []);
643 $outstandingBalance = CRM_Contribute_BAO_Contribution::getContributionBalance($params['contribution_id']);
644 if ($outstandingBalance !== 0.0) {
645 $ratio = $params['total_amount'] / $outstandingBalance;
646 }
647 else {
648 // Help we are making a payment but no money is owed. We won't allocate the overpayment to any line item.
649 $ratio = 0;
650 }
651 foreach ($lineItems as $lineItemID => $lineItem) {
4f6e08d0 652 $lineItems[$lineItemID]['paid'] = self::getAmountOfLineItemPaid($lineItemID);
f5a468db 653 $lineItems[$lineItemID]['balance'] = $lineItem['subTotal'] - $lineItems[$lineItemID]['paid'];
654
655 if (!empty($lineItemOverrides)) {
656 $lineItems[$lineItemID]['allocation'] = CRM_Utils_Array::value($lineItemID, $lineItemOverrides);
657 }
658 else {
659 $lineItems[$lineItemID]['allocation'] = $lineItems[$lineItemID]['balance'] * $ratio;
660 }
661 }
662 return $lineItems;
663 }
664
4f6e08d0 665 /**
666 * Get the amount paid so far against this line item.
667 *
668 * @param int $lineItemID
669 *
670 * @return float
671 *
672 * @throws \CiviCRM_API3_Exception
673 */
674 protected static function getAmountOfLineItemPaid($lineItemID) {
675 $paid = 0;
676 $financialItems = civicrm_api3('FinancialItem', 'get', [
677 'entity_id' => $lineItemID,
678 'entity_table' => 'civicrm_line_item',
679 'options' => ['sort' => 'id DESC', 'limit' => 0],
680 ])['values'];
681 if (!empty($financialItems)) {
682 $entityFinancialTrxns = civicrm_api3('EntityFinancialTrxn', 'get', [
683 'entity_table' => 'civicrm_financial_item',
684 'entity_id' => ['IN' => array_keys($financialItems)],
685 'options' => ['limit' => 0],
686 'financial_trxn_id.is_payment' => 1,
687 ])['values'];
688 foreach ($entityFinancialTrxns as $entityFinancialTrxn) {
689 $paid += $entityFinancialTrxn['amount'];
690 }
691 }
692 return (float) $paid;
693 }
694
0c9b306a 695}