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 | */ |
37 | class 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 |
501 | FROM civicrm_financial_trxn ft |
502 | LEFT JOIN civicrm_entity_financial_trxn eft |
503 | ON (ft.id = eft.financial_trxn_id) |
504 | WHERE 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 = " |
558 | UPDATE 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') |
561 | SET status_id = {$paidStatus} |
562 | WHERE 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 | } |