2025-03-25 17:27:59 +01:00
< ? php
/*
* BudgetController . php
* Copyright ( c ) 2023 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\Api\V1\Controllers\Chart ;
use Carbon\Carbon ;
use FireflyIII\Api\V1\Controllers\Controller ;
2025-08-16 14:52:29 +02:00
use FireflyIII\Api\V1\Requests\Data\SameDateRequest ;
2025-03-25 17:27:59 +01:00
use FireflyIII\Enums\UserRoleEnum ;
use FireflyIII\Exceptions\FireflyException ;
use FireflyIII\Models\Budget ;
use FireflyIII\Models\BudgetLimit ;
use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface ;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface ;
use FireflyIII\Repositories\Budget\OperationsRepositoryInterface ;
2025-09-07 14:49:49 +02:00
use FireflyIII\Support\Facades\Amount ;
2025-03-25 17:27:59 +01:00
use FireflyIII\Support\Http\Api\CleansChartData ;
2025-07-30 09:59:52 +02:00
use FireflyIII\Support\Http\Api\ExchangeRateConverter ;
2025-03-25 17:27:59 +01:00
use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait ;
use Illuminate\Http\JsonResponse ;
use Illuminate\Support\Collection ;
use Illuminate\Support\Facades\Log ;
/**
* Class BudgetController
*/
class BudgetController extends Controller
{
use CleansChartData ;
use ValidatesUserGroupTrait ;
2025-08-15 13:37:27 +02:00
protected array $acceptedRoles = [ UserRoleEnum :: READ_ONLY ];
2025-03-25 17:27:59 +01:00
protected OperationsRepositoryInterface $opsRepository ;
private BudgetLimitRepositoryInterface $blRepository ;
private array $currencies = [];
private BudgetRepositoryInterface $repository ;
public function __construct ()
{
parent :: __construct ();
$this -> middleware (
function ( $request , $next ) {
2025-08-15 19:57:32 +02:00
$this -> validateUserGroup ( $request );
2025-03-25 17:27:59 +01:00
$this -> repository = app ( BudgetRepositoryInterface :: class );
$this -> blRepository = app ( BudgetLimitRepositoryInterface :: class );
$this -> opsRepository = app ( OperationsRepositoryInterface :: class );
2025-08-15 19:57:32 +02:00
$this -> repository -> setUserGroup ( $this -> userGroup );
$this -> opsRepository -> setUserGroup ( $this -> userGroup );
$this -> blRepository -> setUserGroup ( $this -> userGroup );
$this -> repository -> setUser ( $this -> user );
$this -> opsRepository -> setUser ( $this -> user );
$this -> blRepository -> setUser ( $this -> user );
2025-03-25 17:27:59 +01:00
return $next ( $request );
}
);
}
/**
* TODO see autocomplete / accountcontroller
2025-07-30 08:56:14 +02:00
*
2025-07-30 06:59:58 +02:00
* @ throws FireflyException
2025-03-25 17:27:59 +01:00
*/
2025-08-16 14:52:29 +02:00
public function overview ( SameDateRequest $request ) : JsonResponse
2025-03-25 17:27:59 +01:00
{
2025-08-15 13:37:27 +02:00
$params = $request -> getAll ();
2025-03-25 17:27:59 +01:00
/** @var Carbon $start */
2025-08-15 13:37:27 +02:00
$start = $params [ 'start' ];
2025-03-25 17:27:59 +01:00
/** @var Carbon $end */
2025-08-15 13:37:27 +02:00
$end = $params [ 'end' ];
2025-03-25 17:27:59 +01:00
// code from FrontpageChartGenerator, but not in separate class
$budgets = $this -> repository -> getActiveBudgets ();
$data = [];
/** @var Budget $budget */
foreach ( $budgets as $budget ) {
// could return multiple arrays, so merge.
$data = array_merge ( $data , $this -> processBudget ( $budget , $start , $end ));
}
return response () -> json ( $this -> clean ( $data ));
}
/**
* @ throws FireflyException
*/
private function processBudget ( Budget $budget , Carbon $start , Carbon $end ) : array
{
// get all limits:
2025-08-15 07:50:13 +02:00
$limits = $this -> blRepository -> getBudgetLimits ( $budget , $start , $end );
$rows = [];
2025-09-10 16:07:19 +02:00
$spent = $this -> opsRepository -> listExpenses ( $start , $end , null , new Collection () -> push ( $budget ));
2025-08-15 07:50:13 +02:00
$expenses = $this -> processExpenses ( $budget -> id , $spent , $start , $end );
$converter = new ExchangeRateConverter ();
2025-08-15 13:37:27 +02:00
$currencies = [ $this -> primaryCurrency -> id => $this -> primaryCurrency ];
2025-07-30 06:59:58 +02:00
/**
2025-08-15 13:37:27 +02:00
* @ var int $currencyId
2025-07-30 06:59:58 +02:00
* @ var array $row
*/
foreach ( $expenses as $currencyId => $row ) {
// budgeted, left and overspent are now 0.
2025-08-15 13:37:27 +02:00
$limit = $this -> filterLimit ( $currencyId , $limits );
2025-08-15 07:50:13 +02:00
// primary currency entries
$row [ 'pc_budgeted' ] = '0' ;
2025-08-15 13:37:27 +02:00
$row [ 'pc_spent' ] = '0' ;
2025-08-15 07:50:13 +02:00
$row [ 'pc_left' ] = '0' ;
$row [ 'pc_overspent' ] = '0' ;
2025-09-07 17:42:16 +02:00
if ( $limit instanceof BudgetLimit ) {
2025-07-30 06:59:58 +02:00
$row [ 'budgeted' ] = $limit -> amount ;
2025-09-07 17:42:16 +02:00
$row [ 'left' ] = bcsub (( string ) $row [ 'budgeted' ], bcmul (( string ) $row [ 'spent' ], '-1' ));
2025-07-30 06:59:58 +02:00
$row [ 'overspent' ] = bcmul ( $row [ 'left' ], '-1' );
$row [ 'left' ] = 1 === bccomp ( $row [ 'left' ], '0' ) ? $row [ 'left' ] : '0' ;
$row [ 'overspent' ] = 1 === bccomp ( $row [ 'overspent' ], '0' ) ? $row [ 'overspent' ] : '0' ;
}
2025-08-15 07:50:13 +02:00
// convert data if necessary.
if ( true === $this -> convertToPrimary && $currencyId !== $this -> primaryCurrency -> id ) {
2025-09-07 14:49:49 +02:00
$currencies [ $currencyId ] ? ? = Amount :: getTransactionCurrencyById ( $currencyId );
2025-08-15 13:37:27 +02:00
$row [ 'pc_budgeted' ] = $converter -> convert ( $currencies [ $currencyId ], $this -> primaryCurrency , $start , $row [ 'budgeted' ]);
$row [ 'pc_spent' ] = $converter -> convert ( $currencies [ $currencyId ], $this -> primaryCurrency , $start , $row [ 'spent' ]);
$row [ 'pc_left' ] = $converter -> convert ( $currencies [ $currencyId ], $this -> primaryCurrency , $start , $row [ 'left' ]);
$row [ 'pc_overspent' ] = $converter -> convert ( $currencies [ $currencyId ], $this -> primaryCurrency , $start , $row [ 'overspent' ]);
2025-08-15 07:50:13 +02:00
}
if ( true === $this -> convertToPrimary && $currencyId === $this -> primaryCurrency -> id ) {
2025-08-15 13:37:27 +02:00
$row [ 'pc_budgeted' ] = $row [ 'budgeted' ];
$row [ 'pc_spent' ] = $row [ 'spent' ];
$row [ 'pc_left' ] = $row [ 'left' ];
$row [ 'pc_overspent' ] = $row [ 'overspent' ];
2025-08-15 07:50:13 +02:00
}
2025-08-15 13:37:27 +02:00
$rows [] = $row ;
2025-03-25 17:27:59 +01:00
}
2025-07-30 06:59:58 +02:00
2025-03-25 17:27:59 +01:00
// is always an array
2025-08-15 13:37:27 +02:00
$return = [];
2025-03-25 17:27:59 +01:00
foreach ( $rows as $row ) {
$current = [
2025-08-15 13:37:27 +02:00
'label' => $budget -> name ,
'currency_id' => ( string ) $row [ 'currency_id' ],
'currency_name' => $row [ 'currency_name' ],
'currency_code' => $row [ 'currency_code' ],
'currency_decimal_places' => $row [ 'currency_decimal_places' ],
2025-08-15 07:50:13 +02:00
'primary_currency_id' => ( string ) $this -> primaryCurrency -> id ,
'primary_currency_name' => $this -> primaryCurrency -> name ,
'primary_currency_code' => $this -> primaryCurrency -> code ,
'primary_currency_symbol' => $this -> primaryCurrency -> symbol ,
'primary_currency_decimal_places' => $this -> primaryCurrency -> decimal_places ,
2025-08-15 13:37:27 +02:00
'period' => null ,
'date' => $row [ 'start' ],
'start_date' => $row [ 'start' ],
'end_date' => $row [ 'end' ],
'yAxisID' => 0 ,
'type' => 'bar' ,
'entries' => [
2025-07-26 06:47:21 +02:00
'budgeted' => $row [ 'budgeted' ],
2025-03-25 17:27:59 +01:00
'spent' => $row [ 'spent' ],
'left' => $row [ 'left' ],
'overspent' => $row [ 'overspent' ],
],
2025-08-15 13:37:27 +02:00
'pc_entries' => [
2025-08-15 07:50:13 +02:00
'budgeted' => $row [ 'pc_budgeted' ],
2025-08-15 19:57:32 +02:00
'spent' => $row [ 'pc_spent' ],
'left' => $row [ 'pc_left' ],
'overspent' => $row [ 'pc_overspent' ],
2025-08-15 07:44:14 +02:00
],
2025-03-25 17:27:59 +01:00
];
$return [] = $current ;
}
return $return ;
}
2025-09-07 07:56:10 +02:00
// /**
// * When no budget 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.
// *
// * @throws FireflyException
// */
// private function noBudgetLimits(Budget $budget, Carbon $start, Carbon $end): array
// {
2025-09-10 16:07:19 +02:00
// $spent = $this->opsRepository->listExpenses($start, $end, null, new Collection()->push($budget));
2025-09-07 07:56:10 +02:00
//
// return $this->processExpenses($budget->id, $spent, $start, $end);
// }
2025-03-25 17:27:59 +01:00
/**
* Shared between the " noBudgetLimits " function and " processLimit " . Will take a single set of expenses and return
* its info .
*
* @ throws FireflyException
*/
2025-07-26 06:47:21 +02:00
private function processExpenses ( int $budgetId , array $spent , Carbon $start , Carbon $end ) : array
2025-03-25 17:27:59 +01:00
{
$return = [];
/**
* This array contains the expenses in this budget . Grouped per currency .
* The grouping is on the main currency only .
*
2025-08-15 13:37:27 +02:00
* @ var int $currencyId
2025-03-25 17:27:59 +01:00
* @ var array $block
*/
2025-07-26 06:47:21 +02:00
foreach ( $spent as $currencyId => $block ) {
2025-09-07 14:49:49 +02:00
$this -> currencies [ $currencyId ] ? ? = Amount :: getTransactionCurrencyById ( $currencyId );
2025-03-25 17:27:59 +01:00
$return [ $currencyId ] ? ? = [
2025-07-26 06:47:21 +02:00
'currency_id' => ( string ) $currencyId ,
2025-03-25 17:27:59 +01:00
'currency_code' => $block [ 'currency_code' ],
'currency_name' => $block [ 'currency_name' ],
'currency_symbol' => $block [ 'currency_symbol' ],
2025-07-26 06:47:21 +02:00
'currency_decimal_places' => ( int ) $block [ 'currency_decimal_places' ],
2025-03-25 17:27:59 +01:00
'start' => $start -> toAtomString (),
'end' => $end -> toAtomString (),
2025-07-26 06:47:21 +02:00
'budgeted' => '0' ,
2025-03-25 17:27:59 +01:00
'spent' => '0' ,
'left' => '0' ,
'overspent' => '0' ,
];
2025-08-15 13:37:27 +02:00
$currentBudgetArray = $block [ 'budgets' ][ $budgetId ];
2025-03-25 17:27:59 +01:00
// var_dump($return);
/** @var array $journal */
foreach ( $currentBudgetArray [ 'transaction_journals' ] as $journal ) {
2025-09-07 14:28:58 +02:00
/** @var numeric-string $amount */
2025-09-07 14:58:46 +02:00
$amount = ( string ) $journal [ 'amount' ];
2025-09-07 14:28:58 +02:00
$return [ $currencyId ][ 'spent' ] = bcadd ( $return [ $currencyId ][ 'spent' ], $amount );
2025-03-25 17:27:59 +01:00
}
}
return $return ;
}
2025-09-07 07:56:10 +02:00
// /**
// * Function that processes each budget limit (per budget).
// *
// * If you have a budget limit in EUR, only transactions in EUR will be considered.
// * If you have a budget limit in GBP, only transactions in GBP will be considered.
// *
// * If you have a budget limit in EUR, and a transaction in GBP, it will not be considered for the EUR budget limit.
// *
// * @throws FireflyException
// */
// private function budgetLimits(Budget $budget, Collection $limits): array
// {
// Log::debug(sprintf('Now in budgetLimits(#%d)', $budget->id));
// $data = [];
//
// /** @var BudgetLimit $limit */
// foreach ($limits as $limit) {
// $data = array_merge($data, $this->processLimit($budget, $limit));
// }
//
// return $data;
// }
// /**
// * @throws FireflyException
// */
// private function processLimit(Budget $budget, BudgetLimit $limit): array
// {
// Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__));
// $end = clone $limit->end_date;
// $end->endOfDay();
2025-09-10 16:07:19 +02:00
// $spent = $this->opsRepository->listExpenses($limit->start_date, $end, null, new Collection()->push($budget));
2025-09-07 07:56:10 +02:00
// $limitCurrencyId = $limit->transaction_currency_id;
//
// /** @var array $entry */
// // only spent the entry where the entry's currency matches the budget limit's currency
// // so $filtered will only have 1 or 0 entries
// $filtered = array_filter($spent, fn ($entry) => $entry['currency_id'] === $limitCurrencyId);
// $result = $this->processExpenses($budget->id, $filtered, $limit->start_date, $end);
// if (1 === count($result)) {
// $compare = bccomp($limit->amount, (string)app('steam')->positive($result[$limitCurrencyId]['spent']));
// $result[$limitCurrencyId]['budgeted'] = $limit->amount;
// if (1 === $compare) {
// // convert this amount into the primary currency:
// $result[$limitCurrencyId]['left'] = bcadd($limit->amount, (string)$result[$limitCurrencyId]['spent']);
// }
// if ($compare <= 0) {
// $result[$limitCurrencyId]['overspent'] = app('steam')->positive(bcadd($limit->amount, (string)$result[$limitCurrencyId]['spent']));
// }
// }
//
// return $result;
// }
2025-07-30 06:59:58 +02:00
private function filterLimit ( int $currencyId , Collection $limits ) : ? BudgetLimit
{
2025-07-30 14:37:57 +02:00
$amount = '0' ;
$limit = null ;
$converter = new ExchangeRateConverter ();
2025-07-30 09:59:52 +02:00
/** @var BudgetLimit $current */
foreach ( $limits as $current ) {
2025-07-31 20:38:57 +02:00
if ( true === $this -> convertToPrimary ) {
if ( $current -> transaction_currency_id === $this -> primaryCurrency -> id ) {
2025-07-30 09:59:52 +02:00
// simply add it.
$amount = bcadd ( $amount , ( string ) $current -> amount );
2025-07-30 14:37:57 +02:00
Log :: debug ( sprintf ( 'Set amount in limit to %s' , $amount ));
2025-07-30 09:59:52 +02:00
}
2025-07-31 20:38:57 +02:00
if ( $current -> transaction_currency_id !== $this -> primaryCurrency -> id ) {
2025-07-30 09:59:52 +02:00
// convert and then add it.
2025-08-02 11:07:35 +02:00
$converted = $converter -> convert ( $current -> transactionCurrency , $this -> primaryCurrency , $current -> start_date , $current -> amount );
2025-07-30 14:37:57 +02:00
$amount = bcadd ( $amount , $converted );
2025-07-31 20:38:57 +02:00
Log :: debug ( sprintf ( 'Budgeted in limit #%d: %s %s, converted to %s %s' , $current -> id , $current -> transactionCurrency -> code , $current -> amount , $this -> primaryCurrency -> code , $converted ));
2025-07-30 09:59:52 +02:00
Log :: debug ( sprintf ( 'Set amount in limit to %s' , $amount ));
}
}
if ( $current -> transaction_currency_id === $currencyId ) {
2025-07-30 14:37:57 +02:00
$limit = $current ;
2025-07-30 06:59:58 +02:00
}
}
2025-07-31 20:38:57 +02:00
if ( null !== $limit && true === $this -> convertToPrimary ) {
2025-07-30 09:59:52 +02:00
// convert and add all amounts.
$limit -> amount = app ( 'steam' ) -> positive ( $amount );
Log :: debug ( sprintf ( 'Final amount in limit with converted amount %s' , $limit -> amount ));
}
2025-07-30 08:56:14 +02:00
2025-07-30 09:59:52 +02:00
return $limit ;
2025-07-30 06:59:58 +02:00
}
2025-03-25 17:27:59 +01:00
}