Merge pull request #14266 from seamuslee001/dev_core_369
[civicrm-core.git] / CRM / Financial / BAO / Payment.php
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
54 * @throws \CiviCRM_API3_Exception
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');
59
60 $isPaymentCompletesContribution = self::isPaymentCompletesContribution($params['contribution_id'], $params['total_amount']);
61
62 // For legacy reasons Pending payments are completed through completetransaction.
63 // @todo completetransaction should transition components but financial transactions
64 // should be handled through Payment.create.
65 $isSkipRecordingPaymentHereForLegacyHandlingReasons = ($contributionStatus == 'Pending' && $isPaymentCompletesContribution);
66
67 if (!$isSkipRecordingPaymentHereForLegacyHandlingReasons && $params['total_amount'] > 0) {
68 $trxn = CRM_Contribute_BAO_Contribution::recordPartialPayment($contribution, $params);
69
70 if (CRM_Utils_Array::value('line_item', $params) && !empty($trxn)) {
71 foreach ($params['line_item'] as $values) {
72 foreach ($values as $id => $amount) {
73 $p = ['id' => $id];
74 $check = CRM_Price_BAO_LineItem::retrieve($p, $defaults);
75 if (empty($check)) {
76 throw new API_Exception('Please specify a valid Line Item.');
77 }
78 // get financial item
79 $sql = "SELECT fi.id
80 FROM civicrm_financial_item fi
81 INNER JOIN civicrm_line_item li ON li.id = fi.entity_id and fi.entity_table = 'civicrm_line_item'
82 WHERE li.contribution_id = %1 AND li.id = %2";
83 $sqlParams = [
84 1 => [$params['contribution_id'], 'Integer'],
85 2 => [$id, 'Integer'],
86 ];
87 $fid = CRM_Core_DAO::singleValueQuery($sql, $sqlParams);
88 // Record Entity Financial Trxn
89 $eftParams = [
90 'entity_table' => 'civicrm_financial_item',
91 'financial_trxn_id' => $trxn->id,
92 'amount' => $amount,
93 'entity_id' => $fid,
94 ];
95 civicrm_api3('EntityFinancialTrxn', 'create', $eftParams);
96 }
97 }
98 }
99 elseif (!empty($trxn)) {
100 CRM_Contribute_BAO_Contribution::assignProportionalLineItems($params, $trxn->id, $contribution['total_amount']);
101 }
102 }
103 elseif ($params['total_amount'] < 0) {
104 $trxn = self::recordRefundPayment($params['contribution_id'], $params, FALSE);
105 }
106
107 if ($isPaymentCompletesContribution) {
108 if ($contributionStatus == 'Pending refund') {
109 // Ideally we could still call completetransaction as non-payment related actions should
110 // be outside this class. However, for now we just update the contribution here.
111 // Unit test cover in CRM_Event_BAO_AdditionalPaymentTest::testTransactionInfo.
112 civicrm_api3('Contribution', 'create',
113 [
114 'id' => $contribution['id'],
115 'contribution_status_id' => 'Completed',
116 ]
117 );
118 }
119 else {
120 civicrm_api3('Contribution', 'completetransaction', ['id' => $contribution['id']]);
121 // Get the trxn
122 $trxnId = CRM_Core_BAO_FinancialTrxn::getFinancialTrxnId($contribution['id'], 'DESC');
123 $ftParams = ['id' => $trxnId['financialTrxnId']];
124 $trxn = CRM_Core_BAO_FinancialTrxn::retrieve($ftParams, CRM_Core_DAO::$_nullArray);
125 }
126 }
127 elseif ($contributionStatus === 'Pending') {
128 civicrm_api3('Contribution', 'create',
129 [
130 'id' => $contribution['id'],
131 'contribution_status_id' => 'Partially paid',
132 ]
133 );
134 }
135
136 return $trxn;
137 }
138
139 /**
140 * Send an email confirming a payment that has been received.
141 *
142 * @param array $params
143 *
144 * @return array
145 */
146 public static function sendConfirmation($params) {
147
148 $entities = self::loadRelatedEntities($params['id']);
149 $sendTemplateParams = [
150 'groupName' => 'msg_tpl_workflow_contribution',
151 'valueName' => 'payment_or_refund_notification',
152 'PDFFilename' => ts('notification') . '.pdf',
153 'contactId' => $entities['contact']['id'],
154 'toName' => $entities['contact']['display_name'],
155 'toEmail' => $entities['contact']['email'],
156 'tplParams' => self::getConfirmationTemplateParameters($entities),
157 ];
158 return CRM_Core_BAO_MessageTemplate::sendTemplate($sendTemplateParams);
159 }
160
161 /**
162 * Load entities related to the current payment id.
163 *
164 * This gives us all the data we need to send an email confirmation but avoiding
165 * getting anything not tested for the confirmations. We retrieve the 'full' event as
166 * it has been traditionally assigned in full.
167 *
168 * @param int $id
169 *
170 * @return array
171 * - contact = ['id' => x, 'display_name' => y, 'email' => z]
172 * - event = [.... full event details......]
173 * - contribution = ['id' => x],
174 * - payment = [payment info + payment summary info]
175 */
176 protected static function loadRelatedEntities($id) {
177 $entities = [];
178 $contributionID = (int) civicrm_api3('EntityFinancialTrxn', 'getvalue', [
179 'financial_trxn_id' => $id,
180 'entity_table' => 'civicrm_contribution',
181 'return' => 'entity_id',
182 ]);
183 $entities['contribution'] = ['id' => $contributionID];
184 $entities['payment'] = array_merge(civicrm_api3('FinancialTrxn', 'getsingle', ['id' => $id]),
185 CRM_Contribute_BAO_Contribution::getPaymentInfo($contributionID)
186 );
187
188 $contactID = self::getPaymentContactID($contributionID);
189 list($displayName, $email) = CRM_Contact_BAO_Contact_Location::getEmailDetails($contactID);
190 $entities['contact'] = ['id' => $contactID, 'display_name' => $displayName, 'email' => $email];
191 $contact = civicrm_api3('Contact', 'getsingle', ['id' => $contactID, 'return' => 'email_greeting']);
192 $entities['contact']['email_greeting'] = $contact['email_greeting_display'];
193
194 $participantRecords = civicrm_api3('ParticipantPayment', 'get', [
195 'contribution_id' => $contributionID,
196 'api.Participant.get' => ['return' => 'event_id'],
197 'sequential' => 1,
198 ])['values'];
199 if (!empty($participantRecords)) {
200 $entities['event'] = civicrm_api3('Event', 'getsingle', ['id' => $participantRecords[0]['api.Participant.get']['values'][0]['event_id']]);
201 if (!empty($entities['event']['is_show_location'])) {
202 $locationParams = [
203 'entity_id' => $entities['event']['id'],
204 'entity_table' => 'civicrm_event',
205 ];
206 $entities['location'] = CRM_Core_BAO_Location::getValues($locationParams, TRUE);
207 }
208 }
209
210 return $entities;
211 }
212
213 /**
214 * @param int $contributionID
215 *
216 * @return int
217 */
218 public static function getPaymentContactID($contributionID) {
219 $contribution = civicrm_api3('Contribution', 'getsingle', [
220 'id' => $contributionID ,
221 'return' => ['contact_id'],
222 ]);
223 return (int) $contribution['contact_id'];
224 }
225
226 /**
227 * @param array $entities
228 * Related entities as an array keyed by the various entities.
229 *
230 * @return array
231 * Values required for the notification
232 * - contact_id
233 * - template_variables
234 * - event (DAO of event if relevant)
235 */
236 public static function getConfirmationTemplateParameters($entities) {
237 $templateVariables = [
238 'contactDisplayName' => $entities['contact']['display_name'],
239 'emailGreeting' => $entities['contact']['email_greeting'],
240 'totalAmount' => $entities['payment']['total'],
241 'amountOwed' => $entities['payment']['balance'],
242 'totalPaid' => $entities['payment']['paid'],
243 'paymentAmount' => $entities['payment']['total_amount'],
244 'checkNumber' => CRM_Utils_Array::value('check_number', $entities['payment']),
245 'receive_date' => $entities['payment']['trxn_date'],
246 'paidBy' => CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_FinancialTrxn', 'payment_instrument_id', $entities['payment']['payment_instrument_id']),
247 'isShowLocation' => (!empty($entities['event']) ? $entities['event']['is_show_location'] : FALSE),
248 'location' => CRM_Utils_Array::value('location', $entities),
249 'event' => CRM_Utils_Array::value('event', $entities),
250 'component' => (!empty($entities['event']) ? 'event' : 'contribution'),
251 'isRefund' => $entities['payment']['total_amount'] < 0,
252 'isAmountzero' => $entities['payment']['total_amount'] === 0,
253 'refundAmount' => ($entities['payment']['total_amount'] < 0 ? $entities['payment']['total_amount'] : NULL),
254 'paymentsComplete' => ($entities['payment']['balance'] == 0),
255 ];
256
257 return self::filterUntestedTemplateVariables($templateVariables);
258 }
259
260 /**
261 * Filter out any untested variables.
262 *
263 * This just serves to highlight if any variables are added without a unit test also being added.
264 *
265 * (if hit then add a unit test for the param & add to this array).
266 *
267 * @param array $params
268 *
269 * @return array
270 */
271 public static function filterUntestedTemplateVariables($params) {
272 $testedTemplateVariables = [
273 'contactDisplayName',
274 'totalAmount',
275 'amountOwed',
276 'paymentAmount',
277 'event',
278 'component',
279 'checkNumber',
280 'receive_date',
281 'paidBy',
282 'isShowLocation',
283 'location',
284 'isRefund',
285 'isAmountzero',
286 'refundAmount',
287 'totalPaid',
288 'paymentsComplete',
289 'emailGreeting',
290 ];
291 // These are assigned by the payment form - they still 'get through' from the
292 // form for now without being in here but we should ideally load
293 // and assign. Note we should update the tpl to use {if $billingName}
294 // and ditch contributeMode - although it might need to be deprecated rather than removed.
295 $todoParams = [
296 'contributeMode',
297 'billingName',
298 'address',
299 'credit_card_type',
300 'credit_card_number',
301 'credit_card_exp_date',
302 ];
303 $filteredParams = [];
304 foreach ($testedTemplateVariables as $templateVariable) {
305 // This will cause an a-notice if any are NOT set - by design. Ensuring
306 // they are set prevents leakage.
307 $filteredParams[$templateVariable] = $params[$templateVariable];
308 }
309 return $filteredParams;
310 }
311
312 /**
313 * @param $contributionId
314 * @param $trxnData
315 * @param $updateStatus
316 * - deprecate this param
317 *
318 * @todo - make this protected once recordAdditionalPayment no longer calls it.
319 *
320 * @return CRM_Financial_DAO_FinancialTrxn
321 */
322 public static function recordRefundPayment($contributionId, $trxnData, $updateStatus) {
323 list($contributionDAO, $params) = self::getContributionAndParamsInFormatForRecordFinancialTransaction($contributionId);
324
325 $params['payment_instrument_id'] = CRM_Utils_Array::value('payment_instrument_id', $trxnData, CRM_Utils_Array::value('payment_instrument_id', $params));
326
327 $paidStatus = CRM_Core_PseudoConstant::getKey('CRM_Financial_DAO_FinancialItem', 'status_id', 'Paid');
328 $arAccountId = CRM_Contribute_PseudoConstant::getRelationalFinancialAccount($contributionDAO->financial_type_id, 'Accounts Receivable Account is');
329 $completedStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
330
331 $trxnData['total_amount'] = $trxnData['net_amount'] = $trxnData['total_amount'];
332 $trxnData['from_financial_account_id'] = $arAccountId;
333 $trxnData['status_id'] = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Refunded');
334 // record the entry
335 $financialTrxn = CRM_Contribute_BAO_Contribution::recordFinancialAccounts($params, $trxnData);
336
337 // note : not using the self::add method,
338 // the reason because it performs 'status change' related code execution for financial records
339 // which in 'Pending Refund' => 'Completed' is not useful, instead specific financial record updates
340 // are coded below i.e. just updating financial_item status to 'Paid'
341 if ($updateStatus) {
342 CRM_Core_DAO::setFieldValue('CRM_Contribute_BAO_Contribution', $contributionId, 'contribution_status_id', $completedStatusId);
343 }
344 // add financial item entry
345 $lineItems = CRM_Price_BAO_LineItem::getLineItemsByContributionID($contributionDAO->id);
346 if (!empty($lineItems)) {
347 foreach ($lineItems as $lineItemId => $lineItemValue) {
348 // don't record financial item for cancelled line-item
349 if ($lineItemValue['qty'] == 0) {
350 continue;
351 }
352 $paid = $lineItemValue['line_total'] * ($financialTrxn->total_amount / $contributionDAO->total_amount);
353 $addFinancialEntry = [
354 'transaction_date' => $financialTrxn->trxn_date,
355 'contact_id' => $contributionDAO->contact_id,
356 'amount' => round($paid, 2),
357 'currency' => $contributionDAO->currency,
358 'status_id' => $paidStatus,
359 'entity_id' => $lineItemId,
360 'entity_table' => 'civicrm_line_item',
361 ];
362 $trxnIds = ['id' => $financialTrxn->id];
363 CRM_Financial_BAO_FinancialItem::create($addFinancialEntry, NULL, $trxnIds);
364 }
365 }
366 return $financialTrxn;
367 }
368
369 /**
370 * @param int $contributionId
371 * @param array $trxnData
372 * @param int $participantId
373 *
374 * @return \CRM_Core_BAO_FinancialTrxn
375 */
376 public static function recordPayment($contributionId, $trxnData, $participantId) {
377 list($contributionDAO, $params) = self::getContributionAndParamsInFormatForRecordFinancialTransaction($contributionId);
378
379 $trxnData['trxn_date'] = !empty($trxnData['trxn_date']) ? $trxnData['trxn_date'] : date('YmdHis');
380 $params['payment_instrument_id'] = CRM_Utils_Array::value('payment_instrument_id', $trxnData, CRM_Utils_Array::value('payment_instrument_id', $params));
381
382 $paidStatus = CRM_Core_PseudoConstant::getKey('CRM_Financial_DAO_FinancialItem', 'status_id', 'Paid');
383 $arAccountId = CRM_Contribute_PseudoConstant::getRelationalFinancialAccount($contributionDAO->financial_type_id, 'Accounts Receivable Account is');
384 $completedStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
385
386 $params['partial_payment_total'] = $contributionDAO->total_amount;
387 $params['partial_amount_to_pay'] = $trxnData['total_amount'];
388 $trxnData['net_amount'] = !empty($trxnData['net_amount']) ? $trxnData['net_amount'] : $trxnData['total_amount'];
389 $params['pan_truncation'] = CRM_Utils_Array::value('pan_truncation', $trxnData);
390 $params['card_type_id'] = CRM_Utils_Array::value('card_type_id', $trxnData);
391 $params['check_number'] = CRM_Utils_Array::value('check_number', $trxnData);
392
393 // record the entry
394 $financialTrxn = CRM_Contribute_BAO_Contribution::recordFinancialAccounts($params, $trxnData);
395 $toFinancialAccount = $arAccountId;
396 $trxnId = CRM_Core_BAO_FinancialTrxn::getBalanceTrxnAmt($contributionId, $contributionDAO->financial_type_id);
397 if (!empty($trxnId)) {
398 $trxnId = $trxnId['trxn_id'];
399 }
400 elseif (!empty($contributionDAO->payment_instrument_id)) {
401 $trxnId = CRM_Financial_BAO_FinancialTypeAccount::getInstrumentFinancialAccount($contributionDAO->payment_instrument_id);
402 }
403 else {
404 $relationTypeId = key(CRM_Core_PseudoConstant::accountOptionValues('financial_account_type', NULL, " AND v.name LIKE 'Asset' "));
405 $queryParams = [1 => [$relationTypeId, 'Integer']];
406 $trxnId = CRM_Core_DAO::singleValueQuery("SELECT id FROM civicrm_financial_account WHERE is_default = 1 AND financial_account_type_id = %1", $queryParams);
407 }
408
409 // update statuses
410 // criteria for updates contribution total_amount == financial_trxns of partial_payments
411 $sql = "SELECT SUM(ft.total_amount) as sum_of_payments, SUM(ft.net_amount) as net_amount_total
412 FROM civicrm_financial_trxn ft
413 LEFT JOIN civicrm_entity_financial_trxn eft
414 ON (ft.id = eft.financial_trxn_id)
415 WHERE eft.entity_table = 'civicrm_contribution'
416 AND eft.entity_id = {$contributionId}
417 AND ft.to_financial_account_id != {$toFinancialAccount}
418 AND ft.status_id = {$completedStatusId}
419 ";
420 $query = CRM_Core_DAO::executeQuery($sql);
421 $query->fetch();
422 $sumOfPayments = $query->sum_of_payments;
423
424 // update statuses
425 if ($contributionDAO->total_amount == $sumOfPayments) {
426 // update contribution status and
427 // clean cancel info (if any) if prev. contribution was updated in case of 'Refunded' => 'Completed'
428 $contributionDAO->contribution_status_id = $completedStatusId;
429 $contributionDAO->cancel_date = 'null';
430 $contributionDAO->cancel_reason = NULL;
431 $netAmount = !empty($trxnData['net_amount']) ? NULL : $trxnData['total_amount'];
432 $contributionDAO->net_amount = $query->net_amount_total + $netAmount;
433 $contributionDAO->fee_amount = $contributionDAO->total_amount - $contributionDAO->net_amount;
434 $contributionDAO->save();
435
436 //Change status of financial record too
437 $financialTrxn->status_id = $completedStatusId;
438 $financialTrxn->save();
439
440 // note : not using the self::add method,
441 // the reason because it performs 'status change' related code execution for financial records
442 // which in 'Partial Paid' => 'Completed' is not useful, instead specific financial record updates
443 // are coded below i.e. just updating financial_item status to 'Paid'
444
445 if (!$participantId) {
446 $participantId = CRM_Core_DAO::getFieldValue('CRM_Event_DAO_ParticipantPayment', $contributionId, 'participant_id', 'contribution_id');
447 }
448 if ($participantId) {
449 // update participant status
450 $participantStatuses = CRM_Event_PseudoConstant::participantStatus();
451 $ids = CRM_Event_BAO_Participant::getParticipantIds($contributionId);
452 foreach ($ids as $val) {
453 $participantUpdate['id'] = $val;
454 $participantUpdate['status_id'] = array_search('Registered', $participantStatuses);
455 CRM_Event_BAO_Participant::add($participantUpdate);
456 }
457 }
458
459 // Remove this - completeOrder does it.
460 CRM_Contribute_BAO_Contribution::updateMembershipBasedOnCompletionOfContribution(
461 $contributionDAO,
462 $contributionId,
463 $trxnData['trxn_date']
464 );
465
466 // update financial item statuses
467 $baseTrxnId = CRM_Core_BAO_FinancialTrxn::getFinancialTrxnId($contributionId);
468 $sqlFinancialItemUpdate = "
469 UPDATE civicrm_financial_item fi
470 LEFT JOIN civicrm_entity_financial_trxn eft
471 ON (eft.entity_id = fi.id AND eft.entity_table = 'civicrm_financial_item')
472 SET status_id = {$paidStatus}
473 WHERE eft.financial_trxn_id IN ({$trxnId}, {$baseTrxnId['financialTrxnId']})
474 ";
475 CRM_Core_DAO::executeQuery($sqlFinancialItemUpdate);
476 }
477 return $financialTrxn;
478 }
479
480 /**
481 * The recordFinancialTransactions function has capricious requirements for input parameters - load them.
482 *
483 * The function needs rework but for now we need to give it what it wants.
484 *
485 * @param int $contributionId
486 *
487 * @return array
488 */
489 protected static function getContributionAndParamsInFormatForRecordFinancialTransaction($contributionId) {
490 $getInfoOf['id'] = $contributionId;
491 $defaults = [];
492 $contributionDAO = CRM_Contribute_BAO_Contribution::retrieve($getInfoOf, $defaults, CRM_Core_DAO::$_nullArray);
493
494 // build params for recording financial trxn entry
495 $params['contribution'] = $contributionDAO;
496 $params = array_merge($defaults, $params);
497 $params['skipLineItem'] = TRUE;
498 return [$contributionDAO, $params];
499 }
500
501 /**
502 * Does this payment complete the contribution
503 *
504 * @param int $contributionID
505 * @param float $paymentAmount
506 *
507 * @return bool
508 */
509 protected static function isPaymentCompletesContribution($contributionID, $paymentAmount) {
510 $outstandingBalance = CRM_Contribute_BAO_Contribution::getContributionBalance($contributionID);
511 $cmp = bccomp($paymentAmount, $outstandingBalance, 5);
512 return ($cmp == 0 || $cmp == 1);
513 }
514
515 }