Skip to content

Commit 2d20a69

Browse files
committed
Merge remote-tracking branch 'origin/ACP2E-3930' into PR_2025_05_28
2 parents 06c8e42 + 9f6da2f commit 2d20a69

File tree

2 files changed

+265
-11
lines changed

2 files changed

+265
-11
lines changed

app/code/Magento/Fedex/Model/Carrier.php

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Magento\Shipping\Model\Carrier\AbstractCarrier;
2121
use Magento\Shipping\Model\Carrier\AbstractCarrierOnline;
2222
use Magento\Shipping\Model\Rate\Result;
23+
use Magento\Framework\App\CacheInterface;
2324

2425
/**
2526
* Fedex shipping implementation
@@ -174,6 +175,11 @@ class Carrier extends AbstractCarrierOnline implements \Magento\Shipping\Model\C
174175
*/
175176
private $decoderInterface;
176177

178+
/**
179+
* @var CacheInterface
180+
*/
181+
private $cache;
182+
177183
/**
178184
* @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
179185
* @param \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory
@@ -194,6 +200,7 @@ class Carrier extends AbstractCarrierOnline implements \Magento\Shipping\Model\C
194200
* @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory
195201
* @param \Magento\Framework\HTTP\Client\CurlFactory $curlFactory
196202
* @param \Magento\Framework\Url\DecoderInterface $decoderInterface
203+
* @param CacheInterface $cache
197204
* @param array $data
198205
* @param Json|null $serializer
199206
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
@@ -218,6 +225,7 @@ public function __construct(
218225
\Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory,
219226
CurlFactory $curlFactory,
220227
DecoderInterface $decoderInterface,
228+
CacheInterface $cache,
221229
array $data = [],
222230
?Json $serializer = null
223231
) {
@@ -244,6 +252,7 @@ public function __construct(
244252
$this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class);
245253
$this->curlFactory = $curlFactory;
246254
$this->decoderInterface = $decoderInterface;
255+
$this->cache = $cache;
247256
}
248257

249258
/**
@@ -962,11 +971,14 @@ protected function _getAccessToken(): string|null
962971
*/
963972
private function retrieveAccessToken(?string $apiKey, ?string $secretKey): string|null
964973
{
965-
if (!$apiKey || !$secretKey) {
966-
$this->_debug(__('Authentication keys are missing.'));
974+
if (!$this->areAuthKeysValid($apiKey, $secretKey)) {
967975
return null;
968976
}
969-
977+
$cacheKey = 'fedex_access_token_' . hash('sha256', $apiKey . $secretKey);
978+
$cacheType = 'fedex_api';
979+
if ($cachedToken = $this->getCachedAccessToken($cacheKey)) {
980+
return $cachedToken;
981+
}
970982
$requestArray = [
971983
'grant_type' => self::AUTHENTICATION_GRANT_TYPE,
972984
'client_id' => $apiKey,
@@ -980,13 +992,57 @@ private function retrieveAccessToken(?string $apiKey, ?string $secretKey): strin
980992
if (!empty($response['errors'])) {
981993
$debugData = ['request_type' => 'Access Token Request', 'result' => $response];
982994
$this->_debug($debugData);
983-
} elseif (!empty($response['access_token'])) {
995+
} elseif (!empty($response['access_token']) && isset($response['expires_in'])) {
984996
$accessToken = $response['access_token'];
997+
$expiresAt = time() + (int)$response['expires_in'];
998+
$cacheData = [
999+
'access_token' => $accessToken,
1000+
'expires_at' => $expiresAt
1001+
];
1002+
$this->cache->save(json_encode($cacheData), $cacheKey, [$cacheType], (int)$response['expires_in']);
9851003
}
9861004

9871005
return $accessToken;
9881006
}
9891007

1008+
/**
1009+
* Validate apiKey and secretKey
1010+
*
1011+
* @param string|null $apiKey
1012+
* @param string|null $secretKey
1013+
* @return bool
1014+
*/
1015+
private function areAuthKeysValid(?string $apiKey, ?string $secretKey): bool
1016+
{
1017+
if (!$apiKey || !$secretKey) {
1018+
$this->_debug(__('Authentication keys are missing.'));
1019+
return false;
1020+
}
1021+
return true;
1022+
}
1023+
1024+
/**
1025+
* Retrieve access token from cache
1026+
*
1027+
* @param string $cacheKey
1028+
* @return string|null
1029+
*/
1030+
private function getCachedAccessToken(string $cacheKey): ?string
1031+
{
1032+
$cachedData = $this->cache->load($cacheKey);
1033+
if (!$cachedData) {
1034+
return null;
1035+
}
1036+
1037+
$cachedData = json_decode($cachedData, true);
1038+
$currentTime = time();
1039+
if (isset($cachedData['access_token'], $cachedData['expires_at']) && $currentTime < $cachedData['expires_at']) {
1040+
return $cachedData['access_token'];
1041+
}
1042+
1043+
return null;
1044+
}
1045+
9901046
/**
9911047
* Get Access Token for Tracking Rest API
9921048
*

app/code/Magento/Fedex/Test/Unit/Model/CarrierTest.php

Lines changed: 205 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Magento\Framework\Exception\LocalizedException;
2323
use Magento\Framework\HTTP\Client\Curl;
2424
use Magento\Framework\HTTP\Client\CurlFactory;
25+
use Magento\Framework\HTTP\ClientInterface;
2526
use Magento\Framework\Pricing\PriceCurrencyInterface;
2627
use Magento\Framework\Serialize\Serializer\Json;
2728
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
@@ -49,6 +50,7 @@
4950
use PHPUnit\Framework\TestCase;
5051
use Psr\Log\LoggerInterface;
5152
use Magento\Catalog\Api\Data\ProductInterface;
53+
use Magento\Framework\App\CacheInterface;
5254

5355
/**
5456
* CarrierTest contains units test for Fedex carrier methods
@@ -102,11 +104,6 @@ class CarrierTest extends TestCase
102104
*/
103105
private Json $serializer;
104106

105-
/**
106-
* @var LoggerInterface|MockObject
107-
*/
108-
private LoggerInterface $logger;
109-
110107
/**
111108
* @var CurrencyFactory|MockObject
112109
*/
@@ -132,6 +129,11 @@ class CarrierTest extends TestCase
132129
*/
133130
private DecoderInterface $decoderInterface;
134131

132+
/**
133+
* @var CacheInterface|MockObject
134+
*/
135+
private $cacheMock;
136+
135137
/**
136138
* @return void
137139
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
@@ -189,7 +191,7 @@ protected function setUp(): void
189191
->disableOriginalConstructor()
190192
->getMock();
191193

192-
$this->logger = $this->getMockForAbstractClass(LoggerInterface::class);
194+
$logger = $this->getMockForAbstractClass(LoggerInterface::class);
193195

194196
$this->curlFactory = $this->getMockBuilder(CurlFactory::class)
195197
->disableOriginalConstructor()
@@ -206,13 +208,16 @@ protected function setUp(): void
206208
->onlyMethods(['decode'])
207209
->getMock();
208210

211+
$this->cacheMock = $this->getMockBuilder(CacheInterface::class)
212+
->onlyMethods(['load', 'save'])
213+
->getMockForAbstractClass();
209214
$this->carrier = $this->getMockBuilder(Carrier::class)
210215
->addMethods(['rateRequest'])
211216
->setConstructorArgs(
212217
[
213218
'scopeConfig' => $this->scope,
214219
'rateErrorFactory' => $this->errorFactory,
215-
'logger' => $this->logger,
220+
'logger' => $logger,
216221
'xmlSecurity' => new Security(),
217222
'xmlElFactory' => $elementFactory,
218223
'rateFactory' => $rateFactory,
@@ -229,6 +234,7 @@ protected function setUp(): void
229234
'productCollectionFactory' => $this->collectionFactory,
230235
'curlFactory' => $this->curlFactory,
231236
'decoderInterface' => $this->decoderInterface,
237+
'cache' => $this->cacheMock,
232238
'data' => [],
233239
'serializer' => $this->serializer,
234240
]
@@ -1064,6 +1070,173 @@ public function testGetCodeWithDropoffTypeNoCode(): void
10641070
$this->assertEquals(__('Regular Pickup'), $result['REGULAR_PICKUP']);
10651071
}
10661072

1073+
/**
1074+
* Test get access token from cache
1075+
*/
1076+
public function testCollectRatesWithCachedAccessToken(): void
1077+
{
1078+
$apiKey = 'TestApiKey';
1079+
$secretKey = 'TestSecretKey';
1080+
$accessToken = 'CachedTestAccessToken';
1081+
$cacheKey = 'fedex_access_token_' . hash('sha256', $apiKey . $secretKey);
1082+
$expiresAt = time() + 3600;
1083+
$cachedData = json_encode([
1084+
'access_token' => $accessToken,
1085+
'expires_at' => $expiresAt
1086+
]);
1087+
$this->scope->expects($this->any())
1088+
->method('getValue')
1089+
->willReturnCallback([$this, 'scopeConfigGetValue']);
1090+
$this->scope->expects($this->exactly(2))
1091+
->method('isSetFlag')
1092+
->willReturn(true);
1093+
1094+
$this->cacheMock->expects($this->once())
1095+
->method('load')
1096+
->with($cacheKey)
1097+
->willReturn($cachedData);
1098+
1099+
$rateResponseMock = [
1100+
'output' => [
1101+
'rateReplyDetails' => [
1102+
[
1103+
'serviceType' => 'FEDEX_GROUND',
1104+
'ratedShipmentDetails' => [
1105+
[
1106+
'totalNetCharge' => '28.75',
1107+
'currency' => 'USD',
1108+
'ratedPackages' => [
1109+
['packageRateDetail' => ['rateType' => 'RATED_ACCOUNT_PACKAGE']]
1110+
]
1111+
]
1112+
]
1113+
]
1114+
]
1115+
]
1116+
];
1117+
$this->serializer->expects($this->once())
1118+
->method('serialize')
1119+
->willReturn(json_encode(['mocked_request' => 'data']));
1120+
$this->serializer->expects($this->once())
1121+
->method('unserialize')
1122+
->willReturn($rateResponseMock);
1123+
$this->curlFactory->expects($this->once())
1124+
->method('create')
1125+
->willReturn($this->curlClient);
1126+
$this->curlClient->expects($this->once())
1127+
->method('setHeaders')
1128+
->willReturnSelf();
1129+
$this->curlClient->expects($this->once())
1130+
->method('post')
1131+
->willReturnSelf();
1132+
$this->curlClient->expects($this->once())
1133+
->method('getBody')
1134+
->willReturn(json_encode($rateResponseMock));
1135+
$request = $this->getMockBuilder(RateRequest::class)
1136+
->addMethods(['getBaseCurrency', 'getPackageWeight'])
1137+
->disableOriginalConstructor()
1138+
->getMock();
1139+
$request->method('getPackageWeight')
1140+
->willReturn(10.0);
1141+
$result = $this->carrier->collectRates($request);
1142+
$this->assertInstanceOf(RateResult::class, $result);
1143+
$rates = $result->getAllRates();
1144+
$this->assertNotEmpty($rates);
1145+
}
1146+
1147+
/**
1148+
* Test getTracking when a new access token is requested and saved to cache
1149+
*/
1150+
public function testGetTrackingWithNewAccessTokenSavedToCache(): void
1151+
{
1152+
$apiKey = 'TestApiKey';
1153+
$secretKey = 'TestSecretKey';
1154+
$accessToken = 'NewTrackingTestAccessToken';
1155+
$cacheKey = 'fedex_access_token_' . hash('sha256', $apiKey . $secretKey);
1156+
$expiresIn = 3600;
1157+
$cacheType = 'fedex_api';
1158+
$tokenResponse = [
1159+
'access_token' => $accessToken,
1160+
'expires_in' => $expiresIn
1161+
];
1162+
$trackingNumber = '123456789012';
1163+
$this->scope->expects($this->any())
1164+
->method('getValue')
1165+
->willReturnCallback([$this, 'scopeConfigGetValue']);
1166+
$this->cacheMock->expects($this->once())
1167+
->method('load')
1168+
->with($cacheKey)
1169+
->willReturn(false);
1170+
$this->cacheMock->expects($this->once())
1171+
->method('save')
1172+
->with(
1173+
$this->callback(function ($data) use ($accessToken, $expiresIn) {
1174+
$decoded = json_decode($data, true);
1175+
return $decoded['access_token'] === $accessToken &&
1176+
$decoded['expires_at'] <= (time() + $expiresIn) &&
1177+
$decoded['expires_at'] > time();
1178+
}),
1179+
$cacheKey,
1180+
[$cacheType],
1181+
$expiresIn
1182+
)
1183+
->willReturn(true);
1184+
$curlTokenClient = $this->createMock(ClientInterface::class);
1185+
$this->curlFactory->expects($this->exactly(2))
1186+
->method('create')
1187+
->willReturnOnConsecutiveCalls($curlTokenClient, $this->curlClient);
1188+
$curlTokenClient->expects($this->once())
1189+
->method('setHeaders')
1190+
->willReturnSelf();
1191+
$curlTokenClient->expects($this->once())
1192+
->method('post')
1193+
->willReturnSelf();
1194+
$curlTokenClient->expects($this->once())
1195+
->method('getBody')
1196+
->willReturn(json_encode($tokenResponse));
1197+
$trackingResponse = $this->getTrackingResponse();
1198+
$trackingStatusMock = $this->getMockBuilder(Status::class)
1199+
->addMethods(['setCarrier', 'setCarrierTitle', 'setTracking'])
1200+
->onlyMethods(['addData'])
1201+
->getMock();
1202+
$this->statusFactory->expects($this->once())
1203+
->method('create')
1204+
->willReturn($trackingStatusMock);
1205+
$trackingStatusMock->expects($this->once())
1206+
->method('setCarrier')
1207+
->with(Carrier::CODE)
1208+
->willReturnSelf();
1209+
$trackingStatusMock->expects($this->once())
1210+
->method('setCarrierTitle')
1211+
->willReturnSelf();
1212+
$trackingStatusMock->expects($this->once())
1213+
->method('setTracking')
1214+
->with($trackingNumber)
1215+
->willReturnSelf();
1216+
$trackingStatusMock->expects($this->once())
1217+
->method('addData')
1218+
->willReturnSelf();
1219+
$this->serializer->expects($this->once())
1220+
->method('serialize')
1221+
->willReturn(json_encode($this->getTrackRequest($trackingNumber)));
1222+
$this->serializer->expects($this->exactly(2))
1223+
->method('unserialize')
1224+
->willReturnOnConsecutiveCalls($tokenResponse, $trackingResponse);
1225+
$this->curlClient->expects($this->once())
1226+
->method('setHeaders')
1227+
->willReturnSelf();
1228+
$this->curlClient->expects($this->once())
1229+
->method('post')
1230+
->willReturnSelf();
1231+
$this->curlClient->expects($this->once())
1232+
->method('getBody')
1233+
->willReturn(json_encode($trackingResponse));
1234+
$trackings = [$trackingNumber];
1235+
$result = $this->carrier->getTracking($trackings);
1236+
$this->assertInstanceOf(Result::class, $result);
1237+
$trackingsResult = $result->getAllTrackings();
1238+
$this->assertNotEmpty($trackingsResult);
1239+
}
10671240
/**
10681241
* Gets list of variations for testing ship date.
10691242
*
@@ -1314,4 +1487,29 @@ private function getShipmentRequestMock(): MockObject
13141487
])
13151488
->getMock();
13161489
}
1490+
1491+
/**
1492+
* @return array
1493+
*/
1494+
private function getTrackingResponse(): array
1495+
{
1496+
return [
1497+
'output' => [
1498+
'completeTrackResults' => [
1499+
[
1500+
'trackingNumber' => '123456789012',
1501+
'trackResults' => [
1502+
[
1503+
'trackingNumberInfo' => ['trackingNumber' => '123456789012'],
1504+
'statusDetail' => ['description' => 'Delivered'],
1505+
'dateAndTimes' => [
1506+
['type' => 'ACTUAL_DELIVERY', 'dateTime' => '2025-05-20T10:00:00Z']
1507+
]
1508+
]
1509+
]
1510+
]
1511+
]
1512+
]
1513+
];
1514+
}
13171515
}

0 commit comments

Comments
 (0)