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 * Create Accounts Receivable financial trxn entry for Completed Contribution.
247 * @param array $trxnParams
248 * Financial trxn params
249 * @param array $contributionParams
250 * Contribution Params
254 public static function recordAlwaysAccountsReceivable(&$trxnParams, $contributionParams) {
255 if (!Civi
::settings()->get('always_post_to_accounts_receivable')) {
258 $statusId = $contributionParams['contribution']->contribution_status_id
;
259 $contributionStatuses = CRM_Contribute_PseudoConstant
::contributionStatus(NULL, 'name');
260 $contributionStatus = empty($statusId) ?
NULL : $contributionStatuses[$statusId];
261 $previousContributionStatus = empty($contributionParams['prevContribution']) ?
NULL : $contributionStatuses[$contributionParams['prevContribution']->contribution_status_id
];
262 // Return if contribution status is not completed.
263 if (!($contributionStatus == 'Completed' && (empty($previousContributionStatus)
264 ||
(!empty($previousContributionStatus) && $previousContributionStatus == 'Pending'
265 && $contributionParams['prevContribution']->is_pay_later
== 0
271 $params = $trxnParams;
272 $financialTypeID = !empty($contributionParams['financial_type_id']) ?
$contributionParams['financial_type_id'] : $contributionParams['prevContribution']->financial_type_id
;
273 $arAccountId = CRM_Contribute_PseudoConstant
::getRelationalFinancialAccount($financialTypeID, 'Accounts Receivable Account is');
274 $params['to_financial_account_id'] = $arAccountId;
275 $params['status_id'] = array_search('Pending', $contributionStatuses);
276 $params['is_payment'] = FALSE;
277 $trxn = CRM_Core_BAO_FinancialTrxn
::create($params);
278 $trxnParams['from_financial_account_id'] = $params['to_financial_account_id'];