diff --git a/README.md b/README.md index 2b60508..5e846a6 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Here is the complete list of the currently implemented services: |---------------------------------------------------------------------------|----------------------|----------------|----------------| | [Fixer](https://fixer.io) | EUR (free, no SSL), * (paid) | * | Yes | | [currencylayer](https://currencylayer.com) | USD (free), * (paid) | * | Yes | -| [exchangeratesapi](https://exchangeratesapi.io) | USD (free), * (paid) | * | Yes | +| [exchangeratesapi](https://exchangeratesapi.io) | EUR (free), * (paid) | * | Yes | | [Abstract](https://www.abstractapi.com) | * | * | No | | [coinlayer](https://coinlayer.com) | * Crypto (Limited standard currencies) | * Crypto (Limited standard currencies) | Yes | | [European Central Bank](https://www.ecb.europa.eu/home/html/index.en.html) | EUR | * | Yes | diff --git a/doc/readme.md b/doc/readme.md index 403c471..84a2ec1 100644 --- a/doc/readme.md +++ b/doc/readme.md @@ -73,7 +73,7 @@ $service = new Fixer($client, null, ['access_key' => 'YOUR_KEY']); $service = new CurrencyLayer($client, null, ['access_key' => 'access_key', 'enterprise' => false]); // Or use the exchangeratesapi.io service -$service = new ExchangeRatesApi($client, null, ['access_key' => 'access_key']); +$service = new ExchangeRatesApi($client, null, ['access_key' => 'access_key', 'enterprise' => false]); // Create Exchanger with your service $exchanger = new Exchanger($service); @@ -139,7 +139,7 @@ use Exchanger\Service\ExchangeRatesApi; $service = new Chain([ new Fixer($client, null, ['access_key' => 'YOUR_KEY']), new CurrencyLayer($client, null, ['access_key' => 'access_key', 'enterprise' => false]), - new ExchangeRatesApi($client, null, ['access_key' => 'access_key']), + new ExchangeRatesApi($client, null, ['access_key' => 'access_key', 'enterprise' => false]), ]); ``` @@ -405,7 +405,7 @@ use Exchanger\Service\AbstractApi; $service = new Chain([ new Fixer($client, null, ['access_key' => 'YOUR_KEY']), new CurrencyLayer($client, null, ['access_key' => 'access_key', 'enterprise' => false]), - new ExchangeRatesApi($client, null, ['access_key' => 'access_key']), + new ExchangeRatesApi($client, null, ['access_key' => 'access_key', 'enterprise' => false]), new AbstractApi($client, null, ['api_key' => 'api_key']), new CoinLayer($client, null, ['access_key' => 'access_key', 'paid' => false]), new EuropeanCentralBank(), diff --git a/src/Service/ExchangeRatesApi.php b/src/Service/ExchangeRatesApi.php index 7845ac2..c7a867c 100755 --- a/src/Service/ExchangeRatesApi.php +++ b/src/Service/ExchangeRatesApi.php @@ -26,15 +26,21 @@ /** * ExchangeRatesApi Service. * + * @see https://exchangeratesapi.io/documentation/ + * * @author Arjan Westdorp */ final class ExchangeRatesApi extends HttpService { use SupportsHistoricalQueries; - const LATEST_URL = 'https://api.exchangeratesapi.io/latest?base=%s&access_key=%s'; + const LATEST_URL = 'https://api.exchangeratesapi.io/latest?base=%s&access_key=%s&symbols=%s'; + + const HISTORICAL_URL = 'https://api.exchangeratesapi.io/%s?base=%s&access_key=%s&symbols=%s'; + + const FREE_LATEST_URL = 'http://api.exchangeratesapi.io/latest?access_key=%s&symbols=%s'; - const HISTORICAL_URL = 'https://api.exchangeratesapi.io/%s?base=%s&access_key=%s'; + const FREE_HISTORICAL_URL = 'http://api.exchangeratesapi.io/%s?access_key=%s&symbols=%s'; const ACCESS_KEY_OPTION = 'access_key'; @@ -46,6 +52,10 @@ public function processOptions(array &$options): void if (!isset($options[self::ACCESS_KEY_OPTION])) { throw new NonBreakingInvalidArgumentException('The "access_key" option must be provided to use exchangeratesapi.io'); } + + if (!isset($options['enterprise'])) { + $options['enterprise'] = false; + } } /** @@ -54,12 +64,20 @@ public function processOptions(array &$options): void protected function getLatestExchangeRate(ExchangeRateQuery $exchangeQuery): ExchangeRateContract { $currencyPair = $exchangeQuery->getCurrencyPair(); - - $url = sprintf( - self::LATEST_URL, - $currencyPair->getBaseCurrency(), - $this->options[self::ACCESS_KEY_OPTION] - ); + if ($this->options['enterprise']) { + $url = sprintf( + self::LATEST_URL, + $currencyPair->getBaseCurrency(), + $this->options[self::ACCESS_KEY_OPTION], + $currencyPair->getQuoteCurrency() + ); + } else { + $url = sprintf( + self::FREE_LATEST_URL, + $this->options[self::ACCESS_KEY_OPTION], + $currencyPair->getQuoteCurrency() + ); + } return $this->doCreateRate($url, $currencyPair); } @@ -70,13 +88,22 @@ protected function getLatestExchangeRate(ExchangeRateQuery $exchangeQuery): Exch protected function getHistoricalExchangeRate(HistoricalExchangeRateQuery $exchangeQuery): ExchangeRateContract { $currencyPair = $exchangeQuery->getCurrencyPair(); - - $url = sprintf( - self::HISTORICAL_URL, - $exchangeQuery->getDate()->format('Y-m-d'), - $exchangeQuery->getCurrencyPair()->getBaseCurrency(), - $this->options[self::ACCESS_KEY_OPTION] - ); + if ($this->options['enterprise']) { + $url = sprintf( + self::HISTORICAL_URL, + $exchangeQuery->getDate()->format('Y-m-d'), + $exchangeQuery->getCurrencyPair()->getBaseCurrency(), + $this->options[self::ACCESS_KEY_OPTION], + $currencyPair->getQuoteCurrency() + ); + } else { + $url = sprintf( + self::FREE_HISTORICAL_URL, + $exchangeQuery->getDate()->format('Y-m-d'), + $this->options[self::ACCESS_KEY_OPTION], + $exchangeQuery->getCurrencyPair()->getQuoteCurrency() + ); + } return $this->doCreateRate($url, $currencyPair); } @@ -86,16 +113,13 @@ protected function getHistoricalExchangeRate(HistoricalExchangeRateQuery $exchan */ public function supportQuery(ExchangeRateQuery $exchangeQuery): bool { - return true; + return $this->options['enterprise'] || 'EUR' === $exchangeQuery->getCurrencyPair()->getBaseCurrency(); } /** * Creates a rate. * - * @param string $url - * @param CurrencyPair $currencyPair - * - * @return ExchangeRate + * @param string $url * * @throws Exception */ @@ -105,7 +129,22 @@ private function doCreateRate($url, CurrencyPair $currencyPair): ExchangeRate $data = StringUtil::jsonToArray($content); if (isset($data['error'])) { - throw new Exception($data['error']); + if (isset($data['error']['code'])) { + if (\in_array($data['error']['code'], [ + 'invalid_currency_codes', + 'invalid_base_currency', + 'no_rates_available', + ], true)) { + throw new UnsupportedCurrencyPairException($currencyPair, $this); + } + if (isset($data['error']['message'])) { + throw new Exception($data['error']['message']); + } else { + throw new Exception('Service return error code: '.$data['error']['code']); + } + } else { + throw new Exception('Service return unhandled error'); + } } if (isset($data['rates'][$currencyPair->getQuoteCurrency()])) { diff --git a/tests/Fixtures/Service/ExchangeRatesApi/base_currency_access_restricted.json b/tests/Fixtures/Service/ExchangeRatesApi/base_currency_access_restricted.json new file mode 100644 index 0000000..b0113bd --- /dev/null +++ b/tests/Fixtures/Service/ExchangeRatesApi/base_currency_access_restricted.json @@ -0,0 +1,6 @@ +{ + "error":{ + "code":"base_currency_access_restricted", + "message":"An unexpected error ocurred. [Technical Support: support@apilayer.com]" + } +} diff --git a/tests/Fixtures/Service/ExchangeRatesApi/error.json b/tests/Fixtures/Service/ExchangeRatesApi/error.json deleted file mode 100644 index cd46da9..0000000 --- a/tests/Fixtures/Service/ExchangeRatesApi/error.json +++ /dev/null @@ -1 +0,0 @@ -{"error":"Base 'FOO' is not supported."} \ No newline at end of file diff --git a/tests/Fixtures/Service/ExchangeRatesApi/historical.json b/tests/Fixtures/Service/ExchangeRatesApi/historical.json index 88377aa..7299fe1 100644 --- a/tests/Fixtures/Service/ExchangeRatesApi/historical.json +++ b/tests/Fixtures/Service/ExchangeRatesApi/historical.json @@ -1,33 +1,10 @@ { - "base": "USD", - "date": "2000-01-03", - "rates": { - "AUD": 1.5209, - "CAD": 1.4447, - "CHF": 1.59, - "CYP": 0.57156, - "CZK": 35.741, - "DKK": 7.374, - "EEK": 15.507, - "GBP": 0.61903, - "HKD": 7.7923, - "HUF": 252.26, - "ISK": 72.379, - "JPY": 101.83, - "KRW": 1129.9, - "LTL": 4.0093, - "LVL": 0.58632, - "MTL": 0.4114, - "NOK": 7.9901, - "NZD": 1.9159, - "PLN": 4.1462, - "ROL": 18110.0, - "SEK": 8.4757, - "SGD": 1.6619, - "SIT": 197.12, - "SKK": 41.94, - "TRL": 541260.0, - "ZAR": 6.146, - "EUR": 0.99108 - } + "success":true, + "timestamp":1618531199, + "historical":true, + "base":"EUR", + "date":"2021-04-15", + "rates":{ + "USD":1.196953 + } } diff --git a/tests/Fixtures/Service/ExchangeRatesApi/https_access_restricted.json b/tests/Fixtures/Service/ExchangeRatesApi/https_access_restricted.json new file mode 100644 index 0000000..f09f123 --- /dev/null +++ b/tests/Fixtures/Service/ExchangeRatesApi/https_access_restricted.json @@ -0,0 +1,6 @@ +{ + "error":{ + "code":"https_access_restricted", + "message":"Access Restricted - Your current Subscription Plan does not support HTTPS Encryption." + } +} diff --git a/tests/Fixtures/Service/ExchangeRatesApi/invalid_access_key.json b/tests/Fixtures/Service/ExchangeRatesApi/invalid_access_key.json new file mode 100644 index 0000000..416a348 --- /dev/null +++ b/tests/Fixtures/Service/ExchangeRatesApi/invalid_access_key.json @@ -0,0 +1,6 @@ +{ + "error":{ + "code":"invalid_access_key", + "message":"You have not supplied a valid API Access Key. [Technical Support: support@apilayer.com]" + } +} diff --git a/tests/Fixtures/Service/ExchangeRatesApi/invalid_base_currency.json b/tests/Fixtures/Service/ExchangeRatesApi/invalid_base_currency.json new file mode 100644 index 0000000..57ae65e --- /dev/null +++ b/tests/Fixtures/Service/ExchangeRatesApi/invalid_base_currency.json @@ -0,0 +1,6 @@ +{ + "error":{ + "code":"invalid_base_currency", + "message":"An unexpected error ocurred. [Technical Support: support@apilayer.com]" + } +} diff --git a/tests/Fixtures/Service/ExchangeRatesApi/invalid_currency_codes.json b/tests/Fixtures/Service/ExchangeRatesApi/invalid_currency_codes.json new file mode 100644 index 0000000..1726e5c --- /dev/null +++ b/tests/Fixtures/Service/ExchangeRatesApi/invalid_currency_codes.json @@ -0,0 +1,6 @@ +{ + "error":{ + "code":"invalid_currency_codes", + "message":"You have provided one or more invalid Currency Codes. [Required format: currencies=EUR,USD,GBP,...]" + } +} diff --git a/tests/Fixtures/Service/ExchangeRatesApi/invalid_date.json b/tests/Fixtures/Service/ExchangeRatesApi/invalid_date.json new file mode 100644 index 0000000..dedde85 --- /dev/null +++ b/tests/Fixtures/Service/ExchangeRatesApi/invalid_date.json @@ -0,0 +1,6 @@ +{ + "error":{ + "code":"invalid_date", + "message":"You have entered an invalid date. [Required format: date=YYYY-MM-DD]" + } +} diff --git a/tests/Fixtures/Service/ExchangeRatesApi/latest.json b/tests/Fixtures/Service/ExchangeRatesApi/latest.json index a98c40a..f14e149 100644 --- a/tests/Fixtures/Service/ExchangeRatesApi/latest.json +++ b/tests/Fixtures/Service/ExchangeRatesApi/latest.json @@ -1,37 +1,9 @@ { - "base": "EUR", - "date": "2016-08-26", - "rates": { - "AUD": 1.4771, - "BGN": 1.9558, - "BRL": 3.6441, - "CAD": 1.4546, - "CHF": 1.0933, - "CNY": 7.5318, - "CZK": 27.024, - "DKK": 7.4462, - "GBP": 0.8545, - "HKD": 8.7555, - "HRK": 7.4893, - "HUF": 308.5, - "IDR": 14906.0, - "ILS": 4.2444, - "INR": 75.647, - "JPY": 113.33, - "KRW": 1258.02, - "MXN": 20.7197, - "MYR": 4.5349, - "NOK": 9.2501, - "NZD": 1.5418, - "PHP": 52.29, - "PLN": 4.3244, - "RON": 4.4578, - "RUB": 73.1392, - "SEK": 9.4903, - "SGD": 1.5269, - "THB": 39.004, - "TRY": 3.316, - "USD": 1.129, - "ZAR": 15.8622 - } + "success":true, + "timestamp":1619171643, + "base":"EUR", + "date":"2021-04-23", + "rates":{ + "USD":1.20555 + } } diff --git a/tests/Fixtures/Service/ExchangeRatesApi/no_rates_available.json b/tests/Fixtures/Service/ExchangeRatesApi/no_rates_available.json new file mode 100644 index 0000000..9c9f2b8 --- /dev/null +++ b/tests/Fixtures/Service/ExchangeRatesApi/no_rates_available.json @@ -0,0 +1,6 @@ +{ + "error":{ + "code":"no_rates_available", + "message":"Your query did not return any results. Please try again." + } +} diff --git a/tests/Tests/Service/ExchangeRatesApiTest.php b/tests/Tests/Service/ExchangeRatesApiTest.php index 21bc8ed..83fd0a1 100644 --- a/tests/Tests/Service/ExchangeRatesApiTest.php +++ b/tests/Tests/Service/ExchangeRatesApiTest.php @@ -15,6 +15,7 @@ use Exchanger\CurrencyPair; use Exchanger\Exception\Exception; +use Exchanger\Exception\UnsupportedCurrencyPairException; use Exchanger\ExchangeRateQuery; use Exchanger\HistoricalExchangeRateQuery; use Exchanger\Service\ExchangeRatesApi; @@ -29,9 +30,12 @@ class ExchangeRatesApiTest extends ServiceTestCase */ public function it_does_support_all_queries() { - $service = new ExchangeRatesApi($this->createMock('Http\Client\HttpClient'), null, ['access_key' => 'x']); - - $this->assertTrue($service->supportQuery(new ExchangeRateQuery(CurrencyPair::createFromString('USD/EUR')))); + $service = new ExchangeRatesApi( + $this->createMock('Http\Client\HttpClient'), + null, + ['access_key' => 'x', 'enterprise' => false] + ); + $this->assertFalse($service->supportQuery(new ExchangeRateQuery(CurrencyPair::createFromString('USD/EUR')))); } /** @@ -48,23 +52,148 @@ public function it_supports_eur_base() */ public function it_does_support_other_than_eur() { - $service = new ExchangeRatesApi($this->createMock('Http\Client\HttpClient'), null, ['access_key' => 'x']); + $service = new ExchangeRatesApi( + $this->createMock('Http\Client\HttpClient'), + null, + ['access_key' => 'x', 'enterprise' => true] + ); $this->assertTrue($service->supportQuery(new ExchangeRateQuery(CurrencyPair::createFromString('USD/CAD')))); } /** * @test + * @dataProvider unsupportedCurrencyPairResponsesProvider */ - public function it_throws_an_exception_with_error_response() + public function it_throws_An_unsupported_currency_pair_exception( + string $contentPath, + string $uri, + string $accessKey, + bool $enterprise, + string $currencyPair, + bool $historical = false, + string $dateStr = '2020-04-15' + ) { + $this->expectException(UnsupportedCurrencyPairException::class); + + $content = file_get_contents($contentPath); + + $service = new ExchangeRatesApi( + $this->getHttpAdapterMock($uri, $content), + null, + ['access_key' => $accessKey, 'enterprise' => $enterprise] + ); + if ($historical) { + $date = new \DateTimeImmutable($dateStr); + $query = new HistoricalExchangeRateQuery(CurrencyPair::createFromString($currencyPair), $date); + } else { + $query = new ExchangeRateQuery(CurrencyPair::createFromString($currencyPair)); + } + $service->getExchangeRate($query); + } + + public function unsupportedCurrencyPairResponsesProvider(): array { - $this->expectException(Exception::class); - $this->expectExceptionMessage('Base \'FOO\' is not supported.'); + $dir = __DIR__.'/../../Fixtures/Service/ExchangeRatesApi/'; + + return [ + 'invalid_base_currency' => [ + $dir.'invalid_base_currency.json', + sprintf(ExchangeRatesApi::LATEST_URL, $baseCurrency = 'XTS', $accessKey = 'valid', $currency = 'USD'), + $accessKey, + true, + $baseCurrency.'/'.$currency, + ], + 'invalid_currency_codes' => [ + $dir.'invalid_currency_codes.json', + sprintf(ExchangeRatesApi::LATEST_URL, $baseCurrency = 'USD', $accessKey = 'valid', $currency = 'XTS'), + $accessKey, + true, + $baseCurrency.'/'.$currency, + ], + 'no_rates_available' => [ + $dir.'no_rates_available.json', + sprintf(ExchangeRatesApi::HISTORICAL_URL, $date = '1998-12-31', $baseCurrency = 'USD', $accessKey = 'valid', $currency = 'EUR'), + $accessKey, + true, + $baseCurrency.'/'.$currency, + true, + $date, + ], + ]; + } - $uri = 'https://api.exchangeratesapi.io/latest?base=FOO&access_key=x'; - $content = file_get_contents(__DIR__.'/../../Fixtures/Service/ExchangeRatesApi/error.json'); + /** + * @dataProvider errorResponsesProvider + */ + public function it_throws_an_exception_with_error_response( + string $contentPath, + string $uri, + string $accessKey, + bool $enterprise, + string $currencyPair, + string $message, + bool $historical = false, + string $dateStr = '2020-04-15' + ) { + $this->expectException(Exception::class); + $this->expectExceptionMessage($message); + + $content = file_get_contents($contentPath); + + $service = new ExchangeRatesApi( + $this->getHttpAdapterMock($uri, $content), + null, + ['access_key' => $accessKey, 'enterprise' => $enterprise] + ); + if ($historical) { + $date = new \DateTimeImmutable($dateStr); + $query = new HistoricalExchangeRateQuery(CurrencyPair::createFromString($currencyPair), $date); + } else { + $query = new ExchangeRateQuery(CurrencyPair::createFromString($currencyPair)); + } + $service->getExchangeRate($query); + } - $service = new ExchangeRatesApi($this->getHttpAdapterMock($uri, $content), null, ['access_key' => 'x']); - $service->getExchangeRate(new ExchangeRateQuery(CurrencyPair::createFromString('FOO/EUR'))); + public function errorResponsesProvider(): array + { + $dir = __DIR__.'/../../Fixtures/Service/ExchangeRatesApi/'; + + return [ + 'invalid_access_key' => [ + $dir.'invalid_access_key.json', + sprintf(ExchangeRatesApi::FREE_LATEST_URL, $accessKey = 'invalid', $currency = 'USD'), + $accessKey, + false, + 'EUR/'.$currency, + 'You have not supplied a valid API Access Key. [Technical Support: support@apilayer.com]', + ], + 'base_currency_access_restricted' => [ + $dir.'base_currency_access_restricted.json', + sprintf(ExchangeRatesApi::LATEST_URL, $baseCurrency = 'USD', $accessKey = 'valid', $currency = 'EUR'), + $accessKey, + true, + $baseCurrency.'/'.$currency, + 'An unexpected error ocurred. [Technical Support: support@apilayer.com]', + ], + 'https_access_restricted' => [ + $dir.'https_access_restricted.json', + sprintf(ExchangeRatesApi::LATEST_URL, $baseCurrency = 'EUR', $accessKey = 'valid', $currency = 'USD'), + $accessKey, + true, + $baseCurrency.'/'.$currency, + 'Access Restricted - Your current Subscription Plan does not support HTTPS Encryption.', + ], + 'invalid_date' => [ + $dir.'invalid_date.json', + sprintf(ExchangeRatesApi::FREE_HISTORICAL_URL, $date = '2056-01-01', $accessKey = 'valid', $currency = 'USD'), + $accessKey, + false, + $baseCurrency.'/'.$currency, + 'You have entered an invalid date. [Required format: date=YYYY-MM-DD]', + true, + $date, + ], + ]; } /** @@ -72,15 +201,19 @@ public function it_throws_an_exception_with_error_response() */ public function it_fetches_a_rate() { - $pair = CurrencyPair::createFromString('EUR/CHF'); - $uri = 'https://api.exchangeratesapi.io/latest?base=EUR&access_key=x'; + $pair = CurrencyPair::createFromString('EUR/USD'); + $uri = 'https://api.exchangeratesapi.io/latest?base=EUR&access_key=x&symbols=USD'; $content = file_get_contents(__DIR__.'/../../Fixtures/Service/ExchangeRatesApi/latest.json'); - $service = new ExchangeRatesApi($this->getHttpAdapterMock($uri, $content), null, ['access_key' => 'x']); + $service = new ExchangeRatesApi( + $this->getHttpAdapterMock($uri, $content), + null, + ['access_key' => 'x', 'enterprise' => true] + ); $rate = $service->getExchangeRate(new ExchangeRateQuery($pair)); - $this->assertEquals(1.0933, $rate->getValue()); - $this->assertEquals(new \DateTime('2016-08-26'), $rate->getDate()); + $this->assertEquals(1.20555, $rate->getValue()); + $this->assertEquals(new \DateTime('2021-04-23'), $rate->getDate()); $this->assertEquals('exchange_rates_api', $rate->getProviderName()); $this->assertSame($pair, $rate->getCurrencyPair()); } @@ -90,15 +223,19 @@ public function it_fetches_a_rate() */ public function it_fetches_a_historical_rate() { - $pair = CurrencyPair::createFromString('EUR/AUD'); - $uri = 'https://api.exchangeratesapi.io/2000-01-03?base=EUR&access_key=x'; + $pair = CurrencyPair::createFromString('EUR/USD'); + $uri = 'https://api.exchangeratesapi.io/2021-04-15?base=EUR&access_key=x&symbols=USD'; $content = file_get_contents(__DIR__.'/../../Fixtures/Service/ExchangeRatesApi/historical.json'); - $date = new \DateTime('2000-01-03'); + $date = new \DateTime('2021-04-15'); - $service = new ExchangeRatesApi($this->getHttpAdapterMock($uri, $content), null, ['access_key' => 'x']); + $service = new ExchangeRatesApi( + $this->getHttpAdapterMock($uri, $content), + null, + ['access_key' => 'x', 'enterprise' => true] + ); $rate = $service->getExchangeRate(new HistoricalExchangeRateQuery($pair, $date)); - $this->assertEquals(1.5209, $rate->getValue()); + $this->assertEquals(1.196953, $rate->getValue()); $this->assertEquals($date, $rate->getDate()); $this->assertEquals('exchange_rates_api', $rate->getProviderName()); $this->assertSame($pair, $rate->getCurrencyPair());