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