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