Commit | Line | Data |
---|---|---|
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 | */ | |
21 | class 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 | } |