2020-10-18 12:41:49 +02:00
< ? php
2022-12-29 19:42:26 +01:00
2020-10-18 12:41:49 +02:00
/**
* FrontpageChartGenerator . php
* Copyright ( c ) 2020 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\Chart\Budget ;
use Carbon\Carbon ;
use FireflyIII\Models\Budget ;
use FireflyIII\Models\BudgetLimit ;
2024-12-24 10:29:07 +01:00
use FireflyIII\Models\TransactionCurrency ;
2020-10-18 12:41:49 +02:00
use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface ;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface ;
use FireflyIII\Repositories\Budget\OperationsRepositoryInterface ;
use FireflyIII\User ;
use Illuminate\Support\Collection ;
2024-12-24 10:29:07 +01:00
use Illuminate\Support\Facades\Log ;
2021-04-06 17:00:16 +02:00
2020-10-18 12:41:49 +02:00
/**
* Class FrontpageChartGenerator
*/
class FrontpageChartGenerator
{
2025-08-01 12:31:01 +02:00
public bool $convertToPrimary = false ;
2025-05-04 17:41:26 +02:00
public TransactionCurrency $default ;
protected OperationsRepositoryInterface $opsRepository ;
private readonly BudgetLimitRepositoryInterface $blRepository ;
private readonly BudgetRepositoryInterface $budgetRepository ;
private Carbon $end ;
private string $monthAndDayFormat ;
private Carbon $start ;
2024-12-24 10:29:07 +01:00
2020-10-18 12:41:49 +02:00
/**
* FrontpageChartGenerator constructor .
*/
public function __construct ()
{
$this -> budgetRepository = app ( BudgetRepositoryInterface :: class );
$this -> blRepository = app ( BudgetLimitRepositoryInterface :: class );
$this -> opsRepository = app ( OperationsRepositoryInterface :: class );
$this -> monthAndDayFormat = '' ;
}
/**
* Generate the data for a budget chart . Collect all budgets and process each budget .
*
* @ return array []
*/
public function generate () : array
{
2024-12-24 10:29:07 +01:00
Log :: debug ( 'Now in generate for budget chart.' );
2020-10-18 12:41:49 +02:00
$budgets = $this -> budgetRepository -> getActiveBudgets ();
$data = [
2024-12-22 08:43:12 +01:00
[ 'label' => ( string ) trans ( 'firefly.spent_in_budget' ), 'entries' => [], 'type' => 'bar' ],
[ 'label' => ( string ) trans ( 'firefly.left_to_spend' ), 'entries' => [], 'type' => 'bar' ],
[ 'label' => ( string ) trans ( 'firefly.overspent' ), 'entries' => [], 'type' => 'bar' ],
2020-10-18 12:41:49 +02:00
];
// loop al budgets:
/** @var Budget $budget */
foreach ( $budgets as $budget ) {
$data = $this -> processBudget ( $data , $budget );
}
2024-12-24 10:29:07 +01:00
Log :: debug ( 'DONE with generate budget chart.' );
2020-10-18 12:41:49 +02:00
return $data ;
}
/**
2023-06-21 12:34:58 +02:00
* For each budget , gets all budget limits for the current time range .
* When no limits are present , the time range is used to collect information on money spent .
* If limits are present , each limit is processed individually .
2020-10-18 12:41:49 +02:00
*/
2023-06-21 12:34:58 +02:00
private function processBudget ( array $data , Budget $budget ) : array
2020-10-18 12:41:49 +02:00
{
2024-12-24 10:29:07 +01:00
Log :: debug ( sprintf ( 'Now processing budget #%d ("%s")' , $budget -> id , $budget -> name ));
2023-06-21 12:34:58 +02:00
// get all limits:
$limits = $this -> blRepository -> getBudgetLimits ( $budget , $this -> start , $this -> end );
2024-12-24 10:29:07 +01:00
Log :: debug ( sprintf ( 'Found %d limit(s) for budget #%d.' , $limits -> count (), $budget -> id ));
2023-06-21 12:34:58 +02:00
// if no limits
if ( 0 === $limits -> count ()) {
2024-12-24 10:29:07 +01:00
$result = $this -> noBudgetLimits ( $data , $budget );
Log :: debug ( sprintf ( 'Now DONE processing budget #%d ("%s")' , $budget -> id , $budget -> name ));
2024-12-25 07:13:41 +01:00
2024-12-24 10:29:07 +01:00
return $result ;
2020-10-18 12:41:49 +02:00
}
2024-12-24 10:29:07 +01:00
$result = $this -> budgetLimits ( $data , $budget , $limits );
Log :: debug ( sprintf ( 'Now DONE processing budget #%d ("%s")' , $budget -> id , $budget -> name ));
2024-12-25 07:13:41 +01:00
2024-12-24 10:29:07 +01:00
return $result ;
2020-10-18 12:41:49 +02:00
}
/**
* When no limits are present , the expenses of the whole period are collected and grouped .
* This is grouped per currency . Because there is no limit set , " left to spend " and " overspent " are empty .
*/
private function noBudgetLimits ( array $data , Budget $budget ) : array
{
2021-05-24 08:57:02 +02:00
$spent = $this -> opsRepository -> sumExpenses ( $this -> start , $this -> end , null , new Collection ([ $budget ]));
2023-12-20 19:35:52 +01:00
2020-10-18 12:41:49 +02:00
/** @var array $entry */
foreach ( $spent as $entry ) {
$title = sprintf ( '%s (%s)' , $budget -> name , $entry [ 'currency_name' ]);
2025-05-04 13:47:00 +02:00
$data [ 0 ][ 'entries' ][ $title ] = bcmul (( string ) $entry [ 'sum' ], '-1' ); // spent
2025-05-04 17:41:26 +02:00
$data [ 1 ][ 'entries' ][ $title ] = 0 ; // left to spend
$data [ 2 ][ 'entries' ][ $title ] = 0 ; // overspent
2020-10-18 12:41:49 +02:00
}
2020-10-27 19:04:19 +01:00
2020-10-18 12:41:49 +02:00
return $data ;
}
/**
2023-06-21 12:34:58 +02:00
* If a budget has budget limit , each limit is processed individually .
2020-10-18 12:41:49 +02:00
*/
2023-06-21 12:34:58 +02:00
private function budgetLimits ( array $data , Budget $budget , Collection $limits ) : array
2020-10-18 12:41:49 +02:00
{
2024-12-24 10:29:07 +01:00
Log :: debug ( 'Start processing budget limits.' );
2024-12-25 07:13:41 +01:00
2023-06-21 12:34:58 +02:00
/** @var BudgetLimit $limit */
foreach ( $limits as $limit ) {
$data = $this -> processLimit ( $data , $budget , $limit );
2020-10-18 12:41:49 +02:00
}
2024-12-24 10:29:07 +01:00
Log :: debug ( 'Done processing budget limits.' );
2020-10-27 19:04:19 +01:00
2023-06-21 12:34:58 +02:00
return $data ;
2020-10-18 12:41:49 +02:00
}
/**
2023-06-21 12:34:58 +02:00
* For each limit , the expenses from the time range of the limit are collected . Each row from the result is
* processed individually .
2020-10-18 12:41:49 +02:00
*/
private function processLimit ( array $data , Budget $budget , BudgetLimit $limit ) : array
{
2025-08-01 12:31:01 +02:00
$usePrimary = $this -> convertToPrimary && $this -> default -> id !== $limit -> transaction_currency_id ;
2025-08-01 13:10:11 +02:00
$currency = $limit -> transactionCurrency ;
2025-08-01 12:31:01 +02:00
if ( $usePrimary ) {
Log :: debug ( sprintf ( 'Processing limit #%d with (primary currency) %s %s' , $limit -> id , $this -> default -> code , $limit -> native_amount ));
2024-12-24 10:29:07 +01:00
}
2025-08-01 12:31:01 +02:00
if ( ! $usePrimary ) {
2024-12-24 10:29:07 +01:00
Log :: debug ( sprintf ( 'Processing limit #%d with %s %s' , $limit -> id , $limit -> transactionCurrency -> code , $limit -> amount ));
}
2023-12-20 19:35:52 +01:00
2025-08-01 13:10:11 +02:00
$spent = $this -> opsRepository -> sumExpenses ( $limit -> start_date , $limit -> end_date , null , new Collection ([ $budget ]), $currency );
2024-12-24 10:29:07 +01:00
Log :: debug ( sprintf ( 'Spent array has %d entries.' , count ( $spent )));
2024-12-25 07:13:41 +01:00
2020-10-18 12:41:49 +02:00
/** @var array $entry */
foreach ( $spent as $entry ) {
2020-10-27 19:04:19 +01:00
// only spent the entry where the entry's currency matches the budget limit's currency
2025-08-01 12:31:01 +02:00
// or when usePrimary is true.
if ( $entry [ 'currency_id' ] === $limit -> transaction_currency_id || $usePrimary ) {
2024-12-24 10:29:07 +01:00
Log :: debug ( sprintf ( 'Process spent row (%s)' , $entry [ 'currency_code' ]));
2020-10-27 19:04:19 +01:00
$data = $this -> processRow ( $data , $budget , $limit , $entry );
}
2025-08-01 12:31:01 +02:00
if ( ! ( $entry [ 'currency_id' ] === $limit -> transaction_currency_id || $usePrimary )) {
2024-12-24 16:56:31 +01:00
Log :: debug ( sprintf ( 'Skipping spent row (%s).' , $entry [ 'currency_code' ]));
}
2020-10-18 12:41:49 +02:00
}
2020-10-27 19:04:19 +01:00
2020-10-18 12:41:49 +02:00
return $data ;
}
/**
* Each row of expenses from a budget limit is in another currency ( note $entry [ 'currency_name' ]) .
*
2023-06-21 12:34:58 +02:00
* Each one is added to the $data array . If the limit ' s date range is different from the global $start and $end
* dates , for example when a limit only partially falls into this month , the title is expanded to clarify .
2020-10-18 12:41:49 +02:00
*/
private function processRow ( array $data , Budget $budget , BudgetLimit $limit , array $entry ) : array
{
2024-12-25 07:13:41 +01:00
$title = sprintf ( '%s (%s)' , $budget -> name , $entry [ 'currency_name' ]);
2024-12-24 16:56:31 +01:00
Log :: debug ( sprintf ( 'Title is "%s"' , $title ));
2020-10-18 12:41:49 +02:00
if ( $limit -> start_date -> startOfDay () -> ne ( $this -> start -> startOfDay ()) || $limit -> end_date -> startOfDay () -> ne ( $this -> end -> startOfDay ())) {
$title = sprintf (
'%s (%s) (%s - %s)' ,
$budget -> name ,
$entry [ 'currency_name' ],
2022-03-27 20:24:13 +02:00
$limit -> start_date -> isoFormat ( $this -> monthAndDayFormat ),
$limit -> end_date -> isoFormat ( $this -> monthAndDayFormat )
2020-10-18 12:41:49 +02:00
);
}
2025-08-01 13:10:11 +02:00
$usePrimary = $this -> convertToPrimary && $this -> default -> id !== $limit -> transaction_currency_id ;
2024-12-25 07:13:41 +01:00
$amount = $limit -> amount ;
2024-12-29 06:32:50 +01:00
Log :: debug ( sprintf ( 'Amount is "%s".' , $amount ));
2025-08-01 12:31:01 +02:00
if ( $usePrimary && $limit -> transaction_currency_id !== $this -> default -> id ) {
2024-12-24 16:56:31 +01:00
$amount = $limit -> native_amount ;
2024-12-29 06:32:50 +01:00
Log :: debug ( sprintf ( 'Amount is now "%s".' , $amount ));
2024-12-24 16:56:31 +01:00
}
2025-05-04 13:55:42 +02:00
$amount ? ? = '0' ;
2025-05-04 13:47:00 +02:00
$sumSpent = bcmul (( string ) $entry [ 'sum' ], '-1' ); // spent
2024-12-24 10:29:07 +01:00
$data [ 0 ][ 'entries' ][ $title ] ? ? = '0' ;
$data [ 1 ][ 'entries' ][ $title ] ? ? = '0' ;
$data [ 2 ][ 'entries' ][ $title ] ? ? = '0' ;
2025-05-04 17:41:26 +02:00
$data [ 0 ][ 'entries' ][ $title ] = bcadd (( string ) $data [ 0 ][ 'entries' ][ $title ], 1 === bccomp ( $sumSpent , $amount ) ? $amount : $sumSpent ); // spent
2025-05-04 13:47:00 +02:00
$data [ 1 ][ 'entries' ][ $title ] = bcadd (( string ) $data [ 1 ][ 'entries' ][ $title ], 1 === bccomp ( $amount , $sumSpent ) ? bcadd (( string ) $entry [ 'sum' ], $amount ) : '0' ); // left to spent
$data [ 2 ][ 'entries' ][ $title ] = bcadd (( string ) $data [ 2 ][ 'entries' ][ $title ], 1 === bccomp ( $amount , $sumSpent ) ? '0' : bcmul ( bcadd (( string ) $entry [ 'sum' ], $amount ), '-1' )); // overspent
2024-12-24 16:56:31 +01:00
Log :: debug ( sprintf ( 'Amount [spent] is now %s.' , $data [ 0 ][ 'entries' ][ $title ]));
Log :: debug ( sprintf ( 'Amount [left] is now %s.' , $data [ 1 ][ 'entries' ][ $title ]));
Log :: debug ( sprintf ( 'Amount [overspent] is now %s.' , $data [ 2 ][ 'entries' ][ $title ]));
2020-10-18 12:41:49 +02:00
return $data ;
}
2024-02-22 20:11:09 +01:00
public function setEnd ( Carbon $end ) : void
{
$this -> end = $end ;
}
public function setStart ( Carbon $start ) : void
{
$this -> start = $start ;
}
/**
* A basic setter for the user . Also updates the repositories with the right user .
*/
public function setUser ( User $user ) : void
{
$this -> budgetRepository -> setUser ( $user );
$this -> blRepository -> setUser ( $user );
$this -> opsRepository -> setUser ( $user );
$locale = app ( 'steam' ) -> getLocale ();
2024-12-22 08:43:12 +01:00
$this -> monthAndDayFormat = ( string ) trans ( 'config.month_and_day_js' , [], $locale );
2024-02-22 20:11:09 +01:00
}
2020-12-22 05:35:06 +01:00
}