diff --git a/app/Api/V1/Controllers/Models/Transaction/UpdateController.php b/app/Api/V1/Controllers/Models/Transaction/UpdateController.php index 430bf872e5..fba49b20bc 100644 --- a/app/Api/V1/Controllers/Models/Transaction/UpdateController.php +++ b/app/Api/V1/Controllers/Models/Transaction/UpdateController.php @@ -26,7 +26,8 @@ namespace FireflyIII\Api\V1\Controllers\Models\Transaction; use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Requests\Models\Transaction\UpdateRequest; -use FireflyIII\Events\UpdatedTransactionGroup; +use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags; +use FireflyIII\Events\Model\TransactionGroup\UpdatedSingleTransactionGroup; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\TransactionGroup; use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface; @@ -54,7 +55,7 @@ class UpdateController extends Controller parent::__construct(); $this->middleware(function ($request, $next) { /** @var User $admin */ - $admin = auth()->user(); + $admin = auth()->user(); $this->groupRepository = app(TransactionGroupRepositoryInterface::class); $this->groupRepository->setUser($admin); @@ -72,49 +73,50 @@ class UpdateController extends Controller public function update(UpdateRequest $request, TransactionGroup $transactionGroup): JsonResponse { Log::debug('Now in update routine for transaction group'); - $data = $request->getAll(); - $oldHash = $this->groupRepository->getCompareHash($transactionGroup); - $transactionGroup = $this->groupRepository->update($transactionGroup, $data); - $newHash = $this->groupRepository->getCompareHash($transactionGroup); - $manager = $this->getManager(); + $data = $request->getAll(); + $oldHash = $this->groupRepository->getCompareHash($transactionGroup); + $transactionGroup = $this->groupRepository->update($transactionGroup, $data); + $newHash = $this->groupRepository->getCompareHash($transactionGroup); + $manager = $this->getManager(); Preferences::mark(); $applyRules = $data['apply_rules'] ?? true; $fireWebhooks = $data['fire_webhooks'] ?? true; $runRecalculations = $oldHash !== $newHash; - // FIXME responds to a single event. - // flags in array? - event(new UpdatedTransactionGroup($transactionGroup, $applyRules, $fireWebhooks, $runRecalculations)); + $flags = new TransactionGroupEventFlags(); + $flags->applyRules = $applyRules; + $flags->fireWebhooks = $fireWebhooks; + $flags->recalculateCredit = $runRecalculations; + event(new UpdatedSingleTransactionGroup($transactionGroup, $flags)); /** @var User $admin */ - $admin = auth()->user(); + $admin = auth()->user(); // use new group collector: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector ->setUser($admin) // filter on transaction group. ->setTransactionGroup($transactionGroup) // all info needed for the API: - ->withAPIInformation() - ; + ->withAPIInformation(); - $selectedGroup = $collector->getGroups()->first(); + $selectedGroup = $collector->getGroups()->first(); if (null === $selectedGroup) { throw new NotFoundHttpException(); } // enrich - $enrichment = new TransactionGroupEnrichment(); + $enrichment = new TransactionGroupEnrichment(); $enrichment->setUser($admin); - $selectedGroup = $enrichment->enrichSingle($selectedGroup); + $selectedGroup = $enrichment->enrichSingle($selectedGroup); /** @var TransactionGroupTransformer $transformer */ - $transformer = app(TransactionGroupTransformer::class); + $transformer = app(TransactionGroupTransformer::class); $transformer->setParameters($this->parameters); - $resource = new Item($selectedGroup, $transformer, 'transactions'); + $resource = new Item($selectedGroup, $transformer, 'transactions'); return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE); } diff --git a/app/Console/Commands/Correction/CorrectsGroupAccounts.php b/app/Console/Commands/Correction/CorrectsGroupAccounts.php index 2c82f4cfc7..57d078ac3a 100644 --- a/app/Console/Commands/Correction/CorrectsGroupAccounts.php +++ b/app/Console/Commands/Correction/CorrectsGroupAccounts.php @@ -25,8 +25,8 @@ declare(strict_types=1); namespace FireflyIII\Console\Commands\Correction; use FireflyIII\Console\Commands\ShowsFriendlyMessages; -use FireflyIII\Events\UpdatedTransactionGroup; -use FireflyIII\Handlers\Events\UpdatedGroupEventHandler; +use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags; +use FireflyIII\Events\Model\TransactionGroup\UpdatedSingleTransactionGroup; use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; use Illuminate\Console\Command; @@ -44,22 +44,22 @@ class CorrectsGroupAccounts extends Command */ public function handle(): int { - $groups = []; - $res = TransactionJournal::groupBy('transaction_group_id')->get(['transaction_group_id', DB::raw('COUNT(transaction_group_id) as the_count')]); + $groups = []; + $res = TransactionJournal::groupBy('transaction_group_id')->get(['transaction_group_id', DB::raw('COUNT(transaction_group_id) as the_count')]); /** @var TransactionJournal $journal */ foreach ($res as $journal) { - if ((int) $journal->the_count > 1) { - $groups[] = (int) $journal->transaction_group_id; + if ((int)$journal->the_count > 1) { + $groups[] = (int)$journal->transaction_group_id; } } - $handler = new UpdatedGroupEventHandler(); foreach ($groups as $groupId) { - $group = TransactionGroup::find($groupId); - // TODO in theory the "unifyAccounts" method could lead to the need for run recalculations. - // FIXME needs to be a collection. - $event = new UpdatedTransactionGroup($group, true, true, false); - $handler->unifyAccounts($event); + $group = TransactionGroup::find($groupId); + $flags = new TransactionGroupEventFlags(); + $flags->applyRules = true; + $flags->fireWebhooks = true; + $flags->recalculateCredit = true; + event(new UpdatedSingleTransactionGroup($group, $flags)); } return 0; diff --git a/app/Events/Model/TransactionGroup/TriggeredStoredTransactionGroup.php b/app/Events/Model/TransactionGroup/TriggeredStoredTransactionGroup.php index 37b1598f71..916ddd34e6 100644 --- a/app/Events/Model/TransactionGroup/TriggeredStoredTransactionGroup.php +++ b/app/Events/Model/TransactionGroup/TriggeredStoredTransactionGroup.php @@ -29,6 +29,9 @@ use FireflyIII\Models\RuleGroup; use FireflyIII\Models\TransactionGroup; use Illuminate\Queue\SerializesModels; +/** + * @deprecated + */ class TriggeredStoredTransactionGroup extends Event { use SerializesModels; diff --git a/app/Events/Model/TransactionGroup/UpdatedSingleTransactionGroup.php b/app/Events/Model/TransactionGroup/UpdatedSingleTransactionGroup.php new file mode 100644 index 0000000000..8826825166 --- /dev/null +++ b/app/Events/Model/TransactionGroup/UpdatedSingleTransactionGroup.php @@ -0,0 +1,36 @@ +. + */ + +namespace FireflyIII\Events\Model\TransactionGroup; + +use FireflyIII\Events\Event; +use FireflyIII\Models\TransactionGroup; +use Illuminate\Queue\SerializesModels; + +class UpdatedSingleTransactionGroup extends Event +{ + use SerializesModels; + + /** + * Create a new event instance. + */ + public function __construct(public TransactionGroup $transactionGroup, public TransactionGroupEventFlags $flags) {} +} diff --git a/app/Handlers/Events/UpdatedGroupEventHandler.php b/app/Handlers/Events/UpdatedGroupEventHandler.php index 31ee34c40b..a5ee0ff189 100644 --- a/app/Handlers/Events/UpdatedGroupEventHandler.php +++ b/app/Handlers/Events/UpdatedGroupEventHandler.php @@ -23,13 +23,10 @@ declare(strict_types=1); namespace FireflyIII\Handlers\Events; -use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Enums\WebhookTrigger; use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending; use FireflyIII\Events\UpdatedTransactionGroup; use FireflyIII\Generator\Webhook\MessageGeneratorInterface; -use FireflyIII\Models\Account; -use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface; use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface; @@ -45,18 +42,6 @@ use Illuminate\Support\Facades\Log; */ class UpdatedGroupEventHandler { - public function runAllHandlers(UpdatedTransactionGroup $event): void - { - $this->unifyAccounts($event); - $this->processRules($event); - $this->recalculateCredit($event); - $this->triggerWebhooks($event); - $this->removePeriodStatistics($event); - if ($event->runRecalculations) { - $this->updateRunningBalance($event); - } - } - /** * TODO duplicate */ @@ -67,8 +52,8 @@ class UpdatedGroupEventHandler /** @var TransactionJournal $journal */ foreach ($event->transactionGroup->transactionJournals as $journal) { - $source = $journal->transactions()->where('amount', '<', '0')->first(); - $dest = $journal->transactions()->where('amount', '>', '0')->first(); + $source = $journal->transactions()->where('amount', '<', '0')->first(); + $dest = $journal->transactions()->where('amount', '>', '0')->first(); if (null !== $source) { $repository->deleteStatisticsForModel($source->account, $journal->date); } @@ -98,128 +83,6 @@ class UpdatedGroupEventHandler } } - /** - * This method will make sure all source / destination accounts are the same. - */ - public function unifyAccounts(UpdatedTransactionGroup $updatedGroupEvent): void - { - $group = $updatedGroupEvent->transactionGroup; - if (1 === $group->transactionJournals->count()) { - return; - } - // first journal: - /** @var null|TransactionJournal $first */ - $first = $group - ->transactionJournals() - ->orderBy('transaction_journals.date', 'DESC') - ->orderBy('transaction_journals.order', 'ASC') - ->orderBy('transaction_journals.id', 'DESC') - ->orderBy('transaction_journals.description', 'DESC') - ->first() - ; - if (null === $first) { - Log::warning(sprintf('Group #%d has no transaction journals.', $group->id)); - - return; - } - - $all = $group->transactionJournals()->get()->pluck('id')->toArray(); - - /** @var Account $sourceAccount */ - $sourceAccount = $first->transactions()->where('amount', '<', '0')->first()->account; - - /** @var Account $destAccount */ - $destAccount = $first->transactions()->where('amount', '>', '0')->first()->account; - - $type = $first->transactionType->type; - if (TransactionTypeEnum::TRANSFER->value === $type || TransactionTypeEnum::WITHDRAWAL->value === $type) { - // set all source transactions to source account: - Transaction::whereIn('transaction_journal_id', $all)->where('amount', '<', 0)->update(['account_id' => $sourceAccount->id]); - } - if (TransactionTypeEnum::TRANSFER->value === $type || TransactionTypeEnum::DEPOSIT->value === $type) { - // set all destination transactions to destination account: - Transaction::whereIn('transaction_journal_id', $all)->where('amount', '>', 0)->update(['account_id' => $destAccount->id]); - } - } - - /** - * This method will check all the rules when a journal is updated. - */ - private function processRules(UpdatedTransactionGroup $updatedGroupEvent): void - { - if (false === $updatedGroupEvent->applyRules) { - Log::info(sprintf('Will not run rules on group #%d', $updatedGroupEvent->transactionGroup->id)); - - return; - } - - $journals = $updatedGroupEvent->transactionGroup->transactionJournals; - $array = []; - - /** @var TransactionJournal $journal */ - foreach ($journals as $journal) { - $array[] = $journal->id; - } - $journalIds = implode(',', $array); - Log::debug(sprintf('Add local operator for journal(s): %s', $journalIds)); - - // collect rules: - $ruleGroupRepository = app(RuleGroupRepositoryInterface::class); - $ruleGroupRepository->setUser($updatedGroupEvent->transactionGroup->user); - - $groups = $ruleGroupRepository->getRuleGroupsWithRules('update-journal'); - - // file rule engine. - $newRuleEngine = app(RuleEngineInterface::class); - $newRuleEngine->setUser($updatedGroupEvent->transactionGroup->user); - $newRuleEngine->addOperator(['type' => 'journal_id', 'value' => $journalIds]); - $newRuleEngine->setRuleGroups($groups); - $newRuleEngine->fire(); - } - - private function recalculateCredit(UpdatedTransactionGroup $event): void - { - $group = $event->transactionGroup; - - /** @var CreditRecalculateService $object */ - $object = app(CreditRecalculateService::class); - $object->setGroup($group); - $object->recalculate(); - } - - private function triggerWebhooks(UpdatedTransactionGroup $updatedGroupEvent): void - { - Log::debug(__METHOD__); - $group = $updatedGroupEvent->transactionGroup; - if (false === $updatedGroupEvent->fireWebhooks) { - Log::info(sprintf('Will not fire webhooks for transaction group #%d', $group->id)); - - return; - } - $user = $group->user; - - /** @var MessageGeneratorInterface $engine */ - $engine = app(MessageGeneratorInterface::class); - $engine->setUser($user); - $engine->setObjects(new Collection()->push($group)); - $engine->setTrigger(WebhookTrigger::UPDATE_TRANSACTION); - $engine->generateMessages(); - - Log::debug(sprintf('send event WebhookMessagesRequestSending from %s', __METHOD__)); - event(new WebhookMessagesRequestSending()); - } - - private function updateRunningBalance(UpdatedTransactionGroup $event): void - { - if (false === FireflyConfig::get('use_running_balance', config('firefly.feature_flags.running_balance_column'))->data) { - return; - } - Log::debug(__METHOD__); - $group = $event->transactionGroup; - foreach ($group->transactionJournals as $journal) { - AccountBalanceCalculator::recalculateForJournal($journal); - } - } } diff --git a/app/Http/Controllers/Transaction/BulkController.php b/app/Http/Controllers/Transaction/BulkController.php index 3ca968c110..5c440b7e1e 100644 --- a/app/Http/Controllers/Transaction/BulkController.php +++ b/app/Http/Controllers/Transaction/BulkController.php @@ -24,7 +24,8 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Transaction; -use FireflyIII\Events\UpdatedTransactionGroup; +use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags; +use FireflyIII\Events\Model\TransactionGroup\UpdatedSingleTransactionGroup; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Requests\BulkEditJournalRequest; use FireflyIII\Models\TransactionJournal; @@ -56,7 +57,7 @@ class BulkController extends Controller $this->middleware(function ($request, $next) { $this->repository = app(JournalRepositoryInterface::class); - app('view')->share('title', (string) trans('firefly.transactions')); + app('view')->share('title', (string)trans('firefly.transactions')); app('view')->share('mainTitleIcon', 'fa-exchange'); return $next($request); @@ -70,9 +71,9 @@ class BulkController extends Controller * * @return Factory|View */ - public function edit(array $journals): Factory|\Illuminate\Contracts\View\View + public function edit(array $journals): Factory | \Illuminate\Contracts\View\View { - $subTitle = (string) trans('firefly.mass_bulk_journals'); + $subTitle = (string)trans('firefly.mass_bulk_journals'); $this->rememberPreviousUrl('transactions.bulk-edit.url'); @@ -83,7 +84,7 @@ class BulkController extends Controller $budgetRepos = app(BudgetRepositoryInterface::class); $budgetList = app('expandedform')->makeSelectListWithEmpty($budgetRepos->getActiveBudgets()); - return view('transactions.bulk.edit', ['journals' => $journals, 'subTitle' => $subTitle, 'budgetList' => $budgetList]); + return view('transactions.bulk.edit', ['journals' => $journals, 'subTitle' => $subTitle, 'budgetList' => $budgetList]); } /** @@ -91,18 +92,18 @@ class BulkController extends Controller * * @return Application|Redirector|RedirectResponse */ - public function update(BulkEditJournalRequest $request): Redirector|RedirectResponse + public function update(BulkEditJournalRequest $request): Redirector | RedirectResponse { $journalIds = $request->get('journals'); $journalIds = is_array($journalIds) ? $journalIds : []; - $ignoreCategory = 1 === (int) $request->get('ignore_category'); - $ignoreBudget = 1 === (int) $request->get('ignore_budget'); + $ignoreCategory = 1 === (int)$request->get('ignore_category'); + $ignoreBudget = 1 === (int)$request->get('ignore_budget'); $tagsAction = $request->get('tags_action'); $collection = new Collection(); $count = 0; foreach ($journalIds as $journalId) { - $journalId = (int) $journalId; + $journalId = (int)$journalId; $journal = $this->repository->find($journalId); if (null !== $journal) { $resultA = $this->updateJournalBudget($journal, $ignoreBudget, $request->integer('budget_id')); @@ -115,10 +116,11 @@ class BulkController extends Controller } } + $flags = new TransactionGroupEventFlags(); // run rules on changed journals: /** @var TransactionJournal $journal */ - foreach ($collection as $journal) { // @phpstan-ignore-line - event(new UpdatedTransactionGroup($journal->transactionGroup, true, true, false)); + foreach ($collection as $journal) { + event(new UpdatedSingleTransactionGroup($journal->transactionGroup, $flags)); } Preferences::mark(); diff --git a/app/Http/Controllers/Transaction/ConvertController.php b/app/Http/Controllers/Transaction/ConvertController.php index 9fc607961e..7d2ca297cb 100644 --- a/app/Http/Controllers/Transaction/ConvertController.php +++ b/app/Http/Controllers/Transaction/ConvertController.php @@ -26,7 +26,8 @@ namespace FireflyIII\Http\Controllers\Transaction; use Exception; use FireflyIII\Enums\AccountTypeEnum; use FireflyIII\Enums\TransactionTypeEnum; -use FireflyIII\Events\UpdatedTransactionGroup; +use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags; +use FireflyIII\Events\Model\TransactionGroup\UpdatedSingleTransactionGroup; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\Account; @@ -71,7 +72,7 @@ class ConvertController extends Controller // some useful repositories: $this->middleware(function ($request, $next) { $this->accountRepository = app(AccountRepositoryInterface::class); - app('view')->share('title', (string) trans('firefly.transactions')); + app('view')->share('title', (string)trans('firefly.transactions')); app('view')->share('mainTitleIcon', 'fa-exchange'); return $next($request); @@ -85,23 +86,23 @@ class ConvertController extends Controller * * @throws Exception */ - public function index(TransactionType $destinationType, TransactionGroup $group): Factory|\Illuminate\Contracts\View\View|Redirector|RedirectResponse + public function index(TransactionType $destinationType, TransactionGroup $group): Factory | \Illuminate\Contracts\View\View | Redirector | RedirectResponse { if (!$this->isEditableGroup($group)) { return $this->redirectGroupToAccount($group); } /** @var TransactionGroupTransformer $transformer */ - $transformer = app(TransactionGroupTransformer::class); + $transformer = app(TransactionGroupTransformer::class); /** @var TransactionJournal $first */ - $first = $group->transactionJournals()->first(); - $sourceType = $first->transactionType; + $first = $group->transactionJournals()->first(); + $sourceType = $first->transactionType; - $groupTitle = $group->title ?? $first->description; - $groupArray = $transformer->transformObject($group); - $subTitle = (string) trans('firefly.convert_to_'.$destinationType->type, ['description' => $groupTitle]); - $subTitleIcon = 'fa-exchange'; + $groupTitle = $group->title ?? $first->description; + $groupArray = $transformer->transformObject($group); + $subTitle = (string)trans('firefly.convert_to_' . $destinationType->type, ['description' => $groupTitle]); + $subTitleIcon = 'fa-exchange'; // get a list of asset accounts and liabilities and stuff, in various combinations: $validDepositSources = $this->getValidDepositSources(); @@ -110,11 +111,11 @@ class ConvertController extends Controller $assets = $this->getAssetAccounts(); // old input variables: - $preFilled = ['source_name' => old('source_name')]; + $preFilled = ['source_name' => old('source_name')]; if ($sourceType->type === $destinationType->type) { // cannot convert to its own type. Log::debug('This is already a transaction of the expected type..'); - session()->flash('info', (string) trans('firefly.convert_is_already_type_'.$destinationType->type)); + session()->flash('info', (string)trans('firefly.convert_is_already_type_' . $destinationType->type)); return redirect(route('transactions.show', [$group->id])); } @@ -140,26 +141,26 @@ class ConvertController extends Controller // make repositories $liabilityTypes = [AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::CREDITCARD->value, AccountTypeEnum::LOAN->value]; $accountList = $this->accountRepository->getActiveAccountsByType([ - AccountTypeEnum::REVENUE->value, - AccountTypeEnum::CASH->value, - AccountTypeEnum::LOAN->value, - AccountTypeEnum::DEBT->value, - AccountTypeEnum::MORTGAGE->value, - ]); + AccountTypeEnum::REVENUE->value, + AccountTypeEnum::CASH->value, + AccountTypeEnum::LOAN->value, + AccountTypeEnum::DEBT->value, + AccountTypeEnum::MORTGAGE->value, + ]); $grouped = []; // group accounts: /** @var Account $account */ foreach ($accountList as $account) { - $role = (string) $this->accountRepository->getMetaValue($account, 'account_role'); - $name = $account->name; + $role = (string)$this->accountRepository->getMetaValue($account, 'account_role'); + $name = $account->name; if ('' === $role) { $role = 'no_account_type'; } // maybe it's a liability thing: if (in_array($account->accountType->type, $liabilityTypes, true)) { - $role = 'l_'.$account->accountType->type; + $role = 'l_' . $account->accountType->type; } if (AccountTypeEnum::CASH->value === $account->accountType->type) { $role = 'cash_account'; @@ -169,7 +170,7 @@ class ConvertController extends Controller $role = 'revenue_account'; } - $key = (string) trans('firefly.opt_group_'.$role); + $key = (string)trans('firefly.opt_group_' . $role); $grouped[$key][$account->id] = $name; } @@ -181,26 +182,26 @@ class ConvertController extends Controller // make repositories $liabilityTypes = [AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::CREDITCARD->value, AccountTypeEnum::LOAN->value]; $accountList = $this->accountRepository->getActiveAccountsByType([ - AccountTypeEnum::EXPENSE->value, - AccountTypeEnum::CASH->value, - AccountTypeEnum::LOAN->value, - AccountTypeEnum::DEBT->value, - AccountTypeEnum::MORTGAGE->value, - ]); + AccountTypeEnum::EXPENSE->value, + AccountTypeEnum::CASH->value, + AccountTypeEnum::LOAN->value, + AccountTypeEnum::DEBT->value, + AccountTypeEnum::MORTGAGE->value, + ]); $grouped = []; // group accounts: /** @var Account $account */ foreach ($accountList as $account) { - $role = (string) $this->accountRepository->getMetaValue($account, 'account_role'); - $name = $account->name; + $role = (string)$this->accountRepository->getMetaValue($account, 'account_role'); + $name = $account->name; if ('' === $role) { $role = 'no_account_type'; } // maybe it's a liability thing: if (in_array($account->accountType->type, $liabilityTypes, true)) { - $role = 'l_'.$account->accountType->type; + $role = 'l_' . $account->accountType->type; } if (AccountTypeEnum::CASH->value === $account->accountType->type) { $role = 'cash_account'; @@ -210,7 +211,7 @@ class ConvertController extends Controller $role = 'expense_account'; } - $key = (string) trans('firefly.opt_group_'.$role); + $key = (string)trans('firefly.opt_group_' . $role); $grouped[$key][$account->id] = $name; } @@ -224,23 +225,23 @@ class ConvertController extends Controller { // make repositories $accountList = $this->accountRepository->getActiveAccountsByType([ - AccountTypeEnum::LOAN->value, - AccountTypeEnum::DEBT->value, - AccountTypeEnum::MORTGAGE->value, - ]); + AccountTypeEnum::LOAN->value, + AccountTypeEnum::DEBT->value, + AccountTypeEnum::MORTGAGE->value, + ]); $grouped = []; // group accounts: /** @var Account $account */ foreach ($accountList as $account) { - $date = today()->endOfDay(); + $date = today()->endOfDay(); Log::debug(sprintf('getLiabilities: Call finalAccountBalance with date/time "%s"', $date->toIso8601String())); // 2025-10-08 replace finalAccountBalance with accountsBalancesOptimized. // $balance = Steam::finalAccountBalance($account, $date)['balance']; $balance = Steam::accountsBalancesOptimized(new Collection()->push($account), $date)[$account->id]['balance'] ?? '0'; $currency = $this->accountRepository->getAccountCurrency($account) ?? $this->primaryCurrency; $role = sprintf('l_%s', $account->accountType->type); - $key = (string) trans(sprintf('firefly.opt_group_%s', $role)); + $key = (string)trans(sprintf('firefly.opt_group_%s', $role)); $grouped[$key][$account->id] = sprintf('%s (%s)', $account->name, Amount::formatAnything($currency, $balance, false)); } @@ -259,19 +260,19 @@ class ConvertController extends Controller // group accounts: /** @var Account $account */ foreach ($accountList as $account) { - $date = today()->endOfDay(); + $date = today()->endOfDay(); Log::debug(sprintf('getAssetAccounts: Call finalAccountBalance with date/time "%s"', $date->toIso8601String())); // 2025-10-08 replace finalAccountBalance with accountsBalancesOptimized. // $balance = Steam::finalAccountBalance($account, $date)['balance']; - $balance = Steam::accountsBalancesOptimized(new Collection()->push($account), $date)[$account->id]['balance'] ?? '0'; + $balance = Steam::accountsBalancesOptimized(new Collection()->push($account), $date)[$account->id]['balance'] ?? '0'; - $currency = $this->accountRepository->getAccountCurrency($account) ?? $this->primaryCurrency; - $role = (string) $this->accountRepository->getMetaValue($account, 'account_role'); + $currency = $this->accountRepository->getAccountCurrency($account) ?? $this->primaryCurrency; + $role = (string)$this->accountRepository->getMetaValue($account, 'account_role'); if ('' === $role) { $role = 'no_account_type'; } - $key = (string) trans(sprintf('firefly.opt_group_%s', $role)); + $key = (string)trans(sprintf('firefly.opt_group_%s', $role)); $grouped[$key][$account->id] = sprintf('%s (%s)', $account->name, Amount::formatAnything($currency, $balance, false)); } @@ -304,8 +305,9 @@ class ConvertController extends Controller // correct transfers: $group->refresh(); - session()->flash('success', (string) trans('firefly.converted_to_'.$destinationType->type)); - event(new UpdatedTransactionGroup($group, true, true, true)); + session()->flash('success', (string)trans('firefly.converted_to_' . $destinationType->type)); + $flags = new TransactionGroupEventFlags(); + event(new UpdatedSingleTransactionGroup($group, $flags)); return redirect(route('transactions.show', [$group->id])); } @@ -316,22 +318,22 @@ class ConvertController extends Controller private function convertJournal(TransactionJournal $journal, TransactionType $transactionType, array $data): TransactionJournal { /** @var AccountValidator $validator */ - $validator = app(AccountValidator::class); + $validator = app(AccountValidator::class); $validator->setUser(auth()->user()); $validator->setTransactionType($transactionType->type); - $sourceId = $data['source_id'][$journal->id] ?? null; - $sourceName = $data['source_name'][$journal->id] ?? null; - $destinationId = $data['destination_id'][$journal->id] ?? null; - $destinationName = $data['destination_name'][$journal->id] ?? null; + $sourceId = $data['source_id'][$journal->id] ?? null; + $sourceName = $data['source_name'][$journal->id] ?? null; + $destinationId = $data['destination_id'][$journal->id] ?? null; + $destinationName = $data['destination_name'][$journal->id] ?? null; // double check it's not an empty string. - $sourceId = '' === $sourceId || null === $sourceId ? null : (int) $sourceId; - $sourceName = '' === $sourceName ? null : (string) $sourceName; - $destinationId = '' === $destinationId || null === $destinationId ? null : (int) $destinationId; - $destinationName = '' === $destinationName ? null : (string) $destinationName; - $validSource = $validator->validateSource(['id' => $sourceId, 'name' => $sourceName]); - $validDestination = $validator->validateDestination(['id' => $destinationId, 'name' => $destinationName]); + $sourceId = '' === $sourceId || null === $sourceId ? null : (int)$sourceId; + $sourceName = '' === $sourceName ? null : (string)$sourceName; + $destinationId = '' === $destinationId || null === $destinationId ? null : (int)$destinationId; + $destinationName = '' === $destinationName ? null : (string)$destinationName; + $validSource = $validator->validateSource(['id' => $sourceId, 'name' => $sourceName]); + $validDestination = $validator->validateDestination(['id' => $destinationId, 'name' => $destinationName]); if (false === $validSource) { throw new FireflyException(sprintf(trans('firefly.convert_invalid_source'), $journal->id)); @@ -342,7 +344,7 @@ class ConvertController extends Controller // TODO typeOverrule: the account validator may have another opinion on the transaction type. - $update = [ + $update = [ 'source_id' => $sourceId, 'source_name' => $sourceName, 'destination_id' => $destinationId, @@ -356,9 +358,9 @@ class ConvertController extends Controller // also set the currency to the currency of the source account, in case you're converting a deposit into a transfer. if (TransactionTypeEnum::TRANSFER->value === $transactionType->type && TransactionTypeEnum::DEPOSIT->value === $journal->transactionType->type) { - $source = $this->accountRepository->find((int) $sourceId); + $source = $this->accountRepository->find((int)$sourceId); $sourceCurrency = $this->accountRepository->getAccountCurrency($source); - $dest = $this->accountRepository->find((int) $destinationId); + $dest = $this->accountRepository->find((int)$destinationId); $destCurrency = $this->accountRepository->getAccountCurrency($dest); if ( $sourceCurrency instanceof TransactionCurrency @@ -373,9 +375,9 @@ class ConvertController extends Controller // same thing for converting a withdrawal into a transfer, but with the currency of the destination account. if (TransactionTypeEnum::TRANSFER->value === $transactionType->type && TransactionTypeEnum::WITHDRAWAL->value === $journal->transactionType->type) { - $source = $this->accountRepository->find((int) $sourceId); + $source = $this->accountRepository->find((int)$sourceId); $sourceCurrency = $this->accountRepository->getAccountCurrency($source); - $dest = $this->accountRepository->find((int) $destinationId); + $dest = $this->accountRepository->find((int)$destinationId); $destCurrency = $this->accountRepository->getAccountCurrency($dest); if ( $sourceCurrency instanceof TransactionCurrency @@ -389,7 +391,7 @@ class ConvertController extends Controller } /** @var JournalUpdateService $service */ - $service = app(JournalUpdateService::class); + $service = app(JournalUpdateService::class); $service->setTransactionJournal($journal); $service->setData($update); $service->update(); diff --git a/app/Http/Controllers/Transaction/MassController.php b/app/Http/Controllers/Transaction/MassController.php index 4e25ba027d..67bed09edf 100644 --- a/app/Http/Controllers/Transaction/MassController.php +++ b/app/Http/Controllers/Transaction/MassController.php @@ -26,7 +26,8 @@ namespace FireflyIII\Http\Controllers\Transaction; use Carbon\Carbon; use FireflyIII\Enums\AccountTypeEnum; use FireflyIII\Enums\TransactionTypeEnum; -use FireflyIII\Events\UpdatedTransactionGroup; +use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags; +use FireflyIII\Events\Model\TransactionGroup\UpdatedSingleTransactionGroup; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Requests\MassDeleteJournalRequest; @@ -60,7 +61,7 @@ class MassController extends Controller parent::__construct(); $this->middleware(function ($request, $next) { - app('view')->share('title', (string) trans('firefly.transactions')); + app('view')->share('title', (string)trans('firefly.transactions')); app('view')->share('mainTitleIcon', 'fa-exchange'); $this->repository = app(JournalRepositoryInterface::class); @@ -73,7 +74,7 @@ class MassController extends Controller */ public function delete(array $journals): IlluminateView { - $subTitle = (string) trans('firefly.mass_delete_journals'); + $subTitle = (string)trans('firefly.mass_delete_journals'); // put previous url in session $this->rememberPreviousUrl('transactions.mass-delete.url'); @@ -86,7 +87,7 @@ class MassController extends Controller * * @return Application|Redirector|RedirectResponse */ - public function destroy(MassDeleteJournalRequest $request): Redirector|RedirectResponse + public function destroy(MassDeleteJournalRequest $request): Redirector | RedirectResponse { Log::debug(sprintf('Now in %s', __METHOD__)); $ids = $request->get('confirm_mass_delete'); @@ -99,8 +100,8 @@ class MassController extends Controller Log::debug(sprintf('Searching for ID #%d', $journalId)); /** @var null|TransactionJournal $journal */ - $journal = $this->repository->find((int) $journalId); - if (null !== $journal && (int) $journalId === $journal->id) { + $journal = $this->repository->find((int)$journalId); + if (null !== $journal && (int)$journalId === $journal->id) { $this->repository->destroyJournal($journal); ++$count; Log::debug(sprintf('Deleted transaction journal #%d', $journalId)); @@ -122,22 +123,22 @@ class MassController extends Controller */ public function edit(array $journals): IlluminateView { - $subTitle = (string) trans('firefly.mass_edit_journals'); + $subTitle = (string)trans('firefly.mass_edit_journals'); /** @var AccountRepositoryInterface $accountRepository */ - $accountRepository = app(AccountRepositoryInterface::class); + $accountRepository = app(AccountRepositoryInterface::class); // valid withdrawal sources: - $array = array_keys(config(sprintf('firefly.source_dests.%s', TransactionTypeEnum::WITHDRAWAL->value))); - $withdrawalSources = $accountRepository->getAccountsByType($array); + $array = array_keys(config(sprintf('firefly.source_dests.%s', TransactionTypeEnum::WITHDRAWAL->value))); + $withdrawalSources = $accountRepository->getAccountsByType($array); // valid deposit destinations: $array = config(sprintf('firefly.source_dests.%s.%s', TransactionTypeEnum::DEPOSIT->value, AccountTypeEnum::REVENUE->value)); $depositDestinations = $accountRepository->getAccountsByType($array); /** @var BudgetRepositoryInterface $budgetRepository */ - $budgetRepository = app(BudgetRepositoryInterface::class); - $budgets = $budgetRepository->getBudgets(); + $budgetRepository = app(BudgetRepositoryInterface::class); + $budgets = $budgetRepository->getBudgets(); // reverse amounts foreach ($journals as $index => $journal) { @@ -161,18 +162,18 @@ class MassController extends Controller * * @throws FireflyException */ - public function update(MassEditJournalRequest $request): Redirector|RedirectResponse + public function update(MassEditJournalRequest $request): Redirector | RedirectResponse { $journalIds = $request->get('journals'); if (!is_array($journalIds)) { // TODO this is a weird error, should be caught. throw new FireflyException('This is not an array.'); } - $count = 0; + $count = 0; /** @var string $journalId */ foreach ($journalIds as $journalId) { - $integer = (int) $journalId; + $integer = (int)$journalId; try { $this->updateJournal($integer, $request); @@ -194,15 +195,15 @@ class MassController extends Controller */ private function updateJournal(int $journalId, MassEditJournalRequest $request): void { - $journal = $this->repository->find($journalId); + $journal = $this->repository->find($journalId); if (!$journal instanceof TransactionJournal) { throw new FireflyException(sprintf('Trying to edit non-existent or deleted journal #%d', $journalId)); } - $service = app(JournalUpdateService::class); + $service = app(JournalUpdateService::class); // for each field, call the update service. $service->setTransactionJournal($journal); - $data = [ + $data = [ 'date' => $this->getDateFromRequest($request, $journal->id, 'date'), 'description' => $this->getStringFromRequest($request, $journal->id, 'description'), 'source_id' => $this->getIntFromRequest($request, $journal->id, 'source_id'), @@ -220,8 +221,10 @@ class MassController extends Controller $service->setData($data); $service->update(); // trigger rules - $runRecalculations = $service->isCompareHashChanged(); - event(new UpdatedTransactionGroup($journal->transactionGroup, true, true, $runRecalculations)); + $runRecalculations = $service->isCompareHashChanged(); + $flags = new TransactionGroupEventFlags(); + $flags->recalculateCredit = $runRecalculations; + event(new UpdatedSingleTransactionGroup($journal->transactionGroup, $flags)); } private function getDateFromRequest(MassEditJournalRequest $request, int $journalId, string $key): ?Carbon @@ -256,7 +259,7 @@ class MassController extends Controller return null; } - return (string) $value[$journalId]; + return (string)$value[$journalId]; } private function getIntFromRequest(MassEditJournalRequest $request, int $journalId, string $string): ?int @@ -269,6 +272,6 @@ class MassController extends Controller return null; } - return (int) $value[$journalId]; + return (int)$value[$journalId]; } } diff --git a/app/Listeners/Model/TransactionGroup/ProcessesNewTransactionGroup.php b/app/Listeners/Model/TransactionGroup/ProcessesNewTransactionGroup.php index 4956227457..c8b3a095d2 100644 --- a/app/Listeners/Model/TransactionGroup/ProcessesNewTransactionGroup.php +++ b/app/Listeners/Model/TransactionGroup/ProcessesNewTransactionGroup.php @@ -141,7 +141,7 @@ class ProcessesNewTransactionGroup implements ShouldQueue AccountBalanceCalculator::optimizedCalculation($accounts, $earliest); } - private function removePeriodStatistics(Collection $set): void + public static function removePeriodStatistics(Collection $set): void { if (auth()->check()) { Log::debug('Always remove period statistics'); diff --git a/app/Listeners/Model/TransactionGroup/ProcessesUpdatedTransactionGroup.php b/app/Listeners/Model/TransactionGroup/ProcessesUpdatedTransactionGroup.php new file mode 100644 index 0000000000..541975cf62 --- /dev/null +++ b/app/Listeners/Model/TransactionGroup/ProcessesUpdatedTransactionGroup.php @@ -0,0 +1,195 @@ +. + */ + +namespace FireflyIII\Listeners\Model\TransactionGroup; + +use FireflyIII\Enums\TransactionTypeEnum; +use FireflyIII\Enums\WebhookTrigger; +use FireflyIII\Events\Model\TransactionGroup\UpdatedSingleTransactionGroup; +use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending; +use FireflyIII\Events\UpdatedTransactionGroup; +use FireflyIII\Generator\Webhook\MessageGeneratorInterface; +use FireflyIII\Models\Account; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface; +use FireflyIII\Services\Internal\Support\CreditRecalculateService; +use FireflyIII\Support\Facades\FireflyConfig; +use FireflyIII\Support\Models\AccountBalanceCalculator; +use FireflyIII\TransactionRules\Engine\RuleEngineInterface; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; + +class ProcessesUpdatedTransactionGroup +{ + public function handle(UpdatedSingleTransactionGroup $event): void + { + Log::debug('Now in handle() for UpdatedSingleTransactionGroup'); + $this->unifyAccounts($event); + $this->processRules($event); + $this->recalculateCredit($event); + $this->triggerWebhooks($event); + $this->removePeriodStatistics($event); + if ($event->flags->recalculateCredit) { + $this->updateRunningBalance($event); + } + + Log::debug('Done with handle() for UpdatedSingleTransactionGroup'); + } + + + /** + * This method will make sure all source / destination accounts are the same. + */ + public function unifyAccounts(UpdatedSingleTransactionGroup $updatedGroupEvent): void + { + Log::debug('Now in unifyAccounts()'); + $group = $updatedGroupEvent->transactionGroup; + if (1 === $group->transactionJournals->count()) { + Log::debug('Nothing to do in unifyAccounts()'); + return; + } + + // first journal: + /** @var null|TransactionJournal $first */ + $first = $group + ->transactionJournals() + ->orderBy('transaction_journals.date', 'DESC') + ->orderBy('transaction_journals.order', 'ASC') + ->orderBy('transaction_journals.id', 'DESC') + ->orderBy('transaction_journals.description', 'DESC') + ->first(); + + if (null === $first) { + Log::warning(sprintf('Group #%d has no transaction journals.', $group->id)); + + return; + } + + $all = $group->transactionJournals()->get()->pluck('id')->toArray(); + + /** @var Account $sourceAccount */ + $sourceAccount = $first->transactions()->where('amount', '<', '0')->first()->account; + + /** @var Account $destAccount */ + $destAccount = $first->transactions()->where('amount', '>', '0')->first()->account; + + $type = $first->transactionType->type; + if (TransactionTypeEnum::TRANSFER->value === $type || TransactionTypeEnum::WITHDRAWAL->value === $type) { + // set all source transactions to source account: + Transaction::whereIn('transaction_journal_id', $all)->where('amount', '<', 0)->update(['account_id' => $sourceAccount->id]); + } + if (TransactionTypeEnum::TRANSFER->value === $type || TransactionTypeEnum::DEPOSIT->value === $type) { + // set all destination transactions to destination account: + Transaction::whereIn('transaction_journal_id', $all)->where('amount', '>', 0)->update(['account_id' => $destAccount->id]); + } + Log::debug('Done with unifyAccounts()'); + } + + + /** + * This method will check all the rules when a journal is updated. + */ + private function processRules(UpdatedSingleTransactionGroup $updatedGroupEvent): void + { + Log::debug('Now in processRules()'); + if (false === $updatedGroupEvent->flags->applyRules) { + Log::info(sprintf('Will not run rules on group #%d', $updatedGroupEvent->transactionGroup->id)); + + return; + } + + $journals = $updatedGroupEvent->transactionGroup->transactionJournals; + $array = []; + + /** @var TransactionJournal $journal */ + foreach ($journals as $journal) { + $array[] = $journal->id; + } + $journalIds = implode(',', $array); + Log::debug(sprintf('Add local operator for journal(s): %s', $journalIds)); + + // collect rules: + $ruleGroupRepository = app(RuleGroupRepositoryInterface::class); + $ruleGroupRepository->setUser($updatedGroupEvent->transactionGroup->user); + + $groups = $ruleGroupRepository->getRuleGroupsWithRules('update-journal'); + + // file rule engine. + $newRuleEngine = app(RuleEngineInterface::class); + $newRuleEngine->setUser($updatedGroupEvent->transactionGroup->user); + $newRuleEngine->addOperator(['type' => 'journal_id', 'value' => $journalIds]); + $newRuleEngine->setRuleGroups($groups); + $newRuleEngine->fire(); + Log::debug('Done with processRules()'); + } + + + private function recalculateCredit(UpdatedSingleTransactionGroup $event): void + { + Log::debug('Now in recalculateCredit()'); + $group = $event->transactionGroup; + + /** @var CreditRecalculateService $object */ + $object = app(CreditRecalculateService::class); + $object->setGroup($group); + $object->recalculate(); + Log::debug('Done with recalculateCredit()'); + } + + + private function triggerWebhooks(UpdatedTransactionGroup $updatedGroupEvent): void + { + Log::debug('Now in triggerWebhooks()'); + $group = $updatedGroupEvent->transactionGroup; + if (false === $updatedGroupEvent->fireWebhooks) { + Log::info(sprintf('Will not fire webhooks for transaction group #%d', $group->id)); + + return; + } + $user = $group->user; + + /** @var MessageGeneratorInterface $engine */ + $engine = app(MessageGeneratorInterface::class); + $engine->setUser($user); + $engine->setObjects(new Collection()->push($group)); + $engine->setTrigger(WebhookTrigger::UPDATE_TRANSACTION); + $engine->generateMessages(); + + Log::debug(sprintf('send event WebhookMessagesRequestSending from %s', __METHOD__)); + event(new WebhookMessagesRequestSending()); + Log::debug('End of triggerWebhooks()'); + } + + private function updateRunningBalance(UpdatedTransactionGroup $event): void + { + Log::debug('Now in updateRunningBalance()'); + if (false === FireflyConfig::get('use_running_balance', config('firefly.feature_flags.running_balance_column'))->data) { + return; + } + Log::debug(__METHOD__); + $group = $event->transactionGroup; + foreach ($group->transactionJournals as $journal) { + AccountBalanceCalculator::recalculateForJournal($journal); + } + Log::debug('Done with updateRunningBalance()'); + } +}