New feature: Exchange rates API integration from Czech National Bank
This commit is contained in:
parent
3b63cd4cb3
commit
45c80dcb9a
|
|
@ -70,3 +70,4 @@ GOCARDLESS_CLIENT_ID=
|
|||
GOCARDLESS_CLIENT_SECRET=
|
||||
|
||||
OPENEXCHANGE_APP_ID=
|
||||
USE_CNB_EXCHANGE_RATES=
|
||||
|
|
@ -30,6 +30,7 @@ use App\Jobs\Ninja\CompanySizeCheck;
|
|||
use App\Jobs\Ninja\SystemMaintenance;
|
||||
use App\Jobs\Quote\QuoteCheckExpired;
|
||||
use App\Jobs\Util\UpdateExchangeRates;
|
||||
use App\Jobs\Util\UpdateCNBExchangeRates;
|
||||
use App\Jobs\Ninja\BankTransactionSync;
|
||||
use App\Jobs\Cron\RecurringExpensesCron;
|
||||
use App\Jobs\Cron\RecurringInvoicesCron;
|
||||
|
|
@ -108,6 +109,9 @@ class Kernel extends ConsoleKernel
|
|||
/* Pulls in the latest exchange rates */
|
||||
$schedule->job(new UpdateExchangeRates())->dailyAt('23:30')->withoutOverlapping()->name('exchange-rate-job')->onOneServer();
|
||||
|
||||
/* Pulls in the latest exchange rates from CNB, gets updated at 14:30 CE(S)T daily - since we use UTC time, 14:00 works best */
|
||||
$schedule->job(new UpdateCNBExchangeRates())->dailyAt('14:00')->withoutOverlapping()->name('cnb-exchange-rate-job')->onOneServer();
|
||||
|
||||
/* Runs cleanup code for subscriptions */
|
||||
$schedule->job(new SubscriptionCron())->hourlyAt(1)->withoutOverlapping()->name('subscription-job')->onOneServer();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Jobs\Util;
|
||||
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Models\Currency;
|
||||
use App\Models\Company;
|
||||
use GuzzleHttp\Client;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class UpdateCNBExchangeRates implements ShouldQueue
|
||||
{
|
||||
use Dispatchable;
|
||||
use InteractsWithQueue;
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
public function handle(): void
|
||||
{
|
||||
info('updating currencies from Czech National Bank');
|
||||
|
||||
//Even if the CNB flag is enabled in the config, if the OpenExchange API key is set, we will not interfere
|
||||
if (!empty(config('ninja.currency_converter_api_key')) || config('ninja.currency_converter_use_cnb') !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cc_endpoint = 'https://api.cnb.cz/cnbapi/exrates/daily?lang=EN';
|
||||
$client = new Client();
|
||||
$response = $client->get($cc_endpoint);
|
||||
$currency_api = json_decode($response->getBody());
|
||||
|
||||
//parse the JSON
|
||||
$rows = $currency_api->rates;
|
||||
$perCzk = [];
|
||||
foreach ($rows as $row) {
|
||||
// ensure we divide by the 'amount' (CNB may quote 100 JPY = 17.6 CZK)
|
||||
$perCzk[$row->currencyCode] = 1/$row->rate / $row->amount; //we need inverse values
|
||||
}
|
||||
// CZK is not in the feed, but per CZK is obviously 1
|
||||
$perCzk['CZK'] = 1.0;
|
||||
|
||||
|
||||
if (config('ninja.db.multi_db_enabled')) {
|
||||
foreach (MultiDB::$dbs as $db) {
|
||||
MultiDB::setDB($db);
|
||||
//Adjust the currency rates to our company rates
|
||||
//which currency is our base?
|
||||
//Based on my testing, the exchange rates are always based on first company and transformed as needed for subsequent companies, if any.
|
||||
$companyRate = $perCzk[Company::first()->currency()->code] ?? 'EUR'; //defaulting to EUR if unset.
|
||||
// now scale: per company currency = (per CZK) / (company per CZK)
|
||||
$perCompanyCurrency = [];
|
||||
foreach ($perCzk as $code => $rate) {
|
||||
$perCompanyCurrency[$code] = $rate / $companyRate;
|
||||
}
|
||||
|
||||
/* Update all currencies */
|
||||
Currency::all()->each(function ($currency) use ($perCompanyCurrency) {
|
||||
if (array_key_exists($currency->code, $perCompanyCurrency)) {
|
||||
$currency->exchange_rate = $perCompanyCurrency[$currency->code];
|
||||
$currency->save();
|
||||
}
|
||||
});
|
||||
|
||||
/* Rebuild the cache */
|
||||
$currencies = Currency::orderBy('name')->get();
|
||||
|
||||
Cache::forever('currencies', $currencies);
|
||||
}
|
||||
} else {
|
||||
//Adjust the currency rates to our company rates
|
||||
//which currency is our base?
|
||||
//Based on my testing, the exchange rates are always based on first company and transformed as needed for subsequent companies, if any.
|
||||
$companyRate = $perCzk[Company::first()->currency()->code] ?? 'EUR'; //defaulting to EUR if unset.
|
||||
// now scale: per company currency = (per CZK) / (company per CZK)
|
||||
$perCompanyCurrency = [];
|
||||
foreach ($perCzk as $code => $rate) {
|
||||
$perCompanyCurrency[$code] = $rate / $companyRate;
|
||||
}
|
||||
|
||||
/* Update all currencies */
|
||||
Currency::all()->each(function ($currency) use ($perCompanyCurrency) {
|
||||
if (array_key_exists($currency->code, $perCompanyCurrency)) {
|
||||
$currency->exchange_rate = $perCompanyCurrency[$currency->code];
|
||||
$currency->save();
|
||||
}
|
||||
});
|
||||
|
||||
/* Rebuild the cache */
|
||||
$currencies = Currency::orderBy('name')->get();
|
||||
|
||||
Cache::forever('currencies', $currencies);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -33,6 +33,7 @@ return [
|
|||
'company_id' => 0,
|
||||
'hash_salt' => env('HASH_SALT', ''),
|
||||
'currency_converter_api_key' => env('OPENEXCHANGE_APP_ID', ''),
|
||||
'currency_converter_use_cnb' => env('USE_CNB_EXCHANGE_RATES', false),
|
||||
'enabled_modules' => 65535,
|
||||
'phantomjs_key' => env('PHANTOMJS_KEY', 'a-demo-key-with-low-quota-per-ip-address'),
|
||||
'phantomjs_secret' => env('PHANTOMJS_SECRET', false),
|
||||
|
|
|
|||
Loading…
Reference in New Issue