diff --git a/app/Events/Model/TransactionGroup/TransactionGroupEventFlags.php b/app/Events/Model/TransactionGroup/TransactionGroupEventFlags.php index f68e2f83a5..0c4c13263f 100644 --- a/app/Events/Model/TransactionGroup/TransactionGroupEventFlags.php +++ b/app/Events/Model/TransactionGroup/TransactionGroupEventFlags.php @@ -26,5 +26,6 @@ class TransactionGroupEventFlags public bool $applyRules = true; public bool $fireWebhooks = true; public bool $batchSubmission = false; + public bool $recalculateCredit = true; } diff --git a/app/Handlers/Events/StoredGroupEventHandler.php b/app/Handlers/Events/StoredGroupEventHandler.php index dddf285924..cbcc93a122 100644 --- a/app/Handlers/Events/StoredGroupEventHandler.php +++ b/app/Handlers/Events/StoredGroupEventHandler.php @@ -47,9 +47,9 @@ class StoredGroupEventHandler { public function runAllHandlers(StoredTransactionGroup $event): void { - $this->processRules($event, null); - $this->recalculateCredit($event); - $this->triggerWebhooks($event); + // $this->processRules($event, null); + // $this->recalculateCredit($event); + // $this->triggerWebhooks($event); $this->removePeriodStatistics($event); } diff --git a/app/Http/Middleware/Binder.php b/app/Http/Middleware/Binder.php index 07a80af026..e8e7e42283 100644 --- a/app/Http/Middleware/Binder.php +++ b/app/Http/Middleware/Binder.php @@ -75,7 +75,7 @@ class Binder * * @return mixed */ - private function performBinding(string $key, string $value, Route $route) + private function performBinding(string $key, object|string $value, Route $route) { $class = $this->binders[$key]; diff --git a/app/Listeners/Model/TransactionGroup/ProcessesNewTransactionGroup.php b/app/Listeners/Model/TransactionGroup/ProcessesNewTransactionGroup.php index 8131be05fa..eaef9fc3de 100644 --- a/app/Listeners/Model/TransactionGroup/ProcessesNewTransactionGroup.php +++ b/app/Listeners/Model/TransactionGroup/ProcessesNewTransactionGroup.php @@ -21,8 +21,18 @@ namespace FireflyIII\Listeners\Model\TransactionGroup; +use FireflyIII\Enums\WebhookTrigger; use FireflyIII\Events\Model\TransactionGroup\CreatedSingleTransactionGroup; +use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending; +use FireflyIII\Generator\Webhook\MessageGeneratorInterface; +use FireflyIII\Models\TransactionGroup; +use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface; +use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface; +use FireflyIII\Services\Internal\Support\CreditRecalculateService; +use FireflyIII\TransactionRules\Engine\RuleEngineInterface; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; class ProcessesNewTransactionGroup implements ShouldQueue @@ -35,6 +45,101 @@ class ProcessesNewTransactionGroup implements ShouldQueue return; } Log::debug(sprintf('Will join group #%d with all other open transaction groups and process them.', $event->transactionGroup->id)); + $collection = $event->transactionGroup->transactionJournals; + $repository = app(JournalRepositoryInterface::class); + $set = $collection->merge($repository->getUncompletedJournals()); + if (0 === $set->count()) { + Log::debug('Set is empty, never mind.'); + return; + } + if (!$event->flags->applyRules) { + Log::debug(sprintf('Will NOT process rules for %d journal(s)', $set->count())); + } + if (!$event->flags->recalculateCredit) { + Log::debug(sprintf('Will NOT recalculate credit for %d journal(s)', $set->count())); + } + if (!$event->flags->fireWebhooks) { + Log::debug(sprintf('Will NOT fire webhooks for %d journal(s)', $set->count())); + } + if ($event->flags->applyRules) { + $this->processRules($set); + } + if($event->flags->recalculateCredit) { + $this->recalculateCredit($set); + } + if($event->flags->fireWebhooks) { + $this->fireWebhooks($set); + } + // always remove old statistics. + $this->removePeriodStatistics($set); + + } + + private function removePeriodStatistics(Collection $set): void + { + Log::debug('Always remove period statistics'); + /** @var PeriodStatisticRepositoryInterface $repository */ + $repository = app(PeriodStatisticRepositoryInterface::class); + $repository->deleteStatisticsForCollection($set); + } + + private function fireWebhooks(Collection $set): void + { + // collect transaction groups by set ids. + $groups = TransactionGroup::whereIn('id',array_unique($set->pluck('transaction_group_id')->toArray()))->get(); + + Log::debug(__METHOD__); + $user = $set->first()->user; + + /** @var MessageGeneratorInterface $engine */ + $engine = app(MessageGeneratorInterface::class); + $engine->setUser($user); + + // tell the generator which trigger it should look for + $engine->setTrigger(WebhookTrigger::STORE_TRANSACTION); + // tell the generator which objects to process + $engine->setObjects($groups); + // tell the generator to generate the messages + $engine->generateMessages(); + + // trigger event to send them: + Log::debug(sprintf('send event WebhookMessagesRequestSending from %s', __METHOD__)); + event(new WebhookMessagesRequestSending()); + } + + private function recalculateCredit(Collection $set): void + { + Log::debug(sprintf('Will now recalculateCredit for %d journal(s)', $set->count())); + + /** @var CreditRecalculateService $object */ + $object = app(CreditRecalculateService::class); + $object->setJournals($set); + $object->recalculate(); + } + + private function processRules(Collection $set): void + { + Log::debug(sprintf('Will now processRules for %d journal(s)', $set->count())); + $array = $set->pluck('id')->toArray(); + $journalIds = implode(',', $array); + $user = $set->first()->user; + Log::debug(sprintf('Add local operator for journal(s): %s', $journalIds)); + + // collect rules: + $ruleGroupRepository = app(RuleGroupRepositoryInterface::class); + $ruleGroupRepository->setUser($user); + + // add the groups to the rule engine. + // it should run the rules in the group and cancel the group if necessary. + Log::debug('Fire processRules with ALL store-journal rule groups.'); + $groups = $ruleGroupRepository->getRuleGroupsWithRules('store-journal'); + + // create and fire rule engine. + $newRuleEngine = app(RuleEngineInterface::class); + $newRuleEngine->setUser($user); + $newRuleEngine->addOperator(['type' => 'journal_id', 'value' => $journalIds]); + $newRuleEngine->setRuleGroups($groups); + $newRuleEngine->fire(); } } diff --git a/app/Models/Account.php b/app/Models/Account.php index 13e71b4f55..f0db76e8d6 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -50,9 +50,9 @@ class Account extends Model use ReturnsIntegerUserIdTrait; use SoftDeletes; - protected $fillable = ['user_id', 'user_group_id', 'account_type_id', 'name', 'active', 'virtual_balance', 'iban', 'native_virtual_balance']; + protected $fillable = ['user_id', 'user_group_id', 'account_type_id', 'name', 'active', 'virtual_balance', 'iban', 'native_virtual_balance']; - protected $hidden = ['encrypted']; + protected $hidden = ['encrypted']; private bool $joinedAccountTypes = false; /** @@ -60,16 +60,19 @@ class Account extends Model * * @throws NotFoundHttpException */ - public static function routeBinder(string $value): self + public static function routeBinder(self | string $value): self { + if ($value instanceof self) { + $value = (int)$value->id; + } if (auth()->check()) { - $accountId = (int) $value; + $accountId = (int)$value; /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); /** @var null|Account $account */ - $account = $user->accounts()->with(['accountType'])->find($accountId); + $account = $user->accounts()->with(['accountType'])->find($accountId); if (null !== $account) { return $account; } @@ -126,7 +129,7 @@ class Account extends Model public function setVirtualBalanceAttribute(mixed $value): void { - $value = (string) $value; + $value = (string)$value; if ('' === $value) { $value = null; } @@ -145,7 +148,7 @@ class Account extends Model protected function accountId(): Attribute { - return Attribute::make(get: static fn ($value): int => (int) $value); + return Attribute::make(get: static fn($value): int => (int)$value); } /** @@ -171,7 +174,7 @@ class Account extends Model */ protected function accountTypeId(): Attribute { - return Attribute::make(get: static fn ($value): int => (int) $value); + return Attribute::make(get: static fn($value): int => (int)$value); } #[Scope] @@ -213,12 +216,12 @@ class Account extends Model protected function iban(): Attribute { - return Attribute::make(get: static fn ($value): ?string => null === $value ? null : trim(str_replace(' ', '', (string) $value))); + return Attribute::make(get: static fn($value): ?string => null === $value ? null : trim(str_replace(' ', '', (string)$value))); } protected function order(): Attribute { - return Attribute::make(get: static fn ($value): int => (int) $value); + return Attribute::make(get: static fn($value): int => (int)$value); } /** @@ -226,7 +229,7 @@ class Account extends Model */ protected function virtualBalance(): Attribute { - return Attribute::make(get: static fn ($value): string => (string) $value); + return Attribute::make(get: static fn($value): string => (string)$value); } public function primaryPeriodStatistics(): MorphMany diff --git a/app/Models/Attachment.php b/app/Models/Attachment.php index 18431fbb77..5aecec1793 100644 --- a/app/Models/Attachment.php +++ b/app/Models/Attachment.php @@ -62,8 +62,11 @@ class Attachment extends Model * * @throws NotFoundHttpException */ - public static function routeBinder(string $value): self + public static function routeBinder(self|string $value): self { + if($value instanceof self) { + $value = (int)$value->id; + } if (auth()->check()) { $attachmentId = (int) $value; diff --git a/app/Models/AvailableBudget.php b/app/Models/AvailableBudget.php index 2428a029f5..5940d3716b 100644 --- a/app/Models/AvailableBudget.php +++ b/app/Models/AvailableBudget.php @@ -59,8 +59,11 @@ class AvailableBudget extends Model * * @throws NotFoundHttpException */ - public static function routeBinder(string $value): self + public static function routeBinder(self|string $value): self { + if($value instanceof self) { + $value = (int)$value->id; + } if (auth()->check()) { $availableBudgetId = (int) $value; diff --git a/app/Models/Bill.php b/app/Models/Bill.php index 4b56656755..b3556ae206 100644 --- a/app/Models/Bill.php +++ b/app/Models/Bill.php @@ -74,8 +74,11 @@ class Bill extends Model * * @throws NotFoundHttpException */ - public static function routeBinder(string $value): self + public static function routeBinder(self|string $value): self { + if($value instanceof self) { + $value = (int)$value->id; + } if (auth()->check()) { $billId = (int) $value; diff --git a/app/Models/Budget.php b/app/Models/Budget.php index c6db07669f..0429aa786a 100644 --- a/app/Models/Budget.php +++ b/app/Models/Budget.php @@ -53,8 +53,11 @@ class Budget extends Model * * @throws NotFoundHttpException */ - public static function routeBinder(string $value): self + public static function routeBinder(self|string $value): self { + if($value instanceof self) { + $value = (int)$value->id; + } if (auth()->check()) { $budgetId = (int) $value; diff --git a/app/Models/BudgetLimit.php b/app/Models/BudgetLimit.php index 81acb6ce38..913542f7eb 100644 --- a/app/Models/BudgetLimit.php +++ b/app/Models/BudgetLimit.php @@ -53,8 +53,11 @@ class BudgetLimit extends Model * * @throws NotFoundHttpException */ - public static function routeBinder(string $value): self + public static function routeBinder(self|string $value): self { + if($value instanceof self) { + $value = (int)$value->id; + } if (auth()->check()) { $budgetLimitId = (int) $value; $budgetLimit = self::where('budget_limits.id', $budgetLimitId) diff --git a/app/Models/Category.php b/app/Models/Category.php index 7a3f13bde7..cedef52077 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -52,8 +52,11 @@ class Category extends Model * * @throws NotFoundHttpException */ - public static function routeBinder(string $value): self + public static function routeBinder(self|string $value): self { + if($value instanceof self) { + $value = (int)$value->id; + } if (auth()->check()) { $categoryId = (int) $value; diff --git a/app/Models/InvitedUser.php b/app/Models/InvitedUser.php index b6641989cd..0bcf570b7e 100644 --- a/app/Models/InvitedUser.php +++ b/app/Models/InvitedUser.php @@ -42,8 +42,11 @@ class InvitedUser extends Model /** * Route binder. Converts the key in the URL to the specified object (or throw 404). */ - public static function routeBinder(string $value): self + public static function routeBinder(self|string $value): self { + if($value instanceof self) { + $value = (int)$value->id; + } if (auth()->check()) { $attemptId = (int) $value; diff --git a/app/Models/LinkType.php b/app/Models/LinkType.php index cd3e06a8f0..3ddc0b5095 100644 --- a/app/Models/LinkType.php +++ b/app/Models/LinkType.php @@ -41,8 +41,11 @@ class LinkType extends Model * * @throws NotFoundHttpException */ - public static function routeBinder(string $value): self + public static function routeBinder(self|string $value): self { + if($value instanceof self) { + $value = (int)$value->id; + } if (auth()->check()) { $linkTypeId = (int) $value; $linkType = self::find($linkTypeId); diff --git a/app/Models/ObjectGroup.php b/app/Models/ObjectGroup.php index be4b5f6033..07da7cde19 100644 --- a/app/Models/ObjectGroup.php +++ b/app/Models/ObjectGroup.php @@ -45,8 +45,11 @@ class ObjectGroup extends Model * * @throws NotFoundHttpException */ - public static function routeBinder(string $value): self + public static function routeBinder(self|string $value): self { + if($value instanceof self) { + $value = (int)$value->id; + } if (auth()->check()) { $objectGroupId = (int) $value; diff --git a/app/Models/PiggyBank.php b/app/Models/PiggyBank.php index a03d61d7ca..9d5770166b 100644 --- a/app/Models/PiggyBank.php +++ b/app/Models/PiggyBank.php @@ -60,8 +60,11 @@ class PiggyBank extends Model * * @throws NotFoundHttpException */ - public static function routeBinder(string $value): self + public static function routeBinder(self|string $value): self { + if($value instanceof self) { + $value = (int)$value->id; + } if (auth()->check()) { $piggyBankId = (int) $value; $piggyBank = self::where('piggy_banks.id', $piggyBankId) diff --git a/app/Models/Preference.php b/app/Models/Preference.php index 0e282cf1a5..38d39f0901 100644 --- a/app/Models/Preference.php +++ b/app/Models/Preference.php @@ -42,8 +42,11 @@ class Preference extends Model * * @throws NotFoundHttpException */ - public static function routeBinder(string $value): self + public static function routeBinder(self|string $value): self { + if($value instanceof self) { + $value = (int)$value->id; + } if (auth()->check()) { /** @var User $user */ $user = auth()->user(); diff --git a/app/Models/Recurrence.php b/app/Models/Recurrence.php index 68fc118007..9b11c84d84 100644 --- a/app/Models/Recurrence.php +++ b/app/Models/Recurrence.php @@ -74,8 +74,11 @@ class Recurrence extends Model * * @throws NotFoundHttpException */ - public static function routeBinder(string $value): self + public static function routeBinder(self|string $value): self { + if($value instanceof self) { + $value = (int)$value->id; + } if (auth()->check()) { $recurrenceId = (int) $value; diff --git a/app/Models/Rule.php b/app/Models/Rule.php index 86fdee9aed..1caf94ee79 100644 --- a/app/Models/Rule.php +++ b/app/Models/Rule.php @@ -52,8 +52,11 @@ class Rule extends Model * * @throws NotFoundHttpException */ - public static function routeBinder(string $value): self + public static function routeBinder(self|string $value): self { + if($value instanceof self) { + $value = (int)$value->id; + } if (auth()->check()) { $ruleId = (int) $value; diff --git a/app/Models/RuleGroup.php b/app/Models/RuleGroup.php index f0a85a78e1..f3c76a04d7 100644 --- a/app/Models/RuleGroup.php +++ b/app/Models/RuleGroup.php @@ -54,8 +54,11 @@ class RuleGroup extends Model * * @throws NotFoundHttpException */ - public static function routeBinder(string $value): self + public static function routeBinder(self|string $value): self { + if($value instanceof self) { + $value = (int)$value->id; + } if (auth()->check()) { $ruleGroupId = (int) $value; diff --git a/app/Models/Tag.php b/app/Models/Tag.php index 15f1489d9e..857999aaf1 100644 --- a/app/Models/Tag.php +++ b/app/Models/Tag.php @@ -52,8 +52,11 @@ class Tag extends Model * * @throws NotFoundHttpException */ - public static function routeBinder(string $value): self + public static function routeBinder(self|string $value): self { + if($value instanceof self) { + $value = (int)$value->id; + } if (auth()->check()) { $tagId = (int) $value; diff --git a/app/Models/TransactionCurrency.php b/app/Models/TransactionCurrency.php index 942b9ef1fd..5d001e062f 100644 --- a/app/Models/TransactionCurrency.php +++ b/app/Models/TransactionCurrency.php @@ -48,8 +48,11 @@ class TransactionCurrency extends Model * * @throws NotFoundHttpException */ - public static function routeBinder(string $value): self + public static function routeBinder(self|string $value): self { + if($value instanceof self) { + $value = (int)$value->id; + } if (auth()->check()) { $currencyId = (int) $value; $currency = self::find($currencyId); diff --git a/app/Models/TransactionGroup.php b/app/Models/TransactionGroup.php index 7c55c1f2fa..2c434d7221 100644 --- a/app/Models/TransactionGroup.php +++ b/app/Models/TransactionGroup.php @@ -55,8 +55,11 @@ class TransactionGroup extends Model * * @throws NotFoundHttpException */ - public static function routeBinder(string $value): self + public static function routeBinder(self|string $value): self { + if($value instanceof self) { + $value = (int)$value->id; + } Log::debug(sprintf('Now in %s("%s")', __METHOD__, $value)); if (auth()->check()) { $groupId = (int) $value; diff --git a/app/Models/TransactionJournal.php b/app/Models/TransactionJournal.php index aee0f75ade..cf44d4b146 100644 --- a/app/Models/TransactionJournal.php +++ b/app/Models/TransactionJournal.php @@ -79,8 +79,11 @@ class TransactionJournal extends Model * * @throws NotFoundHttpException */ - public static function routeBinder(string $value): self + public static function routeBinder(self|string $value): self { + if($value instanceof self) { + $value = (int)$value->id; + } if (auth()->check()) { $journalId = (int) $value; diff --git a/app/Models/TransactionJournalLink.php b/app/Models/TransactionJournalLink.php index b06dd28dc0..fcf8215a2e 100644 --- a/app/Models/TransactionJournalLink.php +++ b/app/Models/TransactionJournalLink.php @@ -41,8 +41,12 @@ class TransactionJournalLink extends Model * * @throws NotFoundHttpException */ - public static function routeBinder(string $value): self + public static function routeBinder(self|string $value): self { + if($value instanceof self) { + $value = (int)$value->id; + } + if (auth()->check()) { $linkId = (int) $value; $link = self::where('journal_links.id', $linkId) diff --git a/app/Models/TransactionType.php b/app/Models/TransactionType.php index ec47b0584a..2218128e3a 100644 --- a/app/Models/TransactionType.php +++ b/app/Models/TransactionType.php @@ -72,12 +72,15 @@ class TransactionType extends Model * * @throws NotFoundHttpException */ - public static function routeBinder(string $type): self + public static function routeBinder(self|string $value): self { if (!auth()->check()) { throw new NotFoundHttpException(); } - $transactionType = self::where('type', ucfirst($type))->first(); + if($value instanceof self) { + $value = (string)$value->type; + } + $transactionType = self::where('type', ucfirst($value))->first(); if (null !== $transactionType) { return $transactionType; } diff --git a/app/Models/UserGroup.php b/app/Models/UserGroup.php index e234113474..fed732beee 100644 --- a/app/Models/UserGroup.php +++ b/app/Models/UserGroup.php @@ -44,9 +44,13 @@ class UserGroup extends Model * * @throws NotFoundHttpException */ - public static function routeBinder(string $value): self + public static function routeBinder(self|string $value): self { if (auth()->check()) { + if($value instanceof self) { + $value = (int)$value->id; + } + $userGroupId = (int) $value; /** @var User $user */ diff --git a/app/Models/Webhook.php b/app/Models/Webhook.php index f2447994d2..938637ee4b 100644 --- a/app/Models/Webhook.php +++ b/app/Models/Webhook.php @@ -130,9 +130,12 @@ class Webhook extends Model * * @throws NotFoundHttpException */ - public static function routeBinder(string $value): self + public static function routeBinder(self|string $value): self { if (auth()->check()) { + if($value instanceof self) { + $value = (int)$value->id; + } $webhookId = (int) $value; /** @var User $user */ diff --git a/app/Models/WebhookAttempt.php b/app/Models/WebhookAttempt.php index f3678a22b5..f1685a1047 100644 --- a/app/Models/WebhookAttempt.php +++ b/app/Models/WebhookAttempt.php @@ -42,9 +42,12 @@ class WebhookAttempt extends Model * * @throws NotFoundHttpException */ - public static function routeBinder(string $value): self + public static function routeBinder(self|string $value): self { if (auth()->check()) { + if($value instanceof self) { + $value = (int)$value->id; + } $attemptId = (int) $value; /** @var User $user */ diff --git a/app/Models/WebhookMessage.php b/app/Models/WebhookMessage.php index 3a896fcada..da00ba2e75 100644 --- a/app/Models/WebhookMessage.php +++ b/app/Models/WebhookMessage.php @@ -44,9 +44,12 @@ class WebhookMessage extends Model * * @throws NotFoundHttpException */ - public static function routeBinder(string $value): self + public static function routeBinder(self|string $value): self { if (auth()->check()) { + if($value instanceof self) { + $value = (int)$value->id; + } $messageId = (int) $value; /** @var User $user */ diff --git a/app/Repositories/Journal/JournalRepository.php b/app/Repositories/Journal/JournalRepository.php index 7439cf6d48..025ded15ce 100644 --- a/app/Repositories/Journal/JournalRepository.php +++ b/app/Repositories/Journal/JournalRepository.php @@ -250,4 +250,10 @@ class JournalRepository implements JournalRepositoryInterface, UserGroupInterfac return $journal; } + + #[\Override] + public function getUncompletedJournals(): Collection + { + return $this->userGroup->transactionJournals()->where('completed', false)->get(['transaction_journals.*']); + } } diff --git a/app/Repositories/Journal/JournalRepositoryInterface.php b/app/Repositories/Journal/JournalRepositoryInterface.php index 8971bf9234..9df1fe9a1e 100644 --- a/app/Repositories/Journal/JournalRepositoryInterface.php +++ b/app/Repositories/Journal/JournalRepositoryInterface.php @@ -52,6 +52,8 @@ interface JournalRepositoryInterface */ public function destroyGroup(TransactionGroup $transactionGroup): void; + public function getUncompletedJournals(): Collection; + /** * Deletes a journal. */ diff --git a/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php index 0adbae44f0..a0ba97a47a 100644 --- a/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php +++ b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php @@ -25,12 +25,18 @@ declare(strict_types=1); namespace FireflyIII\Repositories\PeriodStatistic; use Carbon\Carbon; +use FireflyIII\Models\Account; +use FireflyIII\Models\Budget; +use FireflyIII\Models\Category; use FireflyIII\Models\PeriodStatistic; -use FireflyIII\Models\UserGroup; +use FireflyIII\Models\Tag; +use FireflyIII\Models\Transaction; use FireflyIII\Support\Repositories\UserGroup\UserGroupInterface; use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Override; @@ -50,7 +56,7 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface, U public function saveStatistic(Model $model, int $currencyId, Carbon $start, Carbon $end, string $type, int $count, string $amount): PeriodStatistic { - $stat = new PeriodStatistic(); + $stat = new PeriodStatistic(); $stat->primaryStatable()->associate($model); $stat->transaction_currency_id = $currencyId; $stat->user_group_id = $this->getUserGroup()->id; @@ -64,16 +70,16 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface, U $stat->save(); Log::debug(sprintf( - 'Saved #%d [currency #%d, Model %s #%d, %s to %s, %d, %s] as new statistic.', - $stat->id, - $model::class, - $model->id, - $stat->transaction_currency_id, - $stat->start->toW3cString(), - $stat->end->toW3cString(), - $count, - $amount - )); + 'Saved #%d [currency #%d, Model %s #%d, %s to %s, %d, %s] as new statistic.', + $stat->id, + $model::class, + $model->id, + $stat->transaction_currency_id, + $stat->start->toW3cString(), + $stat->end->toW3cString(), + $count, + $amount + )); return $stat; } @@ -96,20 +102,20 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface, U ->where('type', 'LIKE', sprintf('%s%%', $prefix)) ->where('start', '>=', $start) ->where('end', '<=', $end) - ->get() - ; + ->get(); } #[Override] public function savePrefixedStatistic( string $prefix, - int $currencyId, + int $currencyId, Carbon $start, Carbon $end, string $type, - int $count, + int $count, string $amount - ): PeriodStatistic { + ): PeriodStatistic + { $stat = new PeriodStatistic(); $stat->transaction_currency_id = $currencyId; $stat->user_group_id = $this->getUserGroup()->id; @@ -123,22 +129,87 @@ class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface, U $stat->save(); Log::debug(sprintf( - 'Saved #%d [currency #%d, type "%s", %s to %s, %d, %s] as new statistic.', - $stat->id, - $stat->transaction_currency_id, - $stat->type, - $stat->start->toW3cString(), - $stat->end->toW3cString(), - $count, - $amount - )); + 'Saved #%d [currency #%d, type "%s", %s to %s, %d, %s] as new statistic.', + $stat->id, + $stat->transaction_currency_id, + $stat->type, + $stat->start->toW3cString(), + $stat->end->toW3cString(), + $count, + $amount + )); return $stat; } #[Override] - public function deleteStatisticsForPrefix(UserGroup $userGroup, string $prefix, Carbon $date): void + public function deleteStatisticsForPrefix(string $prefix, Collection $dates): void { - $userGroup->periodStatistics()->where('start', '<=', $date)->where('end', '>=', $date)->where('type', 'LIKE', sprintf('%s%%', $prefix))->delete(); + $count = $this->userGroup->periodStatistics() + ->where(function (Builder $q) use ($dates) { + foreach ($dates as $date) { + $q->where(function (Builder $q1) use ($date) { + $q1->where('start', '<=', $date)->where('end', '>=', $date); + }); + } + }) + ->where('type', 'LIKE', sprintf('%s%%', $prefix))->delete(); + Log::debug(sprintf('Deleted %d entries for prefix "%s"', $count, $prefix)); + } + + public function deleteStatisticsForType(string $class, Collection $objects, Collection $dates): void + { + if (0 === count($objects)) { + Log::debug(sprintf('Nothing to delete in deleteStatisticsForType("%s")', $class)); + return; + } + $count = PeriodStatistic + ::where('primary_statable_type', $class) + ->whereIn('primary_statable_id', $objects->pluck('id')->toArray()) + ->where(function (Builder $q) use ($dates) { + foreach ($dates as $date) { + $q->where(function (Builder $q1) use ($date) { + $q1->where('start', '<=', $date)->where('end', '>=', $date); + }); + } + }) + ->delete(); + Log::debug(sprintf('Delete %d statistics for %dx %s', $count, $objects->count(), $class)); + } + + #[\Override] + public function deleteStatisticsForCollection(Collection $set) + { + Log::debug(sprintf('Delete statistics for %d transaction journals.', count($set))); + // collect all transactions: + $transactions = Transaction::whereIn('transaction_journal_id', $set->pluck('id')->toArray())->get(['transactions.*']); + + // collect all accounts and delete stats: + $accounts = Account::whereIn('id', $transactions->pluck('account_id')->toArray())->get(['accounts.*']); + $dates = $set->pluck('date'); + $this->deleteStatisticsForType(Account::class, $accounts, $dates); + + // collect all categories, and remove stats. + $categories = Category::whereIn('id', DB::table('category_transaction_journal')->whereIn('transaction_journal_id', $set->pluck('id')->toArray())->get(['category_transaction_journal.category_id'])->pluck('category_id')->toArray())->get(['categories.*']); + $this->deleteStatisticsForType(Category::class, $categories, $dates); + + // budgets, same thing + $budgets = Budget::whereIn('id', DB::table('budget_transaction_journal')->whereIn('transaction_journal_id', $set->pluck('id')->toArray())->get(['budget_transaction_journal.budget_id'])->pluck('budget_id')->toArray())->get(['budgets.*']); + $this->deleteStatisticsForType(Budget::class, $budgets, $dates); + + // tags + $tags = Tag::whereIn('id', DB::table('tag_transaction_journal')->whereIn('transaction_journal_id', $set->pluck('id')->toArray())->get(['tag_transaction_journal.tag_id'])->pluck('tag_id')->toArray())->get(['tags.*']); + $this->deleteStatisticsForType(Tag::class, $tags, $dates); + + // remove for no tag, no cat, etc. + if (0 === $categories->count()) { + $this->deleteStatisticsForPrefix('no_category', $dates); + } + if (0 === $budgets->count()) { + $this->deleteStatisticsForPrefix('no_budget', $dates); + } + if (0 === $tags->count()) { + $this->deleteStatisticsForPrefix('no_tag', $dates); + } } } diff --git a/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php b/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php index 063fa6fdb2..4c7b100cef 100644 --- a/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php +++ b/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php @@ -32,6 +32,8 @@ use Illuminate\Support\Collection; interface PeriodStatisticRepositoryInterface { + public function deleteStatisticsForCollection(Collection $set); + public function findPeriodStatistics(Model $model, Carbon $start, Carbon $end, array $types): Collection; public function findPeriodStatistic(Model $model, Carbon $start, Carbon $end, string $type): Collection; @@ -54,5 +56,5 @@ interface PeriodStatisticRepositoryInterface public function deleteStatisticsForModel(Model $model, Carbon $date): void; - public function deleteStatisticsForPrefix(UserGroup $userGroup, string $prefix, Carbon $date): void; + public function deleteStatisticsForPrefix(string $prefix, Collection $dates): void; } diff --git a/app/Services/Internal/Support/CreditRecalculateService.php b/app/Services/Internal/Support/CreditRecalculateService.php index 50290f81b3..f21d3df510 100644 --- a/app/Services/Internal/Support/CreditRecalculateService.php +++ b/app/Services/Internal/Support/CreditRecalculateService.php @@ -34,6 +34,7 @@ use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Support\Facades\Steam; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; /** @@ -41,10 +42,16 @@ use Illuminate\Support\Facades\Log; */ class CreditRecalculateService { - private ?Account $account = null; - private ?TransactionGroup $group = null; + private ?Account $account = null; + private ?TransactionGroup $group = null; + private Collection $journals; private AccountRepositoryInterface $repository; - private array $work = []; + private array $work = []; + + public function __construct() + { + $this->journals = new Collection(); + } public function recalculate(): void { @@ -58,7 +65,11 @@ class CreditRecalculateService // work based on account. $this->processAccount(); } + if ($this->journals->count() > 0) { + $this->processJournals(); + } if (0 === count($this->work)) { + Log::debug('No work found for recalculate() to do.'); return; } $this->processWork(); @@ -77,6 +88,19 @@ class CreditRecalculateService } } + private function processJournals(): void + { + foreach ($this->journals as $journal) { + try { + $this->findByJournal($journal); + } catch (FireflyException $e) { + Log::error($e->getTraceAsString()); + Log::error(sprintf('Could not find work account for transaction journal #%d.', $journal->id)); + } + } + } + + /** * @throws FireflyException */ @@ -86,7 +110,7 @@ class CreditRecalculateService $destination = $this->getDestinationAccount($journal); // destination or source must be liability. - $valid = config('firefly.valid_liabilities'); + $valid = config('firefly.valid_liabilities'); if (in_array($destination->accountType->type, $valid, true)) { $this->work[] = $destination; } @@ -109,7 +133,7 @@ class CreditRecalculateService private function getAccountByDirection(TransactionJournal $journal, string $direction): Account { /** @var null|Transaction $transaction */ - $transaction = $journal->transactions()->where('amount', $direction, '0')->first(); + $transaction = $journal->transactions()->where('amount', $direction, '0')->first(); if (null === $transaction) { throw new FireflyException(sprintf('Cannot find "%s"-transaction of journal #%d', $direction, $journal->id)); } @@ -152,19 +176,19 @@ class CreditRecalculateService Log::debug(sprintf('Now processing account #%d ("%s").', $account->id, $account->name)); // get opening balance (if present) $this->repository->setUser($account->user); - $direction = (string) $this->repository->getMetaValue($account, 'liability_direction'); + $direction = (string)$this->repository->getMetaValue($account, 'liability_direction'); $openingBalance = $this->repository->getOpeningBalance($account); // Log::debug(sprintf('Found opening balance transaction journal #%d', $openingBalance->id)); // if account direction is "debit" ("I owe this amount") the opening balance must always be AWAY from the account: if ($openingBalance instanceof TransactionJournal && 'debit' === $direction) { $this->validateOpeningBalance($account, $openingBalance); } - $startOfDebt = $this->repository->getOpeningBalanceAmount($account, false) ?? '0'; - $leftOfDebt = Steam::positive($startOfDebt); + $startOfDebt = $this->repository->getOpeningBalanceAmount($account, false) ?? '0'; + $leftOfDebt = Steam::positive($startOfDebt); // Log::debug(sprintf('Start of debt is "%s", so initial left of debt is "%s"', \FireflyIII\Support\Facades\Steam::bcround($startOfDebt, 2), \FireflyIII\Support\Facades\Steam::bcround($leftOfDebt, 2))); /** @var AccountMetaFactory $factory */ - $factory = app(AccountMetaFactory::class); + $factory = app(AccountMetaFactory::class); // amount is positive or negative, doesn't matter. $factory->crud($account, 'start_of_debt', $startOfDebt); @@ -172,12 +196,11 @@ class CreditRecalculateService // Log::debug(sprintf('Debt direction is "%s"', $direction)); // now loop all transactions (except opening balance and credit thing) - $transactions = $account + $transactions = $account ->transactions() ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->orderBy('transaction_journals.date', 'ASC') - ->get(['transactions.*']) - ; + ->get(['transactions.*']); $transactions->count(); // Log::debug(sprintf('Found %d transaction(s) to process.', $total)); @@ -199,7 +222,7 @@ class CreditRecalculateService $source = $openingBalance->transactions()->where('amount', '<', 0)->first(); /** @var Transaction $dest */ - $dest = $openingBalance->transactions()->where('amount', '>', 0)->first(); + $dest = $openingBalance->transactions()->where('amount', '>', 0)->first(); if ($source->account_id !== $account->id) { Log::info(sprintf('Liability #%d has a reversed opening balance. Will fix this now.', $account->id)); Log::debug(sprintf('Source amount "%s" is now "%s"', $source->amount, Steam::positive($source->amount))); @@ -230,7 +253,7 @@ class CreditRecalculateService */ private function processTransaction(Account $account, string $direction, Transaction $transaction, string $leftOfDebt): string { - $journal = $transaction->transactionJournal; + $journal = $transaction->transactionJournal; // here be null pointers. if (null === $journal) { @@ -257,15 +280,15 @@ class CreditRecalculateService // Log::debug(sprintf('Liability direction is "%s"', $direction)); // amount to use depends on the currency: - $usedAmount = $this->getAmountToUse($transaction, $accountCurrency, $foreignCurrency); - $isSameAccount = $account->id === $transaction->account_id; - $isDebit = 'debit' === $direction; - $isCredit = 'credit' === $direction; + $usedAmount = $this->getAmountToUse($transaction, $accountCurrency, $foreignCurrency); + $isSameAccount = $account->id === $transaction->account_id; + $isDebit = 'debit' === $direction; + $isCredit = 'credit' === $direction; if ($isSameAccount && $isCredit && $this->isWithdrawalIn($usedAmount, $type)) { // case 1 $usedAmount = Steam::positive($usedAmount); - return bcadd($leftOfDebt, (string) $usedAmount); + return bcadd($leftOfDebt, (string)$usedAmount); // Log::debug(sprintf('Case 1 (withdrawal into credit liability): %s + %s = %s', \FireflyIII\Support\Facades\Steam::bcround($leftOfDebt, 2), \FireflyIII\Support\Facades\Steam::bcround($usedAmount, 2), \FireflyIII\Support\Facades\Steam::bcround($result, 2))); } @@ -273,7 +296,7 @@ class CreditRecalculateService if ($isSameAccount && $isCredit && $this->isWithdrawalOut($usedAmount, $type)) { // case 2 $usedAmount = Steam::positive($usedAmount); - return bcsub($leftOfDebt, (string) $usedAmount); + return bcsub($leftOfDebt, (string)$usedAmount); // Log::debug(sprintf('Case 2 (withdrawal away from liability): %s - %s = %s', \FireflyIII\Support\Facades\Steam::bcround($leftOfDebt, 2), \FireflyIII\Support\Facades\Steam::bcround($usedAmount, 2), \FireflyIII\Support\Facades\Steam::bcround($result, 2))); } @@ -281,7 +304,7 @@ class CreditRecalculateService if ($isSameAccount && $isCredit && $this->isDepositOut($usedAmount, $type)) { // case 3 $usedAmount = Steam::positive($usedAmount); - return bcsub($leftOfDebt, (string) $usedAmount); + return bcsub($leftOfDebt, (string)$usedAmount); // Log::debug(sprintf('Case 3 (deposit away from liability): %s - %s = %s', \FireflyIII\Support\Facades\Steam::bcround($leftOfDebt, 2), \FireflyIII\Support\Facades\Steam::bcround($usedAmount, 2), \FireflyIII\Support\Facades\Steam::bcround($result, 2))); } @@ -289,35 +312,35 @@ class CreditRecalculateService if ($isSameAccount && $isCredit && $this->isDepositIn($usedAmount, $type)) { // case 4 $usedAmount = Steam::positive($usedAmount); - return bcadd($leftOfDebt, (string) $usedAmount); + return bcadd($leftOfDebt, (string)$usedAmount); // Log::debug(sprintf('Case 4 (deposit into credit liability): %s + %s = %s', \FireflyIII\Support\Facades\Steam::bcround($leftOfDebt, 2), \FireflyIII\Support\Facades\Steam::bcround($usedAmount, 2), \FireflyIII\Support\Facades\Steam::bcround($result, 2))); } if ($isSameAccount && $isCredit && $this->isTransferIn($usedAmount, $type)) { // case 5 $usedAmount = Steam::positive($usedAmount); - return bcadd($leftOfDebt, (string) $usedAmount); + return bcadd($leftOfDebt, (string)$usedAmount); // Log::debug(sprintf('Case 5 (transfer into credit liability): %s + %s = %s', \FireflyIII\Support\Facades\Steam::bcround($leftOfDebt, 2), \FireflyIII\Support\Facades\Steam::bcround($usedAmount, 2), \FireflyIII\Support\Facades\Steam::bcround($result, 2))); } if ($isSameAccount && $isDebit && $this->isWithdrawalIn($usedAmount, $type)) { // case 6 $usedAmount = Steam::positive($usedAmount); - return bcsub($leftOfDebt, (string) $usedAmount); + return bcsub($leftOfDebt, (string)$usedAmount); // Log::debug(sprintf('Case 6 (withdrawal into debit liability): %s - %s = %s', \FireflyIII\Support\Facades\Steam::bcround($leftOfDebt, 2), \FireflyIII\Support\Facades\Steam::bcround($usedAmount, 2), \FireflyIII\Support\Facades\Steam::bcround($result, 2))); } if ($isSameAccount && $isDebit && $this->isDepositOut($usedAmount, $type)) { // case 7 $usedAmount = Steam::positive($usedAmount); - return bcadd($leftOfDebt, (string) $usedAmount); + return bcadd($leftOfDebt, (string)$usedAmount); // Log::debug(sprintf('Case 7 (deposit away from liability): %s + %s = %s', \FireflyIII\Support\Facades\Steam::bcround($leftOfDebt, 2), \FireflyIII\Support\Facades\Steam::bcround($usedAmount, 2), \FireflyIII\Support\Facades\Steam::bcround($result, 2))); } if ($isSameAccount && $isDebit && $this->isWithdrawalOut($usedAmount, $type)) { // case 8 $usedAmount = Steam::positive($usedAmount); - return bcadd($leftOfDebt, (string) $usedAmount); + return bcadd($leftOfDebt, (string)$usedAmount); // Log::debug(sprintf('Case 8 (withdrawal away from liability): %s + %s = %s', \FireflyIII\Support\Facades\Steam::bcround($leftOfDebt, 2), \FireflyIII\Support\Facades\Steam::bcround($usedAmount, 2), \FireflyIII\Support\Facades\Steam::bcround($result, 2))); } @@ -325,7 +348,7 @@ class CreditRecalculateService if ($isSameAccount && $isDebit && $this->isTransferIn($usedAmount, $type)) { // case 9 $usedAmount = Steam::positive($usedAmount); - return bcsub($leftOfDebt, (string) $usedAmount); + return bcsub($leftOfDebt, (string)$usedAmount); // 2024-10-05, #9225 this used to say you would owe more, but a transfer INTO a debit from wherever means you owe LESS. // Log::debug(sprintf('Case 9 (transfer into debit liability, means you owe LESS): %s - %s = %s', \FireflyIII\Support\Facades\Steam::bcround($leftOfDebt, 2), \FireflyIII\Support\Facades\Steam::bcround($usedAmount, 2), \FireflyIII\Support\Facades\Steam::bcround($result, 2))); @@ -333,7 +356,7 @@ class CreditRecalculateService if ($isSameAccount && $isDebit && $this->isTransferOut($usedAmount, $type)) { // case 10 $usedAmount = Steam::positive($usedAmount); - return bcadd($leftOfDebt, (string) $usedAmount); + return bcadd($leftOfDebt, (string)$usedAmount); // 2024-10-05, #9225 this used to say you would owe less, but a transfer OUT OF a debit from wherever means you owe MORE. // Log::debug(sprintf('Case 10 (transfer out of debit liability, means you owe MORE): %s + %s = %s', \FireflyIII\Support\Facades\Steam::bcround($leftOfDebt, 2), \FireflyIII\Support\Facades\Steam::bcround($usedAmount, 2), \FireflyIII\Support\Facades\Steam::bcround($result, 2))); @@ -343,7 +366,7 @@ class CreditRecalculateService if (in_array($type, [TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::DEPOSIT->value, TransactionTypeEnum::TRANSFER->value], true)) { $usedAmount = Steam::negative($usedAmount); - return bcadd($leftOfDebt, (string) $usedAmount); + return bcadd($leftOfDebt, (string)$usedAmount); // Log::debug(sprintf('Case X (all other cases): %s + %s = %s', \FireflyIII\Support\Facades\Steam::bcround($leftOfDebt, 2), \FireflyIII\Support\Facades\Steam::bcround($usedAmount, 2), \FireflyIII\Support\Facades\Steam::bcround($result, 2))); } @@ -460,4 +483,9 @@ class CreditRecalculateService { $this->group = $group; } + + public function setJournals(Collection $journals): void + { + $this->journals = $journals; + } } diff --git a/app/User.php b/app/User.php index 0e8ebbb748..23246f969b 100644 --- a/app/User.php +++ b/app/User.php @@ -86,8 +86,11 @@ class User extends Authenticatable /** * @throws NotFoundHttpException */ - public static function routeBinder(string $value): self + public static function routeBinder(self|string $value): self { + if($value instanceof self) { + $value = (int)$value->id; + } if (auth()->check()) { $userId = (int) $value; $user = self::find($userId); diff --git a/bootstrap/app.php b/bootstrap/app.php index e4b25b5ada..4d43d2d3d3 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -92,6 +92,7 @@ $app = Application::configure(basePath: dirname(__DIR__)) health : '/up', ) ->withMiddleware(function (Middleware $middleware): void { + // always append secure header thing. $middleware->append(\FireflyIII\Http\Middleware\SecureHeaders::class);