diff --git a/src/Service/ApiLayer/CurrencyData.php b/src/Service/ApiLayer/CurrencyData.php new file mode 100644 index 0000000..fedee0d --- /dev/null +++ b/src/Service/ApiLayer/CurrencyData.php @@ -0,0 +1,126 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Exchanger\Service\ApiLayer; + +use Exchanger\Contract\CurrencyPair; +use Exchanger\Contract\ExchangeRateQuery; +use Exchanger\Contract\HistoricalExchangeRateQuery; +use Exchanger\Exception\Exception; +use Exchanger\Exception\UnsupportedCurrencyPairException; +use Exchanger\ExchangeRate; +use Exchanger\Service\HttpService; +use Exchanger\Service\SupportsHistoricalQueries; +use Exchanger\StringUtil; +use Exchanger\Contract\ExchangeRate as ExchangeRateContract; + +/** + * ApiLayer Currency Data Service (https://apilayer.com/marketplace/currency_data-api). + * + * @author Florian Voutzinos + */ +final class CurrencyData extends HttpService +{ + use SupportsHistoricalQueries; + + const API_KEY_OPTION = 'api_key'; + + const LATEST_URL = 'https://api.apilayer.com/currency_data/live?apikey=%s¤cies=%s'; + + const HISTORICAL_URL = 'https://api.apilayer.com/currency_data/historical?apikey=%s&date=%s'; + + /** + * {@inheritdoc} + */ + public function processOptions(array &$options): void + { + if (!isset($options[self::API_KEY_OPTION])) { + throw new \InvalidArgumentException('The "api_key" option must be provided to use CurrencyData (https://apilayer.com/marketplace/currency_data-api).'); + } + } + + /** + * {@inheritdoc} + */ + protected function getLatestExchangeRate(ExchangeRateQuery $exchangeQuery): ExchangeRateContract + { + $currencyPair = $exchangeQuery->getCurrencyPair(); + + $url = sprintf( + self::LATEST_URL, + $this->options[self::API_KEY_OPTION], + $currencyPair->getQuoteCurrency() + ); + + return $this->doCreateRate($url, $currencyPair); + } + + /** + * {@inheritdoc} + */ + protected function getHistoricalExchangeRate(HistoricalExchangeRateQuery $exchangeQuery): ExchangeRateContract + { + $url = sprintf( + self::HISTORICAL_URL, + $this->options[self::API_KEY_OPTION], + $exchangeQuery->getDate()->format('Y-m-d') + ); + + return $this->doCreateRate($url, $exchangeQuery->getCurrencyPair()); + } + + /** + * {@inheritdoc} + */ + public function supportQuery(ExchangeRateQuery $exchangeQuery): bool + { + return true; + } + + /** + * Creates a rate. + * + * @param string $url + * @param CurrencyPair $currencyPair + * + * @return ExchangeRate|null + * + * @throws Exception + */ + private function doCreateRate($url, CurrencyPair $currencyPair): ExchangeRate + { + $content = $this->request($url); + $data = StringUtil::jsonToArray($content); + + if (empty($data['success'])) { + throw new Exception($data['error']['info']); + } + + $date = (new \DateTime())->setTimestamp($data['timestamp']); + $hash = $currencyPair->getBaseCurrency().$currencyPair->getQuoteCurrency(); + + if ($data['source'] === $currencyPair->getBaseCurrency() && isset($data['quotes'][$hash])) { + return $this->createRate($currencyPair, (float) ($data['quotes'][$hash]), $date); + } + + throw new UnsupportedCurrencyPairException($currencyPair, $this); + } + + /** + * {@inheritdoc} + */ + public function getName(): string + { + return 'apilayer_currency_data'; + } +} diff --git a/src/Service/ApiLayer/Fixer.php b/src/Service/ApiLayer/Fixer.php index 131f4b4..78a587b 100644 --- a/src/Service/ApiLayer/Fixer.php +++ b/src/Service/ApiLayer/Fixer.php @@ -25,7 +25,7 @@ use Exchanger\Contract\ExchangeRate as ExchangeRateContract; /** - * Fixer Service (https://apilayer.com/marketplace/fixer-api). + * ApiLayer Fixer Service (https://apilayer.com/marketplace/fixer-api). * * @author Florian Voutzinos */ @@ -33,7 +33,7 @@ final class Fixer extends HttpService { use SupportsHistoricalQueries; - const ACCESS_KEY_OPTION = 'access_key'; + const API_KEY_OPTION = 'api_key'; const LATEST_URL = 'https://api.apilayer.com/fixer/latest?base=%s&apikey=%s'; @@ -44,8 +44,8 @@ final class Fixer extends HttpService */ public function processOptions(array &$options): void { - if (!isset($options[self::ACCESS_KEY_OPTION])) { - throw new \InvalidArgumentException('The "access_key" option must be provided to use https://apilayer.com/marketplace/fixer-api'); + if (!isset($options[self::API_KEY_OPTION])) { + throw new \InvalidArgumentException('The "api_key" option must be provided to use Fixer (https://apilayer.com/marketplace/fixer-api).'); } } @@ -59,7 +59,7 @@ protected function getLatestExchangeRate(ExchangeRateQuery $exchangeQuery): Exch $url = sprintf( self::LATEST_URL, $exchangeQuery->getCurrencyPair()->getBaseCurrency(), - $this->options[self::ACCESS_KEY_OPTION] + $this->options[self::API_KEY_OPTION] ); return $this->doCreateRate($url, $currencyPair); @@ -76,7 +76,7 @@ protected function getHistoricalExchangeRate(HistoricalExchangeRateQuery $exchan self::HISTORICAL_URL, $exchangeQuery->getDate()->format('Y-m-d'), $exchangeQuery->getCurrencyPair()->getBaseCurrency(), - $this->options[self::ACCESS_KEY_OPTION] + $this->options[self::API_KEY_OPTION] ); return $this->doCreateRate($url, $currencyPair); diff --git a/src/Service/Registry.php b/src/Service/Registry.php index 5de1643..92d8b01 100644 --- a/src/Service/Registry.php +++ b/src/Service/Registry.php @@ -53,7 +53,8 @@ public static function getServices(): array 'fastforex' => FastForex::class, 'abstract_api' => AbstractApi::class, 'exchangeratehost' => ExchangerateHost::class, - 'apilayer_fixer' => ApiLayer\Fixer::class + 'apilayer_fixer' => ApiLayer\Fixer::class, + 'apilayer_currency_data' => ApiLayer\CurrencyData::class ]; } } diff --git a/tests/Fixtures/Service/ApiLayer/CurrencyData/error.json b/tests/Fixtures/Service/ApiLayer/CurrencyData/error.json new file mode 100644 index 0000000..c5f5910 --- /dev/null +++ b/tests/Fixtures/Service/ApiLayer/CurrencyData/error.json @@ -0,0 +1,8 @@ +{ + "success": false, + "error": { + "code": 101, + "type": "invalid_access_key", + "info": "You have not supplied a valid API Access Key. [Technical Support: support@apilayer.com]" + } +} \ No newline at end of file diff --git a/tests/Fixtures/Service/ApiLayer/CurrencyData/historical_success.json b/tests/Fixtures/Service/ApiLayer/CurrencyData/historical_success.json new file mode 100644 index 0000000..1d7ff0d --- /dev/null +++ b/tests/Fixtures/Service/ApiLayer/CurrencyData/historical_success.json @@ -0,0 +1,179 @@ +{ + "success": true, + "terms": "https:\/\/currencylayer.com\/terms", + "privacy": "https:\/\/currencylayer.com\/privacy", + "historical": true, + "date": "2015-05-05", + "timestamp": 1430870399, + "source": "USD", + "quotes": { + "USDAED": 3.673069, + "USDAFN": 58.160267, + "USDALL": 125.971999, + "USDAMD": 478.706002, + "USDANG": 1.78954, + "USDAOA": 109.314001, + "USDARS": 8.910188, + "USDAUD": 1.260714, + "USDAWG": 1.7925, + "USDAZN": 1.04915, + "USDBAM": 1.749085, + "USDBBD": 2, + "USDBDT": 77.789719, + "USDBGN": 1.74921, + "USDBHD": 0.377207, + "USDBIF": 1568.935867, + "USDBMD": 1, + "USDBND": 1.331299, + "USDBOB": 6.900772, + "USDBRL": 3.053759, + "USDBSD": 1, + "USDBTC": 0.004235, + "USDBTN": 63.515033, + "USDBWP": 9.833948, + "USDBYR": 14374.316667, + "USDBZD": 1.997894, + "USDCAD": 1.208877, + "USDCDF": 923.5605, + "USDCHF": 0.928519, + "USDCLF": 0.024602, + "USDCLP": 610.728796, + "USDCNY": 6.185728, + "USDCOP": 2388.811663, + "USDCRC": 531.94912, + "USDCUC": 1, + "USDCUP": 1.000038, + "USDCVE": 98.654687, + "USDCZK": 24.4979, + "USDDJF": 177.377399, + "USDDKK": 6.675821, + "USDDOP": 44.77952, + "USDDZD": 97.40122, + "USDEEK": 13.999775, + "USDEGP": 7.622161, + "USDERN": 15.1151, + "USDETB": 20.51218, + "USDEUR": 0.894203, + "USDFJD": 2.028702, + "USDFKP": 0.659043, + "USDGBP": 0.659043, + "USDGEL": 2.319325, + "USDGGP": 0.659043, + "USDGHS": 3.864928, + "USDGIP": 0.659043, + "USDGMD": 43.0138, + "USDGNF": 7312.395, + "USDGTQ": 7.746544, + "USDGYD": 206.185002, + "USDHKD": 7.75158, + "USDHNL": 21.95625, + "USDHRK": 6.782218, + "USDHTG": 47.60719, + "USDHUF": 271.684098, + "USDIDR": 13031.9, + "USDILS": 3.875593, + "USDIMP": 0.659043, + "USDINR": 63.45038, + "USDIQD": 1190.3009, + "USDIRR": 28255.667967, + "USDISK": 131.851, + "USDJEP": 0.659043, + "USDJMD": 115.103, + "USDJOD": 0.70845, + "USDJPY": 119.957, + "USDKES": 95.039721, + "USDKGS": 59.521667, + "USDKHR": 4045.91835, + "USDKMF": 440.441716, + "USDKPW": 900.09, + "USDKRW": 1081.599998, + "USDKWD": 0.301784, + "USDKYD": 0.821907, + "USDKZT": 185.8362, + "USDLAK": 8089.616732, + "USDLBP": 1508.5, + "USDLKR": 133.1648, + "USDLRD": 84.720001, + "USDLSL": 11.99574, + "USDLTL": 2.933833, + "USDLVL": 0.62865, + "USDLYD": 1.36872, + "USDMAD": 9.71972, + "USDMDL": 18.03774, + "USDMGA": 3061.686699, + "USDMKD": 55.07277, + "USDMMK": 1087.42, + "USDMNT": 1955.166667, + "USDMOP": 7.988893, + "USDMRO": 308.661333, + "USDMUR": 34.92106, + "USDMVR": 15.276788, + "USDMWK": 443.125199, + "USDMXN": 15.38044, + "USDMYR": 3.593208, + "USDMZN": 35.283833, + "USDNAD": 11.99574, + "USDNGN": 198.924, + "USDNIO": 26.83499, + "USDNOK": 7.584483, + "USDNPR": 101.4441, + "USDNZD": 1.32795, + "USDOMR": 0.385045, + "USDPAB": 1, + "USDPEN": 3.144932, + "USDPGK": 2.693605, + "USDPHP": 44.61504, + "USDPKR": 101.797999, + "USDPLN": 3.607051, + "USDPYG": 5018.168262, + "USDQAR": 3.641438, + "USDRON": 3.963029, + "USDRSD": 107.7551, + "USDRUB": 50.8311, + "USDRWF": 695.9579, + "USDSAR": 3.750335, + "USDSBD": 7.734353, + "USDSCR": 13.58486, + "USDSDG": 5.959468, + "USDSEK": 8.356247, + "USDSGD": 1.331973, + "USDSHP": 0.659043, + "USDSLL": 4332.5, + "USDSOS": 697.386702, + "USDSRD": 3.317, + "USDSTD": 21951.433333, + "USDSVC": 8.738193, + "USDSYP": 188.929501, + "USDSZL": 11.99574, + "USDTHB": 33.29961, + "USDTJS": 6.275233, + "USDTMT": 3.4999, + "USDTND": 1.914979, + "USDTOP": 2.025876, + "USDTRY": 2.706331, + "USDTTD": 6.3385, + "USDTWD": 30.69802, + "USDTZS": 1975.558325, + "USDUAH": 20.95269, + "USDUGX": 2995.7, + "USDUSD": 1, + "USDUYU": 26.34045, + "USDUZS": 2509.996683, + "USDVEF": 6.318751, + "USDVND": 21638.333333, + "USDVUV": 105.151666, + "USDWST": 2.442526, + "USDXAF": 587.832418, + "USDXAG": 0.060369, + "USDXAU": 0.000838, + "USDXCD": 2.70102, + "USDXDR": 0.713827, + "USDXOF": 587.207858, + "USDXPF": 106.83024, + "USDYER": 215.025199, + "USDZAR": 11.99449, + "USDZMK": 5253.075255, + "USDZMW": 7.382381, + "USDZWL": 322.355006 + } +} diff --git a/tests/Fixtures/Service/ApiLayer/CurrencyData/success.json b/tests/Fixtures/Service/ApiLayer/CurrencyData/success.json new file mode 100644 index 0000000..f33fe5d --- /dev/null +++ b/tests/Fixtures/Service/ApiLayer/CurrencyData/success.json @@ -0,0 +1,10 @@ +{ + "success": true, + "terms": "https:\/\/currencylayer.com\/terms", + "privacy": "https:\/\/currencylayer.com\/privacy", + "timestamp": 1399748450, + "source": "USD", + "quotes": { + "USDEUR": 0.726804 + } +} \ No newline at end of file diff --git a/tests/Tests/Service/ApiLayer/CurrencyDataTest.php b/tests/Tests/Service/ApiLayer/CurrencyDataTest.php new file mode 100644 index 0000000..aaf110e --- /dev/null +++ b/tests/Tests/Service/ApiLayer/CurrencyDataTest.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Exchanger\Tests\Service; + +use Exchanger\Exception\Exception; +use Exchanger\ExchangeRateQuery; +use Exchanger\HistoricalExchangeRateQuery; +use Exchanger\CurrencyPair; +use Exchanger\Service\ApiLayer\CurrencyData; + +/** + * @author Florian Voutzinos + */ +class CurrencyDataTest extends ServiceTestCase +{ + /** + * @test + */ + public function it_throws_an_exception_if_api_key_option_missing() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The "api_key" option must be provided to use CurrencyData (https://apilayer.com/marketplace/currency_data-api).'); + new CurrencyData($this->createMock('Http\Client\HttpClient')); + } + + /** + * @test + */ + public function it_supports_all_queries() + { + $service = new CurrencyData($this->createMock('Http\Client\HttpClient'), null, ['api_key' => 'secret']); + $this->assertTrue($service->supportQuery(new ExchangeRateQuery(CurrencyPair::createFromString('EUR/EUR')))); + } + + /** + * @test + */ + public function it_throws_an_exception_with_error_response() + { + $this->expectException(Exception::class); + $uri = 'https://api.apilayer.com/currency_data/live?apikey=secret¤cies=EUR'; + $content = file_get_contents(__DIR__.'/../../../../Fixtures/Service/CurrencyData/error.json'); + + $service = new CurrencyData($this->getHttpAdapterMock($uri, $content), null, ['api_key' => 'secret']); + $service->getExchangeRate(new ExchangeRateQuery(CurrencyPair::createFromString('USD/EUR'))); + } + + /** + * @test + */ + public function it_fetches_a_rate() + { + $uri = 'https://api.apilayer.com/currency_data/live?apikey=secret¤cies=EUR'; + $expectedDate = new \DateTime(); + $expectedDate->setTimestamp(1399748450); + $content = file_get_contents(__DIR__.'/../../../Fixtures/Service/CurrencyData/success.json'); + + $pair = CurrencyPair::createFromString('USD/EUR'); + $service = new CurrencyData($this->getHttpAdapterMock($uri, $content), null, ['api_key' => 'secret']); + $rate = $service->getExchangeRate(new ExchangeRateQuery($pair)); + + $this->assertEquals(0.726804, $rate->getValue()); + $this->assertEquals($expectedDate, $rate->getDate()); + $this->assertEquals('apilayer_currency_data', $rate->getProviderName()); + $this->assertSame($pair, $rate->getCurrencyPair()); + } + + /** + * @test + */ + public function it_fetches_a_historical_rate() + { + $pair = CurrencyPair::createFromString('USD/AED'); + $uri = 'http://apilayer.net/api/historical?apikey=secret&date=2015-05-06'; + $content = file_get_contents(__DIR__.'/../../../Fixtures/Service/CurrencyData/historical_success.json'); + $date = new \DateTime('2015-05-06'); + $expectedDate = new \DateTime(); + $expectedDate->setTimestamp(1430870399); + + $service = new CurrencyData($this->getHttpAdapterMock($uri, $content), null, ['api_key' => 'secret']); + $rate = $service->getExchangeRate(new HistoricalExchangeRateQuery($pair, $date)); + + $this->assertEquals(3.673069, $rate->getValue()); + $this->assertEquals($expectedDate, $rate->getDate()); + $this->assertEquals('apilayer_currency_data', $rate->getProviderName()); + $this->assertSame($pair, $rate->getCurrencyPair()); + } + + /** + * @test + */ + public function it_has_a_name() + { + $service = new CurrencyData($this->createMock('Http\Client\HttpClient'), null, ['api_key' => 'secret']); + + $this->assertSame('apilayer_currency_data', $service->getName()); + } +} diff --git a/tests/Tests/Service/ApiLayer/FixerTest.php b/tests/Tests/Service/ApiLayer/FixerTest.php index 32ce4a4..3dceab6 100644 --- a/tests/Tests/Service/ApiLayer/FixerTest.php +++ b/tests/Tests/Service/ApiLayer/FixerTest.php @@ -25,12 +25,22 @@ */ class FixerTest extends ServiceTestCase { + /** + * @test + */ + public function it_throws_an_exception_if_api_key_option_missing() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The "api_key" option must be provided to use Fixer (https://apilayer.com/marketplace/fixer-api).'); + new Fixer($this->createMock('Http\Client\HttpClient')); + } + /** * @test */ public function it_supports_all_queries() { - $service = new Fixer($this->createMock('Http\Client\HttpClient'), null, ['access_key' => 'x']); + $service = new Fixer($this->createMock('Http\Client\HttpClient'), null, ['api_key' => 'x']); $this->assertTrue($service->supportQuery(new ExchangeRateQuery(CurrencyPair::createFromString('USD/EUR')))); } @@ -46,7 +56,7 @@ public function it_throws_an_exception_with_error_response() $uri = 'https://api.apilayer.com/fixer/latest?base=USD&apikey=x'; $content = file_get_contents(__DIR__.'/../../../Fixtures/Service/ApiLayer/Fixer/error.json'); - $service = new Fixer($this->getHttpAdapterMock($uri, $content), null, ['access_key' => 'x']); + $service = new Fixer($this->getHttpAdapterMock($uri, $content), null, ['api_key' => 'x']); $service->getExchangeRate(new ExchangeRateQuery(CurrencyPair::createFromString('USD/EUR'))); } @@ -59,7 +69,7 @@ public function it_fetches_a_rate() $uri = 'https://api.apilayer.com/fixer/latest?base=EUR&apikey=x'; $content = file_get_contents(__DIR__.'/../../../Fixtures/Service/ApiLayer/Fixer/latest.json'); - $service = new Fixer($this->getHttpAdapterMock($uri, $content), null, ['access_key' => 'x']); + $service = new Fixer($this->getHttpAdapterMock($uri, $content), null, ['api_key' => 'x']); $rate = $service->getExchangeRate(new ExchangeRateQuery($pair)); $this->assertEquals(1.0933, $rate->getValue()); @@ -78,7 +88,7 @@ public function it_fetches_a_historical_rate() $content = file_get_contents(__DIR__.'/../../../Fixtures/Service/ApiLayer/Fixer/historical.json'); $date = new \DateTime('2000-01-03'); - $service = new Fixer($this->getHttpAdapterMock($uri, $content), null, ['access_key' => 'x']); + $service = new Fixer($this->getHttpAdapterMock($uri, $content), null, ['api_key' => 'x']); $rate = $service->getExchangeRate(new HistoricalExchangeRateQuery($pair, $date)); $this->assertEquals(1.5209, $rate->getValue()); @@ -92,7 +102,7 @@ public function it_fetches_a_historical_rate() */ public function it_has_a_name() { - $service = new Fixer($this->createMock('Http\Client\HttpClient'), null, ['access_key' => 'x']); + $service = new Fixer($this->createMock('Http\Client\HttpClient'), null, ['api_key' => 'x']); $this->assertSame('apilayer_fixer', $service->getName()); }