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\Transaction ;
2024-05-12 18:24:38 +02:00
use FireflyIII\Models\TransactionJournal ;
2026-06-06 06:38:15 +02:00
use FireflyIII\Support\Facades\AppConfiguration ;
2026-01-18 06:07:50 +01:00
use FireflyIII\Support\Facades\Steam ;
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
}
2026-01-25 10:47:30 +01:00
public static function getLatestBalance ( int $accountId , int $currencyId , ? Carbon $notBefore ) : string
2025-09-26 06:05:37 +02:00
{
if ( ! $notBefore instanceof Carbon ) {
2026-01-25 17:27:27 +01:00
// Log::debug(sprintf('Start balance for account #%d and currency #%d is 0.', $accountId, $currencyId));
2025-12-28 06:59:39 +01:00
2025-09-26 06:05:37 +02:00
return '0' ;
}
2026-05-17 04:57:34 +02:00
$query = Transaction :: leftJoin ( 'transaction_journals' , 'transaction_journals.id' , '=' , 'transactions.transaction_journal_id' )
-> whereNull ( 'transactions.deleted_at' )
-> where ( 'transactions.transaction_currency_id' , $currencyId )
-> whereNull ( 'transaction_journals.deleted_at' )
2025-09-26 06:05:37 +02:00
// this order is the same as GroupCollector
2026-05-17 04:57:34 +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 )
;
2025-09-26 06:05:37 +02:00
$query -> where ( 'transaction_journals.date' , '<' , $notBefore );
2026-05-17 04:57:34 +02:00
$first = $query -> first ([
'transactions.id' ,
'transactions.balance_dirty' ,
'transactions.transaction_currency_id' ,
'transaction_journals.date' ,
'transactions.account_id' ,
'transactions.amount' ,
'transactions.balance_after' ,
]);
2026-01-18 06:07:50 +01:00
2026-01-18 10:53:13 +01:00
if ( null === $first ) {
2026-02-08 08:16:33 +01:00
// Log::debug(sprintf('Found no transactions for currency #%d and account #%d, return 0.', $currencyId, $accountId));
2026-01-18 10:53:13 +01:00
2026-01-18 06:07:50 +01:00
return '0' ;
}
2026-05-17 04:57:34 +02:00
$balance = ( string ) ( $first -> balance_after ?? '0' );
2026-01-23 15:09:50 +01:00
Log :: debug ( sprintf (
2026-05-17 04:57:34 +02:00
'getLatestBalance: found balance: %s in transaction #%d on moment %s' ,
Steam :: bcround ( $balance , 2 ),
$first -> id ?? 0 ,
$notBefore -> format ( 'Y-m-d H:i:s' )
));
2025-09-26 06:05:37 +02:00
return $balance ;
}
2026-01-25 10:47:30 +01:00
public static function optimizedCalculation ( Collection $accounts , ? Carbon $notBefore = null ) : void
2024-07-29 19:51:04 +02:00
{
2026-01-18 06:07:50 +01:00
if ( $notBefore instanceof Carbon ) {
$notBefore -> startOfDay ();
}
Log :: debug ( sprintf ( 'start of optimizedCalculation with date "%s"' , $notBefore ?-> format ( 'Y-m-d H:i:s' )));
2024-07-29 19:51:04 +02:00
if ( $accounts -> count () > 0 ) {
2026-01-18 10:53:13 +01:00
Log :: debug ( sprintf ( 'Limited to %d account(s): %s' , $accounts -> count (), implode ( ', ' , $accounts -> pluck ( 'id' ) -> toArray ())));
2024-07-29 19:51:04 +02:00
}
// collect all transactions and the change they make.
$balances = [];
$count = 0 ;
$query = Transaction :: leftJoin ( 'transaction_journals' , 'transaction_journals.id' , '=' , 'transactions.transaction_journal_id' )
2026-05-17 04:57:34 +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.
2026-05-17 04:57:34 +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
$query -> where ( 'transaction_journals.date' , '>=' , $notBefore );
}
2024-07-29 19:51:04 +02:00
2026-05-17 04:57:34 +02:00
$set = $query -> get ([
'transactions.id' ,
'transactions.balance_dirty' ,
'transactions.transaction_currency_id' ,
'transaction_journals.date' ,
'transactions.account_id' ,
'transactions.amount' ,
]);
2026-01-18 06:07:50 +01:00
Log :: debug ( sprintf ( 'Found %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 ) {
2026-06-30 14:42:20 +02:00
Log :: debug ( sprintf ( '[%s] Processing transaction #%d on acount #%d with currency #%d and amount %s' , $entry -> date , $entry -> id , $entry -> account_id , $entry -> transaction_currency_id , Steam :: bcround ( $entry -> amount , 2 )));
2024-07-29 19:51:04 +02:00
// start with empty array:
2026-05-17 04:57:34 +02:00
$entry -> account_id = ( int ) $entry -> account_id ;
$entry -> transaction_currency_id = ( int ) $entry -> transaction_currency_id ;
2026-05-17 04:26:45 +02:00
2026-01-23 15:14:29 +01:00
$balances [ $entry -> account_id ] ??= [];
2026-01-23 15:09:50 +01:00
$balances [ $entry -> account_id ][ $entry -> transaction_currency_id ] ??= [
2026-01-25 10:47:30 +01:00
self :: getLatestBalance ( $entry -> account_id , $entry -> transaction_currency_id , $notBefore ),
2026-01-23 15:14:29 +01:00
null ,
2026-01-23 15:09:50 +01:00
];
2024-07-29 19:51:04 +02:00
// before and after are easy:
2026-05-17 04:57:34 +02:00
$before = $balances [ $entry -> account_id ][ $entry -> transaction_currency_id ][ 0 ];
$after = bcadd ( $before , ( string ) $entry -> amount );
2026-01-18 06:07:50 +01:00
2026-01-21 19:42:27 +01:00
// Log::debug(sprintf('Before:%s, after:%s', Steam::bcround($before, 2), Steam::bcround($after, 2)));
2026-01-18 06:07:50 +01:00
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 ;
2026-01-23 15:14:29 +01:00
$entry -> balance_after = $after ;
$entry -> balance_dirty = false ;
2024-07-29 19:51:04 +02:00
$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 ));
2026-01-23 15:09:50 +01:00
// then update all transactions.
2024-10-20 10:16:54 +02:00
// save all collected balances in their respective account objects.
2025-12-19 16:30:39 +01:00
// $this->storeAccountBalances($balances);
2024-07-29 19:51:04 +02:00
}
2026-02-06 13:55:17 +01:00
/**
* Recalculate all account and transaction balances.
*/
public static function recalculateAll ( bool $forced ) : void
{
if ( $forced ) {
2026-05-21 06:31:24 +02:00
Transaction :: query () -> whereNull ( 'deleted_at' ) -> update ([ 'balance_dirty' => true ]);
2026-02-06 13:55:17 +01:00
// also delete account balances.
// AccountBalance::whereNotNull('created_at')->delete();
}
$object = new self ();
self :: optimizedCalculation ( new Collection ());
}
public static function recalculateForJournal ( TransactionJournal $transactionJournal ) : void
{
2026-06-06 06:38:15 +02:00
if ( false === AppConfiguration :: get ( 'use_running_balance' , config ( 'firefly.feature_flags.running_balance_column' )) -> data ) {
2026-02-06 13:55:17 +01:00
return ;
}
Log :: debug ( __METHOD__ );
2026-05-17 04:57:34 +02:00
$object = new self ();
2026-02-06 13:55:17 +01:00
2026-05-17 04:57:34 +02:00
$set = [];
2026-02-06 13:55:17 +01:00
foreach ( $transactionJournal -> transactions as $transaction ) {
$set [ $transaction -> account_id ] = $transaction -> account ;
}
$accounts = new Collection () -> push ( ... $set );
// find meta value:
2026-05-17 04:57:34 +02:00
$date = $transactionJournal -> date ;
$meta = $transactionJournal -> transactionJournalMeta () -> where ( 'name' , '_internal_previous_date' ) -> where ( 'data' , '!=' , '' ) -> first ();
2026-02-06 13:55:17 +01:00
Log :: debug ( sprintf ( 'Date used is "%s"' , $date -> toW3cString ()));
if ( null !== $meta ) {
$date = Carbon :: parse ( $meta -> data );
Log :: debug ( sprintf ( 'Date is overruled with "%s"' , $date -> toW3cString ()));
}
self :: optimizedCalculation ( $accounts , $date );
}
2024-05-12 13:31:33 +02:00
}