diff --git a/app/Api/V1/Controllers/Models/BudgetLimit/ShowController.php b/app/Api/V1/Controllers/Models/BudgetLimit/ShowController.php index 3b5c2c3569..0253b3aac9 100644 --- a/app/Api/V1/Controllers/Models/BudgetLimit/ShowController.php +++ b/app/Api/V1/Controllers/Models/BudgetLimit/ShowController.php @@ -96,7 +96,6 @@ class ShowController extends Controller $paginator = new LengthAwarePaginator($budgetLimits, $count, $pageSize, $this->parameters->get('page')); $paginator->setPath(route('api.v1.budgets.limits.index', [$budget->id]).$this->buildParams()); - // enrich $enrichment = new BudgetLimitEnrichment(); $enrichment->setUser($admin); diff --git a/app/Generator/Webhook/StandardMessageGenerator.php b/app/Generator/Webhook/StandardMessageGenerator.php index 7bc9a32a17..733b2fbc44 100644 --- a/app/Generator/Webhook/StandardMessageGenerator.php +++ b/app/Generator/Webhook/StandardMessageGenerator.php @@ -27,13 +27,19 @@ namespace FireflyIII\Generator\Webhook; use FireflyIII\Enums\WebhookResponse; use FireflyIII\Enums\WebhookTrigger; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\Budget; +use FireflyIII\Models\BudgetLimit; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\Webhook; use FireflyIII\Models\WebhookMessage; 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\BudgetLimitTransformer; +use FireflyIII\Transformers\BudgetTransformer; use FireflyIII\Transformers\TransactionGroupTransformer; use FireflyIII\User; use Illuminate\Database\Eloquent\Model; @@ -109,18 +115,16 @@ class StandardMessageGenerator implements MessageGeneratorInterface */ 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. 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(); $basicMessage = [ 'uuid' => $uuid->toString(), 'user_id' => 0, 'user_group_id' => 0, - 'trigger' => WebhookTrigger::from((int) $webhook->trigger)->name, - 'response' => WebhookResponse::from((int) $webhook->response)->name, + 'trigger' => WebhookTrigger::from((int)$webhook->trigger)->name, + 'response' => WebhookResponse::from((int)$webhook->response)->name, 'url' => $webhook->url, 'version' => sprintf('v%d', $this->getVersion()), '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)); 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: /** @var TransactionGroup $model */ $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)); 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: $basicMessage['content'] = []; @@ -156,7 +198,7 @@ class StandardMessageGenerator implements MessageGeneratorInterface case WebhookResponse::TRANSACTIONS->value: /** @var TransactionGroup $model */ - $transformer = new TransactionGroupTransformer(); + $transformer = new TransactionGroupTransformer(); try { $basicMessage['content'] = $transformer->transformObject($model); @@ -173,13 +215,13 @@ class StandardMessageGenerator implements MessageGeneratorInterface case WebhookResponse::ACCOUNTS->value: /** @var TransactionGroup $model */ - $accounts = $this->collectAccounts($model); - $enrichment = new AccountEnrichment(); + $accounts = $this->collectAccounts($model); + $enrichment = new AccountEnrichment(); $enrichment->setDate(null); $enrichment->setUser($model->user); - $accounts = $enrichment->enrich($accounts); + $accounts = $enrichment->enrich($accounts); foreach ($accounts as $account) { - $transformer = new AccountTransformer(); + $transformer = new AccountTransformer(); $transformer->setParameters(new ParameterBag()); $basicMessage['content'][] = $transformer->transform($account); } @@ -209,7 +251,7 @@ class StandardMessageGenerator implements MessageGeneratorInterface private function storeMessage(Webhook $webhook, array $message): void { - $webhookMessage = new WebhookMessage(); + $webhookMessage = new WebhookMessage(); $webhookMessage->webhook()->associate($webhook); $webhookMessage->sent = false; $webhookMessage->errored = false; diff --git a/app/Handlers/Events/Model/BudgetLimitHandler.php b/app/Handlers/Events/Model/BudgetLimitHandler.php index 854bf6a463..63235f9014 100644 --- a/app/Handlers/Events/Model/BudgetLimitHandler.php +++ b/app/Handlers/Events/Model/BudgetLimitHandler.php @@ -31,6 +31,7 @@ use FireflyIII\Models\AvailableBudget; use FireflyIII\Models\Budget; use FireflyIII\Models\BudgetLimit; use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface; +use FireflyIII\Support\Observers\RecalculatesAvailableBudgetsTrait; use FireflyIII\User; use Illuminate\Support\Facades\Log; use Psr\Container\ContainerExceptionInterface; @@ -44,202 +45,10 @@ use Spatie\Period\Precision; */ class BudgetLimitHandler { + public function created(Created $event): void { 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 @@ -249,7 +58,6 @@ class BudgetLimitHandler public function updated(Updated $event): void { - Log::debug(sprintf('BudgetLimitHandler::updated(#%s)', $event->budgetLimit->id)); - self::updateAvailableBudget($event->budgetLimit); + } } diff --git a/app/Handlers/Observer/BudgetLimitObserver.php b/app/Handlers/Observer/BudgetLimitObserver.php index fcc4b405a8..e2ab062c34 100644 --- a/app/Handlers/Observer/BudgetLimitObserver.php +++ b/app/Handlers/Observer/BudgetLimitObserver.php @@ -24,17 +24,35 @@ declare(strict_types=1); namespace FireflyIII\Handlers\Observer; +use FireflyIII\Enums\WebhookTrigger; +use FireflyIII\Events\RequestedSendWebhookMessages; +use FireflyIII\Generator\Webhook\MessageGeneratorInterface; use FireflyIII\Models\BudgetLimit; use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Http\Api\ExchangeRateConverter; +use FireflyIII\Support\Observers\RecalculatesAvailableBudgetsTrait; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; class BudgetLimitObserver { + use RecalculatesAvailableBudgetsTrait; public function created(BudgetLimit $budgetLimit): void { Log::debug('Observe "created" of a budget limit.'); $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 @@ -60,5 +78,17 @@ class BudgetLimitObserver { Log::debug('Observe "updated" of a budget limit.'); $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()); } } diff --git a/app/Handlers/Observer/BudgetObserver.php b/app/Handlers/Observer/BudgetObserver.php index 9a697c79ea..9f89ab2577 100644 --- a/app/Handlers/Observer/BudgetObserver.php +++ b/app/Handlers/Observer/BudgetObserver.php @@ -23,21 +23,71 @@ declare(strict_types=1); namespace FireflyIII\Handlers\Observer; +use FireflyIII\Enums\WebhookTrigger; +use FireflyIII\Events\RequestedSendWebhookMessages; +use FireflyIII\Generator\Webhook\MessageGeneratorInterface; use FireflyIII\Models\Attachment; use FireflyIII\Models\Budget; use FireflyIII\Models\BudgetLimit; use FireflyIII\Repositories\Attachment\AttachmentRepositoryInterface; +use FireflyIII\Support\Observers\RecalculatesAvailableBudgetsTrait; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; /** * 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 { - 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); /** @var Attachment $attachment */ @@ -49,7 +99,10 @@ class BudgetObserver /** @var BudgetLimit $budgetLimit */ foreach ($budgetLimits as $budgetLimit) { // 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(); diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 1b7e75dfe2..5cff75ae42 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -29,9 +29,6 @@ use FireflyIII\Events\DestroyedTransactionGroup; use FireflyIII\Events\DetectedNewIPAddress; use FireflyIII\Events\Model\Bill\WarnUserAboutBill; 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\ChangedName; use FireflyIII\Events\Model\Rule\RuleActionFailedOnArray; @@ -219,17 +216,6 @@ class EventServiceProvider extends ServiceProvider '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 RuleActionFailedOnArray::class => [ 'FireflyIII\Handlers\Events\Model\RuleHandler@ruleActionFailedOnArray', diff --git a/app/Support/Observers/RecalculatesAvailableBudgetsTrait.php b/app/Support/Observers/RecalculatesAvailableBudgetsTrait.php new file mode 100644 index 0000000000..b15d41a455 --- /dev/null +++ b/app/Support/Observers/RecalculatesAvailableBudgetsTrait.php @@ -0,0 +1,210 @@ +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; + } +}