mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-10-11 23:28:54 +00:00
Include budget events.
This commit is contained in:
@@ -96,7 +96,6 @@ class ShowController extends Controller
|
|||||||
$paginator = new LengthAwarePaginator($budgetLimits, $count, $pageSize, $this->parameters->get('page'));
|
$paginator = new LengthAwarePaginator($budgetLimits, $count, $pageSize, $this->parameters->get('page'));
|
||||||
$paginator->setPath(route('api.v1.budgets.limits.index', [$budget->id]).$this->buildParams());
|
$paginator->setPath(route('api.v1.budgets.limits.index', [$budget->id]).$this->buildParams());
|
||||||
|
|
||||||
|
|
||||||
// enrich
|
// enrich
|
||||||
$enrichment = new BudgetLimitEnrichment();
|
$enrichment = new BudgetLimitEnrichment();
|
||||||
$enrichment->setUser($admin);
|
$enrichment->setUser($admin);
|
||||||
|
@@ -27,13 +27,19 @@ namespace FireflyIII\Generator\Webhook;
|
|||||||
use FireflyIII\Enums\WebhookResponse;
|
use FireflyIII\Enums\WebhookResponse;
|
||||||
use FireflyIII\Enums\WebhookTrigger;
|
use FireflyIII\Enums\WebhookTrigger;
|
||||||
use FireflyIII\Exceptions\FireflyException;
|
use FireflyIII\Exceptions\FireflyException;
|
||||||
|
use FireflyIII\Models\Budget;
|
||||||
|
use FireflyIII\Models\BudgetLimit;
|
||||||
use FireflyIII\Models\Transaction;
|
use FireflyIII\Models\Transaction;
|
||||||
use FireflyIII\Models\TransactionGroup;
|
use FireflyIII\Models\TransactionGroup;
|
||||||
use FireflyIII\Models\TransactionJournal;
|
use FireflyIII\Models\TransactionJournal;
|
||||||
use FireflyIII\Models\Webhook;
|
use FireflyIII\Models\Webhook;
|
||||||
use FireflyIII\Models\WebhookMessage;
|
use FireflyIII\Models\WebhookMessage;
|
||||||
use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment;
|
use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment;
|
||||||
|
use FireflyIII\Support\JsonApi\Enrichments\BudgetEnrichment;
|
||||||
|
use FireflyIII\Support\JsonApi\Enrichments\BudgetLimitEnrichment;
|
||||||
use FireflyIII\Transformers\AccountTransformer;
|
use FireflyIII\Transformers\AccountTransformer;
|
||||||
|
use FireflyIII\Transformers\BudgetLimitTransformer;
|
||||||
|
use FireflyIII\Transformers\BudgetTransformer;
|
||||||
use FireflyIII\Transformers\TransactionGroupTransformer;
|
use FireflyIII\Transformers\TransactionGroupTransformer;
|
||||||
use FireflyIII\User;
|
use FireflyIII\User;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
@@ -109,18 +115,16 @@ class StandardMessageGenerator implements MessageGeneratorInterface
|
|||||||
*/
|
*/
|
||||||
private function generateMessage(Webhook $webhook, Model $model): void
|
private function generateMessage(Webhook $webhook, Model $model): void
|
||||||
{
|
{
|
||||||
$class = $model::class;
|
$class = $model::class;
|
||||||
// Line is ignored because all of Firefly III's Models have an id property.
|
// Line is ignored because all of Firefly III's Models have an id property.
|
||||||
Log::debug(sprintf('Now in generateMessage(#%d, %s#%d)', $webhook->id, $class, $model->id));
|
Log::debug(sprintf('Now in generateMessage(#%d, %s#%d)', $webhook->id, $class, $model->id));
|
||||||
Log::debug($webhook->response);
|
|
||||||
Log::debug(WebhookResponse::from($webhook->response)->name);
|
|
||||||
$uuid = Uuid::uuid4();
|
$uuid = Uuid::uuid4();
|
||||||
$basicMessage = [
|
$basicMessage = [
|
||||||
'uuid' => $uuid->toString(),
|
'uuid' => $uuid->toString(),
|
||||||
'user_id' => 0,
|
'user_id' => 0,
|
||||||
'user_group_id' => 0,
|
'user_group_id' => 0,
|
||||||
'trigger' => WebhookTrigger::from((int) $webhook->trigger)->name,
|
'trigger' => WebhookTrigger::from((int)$webhook->trigger)->name,
|
||||||
'response' => WebhookResponse::from((int) $webhook->response)->name,
|
'response' => WebhookResponse::from((int)$webhook->response)->name,
|
||||||
'url' => $webhook->url,
|
'url' => $webhook->url,
|
||||||
'version' => sprintf('v%d', $this->getVersion()),
|
'version' => sprintf('v%d', $this->getVersion()),
|
||||||
'content' => [],
|
'content' => [],
|
||||||
@@ -133,7 +137,15 @@ class StandardMessageGenerator implements MessageGeneratorInterface
|
|||||||
Log::error(sprintf('Webhook #%d was given %s#%d to deal with but can\'t extract user ID from it.', $webhook->id, $class, $model->id));
|
Log::error(sprintf('Webhook #%d was given %s#%d to deal with but can\'t extract user ID from it.', $webhook->id, $class, $model->id));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
case Budget::class:
|
||||||
|
/** @var Budget $model */
|
||||||
|
$basicMessage['user_id'] = $model->user_id;
|
||||||
|
$basicMessage['user_group_id'] = $model->user_group_id;
|
||||||
|
break;
|
||||||
|
case BudgetLimit::class:
|
||||||
|
$basicMessage['user_id'] = $model->budget->user_id;
|
||||||
|
$basicMessage['user_group_id'] = $model->budget->user_group_id;
|
||||||
|
break;
|
||||||
case TransactionGroup::class:
|
case TransactionGroup::class:
|
||||||
/** @var TransactionGroup $model */
|
/** @var TransactionGroup $model */
|
||||||
$basicMessage['user_id'] = $model->user_id;
|
$basicMessage['user_id'] = $model->user_id;
|
||||||
@@ -148,6 +160,36 @@ class StandardMessageGenerator implements MessageGeneratorInterface
|
|||||||
Log::error(sprintf('The response code for webhook #%d is "%d" and the message generator cant handle it. Soft fail.', $webhook->id, $webhook->response));
|
Log::error(sprintf('The response code for webhook #%d is "%d" and the message generator cant handle it. Soft fail.', $webhook->id, $webhook->response));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
case WebhookResponse::BUDGET->value;
|
||||||
|
$basicMessage['content'] = [];
|
||||||
|
if($model instanceof Budget) {
|
||||||
|
$enrichment = new BudgetEnrichment();
|
||||||
|
$enrichment->setUser($model->user);
|
||||||
|
$model = $enrichment->enrichSingle($model);
|
||||||
|
$transformer = new BudgetTransformer();
|
||||||
|
$basicMessage['content'] = $transformer->transform($model);
|
||||||
|
}
|
||||||
|
if($model instanceof BudgetLimit) {
|
||||||
|
$user = $model->budget->user;
|
||||||
|
$enrichment = new BudgetEnrichment();
|
||||||
|
$enrichment->setUser($user);
|
||||||
|
$enrichment->setStart($model->start_date);
|
||||||
|
$enrichment->setEnd($model->end_date);
|
||||||
|
$budget = $enrichment->enrichSingle($model->budget);
|
||||||
|
|
||||||
|
$enrichment = new BudgetLimitEnrichment();
|
||||||
|
$enrichment->setUser($user);
|
||||||
|
|
||||||
|
$parameters = new ParameterBag();
|
||||||
|
$parameters->set('start', $model->start_date);
|
||||||
|
$parameters->set('end', $model->end_date);
|
||||||
|
|
||||||
|
$model = $enrichment->enrichSingle($model);
|
||||||
|
$transformer = new BudgetLimitTransformer();
|
||||||
|
$transformer->setParameters($parameters);
|
||||||
|
$basicMessage['content'] = $transformer->transform($model);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case WebhookResponse::NONE->value:
|
case WebhookResponse::NONE->value:
|
||||||
$basicMessage['content'] = [];
|
$basicMessage['content'] = [];
|
||||||
@@ -156,7 +198,7 @@ class StandardMessageGenerator implements MessageGeneratorInterface
|
|||||||
|
|
||||||
case WebhookResponse::TRANSACTIONS->value:
|
case WebhookResponse::TRANSACTIONS->value:
|
||||||
/** @var TransactionGroup $model */
|
/** @var TransactionGroup $model */
|
||||||
$transformer = new TransactionGroupTransformer();
|
$transformer = new TransactionGroupTransformer();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$basicMessage['content'] = $transformer->transformObject($model);
|
$basicMessage['content'] = $transformer->transformObject($model);
|
||||||
@@ -173,13 +215,13 @@ class StandardMessageGenerator implements MessageGeneratorInterface
|
|||||||
|
|
||||||
case WebhookResponse::ACCOUNTS->value:
|
case WebhookResponse::ACCOUNTS->value:
|
||||||
/** @var TransactionGroup $model */
|
/** @var TransactionGroup $model */
|
||||||
$accounts = $this->collectAccounts($model);
|
$accounts = $this->collectAccounts($model);
|
||||||
$enrichment = new AccountEnrichment();
|
$enrichment = new AccountEnrichment();
|
||||||
$enrichment->setDate(null);
|
$enrichment->setDate(null);
|
||||||
$enrichment->setUser($model->user);
|
$enrichment->setUser($model->user);
|
||||||
$accounts = $enrichment->enrich($accounts);
|
$accounts = $enrichment->enrich($accounts);
|
||||||
foreach ($accounts as $account) {
|
foreach ($accounts as $account) {
|
||||||
$transformer = new AccountTransformer();
|
$transformer = new AccountTransformer();
|
||||||
$transformer->setParameters(new ParameterBag());
|
$transformer->setParameters(new ParameterBag());
|
||||||
$basicMessage['content'][] = $transformer->transform($account);
|
$basicMessage['content'][] = $transformer->transform($account);
|
||||||
}
|
}
|
||||||
@@ -209,7 +251,7 @@ class StandardMessageGenerator implements MessageGeneratorInterface
|
|||||||
|
|
||||||
private function storeMessage(Webhook $webhook, array $message): void
|
private function storeMessage(Webhook $webhook, array $message): void
|
||||||
{
|
{
|
||||||
$webhookMessage = new WebhookMessage();
|
$webhookMessage = new WebhookMessage();
|
||||||
$webhookMessage->webhook()->associate($webhook);
|
$webhookMessage->webhook()->associate($webhook);
|
||||||
$webhookMessage->sent = false;
|
$webhookMessage->sent = false;
|
||||||
$webhookMessage->errored = false;
|
$webhookMessage->errored = false;
|
||||||
|
@@ -31,6 +31,7 @@ use FireflyIII\Models\AvailableBudget;
|
|||||||
use FireflyIII\Models\Budget;
|
use FireflyIII\Models\Budget;
|
||||||
use FireflyIII\Models\BudgetLimit;
|
use FireflyIII\Models\BudgetLimit;
|
||||||
use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface;
|
use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface;
|
||||||
|
use FireflyIII\Support\Observers\RecalculatesAvailableBudgetsTrait;
|
||||||
use FireflyIII\User;
|
use FireflyIII\User;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Psr\Container\ContainerExceptionInterface;
|
use Psr\Container\ContainerExceptionInterface;
|
||||||
@@ -44,202 +45,10 @@ use Spatie\Period\Precision;
|
|||||||
*/
|
*/
|
||||||
class BudgetLimitHandler
|
class BudgetLimitHandler
|
||||||
{
|
{
|
||||||
|
|
||||||
public function created(Created $event): void
|
public function created(Created $event): void
|
||||||
{
|
{
|
||||||
Log::debug(sprintf('BudgetLimitHandler::created(#%s)', $event->budgetLimit->id));
|
Log::debug(sprintf('BudgetLimitHandler::created(#%s)', $event->budgetLimit->id));
|
||||||
self::updateAvailableBudget($event->budgetLimit);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function updateAvailableBudget(BudgetLimit $budgetLimit): void
|
|
||||||
{
|
|
||||||
Log::debug(sprintf('Now in updateAvailableBudget(limit #%d)', $budgetLimit->id));
|
|
||||||
|
|
||||||
/** @var null|Budget $budget */
|
|
||||||
$budget = Budget::find($budgetLimit->budget_id);
|
|
||||||
if (null === $budget) {
|
|
||||||
Log::warning('Budget is null, probably deleted, find deleted version.');
|
|
||||||
|
|
||||||
/** @var null|Budget $budget */
|
|
||||||
$budget = Budget::withTrashed()->find($budgetLimit->budget_id);
|
|
||||||
}
|
|
||||||
if (null === $budget) {
|
|
||||||
Log::warning('Budget is still null, cannot continue, will delete budget limit.');
|
|
||||||
$budgetLimit->forceDelete();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var null|User $user */
|
|
||||||
$user = $budget->user;
|
|
||||||
|
|
||||||
// sanity check. It happens when the budget has been deleted so the original user is unknown.
|
|
||||||
if (null === $user) {
|
|
||||||
Log::warning('User is null, cannot continue.');
|
|
||||||
$budgetLimit->forceDelete();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
try {
|
|
||||||
$viewRange = app('preferences')->getForUser($user, 'viewRange', '1M')->data;
|
|
||||||
} catch (ContainerExceptionInterface|NotFoundExceptionInterface $e) {
|
|
||||||
Log::error($e->getMessage());
|
|
||||||
$viewRange = '1M';
|
|
||||||
}
|
|
||||||
// safety catch
|
|
||||||
if (null === $viewRange || is_array($viewRange)) {
|
|
||||||
$viewRange = '1M';
|
|
||||||
}
|
|
||||||
$viewRange = (string) $viewRange;
|
|
||||||
|
|
||||||
$start = app('navigation')->startOfPeriod($budgetLimit->start_date, $viewRange);
|
|
||||||
$end = app('navigation')->startOfPeriod($budgetLimit->end_date, $viewRange);
|
|
||||||
$end = app('navigation')->endOfPeriod($end, $viewRange);
|
|
||||||
|
|
||||||
// limit period in total is:
|
|
||||||
$limitPeriod = Period::make($start, $end, precision: Precision::DAY(), boundaries: Boundaries::EXCLUDE_NONE());
|
|
||||||
Log::debug(sprintf('Limit period is from %s to %s', $start->format('Y-m-d'), $end->format('Y-m-d')));
|
|
||||||
|
|
||||||
// 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 null|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 1 AB, will update.');
|
|
||||||
self::calculateAmount($availableBudget);
|
|
||||||
}
|
|
||||||
if (null === $availableBudget) {
|
|
||||||
Log::debug('No AB found, will create.');
|
|
||||||
// if not exists:
|
|
||||||
$currentPeriod = Period::make($current, $currentEnd, precision: Precision::DAY(), boundaries: Boundaries::EXCLUDE_NONE());
|
|
||||||
$daily = self::getDailyAmount($budgetLimit);
|
|
||||||
$amount = bcmul($daily, (string) $currentPeriod->length(), 12);
|
|
||||||
|
|
||||||
// no need to calculate if period is equal.
|
|
||||||
if ($currentPeriod->equals($limitPeriod)) {
|
|
||||||
$amount = 0 === $budgetLimit->id ? '0' : $budgetLimit->amount;
|
|
||||||
}
|
|
||||||
if (0 === bccomp($amount, '0')) {
|
|
||||||
Log::debug('Amount is zero, will not create AB.');
|
|
||||||
}
|
|
||||||
if (0 !== bccomp($amount, '0')) {
|
|
||||||
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' => $user->id,
|
|
||||||
'user_group_id' => $user->user_group_id,
|
|
||||||
'transaction_currency_id' => $budgetLimit->transaction_currency_id,
|
|
||||||
'start_date' => $current,
|
|
||||||
'start_date_tz' => $current->format('e'),
|
|
||||||
'end_date' => $currentEnd,
|
|
||||||
'end_date_tz' => $currentEnd->format('e'),
|
|
||||||
'amount' => $amount,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
$availableBudget->save();
|
|
||||||
Log::debug(sprintf('ID of new AB is #%d', $availableBudget->id));
|
|
||||||
self::calculateAmount($availableBudget);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// prep for next loop
|
|
||||||
$current = app('navigation')->addPeriod($current, $viewRange, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static 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 recalculate everything just in case.
|
|
||||||
$set = $repository->getAllBudgetLimitsByCurrency($availableBudget->transactionCurrency, $availableBudget->start_date, $availableBudget->end_date);
|
|
||||||
Log::debug(sprintf('Found %d interesting budget limit(s).', $set->count()));
|
|
||||||
|
|
||||||
/** @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 each other, amount from this BL must be added to the AB
|
|
||||||
if ($limitPeriod->equals($abPeriod)) {
|
|
||||||
Log::debug('This budget limit is equal to the available budget period.');
|
|
||||||
$newAmount = bcadd($newAmount, (string) $budgetLimit->amount);
|
|
||||||
}
|
|
||||||
// if budget limit period is inside AB period, it can be added in full.
|
|
||||||
if (!$limitPeriod->equals($abPeriod) && $abPeriod->contains($limitPeriod)) {
|
|
||||||
Log::debug('This budget limit is smaller than the available budget period.');
|
|
||||||
$newAmount = bcadd($newAmount, (string) $budgetLimit->amount);
|
|
||||||
}
|
|
||||||
if (!$limitPeriod->equals($abPeriod) && !$abPeriod->contains($limitPeriod) && $abPeriod->overlapsWith($limitPeriod)) {
|
|
||||||
Log::debug('This budget limit is something else entirely!');
|
|
||||||
$overlap = $abPeriod->overlap($limitPeriod);
|
|
||||||
if ($overlap instanceof Period) {
|
|
||||||
$length = $overlap->length();
|
|
||||||
$daily = bcmul(self::getDailyAmount($budgetLimit), (string) $length);
|
|
||||||
$newAmount = bcadd($newAmount, $daily);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (0 === bccomp('0', $newAmount)) {
|
|
||||||
Log::debug('New amount is zero, deleting AB.');
|
|
||||||
$availableBudget->delete();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Log::debug(sprintf('Concluded new amount for this AB must be %s', $newAmount));
|
|
||||||
$availableBudget->amount = app('steam')->bcround($newAmount, $availableBudget->transactionCurrency->decimal_places);
|
|
||||||
$availableBudget->save();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function getDailyAmount(BudgetLimit $budgetLimit): string
|
|
||||||
{
|
|
||||||
if (0 === $budgetLimit->id) {
|
|
||||||
return '0';
|
|
||||||
}
|
|
||||||
$limitPeriod = Period::make(
|
|
||||||
$budgetLimit->start_date,
|
|
||||||
$budgetLimit->end_date,
|
|
||||||
precision : Precision::DAY(),
|
|
||||||
boundaries: Boundaries::EXCLUDE_NONE()
|
|
||||||
);
|
|
||||||
$days = $limitPeriod->length();
|
|
||||||
$amount = bcdiv($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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function deleted(Deleted $event): void
|
public function deleted(Deleted $event): void
|
||||||
@@ -249,7 +58,6 @@ class BudgetLimitHandler
|
|||||||
|
|
||||||
public function updated(Updated $event): void
|
public function updated(Updated $event): void
|
||||||
{
|
{
|
||||||
Log::debug(sprintf('BudgetLimitHandler::updated(#%s)', $event->budgetLimit->id));
|
|
||||||
self::updateAvailableBudget($event->budgetLimit);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -24,17 +24,35 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace FireflyIII\Handlers\Observer;
|
namespace FireflyIII\Handlers\Observer;
|
||||||
|
|
||||||
|
use FireflyIII\Enums\WebhookTrigger;
|
||||||
|
use FireflyIII\Events\RequestedSendWebhookMessages;
|
||||||
|
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
|
||||||
use FireflyIII\Models\BudgetLimit;
|
use FireflyIII\Models\BudgetLimit;
|
||||||
use FireflyIII\Support\Facades\Amount;
|
use FireflyIII\Support\Facades\Amount;
|
||||||
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
|
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
|
||||||
|
use FireflyIII\Support\Observers\RecalculatesAvailableBudgetsTrait;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class BudgetLimitObserver
|
class BudgetLimitObserver
|
||||||
{
|
{
|
||||||
|
use RecalculatesAvailableBudgetsTrait;
|
||||||
public function created(BudgetLimit $budgetLimit): void
|
public function created(BudgetLimit $budgetLimit): void
|
||||||
{
|
{
|
||||||
Log::debug('Observe "created" of a budget limit.');
|
Log::debug('Observe "created" of a budget limit.');
|
||||||
$this->updatePrimaryCurrencyAmount($budgetLimit);
|
$this->updatePrimaryCurrencyAmount($budgetLimit);
|
||||||
|
$this->updateAvailableBudget($budgetLimit);
|
||||||
|
|
||||||
|
$user = $budgetLimit->budget->user;
|
||||||
|
|
||||||
|
/** @var MessageGeneratorInterface $engine */
|
||||||
|
$engine = app(MessageGeneratorInterface::class);
|
||||||
|
$engine->setUser($user);
|
||||||
|
$engine->setObjects(new Collection()->push($budgetLimit));
|
||||||
|
$engine->setTrigger(WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT);
|
||||||
|
$engine->generateMessages();
|
||||||
|
|
||||||
|
event(new RequestedSendWebhookMessages());
|
||||||
}
|
}
|
||||||
|
|
||||||
private function updatePrimaryCurrencyAmount(BudgetLimit $budgetLimit): void
|
private function updatePrimaryCurrencyAmount(BudgetLimit $budgetLimit): void
|
||||||
@@ -60,5 +78,17 @@ class BudgetLimitObserver
|
|||||||
{
|
{
|
||||||
Log::debug('Observe "updated" of a budget limit.');
|
Log::debug('Observe "updated" of a budget limit.');
|
||||||
$this->updatePrimaryCurrencyAmount($budgetLimit);
|
$this->updatePrimaryCurrencyAmount($budgetLimit);
|
||||||
|
$this->updateAvailableBudget($budgetLimit);
|
||||||
|
|
||||||
|
$user = $budgetLimit->budget->user;
|
||||||
|
|
||||||
|
/** @var MessageGeneratorInterface $engine */
|
||||||
|
$engine = app(MessageGeneratorInterface::class);
|
||||||
|
$engine->setUser($user);
|
||||||
|
$engine->setObjects(new Collection()->push($budgetLimit));
|
||||||
|
$engine->setTrigger(WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT);
|
||||||
|
$engine->generateMessages();
|
||||||
|
|
||||||
|
event(new RequestedSendWebhookMessages());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -23,21 +23,71 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace FireflyIII\Handlers\Observer;
|
namespace FireflyIII\Handlers\Observer;
|
||||||
|
|
||||||
|
use FireflyIII\Enums\WebhookTrigger;
|
||||||
|
use FireflyIII\Events\RequestedSendWebhookMessages;
|
||||||
|
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
|
||||||
use FireflyIII\Models\Attachment;
|
use FireflyIII\Models\Attachment;
|
||||||
use FireflyIII\Models\Budget;
|
use FireflyIII\Models\Budget;
|
||||||
use FireflyIII\Models\BudgetLimit;
|
use FireflyIII\Models\BudgetLimit;
|
||||||
use FireflyIII\Repositories\Attachment\AttachmentRepositoryInterface;
|
use FireflyIII\Repositories\Attachment\AttachmentRepositoryInterface;
|
||||||
|
use FireflyIII\Support\Observers\RecalculatesAvailableBudgetsTrait;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class BudgetObserver
|
* Class BudgetObserver
|
||||||
*/
|
*/
|
||||||
class BudgetObserver
|
class BudgetObserver
|
||||||
{
|
{
|
||||||
|
use RecalculatesAvailableBudgetsTrait;
|
||||||
|
|
||||||
|
public function created(Budget $budget): void
|
||||||
|
{
|
||||||
|
Log::debug(sprintf('Observe "created" of budget #%d ("%s").', $budget->id, $budget->name));
|
||||||
|
|
||||||
|
// fire event.
|
||||||
|
$user = $budget->user;
|
||||||
|
/** @var MessageGeneratorInterface $engine */
|
||||||
|
$engine = app(MessageGeneratorInterface::class);
|
||||||
|
$engine->setUser($user);
|
||||||
|
$engine->setObjects(new Collection()->push($budget));
|
||||||
|
$engine->setTrigger(WebhookTrigger::STORE_BUDGET);
|
||||||
|
$engine->generateMessages();
|
||||||
|
|
||||||
|
event(new RequestedSendWebhookMessages());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updated(Budget $budget): void
|
||||||
|
{
|
||||||
|
Log::debug(sprintf('Observe "updated" of budget #%d ("%s").', $budget->id, $budget->name));
|
||||||
|
$user = $budget->user;
|
||||||
|
|
||||||
|
/** @var MessageGeneratorInterface $engine */
|
||||||
|
$engine = app(MessageGeneratorInterface::class);
|
||||||
|
$engine->setUser($user);
|
||||||
|
$engine->setObjects(new Collection()->push($budget));
|
||||||
|
$engine->setTrigger(WebhookTrigger::UPDATE_BUDGET);
|
||||||
|
$engine->generateMessages();
|
||||||
|
|
||||||
|
event(new RequestedSendWebhookMessages());
|
||||||
|
}
|
||||||
|
|
||||||
public function deleting(Budget $budget): void
|
public function deleting(Budget $budget): void
|
||||||
{
|
{
|
||||||
app('log')->debug('Observe "deleting" of a budget.');
|
Log::debug('Observe "deleting" of a budget.');
|
||||||
|
|
||||||
$repository = app(AttachmentRepositoryInterface::class);
|
$user = $budget->user;
|
||||||
|
|
||||||
|
/** @var MessageGeneratorInterface $engine */
|
||||||
|
$engine = app(MessageGeneratorInterface::class);
|
||||||
|
$engine->setUser($user);
|
||||||
|
$engine->setObjects(new Collection()->push($budget));
|
||||||
|
$engine->setTrigger(WebhookTrigger::DESTROY_BUDGET);
|
||||||
|
$engine->generateMessages();
|
||||||
|
|
||||||
|
event(new RequestedSendWebhookMessages());
|
||||||
|
|
||||||
|
$repository = app(AttachmentRepositoryInterface::class);
|
||||||
$repository->setUser($budget->user);
|
$repository->setUser($budget->user);
|
||||||
|
|
||||||
/** @var Attachment $attachment */
|
/** @var Attachment $attachment */
|
||||||
@@ -49,7 +99,10 @@ class BudgetObserver
|
|||||||
/** @var BudgetLimit $budgetLimit */
|
/** @var BudgetLimit $budgetLimit */
|
||||||
foreach ($budgetLimits as $budgetLimit) {
|
foreach ($budgetLimits as $budgetLimit) {
|
||||||
// this loop exists so several events are fired.
|
// this loop exists so several events are fired.
|
||||||
$budgetLimit->delete();
|
$copy = clone $budgetLimit;
|
||||||
|
$copy->id = 0;
|
||||||
|
$this->updateAvailableBudget($copy);
|
||||||
|
$budgetLimit->deleteQuietly(); // delete is quietly when in a loop.
|
||||||
}
|
}
|
||||||
|
|
||||||
$budget->notes()->delete();
|
$budget->notes()->delete();
|
||||||
|
@@ -29,9 +29,6 @@ use FireflyIII\Events\DestroyedTransactionGroup;
|
|||||||
use FireflyIII\Events\DetectedNewIPAddress;
|
use FireflyIII\Events\DetectedNewIPAddress;
|
||||||
use FireflyIII\Events\Model\Bill\WarnUserAboutBill;
|
use FireflyIII\Events\Model\Bill\WarnUserAboutBill;
|
||||||
use FireflyIII\Events\Model\Bill\WarnUserAboutOverdueSubscriptions;
|
use FireflyIII\Events\Model\Bill\WarnUserAboutOverdueSubscriptions;
|
||||||
use FireflyIII\Events\Model\BudgetLimit\Created;
|
|
||||||
use FireflyIII\Events\Model\BudgetLimit\Deleted;
|
|
||||||
use FireflyIII\Events\Model\BudgetLimit\Updated;
|
|
||||||
use FireflyIII\Events\Model\PiggyBank\ChangedAmount;
|
use FireflyIII\Events\Model\PiggyBank\ChangedAmount;
|
||||||
use FireflyIII\Events\Model\PiggyBank\ChangedName;
|
use FireflyIII\Events\Model\PiggyBank\ChangedName;
|
||||||
use FireflyIII\Events\Model\Rule\RuleActionFailedOnArray;
|
use FireflyIII\Events\Model\Rule\RuleActionFailedOnArray;
|
||||||
@@ -219,17 +216,6 @@ class EventServiceProvider extends ServiceProvider
|
|||||||
'FireflyIII\Handlers\Events\Model\PiggyBankEventHandler@changedPiggyBankName',
|
'FireflyIII\Handlers\Events\Model\PiggyBankEventHandler@changedPiggyBankName',
|
||||||
],
|
],
|
||||||
|
|
||||||
// budget related events: CRUD budget limit
|
|
||||||
Created::class => [
|
|
||||||
'FireflyIII\Handlers\Events\Model\BudgetLimitHandler@created',
|
|
||||||
],
|
|
||||||
Updated::class => [
|
|
||||||
'FireflyIII\Handlers\Events\Model\BudgetLimitHandler@updated',
|
|
||||||
],
|
|
||||||
Deleted::class => [
|
|
||||||
'FireflyIII\Handlers\Events\Model\BudgetLimitHandler@deleted',
|
|
||||||
],
|
|
||||||
|
|
||||||
// rule actions
|
// rule actions
|
||||||
RuleActionFailedOnArray::class => [
|
RuleActionFailedOnArray::class => [
|
||||||
'FireflyIII\Handlers\Events\Model\RuleHandler@ruleActionFailedOnArray',
|
'FireflyIII\Handlers\Events\Model\RuleHandler@ruleActionFailedOnArray',
|
||||||
|
210
app/Support/Observers/RecalculatesAvailableBudgetsTrait.php
Normal file
210
app/Support/Observers/RecalculatesAvailableBudgetsTrait.php
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace FireflyIII\Support\Observers;
|
||||||
|
|
||||||
|
use FireflyIII\Models\AvailableBudget;
|
||||||
|
use FireflyIII\Models\Budget;
|
||||||
|
use FireflyIII\Models\BudgetLimit;
|
||||||
|
use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface;
|
||||||
|
use FireflyIII\User;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Psr\Container\ContainerExceptionInterface;
|
||||||
|
use Psr\Container\NotFoundExceptionInterface;
|
||||||
|
use Spatie\Period\Boundaries;
|
||||||
|
use Spatie\Period\Period;
|
||||||
|
use Spatie\Period\Precision;
|
||||||
|
|
||||||
|
trait RecalculatesAvailableBudgetsTrait
|
||||||
|
{
|
||||||
|
private function updateAvailableBudget(BudgetLimit $budgetLimit): void
|
||||||
|
{
|
||||||
|
Log::debug(sprintf('Now in updateAvailableBudget(limit #%d)', $budgetLimit->id));
|
||||||
|
|
||||||
|
/** @var null|Budget $budget */
|
||||||
|
$budget = Budget::find($budgetLimit->budget_id);
|
||||||
|
if (null === $budget) {
|
||||||
|
Log::warning('Budget is null, probably deleted, find deleted version.');
|
||||||
|
|
||||||
|
/** @var null|Budget $budget */
|
||||||
|
$budget = Budget::withTrashed()->find($budgetLimit->budget_id);
|
||||||
|
}
|
||||||
|
if (null === $budget) {
|
||||||
|
Log::warning('Budget is still null, cannot continue, will delete budget limit.');
|
||||||
|
$budgetLimit->forceDelete();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var null|User $user */
|
||||||
|
$user = $budget->user;
|
||||||
|
|
||||||
|
// sanity check. It happens when the budget has been deleted so the original user is unknown.
|
||||||
|
if (null === $user) {
|
||||||
|
Log::warning('User is null, cannot continue.');
|
||||||
|
$budgetLimit->forceDelete();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
try {
|
||||||
|
$viewRange = app('preferences')->getForUser($user, 'viewRange', '1M')->data;
|
||||||
|
} catch (ContainerExceptionInterface|NotFoundExceptionInterface $e) {
|
||||||
|
Log::error($e->getMessage());
|
||||||
|
$viewRange = '1M';
|
||||||
|
}
|
||||||
|
// safety catch
|
||||||
|
if (null === $viewRange || is_array($viewRange)) {
|
||||||
|
$viewRange = '1M';
|
||||||
|
}
|
||||||
|
$viewRange = (string) $viewRange;
|
||||||
|
|
||||||
|
$start = app('navigation')->startOfPeriod($budgetLimit->start_date, $viewRange);
|
||||||
|
$end = app('navigation')->startOfPeriod($budgetLimit->end_date, $viewRange);
|
||||||
|
$end = app('navigation')->endOfPeriod($end, $viewRange);
|
||||||
|
|
||||||
|
// limit period in total is:
|
||||||
|
$limitPeriod = Period::make($start, $end, precision: Precision::DAY(), boundaries: Boundaries::EXCLUDE_NONE());
|
||||||
|
Log::debug(sprintf('Limit period is from %s to %s', $start->format('Y-m-d'), $end->format('Y-m-d')));
|
||||||
|
|
||||||
|
// 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 null|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 1 AB, will update.');
|
||||||
|
$this->calculateAmount($availableBudget);
|
||||||
|
}
|
||||||
|
if (null === $availableBudget) {
|
||||||
|
Log::debug('No AB found, will create.');
|
||||||
|
// 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 = 0 === $budgetLimit->id ? '0' : $budgetLimit->amount;
|
||||||
|
}
|
||||||
|
if (0 === bccomp($amount, '0')) {
|
||||||
|
Log::debug('Amount is zero, will not create AB.');
|
||||||
|
}
|
||||||
|
if (0 !== bccomp($amount, '0')) {
|
||||||
|
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' => $user->id,
|
||||||
|
'user_group_id' => $user->user_group_id,
|
||||||
|
'transaction_currency_id' => $budgetLimit->transaction_currency_id,
|
||||||
|
'start_date' => $current,
|
||||||
|
'start_date_tz' => $current->format('e'),
|
||||||
|
'end_date' => $currentEnd,
|
||||||
|
'end_date_tz' => $currentEnd->format('e'),
|
||||||
|
'amount' => $amount,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$availableBudget->save();
|
||||||
|
Log::debug(sprintf('ID of new AB is #%d', $availableBudget->id));
|
||||||
|
$this->calculateAmount($availableBudget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// prep for next loop
|
||||||
|
$current = app('navigation')->addPeriod($current, $viewRange, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 recalculate everything just in case.
|
||||||
|
$set = $repository->getAllBudgetLimitsByCurrency($availableBudget->transactionCurrency, $availableBudget->start_date, $availableBudget->end_date);
|
||||||
|
Log::debug(sprintf('Found %d interesting budget limit(s).', $set->count()));
|
||||||
|
|
||||||
|
/** @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 each other, amount from this BL must be added to the AB
|
||||||
|
if ($limitPeriod->equals($abPeriod)) {
|
||||||
|
Log::debug('This budget limit is equal to the available budget period.');
|
||||||
|
$newAmount = bcadd($newAmount, (string) $budgetLimit->amount);
|
||||||
|
}
|
||||||
|
// if budget limit period is inside AB period, it can be added in full.
|
||||||
|
if (!$limitPeriod->equals($abPeriod) && $abPeriod->contains($limitPeriod)) {
|
||||||
|
Log::debug('This budget limit is smaller than the available budget period.');
|
||||||
|
$newAmount = bcadd($newAmount, (string) $budgetLimit->amount);
|
||||||
|
}
|
||||||
|
if (!$limitPeriod->equals($abPeriod) && !$abPeriod->contains($limitPeriod) && $abPeriod->overlapsWith($limitPeriod)) {
|
||||||
|
Log::debug('This budget limit is something else entirely!');
|
||||||
|
$overlap = $abPeriod->overlap($limitPeriod);
|
||||||
|
if ($overlap instanceof Period) {
|
||||||
|
$length = $overlap->length();
|
||||||
|
$daily = bcmul($this->getDailyAmount($budgetLimit), (string) $length);
|
||||||
|
$newAmount = bcadd($newAmount, $daily);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (0 === bccomp('0', $newAmount)) {
|
||||||
|
Log::debug('New amount is zero, deleting AB.');
|
||||||
|
$availableBudget->delete();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log::debug(sprintf('Concluded new amount for this AB must be %s', $newAmount));
|
||||||
|
$availableBudget->amount = app('steam')->bcround($newAmount, $availableBudget->transactionCurrency->decimal_places);
|
||||||
|
$availableBudget->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDailyAmount(BudgetLimit $budgetLimit): string
|
||||||
|
{
|
||||||
|
if (0 === $budgetLimit->id) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
$limitPeriod = Period::make(
|
||||||
|
$budgetLimit->start_date,
|
||||||
|
$budgetLimit->end_date,
|
||||||
|
precision : Precision::DAY(),
|
||||||
|
boundaries: Boundaries::EXCLUDE_NONE()
|
||||||
|
);
|
||||||
|
$days = $limitPeriod->length();
|
||||||
|
$amount = bcdiv($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;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user