Merge pull request #22584 from civicrm/5.46
[civicrm-core.git] / CRM / Contribute / BAO / FinancialProcessor.php
CommitLineData
538e521c
EM
1<?php
2/*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12/**
13 * Class for handling processing of financial records.
14 *
15 * This is a place to extract the financial record processing code to
16 * in order to clean it up.
17 *
18 * @internal core use only.
19 *
20 * @package CRM
21 * @copyright CiviCRM LLC https://civicrm.org/licensing
22 */
23class CRM_Contribute_BAO_FinancialProcessor {
24
25 /**
26 * Get the financial account for the item associated with the new transaction.
27 *
28 * @param array $params
29 * @param int $default
30 *
31 * @return int
32 */
c259e5f3 33 private static function getFinancialAccountForStatusChangeTrxn($params, $default): int {
538e521c
EM
34 if (!empty($params['financial_account_id'])) {
35 return $params['financial_account_id'];
36 }
37
38 $contributionStatus = CRM_Contribute_PseudoConstant::contributionStatus($params['contribution_status_id'], 'name');
39 $preferredAccountsRelationships = [
40 'Refunded' => 'Credit/Contra Revenue Account is',
41 'Chargeback' => 'Chargeback Account is',
42 ];
43
44 if (array_key_exists($contributionStatus, $preferredAccountsRelationships)) {
45 $financialTypeID = !empty($params['financial_type_id']) ? $params['financial_type_id'] : $params['prevContribution']->financial_type_id;
46 return CRM_Financial_BAO_FinancialAccount::getFinancialAccountForFinancialTypeByRelationship(
47 $financialTypeID,
48 $preferredAccountsRelationships[$contributionStatus]
49 );
50 }
51 return $default;
52 }
53
c259e5f3
EM
54 /**
55 * Create the financial items for the line.
56 *
57 * @param array $params
58 * @param string $context
59 * @param array $fields
60 * @param array $previousLineItems
61 * @param array $inputParams
62 * @param bool $isARefund
63 * @param array $trxnIds
64 * @param int $fieldId
65 *
66 * @internal
67 *
68 * @return array
69 */
62e0c841 70 private static function createFinancialItemsForLine($params, $context, $fields, array $previousLineItems, array $inputParams, bool $isARefund, $trxnIds, $fieldId): array {
c259e5f3
EM
71 foreach ($fields as $fieldValueId => $lineItemDetails) {
72 $prevFinancialItem = CRM_Financial_BAO_FinancialItem::getPreviousFinancialItem($lineItemDetails['id']);
73 $receiveDate = CRM_Utils_Date::isoToMysql($params['prevContribution']->receive_date);
74 if ($params['contribution']->receive_date) {
75 $receiveDate = CRM_Utils_Date::isoToMysql($params['contribution']->receive_date);
76 }
77
78 $financialAccount = CRM_Contribute_BAO_FinancialProcessor::getFinancialAccountForStatusChangeTrxn($params, CRM_Utils_Array::value('financial_account_id', $prevFinancialItem));
79
80 $currency = $params['prevContribution']->currency;
81 if ($params['contribution']->currency) {
82 $currency = $params['contribution']->currency;
83 }
84 $previousLineItemTotal = CRM_Utils_Array::value('line_total', CRM_Utils_Array::value($fieldValueId, $previousLineItems), 0);
85 $itemParams = [
86 'transaction_date' => $receiveDate,
87 'contact_id' => $params['prevContribution']->contact_id,
88 'currency' => $currency,
89 'amount' => self::getFinancialItemAmountFromParams($inputParams, $context, $lineItemDetails, $isARefund, $previousLineItemTotal),
90 'description' => $prevFinancialItem['description'] ?? NULL,
91 'status_id' => $prevFinancialItem['status_id'],
92 'financial_account_id' => $financialAccount,
93 'entity_table' => 'civicrm_line_item',
94 'entity_id' => $lineItemDetails['id'],
95 ];
96 $financialItem = CRM_Financial_BAO_FinancialItem::create($itemParams, NULL, $trxnIds);
97 $params['line_item'][$fieldId][$fieldValueId]['deferred_line_total'] = $itemParams['amount'];
98 $params['line_item'][$fieldId][$fieldValueId]['financial_item_id'] = $financialItem->id;
99
100 if (($lineItemDetails['tax_amount'] && $lineItemDetails['tax_amount'] !== 'null') || ($context === 'changeFinancialType')) {
101 $taxAmount = (float) $lineItemDetails['tax_amount'];
102 if ($context === 'changeFinancialType' && $lineItemDetails['tax_amount'] === 'null') {
103 // reverse the Sale Tax amount if there is no tax rate associated with new Financial Type
104 $taxAmount = CRM_Utils_Array::value('tax_amount', CRM_Utils_Array::value($fieldValueId, $previousLineItems), 0);
105 }
106 elseif ($previousLineItemTotal != $lineItemDetails['line_total']) {
107 $taxAmount -= CRM_Utils_Array::value('tax_amount', CRM_Utils_Array::value($fieldValueId, $previousLineItems), 0);
108 }
109 if ($taxAmount != 0) {
110 $itemParams['amount'] = CRM_Contribute_BAO_FinancialProcessor::getMultiplier($params['contribution']->contribution_status_id, $context) * $taxAmount;
111 $itemParams['description'] = CRM_Invoicing_Utils::getTaxTerm();
112 if ($lineItemDetails['financial_type_id']) {
113 $itemParams['financial_account_id'] = CRM_Financial_BAO_FinancialAccount::getSalesTaxFinancialAccount($lineItemDetails['financial_type_id']);
114 }
115 CRM_Financial_BAO_FinancialItem::create($itemParams, NULL, $trxnIds);
116 }
117 }
118 }
119 return $params;
120 }
121
122 /**
123 * Get the multiplier for adjusting rows.
124 *
125 * If we are dealing with a refund or cancellation then it will be a negative
126 * amount to reflect the negative transaction.
127 *
128 * If we are changing Financial Type it will be a negative amount to
129 * adjust down the old type.
130 *
131 * @param int $contribution_status_id
132 * @param string $context
133 *
134 * @return int
135 */
62e0c841 136 private static function getMultiplier($contribution_status_id, $context) {
c259e5f3
EM
137 if ($context === 'changeFinancialType' || CRM_Contribute_BAO_Contribution::isContributionStatusNegative($contribution_status_id)) {
138 return -1;
139 }
140 return 1;
141 }
142
143 /**
144 * Get the amount for the financial item row.
145 *
146 * Helper function to start to break down recordFinancialTransactions for readability.
147 *
148 * The logic is more historical than .. logical. Paths other than the deprecated one are tested.
149 *
150 * Codewise, several somewhat disimmilar things have been squished into recordFinancialAccounts
151 * for historical reasons. Going forwards we can hope to add tests & improve readibility
152 * of that function
153 *
154 * @param array $params
155 * Params as passed to contribution.create
156 *
157 * @param string $context
158 * changeFinancialType| changedAmount
159 * @param array $lineItemDetails
160 * Line items.
161 * @param bool $isARefund
162 * Is this a refund / negative transaction.
163 * @param int $previousLineItemTotal
164 *
165 * @return float
166 * @todo move recordFinancialAccounts & helper functions to their own class?
167 *
168 */
169 protected static function getFinancialItemAmountFromParams($params, $context, $lineItemDetails, $isARefund, $previousLineItemTotal) {
170 if ($context == 'changedAmount') {
171 $lineTotal = $lineItemDetails['line_total'];
172 if ($lineTotal != $previousLineItemTotal) {
173 $lineTotal -= $previousLineItemTotal;
174 }
175 return $lineTotal;
176 }
177 elseif ($context == 'changeFinancialType') {
178 return -$lineItemDetails['line_total'];
179 }
180 elseif ($context == 'changedStatus') {
181 $cancelledTaxAmount = 0;
182 if ($isARefund) {
183 $cancelledTaxAmount = CRM_Utils_Array::value('tax_amount', $lineItemDetails, '0.00');
184 }
185 return CRM_Contribute_BAO_FinancialProcessor::getMultiplier($params['contribution']->contribution_status_id, $context) * ((float) $lineItemDetails['line_total'] + (float) $cancelledTaxAmount);
186 }
187 elseif ($context === NULL) {
188 // erm, yes because? but, hey, it's tested.
189 return $lineItemDetails['line_total'];
190 }
191 else {
192 return CRM_Contribute_BAO_FinancialProcessor::getMultiplier($params['contribution']->contribution_status_id, $context) * ((float) $lineItemDetails['line_total']);
193 }
194 }
195
6a1dbda6
EM
196 /**
197 * Update all financial accounts entry.
198 *
199 * @param array $params
200 * Contribution object, line item array and params for trxn.
201 *
202 * @param string $context
203 * Update scenarios.
204 *
205 * @todo stop passing $params by reference. It is unclear the purpose of doing this &
206 * adds unpredictability.
207 *
208 */
209 public static function updateFinancialAccounts(&$params, $context = NULL) {
210 $inputParams = $params;
211 $isARefund = self::isContributionUpdateARefund($params['prevContribution']->contribution_status_id, $params['contribution']->contribution_status_id);
212
213 if ($context === 'changedAmount' || $context === 'changeFinancialType') {
214 // @todo we should stop passing $params by reference - splitting this out would be a step towards that.
215 $params['trxnParams']['total_amount'] = $params['trxnParams']['net_amount'] = ($params['total_amount'] - $params['prevContribution']->total_amount);
216 }
217
218 $trxn = CRM_Core_BAO_FinancialTrxn::create($params['trxnParams']);
219 // @todo we should stop passing $params by reference - splitting this out would be a step towards that.
220 $params['entity_id'] = $trxn->id;
221
222 $trxnIds['id'] = $params['entity_id'];
223 $previousLineItems = CRM_Price_BAO_LineItem::getLineItemsByContributionID($params['contribution']->id);
224 foreach ($params['line_item'] as $fieldId => $fields) {
225 $params = CRM_Contribute_BAO_FinancialProcessor::createFinancialItemsForLine($params, $context, $fields, $previousLineItems, $inputParams, $isARefund, $trxnIds, $fieldId);
226 }
227 }
228
229 /**
230 * Does this contribution status update represent a refund.
231 *
232 * @param int $previousContributionStatusID
233 * @param int $currentContributionStatusID
234 *
235 * @return bool
236 */
237 public static function isContributionUpdateARefund($previousContributionStatusID, $currentContributionStatusID): bool {
238 if ('Completed' !== CRM_Core_PseudoConstant::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', $previousContributionStatusID)) {
239 return FALSE;
240 }
241 return CRM_Contribute_BAO_Contribution::isContributionStatusNegative($currentContributionStatusID);
242 }
243
213cfa8a
EM
244 /**
245 * Do any accounting updates required as a result of a contribution status change.
246 *
247 * Currently we have a bit of a roundabout where adding a payment results in this being called &
248 * this may attempt to add a payment. We need to resolve that....
249 *
250 * The 'right' way to add payments or refunds is through the Payment.create api. That api
251 * then updates the contribution but this process should not also record another financial trxn.
252 * Currently we have weak detection fot that scenario & where it is detected the first returned
253 * value is FALSE - meaning 'do not continue'.
254 *
255 * We should also look at the fact that the calling function - updateFinancialAccounts
256 * bunches together some disparate processes rather than having separate appropriate
257 * functions.
258 *
259 * @param array $params
260 *
261 * @return bool
262 * Return indicates whether the updateFinancialAccounts function should continue.
263 */
264 public static function updateFinancialAccountsOnContributionStatusChange(&$params) {
265 $previousContributionStatus = CRM_Contribute_PseudoConstant::contributionStatus($params['prevContribution']->contribution_status_id, 'name');
266 $currentContributionStatus = CRM_Core_PseudoConstant::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', $params['contribution']->contribution_status_id);
267
268 if ((($previousContributionStatus === 'Partially paid' && $currentContributionStatus === 'Completed')
269 || ($previousContributionStatus === 'Pending refund' && $currentContributionStatus === 'Completed')
270 // This concept of pay_later as different to any other sort of pending is deprecated & it's unclear
271 // why it is here or where it is handled instead.
272 || ($previousContributionStatus === 'Pending' && $params['prevContribution']->is_pay_later == TRUE
273 && $currentContributionStatus === 'Partially paid'))
274 ) {
275 return FALSE;
276 }
277
278 if (CRM_Contribute_BAO_FinancialProcessor::isContributionUpdateARefund($params['prevContribution']->contribution_status_id, $params['contribution']->contribution_status_id)) {
279 // @todo we should stop passing $params by reference - splitting this out would be a step towards that.
280 $params['trxnParams']['total_amount'] = -$params['total_amount'];
281 }
282 elseif (($previousContributionStatus === 'Pending'
283 && $params['prevContribution']->is_pay_later) || $previousContributionStatus === 'In Progress'
284 ) {
285 $financialTypeID = !empty($params['financial_type_id']) ? $params['financial_type_id'] : $params['prevContribution']->financial_type_id;
286 $arAccountId = CRM_Contribute_PseudoConstant::getRelationalFinancialAccount($financialTypeID, 'Accounts Receivable Account is');
287
288 if ($currentContributionStatus === 'Cancelled') {
289 // @todo we should stop passing $params by reference - splitting this out would be a step towards that.
290 $params['trxnParams']['to_financial_account_id'] = $arAccountId;
291 $params['trxnParams']['total_amount'] = -$params['total_amount'];
292 }
293 else {
294 // @todo we should stop passing $params by reference - splitting this out would be a step towards that.
295 $params['trxnParams']['from_financial_account_id'] = $arAccountId;
296 }
297 }
298
299 if (($previousContributionStatus === 'Pending'
300 || $previousContributionStatus === 'In Progress')
301 && ($currentContributionStatus === 'Completed')
302 ) {
303 if (empty($params['line_item'])) {
304 //CRM-15296
305 //@todo - check with Joe regarding this situation - payment processors create pending transactions with no line items
306 // when creating recurring membership payment - there are 2 lines to comment out in contributionPageTest if fixed
307 // & this can be removed
308 return FALSE;
309 }
310 // @todo we should stop passing $params by reference - splitting this out would be a step towards that.
311 // This is an update so original currency if none passed in.
312 $params['trxnParams']['currency'] = CRM_Utils_Array::value('currency', $params, $params['prevContribution']->currency);
313
314 $transactionIDs[] = CRM_Contribute_BAO_FinancialProcessor::recordAlwaysAccountsReceivable($params['trxnParams'], $params);
315 $trxn = CRM_Core_BAO_FinancialTrxn::create($params['trxnParams']);
316 // @todo we should stop passing $params by reference - splitting this out would be a step towards that.
317 $params['entity_id'] = $transactionIDs[] = $trxn->id;
318
319 $sql = "SELECT id, amount FROM civicrm_financial_item WHERE entity_id = %1 and entity_table = 'civicrm_line_item'";
320
321 $entityParams = [
322 'entity_table' => 'civicrm_financial_item',
323 ];
324 foreach ($params['line_item'] as $fieldId => $fields) {
325 foreach ($fields as $fieldValueId => $lineItemDetails) {
326 self::updateFinancialItemForLineItemToPaid($lineItemDetails['id']);
327 $fparams = [
328 1 => [$lineItemDetails['id'], 'Integer'],
329 ];
330 $financialItem = CRM_Core_DAO::executeQuery($sql, $fparams);
331 while ($financialItem->fetch()) {
332 $entityParams['entity_id'] = $financialItem->id;
333 $entityParams['amount'] = $financialItem->amount;
334 foreach ($transactionIDs as $tID) {
335 $entityParams['financial_trxn_id'] = $tID;
336 CRM_Financial_BAO_FinancialItem::createEntityTrxn($entityParams);
337 }
338 }
339 }
340 }
341 return FALSE;
342 }
343 return TRUE;
344 }
345
346 /**
347 * Update all financial items related to the line item tto have a status of paid.
348 *
349 * @param int $lineItemID
350 */
351 private static function updateFinancialItemForLineItemToPaid($lineItemID) {
352 $fparams = [
353 1 => [
354 CRM_Core_PseudoConstant::getKey('CRM_Financial_BAO_FinancialItem', 'status_id', 'Paid'),
355 'Integer',
356 ],
357 2 => [$lineItemID, 'Integer'],
358 ];
359 $query = "UPDATE civicrm_financial_item SET status_id = %1 WHERE entity_id = %2 and entity_table = 'civicrm_line_item'";
360 CRM_Core_DAO::executeQuery($query, $fparams);
361 }
362
051549d5
EM
363 /**
364 * Create Accounts Receivable financial trxn entry for Completed Contribution.
365 *
366 * @param array $trxnParams
367 * Financial trxn params
368 * @param array $contributionParams
369 * Contribution Params
370 *
371 * @return null|int
372 */
373 public static function recordAlwaysAccountsReceivable(&$trxnParams, $contributionParams) {
374 if (!Civi::settings()->get('always_post_to_accounts_receivable')) {
375 return NULL;
376 }
377 $statusId = $contributionParams['contribution']->contribution_status_id;
378 $contributionStatuses = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name');
379 $contributionStatus = empty($statusId) ? NULL : $contributionStatuses[$statusId];
380 $previousContributionStatus = empty($contributionParams['prevContribution']) ? NULL : $contributionStatuses[$contributionParams['prevContribution']->contribution_status_id];
381 // Return if contribution status is not completed.
382 if (!($contributionStatus == 'Completed' && (empty($previousContributionStatus)
383 || (!empty($previousContributionStatus) && $previousContributionStatus == 'Pending'
384 && $contributionParams['prevContribution']->is_pay_later == 0
385 )))
386 ) {
387 return NULL;
388 }
389
390 $params = $trxnParams;
391 $financialTypeID = !empty($contributionParams['financial_type_id']) ? $contributionParams['financial_type_id'] : $contributionParams['prevContribution']->financial_type_id;
392 $arAccountId = CRM_Contribute_PseudoConstant::getRelationalFinancialAccount($financialTypeID, 'Accounts Receivable Account is');
393 $params['to_financial_account_id'] = $arAccountId;
394 $params['status_id'] = array_search('Pending', $contributionStatuses);
395 $params['is_payment'] = FALSE;
396 $trxn = CRM_Core_BAO_FinancialTrxn::create($params);
397 $trxnParams['from_financial_account_id'] = $params['to_financial_account_id'];
398 return $trxn->id;
399 }
400
213cfa8a
EM
401 /**
402 * Does this transaction reflect a payment instrument change.
403 *
404 * @param array $params
405 * @param array $pendingStatuses
406 *
407 * @return bool
408 */
409 public static function isPaymentInstrumentChange(&$params, $pendingStatuses) {
410 $contributionStatus = CRM_Core_PseudoConstant::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', $params['contribution']->contribution_status_id);
411
412 if (array_key_exists('payment_instrument_id', $params)) {
413 if (CRM_Utils_System::isNull($params['prevContribution']->payment_instrument_id) &&
414 !CRM_Utils_System::isNull($params['payment_instrument_id'])
415 ) {
416 //check if status is changed from Pending to Completed
417 // do not update payment instrument changes for Pending to Completed
418 if (!($contributionStatus == 'Completed' &&
419 in_array($params['prevContribution']->contribution_status_id, $pendingStatuses))
420 ) {
421 return TRUE;
422 }
423 }
424 elseif ((!CRM_Utils_System::isNull($params['payment_instrument_id']) &&
425 !CRM_Utils_System::isNull($params['prevContribution']->payment_instrument_id)) &&
426 $params['payment_instrument_id'] != $params['prevContribution']->payment_instrument_id
427 ) {
428 return TRUE;
429 }
430 elseif (!CRM_Utils_System::isNull($params['contribution']->check_number) &&
431 $params['contribution']->check_number != $params['prevContribution']->check_number
432 ) {
433 // another special case when check number is changed, create new financial records
434 // create financial trxn with negative amount
435 return TRUE;
436 }
437 }
438 return FALSE;
439 }
440
538e521c 441}