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 ;
2025-09-07 14:54:44 +02:00
use FireflyIII\Exceptions\FireflyException ;
2024-05-12 13:31:33 +02:00
use FireflyIII\Models\Account ;
use FireflyIII\Models\AccountBalance ;
use FireflyIII\Models\Transaction ;
2024-05-12 18:24:38 +02:00
use FireflyIII\Models\TransactionJournal ;
2025-09-07 14:49:49 +02:00
use FireflyIII\Support\Facades\Amount ;
2026-01-16 07:34:20 +01:00
use FireflyIII\Support\Facades\FireflyConfig ;
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
}
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
2025-09-26 06:05:37 +02:00
public static function recalculateForJournal ( TransactionJournal $transactionJournal ) : void
{
2026-01-16 07:34:20 +01:00
if ( false === FireflyConfig :: get ( 'use_running_balance' , config ( 'firefly.feature_flags.running_balance_column' )) -> data ) {
return ;
}
2025-09-26 06:05:37 +02:00
Log :: debug ( __METHOD__ );
2025-12-30 16:19:05 +01:00
$object = new self ();
2025-09-26 06:05:37 +02:00
2025-12-30 16:19:05 +01:00
$set = [];
2025-09-26 06:05:37 +02:00
foreach ( $transactionJournal -> transactions as $transaction ) {
$set [ $transaction -> account_id ] = $transaction -> account ;
}
$accounts = new Collection () -> push ( ... $set );
2025-12-30 16:13:28 +01:00
// find meta value:
2025-12-30 16:19:05 +01:00
$date = $transactionJournal -> date ;
$meta = $transactionJournal -> transactionJournalMeta () -> where ( 'name' , '_internal_previous_date' ) -> where ( 'data' , '!=' , '' ) -> first ();
2025-12-30 16:13:28 +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 ()));
}
$object -> optimizedCalculation ( $accounts , $date );
2025-09-26 06:05:37 +02:00
}
private function getLatestBalance ( int $accountId , int $currencyId , ? Carbon $notBefore ) : string
{
if ( ! $notBefore instanceof Carbon ) {
2025-12-28 06:54:03 +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' ;
}
2025-12-30 16:19:05 +01:00
$query = Transaction :: leftJoin ( 'transaction_journals' , 'transaction_journals.id' , '=' , 'transactions.transaction_journal_id' )
-> whereNull ( 'transactions.deleted_at' )
2026-01-18 06:07:50 +01:00
-> where ( 'transactions.transaction_currency_id' , $currencyId )
2025-12-30 16:19:05 +01:00
-> whereNull ( 'transaction_journals.deleted_at' )
2025-09-26 06:05:37 +02:00
// this order is the same as GroupCollector
2025-12-30 16:19:05 +01: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' )
2026-01-18 10:53:13 +01:00
-> where ( 'transactions.account_id' , $accountId )
;
2025-09-26 06:05:37 +02:00
$query -> where ( 'transaction_journals.date' , '<' , $notBefore );
$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-01-18 06:07:50 +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' ;
}
2025-09-26 06:05:37 +02:00
$balance = ( string )( $first -> balance_after ? ? '0' );
2026-01-18 10:53:13 +01:00
Log :: debug ( sprintf ( '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 ;
}
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
{
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' )
2025-12-30 16:19:05 +01: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-12-30 16:19:05 +01:00
-> orderBy ( 'transaction_journals.date' , 'asc' )
-> orderBy ( 'transaction_journals.order' , 'desc' )
-> orderBy ( 'transaction_journals.id' , 'asc' )
-> orderBy ( 'transaction_journals.description' , 'asc' )
2026-01-18 10:53:13 +01:00
-> 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
2025-12-30 16:19:05 +01: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-01-18 06:07:50 +01:00
Log :: debug ( sprintf ( 'Processing transaction #%d with currency #%d and amount %s' , $entry -> id , $entry -> transaction_currency_id , Steam :: bcround ( $entry -> amount )));
2024-07-29 19:51:04 +02:00
// 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-12-30 16:19:05 +01: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-18 10:53:13 +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 ;
$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.
2025-12-19 16:30:39 +01:00
// $this->storeAccountBalances($balances);
2024-07-29 19:51:04 +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
/**
2025-09-07 14:58:46 +02:00
* @ var int $accountId
2024-10-20 10:16:54 +02:00
* @ 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
/**
2025-09-07 14:58:46 +02:00
* @ var int $currencyId
2024-10-20 10:16:54 +02:00
* @ var array $balance
*/
foreach ( $currencies as $currencyId => $balance ) {
2025-09-07 14:54:44 +02:00
try {
$currency = Amount :: getTransactionCurrencyById ( $currencyId );
} catch ( FireflyException ) {
2024-10-20 10:16:54 +02:00
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-05-12 13:31:33 +02:00
}