Skip to content

Commit 95d68a9

Browse files
committed
ACP2E-1044: Fixer.IO API doesn't work
- fix - add test
1 parent fa3d6ec commit 95d68a9

File tree

5 files changed

+355
-2
lines changed

5 files changed

+355
-2
lines changed
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Directory\Model\Currency\Import;
9+
10+
use Exception;
11+
use Laminas\Http\Request;
12+
use Magento\Directory\Model\CurrencyFactory;
13+
use Magento\Framework\App\Config\ScopeConfigInterface;
14+
use Magento\Framework\HTTP\LaminasClientFactory as HttpClientFactory;
15+
use Magento\Store\Model\ScopeInterface;
16+
use Magento\Framework\HTTP\LaminasClient;
17+
18+
/**
19+
* Currency rate import model (https://apilayer.com/marketplace/fixer-api)
20+
*/
21+
class FixerIoApiLayer extends AbstractImport
22+
{
23+
private const CURRENCY_CONVERTER_HOST = 'https://api.apilayer.com';
24+
private const CURRENCY_CONVERTER_URL_PATH = '/fixer/latest?'
25+
. 'apikey={{ACCESS_KEY}}&base={{CURRENCY_FROM}}&symbols={{CURRENCY_TO}}';
26+
27+
/**
28+
* @var HttpClientFactory
29+
*/
30+
private $httpClientFactory;
31+
32+
/**
33+
* Core scope config
34+
*
35+
* @var ScopeConfigInterface
36+
*/
37+
private $scopeConfig;
38+
39+
/**
40+
* Initialize dependencies
41+
*
42+
* @param CurrencyFactory $currencyFactory
43+
* @param ScopeConfigInterface $scopeConfig
44+
* @param HttpClientFactory $httpClientFactory
45+
*/
46+
public function __construct(
47+
CurrencyFactory $currencyFactory,
48+
ScopeConfigInterface $scopeConfig,
49+
HttpClientFactory $httpClientFactory
50+
) {
51+
parent::__construct($currencyFactory);
52+
$this->scopeConfig = $scopeConfig;
53+
$this->httpClientFactory = $httpClientFactory;
54+
}
55+
56+
/**
57+
* @inheritdoc
58+
*/
59+
public function fetchRates()
60+
{
61+
$data = [];
62+
$currencies = $this->_getCurrencyCodes();
63+
$defaultCurrencies = $this->_getDefaultCurrencyCodes();
64+
65+
foreach ($defaultCurrencies as $currencyFrom) {
66+
if (!isset($data[$currencyFrom])) {
67+
$data[$currencyFrom] = [];
68+
}
69+
$data = $this->convertBatch($data, $currencyFrom, $currencies);
70+
ksort($data[$currencyFrom]);
71+
}
72+
return $data;
73+
}
74+
75+
/**
76+
* Return currencies convert rates in batch mode
77+
*
78+
* @param array $data
79+
* @param string $currencyFrom
80+
* @param array $currenciesTo
81+
* @return array
82+
*/
83+
private function convertBatch(array $data, string $currencyFrom, array $currenciesTo): array
84+
{
85+
$accessKey = $this->scopeConfig->getValue('currency/apilayer/api_key', ScopeInterface::SCOPE_STORE);
86+
if (empty($accessKey)) {
87+
$this->_messages[] = __('No API Key was specified or an invalid API Key was specified.');
88+
$data[$currencyFrom] = $this->makeEmptyResponse($currenciesTo);
89+
return $data;
90+
}
91+
92+
$currenciesStr = implode(',', $currenciesTo);
93+
$url = str_replace(
94+
['{{ACCESS_KEY}}', '{{CURRENCY_FROM}}', '{{CURRENCY_TO}}'],
95+
[$accessKey, $currencyFrom, $currenciesStr],
96+
self::CURRENCY_CONVERTER_HOST . self::CURRENCY_CONVERTER_URL_PATH
97+
);
98+
// phpcs:ignore Magento2.Functions.DiscouragedFunction
99+
set_time_limit(0);
100+
try {
101+
$response = $this->getServiceResponse($url);
102+
} finally {
103+
ini_restore('max_execution_time');
104+
}
105+
106+
if (!$this->validateResponse($response, $currencyFrom)) {
107+
$data[$currencyFrom] = $this->makeEmptyResponse($currenciesTo);
108+
return $data;
109+
}
110+
111+
foreach ($currenciesTo as $currencyTo) {
112+
if ($currencyFrom == $currencyTo) {
113+
$data[$currencyFrom][$currencyTo] = $this->_numberFormat(1);
114+
} else {
115+
if (empty($response['rates'][$currencyTo])) {
116+
$message = 'We can\'t retrieve a rate from %1 for %2.';
117+
$this->_messages[] = __($message, self::CURRENCY_CONVERTER_HOST, $currencyTo);
118+
$data[$currencyFrom][$currencyTo] = null;
119+
} else {
120+
$data[$currencyFrom][$currencyTo] = $this->_numberFormat(
121+
(double)$response['rates'][$currencyTo]
122+
);
123+
}
124+
}
125+
}
126+
return $data;
127+
}
128+
129+
/**
130+
* @inheritdoc
131+
*/
132+
protected function _convert($currencyFrom, $currencyTo)
133+
{
134+
return 1;
135+
}
136+
137+
/**
138+
* Get apilayer.com service response
139+
*
140+
* @param string $url
141+
* @param int $retry
142+
* @return array
143+
*/
144+
private function getServiceResponse(string $url, int $retry = 0): array
145+
{
146+
/** @var LaminasClient $httpClient */
147+
$httpClient = $this->httpClientFactory->create();
148+
$response = [];
149+
150+
try {
151+
$httpClient->setUri($url);
152+
$httpClient->setOptions(
153+
[
154+
'timeout' => $this->scopeConfig->getValue(
155+
'currency/apilayer/timeout',
156+
ScopeInterface::SCOPE_STORE
157+
),
158+
]
159+
);
160+
$httpClient->setMethod(Request::METHOD_GET);
161+
$jsonResponse = $httpClient->send()->getBody();
162+
163+
$response = json_decode($jsonResponse, true);
164+
} catch (Exception $e) {
165+
if ($retry == 0) {
166+
$response = $this->getServiceResponse($url, 1);
167+
}
168+
}
169+
return $response;
170+
}
171+
172+
/**
173+
* Creates array for provided currencies with empty rates.
174+
*
175+
* @param array $currenciesTo
176+
* @return array
177+
*/
178+
private function makeEmptyResponse(array $currenciesTo): array
179+
{
180+
return array_fill_keys($currenciesTo, null);
181+
}
182+
183+
/**
184+
* Validates rates response.
185+
*
186+
* @param array $response
187+
* @param string $baseCurrency
188+
* @return bool
189+
*/
190+
private function validateResponse(array $response, string $baseCurrency): bool
191+
{
192+
if ($response['success']) {
193+
return true;
194+
}
195+
196+
$errorCodes = [
197+
101 => __('No API Key was specified or an invalid API Key was specified.'),
198+
102 => __('The account this API request is coming from is inactive.'),
199+
105 => __('The "%1" is not allowed as base currency for your subscription plan.', $baseCurrency),
200+
201 => __('An invalid base currency has been entered.'),
201+
];
202+
203+
$this->_messages[] = $errorCodes[$response['error']['code']] ?? __('Currency rates can\'t be retrieved.');
204+
205+
return false;
206+
}
207+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Directory\Test\Unit\Model\Currency\Import;
9+
10+
use Magento\Directory\Model\Currency;
11+
use Magento\Directory\Model\Currency\Import\FixerIoApiLayer;
12+
use Magento\Directory\Model\CurrencyFactory;
13+
use Magento\Framework\App\Config\ScopeConfigInterface;
14+
use Magento\Framework\DataObject;
15+
use Magento\Framework\HTTP\LaminasClient;
16+
use Magento\Framework\HTTP\LaminasClientFactory;
17+
use PHPUnit\Framework\MockObject\MockObject;
18+
use PHPUnit\Framework\TestCase;
19+
20+
class FixerIoApiLayerTest extends TestCase
21+
{
22+
/**
23+
* @var FixerIoApiLayer
24+
*/
25+
private $model;
26+
27+
/**
28+
* @var CurrencyFactory|MockObject
29+
*/
30+
private $currencyFactory;
31+
32+
/**
33+
* @var LaminasClientFactory|MockObject
34+
*/
35+
private $httpClientFactory;
36+
37+
/**
38+
* @var ScopeConfigInterface|MockObject
39+
*/
40+
private $scopeConfig;
41+
42+
/**
43+
* @inheritdoc
44+
*/
45+
protected function setUp(): void
46+
{
47+
$this->currencyFactory = $this->getMockBuilder(CurrencyFactory::class)
48+
->disableOriginalConstructor()
49+
->setMethods(['create'])
50+
->getMock();
51+
$this->httpClientFactory = $this->getMockBuilder(LaminasClientFactory::class)
52+
->disableOriginalConstructor()
53+
->setMethods(['create'])
54+
->getMock();
55+
$this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class)
56+
->disableOriginalConstructor()
57+
->setMethods([])
58+
->getMockForAbstractClass();
59+
60+
$this->model = new FixerIoApiLayer($this->currencyFactory, $this->scopeConfig, $this->httpClientFactory);
61+
}
62+
63+
/**
64+
* Test Fetch Rates
65+
*
66+
* @return void
67+
*/
68+
public function testFetchRates(): void
69+
{
70+
$currencyFromList = ['USD'];
71+
$currencyToList = ['EUR', 'UAH'];
72+
$responseBody = '{"success":"true","base":"USD","date":"2015-10-07","rates":{"EUR":0.9022}}';
73+
$expectedCurrencyRateList = ['USD' => ['EUR' => 0.9022, 'UAH' => null]];
74+
$message = "We can't retrieve a rate from "
75+
. "https://api.apilayer.com for UAH.";
76+
77+
$this->scopeConfig->method('getValue')
78+
->withConsecutive(
79+
['currency/apilayer/api_key', 'store'],
80+
['currency/apilayer/timeout', 'store']
81+
)
82+
->willReturnOnConsecutiveCalls('api_key', 100);
83+
84+
/** @var Currency|MockObject $currency */
85+
$currency = $this->getMockBuilder(Currency::class)
86+
->disableOriginalConstructor()
87+
->getMock();
88+
/** @var LaminasClient|MockObject $httpClient */
89+
$httpClient = $this->getMockBuilder(LaminasClient::class)
90+
->disableOriginalConstructor()
91+
->getMock();
92+
/** @var DataObject|MockObject $currencyMock */
93+
$httpResponse = $this->getMockBuilder(DataObject::class)
94+
->disableOriginalConstructor()
95+
->setMethods(['getBody'])
96+
->getMock();
97+
98+
$this->currencyFactory->method('create')
99+
->willReturn($currency);
100+
$currency->method('getConfigBaseCurrencies')
101+
->willReturn($currencyFromList);
102+
$currency->method('getConfigAllowCurrencies')
103+
->willReturn($currencyToList);
104+
105+
$this->httpClientFactory->method('create')
106+
->willReturn($httpClient);
107+
$httpClient->method('setUri')
108+
->willReturnSelf();
109+
$httpClient->method('setOptions')
110+
->willReturnSelf();
111+
$httpClient->method('setMethod')
112+
->willReturnSelf();
113+
$httpClient->method('send')
114+
->willReturn($httpResponse);
115+
$httpResponse->method('getBody')
116+
->willReturn($responseBody);
117+
118+
self::assertEquals($expectedCurrencyRateList, $this->model->fetchRates());
119+
120+
$messages = $this->model->getMessages();
121+
self::assertNotEmpty($messages);
122+
self::assertIsArray($messages);
123+
self::assertEquals($message, (string)$messages[0]);
124+
}
125+
}

app/code/Magento/Directory/etc/adminhtml/system.xml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,24 @@
3535
</field>
3636
</group>
3737
<group id="fixerio" translate="label" sortOrder="35" showInDefault="1">
38-
<label>Fixer.io</label>
38+
<label>Fixer.io (legacy)</label>
3939
<field id="api_key" translate="label" type="obscure" sortOrder="5" showInDefault="1">
4040
<label>API Key</label>
4141
<config_path>currency/fixerio/api_key</config_path>
4242
<backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model>
43+
<comment>Use this field if your API Key was generated at Fixer.io. If your key was generated via ApiLayer then use "Setting > General > Currency setup > Fixer Api via APILayer" configuration.</comment>
44+
</field>
45+
<field id="timeout" translate="label" type="text" sortOrder="10" showInDefault="1">
46+
<label>Connection Timeout in Seconds</label>
47+
<validate>validate-zero-or-greater validate-number</validate>
48+
</field>
49+
</group>
50+
<group id="apilayer" translate="label" sortOrder="35" showInDefault="1">
51+
<label>Fixer Api via APILayer</label>
52+
<field id="api_key" translate="label" type="obscure" sortOrder="5" showInDefault="1">
53+
<label>API Key</label>
54+
<config_path>currency/apilayer/api_key</config_path>
55+
<backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model>
4356
</field>
4457
<field id="timeout" translate="label" type="text" sortOrder="10" showInDefault="1">
4558
<label>Connection Timeout in Seconds</label>

app/code/Magento/Directory/etc/config.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
<timeout>100</timeout>
2323
<api_key backend_model="Magento\Config\Model\Config\Backend\Encrypted" />
2424
</fixerio>
25+
<apilayer>
26+
<timeout>100</timeout>
27+
<api_key backend_model="Magento\Config\Model\Config\Backend\Encrypted" />
28+
</apilayer>
2529
<currencyconverterapi>
2630
<timeout>100</timeout>
2731
<api_key backend_model="Magento\Config\Model\Config\Backend\Encrypted" />

app/code/Magento/Directory/etc/di.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@
1111
<arguments>
1212
<argument name="servicesConfig" xsi:type="array">
1313
<item name="fixerio" xsi:type="array">
14-
<item name="label" xsi:type="string" translatable="true">Fixer.io</item>
14+
<item name="label" xsi:type="string" translatable="true">Fixer.io (legacy)</item>
1515
<item name="class" xsi:type="string">Magento\Directory\Model\Currency\Import\FixerIo</item>
1616
</item>
17+
<item name="apilayer" xsi:type="array">
18+
<item name="label" xsi:type="string" translatable="true">Fixer Api via APILayer</item>
19+
<item name="class" xsi:type="string">Magento\Directory\Model\Currency\Import\FixerIoApiLayer</item>
20+
</item>
1721
<item name="currencyconverterapi" xsi:type="array">
1822
<item name="label" xsi:type="string" translatable="true">Currency Converter API</item>
1923
<item name="class" xsi:type="string">Magento\Directory\Model\Currency\Import\CurrencyConverterApi</item>

0 commit comments

Comments
 (0)