mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-10-23 20:38:46 +00:00
Better management and calculation of available budgets.
This commit is contained in:
221
app/Handlers/Events/Model/BudgetLimitHandler.php
Normal file
221
app/Handlers/Events/Model/BudgetLimitHandler.php
Normal file
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/*
|
||||
* BudgetLimitHandler.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/>.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Handlers\Events\Model;
|
||||
|
||||
use FireflyIII\Events\Model\BudgetLimit\Created;
|
||||
use FireflyIII\Events\Model\BudgetLimit\Deleted;
|
||||
use FireflyIII\Events\Model\BudgetLimit\Updated;
|
||||
use FireflyIII\Models\AvailableBudget;
|
||||
use FireflyIII\Models\BudgetLimit;
|
||||
use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Spatie\Period\Boundaries;
|
||||
use Spatie\Period\Period;
|
||||
use Spatie\Period\Precision;
|
||||
|
||||
/**
|
||||
* Class BudgetLimitHandler
|
||||
*/
|
||||
class BudgetLimitHandler
|
||||
{
|
||||
/**
|
||||
* @param Created $event
|
||||
* @return void
|
||||
*/
|
||||
public function created(Created $event): void
|
||||
{
|
||||
Log::debug(sprintf('BudgetLimitHandler::created(%s)', $event->budgetLimit->id));
|
||||
$this->updateAvailableBudget($event->budgetLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Updated $event
|
||||
* @return void
|
||||
*/
|
||||
public function updated(Updated $event): void
|
||||
{
|
||||
Log::debug(sprintf('BudgetLimitHandler::updated(%s)', $event->budgetLimit->id));
|
||||
$this->updateAvailableBudget($event->budgetLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Deleted $event
|
||||
* @return void
|
||||
*/
|
||||
public function deleted(Deleted $event): void
|
||||
{
|
||||
Log::debug(sprintf('BudgetLimitHandler::deleted(%s)', $event->budgetLimit->id));
|
||||
$this->updateAvailableBudget($event->budgetLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AvailableBudget $availableBudget
|
||||
* @return void
|
||||
*/
|
||||
private function calculateAmount(AvailableBudget $availableBudget): void
|
||||
{
|
||||
$repository = app(BudgetLimitRepositoryInterface::class);
|
||||
$repository->setUser($availableBudget->user);
|
||||
$newAmount = '0';
|
||||
$abPeriod = Period::make($availableBudget->start_date, $availableBudget->end_date, Precision::DAY());
|
||||
Log::debug(
|
||||
sprintf(
|
||||
'Now at AB #%d, ("%s" to "%s")',
|
||||
$availableBudget->id,
|
||||
$availableBudget->start_date->format('Y-m-d'),
|
||||
$availableBudget->end_date->format('Y-m-d')
|
||||
)
|
||||
);
|
||||
// have to recalc everything just in case.
|
||||
$set = $repository->getAllBudgetLimitsByCurrency($availableBudget->transactionCurrency, $availableBudget->start_date, $availableBudget->end_date);
|
||||
/** @var BudgetLimit $budgetLimit */
|
||||
foreach ($set as $budgetLimit) {
|
||||
Log::debug(
|
||||
sprintf(
|
||||
'Found interesting budget limit #%d ("%s" to "%s")',
|
||||
$budgetLimit->id,
|
||||
$budgetLimit->start_date->format('Y-m-d'),
|
||||
$budgetLimit->end_date->format('Y-m-d')
|
||||
)
|
||||
);
|
||||
// overlap in days:
|
||||
$limitPeriod = Period::make(
|
||||
$budgetLimit->start_date,
|
||||
$budgetLimit->end_date,
|
||||
precision: Precision::DAY(),
|
||||
boundaries: Boundaries::EXCLUDE_NONE()
|
||||
);
|
||||
// if both equal eachother, amount from this BL must be added to the AB
|
||||
if ($limitPeriod->equals($abPeriod)) {
|
||||
Log::debug('Limit period is the same as AB period.');
|
||||
$newAmount = bcadd($newAmount, $budgetLimit->amount);
|
||||
}
|
||||
// if budget limit period inside AB period, can be added in full.
|
||||
|
||||
if (!$limitPeriod->equals($abPeriod) && $abPeriod->contains($limitPeriod)) {
|
||||
Log::debug('Limit period falls inside of AB period.');
|
||||
$newAmount = bcadd($newAmount, $budgetLimit->amount);
|
||||
}
|
||||
if (!$limitPeriod->equals($abPeriod) && $abPeriod->overlapsWith($limitPeriod)) {
|
||||
Log::debug('Limit period overlaps AB period.');
|
||||
$overlap = $abPeriod->overlap($limitPeriod);
|
||||
if (null !== $overlap) {
|
||||
$length = $overlap->length();
|
||||
$daily = bcmul($this->getDailyAmount($budgetLimit), (string)$length);
|
||||
Log::debug(sprintf('Length of overlap is %d days, so daily amount is %s', $length, $daily));
|
||||
$newAmount = bcadd($newAmount, $daily);
|
||||
}
|
||||
}
|
||||
}
|
||||
Log::debug(sprintf('Concluded new amount for this AB must be %s', $newAmount));
|
||||
$availableBudget->amount = $newAmount;
|
||||
$availableBudget->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param BudgetLimit $budgetLimit
|
||||
* @return string
|
||||
*/
|
||||
private function getDailyAmount(BudgetLimit $budgetLimit): string
|
||||
{
|
||||
$limitPeriod = Period::make(
|
||||
$budgetLimit->start_date,
|
||||
$budgetLimit->end_date,
|
||||
precision: Precision::DAY(),
|
||||
boundaries: Boundaries::EXCLUDE_NONE()
|
||||
);
|
||||
$days = $limitPeriod->length();
|
||||
$amount = bcdiv((string)$budgetLimit->amount, (string)$days, 12);
|
||||
Log::debug(
|
||||
sprintf('Total amount for budget limit #%d is %s. Nr. of days is %d. Amount per day is %s', $budgetLimit->id, $budgetLimit->amount, $days, $amount)
|
||||
);
|
||||
return $amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param BudgetLimit $budgetLimit
|
||||
* @return void
|
||||
* @throws \FireflyIII\Exceptions\FireflyException
|
||||
* @throws \Psr\Container\ContainerExceptionInterface
|
||||
* @throws \Psr\Container\NotFoundExceptionInterface
|
||||
*/
|
||||
private function updateAvailableBudget(BudgetLimit $budgetLimit): void
|
||||
{
|
||||
Log::debug(sprintf('Now in updateAvailableBudget(#%d)', $budgetLimit->id));
|
||||
|
||||
// based on the view range of the user (month week quarter etc) the budget limit could
|
||||
// either overlap multiple available budget periods or be contained in a single one.
|
||||
// all have to be created or updated.
|
||||
$viewRange = app('preferences')->get('viewRange', '1M')->data;
|
||||
$start = app('navigation')->startOfPeriod($budgetLimit->start_date, $viewRange);
|
||||
$end = app('navigation')->startOfPeriod($budgetLimit->end_date, $viewRange);
|
||||
$end = app('navigation')->endOfPeriod($end, $viewRange);
|
||||
$user = $budgetLimit->budget->user;
|
||||
|
||||
// limit period in total is:
|
||||
$limitPeriod = Period::make($start, $end, precision: Precision::DAY(), boundaries: Boundaries::EXCLUDE_NONE());
|
||||
|
||||
// from the start until the end of the budget limit, need to loop!
|
||||
$current = clone $start;
|
||||
while ($current <= $end) {
|
||||
$currentEnd = app('navigation')->endOfPeriod($current, $viewRange);
|
||||
|
||||
// create or find AB for this particular period, and set the amount accordingly.
|
||||
/** @var AvailableBudget $availableBudget */
|
||||
$availableBudget = $user->availableBudgets()->where('start_date', $current->format('Y-m-d'))->where(
|
||||
'end_date',
|
||||
$currentEnd->format('Y-m-d')
|
||||
)->where('transaction_currency_id', $budgetLimit->transaction_currency_id)->first();
|
||||
if (null !== $availableBudget) {
|
||||
Log::debug('Found AB, will update.');
|
||||
$this->calculateAmount($availableBudget);
|
||||
continue;
|
||||
}
|
||||
// if not exists:
|
||||
$currentPeriod = Period::make($current, $currentEnd, precision: Precision::DAY(), boundaries: Boundaries::EXCLUDE_NONE());
|
||||
$daily = $this->getDailyAmount($budgetLimit);
|
||||
$amount = bcmul($daily, (string)$currentPeriod->length(), 12);
|
||||
|
||||
// no need to calculate if period is equal.
|
||||
if ($currentPeriod->equals($limitPeriod)) {
|
||||
$amount = $budgetLimit->amount;
|
||||
}
|
||||
Log::debug(sprintf('Will create AB for period %s to %s', $current->format('Y-m-d'), $currentEnd->format('Y-m-d')));
|
||||
$availableBudget = new AvailableBudget(
|
||||
[
|
||||
'user_id' => $budgetLimit->budget->user->id,
|
||||
'transaction_currency_id' => $budgetLimit->transaction_currency_id,
|
||||
'start_date' => $current,
|
||||
'end_date' => $currentEnd,
|
||||
'amount' => $amount,
|
||||
]
|
||||
);
|
||||
$availableBudget->save();
|
||||
|
||||
// prep for next loop
|
||||
$current = app('navigation')->addPeriod($current, $viewRange, 0);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user