From b52a1f3eb19fcf8f0e29aa08a27aa82db2183d59 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 21 Dec 2024 07:12:11 +0100 Subject: [PATCH] Respond to currency changes. --- .../Upgrade/UpgradeMultiPiggyBanks.php | 4 +- .../UserGroupChangedDefaultCurrency.php | 42 ++++++ .../Events/PreferencesEventHandler.php | 129 ++++++++++++++++++ app/Handlers/Observer/PiggyBankObserver.php | 5 +- .../TransactionCurrency/IndexController.php | 6 +- app/Models/PiggyBank.php | 2 +- app/Models/UserGroup.php | 3 +- app/Providers/EventServiceProvider.php | 5 + .../UserGroups/Budget/BudgetRepository.php | 8 ++ .../Budget/BudgetRepositoryInterface.php | 1 + .../Currency/CurrencyRepository.php | 47 ++++--- .../PiggyBank/PiggyBankRepository.php | 20 +-- .../Http/Api/ExchangeRateConverter.php | 2 +- .../Models/AccountBalanceCalculator.php | 2 +- ..._12_19_061003_add_native_amount_column.php | 3 +- 15 files changed, 234 insertions(+), 45 deletions(-) create mode 100644 app/Events/Preferences/UserGroupChangedDefaultCurrency.php create mode 100644 app/Handlers/Events/PreferencesEventHandler.php diff --git a/app/Console/Commands/Upgrade/UpgradeMultiPiggyBanks.php b/app/Console/Commands/Upgrade/UpgradeMultiPiggyBanks.php index af1d492a82..d48e7441b5 100644 --- a/app/Console/Commands/Upgrade/UpgradeMultiPiggyBanks.php +++ b/app/Console/Commands/Upgrade/UpgradeMultiPiggyBanks.php @@ -98,12 +98,12 @@ class UpgradeMultiPiggyBanks extends Command // update piggy bank to have a currency. $piggyBank->transaction_currency_id = $currency->id; - $piggyBank->save(); + $piggyBank->saveQuietly(); // store current amount in account association. $piggyBank->accounts()->sync([$piggyBank->account->id => ['current_amount' => $repetition->current_amount]]); $piggyBank->account_id = null; - $piggyBank->save(); + $piggyBank->saveQuietly(); // remove all repetitions (no longer used) $piggyBank->piggyBankRepetitions()->delete(); diff --git a/app/Events/Preferences/UserGroupChangedDefaultCurrency.php b/app/Events/Preferences/UserGroupChangedDefaultCurrency.php new file mode 100644 index 0000000000..cafb5016f6 --- /dev/null +++ b/app/Events/Preferences/UserGroupChangedDefaultCurrency.php @@ -0,0 +1,42 @@ +userGroup = $userGroup; + } +} diff --git a/app/Handlers/Events/PreferencesEventHandler.php b/app/Handlers/Events/PreferencesEventHandler.php new file mode 100644 index 0000000000..00f5e3ff7c --- /dev/null +++ b/app/Handlers/Events/PreferencesEventHandler.php @@ -0,0 +1,129 @@ + ['native_virtual_balance'], + 'available_budgets' => ['native_amount'], + 'bills' => ['native_amount_min', 'native_amount_max'], + //'transactions' => ['native_amount', 'native_foreign_amount'] + ]; + foreach ($tables as $table => $columns) { + foreach ($columns as $column) { + Log::debug(sprintf('Resetting column %s in table %s.', $column, $table)); + DB::table($table)->where('user_group_id', $event->userGroup->id)->update([$column => null]); + } + } + $this->resetPiggyBanks($event->userGroup); + $this->resetBudgets($event->userGroup); + $this->resetTransactions($event->userGroup); + } + + private function resetPiggyBanks(UserGroup $userGroup): void + { + $repository = app(PiggyBankRepositoryInterface::class); + $repository->setUserGroup($userGroup); + $piggyBanks = $repository->getPiggyBanks(); + /** @var PiggyBank $piggyBank */ + foreach ($piggyBanks as $piggyBank) { + if (null !== $piggyBank->native_target_amount) { + Log::debug(sprintf('Resetting native_target_amount for piggy bank #%d.', $piggyBank->id)); + $piggyBank->native_target_amount = null; + $piggyBank->saveQuietly(); + } + foreach ($piggyBank->accounts as $account) { + if (null !== $account->pivot->native_current_amount) { + Log::debug(sprintf('Resetting native_current_amount for piggy bank #%d and account #%d.', $piggyBank->id, $account->id)); + $account->pivot->native_current_amount = null; + $account->pivot->save(); + } + } + foreach ($piggyBank->piggyBankEvents as $event) { + if (null !== $event->native_amount) { + Log::debug(sprintf('Resetting native_amount for piggy bank #%d and event #%d.', $piggyBank->id, $event->id)); + $event->native_amount = null; + $event->saveQuietly(); + } + } + } + } + + private function resetBudgets(UserGroup $userGroup): void + { + $repository = app(BudgetRepositoryInterface::class); + $repository->setUserGroup($userGroup); + $set = $repository->getBudgets(); + /** @var Budget $budget */ + foreach ($set as $budget) { + foreach ($budget->autoBudgets as $autoBudget) { + if (null !== $autoBudget->native_amount) { + if (null !== $autoBudget->native_amount) { + Log::debug(sprintf('Resetting native_amount for budget #%d and auto budget #%d.', $budget->id, $autoBudget->id)); + $autoBudget->native_amount = null; + $autoBudget->saveQuietly(); + } + } + } + foreach ($budget->budgetlimits as $limit) { + if (null !== $limit->native_amount) { + Log::debug(sprintf('Resetting native_amount for budget #%d and budget limit #%d.', $budget->id, $limit->id)); + $limit->native_amount = null; + $limit->saveQuietly(); + } + } + } + + } + + private function resetTransactions(UserGroup $userGroup): void + { + // custom query because of the potential size of this update. + DB::table('transactions') + ->join('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->where('transaction_journals.user_group_id', $userGroup->id) + ->where(static function (Builder $q) { + $q->whereNotNull('native_amount') + ->orWhereNotNull('native_foreign_amount'); + }) + ->update(['native_amount' => null, 'native_foreign_amount' => null]); + } +} diff --git a/app/Handlers/Observer/PiggyBankObserver.php b/app/Handlers/Observer/PiggyBankObserver.php index 8619085fdc..32c07b82c2 100644 --- a/app/Handlers/Observer/PiggyBankObserver.php +++ b/app/Handlers/Observer/PiggyBankObserver.php @@ -48,7 +48,10 @@ class PiggyBankObserver private function updateNativeAmount(PiggyBank $piggyBank): void { - $userCurrency = app('amount')->getDefaultCurrencyByUserGroup($piggyBank->accounts()->first()->user->userGroup); + $userCurrency = app('amount')->getDefaultCurrencyByUserGroup($piggyBank->accounts()->first()?->user->userGroup); + if(null === $userCurrency) { + return; + } $piggyBank->native_target_amount = null; if ($piggyBank->transactionCurrency->id !== $userCurrency->id) { $converter = new ExchangeRateConverter(); diff --git a/app/Http/Controllers/TransactionCurrency/IndexController.php b/app/Http/Controllers/TransactionCurrency/IndexController.php index 3dc9fb739f..2e4314ca9f 100644 --- a/app/Http/Controllers/TransactionCurrency/IndexController.php +++ b/app/Http/Controllers/TransactionCurrency/IndexController.php @@ -70,10 +70,8 @@ class IndexController extends Controller $page = 0 === (int)$request->get('page') ? 1 : (int)$request->get('page'); $pageSize = (int)app('preferences')->get('listPageSize', 50)->data; $collection = $this->repository->getAll(); - $total = $collection->count(); - $collection = $collection->slice(($page - 1) * $pageSize, $pageSize); - // order so default is on top: + // order so default and enabled are on top: $collection = $collection->sortBy( static function (TransactionCurrency $currency) { $default = true === $currency->userGroupDefault ? 0 : 1; @@ -82,6 +80,8 @@ class IndexController extends Controller return sprintf('%s-%s-%s', $default, $enabled, $currency->code); } ); + $total = $collection->count(); + $collection = $collection->slice(($page - 1) * $pageSize, $pageSize); $currencies = new LengthAwarePaginator($collection, $total, $pageSize, $page); $currencies->setPath(route('currencies.index')); diff --git a/app/Models/PiggyBank.php b/app/Models/PiggyBank.php index 6cd4344029..1c514d43b6 100644 --- a/app/Models/PiggyBank.php +++ b/app/Models/PiggyBank.php @@ -117,7 +117,7 @@ class PiggyBank extends Model public function accounts(): BelongsToMany { - return $this->belongsToMany(Account::class)->withPivot('current_amount'); + return $this->belongsToMany(Account::class)->withPivot(['current_amount','native_current_amount']); } public function piggyBankRepetitions(): HasMany diff --git a/app/Models/UserGroup.php b/app/Models/UserGroup.php index e161c89c4b..5019d37965 100644 --- a/app/Models/UserGroup.php +++ b/app/Models/UserGroup.php @@ -25,6 +25,7 @@ declare(strict_types=1); namespace FireflyIII\Models; use FireflyIII\Enums\UserRoleEnum; +use FireflyIII\Exceptions\FireflyException; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\User; use Illuminate\Database\Eloquent\Model; @@ -150,7 +151,7 @@ class UserGroup extends Model */ public function piggyBanks(): HasManyThrough { - return $this->hasManyThrough(PiggyBank::class, Account::class); + throw new FireflyException('This user group method is EOL.'); } public function recurrences(): HasMany diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index c6d99b083c..f6c646d424 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -34,6 +34,7 @@ use FireflyIII\Events\Model\PiggyBank\ChangedAmount; use FireflyIII\Events\Model\Rule\RuleActionFailedOnArray; use FireflyIII\Events\Model\Rule\RuleActionFailedOnObject; use FireflyIII\Events\NewVersionAvailable; +use FireflyIII\Events\Preferences\UserGroupChangedDefaultCurrency; use FireflyIII\Events\RegisteredUser; use FireflyIII\Events\RequestedNewPassword; use FireflyIII\Events\RequestedReportOnJournals; @@ -255,6 +256,10 @@ class EventServiceProvider extends ServiceProvider MFAManyFailedAttempts::class => [ 'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFAFailedAttemptsMail', ], + // preferences + UserGroupChangedDefaultCurrency::class => [ + 'FireflyIII\Handlers\Events\PreferencesEventHandler@resetNativeAmounts', + ], ]; /** diff --git a/app/Repositories/UserGroups/Budget/BudgetRepository.php b/app/Repositories/UserGroups/Budget/BudgetRepository.php index 9f95eee2ab..cb3cb69df5 100644 --- a/app/Repositories/UserGroups/Budget/BudgetRepository.php +++ b/app/Repositories/UserGroups/Budget/BudgetRepository.php @@ -42,4 +42,12 @@ class BudgetRepository implements BudgetRepositoryInterface ->get() ; } + public function getBudgets(): Collection + { + return $this->userGroup->budgets() + ->orderBy('order', 'ASC') + ->orderBy('name', 'ASC') + ->get() + ; + } } diff --git a/app/Repositories/UserGroups/Budget/BudgetRepositoryInterface.php b/app/Repositories/UserGroups/Budget/BudgetRepositoryInterface.php index 6b52c939be..70b5694f17 100644 --- a/app/Repositories/UserGroups/Budget/BudgetRepositoryInterface.php +++ b/app/Repositories/UserGroups/Budget/BudgetRepositoryInterface.php @@ -34,6 +34,7 @@ use Illuminate\Support\Collection; interface BudgetRepositoryInterface { public function getActiveBudgets(): Collection; + public function getBudgets(): Collection; public function setUser(User $user): void; diff --git a/app/Repositories/UserGroups/Currency/CurrencyRepository.php b/app/Repositories/UserGroups/Currency/CurrencyRepository.php index e12a5c7b4c..28ec848f15 100644 --- a/app/Repositories/UserGroups/Currency/CurrencyRepository.php +++ b/app/Repositories/UserGroups/Currency/CurrencyRepository.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Repositories\UserGroups\Currency; +use FireflyIII\Events\Preferences\UserGroupChangedDefaultCurrency; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Factory\TransactionCurrencyFactory; use FireflyIII\Models\AccountMeta; @@ -38,6 +39,7 @@ use FireflyIII\Services\Internal\Destroy\CurrencyDestroyService; use FireflyIII\Services\Internal\Update\CurrencyUpdateService; use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; /** * Class CurrencyRepository @@ -62,7 +64,7 @@ class CurrencyRepository implements CurrencyRepositoryInterface public function currencyInUseAt(TransactionCurrency $currency): ?string { app('log')->debug(sprintf('Now in currencyInUse() for #%d ("%s")', $currency->id, $currency->code)); - $countJournals = $this->countJournals($currency); + $countJournals = $this->countJournals($currency); if ($countJournals > 0) { app('log')->info(sprintf('Count journals is %d, return true.', $countJournals)); @@ -77,7 +79,7 @@ class CurrencyRepository implements CurrencyRepositoryInterface } // is being used in accounts: - $meta = AccountMeta::where('name', 'currency_id')->where('data', json_encode((string)$currency->id))->count(); + $meta = AccountMeta::where('name', 'currency_id')->where('data', json_encode((string) $currency->id))->count(); if ($meta > 0) { app('log')->info(sprintf('Used in %d accounts as currency_id, return true. ', $meta)); @@ -85,7 +87,7 @@ class CurrencyRepository implements CurrencyRepositoryInterface } // second search using integer check. - $meta = AccountMeta::where('name', 'currency_id')->where('data', json_encode((int)$currency->id))->count(); + $meta = AccountMeta::where('name', 'currency_id')->where('data', json_encode((int) $currency->id))->count(); if ($meta > 0) { app('log')->info(sprintf('Used in %d accounts as currency_id, return true. ', $meta)); @@ -93,7 +95,7 @@ class CurrencyRepository implements CurrencyRepositoryInterface } // is being used in bills: - $bills = Bill::where('transaction_currency_id', $currency->id)->count(); + $bills = Bill::where('transaction_currency_id', $currency->id)->count(); if ($bills > 0) { app('log')->info(sprintf('Used in %d bills as currency, return true. ', $bills)); @@ -111,10 +113,9 @@ class CurrencyRepository implements CurrencyRepositoryInterface } // is being used in accounts (as integer) - $meta = AccountMeta::leftJoin('accounts', 'accounts.id', '=', 'account_meta.account_id') - ->whereNull('accounts.deleted_at') - ->where('account_meta.name', 'currency_id')->where('account_meta.data', json_encode($currency->id))->count() - ; + $meta = AccountMeta::leftJoin('accounts', 'accounts.id', '=', 'account_meta.account_id') + ->whereNull('accounts.deleted_at') + ->where('account_meta.name', 'currency_id')->where('account_meta.data', json_encode($currency->id))->count(); if ($meta > 0) { app('log')->info(sprintf('Used in %d accounts as currency_id, return true. ', $meta)); @@ -130,7 +131,7 @@ class CurrencyRepository implements CurrencyRepositoryInterface } // is being used in budget limits - $budgetLimit = BudgetLimit::where('transaction_currency_id', $currency->id)->count(); + $budgetLimit = BudgetLimit::where('transaction_currency_id', $currency->id)->count(); if ($budgetLimit > 0) { app('log')->info(sprintf('Used in %d budget limits as currency, return true. ', $budgetLimit)); @@ -138,7 +139,7 @@ class CurrencyRepository implements CurrencyRepositoryInterface } // is the default currency for the user or the system - $count = $this->userGroup->currencies()->where('transaction_currencies.id', $currency->id)->wherePivot('group_default', 1)->count(); + $count = $this->userGroup->currencies()->where('transaction_currencies.id', $currency->id)->wherePivot('group_default', 1)->count(); if ($count > 0) { app('log')->info('Is the default currency of the user, return true.'); @@ -146,7 +147,7 @@ class CurrencyRepository implements CurrencyRepositoryInterface } // is the default currency for the user or the system - $count = $this->userGroup->currencies()->where('transaction_currencies.id', $currency->id)->wherePivot('group_default', 1)->count(); + $count = $this->userGroup->currencies()->where('transaction_currencies.id', $currency->id)->wherePivot('group_default', 1)->count(); if ($count > 0) { app('log')->info('Is the default currency of the user group, return true.'); @@ -179,7 +180,7 @@ class CurrencyRepository implements CurrencyRepositoryInterface return $entry->id === $current->id; }); $isDefault = $local->contains(static function (TransactionCurrency $entry) use ($current) { - return 1 === (int)$entry->pivot->group_default && $entry->id === $current->id; + return 1 === (int) $entry->pivot->group_default && $entry->id === $current->id; }); $current->userGroupEnabled = $hasId; $current->userGroupDefault = $isDefault; @@ -193,7 +194,7 @@ class CurrencyRepository implements CurrencyRepositoryInterface $all = $this->userGroup->currencies()->orderBy('code', 'ASC')->withPivot(['group_default'])->get(); $all->map(static function (TransactionCurrency $current) { $current->userGroupEnabled = true; - $current->userGroupDefault = 1 === (int)$current->pivot->group_default; + $current->userGroupDefault = 1 === (int) $current->pivot->group_default; return $current; }); @@ -260,10 +261,10 @@ class CurrencyRepository implements CurrencyRepositoryInterface public function findCurrencyNull(?int $currencyId, ?string $currencyCode): ?TransactionCurrency { app('log')->debug('Now in findCurrencyNull()'); - $result = $this->find((int)$currencyId); + $result = $this->find((int) $currencyId); if (null === $result) { app('log')->debug(sprintf('Searching for currency with code %s...', $currencyCode)); - $result = $this->findByCode((string)$currencyCode); + $result = $this->findByCode((string) $currencyCode); } if (null !== $result && false === $result->enabled) { app('log')->debug(sprintf('Also enabled currency %s', $result->code)); @@ -308,12 +309,18 @@ class CurrencyRepository implements CurrencyRepositoryInterface public function makeDefault(TransactionCurrency $currency): void { + $current = app('amount')->getDefaultCurrencyByUserGroup($this->userGroup); app('log')->debug(sprintf('Enabled + made default currency %s for user #%d', $currency->code, $this->userGroup->id)); $this->userGroup->currencies()->detach($currency->id); foreach ($this->userGroup->currencies()->get() as $item) { $this->userGroup->currencies()->updateExistingPivot($item->id, ['group_default' => false]); } $this->userGroup->currencies()->syncWithoutDetaching([$currency->id => ['group_default' => true]]); + if ($current->id !== $currency->id) { + Log::debug('Trigger on a different default currency.'); + // clear all native amounts through an event. + event(new UserGroupChangedDefaultCurrency($this->userGroup)); + } } public function searchCurrency(string $search, int $limit): Collection @@ -354,9 +361,6 @@ class CurrencyRepository implements CurrencyRepositoryInterface if (false === $enabled && true === $default) { $enabled = true; } - if (false === $default) { - app('log')->warning(sprintf('Set default=false will NOT do anything for currency %s', $currency->code)); - } // update currency with current user specific settings $currency->refreshForUser($this->user); @@ -375,12 +379,7 @@ class CurrencyRepository implements CurrencyRepositoryInterface // currency must be made default. if (true === $default) { - app('log')->debug(sprintf('Enabled + made default currency %s for user #%d', $currency->code, $this->userGroup->id)); - $this->userGroup->currencies()->detach($currency->id); - foreach ($this->userGroup->currencies()->get() as $item) { - $this->userGroup->currencies()->updateExistingPivot($item->id, ['group_default' => false]); - } - $this->userGroup->currencies()->syncWithoutDetaching([$currency->id => ['group_default' => true]]); + $this->makeDefault($currency); } /** @var CurrencyUpdateService $service */ diff --git a/app/Repositories/UserGroups/PiggyBank/PiggyBankRepository.php b/app/Repositories/UserGroups/PiggyBank/PiggyBankRepository.php index 7652e55c0b..90a4243548 100644 --- a/app/Repositories/UserGroups/PiggyBank/PiggyBankRepository.php +++ b/app/Repositories/UserGroups/PiggyBank/PiggyBankRepository.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Repositories\UserGroups\PiggyBank; +use FireflyIII\Models\PiggyBank; use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait; use Illuminate\Support\Collection; @@ -36,14 +37,15 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface public function getPiggyBanks(): Collection { - return $this->userGroup->piggyBanks() - ->with( - [ - 'account', - 'objectGroups', - ] - ) - ->orderBy('order', 'ASC')->get() - ; + return PiggyBank::leftJoin('account_piggy_bank', 'account_piggy_bank.piggy_bank_id', '=', 'piggy_banks.id') + ->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id') + ->where('accounts.user_group_id', $this->userGroup->id) + ->with( + [ + 'objectGroups', + ] + ) + ->orderBy('piggy_banks.order', 'ASC')->distinct()->get(['piggy_banks.*']) + ; } } diff --git a/app/Support/Http/Api/ExchangeRateConverter.php b/app/Support/Http/Api/ExchangeRateConverter.php index de095dd0ce..b3f8d71630 100644 --- a/app/Support/Http/Api/ExchangeRateConverter.php +++ b/app/Support/Http/Api/ExchangeRateConverter.php @@ -167,7 +167,7 @@ class ExchangeRateConverter /** @var null|CurrencyExchangeRate $result */ $result = auth()->user() - ->currencyExchangeRates() + ?->currencyExchangeRates() ->where('from_currency_id', $from) ->where('to_currency_id', $to) ->where('date', '<=', $date) diff --git a/app/Support/Models/AccountBalanceCalculator.php b/app/Support/Models/AccountBalanceCalculator.php index f807a0a0f8..9f379e7f6f 100644 --- a/app/Support/Models/AccountBalanceCalculator.php +++ b/app/Support/Models/AccountBalanceCalculator.php @@ -243,7 +243,7 @@ class AccountBalanceCalculator $object->balance = $balance[0]; $object->date = $balance[1]; $object->date_tz = $balance[1]?->format('e'); - $object->save(); + $object->saveQuietly(); } } } diff --git a/database/migrations/2024_12_19_061003_add_native_amount_column.php b/database/migrations/2024_12_19_061003_add_native_amount_column.php index 7d777e13b6..9d486328d4 100644 --- a/database/migrations/2024_12_19_061003_add_native_amount_column.php +++ b/database/migrations/2024_12_19_061003_add_native_amount_column.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Schema; return new class extends Migration { private array $tables = [ + // !!! this array is also in PreferencesEventHandler 'accounts' => ['native_virtual_balance'], // works. 'account_piggy_bank' => ['native_current_amount'], // works 'auto_budgets' => ['native_amount'], // works @@ -16,9 +17,7 @@ return new class extends Migration { 'piggy_banks' => ['native_target_amount'], // works 'transactions' => ['native_amount', 'native_foreign_amount'], // works - // TODO native currency changes, reset everything. // TODO button to recalculate all native amounts on selected pages? - // TODO check if you use the correct date for the excange rate ];