Merge pull request #23009 from seamuslee001/dev_core_3132
[civicrm-core.git] / CRM / Financial / BAO / Payment.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 */
17
18 use Civi\Api4\FinancialItem;
19 use Civi\Api4\LineItem;
20
21 /**
22 * This class contains payment related functions.
23 */
24 class CRM_Financial_BAO_Payment {
25
26 /**
27 * Function to process additional payment for partial and refund
28 * contributions.
29 *
30 * This function is called via API payment.create function. All forms that
31 * add payments should use this.
32 *
33 * @param array $params
34 * - contribution_id
35 * - total_amount
36 * - line_item
37 *
38 * @return \CRM_Financial_DAO_FinancialTrxn
39 *
40 * @throws \CRM_Core_Exception
41 * @throws \CiviCRM_API3_Exception
42 * @throws \API_Exception
43 */
44 public static function create(array $params): CRM_Financial_DAO_FinancialTrxn {
45 $contribution = civicrm_api3('Contribution', 'getsingle', ['id' => $params['contribution_id']]);
46 $contributionStatus = CRM_Core_PseudoConstant::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', $contribution['contribution_status_id']);
47 $isPaymentCompletesContribution = self::isPaymentCompletesContribution($params['contribution_id'], $params['total_amount'], $contributionStatus);
48 $lineItems = self::getPayableLineItems($params);
49
50 $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', 'trxn_date', 'order_reference'];
51 $paymentTrxnParams = array_intersect_key($params, array_fill_keys($whiteList, 1));
52 $paymentTrxnParams['is_payment'] = 1;
53 // Really we should have a DB default.
54 $paymentTrxnParams['fee_amount'] = $paymentTrxnParams['fee_amount'] ?? 0;
55
56 if (isset($paymentTrxnParams['payment_processor_id']) && empty($paymentTrxnParams['payment_processor_id'])) {
57 // Don't pass 0 - ie the Pay Later processor as it is a pseudo-processor.
58 unset($paymentTrxnParams['payment_processor_id']);
59 }
60 if (empty($paymentTrxnParams['payment_instrument_id'])) {
61 if (!empty($params['payment_processor_id'])) {
62 $paymentTrxnParams['payment_instrument_id'] = civicrm_api3('PaymentProcessor', 'getvalue', ['return' => 'payment_instrument_id', 'id' => $paymentTrxnParams['payment_processor_id']]);
63 }
64 else {
65 // Fall back on the payment instrument already used - should we deprecate this?
66 $paymentTrxnParams['payment_instrument_id'] = $contribution['payment_instrument_id'];
67 }
68 }
69
70 $paymentTrxnParams['currency'] = $contribution['currency'];
71
72 $accountsReceivableAccount = CRM_Financial_BAO_FinancialAccount::getFinancialAccountForFinancialTypeByRelationship($contribution['financial_type_id'], 'Accounts Receivable Account is');
73 $paymentTrxnParams['to_financial_account_id'] = CRM_Contribute_BAO_Contribution::getToFinancialAccount($contribution, $params);
74 $paymentTrxnParams['from_financial_account_id'] = $accountsReceivableAccount;
75
76 if ($params['total_amount'] > 0) {
77 $paymentTrxnParams['status_id'] = CRM_Core_PseudoConstant::getKey('CRM_Core_BAO_FinancialTrxn', 'status_id', 'Completed');
78 }
79 elseif ($params['total_amount'] < 0) {
80 $paymentTrxnParams['status_id'] = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Refunded');
81 }
82
83 //If Payment is recorded on Failed contribution, update it to Pending.
84 if ($contributionStatus === 'Failed' && $params['total_amount'] > 0) {
85 //Enter a financial trxn to record a payment in receivable account
86 //as failed transaction does not insert any trxn values. Hence, if Payment is
87 //recorded on a failed contribution, the transition happens from Failed -> Pending -> Completed.
88 $ftParams = array_merge($paymentTrxnParams, [
89 'from_financial_account_id' => NULL,
90 'to_financial_account_id' => $accountsReceivableAccount,
91 'is_payment' => 0,
92 'status_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending'),
93 ]);
94 CRM_Core_BAO_FinancialTrxn::create($ftParams);
95 $contributionStatus = 'Pending';
96 self::updateContributionStatus($contribution['id'], $contributionStatus);
97 }
98 $trxn = CRM_Core_BAO_FinancialTrxn::create($paymentTrxnParams);
99
100 if ($params['total_amount'] < 0 && !empty($params['cancelled_payment_id'])) {
101 self::reverseAllocationsFromPreviousPayment($params, $trxn->id);
102 }
103 else {
104 $salesTaxFinancialAccount = CRM_Contribute_BAO_Contribution::getSalesTaxFinancialAccounts();
105 $financialItems = LineItem::get(FALSE)
106 ->addSelect('tax_amount', 'price_field_value_id', 'financial_item.id', 'financial_item.status_id:name', 'financial_item.financial_account_id')
107 ->addJoin(
108 'FinancialItem AS financial_item',
109 'INNER',
110 NULL,
111 ['financial_item.entity_table', '=', '"civicrm_line_item"'],
112 ['financial_item.entity_id', '=', 'id']
113 )
114 ->addOrderBy('financial_item.id', 'DESC')
115 ->addWhere('contribution_id', '=', (int) $params['contribution_id'])->execute();
116
117 foreach ($lineItems as $key => $value) {
118 if ($value['allocation'] === (float) 0) {
119 continue;
120 }
121 $financialItemID = NULL;
122 $currentFinancialItemStatus = NULL;
123 foreach ($financialItems as $item) {
124 // We check against price_field_value_id rather than line item
125 // id because that is what the code did previously - but it's
126 // unclear whether this is for good reason or bad coding.
127 if ($item['price_field_value_id'] === (int) $value['price_field_value_id']
128 && !in_array($item['financial_item.financial_account_id'], $salesTaxFinancialAccount, TRUE)
129 ) {
130 $financialItemID = $item['financial_item.id'];
131 $currentFinancialItemStatus = $item['financial_item.status_id:name'];
132 }
133 }
134 if (!$financialItemID) {
135 $financialItemID = self::getNewFinancialItemID($value, $params['trxn_date'], $contribution['contact_id'], $paymentTrxnParams['currency']);
136 }
137
138 $eftParams = [
139 'entity_table' => 'civicrm_financial_item',
140 'financial_trxn_id' => $trxn->id,
141 'entity_id' => $financialItemID,
142 'amount' => $value['allocation'],
143 ];
144
145 civicrm_api3('EntityFinancialTrxn', 'create', $eftParams);
146 if ($currentFinancialItemStatus && 'Paid' !== $currentFinancialItemStatus) {
147 $newStatus = $value['allocation'] < $value['balance'] ? 'Partially paid' : 'Paid';
148 FinancialItem::update(FALSE)
149 ->addValue('status_id:name', $newStatus)
150 ->addWhere('id', '=', $financialItemID)
151 ->execute();
152 }
153
154 foreach ($financialItems as $item) {
155 if ($item['price_field_value_id'] === (int) $value['price_field_value_id']
156 && in_array($item['financial_item.financial_account_id'], $salesTaxFinancialAccount, TRUE)
157 ) {
158 // @todo - this is expected to be broken - it should be fixed to
159 // a) have the getPayableLineItems add the amount to allocate for tax
160 // b) call EntityFinancialTrxn directly - per above.
161 // - see https://github.com/civicrm/civicrm-core/pull/14763
162 $entityParams = [
163 'contribution_total_amount' => $contribution['total_amount'],
164 'trxn_total_amount' => $params['total_amount'],
165 'trxn_id' => $trxn->id,
166 'line_item_amount' => $item['tax_amount'],
167 ];
168 $eftParams['entity_id'] = $item['financial_item.id'];
169 CRM_Contribute_BAO_Contribution::createProportionalEntry($entityParams, $eftParams);
170 }
171 }
172 }
173 }
174
175 if ($isPaymentCompletesContribution) {
176 if ($contributionStatus === 'Pending refund') {
177 // Ideally we could still call completetransaction as non-payment related actions should
178 // be outside this class. However, for now we just update the contribution here.
179 // Unit test cover in CRM_Event_BAO_AdditionalPaymentTest::testTransactionInfo.
180 civicrm_api3('Contribution', 'create',
181 [
182 'id' => $contribution['id'],
183 'contribution_status_id' => 'Completed',
184 ]
185 );
186 }
187 else {
188 civicrm_api3('Contribution', 'completetransaction', [
189 'id' => $contribution['id'],
190 'is_post_payment_create' => TRUE,
191 'is_email_receipt' => $params['is_send_contribution_notification'],
192 'trxn_date' => $params['trxn_date'],
193 'payment_instrument_id' => $paymentTrxnParams['payment_instrument_id'],
194 'payment_processor_id' => $paymentTrxnParams['payment_processor_id'] ?? NULL,
195 ]);
196 // Get the trxn
197 $trxnId = CRM_Core_BAO_FinancialTrxn::getFinancialTrxnId($contribution['id'], 'DESC');
198 $ftParams = ['id' => $trxnId['financialTrxnId']];
199 $trxn = CRM_Core_BAO_FinancialTrxn::retrieve($ftParams);
200 }
201 }
202 elseif ($contributionStatus === 'Pending' && $params['total_amount'] > 0) {
203 self::updateContributionStatus($contribution['id'], 'Partially Paid');
204 $participantPayments = civicrm_api3('ParticipantPayment', 'get', [
205 'contribution_id' => $contribution['id'],
206 'participant_id.status_id' => ['IN' => ['Pending from pay later', 'Pending from incomplete transaction']],
207 ])['values'];
208 foreach ($participantPayments as $participantPayment) {
209 civicrm_api3('Participant', 'create', ['id' => $participantPayment['participant_id'], 'status_id' => 'Partially paid']);
210 }
211 }
212 elseif ($contributionStatus === 'Completed' && ((float) CRM_Core_BAO_FinancialTrxn::getTotalPayments($contribution['id'], TRUE) === 0.0)) {
213 // If the contribution has previously been completed (fully paid) and now has total payments adding up to 0
214 // change status to refunded.
215 self::updateContributionStatus($contribution['id'], 'Refunded');
216 }
217 self::updateRelatedContribution($params, $params['contribution_id']);
218 CRM_Contribute_BAO_Contribution::recordPaymentActivity($params['contribution_id'], CRM_Utils_Array::value('participant_id', $params), $params['total_amount'], $trxn->currency, $trxn->trxn_date);
219 return $trxn;
220 }
221
222 /**
223 * Function to update contribution's check_number and trxn_id by
224 * concatenating values from financial trxn's check_number and trxn_id
225 * respectively
226 *
227 * @param array $params
228 * @param int $contributionID
229 *
230 * @throws \CiviCRM_API3_Exception
231 */
232 public static function updateRelatedContribution(array $params, int $contributionID): void {
233 $contributionDAO = new CRM_Contribute_DAO_Contribution();
234 $contributionDAO->id = $contributionID;
235 $contributionDAO->find(TRUE);
236 if (isset($params['fee_amount'])) {
237 // Update contribution.fee_amount to be be the total of all fees
238 // since the payment is already saved the total here will be right.
239 $payments = civicrm_api3('Payment', 'get', [
240 'contribution_id' => $contributionID,
241 'return' => 'fee_amount',
242 ])['values'];
243 $totalFees = 0;
244 foreach ($payments as $payment) {
245 $totalFees += $payment['fee_amount'] ?? 0;
246 }
247 $contributionDAO->fee_amount = $totalFees;
248 $contributionDAO->net_amount = $contributionDAO->total_amount - $contributionDAO->fee_amount;
249 }
250
251 foreach (['trxn_id', 'check_number'] as $fieldName) {
252 if (!empty($params[$fieldName])) {
253 $values = [];
254 if (!empty($contributionDAO->$fieldName)) {
255 $values = explode(',', $contributionDAO->$fieldName);
256 }
257 // if submitted check_number or trxn_id value is
258 // already present then ignore else add to $values array
259 if (!in_array($params[$fieldName], $values)) {
260 $values[] = $params[$fieldName];
261 }
262 $contributionDAO->$fieldName = implode(',', $values);
263 }
264 }
265
266 $contributionDAO->save();
267 }
268
269 /**
270 * Send an email confirming a payment that has been received.
271 *
272 * @param array $params
273 *
274 * @return array
275 *
276 * @throws \CiviCRM_API3_Exception
277 */
278 public static function sendConfirmation($params) {
279
280 $entities = self::loadRelatedEntities($params['id']);
281
282 if (!empty($entities['event'])) {
283 CRM_Event_BAO_Event::setOutputTimeZone($entities['event']);
284 }
285
286 $sendTemplateParams = [
287 'groupName' => 'msg_tpl_workflow_contribution',
288 'valueName' => 'payment_or_refund_notification',
289 'PDFFilename' => ts('notification') . '.pdf',
290 'contactId' => $entities['contact']['id'],
291 'toName' => $entities['contact']['display_name'],
292 'toEmail' => $entities['contact']['email'],
293 'tplParams' => self::getConfirmationTemplateParameters($entities),
294 ];
295 if (!empty($params['from']) && !empty($params['check_permissions'])) {
296 // Filter from against permitted emails.
297 $validEmails = self::getValidFromEmailsForPayment($entities['event']['id'] ?? NULL);
298 if (!isset($validEmails[$params['from']])) {
299 // Ignore unpermitted parameter.
300 unset($params['from']);
301 }
302 }
303 $sendTemplateParams['from'] = $params['from'] ?? key(CRM_Core_BAO_Email::domainEmails());
304 return CRM_Core_BAO_MessageTemplate::sendTemplate($sendTemplateParams);
305 }
306
307 /**
308 * Get valid from emails for payment.
309 *
310 * @param int $eventID
311 *
312 * @return array
313 */
314 public static function getValidFromEmailsForPayment($eventID = NULL) {
315 if ($eventID) {
316 $emails = CRM_Event_BAO_Event::getFromEmailIds($eventID);
317 }
318 else {
319 $emails['from_email_id'] = CRM_Core_BAO_Email::getFromEmail();
320 }
321 return $emails['from_email_id'];
322 }
323
324 /**
325 * Load entities related to the current payment id.
326 *
327 * This gives us all the data we need to send an email confirmation but avoiding
328 * getting anything not tested for the confirmations. We retrieve the 'full' event as
329 * it has been traditionally assigned in full.
330 *
331 * @param int $id
332 *
333 * @return array
334 * - contact = ['id' => x, 'display_name' => y, 'email' => z]
335 * - event = [.... full event details......]
336 * - contribution = ['id' => x],
337 * - payment = [payment info + payment summary info]
338 * @throws \CiviCRM_API3_Exception
339 */
340 protected static function loadRelatedEntities($id) {
341 $entities = [];
342 $contributionID = (int) civicrm_api3('EntityFinancialTrxn', 'getvalue', [
343 'financial_trxn_id' => $id,
344 'entity_table' => 'civicrm_contribution',
345 'return' => 'entity_id',
346 ]);
347 $entities['contribution'] = ['id' => $contributionID];
348 $entities['payment'] = array_merge(civicrm_api3('FinancialTrxn', 'getsingle', ['id' => $id]),
349 CRM_Contribute_BAO_Contribution::getPaymentInfo($contributionID)
350 );
351
352 $contactID = self::getPaymentContactID($contributionID);
353 [$displayName, $email] = CRM_Contact_BAO_Contact_Location::getEmailDetails($contactID);
354 $entities['contact'] = ['id' => $contactID, 'display_name' => $displayName, 'email' => $email];
355 $contact = civicrm_api3('Contact', 'getsingle', ['id' => $contactID, 'return' => 'email_greeting']);
356 $entities['contact']['email_greeting'] = $contact['email_greeting_display'];
357
358 $participantRecords = civicrm_api3('ParticipantPayment', 'get', [
359 'contribution_id' => $contributionID,
360 'api.Participant.get' => ['return' => 'event_id'],
361 'sequential' => 1,
362 ])['values'];
363 if (!empty($participantRecords)) {
364 $entities['event'] = civicrm_api3('Event', 'getsingle', ['id' => $participantRecords[0]['api.Participant.get']['values'][0]['event_id']]);
365 if (!empty($entities['event']['is_show_location'])) {
366 $locationParams = [
367 'entity_id' => $entities['event']['id'],
368 'entity_table' => 'civicrm_event',
369 ];
370 $entities['location'] = CRM_Core_BAO_Location::getValues($locationParams, TRUE);
371 }
372 }
373
374 return $entities;
375 }
376
377 /**
378 * @param int $contributionID
379 *
380 * @return int
381 * @throws \CiviCRM_API3_Exception
382 * @throws \CiviCRM_API3_Exception
383 */
384 public static function getPaymentContactID($contributionID) {
385 $contribution = civicrm_api3('Contribution', 'getsingle', [
386 'id' => $contributionID ,
387 'return' => ['contact_id'],
388 ]);
389 return (int) $contribution['contact_id'];
390 }
391
392 /**
393 * @param array $entities
394 * Related entities as an array keyed by the various entities.
395 *
396 * @return array
397 * Values required for the notification
398 * - contact_id
399 * - template_variables
400 * - event (DAO of event if relevant)
401 */
402 public static function getConfirmationTemplateParameters($entities) {
403 $templateVariables = [
404 'contactDisplayName' => $entities['contact']['display_name'],
405 'emailGreeting' => $entities['contact']['email_greeting'],
406 'totalAmount' => $entities['payment']['total'],
407 'amountOwed' => $entities['payment']['balance'],
408 'totalPaid' => $entities['payment']['paid'],
409 'paymentAmount' => $entities['payment']['total_amount'],
410 'checkNumber' => $entities['payment']['check_number'] ?? NULL,
411 'receive_date' => $entities['payment']['trxn_date'],
412 'paidBy' => CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_FinancialTrxn', 'payment_instrument_id', $entities['payment']['payment_instrument_id']),
413 'isShowLocation' => (!empty($entities['event']) ? $entities['event']['is_show_location'] : FALSE),
414 'location' => $entities['location'] ?? NULL,
415 'event' => $entities['event'] ?? NULL,
416 'component' => (!empty($entities['event']) ? 'event' : 'contribution'),
417 'isRefund' => $entities['payment']['total_amount'] < 0,
418 'isAmountzero' => $entities['payment']['total_amount'] === 0,
419 'refundAmount' => ($entities['payment']['total_amount'] < 0 ? $entities['payment']['total_amount'] : NULL),
420 'paymentsComplete' => ($entities['payment']['balance'] == 0),
421 ];
422
423 return self::filterUntestedTemplateVariables($templateVariables);
424 }
425
426 /**
427 * Filter out any untested variables.
428 *
429 * This just serves to highlight if any variables are added without a unit test also being added.
430 *
431 * (if hit then add a unit test for the param & add to this array).
432 *
433 * @param array $params
434 *
435 * @return array
436 */
437 public static function filterUntestedTemplateVariables($params) {
438 $testedTemplateVariables = [
439 'contactDisplayName',
440 'totalAmount',
441 'amountOwed',
442 'paymentAmount',
443 'event',
444 'component',
445 'checkNumber',
446 'receive_date',
447 'paidBy',
448 'isShowLocation',
449 'location',
450 'isRefund',
451 'isAmountzero',
452 'refundAmount',
453 'totalPaid',
454 'paymentsComplete',
455 'emailGreeting',
456 ];
457 // These are assigned by the payment form - they still 'get through' from the
458 // form for now without being in here but we should ideally load
459 // and assign. Note we should update the tpl to use {if $billingName}
460 // and ditch contributeMode - although it might need to be deprecated rather than removed.
461 $todoParams = [
462 'contributeMode',
463 'billingName',
464 'address',
465 'credit_card_type',
466 'credit_card_number',
467 'credit_card_exp_date',
468 ];
469 $filteredParams = [];
470 foreach ($testedTemplateVariables as $templateVariable) {
471 // This will cause an a-notice if any are NOT set - by design. Ensuring
472 // they are set prevents leakage.
473 $filteredParams[$templateVariable] = $params[$templateVariable];
474 }
475 return $filteredParams;
476 }
477
478 /**
479 * Does this payment complete the contribution.
480 *
481 * @param int $contributionID
482 * @param float $paymentAmount
483 * @param string $previousStatus
484 *
485 * @return bool
486 */
487 protected static function isPaymentCompletesContribution($contributionID, $paymentAmount, $previousStatus) {
488 if ($previousStatus === 'Completed') {
489 return FALSE;
490 }
491 $outstandingBalance = CRM_Contribute_BAO_Contribution::getContributionBalance($contributionID);
492 $cmp = bccomp($paymentAmount, $outstandingBalance, 5);
493 return ($cmp == 0 || $cmp == 1);
494 }
495
496 /**
497 * Update the status of the contribution.
498 *
499 * We pass the is_post_payment_create as we have already created the line items
500 *
501 * @param int $contributionID
502 * @param string $status
503 *
504 * @throws \CiviCRM_API3_Exception
505 */
506 private static function updateContributionStatus(int $contributionID, string $status) {
507 civicrm_api3('Contribution', 'create',
508 [
509 'id' => $contributionID,
510 'is_post_payment_create' => TRUE,
511 'contribution_status_id' => $status,
512 ]
513 );
514 }
515
516 /**
517 * Get the line items for the contribution.
518 *
519 * Retrieve the line items and wrangle the following
520 *
521 * - get the outstanding balance on a line item basis.
522 * - determine what amount is being paid on this line item - we get the total being paid
523 * for the whole contribution and determine the ratio of the balance that is being paid
524 * and then assign apply that ratio to each line item.
525 * - if overrides have been passed in we use those amounts instead.
526 *
527 * @param $params
528 *
529 * @return array
530 * @throws \CiviCRM_API3_Exception
531 */
532 protected static function getPayableLineItems($params): array {
533 $lineItems = CRM_Price_BAO_LineItem::getLineItemsByContributionID($params['contribution_id']);
534 $lineItemOverrides = [];
535 if (!empty($params['line_item'])) {
536 // The format is a bit weird here - $params['line_item'] => [[1 => 10], [2 => 40]]
537 // Squash to [1 => 10, 2 => 40]
538 foreach ($params['line_item'] as $lineItem) {
539 $lineItemOverrides += $lineItem;
540 }
541 }
542 $outstandingBalance = CRM_Contribute_BAO_Contribution::getContributionBalance($params['contribution_id']);
543 if ($outstandingBalance !== 0.0) {
544 $ratio = $params['total_amount'] / $outstandingBalance;
545 }
546 elseif ($params['total_amount'] < 0) {
547 $ratio = $params['total_amount'] / (float) CRM_Core_BAO_FinancialTrxn::getTotalPayments($params['contribution_id'], TRUE);
548 }
549 else {
550 // Help we are making a payment but no money is owed. We won't allocate the overpayment to any line item.
551 $ratio = 0;
552 }
553 foreach ($lineItems as $lineItemID => $lineItem) {
554 // Ideally id would be set deeper but for now just add in here.
555 $lineItems[$lineItemID]['id'] = $lineItemID;
556 $lineItems[$lineItemID]['paid'] = self::getAmountOfLineItemPaid($lineItemID);
557 $lineItems[$lineItemID]['balance'] = $lineItem['subTotal'] - $lineItems[$lineItemID]['paid'];
558 if (!empty($lineItemOverrides)) {
559 $lineItems[$lineItemID]['allocation'] = $lineItemOverrides[$lineItemID] ?? NULL;
560 }
561 else {
562 if (empty($lineItems[$lineItemID]['balance']) && !empty($ratio) && $params['total_amount'] < 0) {
563 $lineItems[$lineItemID]['allocation'] = $lineItem['subTotal'] * $ratio;
564 }
565 else {
566 $lineItems[$lineItemID]['allocation'] = $lineItems[$lineItemID]['balance'] * $ratio;
567 }
568 }
569 }
570 return $lineItems;
571 }
572
573 /**
574 * Get the amount paid so far against this line item.
575 *
576 * @param int $lineItemID
577 *
578 * @return float
579 *
580 * @throws \CiviCRM_API3_Exception
581 */
582 protected static function getAmountOfLineItemPaid($lineItemID) {
583 $paid = 0;
584 $financialItems = civicrm_api3('FinancialItem', 'get', [
585 'entity_id' => $lineItemID,
586 'entity_table' => 'civicrm_line_item',
587 'options' => ['sort' => 'id DESC', 'limit' => 0],
588 ])['values'];
589 if (!empty($financialItems)) {
590 $entityFinancialTrxns = civicrm_api3('EntityFinancialTrxn', 'get', [
591 'entity_table' => 'civicrm_financial_item',
592 'entity_id' => ['IN' => array_keys($financialItems)],
593 'options' => ['limit' => 0],
594 'financial_trxn_id.is_payment' => 1,
595 ])['values'];
596 foreach ($entityFinancialTrxns as $entityFinancialTrxn) {
597 $paid += $entityFinancialTrxn['amount'];
598 }
599 }
600 return (float) $paid;
601 }
602
603 /**
604 * Reverse the entity financial transactions associated with the cancelled payment.
605 *
606 * The reversals are linked to the new payemnt.
607 *
608 * @param array $params
609 * @param int $trxnID
610 *
611 * @throws \CiviCRM_API3_Exception
612 */
613 protected static function reverseAllocationsFromPreviousPayment($params, $trxnID) {
614 // Do a direct reversal of any entity_financial_trxn records being cancelled.
615 $entityFinancialTrxns = civicrm_api3('EntityFinancialTrxn', 'get', [
616 'entity_table' => 'civicrm_financial_item',
617 'options' => ['limit' => 0],
618 'financial_trxn_id.id' => $params['cancelled_payment_id'],
619 ])['values'];
620 foreach ($entityFinancialTrxns as $entityFinancialTrxn) {
621 civicrm_api3('EntityFinancialTrxn', 'create', [
622 'entity_table' => 'civicrm_financial_item',
623 'entity_id' => $entityFinancialTrxn['entity_id'],
624 'amount' => -$entityFinancialTrxn['amount'],
625 'financial_trxn_id' => $trxnID,
626 ]);
627 }
628 }
629
630 /**
631 * Create a financial items & return the ID.
632 *
633 * Ideally this will never be called.
634 *
635 * However, I hit a scenario in testing where 'something' had created a pending payment with
636 * no financial items and that would result in a fatal error without handling here. I failed
637 * to replicate & am not investigating via a new test methodology
638 * https://github.com/civicrm/civicrm-core/pull/15706
639 *
640 * After this is in I will do more digging & once I feel confident new instances are not being
641 * created I will add deprecation notices into this function with a view to removing.
642 *
643 * However, I think we want to add it in 5.20 as there is a risk of users experiencing an error
644 * if there is incorrect data & we need time to ensure that what I hit was not a 'thing.
645 * (it might be the demo site data is a bit flawed & that was the issue).
646 *
647 * @param array $lineItem
648 * @param string $trxn_date
649 * @param int $contactID
650 * @param string $currency
651 *
652 * @return int
653 *
654 * @throws \CiviCRM_API3_Exception
655 */
656 protected static function getNewFinancialItemID($lineItem, $trxn_date, $contactID, $currency): int {
657 $financialAccount = CRM_Financial_BAO_FinancialAccount::getFinancialAccountForFinancialTypeByRelationship(
658 $lineItem['financial_type_id'],
659 'Income Account Is'
660 );
661 $itemParams = [
662 'transaction_date' => $trxn_date,
663 'contact_id' => $contactID,
664 'currency' => $currency,
665 'amount' => $lineItem['line_total'],
666 'description' => $lineItem['label'],
667 'status_id' => 'Unpaid',
668 'financial_account_id' => $financialAccount,
669 'entity_table' => 'civicrm_line_item',
670 'entity_id' => $lineItem['id'],
671 ];
672 return (int) civicrm_api3('FinancialItem', 'create', $itemParams)['id'];
673 }
674
675 }