From 0b5e0a268a828e8d107d0b261848506a6099c148 Mon Sep 17 00:00:00 2001 From: James Cole Date: Mon, 15 May 2023 06:18:02 +0200 Subject: [PATCH] feat: a budget type that will rollover but also incorporate overspending from the previous period (if any) --- .../Search/TransactionController.php | 4 +- .../V1/Controllers/System/CronController.php | 4 ++ .../Requests/Models/Budget/StoreRequest.php | 6 +- .../Requests/Models/Budget/UpdateRequest.php | 3 +- app/Console/Commands/Tools/Cron.php | 6 +- app/Enums/AutoBudgetType.php | 1 + .../Controllers/Budget/CreateController.php | 1 + .../Controllers/Budget/EditController.php | 1 + app/Http/Requests/BudgetFormStoreRequest.php | 2 +- app/Jobs/CreateAutoBudgetLimits.php | 71 +++++++++++++++++++ app/Models/AutoBudget.php | 1 + app/Repositories/Budget/BudgetRepository.php | 3 + app/Transformers/BudgetTransformer.php | 1 + resources/lang/en_US/firefly.php | 2 + resources/views/budgets/index.twig | 3 + 15 files changed, 100 insertions(+), 9 deletions(-) diff --git a/app/Api/V1/Controllers/Search/TransactionController.php b/app/Api/V1/Controllers/Search/TransactionController.php index 2a157532f9..f4fbe86782 100644 --- a/app/Api/V1/Controllers/Search/TransactionController.php +++ b/app/Api/V1/Controllers/Search/TransactionController.php @@ -71,6 +71,8 @@ class TransactionController extends Controller $resource = new Collection($transactions, $transformer, 'transactions'); $resource->setPaginator(new IlluminatePaginatorAdapter($groups)); - return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE); + $array = $manager->createData($resource)->toArray(); + + return response()->json($array)->header('Content-Type', self::CONTENT_TYPE); } } diff --git a/app/Api/V1/Controllers/System/CronController.php b/app/Api/V1/Controllers/System/CronController.php index e944a80c4e..8faa85a521 100644 --- a/app/Api/V1/Controllers/System/CronController.php +++ b/app/Api/V1/Controllers/System/CronController.php @@ -54,6 +54,10 @@ class CronController extends Controller $return = []; $return['recurring_transactions'] = $this->runRecurring($config['force'], $config['date']); $return['auto_budgets'] = $this->runAutoBudget($config['force'], $config['date']); + if (true === config('cer.enabled')) { + $return['exchange_rates'] = $this->exchangeRatesCronJob($config['force'], $config['date']); + } + $return['bill_warnings'] = $this->billWarningCronJob($config['force'], $config['date']); return response()->json($return); } diff --git a/app/Api/V1/Requests/Models/Budget/StoreRequest.php b/app/Api/V1/Requests/Models/Budget/StoreRequest.php index 4ab222f327..d53e144fe9 100644 --- a/app/Api/V1/Requests/Models/Budget/StoreRequest.php +++ b/app/Api/V1/Requests/Models/Budget/StoreRequest.php @@ -79,9 +79,9 @@ class StoreRequest extends FormRequest 'currency_code' => 'exists:transaction_currencies,code', 'notes' => 'nullable|between:1,65536', // auto budget info - 'auto_budget_type' => 'in:reset,rollover,none', - 'auto_budget_amount' => 'numeric|min:0|max:1000000000|required_if:auto_budget_type,reset|required_if:auto_budget_type,rollover', - 'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly|required_if:auto_budget_type,reset|required_if:auto_budget_type,rollover', + 'auto_budget_type' => 'in:reset,rollover,adjusted,none', + 'auto_budget_amount' => 'numeric|min:0|max:1000000000|required_if:auto_budget_type,reset|required_if:auto_budget_type,rollover|required_if:auto_budget_type,adjusted', + 'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly|required_if:auto_budget_type,reset|required_if:auto_budget_type,rollover|required_if:auto_budget_type,adjusted', ]; } diff --git a/app/Api/V1/Requests/Models/Budget/UpdateRequest.php b/app/Api/V1/Requests/Models/Budget/UpdateRequest.php index ea584488ee..f4de11b76b 100644 --- a/app/Api/V1/Requests/Models/Budget/UpdateRequest.php +++ b/app/Api/V1/Requests/Models/Budget/UpdateRequest.php @@ -67,6 +67,7 @@ class UpdateRequest extends FormRequest 'none' => 0, 'reset' => 1, 'rollover' => 2, + 'adjusted' => 3, ]; $allData['auto_budget_type'] = $types[$allData['auto_budget_type']] ?? 0; } @@ -88,7 +89,7 @@ class UpdateRequest extends FormRequest 'name' => sprintf('between:1,100|uniqueObjectForUser:budgets,name,%d', $budget->id), 'active' => [new IsBoolean()], 'notes' => 'nullable|between:1,65536', - 'auto_budget_type' => 'in:reset,rollover,none', + 'auto_budget_type' => 'in:reset,rollover,adjusted,none', 'auto_budget_currency_id' => 'exists:transaction_currencies,id', 'auto_budget_currency_code' => 'exists:transaction_currencies,code', 'auto_budget_amount' => 'min:0|max:1000000000', diff --git a/app/Console/Commands/Tools/Cron.php b/app/Console/Commands/Tools/Cron.php index 83c9d1f222..0ff0874a63 100644 --- a/app/Console/Commands/Tools/Cron.php +++ b/app/Console/Commands/Tools/Cron.php @@ -77,7 +77,7 @@ class Cron extends Command */ if (true === config('cer.enabled')) { try { - $this->exchangeRatesCronJob($force, $date); + //$this->exchangeRatesCronJob($force, $date); } catch (FireflyException $e) { Log::error($e->getMessage()); Log::error($e->getTraceAsString()); @@ -89,7 +89,7 @@ class Cron extends Command * Fire recurring transaction cron job. */ try { - $this->recurringCronJob($force, $date); + //$this->recurringCronJob($force, $date); } catch (FireflyException $e) { Log::error($e->getMessage()); Log::error($e->getTraceAsString()); @@ -111,7 +111,7 @@ class Cron extends Command * Fire bill warning cron job */ try { - $this->billWarningCronJob($force, $date); + //$this->billWarningCronJob($force, $date); } catch (FireflyException $e) { Log::error($e->getMessage()); Log::error($e->getTraceAsString()); diff --git a/app/Enums/AutoBudgetType.php b/app/Enums/AutoBudgetType.php index a9d8719021..ac8341a212 100644 --- a/app/Enums/AutoBudgetType.php +++ b/app/Enums/AutoBudgetType.php @@ -31,4 +31,5 @@ enum AutoBudgetType: int { case AUTO_BUDGET_RESET = 1; case AUTO_BUDGET_ROLLOVER = 2; + case AUTO_BUDGET_ADJUSTED = 3; } diff --git a/app/Http/Controllers/Budget/CreateController.php b/app/Http/Controllers/Budget/CreateController.php index 0ae5c47199..8de40a769d 100644 --- a/app/Http/Controllers/Budget/CreateController.php +++ b/app/Http/Controllers/Budget/CreateController.php @@ -78,6 +78,7 @@ class CreateController extends Controller 0 => (string)trans('firefly.auto_budget_none'), AutoBudget::AUTO_BUDGET_RESET => (string)trans('firefly.auto_budget_reset'), AutoBudget::AUTO_BUDGET_ROLLOVER => (string)trans('firefly.auto_budget_rollover'), + AutoBudget::AUTO_BUDGET_ADJUSTED => (string)trans('firefly.auto_budget_adjusted'), ]; $autoBudgetPeriods = [ 'daily' => (string)trans('firefly.auto_budget_period_daily'), diff --git a/app/Http/Controllers/Budget/EditController.php b/app/Http/Controllers/Budget/EditController.php index dd2847d82c..e35b1e30fc 100644 --- a/app/Http/Controllers/Budget/EditController.php +++ b/app/Http/Controllers/Budget/EditController.php @@ -82,6 +82,7 @@ class EditController extends Controller 0 => (string)trans('firefly.auto_budget_none'), AutoBudget::AUTO_BUDGET_RESET => (string)trans('firefly.auto_budget_reset'), AutoBudget::AUTO_BUDGET_ROLLOVER => (string)trans('firefly.auto_budget_rollover'), + AutoBudget::AUTO_BUDGET_ADJUSTED => (string)trans('firefly.auto_budget_adjusted'), ]; $autoBudgetPeriods = [ 'daily' => (string)trans('firefly.auto_budget_period_daily'), diff --git a/app/Http/Requests/BudgetFormStoreRequest.php b/app/Http/Requests/BudgetFormStoreRequest.php index d0706a57ab..3e1817b93c 100644 --- a/app/Http/Requests/BudgetFormStoreRequest.php +++ b/app/Http/Requests/BudgetFormStoreRequest.php @@ -65,7 +65,7 @@ class BudgetFormStoreRequest extends FormRequest return [ 'name' => 'required|between:1,100|uniqueObjectForUser:budgets,name', 'active' => 'numeric|between:0,1', - 'auto_budget_type' => 'numeric|integer|gte:0|lte:2', + 'auto_budget_type' => 'numeric|integer|gte:0|lte:3', 'auto_budget_currency_id' => 'exists:transaction_currencies,id', 'auto_budget_amount' => 'min:0|max:1000000000|required_if:auto_budget_type,1|required_if:auto_budget_type,2', 'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly', diff --git a/app/Jobs/CreateAutoBudgetLimits.php b/app/Jobs/CreateAutoBudgetLimits.php index e308384e04..fba256ac57 100644 --- a/app/Jobs/CreateAutoBudgetLimits.php +++ b/app/Jobs/CreateAutoBudgetLimits.php @@ -81,6 +81,70 @@ class CreateAutoBudgetLimits implements ShouldQueue } } + /** + * @param AutoBudget $autoBudget + * @return void + */ + private function createAdjustedLimit(AutoBudget $autoBudget): void + { + Log::debug(sprintf('Will now manage rollover for auto budget #%d', $autoBudget->id)); + // current period: + $start = app('navigation')->startOfPeriod($this->date, $autoBudget->period); + $end = app('navigation')->endOfPeriod($start, $autoBudget->period); + + // which means previous period: + $previousStart = app('navigation')->subtractPeriod($start, $autoBudget->period); + $previousEnd = app('navigation')->endOfPeriod($previousStart, $autoBudget->period); + + Log::debug( + sprintf( + 'Current period is %s-%s, so previous period is %s-%s', + $start->format('Y-m-d'), + $end->format('Y-m-d'), + $previousStart->format('Y-m-d'), + $previousEnd->format('Y-m-d') + ) + ); + + // has budget limit in previous period? + $budgetLimit = $this->findBudgetLimit($autoBudget->budget, $previousStart, $previousEnd); + + if (null === $budgetLimit) { + Log::debug('No budget limit exists in previous period, so create one.'); + // if not, create standard amount and we're done. + $this->createBudgetLimit($autoBudget, $start, $end); + return; + } + Log::debug('Budget limit exists for previous period.'); + + // if has one, calculate expenses and use that as a base. + $repository = app(OperationsRepositoryInterface::class); + $repository->setUser($autoBudget->budget->user); + $spent = $repository->sumExpenses($previousStart, $previousEnd, null, new Collection([$autoBudget->budget]), $autoBudget->transactionCurrency); + $currencyId = (int)$autoBudget->transaction_currency_id; + $spentAmount = $spent[$currencyId]['sum'] ?? '0'; + Log::debug(sprintf('Spent in previous budget period (%s-%s) is %s', $previousStart->format('Y-m-d'), $previousEnd->format('Y-m-d'), $spentAmount)); + + // what you spent in previous period PLUS the amount for the current period, + // if that is more than zero, that's the amount that will be set. + + $budgetAvailable = bcadd(bcadd($budgetLimit->amount, $autoBudget->amount), $spentAmount); + $totalAmount = $autoBudget->amount; + Log::debug(sprintf('Total amount available for current budget period is %s', $budgetAvailable)); + + if (-1 !== bccomp( $budgetAvailable, $totalAmount)) { + Log::info(sprintf('There is no overspending, no need to adjust. Budget limit amount will be %s.', $totalAmount)); + // create budget limit: + $this->createBudgetLimit($autoBudget, $start, $end, $totalAmount); + } + if (1 !== bccomp($budgetAvailable, $totalAmount)) { + Log::info(sprintf('There was overspending, so the new amount will be %s.', $budgetAvailable)); + // create budget limit: + $this->createBudgetLimit($autoBudget, $start, $end, $budgetAvailable); + } + Log::debug(sprintf('Done with auto budget #%d', $autoBudget->id)); + } + /** * @param AutoBudget $autoBudget * @@ -148,6 +212,13 @@ class CreateAutoBudgetLimits implements ShouldQueue return; } + if (null === $budgetLimit && AutoBudget::AUTO_BUDGET_ADJUSTED === (int)$autoBudget->auto_budget_type) { + // budget limit exists already, + $this->createAdjustedLimit($autoBudget); + Log::debug(sprintf('Done with auto budget #%d', $autoBudget->id)); + + return; + } Log::debug(sprintf('Done with auto budget #%d', $autoBudget->id)); } diff --git a/app/Models/AutoBudget.php b/app/Models/AutoBudget.php index a05537bf99..7d1f646b5f 100644 --- a/app/Models/AutoBudget.php +++ b/app/Models/AutoBudget.php @@ -69,6 +69,7 @@ class AutoBudget extends Model public const AUTO_BUDGET_RESET = 1; public const AUTO_BUDGET_ROLLOVER = 2; + public const AUTO_BUDGET_ADJUSTED = 3; protected $fillable = ['budget_id','amount','period']; diff --git a/app/Repositories/Budget/BudgetRepository.php b/app/Repositories/Budget/BudgetRepository.php index 75213a621f..0257d08d01 100644 --- a/app/Repositories/Budget/BudgetRepository.php +++ b/app/Repositories/Budget/BudgetRepository.php @@ -829,6 +829,9 @@ class BudgetRepository implements BudgetRepositoryInterface if ('rollover' === $type) { $type = AutoBudget::AUTO_BUDGET_ROLLOVER; } + if('adjusted' === $type) { + $type = AutoBudget::AUTO_BUDGET_ADJUSTED; + } $repos = app(CurrencyRepositoryInterface::class); $currency = null; diff --git a/app/Transformers/BudgetTransformer.php b/app/Transformers/BudgetTransformer.php index 2ea099c621..ca3daf4139 100644 --- a/app/Transformers/BudgetTransformer.php +++ b/app/Transformers/BudgetTransformer.php @@ -78,6 +78,7 @@ class BudgetTransformer extends AbstractTransformer $types = [ AutoBudget::AUTO_BUDGET_RESET => 'reset', AutoBudget::AUTO_BUDGET_ROLLOVER => 'rollover', + AutoBudget::AUTO_BUDGET_ADJUSTED => 'adjusted', ]; if (null !== $autoBudget) { diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 461cd5f510..920756f9fd 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -1656,6 +1656,7 @@ return [ 'auto_budget_none' => 'No auto-budget', 'auto_budget_reset' => 'Set a fixed amount every period', 'auto_budget_rollover' => 'Add an amount every period', + 'auto_budget_adjusted' => 'Add an amount every period and correct for overspending', 'auto_budget_period_daily' => 'Daily', 'auto_budget_period_weekly' => 'Weekly', 'auto_budget_period_monthly' => 'Monthly', @@ -1665,6 +1666,7 @@ return [ 'auto_budget_help' => 'You can read more about this feature in the help. Click the top-right (?) icon.', 'auto_budget_reset_icon' => 'This budget will be set periodically', 'auto_budget_rollover_icon' => 'The budget amount will increase periodically', + 'auto_budget_adjusted_icon' => 'The budget amount will increase periodically and will correct for overspending', 'remove_budgeted_amount' => 'Remove budgeted amount in :currency', // bills: diff --git a/resources/views/budgets/index.twig b/resources/views/budgets/index.twig index 0ebfacb9a8..4edf9d4843 100644 --- a/resources/views/budgets/index.twig +++ b/resources/views/budgets/index.twig @@ -239,6 +239,9 @@ {% if 2 == budget.auto_budget.auto_budget_type %} {% endif %} + {% if 3 == budget.auto_budget.auto_budget_type %} + + {% endif %} {% endif %} {% if budget.attachments.count() > 0 %}