Skip to content

Commit dea1326

Browse files
committed
Merge remote-tracking branch 'local/ACP2E-1044' into PR_1_NOV_2022
2 parents 55f034a + e15b137 commit dea1326

File tree

5 files changed

+409
-2
lines changed

5 files changed

+409
-2
lines changed
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
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 implements ImportInterface
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 array
29+
*/
30+
private $messages = [];
31+
32+
/**
33+
* @var HttpClientFactory
34+
*/
35+
private $httpClientFactory;
36+
37+
/**
38+
* @var CurrencyFactory
39+
*/
40+
private $currencyFactory;
41+
42+
/**
43+
* Core scope config
44+
*
45+
* @var ScopeConfigInterface
46+
*/
47+
private $scopeConfig;
48+
49+
/**
50+
* Initialize dependencies
51+
*
52+
* @param CurrencyFactory $currencyFactory
53+
* @param ScopeConfigInterface $scopeConfig
54+
* @param HttpClientFactory $httpClientFactory
55+
*/
56+
public function __construct(
57+
CurrencyFactory $currencyFactory,
58+
ScopeConfigInterface $scopeConfig,
59+
HttpClientFactory $httpClientFactory
60+
) {
61+
$this->currencyFactory = $currencyFactory;
62+
$this->scopeConfig = $scopeConfig;
63+
$this->httpClientFactory = $httpClientFactory;
64+
}
65+
66+
/**
67+
* Import rates
68+
*
69+
* @return $this
70+
*/
71+
public function importRates()
72+
{
73+
$data = $this->fetchRates();
74+
$this->saveRates($data);
75+
return $this;
76+
}
77+
78+
/**
79+
* @inheritdoc
80+
*/
81+
public function fetchRates()
82+
{
83+
$data = [];
84+
$currencies = $this->getCurrencyCodes();
85+
$defaultCurrencies = $this->getDefaultCurrencyCodes();
86+
87+
foreach ($defaultCurrencies as $currencyFrom) {
88+
if (!isset($data[$currencyFrom])) {
89+
$data[$currencyFrom] = [];
90+
}
91+
$data = $this->convertBatch($data, $currencyFrom, $currencies);
92+
ksort($data[$currencyFrom]);
93+
}
94+
return $data;
95+
}
96+
97+
/**
98+
* @inheritdoc
99+
*/
100+
public function getMessages()
101+
{
102+
return $this->messages;
103+
}
104+
105+
/**
106+
* Return currencies convert rates in batch mode
107+
*
108+
* @param array $data
109+
* @param string $currencyFrom
110+
* @param array $currenciesTo
111+
* @return array
112+
*/
113+
private function convertBatch(array $data, string $currencyFrom, array $currenciesTo): array
114+
{
115+
$accessKey = $this->scopeConfig->getValue('currency/fixerio_apilayer/api_key', ScopeInterface::SCOPE_STORE);
116+
if (empty($accessKey)) {
117+
$this->messages[] = __('No API Key was specified or an invalid API Key was specified.');
118+
$data[$currencyFrom] = $this->makeEmptyResponse($currenciesTo);
119+
return $data;
120+
}
121+
122+
$currenciesStr = implode(',', $currenciesTo);
123+
$url = str_replace(
124+
['{{ACCESS_KEY}}', '{{CURRENCY_FROM}}', '{{CURRENCY_TO}}'],
125+
[$accessKey, $currencyFrom, $currenciesStr],
126+
self::CURRENCY_CONVERTER_HOST . self::CURRENCY_CONVERTER_URL_PATH
127+
);
128+
// phpcs:ignore Magento2.Functions.DiscouragedFunction
129+
set_time_limit(0);
130+
try {
131+
$response = $this->getServiceResponse($url);
132+
} finally {
133+
ini_restore('max_execution_time');
134+
}
135+
136+
if (!$this->validateResponse($response, $currencyFrom)) {
137+
$data[$currencyFrom] = $this->makeEmptyResponse($currenciesTo);
138+
return $data;
139+
}
140+
141+
foreach ($currenciesTo as $currencyTo) {
142+
if ($currencyFrom == $currencyTo) {
143+
$data[$currencyFrom][$currencyTo] = 1;
144+
} else {
145+
if (empty($response['rates'][$currencyTo])) {
146+
$message = 'We can\'t retrieve a rate from %1 for %2.';
147+
$this->messages[] = __($message, self::CURRENCY_CONVERTER_HOST, $currencyTo);
148+
$data[$currencyFrom][$currencyTo] = null;
149+
} else {
150+
$data[$currencyFrom][$currencyTo] = (double)$response['rates'][$currencyTo];
151+
}
152+
}
153+
}
154+
return $data;
155+
}
156+
157+
/**
158+
* Saving currency rates
159+
*
160+
* @param array $rates
161+
* @return \Magento\Directory\Model\Currency\Import\FixerIoApiLayer
162+
*/
163+
private function saveRates(array $rates)
164+
{
165+
foreach ($rates as $currencyCode => $currencyRates) {
166+
$this->currencyFactory->create()->setId($currencyCode)->setRates($currencyRates)->save();
167+
}
168+
return $this;
169+
}
170+
171+
/**
172+
* Get apilayer.com service response
173+
*
174+
* @param string $url
175+
* @param int $retry
176+
* @return array
177+
*/
178+
private function getServiceResponse(string $url, int $retry = 0): array
179+
{
180+
/** @var LaminasClient $httpClient */
181+
$httpClient = $this->httpClientFactory->create();
182+
$response = [];
183+
184+
try {
185+
$httpClient->setUri($url);
186+
$httpClient->setOptions(
187+
[
188+
'timeout' => $this->scopeConfig->getValue(
189+
'currency/fixerio_apilayer/timeout',
190+
ScopeInterface::SCOPE_STORE
191+
),
192+
]
193+
);
194+
$httpClient->setMethod(Request::METHOD_GET);
195+
$jsonResponse = $httpClient->send()->getBody();
196+
197+
$response = json_decode($jsonResponse, true);
198+
} catch (Exception $e) {
199+
if ($retry == 0) {
200+
$response = $this->getServiceResponse($url, 1);
201+
}
202+
}
203+
return $response;
204+
}
205+
206+
/**
207+
* Creates array for provided currencies with empty rates.
208+
*
209+
* @param array $currenciesTo
210+
* @return array
211+
*/
212+
private function makeEmptyResponse(array $currenciesTo): array
213+
{
214+
return array_fill_keys($currenciesTo, null);
215+
}
216+
217+
/**
218+
* Validates rates response.
219+
*
220+
* @param array $response
221+
* @param string $baseCurrency
222+
* @return bool
223+
*/
224+
private function validateResponse(array $response, string $baseCurrency): bool
225+
{
226+
if ($response['success']) {
227+
return true;
228+
}
229+
230+
$errorCodes = [
231+
101 => __('No API Key was specified or an invalid API Key was specified.'),
232+
102 => __('The account this API request is coming from is inactive.'),
233+
105 => __('The "%1" is not allowed as base currency for your subscription plan.', $baseCurrency),
234+
201 => __('An invalid base currency has been entered.'),
235+
];
236+
237+
$this->messages[] = $errorCodes[$response['error']['code']] ?? __('Currency rates can\'t be retrieved.');
238+
239+
return false;
240+
}
241+
242+
/**
243+
* Retrieve currency codes
244+
*
245+
* @return array
246+
*/
247+
private function getCurrencyCodes()
248+
{
249+
return $this->currencyFactory->create()->getConfigAllowCurrencies();
250+
}
251+
252+
/**
253+
* Retrieve default currency codes
254+
*
255+
* @return array
256+
*/
257+
private function getDefaultCurrencyCodes()
258+
{
259+
return $this->currencyFactory->create()->getConfigBaseCurrencies();
260+
}
261+
}
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/fixerio_apilayer/api_key', 'store'],
80+
['currency/fixerio_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+
}

0 commit comments

Comments
 (0)