From 994542c75d11f5732747e4c1d20fc996a6f1671a Mon Sep 17 00:00:00 2001 From: James Cole Date: Thu, 13 Apr 2017 20:47:59 +0200 Subject: [PATCH] First basic code for currency exchange rate routines. --- .../Controllers/Json/ExchangeController.php | 60 ++++++++++++++++ app/Models/CurrencyExchangeRate.php | 53 ++++++++++++++ .../Currency/CurrencyRepository.php | 24 +++++++ .../Currency/CurrencyRepositoryInterface.php | 11 +++ .../Currency/ExchangeRateInterface.php | 38 ++++++++++ app/Services/Currency/FixerIO.php | 69 ++++++++++++++++++ app/Support/Binder/CurrencyCode.php | 39 +++++++++++ app/User.php | 9 +++ config/firefly.php | 70 +++++++++++-------- .../2017_04_13_163623_changes_for_v440.php | 51 ++++++++++++++ routes/web.php | 5 +- 11 files changed, 397 insertions(+), 32 deletions(-) create mode 100644 app/Http/Controllers/Json/ExchangeController.php create mode 100644 app/Models/CurrencyExchangeRate.php create mode 100644 app/Services/Currency/ExchangeRateInterface.php create mode 100644 app/Services/Currency/FixerIO.php create mode 100644 app/Support/Binder/CurrencyCode.php create mode 100644 database/migrations/2017_04_13_163623_changes_for_v440.php diff --git a/app/Http/Controllers/Json/ExchangeController.php b/app/Http/Controllers/Json/ExchangeController.php new file mode 100644 index 0000000000..b37f5e392c --- /dev/null +++ b/app/Http/Controllers/Json/ExchangeController.php @@ -0,0 +1,60 @@ +getExchangeRate($fromCurrency, $toCurrency, $date); + $amount = null; + if (is_null($rate->id)) { + $preferred = env('EXCHANGE_RATE_SERVICE', config('firefly.preferred_exchange_service')); + $class = config('firefly.currency_exchange_services.' . $preferred); + /** @var ExchangeRateInterface $object */ + $object = app($class); + $object->setUser(auth()->user()); + $rate = $object->getRate($fromCurrency, $toCurrency, $date); + } + $return = $rate->toArray(); + $return['amount'] = null; + if (!is_null($request->get('amount'))) { + // assume amount is in "from" currency: + $return['amount'] = bcmul($request->get('amount'), $rate->rate); + } + + return Response::json($return); + } + +} \ No newline at end of file diff --git a/app/Models/CurrencyExchangeRate.php b/app/Models/CurrencyExchangeRate.php new file mode 100644 index 0000000000..2c7d12558c --- /dev/null +++ b/app/Models/CurrencyExchangeRate.php @@ -0,0 +1,53 @@ +belongsTo(TransactionCurrency::class, 'from_currency_id'); + } + + /** + * @return BelongsTo + */ + public function toCurrency(): BelongsTo + { + return $this->belongsTo(TransactionCurrency::class, 'to_currency_id'); + } + + /** + * @return BelongsTo + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + +} \ No newline at end of file diff --git a/app/Repositories/Currency/CurrencyRepository.php b/app/Repositories/Currency/CurrencyRepository.php index fd69d254cb..c1b947e805 100644 --- a/app/Repositories/Currency/CurrencyRepository.php +++ b/app/Repositories/Currency/CurrencyRepository.php @@ -14,6 +14,8 @@ declare(strict_types=1); namespace FireflyIII\Repositories\Currency; +use Carbon\Carbon; +use FireflyIII\Models\CurrencyExchangeRate; use FireflyIII\Models\Preference; use FireflyIII\Models\TransactionCurrency; use FireflyIII\User; @@ -178,6 +180,28 @@ class CurrencyRepository implements CurrencyRepositoryInterface return $preferred; } + /** + * @param TransactionCurrency $fromCurrency + * @param TransactionCurrency $toCurrency + * @param Carbon $date + * + * @return CurrencyExchangeRate + */ + public function getExchangeRate(TransactionCurrency $fromCurrency, TransactionCurrency $toCurrency, Carbon $date): CurrencyExchangeRate + { + $rate = $this->user->currencyExchangeRates() + ->where('from_currency_id', $fromCurrency->id) + ->where('to_currency_id', $toCurrency->id) + ->where('date', $date->format('Y-m-d'))->first(); + if (!is_null($rate)) { + return $rate; + } + + return new CurrencyExchangeRate; + + + } + /** * @param User $user */ diff --git a/app/Repositories/Currency/CurrencyRepositoryInterface.php b/app/Repositories/Currency/CurrencyRepositoryInterface.php index 0b9431cc5f..6c2ccdd61b 100644 --- a/app/Repositories/Currency/CurrencyRepositoryInterface.php +++ b/app/Repositories/Currency/CurrencyRepositoryInterface.php @@ -14,6 +14,8 @@ declare(strict_types=1); namespace FireflyIII\Repositories\Currency; +use Carbon\Carbon; +use FireflyIII\Models\CurrencyExchangeRate; use FireflyIII\Models\Preference; use FireflyIII\Models\TransactionCurrency; use FireflyIII\User; @@ -95,6 +97,15 @@ interface CurrencyRepositoryInterface */ public function getCurrencyByPreference(Preference $preference): TransactionCurrency; + /** + * @param TransactionCurrency $fromCurrency + * @param TransactionCurrency $toCurrency + * @param Carbon $date + * + * @return CurrencyExchangeRate + */ + public function getExchangeRate(TransactionCurrency $fromCurrency, TransactionCurrency $toCurrency, Carbon $date): CurrencyExchangeRate; + /** * @param User $user */ diff --git a/app/Services/Currency/ExchangeRateInterface.php b/app/Services/Currency/ExchangeRateInterface.php new file mode 100644 index 0000000000..a37133db9a --- /dev/null +++ b/app/Services/Currency/ExchangeRateInterface.php @@ -0,0 +1,38 @@ +format('Y-m-d'), $fromCurrency->code, $toCurrency->code); + $result = Requests::get($uri); + $rate = 1.0; + $content = null; + if ($result->status_code !== 200) { + Log::error(sprintf('Something went wrong. Received error code %d and body "%s" from FixerIO.', $result->status_code, $result->body)); + } + // get rate from body: + if ($result->status_code === 200) { + $content = json_decode($result->body, true); + } + if (!is_null($content)) { + $code = $toCurrency->code; + $rate = isset($content['rates'][$code]) ? $content['rates'][$code] : '1'; + } + + // create new currency exchange rate object: + $exchangeRate = new CurrencyExchangeRate; + $exchangeRate->user()->associate($this->user); + $exchangeRate->fromCurrency()->associate($fromCurrency); + $exchangeRate->toCurrency()->associate($toCurrency); + $exchangeRate->date = $date; + $exchangeRate->rate = $rate; + $exchangeRate->save(); + + return $exchangeRate; + } + + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + } +} \ No newline at end of file diff --git a/app/Support/Binder/CurrencyCode.php b/app/Support/Binder/CurrencyCode.php new file mode 100644 index 0000000000..4616ce5ef2 --- /dev/null +++ b/app/Support/Binder/CurrencyCode.php @@ -0,0 +1,39 @@ +first(); + if (!is_null($currency)) { + return $currency; + } + throw new NotFoundHttpException; + } +} \ No newline at end of file diff --git a/app/User.php b/app/User.php index a40fa3c4e3..f5ee28f920 100644 --- a/app/User.php +++ b/app/User.php @@ -15,6 +15,7 @@ declare(strict_types=1); namespace FireflyIII; use FireflyIII\Events\RequestedNewPassword; +use FireflyIII\Models\CurrencyExchangeRate; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasManyThrough; @@ -119,6 +120,14 @@ class User extends Authenticatable return $this->hasMany('FireflyIII\Models\Category'); } + /** + * @return HasMany + */ + public function currencyExchangeRates(): HasMany + { + return $this->hasMany(CurrencyExchangeRate::class); + } + /** * @return HasMany */ diff --git a/config/firefly.php b/config/firefly.php index 83aa8d19e2..879c45e0ee 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -18,29 +18,29 @@ declare(strict_types=1); */ return [ - 'configuration' => [ + 'configuration' => [ 'single_user_mode' => true, 'is_demo_site' => false, ], - 'encryption' => (is_null(env('USE_ENCRYPTION')) || env('USE_ENCRYPTION') === true), - 'version' => '4.3.8', - 'maxUploadSize' => 5242880, - 'allowedMimes' => ['image/png', 'image/jpeg', 'application/pdf'], - 'list_length' => 10, - 'export_formats' => [ + 'encryption' => (is_null(env('USE_ENCRYPTION')) || env('USE_ENCRYPTION') === true), + 'version' => '4.3.8', + 'maxUploadSize' => 5242880, + 'allowedMimes' => ['image/png', 'image/jpeg', 'application/pdf'], + 'list_length' => 10, + 'export_formats' => [ 'csv' => 'FireflyIII\Export\Exporter\CsvExporter', ], - 'import_formats' => [ + 'import_formats' => [ 'csv' => 'FireflyIII\Import\Importer\CsvImporter', ], - 'default_export_format' => 'csv', - 'default_import_format' => 'csv', - 'bill_periods' => ['weekly', 'monthly', 'quarterly', 'half-year', 'yearly'], - 'accountRoles' => ['defaultAsset', 'sharedAsset', 'savingAsset', 'ccAsset',], - 'ccTypes' => [ + 'default_export_format' => 'csv', + 'default_import_format' => 'csv', + 'bill_periods' => ['weekly', 'monthly', 'quarterly', 'half-year', 'yearly'], + 'accountRoles' => ['defaultAsset', 'sharedAsset', 'savingAsset', 'ccAsset',], + 'ccTypes' => [ 'monthlyFull' => 'Full payment every month', ], - 'range_to_repeat_freq' => [ + 'range_to_repeat_freq' => [ '1D' => 'weekly', '1W' => 'weekly', '1M' => 'monthly', @@ -49,14 +49,14 @@ return [ '1Y' => 'yearly', 'custom' => 'custom', ], - 'subTitlesByIdentifier' => + 'subTitlesByIdentifier' => [ 'asset' => 'Asset accounts', 'expense' => 'Expense accounts', 'revenue' => 'Revenue accounts', 'cash' => 'Cash accounts', ], - 'subIconsByIdentifier' => + 'subIconsByIdentifier' => [ 'asset' => 'fa-money', 'Asset account' => 'fa-money', @@ -70,14 +70,14 @@ return [ 'import' => 'fa-download', 'Import account' => 'fa-download', ], - 'accountTypesByIdentifier' => + 'accountTypesByIdentifier' => [ 'asset' => ['Default account', 'Asset account'], 'expense' => ['Expense account', 'Beneficiary account'], 'revenue' => ['Revenue account'], 'import' => ['Import account'], ], - 'accountTypeByIdentifier' => + 'accountTypeByIdentifier' => [ 'asset' => 'Asset account', 'expense' => 'Expense account', @@ -86,7 +86,7 @@ return [ 'initial' => 'Initial balance account', 'import' => 'Import account', ], - 'shortNamesByFullName' => + 'shortNamesByFullName' => [ 'Default account' => 'asset', 'Asset account' => 'asset', @@ -96,7 +96,7 @@ return [ 'Revenue account' => 'revenue', 'Cash account' => 'cash', ], - 'languages' => [ + 'languages' => [ 'de_DE' => ['name_locale' => 'Deutsch', 'name_english' => 'German', 'complete' => true], 'en_US' => ['name_locale' => 'English', 'name_english' => 'English', 'complete' => true], 'es_ES' => ['name_locale' => 'Español', 'name_english' => 'Spanish', 'complete' => false], @@ -109,7 +109,7 @@ return [ 'zh-HK' => ['name_locale' => '繁體中文(香港)', 'name_english' => 'Chinese Traditional, Hong Kong', 'complete' => false], 'zh-TW' => ['name_locale' => '正體中文', 'name_english' => 'Chinese Traditional', 'complete' => false], ], - 'transactionTypesByWhat' => [ + 'transactionTypesByWhat' => [ 'expenses' => ['Withdrawal'], 'withdrawal' => ['Withdrawal'], 'revenue' => ['Deposit'], @@ -117,7 +117,7 @@ return [ 'transfer' => ['Transfer'], 'transfers' => ['Transfer'], ], - 'transactionIconsByWhat' => [ + 'transactionIconsByWhat' => [ 'expenses' => 'fa-long-arrow-left', 'withdrawal' => 'fa-long-arrow-left', 'revenue' => 'fa-long-arrow-right', @@ -126,7 +126,7 @@ return [ 'transfers' => 'fa-exchange', ], - 'bindables' => [ + 'bindables' => [ 'account' => 'FireflyIII\Models\Account', 'attachment' => 'FireflyIII\Models\Attachment', 'bill' => 'FireflyIII\Models\Bill', @@ -134,6 +134,8 @@ return [ 'category' => 'FireflyIII\Models\Category', 'transaction_type' => 'FireflyIII\Models\TransactionType', 'currency' => 'FireflyIII\Models\TransactionCurrency', + 'fromCurrencyCode' => 'FireflyIII\Support\Binder\CurrencyCode', + 'toCurrencyCode' => 'FireflyIII\Support\Binder\CurrencyCode', 'limitrepetition' => 'FireflyIII\Models\LimitRepetition', 'budgetlimit' => 'FireflyIII\Models\BudgetLimit', 'piggyBank' => 'FireflyIII\Models\PiggyBank', @@ -152,7 +154,7 @@ return [ 'start_date' => 'FireflyIII\Support\Binder\Date', 'end_date' => 'FireflyIII\Support\Binder\Date', ], - 'rule-triggers' => [ + 'rule-triggers' => [ 'user_action' => 'FireflyIII\Rules\Triggers\UserAction', 'from_account_starts' => 'FireflyIII\Rules\Triggers\FromAccountStarts', 'from_account_ends' => 'FireflyIII\Rules\Triggers\FromAccountEnds', @@ -174,7 +176,7 @@ return [ 'budget_is' => 'FireflyIII\Rules\Triggers\BudgetIs', 'tag_is' => 'FireflyIII\Rules\Triggers\TagIs', ], - 'rule-actions' => [ + 'rule-actions' => [ 'set_category' => 'FireflyIII\Rules\Actions\SetCategory', 'clear_category' => 'FireflyIII\Rules\Actions\ClearCategory', 'set_budget' => 'FireflyIII\Rules\Actions\SetBudget', @@ -189,7 +191,7 @@ return [ 'set_source_account' => 'FireflyIII\Rules\Actions\SetSourceAccount', 'set_destination_account' => 'FireflyIII\Rules\Actions\SetDestinationAccount', ], - 'rule-actions-text' => [ + 'rule-actions-text' => [ 'set_category', 'set_budget', 'add_tag', @@ -198,13 +200,19 @@ return [ 'append_description', 'prepend_description', ], - 'test-triggers' => [ + 'test-triggers' => [ 'limit' => 10, 'range' => 200, ], - 'default_currency' => 'EUR', - 'default_language' => 'en_US', - 'search_modifiers' => ['amount_is', 'amount', 'amount_max', 'amount_min', 'amount_less', 'amount_more', 'source', 'destination', 'category', - 'budget', 'bill', 'type', 'date', 'date_before', 'date_after', 'on', 'before', 'after'], + 'default_currency' => 'EUR', + 'default_language' => 'en_US', + 'search_modifiers' => ['amount_is', 'amount', 'amount_max', 'amount_min', 'amount_less', 'amount_more', 'source', 'destination', 'category', + 'budget', 'bill', 'type', 'date', 'date_before', 'date_after', 'on', 'before', 'after'], // tag notes has_attachments + 'currency_exchange_services' => [ + 'fixerio' => 'FireflyIII\Services\Currency\FixerIO', + 'openexchangerates' => 'FireflyIII\Services\Currency\OpenExchangeRates', + ], + 'preferred_exchange_service' => 'fixerio', + ]; diff --git a/database/migrations/2017_04_13_163623_changes_for_v440.php b/database/migrations/2017_04_13_163623_changes_for_v440.php new file mode 100644 index 0000000000..6342fc46d1 --- /dev/null +++ b/database/migrations/2017_04_13_163623_changes_for_v440.php @@ -0,0 +1,51 @@ +increments('id'); + $table->timestamps(); + $table->softDeletes(); + $table->integer('user_id', false, true); + $table->integer('from_currency_id', false, true); + $table->integer('to_currency_id', false, true); + $table->date('date'); + $table->decimal('rate', 22, 12); + $table->decimal('user_rate', 22, 12)->nullable(); + + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->foreign('from_currency_id')->references('id')->on('transaction_currencies')->onDelete('cascade'); + $table->foreign('to_currency_id')->references('id')->on('transaction_currencies')->onDelete('cascade'); + } + ); + } + + } +} diff --git a/routes/web.php b/routes/web.php index 0806fc6600..09b54beb2a 100755 --- a/routes/web.php +++ b/routes/web.php @@ -414,7 +414,7 @@ Route::group( ); /** - * JSON Controller + * JSON Controller(s) */ Route::group( ['middleware' => 'user-full-auth', 'prefix' => 'json', 'as' => 'json.'], function () { @@ -437,6 +437,9 @@ Route::group( Route::post('end-tour', ['uses' => 'JsonController@endTour', 'as' => 'end-tour']); + // currency conversion: + Route::get('rate/{fromCurrencyCode}/{toCurrencyCode}/{date}', ['uses' => 'Json\ExchangeController@getRate', 'as' => 'rate']); + } );