| 
									
										
										
										
											2024-05-12 13:31:33 +02:00
										 |  |  | <?php | 
					
						
							| 
									
										
										
										
											2024-11-25 04:18:55 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-12 13:31:33 +02:00
										 |  |  | /* | 
					
						
							|  |  |  |  * AccountBalanceCalculator.php | 
					
						
							|  |  |  |  * Copyright (c) 2024 james@firefly-iii.org. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * This file is part of Firefly III (https://github.com/firefly-iii). | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * This program is free software: you can redistribute it and/or modify | 
					
						
							|  |  |  |  * it under the terms of the GNU Affero General Public License as | 
					
						
							|  |  |  |  * published by the Free Software Foundation, either version 3 of the | 
					
						
							|  |  |  |  * License, or (at your option) any later version. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * This program is distributed in the hope that it will be useful, | 
					
						
							|  |  |  |  * but WITHOUT ANY WARRANTY; without even the implied warranty of | 
					
						
							|  |  |  |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | 
					
						
							|  |  |  |  * GNU Affero General Public License for more details. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * You should have received a copy of the GNU Affero General Public License | 
					
						
							|  |  |  |  * along with this program.  If not, see https://www.gnu.org/licenses/. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | declare(strict_types=1); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | namespace FireflyIII\Support\Models; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-28 08:26:54 +02:00
										 |  |  | use Carbon\Carbon; | 
					
						
							| 
									
										
										
										
											2024-05-12 13:31:33 +02:00
										 |  |  | use FireflyIII\Models\Account; | 
					
						
							|  |  |  | use FireflyIII\Models\AccountBalance; | 
					
						
							|  |  |  | use FireflyIII\Models\Transaction; | 
					
						
							| 
									
										
										
										
											2024-10-20 10:16:54 +02:00
										 |  |  | use FireflyIII\Models\TransactionCurrency; | 
					
						
							| 
									
										
										
										
											2024-05-12 18:24:38 +02:00
										 |  |  | use FireflyIII\Models\TransactionJournal; | 
					
						
							| 
									
										
										
										
											2024-07-29 19:51:04 +02:00
										 |  |  | use Illuminate\Support\Collection; | 
					
						
							| 
									
										
										
										
											2024-05-12 13:31:33 +02:00
										 |  |  | use Illuminate\Support\Facades\Log; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-31 08:20:19 +02:00
										 |  |  | /** | 
					
						
							|  |  |  |  * Class AccountBalanceCalculator | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * This class started as a piece of code to create and calculate "account balance" objects, but they | 
					
						
							|  |  |  |  * are at the moment unused. Instead, each transaction gets a before/after balance and an indicator if this | 
					
						
							|  |  |  |  * balance is up-to-date. This class now contains some methods to recalculate those amounts. | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2024-05-12 13:31:33 +02:00
										 |  |  | class AccountBalanceCalculator | 
					
						
							|  |  |  | { | 
					
						
							| 
									
										
										
										
											2024-05-16 07:22:12 +02:00
										 |  |  |     private function __construct() | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2024-05-16 07:14:44 +02:00
										 |  |  |         // no-op
 | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-31 13:09:55 +02:00
										 |  |  |     /** | 
					
						
							| 
									
										
										
										
											2024-10-20 10:16:54 +02:00
										 |  |  |      * Recalculate all account and transaction balances. | 
					
						
							| 
									
										
										
										
											2024-07-31 13:09:55 +02:00
										 |  |  |      */ | 
					
						
							| 
									
										
										
										
											2024-10-20 10:16:54 +02:00
										 |  |  |     public static function recalculateAll(bool $forced): void | 
					
						
							| 
									
										
										
										
											2024-05-12 13:31:33 +02:00
										 |  |  |     { | 
					
						
							| 
									
										
										
										
											2024-10-20 10:16:54 +02:00
										 |  |  |         if ($forced) { | 
					
						
							|  |  |  |             Transaction::whereNull('deleted_at')->update(['balance_dirty' => true]); | 
					
						
							|  |  |  |             // also delete account balances.
 | 
					
						
							|  |  |  |             AccountBalance::whereNotNull('created_at')->delete(); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2024-05-16 07:22:12 +02:00
										 |  |  |         $object = new self(); | 
					
						
							| 
									
										
										
										
											2024-07-29 19:51:04 +02:00
										 |  |  |         $object->optimizedCalculation(new Collection()); | 
					
						
							| 
									
										
										
										
											2024-05-13 20:31:52 +02:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2024-05-12 13:31:33 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-28 08:36:26 +02:00
										 |  |  |     private function optimizedCalculation(Collection $accounts, ?Carbon $notBefore = null): void | 
					
						
							| 
									
										
										
										
											2024-07-29 19:51:04 +02:00
										 |  |  |     { | 
					
						
							|  |  |  |         Log::debug('start of optimizedCalculation'); | 
					
						
							|  |  |  |         if ($accounts->count() > 0) { | 
					
						
							|  |  |  |             Log::debug(sprintf('Limited to %d account(s)', $accounts->count())); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         // collect all transactions and the change they make.
 | 
					
						
							|  |  |  |         $balances = []; | 
					
						
							|  |  |  |         $count    = 0; | 
					
						
							|  |  |  |         $query    = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') | 
					
						
							| 
									
										
										
										
											2025-05-04 13:55:42 +02:00
										 |  |  |             ->whereNull('transactions.deleted_at') | 
					
						
							|  |  |  |             ->whereNull('transaction_journals.deleted_at') | 
					
						
							| 
									
										
										
										
											2024-07-29 19:51:04 +02:00
										 |  |  |             // this order is the same as GroupCollector, but in the exact reverse.
 | 
					
						
							| 
									
										
										
										
											2025-05-04 13:55:42 +02:00
										 |  |  |             ->orderBy('transaction_journals.date', 'asc') | 
					
						
							|  |  |  |             ->orderBy('transaction_journals.order', 'desc') | 
					
						
							|  |  |  |             ->orderBy('transaction_journals.id', 'asc') | 
					
						
							|  |  |  |             ->orderBy('transaction_journals.description', 'asc') | 
					
						
							|  |  |  |             ->orderBy('transactions.amount', 'asc') | 
					
						
							|  |  |  |         ; | 
					
						
							| 
									
										
										
										
											2024-07-31 20:19:17 +02:00
										 |  |  |         if ($accounts->count() > 0) { | 
					
						
							| 
									
										
										
										
											2024-07-29 19:51:04 +02:00
										 |  |  |             $query->whereIn('transactions.account_id', $accounts->pluck('id')->toArray()); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2025-05-27 17:06:15 +02:00
										 |  |  |         if ($notBefore instanceof Carbon) { | 
					
						
							| 
									
										
										
										
											2024-09-28 08:26:54 +02:00
										 |  |  |             $notBefore->startOfDay(); | 
					
						
							|  |  |  |             $query->where('transaction_journals.date', '>=', $notBefore); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2024-07-29 19:51:04 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-04 13:55:42 +02:00
										 |  |  |         $set      = $query->get(['transactions.id', 'transactions.balance_dirty', 'transactions.transaction_currency_id', 'transaction_journals.date', 'transactions.account_id', 'transactions.amount']); | 
					
						
							| 
									
										
										
										
											2025-04-26 17:29:10 +02:00
										 |  |  |         Log::debug(sprintf('Counted %d transaction(s)', $set->count())); | 
					
						
							| 
									
										
										
										
											2024-10-20 10:16:54 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         // the balance value is an array.
 | 
					
						
							|  |  |  |         // first entry is the balance, second is the date.
 | 
					
						
							| 
									
										
										
										
											2024-07-29 19:51:04 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         /** @var Transaction $entry */ | 
					
						
							|  |  |  |         foreach ($set as $entry) { | 
					
						
							|  |  |  |             // start with empty array:
 | 
					
						
							|  |  |  |             $balances[$entry->account_id]                                  ??= []; | 
					
						
							| 
									
										
										
										
											2024-10-20 10:16:54 +02:00
										 |  |  |             $balances[$entry->account_id][$entry->transaction_currency_id] ??= [$this->getLatestBalance($entry->account_id, $entry->transaction_currency_id, $notBefore), null]; | 
					
						
							| 
									
										
										
										
											2024-07-29 19:51:04 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |             // before and after are easy:
 | 
					
						
							| 
									
										
										
										
											2025-05-04 13:55:42 +02:00
										 |  |  |             $before                                                        = $balances[$entry->account_id][$entry->transaction_currency_id][0]; | 
					
						
							|  |  |  |             $after                                                         = bcadd($before, (string) $entry->amount); | 
					
						
							| 
									
										
										
										
											2024-07-31 20:19:17 +02:00
										 |  |  |             if (true === $entry->balance_dirty || $accounts->count() > 0) { | 
					
						
							| 
									
										
										
										
											2024-07-29 19:51:04 +02:00
										 |  |  |                 // update the transaction:
 | 
					
						
							|  |  |  |                 $entry->balance_before = $before; | 
					
						
							|  |  |  |                 $entry->balance_after  = $after; | 
					
						
							|  |  |  |                 $entry->balance_dirty  = false; | 
					
						
							|  |  |  |                 $entry->saveQuietly(); // do not observe this change, or we get stuck in a loop.
 | 
					
						
							| 
									
										
										
										
											2024-07-31 08:31:20 +02:00
										 |  |  |                 ++$count; | 
					
						
							| 
									
										
										
										
											2024-07-29 19:51:04 +02:00
										 |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             // then update the array:
 | 
					
						
							| 
									
										
										
										
											2024-10-20 10:16:54 +02:00
										 |  |  |             $balances[$entry->account_id][$entry->transaction_currency_id] = [$after, $entry->date]; | 
					
						
							| 
									
										
										
										
											2024-07-29 19:51:04 +02:00
										 |  |  |         } | 
					
						
							|  |  |  |         Log::debug(sprintf('end of optimizedCalculation, corrected %d balance(s)', $count)); | 
					
						
							|  |  |  |         // then update all transactions.
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-20 10:16:54 +02:00
										 |  |  |         // save all collected balances in their respective account objects.
 | 
					
						
							|  |  |  |         $this->storeAccountBalances($balances); | 
					
						
							| 
									
										
										
										
											2024-07-29 19:51:04 +02:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-22 08:43:12 +01:00
										 |  |  |     private function getLatestBalance(int $accountId, int $currencyId, ?Carbon $notBefore): string | 
					
						
							| 
									
										
										
										
											2024-05-12 13:31:33 +02:00
										 |  |  |     { | 
					
						
							| 
									
										
										
										
											2025-05-27 17:06:15 +02:00
										 |  |  |         if (!$notBefore instanceof Carbon) { | 
					
						
							| 
									
										
										
										
											2024-12-22 08:43:12 +01:00
										 |  |  |             return '0'; | 
					
						
							| 
									
										
										
										
											2024-05-12 13:31:33 +02:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2024-12-22 08:43:12 +01:00
										 |  |  |         Log::debug(sprintf('getLatestBalance: notBefore date is "%s", calculating', $notBefore->format('Y-m-d'))); | 
					
						
							| 
									
										
										
										
											2025-05-04 13:55:42 +02:00
										 |  |  |         $query   = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') | 
					
						
							|  |  |  |             ->whereNull('transactions.deleted_at') | 
					
						
							|  |  |  |             ->where('transaction_journals.transaction_currency_id', $currencyId) | 
					
						
							|  |  |  |             ->whereNull('transaction_journals.deleted_at') | 
					
						
							| 
									
										
										
										
											2024-12-22 08:43:12 +01:00
										 |  |  |             // this order is the same as GroupCollector
 | 
					
						
							| 
									
										
										
										
											2025-05-04 13:55:42 +02:00
										 |  |  |             ->orderBy('transaction_journals.date', 'DESC') | 
					
						
							|  |  |  |             ->orderBy('transaction_journals.order', 'ASC') | 
					
						
							|  |  |  |             ->orderBy('transaction_journals.id', 'DESC') | 
					
						
							|  |  |  |             ->orderBy('transaction_journals.description', 'DESC') | 
					
						
							|  |  |  |             ->orderBy('transactions.amount', 'DESC') | 
					
						
							|  |  |  |             ->where('transactions.account_id', $accountId) | 
					
						
							|  |  |  |         ; | 
					
						
							| 
									
										
										
										
											2024-12-22 08:43:12 +01:00
										 |  |  |         $notBefore->startOfDay(); | 
					
						
							|  |  |  |         $query->where('transaction_journals.date', '<', $notBefore); | 
					
						
							| 
									
										
										
										
											2024-05-13 05:10:16 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-22 08:43:12 +01:00
										 |  |  |         $first   = $query->first(['transactions.id', 'transactions.balance_dirty', 'transactions.transaction_currency_id', 'transaction_journals.date', 'transactions.account_id', 'transactions.amount', 'transactions.balance_after']); | 
					
						
							|  |  |  |         $balance = (string) ($first->balance_after ?? '0'); | 
					
						
							|  |  |  |         Log::debug(sprintf('getLatestBalance: found balance: %s in transaction #%d', $balance, $first->id ?? 0)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return $balance; | 
					
						
							| 
									
										
										
										
											2024-05-12 13:31:33 +02:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-20 10:16:54 +02:00
										 |  |  |     private function storeAccountBalances(array $balances): void | 
					
						
							| 
									
										
										
										
											2024-05-12 13:31:33 +02:00
										 |  |  |     { | 
					
						
							| 
									
										
										
										
											2024-10-20 10:16:54 +02:00
										 |  |  |         /** | 
					
						
							|  |  |  |          * @var int   $accountId | 
					
						
							|  |  |  |          * @var array $currencies | 
					
						
							|  |  |  |          */ | 
					
						
							|  |  |  |         foreach ($balances as $accountId => $currencies) { | 
					
						
							| 
									
										
										
										
											2025-01-05 07:31:26 +01:00
										 |  |  |             /** @var null|Account $account */ | 
					
						
							| 
									
										
										
										
											2024-10-20 10:16:54 +02:00
										 |  |  |             $account = Account::find($accountId); | 
					
						
							|  |  |  |             if (null === $account) { | 
					
						
							|  |  |  |                 Log::error(sprintf('Could not find account #%d, will not save account balance.', $accountId)); | 
					
						
							| 
									
										
										
										
											2024-10-21 05:15:16 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-20 10:16:54 +02:00
										 |  |  |                 continue; | 
					
						
							| 
									
										
										
										
											2024-06-18 19:44:22 +02:00
										 |  |  |             } | 
					
						
							| 
									
										
										
										
											2024-05-13 20:31:52 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-20 10:16:54 +02:00
										 |  |  |             /** | 
					
						
							|  |  |  |              * @var int   $currencyId | 
					
						
							|  |  |  |              * @var array $balance | 
					
						
							|  |  |  |              */ | 
					
						
							|  |  |  |             foreach ($currencies as $currencyId => $balance) { | 
					
						
							| 
									
										
										
										
											2025-01-05 07:31:26 +01:00
										 |  |  |                 /** @var null|TransactionCurrency $currency */ | 
					
						
							| 
									
										
										
										
											2025-05-04 13:55:42 +02:00
										 |  |  |                 $currency        = TransactionCurrency::find($currencyId); | 
					
						
							| 
									
										
										
										
											2024-10-20 10:16:54 +02:00
										 |  |  |                 if (null === $currency) { | 
					
						
							|  |  |  |                     Log::error(sprintf('Could not find currency #%d, will not save account balance.', $currencyId)); | 
					
						
							| 
									
										
										
										
											2024-10-21 05:15:16 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-20 10:16:54 +02:00
										 |  |  |                     continue; | 
					
						
							| 
									
										
										
										
											2024-06-18 19:44:22 +02:00
										 |  |  |                 } | 
					
						
							| 
									
										
										
										
											2024-10-21 05:15:16 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-20 10:16:54 +02:00
										 |  |  |                 /** @var AccountBalance $object */ | 
					
						
							| 
									
										
										
										
											2024-11-06 11:12:12 +01:00
										 |  |  |                 $object          = $account->accountBalances()->firstOrCreate( | 
					
						
							|  |  |  |                     [ | 
					
						
							| 
									
										
										
										
											2024-11-06 11:57:12 +01:00
										 |  |  |                         'title'                   => 'running_balance', | 
					
						
							|  |  |  |                         'balance'                 => '0', | 
					
						
							| 
									
										
										
										
											2024-11-06 11:12:12 +01:00
										 |  |  |                         'transaction_currency_id' => $currencyId, | 
					
						
							| 
									
										
										
										
											2024-11-06 11:57:12 +01:00
										 |  |  |                         'date'                    => $balance[1], | 
					
						
							| 
									
										
										
										
											2024-11-08 21:02:36 +01:00
										 |  |  |                         'date_tz'                 => $balance[1]?->format('e'), | 
					
						
							| 
									
										
										
										
											2024-11-06 11:12:12 +01:00
										 |  |  |                     ] | 
					
						
							|  |  |  |                 ); | 
					
						
							| 
									
										
										
										
											2024-10-20 10:16:54 +02:00
										 |  |  |                 $object->balance = $balance[0]; | 
					
						
							|  |  |  |                 $object->date    = $balance[1]; | 
					
						
							| 
									
										
										
										
											2024-11-08 21:02:36 +01:00
										 |  |  |                 $object->date_tz = $balance[1]?->format('e'); | 
					
						
							| 
									
										
										
										
											2024-12-21 07:12:11 +01:00
										 |  |  |                 $object->saveQuietly(); | 
					
						
							| 
									
										
										
										
											2024-05-13 20:31:52 +02:00
										 |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2024-05-16 07:14:44 +02:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2024-12-22 08:43:12 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     public static function recalculateForJournal(TransactionJournal $transactionJournal): void | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         Log::debug(__METHOD__); | 
					
						
							| 
									
										
										
										
											2025-05-04 13:55:42 +02:00
										 |  |  |         $object   = new self(); | 
					
						
							| 
									
										
										
										
											2024-12-22 08:43:12 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-04 13:55:42 +02:00
										 |  |  |         $set      = []; | 
					
						
							| 
									
										
										
										
											2024-12-22 08:43:12 +01:00
										 |  |  |         foreach ($transactionJournal->transactions as $transaction) { | 
					
						
							| 
									
										
										
										
											2025-05-02 06:45:34 +02:00
										 |  |  |             $set[$transaction->account_id] = $transaction->account; | 
					
						
							| 
									
										
										
										
											2024-12-22 08:43:12 +01:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2025-05-02 06:45:34 +02:00
										 |  |  |         $accounts = new Collection($set); | 
					
						
							| 
									
										
										
										
											2024-12-22 08:43:12 +01:00
										 |  |  |         $object->optimizedCalculation($accounts, $transactionJournal->date); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2024-05-12 13:31:33 +02:00
										 |  |  | } |