3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 +--------------------------------------------------------------------+
13 * Class for handling processing of financial records.
15 * This is a place to extract the financial record processing code to
16 * in order to clean it up.
18 * @internal core use only.
21 * @copyright CiviCRM LLC https://civicrm.org/licensing
23 class CRM_Contribute_BAO_FinancialProcessor
{
26 * Get the financial account for the item associated with the new transaction.
28 * @param array $params
33 private static function getFinancialAccountForStatusChangeTrxn($params, $default): int {
34 if (!empty($params['financial_account_id'])) {
35 return $params['financial_account_id'];
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',
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(
48 $preferredAccountsRelationships[$contributionStatus]
55 * Create the financial items for the line.
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
70 private static function createFinancialItemsForLine($params, $context, $fields, array $previousLineItems, array $inputParams, bool $isARefund, $trxnIds, $fieldId): array {
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
);
78 $financialAccount = CRM_Contribute_BAO_FinancialProcessor
::getFinancialAccountForStatusChangeTrxn($params, CRM_Utils_Array
::value('financial_account_id', $prevFinancialItem));
80 $currency = $params['prevContribution']->currency
;
81 if ($params['contribution']->currency
) {
82 $currency = $params['contribution']->currency
;
84 $previousLineItemTotal = CRM_Utils_Array
::value('line_total', CRM_Utils_Array
::value($fieldValueId, $previousLineItems), 0);
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'],
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
;
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);
106 elseif ($previousLineItemTotal != $lineItemDetails['line_total']) {
107 $taxAmount -= CRM_Utils_Array
::value('tax_amount', CRM_Utils_Array
::value($fieldValueId, $previousLineItems), 0);
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']);
115 CRM_Financial_BAO_FinancialItem
::create($itemParams, NULL, $trxnIds);
123 * Get the multiplier for adjusting rows.
125 * If we are dealing with a refund or cancellation then it will be a negative
126 * amount to reflect the negative transaction.
128 * If we are changing Financial Type it will be a negative amount to
129 * adjust down the old type.
131 * @param int $contribution_status_id
132 * @param string $context
136 private static function getMultiplier($contribution_status_id, $context) {
137 if ($context === 'changeFinancialType' || CRM_Contribute_BAO_Contribution
::isContributionStatusNegative($contribution_status_id)) {
144 * Get the amount for the financial item row.
146 * Helper function to start to break down recordFinancialTransactions for readability.
148 * The logic is more historical than .. logical. Paths other than the deprecated one are tested.
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
154 * @param array $params
155 * Params as passed to contribution.create
157 * @param string $context
158 * changeFinancialType| changedAmount
159 * @param array $lineItemDetails
161 * @param bool $isARefund
162 * Is this a refund / negative transaction.
163 * @param int $previousLineItemTotal
166 * @todo move recordFinancialAccounts & helper functions to their own class?
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;
177 elseif ($context == 'changeFinancialType') {
178 return -$lineItemDetails['line_total'];
180 elseif ($context == 'changedStatus') {
181 $cancelledTaxAmount = 0;
183 $cancelledTaxAmount = CRM_Utils_Array
::value('tax_amount', $lineItemDetails, '0.00');
185 return CRM_Contribute_BAO_FinancialProcessor
::getMultiplier($params['contribution']->contribution_status_id
, $context) * ((float) $lineItemDetails['line_total'] +
(float) $cancelledTaxAmount);
187 elseif ($context === NULL) {
188 // erm, yes because? but, hey, it's tested.
189 return $lineItemDetails['line_total'];
192 return CRM_Contribute_BAO_FinancialProcessor
::getMultiplier($params['contribution']->contribution_status_id
, $context) * ((float) $lineItemDetails['line_total']);
197 * Update all financial accounts entry.
199 * @param array $params
200 * Contribution object, line item array and params for trxn.
202 * @param string $context
205 * @todo stop passing $params by reference. It is unclear the purpose of doing this &
206 * adds unpredictability.
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
);
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
);
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
;
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);
230 * Does this contribution status update represent a refund.
232 * @param int $previousContributionStatusID
233 * @param int $currentContributionStatusID
237 public static function isContributionUpdateARefund($previousContributionStatusID, $currentContributionStatusID): bool {
238 if ('Completed' !== CRM_Core_PseudoConstant
::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', $previousContributionStatusID)) {
241 return CRM_Contribute_BAO_Contribution
::isContributionStatusNegative($currentContributionStatusID);
245 * Do any accounting updates required as a result of a contribution status change.
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....
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'.
255 * We should also look at the fact that the calling function - updateFinancialAccounts
256 * bunches together some disparate processes rather than having separate appropriate
259 * @param array $params
262 * Return indicates whether the updateFinancialAccounts function should continue.
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
);
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'))
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'];
282 elseif (($previousContributionStatus === 'Pending'
283 && $params['prevContribution']->is_pay_later
) ||
$previousContributionStatus === 'In Progress'
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');
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'];
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;
299 if (($previousContributionStatus === 'Pending'
300 ||
$previousContributionStatus === 'In Progress')
301 && ($currentContributionStatus === 'Completed')
303 if (empty($params['line_item'])) {
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
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
);
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
;
319 $sql = "SELECT id, amount FROM civicrm_financial_item WHERE entity_id = %1 and entity_table = 'civicrm_line_item'";
322 'entity_table' => 'civicrm_financial_item',
324 foreach ($params['line_item'] as $fieldId => $fields) {
325 foreach ($fields as $fieldValueId => $lineItemDetails) {
326 self
::updateFinancialItemForLineItemToPaid($lineItemDetails['id']);
328 1 => [$lineItemDetails['id'], 'Integer'],
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);
347 * Update all financial items related to the line item tto have a status of paid.
349 * @param int $lineItemID
351 private static function updateFinancialItemForLineItemToPaid($lineItemID) {
354 CRM_Core_PseudoConstant
::getKey('CRM_Financial_BAO_FinancialItem', 'status_id', 'Paid'),
357 2 => [$lineItemID, 'Integer'],
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);
364 * Create Accounts Receivable financial trxn entry for Completed Contribution.
366 * @param array $trxnParams
367 * Financial trxn params
368 * @param array $contributionParams
369 * Contribution Params
373 public static function recordAlwaysAccountsReceivable(&$trxnParams, $contributionParams) {
374 if (!Civi
::settings()->get('always_post_to_accounts_receivable')) {
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
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'];
402 * Does this transaction reflect a payment instrument change.
404 * @param array $params
405 * @param array $pendingStatuses
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
);
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'])
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))
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
430 elseif (!CRM_Utils_System
::isNull($params['contribution']->check_number
) &&
431 $params['contribution']->check_number
!= $params['prevContribution']->check_number
433 // another special case when check number is changed, create new financial records
434 // create financial trxn with negative amount