From f6e642f72ebb7ee0fbc3d47698f69e51497ee585 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 22 Dec 2024 13:19:23 +0100 Subject: [PATCH] Add and remove exchange rates --- .../Model/ExchangeRate/DestroyController.php | 66 +++++++ .../Model/ExchangeRate/StoreController.php | 82 ++++++++ .../Model/ExchangeRate/UpdateController.php | 69 +++++++ .../Model/ExchangeRate/DestroyRequest.php | 51 +++++ .../Model/ExchangeRate/StoreRequest.php | 67 +++++++ .../Model/ExchangeRate/UpdateRequest.php | 56 ++++++ .../Integrity/UpdateGroupInformation.php | 2 +- app/Factory/PiggyBankEventFactory.php | 11 +- .../PiggyBank/ModifiesPiggyBanks.php | 63 ++++--- .../PiggyBank/PiggyBankRepository.php | 15 +- .../PiggyBankRepositoryInterface.php | 4 +- .../ExchangeRate/ExchangeRateRepository.php | 66 +++++-- .../ExchangeRateRepositoryInterface.php | 11 ++ app/Support/Binder/UserGroupExchangeRate.php | 51 +++++ app/Support/Domain.php | 2 +- app/Support/Preferences.php | 47 ++--- config/bindables.php | 126 +++++++++++++ config/firefly.php | 58 +----- config/translations.php | 7 + .../src/components/exchange-rates/Rates.vue | 177 ++++++++++++++++-- resources/assets/v1/src/locales/bg.json | 13 +- resources/assets/v1/src/locales/ca.json | 13 +- resources/assets/v1/src/locales/cs.json | 13 +- resources/assets/v1/src/locales/da.json | 13 +- resources/assets/v1/src/locales/de.json | 17 +- resources/assets/v1/src/locales/el.json | 13 +- resources/assets/v1/src/locales/en-gb.json | 13 +- resources/assets/v1/src/locales/en.json | 11 +- resources/assets/v1/src/locales/es.json | 13 +- resources/assets/v1/src/locales/fi.json | 13 +- resources/assets/v1/src/locales/fr.json | 13 +- resources/assets/v1/src/locales/hu.json | 13 +- resources/assets/v1/src/locales/id.json | 13 +- resources/assets/v1/src/locales/it.json | 13 +- resources/assets/v1/src/locales/ja.json | 13 +- resources/assets/v1/src/locales/ko.json | 13 +- resources/assets/v1/src/locales/nb.json | 13 +- resources/assets/v1/src/locales/nl.json | 13 +- resources/assets/v1/src/locales/nn.json | 13 +- resources/assets/v1/src/locales/pl.json | 13 +- resources/assets/v1/src/locales/pt-br.json | 13 +- resources/assets/v1/src/locales/pt.json | 13 +- resources/assets/v1/src/locales/ro.json | 13 +- resources/assets/v1/src/locales/ru.json | 13 +- resources/assets/v1/src/locales/sk.json | 13 +- resources/assets/v1/src/locales/sl.json | 13 +- resources/assets/v1/src/locales/sv.json | 13 +- resources/assets/v1/src/locales/tr.json | 13 +- resources/assets/v1/src/locales/uk.json | 13 +- resources/assets/v1/src/locales/vi.json | 13 +- resources/assets/v1/src/locales/zh-cn.json | 13 +- resources/assets/v1/src/locales/zh-tw.json | 13 +- resources/lang/en_US/firefly.php | 5 +- resources/lang/en_US/form.php | 1 + routes/api.php | 4 +- 55 files changed, 1197 insertions(+), 262 deletions(-) create mode 100644 app/Api/V2/Controllers/Model/ExchangeRate/DestroyController.php create mode 100644 app/Api/V2/Controllers/Model/ExchangeRate/StoreController.php create mode 100644 app/Api/V2/Controllers/Model/ExchangeRate/UpdateController.php create mode 100644 app/Api/V2/Request/Model/ExchangeRate/DestroyRequest.php create mode 100644 app/Api/V2/Request/Model/ExchangeRate/StoreRequest.php create mode 100644 app/Api/V2/Request/Model/ExchangeRate/UpdateRequest.php create mode 100644 app/Support/Binder/UserGroupExchangeRate.php create mode 100644 config/bindables.php diff --git a/app/Api/V2/Controllers/Model/ExchangeRate/DestroyController.php b/app/Api/V2/Controllers/Model/ExchangeRate/DestroyController.php new file mode 100644 index 0000000000..f1575f0d4c --- /dev/null +++ b/app/Api/V2/Controllers/Model/ExchangeRate/DestroyController.php @@ -0,0 +1,66 @@ +middleware( + function ($request, $next) { + $this->repository = app(ExchangeRateRepositoryInterface::class); + $this->repository->setUserGroup($this->validateUserGroup($request)); + + return $next($request); + } + ); + } + + public function destroy(DestroyRequest $request, TransactionCurrency $from, TransactionCurrency $to): JsonResponse + { + $date = $request->getDate(); + $rate = $this->repository->getSpecificRateOnDate($from, $to, $date); + if (null === $rate) { + throw new NotFoundHttpException(); + } + $this->repository->deleteRate($rate); + return response()->json([], 204); + } + +} diff --git a/app/Api/V2/Controllers/Model/ExchangeRate/StoreController.php b/app/Api/V2/Controllers/Model/ExchangeRate/StoreController.php new file mode 100644 index 0000000000..4cc701320c --- /dev/null +++ b/app/Api/V2/Controllers/Model/ExchangeRate/StoreController.php @@ -0,0 +1,82 @@ +middleware( + function ($request, $next) { + $this->repository = app(ExchangeRateRepositoryInterface::class); + $this->repository->setUserGroup($this->validateUserGroup($request)); + + return $next($request); + } + ); + } + + public function store(StoreRequest $request): JsonResponse + { + $date = $request->getDate(); + $rate = $request->getRate(); + $from = $request->getFromCurrency(); + $to = $request->getToCurrency(); + + // already has rate? + $object = $this->repository->getSpecificRateOnDate($from, $to, $date); + if(null !== $object) { + // just update it, no matter. + $rate = $this->repository->updateExchangeRate($object, $rate, $date); + } + if(null === $object) { + // store new + $rate = $this->repository->storeExchangeRate($from, $to, $rate, $date); + } + + $transformer = new ExchangeRateTransformer(); + $transformer->setParameters($this->parameters); + + return response() + ->api($this->jsonApiObject(self::RESOURCE_KEY, $rate, $transformer)) + ->header('Content-Type', self::CONTENT_TYPE); + } + +} diff --git a/app/Api/V2/Controllers/Model/ExchangeRate/UpdateController.php b/app/Api/V2/Controllers/Model/ExchangeRate/UpdateController.php new file mode 100644 index 0000000000..367e17d96d --- /dev/null +++ b/app/Api/V2/Controllers/Model/ExchangeRate/UpdateController.php @@ -0,0 +1,69 @@ +middleware( + function ($request, $next) { + $this->repository = app(ExchangeRateRepositoryInterface::class); + $this->repository->setUserGroup($this->validateUserGroup($request)); + + return $next($request); + } + ); + } + + public function update(UpdateRequest $request, CurrencyExchangeRate $exchangeRate): JsonResponse + { + $date = $request->getDate(); + $rate = $request->getRate(); + $exchangeRate = $this->repository->updateExchangeRate($exchangeRate, $rate, $date); + $transformer = new ExchangeRateTransformer(); + $transformer->setParameters($this->parameters); + + return response() + ->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer)) + ->header('Content-Type', self::CONTENT_TYPE); + } + +} diff --git a/app/Api/V2/Request/Model/ExchangeRate/DestroyRequest.php b/app/Api/V2/Request/Model/ExchangeRate/DestroyRequest.php new file mode 100644 index 0000000000..0db1d18b3e --- /dev/null +++ b/app/Api/V2/Request/Model/ExchangeRate/DestroyRequest.php @@ -0,0 +1,51 @@ +getCarbonDate('date'); + } + + /** + * The rules that the incoming request must be matched against. + */ + public function rules(): array + { + return [ + 'date' => 'required|date|after:1900-01-01|before:2099-12-31', + ]; + } + +} diff --git a/app/Api/V2/Request/Model/ExchangeRate/StoreRequest.php b/app/Api/V2/Request/Model/ExchangeRate/StoreRequest.php new file mode 100644 index 0000000000..16ae36cdf3 --- /dev/null +++ b/app/Api/V2/Request/Model/ExchangeRate/StoreRequest.php @@ -0,0 +1,67 @@ +getCarbonDate('date'); + } + + public function getRate(): string + { + return (string) $this->get('rate'); + } + + public function getFromCurrency(): TransactionCurrency { + return TransactionCurrency::where('code', $this->get('from'))->first(); + } + public function getToCurrency(): TransactionCurrency { + return TransactionCurrency::where('code', $this->get('to'))->first(); + } + + /** + * The rules that the incoming request must be matched against. + */ + public function rules(): array + { + return [ + 'date' => 'required|date|after:1900-01-01|before:2099-12-31', + 'rate' => 'required|numeric|gt:0', + 'from' => 'required|exists:transaction_currencies,code', + 'to' => 'required|exists:transaction_currencies,code', + ]; + } + +} diff --git a/app/Api/V2/Request/Model/ExchangeRate/UpdateRequest.php b/app/Api/V2/Request/Model/ExchangeRate/UpdateRequest.php new file mode 100644 index 0000000000..7bda832ba4 --- /dev/null +++ b/app/Api/V2/Request/Model/ExchangeRate/UpdateRequest.php @@ -0,0 +1,56 @@ +getCarbonDate('date'); + } + + public function getRate(): string { + return (string) $this->get('rate'); + } + + /** + * The rules that the incoming request must be matched against. + */ + public function rules(): array + { + return [ + 'date' => 'date|after:1900-01-01|before:2099-12-31', + 'rate' => 'required|numeric|gt:0', + ]; + } + +} diff --git a/app/Console/Commands/Integrity/UpdateGroupInformation.php b/app/Console/Commands/Integrity/UpdateGroupInformation.php index 57df0f52a6..3be6c7ee0d 100644 --- a/app/Console/Commands/Integrity/UpdateGroupInformation.php +++ b/app/Console/Commands/Integrity/UpdateGroupInformation.php @@ -79,7 +79,7 @@ class UpdateGroupInformation extends Command { $group = $user->userGroup; if (null === $group) { - $this->friendlyWarning(sprintf('User "%s" has no group.', $user->email)); + $this->friendlyWarning(sprintf('User "%s" has no group. Please run "php artisan firefly-iii:create-group-memberships"', $user->email)); return; } diff --git a/app/Factory/PiggyBankEventFactory.php b/app/Factory/PiggyBankEventFactory.php index 5ae8ed384e..7cfed006a5 100644 --- a/app/Factory/PiggyBankEventFactory.php +++ b/app/Factory/PiggyBankEventFactory.php @@ -47,20 +47,13 @@ class PiggyBankEventFactory $piggyRepos = app(PiggyBankRepositoryInterface::class); $piggyRepos->setUser($journal->user); - $repetition = $piggyRepos->getRepetition($piggyBank); - if (null === $repetition) { - app('log')->error(sprintf('No piggy bank repetition on %s!', $journal->date->format('Y-m-d'))); - - return; - } - app('log')->debug('Found repetition'); - $amount = $piggyRepos->getExactAmount($piggyBank, $repetition, $journal); + $amount = $piggyRepos->getExactAmount($piggyBank, $journal); if (0 === bccomp($amount, '0')) { app('log')->debug('Amount is zero, will not create event.'); return; } // amount can be negative here - $piggyRepos->addAmountToRepetition($repetition, $amount, $journal); + $piggyRepos->addAmountToPiggyBank($piggyBank, $amount, $journal); } } diff --git a/app/Repositories/PiggyBank/ModifiesPiggyBanks.php b/app/Repositories/PiggyBank/ModifiesPiggyBanks.php index b3b13cc049..0722f69bf1 100644 --- a/app/Repositories/PiggyBank/ModifiesPiggyBanks.php +++ b/app/Repositories/PiggyBank/ModifiesPiggyBanks.php @@ -30,7 +30,7 @@ use FireflyIII\Factory\PiggyBankFactory; use FireflyIII\Models\Account; use FireflyIII\Models\Note; use FireflyIII\Models\PiggyBank; -use FireflyIII\Models\PiggyBankRepetition; +use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\ObjectGroup\CreatesObjectGroups; use FireflyIII\Support\Http\Api\ExchangeRateConverter; @@ -43,17 +43,20 @@ trait ModifiesPiggyBanks { use CreatesObjectGroups; - public function addAmountToRepetition(PiggyBankRepetition $repetition, string $amount, TransactionJournal $journal): void + public function addAmountToPiggyBank(PiggyBank $piggyBank, string $amount, TransactionJournal $journal): void { - throw new FireflyException('[a] Piggy bank repetitions are EOL.'); - Log::debug(sprintf('addAmountToRepetition: %s', $amount)); + Log::debug(sprintf('addAmountToPiggyBank: %s', $amount)); if (-1 === bccomp($amount, '0')) { + /** @var Transaction $source */ + $source = $journal->transactions()->with(['account'])->where('amount', '<', 0)->first(); Log::debug('Remove amount.'); - $this->removeAmount($repetition->piggyBank, bcmul($amount, '-1'), $journal); + $this->removeAmount($piggyBank, $source->account, bcmul($amount, '-1'), $journal); } if (1 === bccomp($amount, '0')) { + /** @var Transaction $destination */ + $destination = $journal->transactions()->with(['account'])->where('amount', '>', 0)->first(); Log::debug('Add amount.'); - $this->addAmount($repetition->piggyBank, $amount, $journal); + $this->addAmount($piggyBank, $destination->account, $amount, $journal); } } @@ -65,9 +68,9 @@ trait ModifiesPiggyBanks $pivot->native_current_amount = null; // also update native_current_amount. - $userCurrency = app('amount')->getDefaultCurrencyByUserGroup($this->user->userGroup); + $userCurrency = app('amount')->getDefaultCurrencyByUserGroup($this->user->userGroup); if ($userCurrency->id !== $piggyBank->transaction_currency_id) { - $converter = new ExchangeRateConverter(); + $converter = new ExchangeRateConverter(); $converter->setIgnoreSettings(true); $pivot->native_current_amount = $converter->convert($piggyBank->transactionCurrency, $userCurrency, today(), $pivot->current_amount); } @@ -88,9 +91,9 @@ trait ModifiesPiggyBanks $pivot->native_current_amount = null; // also update native_current_amount. - $userCurrency = app('amount')->getDefaultCurrencyByUserGroup($this->user->userGroup); + $userCurrency = app('amount')->getDefaultCurrencyByUserGroup($this->user->userGroup); if ($userCurrency->id !== $piggyBank->transaction_currency_id) { - $converter = new ExchangeRateConverter(); + $converter = new ExchangeRateConverter(); $converter->setIgnoreSettings(true); $pivot->native_current_amount = $converter->convert($piggyBank->transactionCurrency, $userCurrency, today(), $pivot->current_amount); } @@ -122,8 +125,8 @@ trait ModifiesPiggyBanks Log::debug(sprintf('Maximum amount: %s', $maxAmount)); } - $compare = bccomp($amount, $maxAmount); - $result = $compare <= 0; + $compare = bccomp($amount, $maxAmount); + $result = $compare <= 0; Log::debug(sprintf('Compare <= 0? %d, so canAddAmount is %s', $compare, var_export($result, true))); @@ -157,11 +160,11 @@ trait ModifiesPiggyBanks public function setCurrentAmount(PiggyBank $piggyBank, string $amount): PiggyBank { - $repetition = $this->getRepetition($piggyBank); + $repetition = $this->getRepetition($piggyBank); if (null === $repetition) { return $piggyBank; } - $max = $piggyBank->target_amount; + $max = $piggyBank->target_amount; if (1 === bccomp($amount, $max) && 0 !== bccomp($piggyBank->target_amount, '0')) { $amount = $max; } @@ -204,14 +207,14 @@ trait ModifiesPiggyBanks public function update(PiggyBank $piggyBank, array $data): PiggyBank { - $piggyBank = $this->updateProperties($piggyBank, $data); + $piggyBank = $this->updateProperties($piggyBank, $data); if (array_key_exists('notes', $data)) { $this->updateNote($piggyBank, (string) $data['notes']); } // update the order of the piggy bank: - $oldOrder = $piggyBank->order; - $newOrder = (int) ($data['order'] ?? $oldOrder); + $oldOrder = $piggyBank->order; + $newOrder = (int) ($data['order'] ?? $oldOrder); if ($oldOrder !== $newOrder) { $this->setOrder($piggyBank, $newOrder); } @@ -303,7 +306,7 @@ trait ModifiesPiggyBanks return; } - $dbNote = $piggyBank->notes()->first(); + $dbNote = $piggyBank->notes()->first(); if (null === $dbNote) { $dbNote = new Note(); $dbNote->noteable()->associate($piggyBank); @@ -314,16 +317,15 @@ trait ModifiesPiggyBanks public function setOrder(PiggyBank $piggyBank, int $newOrder): bool { - $oldOrder = $piggyBank->order; + $oldOrder = $piggyBank->order; // Log::debug(sprintf('Will move piggy bank #%d ("%s") from %d to %d', $piggyBank->id, $piggyBank->name, $oldOrder, $newOrder)); if ($newOrder > $oldOrder) { 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_id', $this->user->id) - ->where('piggy_banks.order', '<=', $newOrder)->where('piggy_banks.order', '>', $oldOrder) - ->where('piggy_banks.id', '!=', $piggyBank->id) - ->distinct()->decrement('piggy_banks.order') - ; + ->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id') + ->where('accounts.user_id', $this->user->id) + ->where('piggy_banks.order', '<=', $newOrder)->where('piggy_banks.order', '>', $oldOrder) + ->where('piggy_banks.id', '!=', $piggyBank->id) + ->distinct()->decrement('piggy_banks.order'); $piggyBank->order = $newOrder; Log::debug(sprintf('[1] Order of piggy #%d ("%s") from %d to %d', $piggyBank->id, $piggyBank->name, $oldOrder, $newOrder)); @@ -332,12 +334,11 @@ trait ModifiesPiggyBanks return true; } 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_id', $this->user->id) - ->where('piggy_banks.order', '>=', $newOrder)->where('piggy_banks.order', '<', $oldOrder) - ->where('piggy_banks.id', '!=', $piggyBank->id) - ->distinct()->increment('piggy_banks.order') - ; + ->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id') + ->where('accounts.user_id', $this->user->id) + ->where('piggy_banks.order', '>=', $newOrder)->where('piggy_banks.order', '<', $oldOrder) + ->where('piggy_banks.id', '!=', $piggyBank->id) + ->distinct()->increment('piggy_banks.order'); $piggyBank->order = $newOrder; Log::debug(sprintf('[2] Order of piggy #%d ("%s") from %d to %d', $piggyBank->id, $piggyBank->name, $oldOrder, $newOrder)); diff --git a/app/Repositories/PiggyBank/PiggyBankRepository.php b/app/Repositories/PiggyBank/PiggyBankRepository.php index 37862c0a3c..127077083d 100644 --- a/app/Repositories/PiggyBank/PiggyBankRepository.php +++ b/app/Repositories/PiggyBank/PiggyBankRepository.php @@ -129,10 +129,9 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface * * @throws FireflyException */ - public function getExactAmount(PiggyBank $piggyBank, PiggyBankRepetition $repetition, TransactionJournal $journal): string + public function getExactAmount(PiggyBank $piggyBank, TransactionJournal $journal): string { - throw new FireflyException('[c] Piggy bank repetitions are EOL.'); - app('log')->debug(sprintf('Now in getExactAmount(%d, %d, %d)', $piggyBank->id, $repetition->id, $journal->id)); + app('log')->debug(sprintf('Now in getExactAmount(%d, %d)', $piggyBank->id, $journal->id)); $operator = null; $currency = null; @@ -146,9 +145,8 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface $accountRepos->setUser($this->user); $defaultCurrency = app('amount')->getDefaultCurrencyByUserGroup($this->user->userGroup); - $piggyBankCurrency = $accountRepos->getAccountCurrency($piggyBank->account) ?? $defaultCurrency; - app('log')->debug(sprintf('Piggy bank #%d currency is %s', $piggyBank->id, $piggyBankCurrency->code)); + app('log')->debug(sprintf('Piggy bank #%d currency is %s', $piggyBank->id, $piggyBank->transactionCurrency->code)); /** @var Transaction $source */ $source = $journal->transactions()->with(['account'])->where('amount', '<', 0)->first(); @@ -192,8 +190,9 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface } app('log')->debug(sprintf('The currency is %s and the amount is %s', $currency->code, $amount)); - $room = bcsub($piggyBank->target_amount, $repetition->current_amount); - $compare = bcmul($repetition->current_amount, '-1'); + $currentAmount = $this->getCurrentAmount($piggyBank); + $room = bcsub($piggyBank->target_amount, $currentAmount); + $compare = bcmul($currentAmount, '-1'); if (0 === bccomp($piggyBank->target_amount, '0')) { // amount is zero? then the "room" is positive amount of we wish to add or remove. @@ -215,7 +214,7 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface // amount is negative and $currentAmount is smaller than $amount if (-1 === bccomp($amount, '0') && 1 === bccomp($compare, $amount)) { - app('log')->debug(sprintf('Max amount to remove is %f', $repetition->current_amount)); + app('log')->debug(sprintf('Max amount to remove is %f', $currentAmount)); app('log')->debug(sprintf('Cannot remove %f from piggy bank #%d ("%s")', $amount, $piggyBank->id, $piggyBank->name)); app('log')->debug(sprintf('New amount is %f', $compare)); diff --git a/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php b/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php index 739a830c8b..26486227ed 100644 --- a/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php +++ b/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php @@ -40,7 +40,7 @@ interface PiggyBankRepositoryInterface { public function addAmount(PiggyBank $piggyBank, Account $account, string $amount, ?TransactionJournal $journal = null): bool; - public function addAmountToRepetition(PiggyBankRepetition $repetition, string $amount, TransactionJournal $journal): void; + public function addAmountToPiggyBank(PiggyBank $piggyBank, string $amount, TransactionJournal $journal): void; public function canAddAmount(PiggyBank $piggyBank, Account $account, string $amount): bool; @@ -80,7 +80,7 @@ interface PiggyBankRepositoryInterface /** * Used for connecting to a piggy bank. */ - public function getExactAmount(PiggyBank $piggyBank, PiggyBankRepetition $repetition, TransactionJournal $journal): string; + public function getExactAmount(PiggyBank $piggyBank, TransactionJournal $journal): string; /** * Return note for piggy bank. diff --git a/app/Repositories/UserGroups/ExchangeRate/ExchangeRateRepository.php b/app/Repositories/UserGroups/ExchangeRate/ExchangeRateRepository.php index a528cddb88..727cae0dd6 100644 --- a/app/Repositories/UserGroups/ExchangeRate/ExchangeRateRepository.php +++ b/app/Repositories/UserGroups/ExchangeRate/ExchangeRateRepository.php @@ -24,6 +24,8 @@ declare(strict_types=1); namespace FireflyIII\Repositories\UserGroups\ExchangeRate; +use Carbon\Carbon; +use FireflyIII\Models\CurrencyExchangeRate; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait; use Illuminate\Database\Eloquent\Builder; @@ -39,19 +41,57 @@ class ExchangeRateRepository implements ExchangeRateRepositoryInterface // orderBy('date', 'DESC')->toRawSql(); return $this->userGroup->currencyExchangeRates() - ->where(function (Builder $q1) use ($from, $to): void { - $q1->where(function (Builder $q) use ($from, $to): void { - $q->where('from_currency_id', $from->id) - ->where('to_currency_id', $to->id) - ; - })->orWhere(function (Builder $q) use ($from, $to): void { - $q->where('from_currency_id', $to->id) - ->where('to_currency_id', $from->id) - ; - }); - }) - ->orderBy('date', 'DESC')->get(['currency_exchange_rates.*']) - ; + ->where(function (Builder $q1) use ($from, $to): void { + $q1->where(function (Builder $q) use ($from, $to): void { + $q->where('from_currency_id', $from->id) + ->where('to_currency_id', $to->id); + })->orWhere(function (Builder $q) use ($from, $to): void { + $q->where('from_currency_id', $to->id) + ->where('to_currency_id', $from->id); + }); + }) + ->orderBy('date', 'DESC') + ->get(['currency_exchange_rates.*']); } + + #[\Override] public function getSpecificRateOnDate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): ?CurrencyExchangeRate + { + return + $this->userGroup->currencyExchangeRates() + ->where('from_currency_id', $from->id) + ->where('to_currency_id', $to->id) + ->where('date', $date->format('Y-m-d')) + ->first(); + } + + #[\Override] public function deleteRate(CurrencyExchangeRate $rate): void + { + $this->userGroup->currencyExchangeRates()->where('id', $rate->id)->delete(); + } + + #[\Override] public function updateExchangeRate(CurrencyExchangeRate $object, string $rate, ?Carbon $date = null): CurrencyExchangeRate + { + $object->rate = $rate; + if (null !== $date) { + $object->date = $date; + } + $object->save(); + return $object; + } + + #[\Override] public function storeExchangeRate(TransactionCurrency $from, TransactionCurrency $to, string $rate, Carbon $date): CurrencyExchangeRate + { + $object = new CurrencyExchangeRate(); + $object->user_id = auth()->user()->id; + $object->user_group_id = $this->userGroup->id; + $object->from_currency_id = $from->id; + $object->to_currency_id = $to->id; + $object->rate = $rate; + $object->date = $date; + $object->date_tz = $date->format('e'); + $object->save(); + + return $object; + } } diff --git a/app/Repositories/UserGroups/ExchangeRate/ExchangeRateRepositoryInterface.php b/app/Repositories/UserGroups/ExchangeRate/ExchangeRateRepositoryInterface.php index 7390261e34..e5c1035f42 100644 --- a/app/Repositories/UserGroups/ExchangeRate/ExchangeRateRepositoryInterface.php +++ b/app/Repositories/UserGroups/ExchangeRate/ExchangeRateRepositoryInterface.php @@ -24,10 +24,21 @@ declare(strict_types=1); namespace FireflyIII\Repositories\UserGroups\ExchangeRate; +use Carbon\Carbon; +use FireflyIII\Models\CurrencyExchangeRate; use FireflyIII\Models\TransactionCurrency; use Illuminate\Support\Collection; interface ExchangeRateRepositoryInterface { public function getRates(TransactionCurrency $from, TransactionCurrency $to): Collection; + + public function getSpecificRateOnDate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): ?CurrencyExchangeRate; + + public function deleteRate(CurrencyExchangeRate $rate): void; + + public function updateExchangeRate(CurrencyExchangeRate $object, string $rate, ?Carbon $date = null): CurrencyExchangeRate; + + public function storeExchangeRate(TransactionCurrency $from, TransactionCurrency $to, string $rate, Carbon $date): CurrencyExchangeRate; + } diff --git a/app/Support/Binder/UserGroupExchangeRate.php b/app/Support/Binder/UserGroupExchangeRate.php new file mode 100644 index 0000000000..af5a23cc5a --- /dev/null +++ b/app/Support/Binder/UserGroupExchangeRate.php @@ -0,0 +1,51 @@ +. + */ +declare(strict_types=1); + +namespace FireflyIII\Support\Binder; + +use FireflyIII\Models\CurrencyExchangeRate; +use FireflyIII\User; +use Illuminate\Routing\Route; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + +/** + * Class UserGroupTransaction. + */ +class UserGroupExchangeRate implements BinderInterface +{ + public static function routeBinder(string $value, Route $route): CurrencyExchangeRate + { + if (auth()->check()) { + /** @var User $user */ + $user = auth()->user(); + $rate = CurrencyExchangeRate::where('id', (int) $value) + ->where('user_group_id', $user->user_group_id) + ->first(); + if (null !== $rate) { + return $rate; + } + } + + throw new NotFoundHttpException(); + } +} diff --git a/app/Support/Domain.php b/app/Support/Domain.php index c37a70e431..b193717884 100644 --- a/app/Support/Domain.php +++ b/app/Support/Domain.php @@ -30,7 +30,7 @@ class Domain { public static function getBindables(): array { - return config('firefly.bindables'); + return config('bindables.bindables'); } public static function getRuleActions(): array diff --git a/app/Support/Preferences.php b/app/Support/Preferences.php index 4bc5127e7e..46bca8be78 100644 --- a/app/Support/Preferences.php +++ b/app/Support/Preferences.php @@ -47,16 +47,15 @@ class Preferences } return Preference::where('user_id', $user->id) - ->where('name', '!=', 'currencyPreference') - ->where(function (Builder $q) use ($user): void { - $q->whereNull('user_group_id'); - $q->orWhere('user_group_id', $user->user_group_id); - }) - ->get() - ; + ->where('name', '!=', 'currencyPreference') + ->where(function (Builder $q) use ($user): void { + $q->whereNull('user_group_id'); + $q->orWhere('user_group_id', $user->user_group_id); + }) + ->get(); } - public function get(string $name, null|array|bool|int|string $default = null): ?Preference + public function get(string $name, null | array | bool | int | string $default = null): ?Preference { /** @var null|User $user */ $user = auth()->user(); @@ -70,7 +69,7 @@ class Preferences return $this->getForUser($user, $name, $default); } - public function getForUser(User $user, string $name, null|array|bool|int|string $default = null): ?Preference + public function getForUser(User $user, string $name, null | array | bool | int | string $default = null): ?Preference { // don't care about user group ID, except for some specific preferences. $userGroupId = $this->getUserGroupId($user, $name); @@ -122,14 +121,16 @@ class Preferences Cache::put($key, '', 5); } - public function setForUser(User $user, string $name, null|array|bool|int|string $value): Preference + public function setForUser(User $user, string $name, null | array | bool | int | string $value): Preference { - $fullName = sprintf('preference%s%s', $user->id, $name); - $groupId = $this->getUserGroupId($user, $name); + $fullName = sprintf('preference%s%s', $user->id, $name); + $groupId = $this->getUserGroupId($user, $name); + $groupId = 0 === (int)$groupId ? null : (int) $groupId; + Cache::forget($fullName); /** @var null|Preference $pref */ - $pref = Preference::where('user_group_id', $groupId)->where('user_id', $user->id)->where('name', $name)->first(['id', 'name', 'data', 'updated_at', 'created_at']); + $pref = Preference::where('user_group_id', $groupId)->where('user_id', $user->id)->where('name', $name)->first(['id', 'name', 'data', 'updated_at', 'created_at']); if (null !== $pref && null === $value) { $pref->delete(); @@ -144,6 +145,7 @@ class Preferences $pref->user_id = (int) $user->id; $pref->user_group_id = $groupId; $pref->name = $name; + } $pref->data = $value; $pref->save(); @@ -171,13 +173,12 @@ class Preferences { $result = []; $preferences = Preference::where('user_id', $user->id) - ->where(function (Builder $q) use ($user): void { - $q->whereNull('user_group_id'); - $q->orWhere('user_group_id', $user->user_group_id); - }) - ->whereIn('name', $list) - ->get(['id', 'name', 'data']) - ; + ->where(function (Builder $q) use ($user): void { + $q->whereNull('user_group_id'); + $q->orWhere('user_group_id', $user->user_group_id); + }) + ->whereIn('name', $list) + ->get(['id', 'name', 'data']); /** @var Preference $preference */ foreach ($preferences as $preference) { @@ -215,7 +216,7 @@ class Preferences return $result; } - public function getEncryptedForUser(User $user, string $name, null|array|bool|int|string $default = null): ?Preference + public function getEncryptedForUser(User $user, string $name, null | array | bool | int | string $default = null): ?Preference { $result = $this->getForUser($user, $name, $default); if ('' === $result->data) { @@ -236,7 +237,7 @@ class Preferences return $result; } - public function getFresh(string $name, null|array|bool|int|string $default = null): ?Preference + public function getFresh(string $name, null | array | bool | int | string $default = null): ?Preference { /** @var null|User $user */ $user = auth()->user(); @@ -287,7 +288,7 @@ class Preferences return $this->set($name, $encrypted); } - public function set(string $name, null|array|bool|int|string $value): Preference + public function set(string $name, null | array | bool | int | string $value): Preference { /** @var null|User $user */ $user = auth()->user(); diff --git a/config/bindables.php b/config/bindables.php new file mode 100644 index 0000000000..fc660382e7 --- /dev/null +++ b/config/bindables.php @@ -0,0 +1,126 @@ + [ + // models + 'account' => Account::class, + 'attachment' => Attachment::class, + 'availableBudget' => AvailableBudget::class, + 'bill' => Bill::class, + 'budget' => Budget::class, + 'budgetLimit' => BudgetLimit::class, + 'category' => Category::class, + 'linkType' => LinkType::class, + 'transactionType' => TransactionType::class, + 'journalLink' => TransactionJournalLink::class, + 'currency' => TransactionCurrency::class, + 'objectGroup' => ObjectGroup::class, + 'piggyBank' => PiggyBank::class, + 'preference' => Preference::class, + 'tj' => TransactionJournal::class, + 'tag' => Tag::class, + 'recurrence' => Recurrence::class, + 'rule' => Rule::class, + 'ruleGroup' => RuleGroup::class, + 'transactionGroup' => TransactionGroup::class, + 'user' => User::class, + 'webhook' => Webhook::class, + 'webhookMessage' => WebhookMessage::class, + 'webhookAttempt' => WebhookAttempt::class, + 'invitedUser' => InvitedUser::class, + + // strings + 'currency_code' => CurrencyCode::class, + + // dates + 'start_date' => Date::class, + 'end_date' => Date::class, + 'date' => Date::class, + + // lists + 'accountList' => AccountList::class, + 'doubleList' => AccountList::class, + 'budgetList' => BudgetList::class, + 'journalList' => JournalList::class, + 'categoryList' => CategoryList::class, + 'tagList' => TagList::class, + + // others + 'fromCurrencyCode' => CurrencyCode::class, + 'toCurrencyCode' => CurrencyCode::class, + 'cliToken' => CLIToken::class, + 'tagOrId' => TagOrId::class, + 'dynamicConfigKey' => DynamicConfigKey::class, + 'eitherConfigKey' => EitherConfigKey::class, + + // V2 API endpoints: + 'userGroupAccount' => UserGroupAccount::class, + 'userGroupTransaction' => UserGroupTransaction::class, + 'userGroupBill' => UserGroupBill::class, + 'userGroupExchangeRate' => UserGroupExchangeRate::class, + 'userGroup' => UserGroup::class, +], +]; diff --git a/config/firefly.php b/config/firefly.php index 33fd2f4360..82b079c994 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -63,6 +63,7 @@ use FireflyIII\Support\Binder\TagList; use FireflyIII\Support\Binder\TagOrId; use FireflyIII\Support\Binder\UserGroupAccount; use FireflyIII\Support\Binder\UserGroupBill; +use FireflyIII\Support\Binder\UserGroupExchangeRate; use FireflyIII\Support\Binder\UserGroupTransaction; use FireflyIII\TransactionRules\Actions\AddTag; use FireflyIII\TransactionRules\Actions\ClearBudget; @@ -427,64 +428,7 @@ return [ 'transfers' => 'fa-exchange', ], - 'bindables' => [ - // models - 'account' => Account::class, - 'attachment' => Attachment::class, - 'availableBudget' => AvailableBudget::class, - 'bill' => Bill::class, - 'budget' => Budget::class, - 'budgetLimit' => BudgetLimit::class, - 'category' => Category::class, - 'linkType' => LinkType::class, - 'transactionType' => TransactionTypeModel::class, - 'journalLink' => TransactionJournalLink::class, - 'currency' => TransactionCurrency::class, - 'objectGroup' => ObjectGroup::class, - 'piggyBank' => PiggyBank::class, - 'preference' => Preference::class, - 'tj' => TransactionJournal::class, - 'tag' => Tag::class, - 'recurrence' => Recurrence::class, - 'rule' => Rule::class, - 'ruleGroup' => RuleGroup::class, - 'transactionGroup' => TransactionGroup::class, - 'user' => User::class, - 'webhook' => Webhook::class, - 'webhookMessage' => WebhookMessage::class, - 'webhookAttempt' => WebhookAttempt::class, - 'invitedUser' => InvitedUser::class, - // strings - 'currency_code' => CurrencyCode::class, - - // dates - 'start_date' => Date::class, - 'end_date' => Date::class, - 'date' => Date::class, - - // lists - 'accountList' => AccountList::class, - 'doubleList' => AccountList::class, - 'budgetList' => BudgetList::class, - 'journalList' => JournalList::class, - 'categoryList' => CategoryList::class, - 'tagList' => TagList::class, - - // others - 'fromCurrencyCode' => CurrencyCode::class, - 'toCurrencyCode' => CurrencyCode::class, - 'cliToken' => CLIToken::class, - 'tagOrId' => TagOrId::class, - 'dynamicConfigKey' => DynamicConfigKey::class, - 'eitherConfigKey' => EitherConfigKey::class, - - // V2 API endpoints: - 'userGroupAccount' => UserGroupAccount::class, - 'userGroupTransaction' => UserGroupTransaction::class, - 'userGroupBill' => UserGroupBill::class, - 'userGroup' => UserGroup::class, - ], 'rule-actions' => [ 'set_category' => SetCategory::class, 'clear_category' => ClearCategory::class, diff --git a/config/translations.php b/config/translations.php index 8b40d4b1fb..a9c2c9152c 100644 --- a/config/translations.php +++ b/config/translations.php @@ -274,12 +274,18 @@ return [ 'exchange_rates_intro', 'exchange_rates_from_to', 'exchange_rates_intro_rates', + 'header_exchange_rates_rates', + 'header_exchange_rates_table', + 'help_rate_form', + 'add_new_rate', + 'save_new_rate' ], 'form' => [ 'url', 'active', 'interest_date', 'title', + 'date', 'book_date', 'process_date', 'due_date', @@ -292,6 +298,7 @@ return [ 'webhook_delivery', 'from_currency_to_currency', 'to_currency_from_currency', + 'rate', ], 'list' => [ 'active', diff --git a/resources/assets/v1/src/components/exchange-rates/Rates.vue b/resources/assets/v1/src/components/exchange-rates/Rates.vue index 053deb40ea..04678f0eb4 100644 --- a/resources/assets/v1/src/components/exchange-rates/Rates.vue +++ b/resources/assets/v1/src/components/exchange-rates/Rates.vue @@ -74,7 +74,7 @@ v-bind:placeholder="$t('firefly.date')" v-bind:title="$t('firefly.date')" > - + @@ -82,10 +82,18 @@ - - update + delete +
+ + +
@@ -94,6 +102,45 @@ +
+
+
+
+
+

{{ $t('firefly.add_new_rate') }}

+
+
+

+ +

+
+ +
+ +
+
+
+ +
+ +

+ +

+
+
+
+ +
+ +
+
+