3 +--------------------------------------------------------------------+
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2019 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
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. |
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. |
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 +--------------------------------------------------------------------+
31 * @copyright CiviCRM LLC (c) 2004-2019
35 * This class contains payment related functions.
37 class CRM_Financial_BAO_Payment
{
40 * Function to process additional payment for partial and refund contributions.
42 * This function is called via API payment.create function. All forms that add payments
45 * @param array $params
50 * @return \CRM_Financial_DAO_FinancialTrxn
52 * @throws \API_Exception
53 * @throws \CRM_Core_Exception
55 public static function create($params) {
56 $contribution = civicrm_api3('Contribution', 'getsingle', ['id' => $params['contribution_id']]);
57 $contributionStatus = CRM_Contribute_PseudoConstant
::contributionStatus($contribution['contribution_status_id'], 'name');
59 $isPaymentCompletesContribution = self
::isPaymentCompletesContribution($params['contribution_id'], $params['total_amount']);
61 // For legacy reasons Pending payments are completed through completetransaction.
62 // @todo completetransaction should transition components but financial transactions
63 // should be handled through Payment.create.
64 $isSkipRecordingPaymentHereForLegacyHandlingReasons = ($contributionStatus == 'Pending' && $isPaymentCompletesContribution);
66 if (!$isSkipRecordingPaymentHereForLegacyHandlingReasons && $params['total_amount'] > 0) {
67 $trxn = CRM_Contribute_BAO_Contribution
::recordPartialPayment($contribution, $params);
69 if (CRM_Utils_Array
::value('line_item', $params) && !empty($trxn)) {
70 foreach ($params['line_item'] as $values) {
71 foreach ($values as $id => $amount) {
73 $check = CRM_Price_BAO_LineItem
::retrieve($p, $defaults);
75 throw new API_Exception('Please specify a valid Line Item.');
79 FROM civicrm_financial_item fi
80 INNER JOIN civicrm_line_item li ON li.id = fi.entity_id and fi.entity_table = 'civicrm_line_item'
81 WHERE li.contribution_id = %1 AND li.id = %2";
83 1 => [$params['contribution_id'], 'Integer'],
84 2 => [$id, 'Integer'],
86 $fid = CRM_Core_DAO
::singleValueQuery($sql, $sqlParams);
87 // Record Entity Financial Trxn
89 'entity_table' => 'civicrm_financial_item',
90 'financial_trxn_id' => $trxn->id
,
94 civicrm_api3('EntityFinancialTrxn', 'create', $eftParams);
98 elseif (!empty($trxn)) {
99 CRM_Contribute_BAO_Contribution
::assignProportionalLineItems($params, $trxn->id
, $contribution['total_amount']);
102 elseif ($params['total_amount'] < 0) {
103 $trxn = self
::recordRefundPayment($params['contribution_id'], $params, FALSE);
106 if ($isPaymentCompletesContribution) {
107 civicrm_api3('Contribution', 'completetransaction', ['id' => $contribution['id']]);
109 $trxnId = CRM_Core_BAO_FinancialTrxn
::getFinancialTrxnId($contribution['id'], 'DESC');
110 $ftParams = ['id' => $trxnId['financialTrxnId']];
111 $trxn = CRM_Core_BAO_FinancialTrxn
::retrieve($ftParams, CRM_Core_DAO
::$_nullArray);
113 elseif ($contributionStatus === 'Pending') {
114 civicrm_api3('Contribution', 'create',
116 'id' => $contribution['id'],
117 'contribution_status_id' => 'Partially paid',
126 * Send an email confirming a payment that has been received.
128 * @param array $params
132 public static function sendConfirmation($params) {
134 $entities = self
::loadRelatedEntities($params['id']);
135 $sendTemplateParams = [
136 'groupName' => 'msg_tpl_workflow_contribution',
137 'valueName' => 'payment_or_refund_notification',
138 'PDFFilename' => ts('notification') . '.pdf',
139 'contactId' => $entities['contact']['id'],
140 'toName' => $entities['contact']['display_name'],
141 'toEmail' => $entities['contact']['email'],
142 'tplParams' => self
::getConfirmationTemplateParameters($entities),
144 return CRM_Core_BAO_MessageTemplate
::sendTemplate($sendTemplateParams);
148 * Load entities related to the current payment id.
150 * This gives us all the data we need to send an email confirmation but avoiding
151 * getting anything not tested for the confirmations. We retrieve the 'full' event as
152 * it has been traditionally assigned in full.
157 * - contact = ['id' => x, 'display_name' => y, 'email' => z]
158 * - event = [.... full event details......]
159 * - contribution = ['id' => x],
160 * - payment = [payment info + payment summary info]
162 protected static function loadRelatedEntities($id) {
164 $contributionID = (int) civicrm_api3('EntityFinancialTrxn', 'getvalue', [
165 'financial_trxn_id' => $id,
166 'entity_table' => 'civicrm_contribution',
167 'return' => 'entity_id',
169 $entities['contribution'] = ['id' => $contributionID];
170 $entities['payment'] = array_merge(civicrm_api3('FinancialTrxn', 'getsingle', ['id' => $id]),
171 CRM_Contribute_BAO_Contribution
::getPaymentInfo($contributionID)
174 $contactID = self
::getPaymentContactID($contributionID);
175 list($displayName, $email) = CRM_Contact_BAO_Contact_Location
::getEmailDetails($contactID);
176 $entities['contact'] = ['id' => $contactID, 'display_name' => $displayName, 'email' => $email];
177 $contact = civicrm_api3('Contact', 'getsingle', ['id' => $contactID, 'return' => 'email_greeting']);
178 $entities['contact']['email_greeting'] = $contact['email_greeting_display'];
180 $participantRecords = civicrm_api3('ParticipantPayment', 'get', [
181 'contribution_id' => $contributionID,
182 'api.Participant.get' => ['return' => 'event_id'],
185 if (!empty($participantRecords)) {
186 $entities['event'] = civicrm_api3('Event', 'getsingle', ['id' => $participantRecords[0]['api.Participant.get']['values'][0]['event_id']]);
187 if (!empty($entities['event']['is_show_location'])) {
189 'entity_id' => $entities['event']['id'],
190 'entity_table' => 'civicrm_event',
192 $entities['location'] = CRM_Core_BAO_Location
::getValues($locationParams, TRUE);
200 * @param int $contributionID
204 public static function getPaymentContactID($contributionID) {
205 $contribution = civicrm_api3('Contribution', 'getsingle', [
206 'id' => $contributionID ,
207 'return' => ['contact_id'],
209 return (int) $contribution['contact_id'];
213 * @param array $entities
214 * Related entities as an array keyed by the various entities.
217 * Values required for the notification
219 * - template_variables
220 * - event (DAO of event if relevant)
222 public static function getConfirmationTemplateParameters($entities) {
223 $templateVariables = [
224 'contactDisplayName' => $entities['contact']['display_name'],
225 'emailGreeting' => $entities['contact']['email_greeting'],
226 'totalAmount' => $entities['payment']['total'],
227 'amountOwed' => $entities['payment']['balance'],
228 'totalPaid' => $entities['payment']['paid'],
229 'paymentAmount' => $entities['payment']['total_amount'],
230 'checkNumber' => CRM_Utils_Array
::value('check_number', $entities['payment']),
231 'receive_date' => $entities['payment']['trxn_date'],
232 'paidBy' => CRM_Core_PseudoConstant
::getLabel('CRM_Core_BAO_FinancialTrxn', 'payment_instrument_id', $entities['payment']['payment_instrument_id']),
233 'isShowLocation' => (!empty($entities['event']) ?
$entities['event']['is_show_location'] : FALSE),
234 'location' => CRM_Utils_Array
::value('location', $entities),
235 'event' => CRM_Utils_Array
::value('event', $entities),
236 'component' => (!empty($entities['event']) ?
'event' : 'contribution'),
237 'isRefund' => $entities['payment']['total_amount'] < 0,
238 'isAmountzero' => $entities['payment']['total_amount'] === 0,
239 'refundAmount' => ($entities['payment']['total_amount'] < 0 ?
$entities['payment']['total_amount'] : NULL),
240 'paymentsComplete' => ($entities['payment']['balance'] == 0),
243 return self
::filterUntestedTemplateVariables($templateVariables);
247 * Filter out any untested variables.
249 * This just serves to highlight if any variables are added without a unit test also being added.
251 * (if hit then add a unit test for the param & add to this array).
253 * @param array $params
257 public static function filterUntestedTemplateVariables($params) {
258 $testedTemplateVariables = [
259 'contactDisplayName',
277 // These are assigned by the payment form - they still 'get through' from the
278 // form for now without being in here but we should ideally load
279 // and assign. Note we should update the tpl to use {if $billingName}
280 // and ditch contributeMode - although it might need to be deprecated rather than removed.
286 'credit_card_number',
287 'credit_card_exp_date',
289 $filteredParams = [];
290 foreach ($testedTemplateVariables as $templateVariable) {
291 // This will cause an a-notice if any are NOT set - by design. Ensuring
292 // they are set prevents leakage.
293 $filteredParams[$templateVariable] = $params[$templateVariable];
295 return $filteredParams;
299 * @param $contributionId
301 * @param $updateStatus
302 * - deprecate this param
304 * @todo - make this protected once recordAdditionalPayment no longer calls it.
306 * @return CRM_Financial_DAO_FinancialTrxn
308 public static function recordRefundPayment($contributionId, $trxnData, $updateStatus) {
309 list($contributionDAO, $params) = self
::getContributionAndParamsInFormatForRecordFinancialTransaction($contributionId);
311 $params['payment_instrument_id'] = CRM_Utils_Array
::value('payment_instrument_id', $trxnData, CRM_Utils_Array
::value('payment_instrument_id', $params));
313 $paidStatus = CRM_Core_PseudoConstant
::getKey('CRM_Financial_DAO_FinancialItem', 'status_id', 'Paid');
314 $arAccountId = CRM_Contribute_PseudoConstant
::getRelationalFinancialAccount($contributionDAO->financial_type_id
, 'Accounts Receivable Account is');
315 $completedStatusId = CRM_Core_PseudoConstant
::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
317 $trxnData['total_amount'] = $trxnData['net_amount'] = $trxnData['total_amount'];
318 $trxnData['from_financial_account_id'] = $arAccountId;
319 $trxnData['status_id'] = CRM_Core_PseudoConstant
::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Refunded');
321 $financialTrxn = CRM_Contribute_BAO_Contribution
::recordFinancialAccounts($params, $trxnData);
323 // note : not using the self::add method,
324 // the reason because it performs 'status change' related code execution for financial records
325 // which in 'Pending Refund' => 'Completed' is not useful, instead specific financial record updates
326 // are coded below i.e. just updating financial_item status to 'Paid'
328 CRM_Core_DAO
::setFieldValue('CRM_Contribute_BAO_Contribution', $contributionId, 'contribution_status_id', $completedStatusId);
330 // add financial item entry
331 $lineItems = CRM_Price_BAO_LineItem
::getLineItemsByContributionID($contributionDAO->id
);
332 if (!empty($lineItems)) {
333 foreach ($lineItems as $lineItemId => $lineItemValue) {
334 // don't record financial item for cancelled line-item
335 if ($lineItemValue['qty'] == 0) {
338 $paid = $lineItemValue['line_total'] * ($financialTrxn->total_amount
/ $contributionDAO->total_amount
);
339 $addFinancialEntry = [
340 'transaction_date' => $financialTrxn->trxn_date
,
341 'contact_id' => $contributionDAO->contact_id
,
342 'amount' => round($paid, 2),
343 'currency' => $contributionDAO->currency
,
344 'status_id' => $paidStatus,
345 'entity_id' => $lineItemId,
346 'entity_table' => 'civicrm_line_item',
348 $trxnIds = ['id' => $financialTrxn->id
];
349 CRM_Financial_BAO_FinancialItem
::create($addFinancialEntry, NULL, $trxnIds);
352 return $financialTrxn;
356 * @param int $contributionId
357 * @param array $trxnData
358 * @param int $participantId
360 * @return \CRM_Core_BAO_FinancialTrxn
362 public static function recordPayment($contributionId, $trxnData, $participantId) {
363 list($contributionDAO, $params) = self
::getContributionAndParamsInFormatForRecordFinancialTransaction($contributionId);
365 $trxnData['trxn_date'] = !empty($trxnData['trxn_date']) ?
$trxnData['trxn_date'] : date('YmdHis');
366 $params['payment_instrument_id'] = CRM_Utils_Array
::value('payment_instrument_id', $trxnData, CRM_Utils_Array
::value('payment_instrument_id', $params));
368 $paidStatus = CRM_Core_PseudoConstant
::getKey('CRM_Financial_DAO_FinancialItem', 'status_id', 'Paid');
369 $arAccountId = CRM_Contribute_PseudoConstant
::getRelationalFinancialAccount($contributionDAO->financial_type_id
, 'Accounts Receivable Account is');
370 $completedStatusId = CRM_Core_PseudoConstant
::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
372 $params['partial_payment_total'] = $contributionDAO->total_amount
;
373 $params['partial_amount_to_pay'] = $trxnData['total_amount'];
374 $trxnData['net_amount'] = !empty($trxnData['net_amount']) ?
$trxnData['net_amount'] : $trxnData['total_amount'];
375 $params['pan_truncation'] = CRM_Utils_Array
::value('pan_truncation', $trxnData);
376 $params['card_type_id'] = CRM_Utils_Array
::value('card_type_id', $trxnData);
377 $params['check_number'] = CRM_Utils_Array
::value('check_number', $trxnData);
380 $financialTrxn = CRM_Contribute_BAO_Contribution
::recordFinancialAccounts($params, $trxnData);
381 $toFinancialAccount = $arAccountId;
382 $trxnId = CRM_Core_BAO_FinancialTrxn
::getBalanceTrxnAmt($contributionId, $contributionDAO->financial_type_id
);
383 if (!empty($trxnId)) {
384 $trxnId = $trxnId['trxn_id'];
386 elseif (!empty($contributionDAO->payment_instrument_id
)) {
387 $trxnId = CRM_Financial_BAO_FinancialTypeAccount
::getInstrumentFinancialAccount($contributionDAO->payment_instrument_id
);
390 $relationTypeId = key(CRM_Core_PseudoConstant
::accountOptionValues('financial_account_type', NULL, " AND v.name LIKE 'Asset' "));
391 $queryParams = [1 => [$relationTypeId, 'Integer']];
392 $trxnId = CRM_Core_DAO
::singleValueQuery("SELECT id FROM civicrm_financial_account WHERE is_default = 1 AND financial_account_type_id = %1", $queryParams);
396 // criteria for updates contribution total_amount == financial_trxns of partial_payments
397 $sql = "SELECT SUM(ft.total_amount) as sum_of_payments, SUM(ft.net_amount) as net_amount_total
398 FROM civicrm_financial_trxn ft
399 LEFT JOIN civicrm_entity_financial_trxn eft
400 ON (ft.id = eft.financial_trxn_id)
401 WHERE eft.entity_table = 'civicrm_contribution'
402 AND eft.entity_id = {$contributionId}
403 AND ft.to_financial_account_id != {$toFinancialAccount}
404 AND ft.status_id = {$completedStatusId}
406 $query = CRM_Core_DAO
::executeQuery($sql);
408 $sumOfPayments = $query->sum_of_payments
;
411 if ($contributionDAO->total_amount
== $sumOfPayments) {
412 // update contribution status and
413 // clean cancel info (if any) if prev. contribution was updated in case of 'Refunded' => 'Completed'
414 $contributionDAO->contribution_status_id
= $completedStatusId;
415 $contributionDAO->cancel_date
= 'null';
416 $contributionDAO->cancel_reason
= NULL;
417 $netAmount = !empty($trxnData['net_amount']) ?
NULL : $trxnData['total_amount'];
418 $contributionDAO->net_amount
= $query->net_amount_total +
$netAmount;
419 $contributionDAO->fee_amount
= $contributionDAO->total_amount
- $contributionDAO->net_amount
;
420 $contributionDAO->save();
422 //Change status of financial record too
423 $financialTrxn->status_id
= $completedStatusId;
424 $financialTrxn->save();
426 // note : not using the self::add method,
427 // the reason because it performs 'status change' related code execution for financial records
428 // which in 'Partial Paid' => 'Completed' is not useful, instead specific financial record updates
429 // are coded below i.e. just updating financial_item status to 'Paid'
431 if (!$participantId) {
432 $participantId = CRM_Core_DAO
::getFieldValue('CRM_Event_DAO_ParticipantPayment', $contributionId, 'participant_id', 'contribution_id');
434 if ($participantId) {
435 // update participant status
436 $participantStatuses = CRM_Event_PseudoConstant
::participantStatus();
437 $ids = CRM_Event_BAO_Participant
::getParticipantIds($contributionId);
438 foreach ($ids as $val) {
439 $participantUpdate['id'] = $val;
440 $participantUpdate['status_id'] = array_search('Registered', $participantStatuses);
441 CRM_Event_BAO_Participant
::add($participantUpdate);
445 // Remove this - completeOrder does it.
446 CRM_Contribute_BAO_Contribution
::updateMembershipBasedOnCompletionOfContribution(
449 $trxnData['trxn_date']
452 // update financial item statuses
453 $baseTrxnId = CRM_Core_BAO_FinancialTrxn
::getFinancialTrxnId($contributionId);
454 $sqlFinancialItemUpdate = "
455 UPDATE civicrm_financial_item fi
456 LEFT JOIN civicrm_entity_financial_trxn eft
457 ON (eft.entity_id = fi.id AND eft.entity_table = 'civicrm_financial_item')
458 SET status_id = {$paidStatus}
459 WHERE eft.financial_trxn_id IN ({$trxnId}, {$baseTrxnId['financialTrxnId']})
461 CRM_Core_DAO
::executeQuery($sqlFinancialItemUpdate);
463 return $financialTrxn;
467 * The recordFinancialTransactions function has capricious requirements for input parameters - load them.
469 * The function needs rework but for now we need to give it what it wants.
471 * @param int $contributionId
475 protected static function getContributionAndParamsInFormatForRecordFinancialTransaction($contributionId) {
476 $getInfoOf['id'] = $contributionId;
478 $contributionDAO = CRM_Contribute_BAO_Contribution
::retrieve($getInfoOf, $defaults, CRM_Core_DAO
::$_nullArray);
480 // build params for recording financial trxn entry
481 $params['contribution'] = $contributionDAO;
482 $params = array_merge($defaults, $params);
483 $params['skipLineItem'] = TRUE;
484 return [$contributionDAO, $params];
488 * Does this payment complete the contribution
490 * @param int $contributionID
491 * @param float $paymentAmount
495 protected static function isPaymentCompletesContribution($contributionID, $paymentAmount) {
496 $outstandingBalance = CRM_Contribute_BAO_Contribution
::getContributionBalance($contributionID);
497 $cmp = bccomp($paymentAmount, $outstandingBalance, 5);
498 return ($cmp == 0 ||
$cmp == 1);