Commit | Line | Data |
---|---|---|
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 | */ | |
23 | class 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 | } |