First basic code for currency exchange rate routines.

This commit is contained in:
James Cole
2017-04-13 20:47:59 +02:00
parent 8db96025a3
commit 994542c75d
11 changed files with 397 additions and 32 deletions

View File

@@ -0,0 +1,60 @@
<?php
/**
* ExchangeController.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Json;
use Carbon\Carbon;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use FireflyIII\Services\Currency\ExchangeRateInterface;
use Illuminate\Http\Request;
use Response;
/**
* Class ExchangeController
*
* @package FireflyIII\Http\Controllers\Json
*/
class ExchangeController extends Controller
{
/**
* @param Request $request
* @param TransactionCurrency $fromCurrency
* @param TransactionCurrency $toCurrency
* @param Carbon $date
*/
public function getRate(Request $request, TransactionCurrency $fromCurrency, TransactionCurrency $toCurrency, Carbon $date)
{
/** @var CurrencyRepositoryInterface $repository */
$repository = app(CurrencyRepositoryInterface::class);
$rate = $repository->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);
}
}

View File

@@ -0,0 +1,53 @@
<?php
/**
* CurrencyExchange.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* Class CurrencyExchange
*
* @package FireflyIII\Models
*/
class CurrencyExchangeRate extends Model
{
protected $dates = ['created_at', 'updated_at', 'date'];
/**
* @return BelongsTo
*/
public function fromCurrency(): BelongsTo
{
return $this->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);
}
}

View File

@@ -14,6 +14,8 @@ declare(strict_types=1);
namespace FireflyIII\Repositories\Currency; namespace FireflyIII\Repositories\Currency;
use Carbon\Carbon;
use FireflyIII\Models\CurrencyExchangeRate;
use FireflyIII\Models\Preference; use FireflyIII\Models\Preference;
use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionCurrency;
use FireflyIII\User; use FireflyIII\User;
@@ -178,6 +180,28 @@ class CurrencyRepository implements CurrencyRepositoryInterface
return $preferred; 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 * @param User $user
*/ */

View File

@@ -14,6 +14,8 @@ declare(strict_types=1);
namespace FireflyIII\Repositories\Currency; namespace FireflyIII\Repositories\Currency;
use Carbon\Carbon;
use FireflyIII\Models\CurrencyExchangeRate;
use FireflyIII\Models\Preference; use FireflyIII\Models\Preference;
use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionCurrency;
use FireflyIII\User; use FireflyIII\User;
@@ -95,6 +97,15 @@ interface CurrencyRepositoryInterface
*/ */
public function getCurrencyByPreference(Preference $preference): TransactionCurrency; 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 * @param User $user
*/ */

View File

@@ -0,0 +1,38 @@
<?php
/**
* ExchangeRateInterface.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Currency;
use Carbon\Carbon;
use FireflyIII\Models\CurrencyExchangeRate;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\User;
interface ExchangeRateInterface
{
/**
* @param TransactionCurrency $fromCurrency
* @param TransactionCurrency $toCurrency
* @param Carbon $date
*
* @return CurrencyExchangeRate
*/
public function getRate(TransactionCurrency $fromCurrency, TransactionCurrency $toCurrency, Carbon $date): CurrencyExchangeRate;
/**
* @param User $user
*
* @return mixed
*/
public function setUser(User $user);
}

View File

@@ -0,0 +1,69 @@
<?php
/**
* FixerIO.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Currency;
use Carbon\Carbon;
use FireflyIII\Models\CurrencyExchangeRate;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\User;
use Log;
use Requests;
/**
* Class FixerIO
*
* @package FireflyIII\Services\Currency
*/
class FixerIO implements ExchangeRateInterface
{
/** @var User */
protected $user;
public function getRate(TransactionCurrency $fromCurrency, TransactionCurrency $toCurrency, Carbon $date): CurrencyExchangeRate
{
$uri = sprintf('https://api.fixer.io/%s?base=%s&symbols=%s', $date->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;
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* CurrencyCode.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Support\Binder;
use FireflyIII\Models\TransactionCurrency;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Class CurrencyCode
*
* @package FireflyIII\Support\Binder
*/
class CurrencyCode implements BinderInterface
{
/**
* @param $value
* @param $route
*
* @return mixed
*/
public static function routeBinder($value, $route)
{
$currency = TransactionCurrency::where('code', $value)->first();
if (!is_null($currency)) {
return $currency;
}
throw new NotFoundHttpException;
}
}

View File

@@ -15,6 +15,7 @@ declare(strict_types=1);
namespace FireflyIII; namespace FireflyIII;
use FireflyIII\Events\RequestedNewPassword; use FireflyIII\Events\RequestedNewPassword;
use FireflyIII\Models\CurrencyExchangeRate;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Database\Eloquent\Relations\HasManyThrough;
@@ -119,6 +120,14 @@ class User extends Authenticatable
return $this->hasMany('FireflyIII\Models\Category'); return $this->hasMany('FireflyIII\Models\Category');
} }
/**
* @return HasMany
*/
public function currencyExchangeRates(): HasMany
{
return $this->hasMany(CurrencyExchangeRate::class);
}
/** /**
* @return HasMany * @return HasMany
*/ */

View File

@@ -18,29 +18,29 @@ declare(strict_types=1);
*/ */
return [ return [
'configuration' => [ 'configuration' => [
'single_user_mode' => true, 'single_user_mode' => true,
'is_demo_site' => false, 'is_demo_site' => false,
], ],
'encryption' => (is_null(env('USE_ENCRYPTION')) || env('USE_ENCRYPTION') === true), 'encryption' => (is_null(env('USE_ENCRYPTION')) || env('USE_ENCRYPTION') === true),
'version' => '4.3.8', 'version' => '4.3.8',
'maxUploadSize' => 5242880, 'maxUploadSize' => 5242880,
'allowedMimes' => ['image/png', 'image/jpeg', 'application/pdf'], 'allowedMimes' => ['image/png', 'image/jpeg', 'application/pdf'],
'list_length' => 10, 'list_length' => 10,
'export_formats' => [ 'export_formats' => [
'csv' => 'FireflyIII\Export\Exporter\CsvExporter', 'csv' => 'FireflyIII\Export\Exporter\CsvExporter',
], ],
'import_formats' => [ 'import_formats' => [
'csv' => 'FireflyIII\Import\Importer\CsvImporter', 'csv' => 'FireflyIII\Import\Importer\CsvImporter',
], ],
'default_export_format' => 'csv', 'default_export_format' => 'csv',
'default_import_format' => 'csv', 'default_import_format' => 'csv',
'bill_periods' => ['weekly', 'monthly', 'quarterly', 'half-year', 'yearly'], 'bill_periods' => ['weekly', 'monthly', 'quarterly', 'half-year', 'yearly'],
'accountRoles' => ['defaultAsset', 'sharedAsset', 'savingAsset', 'ccAsset',], 'accountRoles' => ['defaultAsset', 'sharedAsset', 'savingAsset', 'ccAsset',],
'ccTypes' => [ 'ccTypes' => [
'monthlyFull' => 'Full payment every month', 'monthlyFull' => 'Full payment every month',
], ],
'range_to_repeat_freq' => [ 'range_to_repeat_freq' => [
'1D' => 'weekly', '1D' => 'weekly',
'1W' => 'weekly', '1W' => 'weekly',
'1M' => 'monthly', '1M' => 'monthly',
@@ -49,14 +49,14 @@ return [
'1Y' => 'yearly', '1Y' => 'yearly',
'custom' => 'custom', 'custom' => 'custom',
], ],
'subTitlesByIdentifier' => 'subTitlesByIdentifier' =>
[ [
'asset' => 'Asset accounts', 'asset' => 'Asset accounts',
'expense' => 'Expense accounts', 'expense' => 'Expense accounts',
'revenue' => 'Revenue accounts', 'revenue' => 'Revenue accounts',
'cash' => 'Cash accounts', 'cash' => 'Cash accounts',
], ],
'subIconsByIdentifier' => 'subIconsByIdentifier' =>
[ [
'asset' => 'fa-money', 'asset' => 'fa-money',
'Asset account' => 'fa-money', 'Asset account' => 'fa-money',
@@ -70,14 +70,14 @@ return [
'import' => 'fa-download', 'import' => 'fa-download',
'Import account' => 'fa-download', 'Import account' => 'fa-download',
], ],
'accountTypesByIdentifier' => 'accountTypesByIdentifier' =>
[ [
'asset' => ['Default account', 'Asset account'], 'asset' => ['Default account', 'Asset account'],
'expense' => ['Expense account', 'Beneficiary account'], 'expense' => ['Expense account', 'Beneficiary account'],
'revenue' => ['Revenue account'], 'revenue' => ['Revenue account'],
'import' => ['Import account'], 'import' => ['Import account'],
], ],
'accountTypeByIdentifier' => 'accountTypeByIdentifier' =>
[ [
'asset' => 'Asset account', 'asset' => 'Asset account',
'expense' => 'Expense account', 'expense' => 'Expense account',
@@ -86,7 +86,7 @@ return [
'initial' => 'Initial balance account', 'initial' => 'Initial balance account',
'import' => 'Import account', 'import' => 'Import account',
], ],
'shortNamesByFullName' => 'shortNamesByFullName' =>
[ [
'Default account' => 'asset', 'Default account' => 'asset',
'Asset account' => 'asset', 'Asset account' => 'asset',
@@ -96,7 +96,7 @@ return [
'Revenue account' => 'revenue', 'Revenue account' => 'revenue',
'Cash account' => 'cash', 'Cash account' => 'cash',
], ],
'languages' => [ 'languages' => [
'de_DE' => ['name_locale' => 'Deutsch', 'name_english' => 'German', 'complete' => true], 'de_DE' => ['name_locale' => 'Deutsch', 'name_english' => 'German', 'complete' => true],
'en_US' => ['name_locale' => 'English', 'name_english' => 'English', 'complete' => true], 'en_US' => ['name_locale' => 'English', 'name_english' => 'English', 'complete' => true],
'es_ES' => ['name_locale' => 'Español', 'name_english' => 'Spanish', 'complete' => false], '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-HK' => ['name_locale' => '繁體中文(香港)', 'name_english' => 'Chinese Traditional, Hong Kong', 'complete' => false],
'zh-TW' => ['name_locale' => '正體中文', 'name_english' => 'Chinese Traditional', 'complete' => false], 'zh-TW' => ['name_locale' => '正體中文', 'name_english' => 'Chinese Traditional', 'complete' => false],
], ],
'transactionTypesByWhat' => [ 'transactionTypesByWhat' => [
'expenses' => ['Withdrawal'], 'expenses' => ['Withdrawal'],
'withdrawal' => ['Withdrawal'], 'withdrawal' => ['Withdrawal'],
'revenue' => ['Deposit'], 'revenue' => ['Deposit'],
@@ -117,7 +117,7 @@ return [
'transfer' => ['Transfer'], 'transfer' => ['Transfer'],
'transfers' => ['Transfer'], 'transfers' => ['Transfer'],
], ],
'transactionIconsByWhat' => [ 'transactionIconsByWhat' => [
'expenses' => 'fa-long-arrow-left', 'expenses' => 'fa-long-arrow-left',
'withdrawal' => 'fa-long-arrow-left', 'withdrawal' => 'fa-long-arrow-left',
'revenue' => 'fa-long-arrow-right', 'revenue' => 'fa-long-arrow-right',
@@ -126,7 +126,7 @@ return [
'transfers' => 'fa-exchange', 'transfers' => 'fa-exchange',
], ],
'bindables' => [ 'bindables' => [
'account' => 'FireflyIII\Models\Account', 'account' => 'FireflyIII\Models\Account',
'attachment' => 'FireflyIII\Models\Attachment', 'attachment' => 'FireflyIII\Models\Attachment',
'bill' => 'FireflyIII\Models\Bill', 'bill' => 'FireflyIII\Models\Bill',
@@ -134,6 +134,8 @@ return [
'category' => 'FireflyIII\Models\Category', 'category' => 'FireflyIII\Models\Category',
'transaction_type' => 'FireflyIII\Models\TransactionType', 'transaction_type' => 'FireflyIII\Models\TransactionType',
'currency' => 'FireflyIII\Models\TransactionCurrency', 'currency' => 'FireflyIII\Models\TransactionCurrency',
'fromCurrencyCode' => 'FireflyIII\Support\Binder\CurrencyCode',
'toCurrencyCode' => 'FireflyIII\Support\Binder\CurrencyCode',
'limitrepetition' => 'FireflyIII\Models\LimitRepetition', 'limitrepetition' => 'FireflyIII\Models\LimitRepetition',
'budgetlimit' => 'FireflyIII\Models\BudgetLimit', 'budgetlimit' => 'FireflyIII\Models\BudgetLimit',
'piggyBank' => 'FireflyIII\Models\PiggyBank', 'piggyBank' => 'FireflyIII\Models\PiggyBank',
@@ -152,7 +154,7 @@ return [
'start_date' => 'FireflyIII\Support\Binder\Date', 'start_date' => 'FireflyIII\Support\Binder\Date',
'end_date' => 'FireflyIII\Support\Binder\Date', 'end_date' => 'FireflyIII\Support\Binder\Date',
], ],
'rule-triggers' => [ 'rule-triggers' => [
'user_action' => 'FireflyIII\Rules\Triggers\UserAction', 'user_action' => 'FireflyIII\Rules\Triggers\UserAction',
'from_account_starts' => 'FireflyIII\Rules\Triggers\FromAccountStarts', 'from_account_starts' => 'FireflyIII\Rules\Triggers\FromAccountStarts',
'from_account_ends' => 'FireflyIII\Rules\Triggers\FromAccountEnds', 'from_account_ends' => 'FireflyIII\Rules\Triggers\FromAccountEnds',
@@ -174,7 +176,7 @@ return [
'budget_is' => 'FireflyIII\Rules\Triggers\BudgetIs', 'budget_is' => 'FireflyIII\Rules\Triggers\BudgetIs',
'tag_is' => 'FireflyIII\Rules\Triggers\TagIs', 'tag_is' => 'FireflyIII\Rules\Triggers\TagIs',
], ],
'rule-actions' => [ 'rule-actions' => [
'set_category' => 'FireflyIII\Rules\Actions\SetCategory', 'set_category' => 'FireflyIII\Rules\Actions\SetCategory',
'clear_category' => 'FireflyIII\Rules\Actions\ClearCategory', 'clear_category' => 'FireflyIII\Rules\Actions\ClearCategory',
'set_budget' => 'FireflyIII\Rules\Actions\SetBudget', 'set_budget' => 'FireflyIII\Rules\Actions\SetBudget',
@@ -189,7 +191,7 @@ return [
'set_source_account' => 'FireflyIII\Rules\Actions\SetSourceAccount', 'set_source_account' => 'FireflyIII\Rules\Actions\SetSourceAccount',
'set_destination_account' => 'FireflyIII\Rules\Actions\SetDestinationAccount', 'set_destination_account' => 'FireflyIII\Rules\Actions\SetDestinationAccount',
], ],
'rule-actions-text' => [ 'rule-actions-text' => [
'set_category', 'set_category',
'set_budget', 'set_budget',
'add_tag', 'add_tag',
@@ -198,13 +200,19 @@ return [
'append_description', 'append_description',
'prepend_description', 'prepend_description',
], ],
'test-triggers' => [ 'test-triggers' => [
'limit' => 10, 'limit' => 10,
'range' => 200, 'range' => 200,
], ],
'default_currency' => 'EUR', 'default_currency' => 'EUR',
'default_language' => 'en_US', 'default_language' => 'en_US',
'search_modifiers' => ['amount_is', 'amount', 'amount_max', 'amount_min', 'amount_less', 'amount_more', 'source', 'destination', 'category', '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'], 'budget', 'bill', 'type', 'date', 'date_before', 'date_after', 'on', 'before', 'after'],
// tag notes has_attachments // tag notes has_attachments
'currency_exchange_services' => [
'fixerio' => 'FireflyIII\Services\Currency\FixerIO',
'openexchangerates' => 'FireflyIII\Services\Currency\OpenExchangeRates',
],
'preferred_exchange_service' => 'fixerio',
]; ];

View File

@@ -0,0 +1,51 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
/**
* Class ChangesForV440
*/
class ChangesForV440 extends Migration
{
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
if (Schema::hasTable('currency_exchange_rates')) {
Schema::drop('currency_exchange_rates');
}
}
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (!Schema::hasTable('currency_exchange_rates')) {
Schema::create(
'currency_exchange_rates', function (Blueprint $table) {
$table->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');
}
);
}
}
}

View File

@@ -414,7 +414,7 @@ Route::group(
); );
/** /**
* JSON Controller * JSON Controller(s)
*/ */
Route::group( Route::group(
['middleware' => 'user-full-auth', 'prefix' => 'json', 'as' => 'json.'], function () { ['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']); Route::post('end-tour', ['uses' => 'JsonController@endTour', 'as' => 'end-tour']);
// currency conversion:
Route::get('rate/{fromCurrencyCode}/{toCurrencyCode}/{date}', ['uses' => 'Json\ExchangeController@getRate', 'as' => 'rate']);
} }
); );