Carrier.php 64 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Fedex\Model;
  7. use Magento\Framework\App\ObjectManager;
  8. use Magento\Framework\DataObject;
  9. use Magento\Framework\Module\Dir;
  10. use Magento\Framework\Serialize\Serializer\Json;
  11. use Magento\Framework\Webapi\Soap\ClientFactory;
  12. use Magento\Framework\Xml\Security;
  13. use Magento\Quote\Model\Quote\Address\RateRequest;
  14. use Magento\Shipping\Model\Carrier\AbstractCarrierOnline;
  15. use Magento\Shipping\Model\Rate\Result;
  16. /**
  17. * Fedex shipping implementation
  18. *
  19. * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
  20. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  21. */
  22. class Carrier extends AbstractCarrierOnline implements \Magento\Shipping\Model\Carrier\CarrierInterface
  23. {
  24. /**
  25. * Code of the carrier
  26. *
  27. * @var string
  28. */
  29. const CODE = 'fedex';
  30. /**
  31. * Purpose of rate request
  32. *
  33. * @var string
  34. */
  35. const RATE_REQUEST_GENERAL = 'general';
  36. /**
  37. * Purpose of rate request
  38. *
  39. * @var string
  40. */
  41. const RATE_REQUEST_SMARTPOST = 'SMART_POST';
  42. /**
  43. * Code of the carrier
  44. *
  45. * @var string
  46. */
  47. protected $_code = self::CODE;
  48. /**
  49. * Types of rates, order is important
  50. *
  51. * @var array
  52. */
  53. protected $_ratesOrder = [
  54. 'RATED_ACCOUNT_PACKAGE',
  55. 'PAYOR_ACCOUNT_PACKAGE',
  56. 'RATED_ACCOUNT_SHIPMENT',
  57. 'PAYOR_ACCOUNT_SHIPMENT',
  58. 'RATED_LIST_PACKAGE',
  59. 'PAYOR_LIST_PACKAGE',
  60. 'RATED_LIST_SHIPMENT',
  61. 'PAYOR_LIST_SHIPMENT',
  62. ];
  63. /**
  64. * Rate request data
  65. *
  66. * @var RateRequest|null
  67. */
  68. protected $_request = null;
  69. /**
  70. * Rate result data
  71. *
  72. * @var Result|null
  73. */
  74. protected $_result = null;
  75. /**
  76. * Path to wsdl file of rate service
  77. *
  78. * @var string
  79. */
  80. protected $_rateServiceWsdl;
  81. /**
  82. * Path to wsdl file of ship service
  83. *
  84. * @var string
  85. */
  86. protected $_shipServiceWsdl = null;
  87. /**
  88. * Path to wsdl file of track service
  89. *
  90. * @var string
  91. */
  92. protected $_trackServiceWsdl = null;
  93. /**
  94. * Container types that could be customized for FedEx carrier
  95. *
  96. * @var string[]
  97. */
  98. protected $_customizableContainerTypes = ['YOUR_PACKAGING'];
  99. /**
  100. * @var \Magento\Store\Model\StoreManagerInterface
  101. */
  102. protected $_storeManager;
  103. /**
  104. * @var \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory
  105. */
  106. protected $_productCollectionFactory;
  107. /**
  108. * @inheritdoc
  109. */
  110. protected $_debugReplacePrivateDataKeys = [
  111. 'Key', 'Password', 'MeterNumber',
  112. ];
  113. /**
  114. * Version of tracking service
  115. * @var int
  116. */
  117. private static $trackServiceVersion = 10;
  118. /**
  119. * List of TrackReply errors
  120. * @var array
  121. */
  122. private static $trackingErrors = ['FAILURE', 'ERROR'];
  123. /**
  124. * @var Json
  125. */
  126. private $serializer;
  127. /**
  128. * @var ClientFactory
  129. */
  130. private $soapClientFactory;
  131. /**
  132. * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
  133. * @param \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory
  134. * @param \Psr\Log\LoggerInterface $logger
  135. * @param Security $xmlSecurity
  136. * @param \Magento\Shipping\Model\Simplexml\ElementFactory $xmlElFactory
  137. * @param \Magento\Shipping\Model\Rate\ResultFactory $rateFactory
  138. * @param \Magento\Quote\Model\Quote\Address\RateResult\MethodFactory $rateMethodFactory
  139. * @param \Magento\Shipping\Model\Tracking\ResultFactory $trackFactory
  140. * @param \Magento\Shipping\Model\Tracking\Result\ErrorFactory $trackErrorFactory
  141. * @param \Magento\Shipping\Model\Tracking\Result\StatusFactory $trackStatusFactory
  142. * @param \Magento\Directory\Model\RegionFactory $regionFactory
  143. * @param \Magento\Directory\Model\CountryFactory $countryFactory
  144. * @param \Magento\Directory\Model\CurrencyFactory $currencyFactory
  145. * @param \Magento\Directory\Helper\Data $directoryData
  146. * @param \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry
  147. * @param \Magento\Store\Model\StoreManagerInterface $storeManager
  148. * @param \Magento\Framework\Module\Dir\Reader $configReader
  149. * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory
  150. * @param array $data
  151. * @param Json|null $serializer
  152. * @param ClientFactory|null $soapClientFactory
  153. * @SuppressWarnings(PHPMD.ExcessiveParameterList)
  154. */
  155. public function __construct(
  156. \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
  157. \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory,
  158. \Psr\Log\LoggerInterface $logger,
  159. Security $xmlSecurity,
  160. \Magento\Shipping\Model\Simplexml\ElementFactory $xmlElFactory,
  161. \Magento\Shipping\Model\Rate\ResultFactory $rateFactory,
  162. \Magento\Quote\Model\Quote\Address\RateResult\MethodFactory $rateMethodFactory,
  163. \Magento\Shipping\Model\Tracking\ResultFactory $trackFactory,
  164. \Magento\Shipping\Model\Tracking\Result\ErrorFactory $trackErrorFactory,
  165. \Magento\Shipping\Model\Tracking\Result\StatusFactory $trackStatusFactory,
  166. \Magento\Directory\Model\RegionFactory $regionFactory,
  167. \Magento\Directory\Model\CountryFactory $countryFactory,
  168. \Magento\Directory\Model\CurrencyFactory $currencyFactory,
  169. \Magento\Directory\Helper\Data $directoryData,
  170. \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry,
  171. \Magento\Store\Model\StoreManagerInterface $storeManager,
  172. \Magento\Framework\Module\Dir\Reader $configReader,
  173. \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory,
  174. array $data = [],
  175. Json $serializer = null,
  176. ClientFactory $soapClientFactory = null
  177. ) {
  178. $this->_storeManager = $storeManager;
  179. $this->_productCollectionFactory = $productCollectionFactory;
  180. parent::__construct(
  181. $scopeConfig,
  182. $rateErrorFactory,
  183. $logger,
  184. $xmlSecurity,
  185. $xmlElFactory,
  186. $rateFactory,
  187. $rateMethodFactory,
  188. $trackFactory,
  189. $trackErrorFactory,
  190. $trackStatusFactory,
  191. $regionFactory,
  192. $countryFactory,
  193. $currencyFactory,
  194. $directoryData,
  195. $stockRegistry,
  196. $data
  197. );
  198. $wsdlBasePath = $configReader->getModuleDir(Dir::MODULE_ETC_DIR, 'Magento_Fedex') . '/wsdl/';
  199. $this->_shipServiceWsdl = $wsdlBasePath . 'ShipService_v10.wsdl';
  200. $this->_rateServiceWsdl = $wsdlBasePath . 'RateService_v10.wsdl';
  201. $this->_trackServiceWsdl = $wsdlBasePath . 'TrackService_v' . self::$trackServiceVersion . '.wsdl';
  202. $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class);
  203. $this->soapClientFactory = $soapClientFactory ?: ObjectManager::getInstance()->get(ClientFactory::class);
  204. }
  205. /**
  206. * Create soap client with selected wsdl
  207. *
  208. * @param string $wsdl
  209. * @param bool|int $trace
  210. * @return \SoapClient
  211. */
  212. protected function _createSoapClient($wsdl, $trace = false)
  213. {
  214. $client = $this->soapClientFactory->create($wsdl, ['trace' => $trace]);
  215. $client->__setLocation(
  216. $this->getConfigFlag(
  217. 'sandbox_mode'
  218. ) ? $this->getConfigData('sandbox_webservices_url') : $this->getConfigData('production_webservices_url')
  219. );
  220. return $client;
  221. }
  222. /**
  223. * Create rate soap client
  224. *
  225. * @return \SoapClient
  226. */
  227. protected function _createRateSoapClient()
  228. {
  229. return $this->_createSoapClient($this->_rateServiceWsdl);
  230. }
  231. /**
  232. * Create ship soap client
  233. *
  234. * @return \SoapClient
  235. */
  236. protected function _createShipSoapClient()
  237. {
  238. return $this->_createSoapClient($this->_shipServiceWsdl, 1);
  239. }
  240. /**
  241. * Create track soap client
  242. *
  243. * @return \SoapClient
  244. */
  245. protected function _createTrackSoapClient()
  246. {
  247. return $this->_createSoapClient($this->_trackServiceWsdl, 1);
  248. }
  249. /**
  250. * Collect and get rates
  251. *
  252. * @param RateRequest $request
  253. * @return Result|bool|null
  254. */
  255. public function collectRates(RateRequest $request)
  256. {
  257. if (!$this->canCollectRates()) {
  258. return $this->getErrorMessage();
  259. }
  260. $this->setRequest($request);
  261. $this->_getQuotes();
  262. $this->_updateFreeMethodQuote($request);
  263. return $this->getResult();
  264. }
  265. /**
  266. * Prepare and set request to this instance
  267. *
  268. * @param RateRequest $request
  269. * @return $this
  270. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  271. * @SuppressWarnings(PHPMD.NPathComplexity)
  272. */
  273. public function setRequest(RateRequest $request)
  274. {
  275. $this->_request = $request;
  276. $r = new \Magento\Framework\DataObject();
  277. if ($request->getLimitMethod()) {
  278. $r->setService($request->getLimitMethod());
  279. }
  280. if ($request->getFedexAccount()) {
  281. $account = $request->getFedexAccount();
  282. } else {
  283. $account = $this->getConfigData('account');
  284. }
  285. $r->setAccount($account);
  286. if ($request->getFedexDropoff()) {
  287. $dropoff = $request->getFedexDropoff();
  288. } else {
  289. $dropoff = $this->getConfigData('dropoff');
  290. }
  291. $r->setDropoffType($dropoff);
  292. if ($request->getFedexPackaging()) {
  293. $packaging = $request->getFedexPackaging();
  294. } else {
  295. $packaging = $this->getConfigData('packaging');
  296. }
  297. $r->setPackaging($packaging);
  298. if ($request->getOrigCountry()) {
  299. $origCountry = $request->getOrigCountry();
  300. } else {
  301. $origCountry = $this->_scopeConfig->getValue(
  302. \Magento\Sales\Model\Order\Shipment::XML_PATH_STORE_COUNTRY_ID,
  303. \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
  304. $request->getStoreId()
  305. );
  306. }
  307. $r->setOrigCountry($this->_countryFactory->create()->load($origCountry)->getData('iso2_code'));
  308. if ($request->getOrigPostcode()) {
  309. $r->setOrigPostal($request->getOrigPostcode());
  310. } else {
  311. $r->setOrigPostal(
  312. $this->_scopeConfig->getValue(
  313. \Magento\Sales\Model\Order\Shipment::XML_PATH_STORE_ZIP,
  314. \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
  315. $request->getStoreId()
  316. )
  317. );
  318. }
  319. if ($request->getDestCountryId()) {
  320. $destCountry = $request->getDestCountryId();
  321. } else {
  322. $destCountry = self::USA_COUNTRY_ID;
  323. }
  324. $r->setDestCountry($this->_countryFactory->create()->load($destCountry)->getData('iso2_code'));
  325. if ($request->getDestPostcode()) {
  326. $r->setDestPostal($request->getDestPostcode());
  327. } else {
  328. }
  329. if ($request->getDestCity()) {
  330. $r->setDestCity($request->getDestCity());
  331. }
  332. $weight = $this->getTotalNumOfBoxes($request->getPackageWeight());
  333. $r->setWeight($weight);
  334. if ($request->getFreeMethodWeight() != $request->getPackageWeight()) {
  335. $r->setFreeMethodWeight($request->getFreeMethodWeight());
  336. }
  337. $r->setValue($request->getPackagePhysicalValue());
  338. $r->setValueWithDiscount($request->getPackageValueWithDiscount());
  339. $r->setMeterNumber($this->getConfigData('meter_number'));
  340. $r->setKey($this->getConfigData('key'));
  341. $r->setPassword($this->getConfigData('password'));
  342. $r->setIsReturn($request->getIsReturn());
  343. $r->setBaseSubtotalInclTax($request->getBaseSubtotalInclTax());
  344. $this->setRawRequest($r);
  345. return $this;
  346. }
  347. /**
  348. * Get result of request
  349. *
  350. * @return Result|null
  351. */
  352. public function getResult()
  353. {
  354. if (!$this->_result) {
  355. $this->_result = $this->_trackFactory->create();
  356. }
  357. return $this->_result;
  358. }
  359. /**
  360. * Get version of rates request
  361. *
  362. * @return array
  363. */
  364. public function getVersionInfo()
  365. {
  366. return ['ServiceId' => 'crs', 'Major' => '10', 'Intermediate' => '0', 'Minor' => '0'];
  367. }
  368. /**
  369. * Forming request for rate estimation depending to the purpose
  370. *
  371. * @param string $purpose
  372. * @return array
  373. */
  374. protected function _formRateRequest($purpose)
  375. {
  376. $r = $this->_rawRequest;
  377. $ratesRequest = [
  378. 'WebAuthenticationDetail' => [
  379. 'UserCredential' => ['Key' => $r->getKey(), 'Password' => $r->getPassword()],
  380. ],
  381. 'ClientDetail' => ['AccountNumber' => $r->getAccount(), 'MeterNumber' => $r->getMeterNumber()],
  382. 'Version' => $this->getVersionInfo(),
  383. 'RequestedShipment' => [
  384. 'DropoffType' => $r->getDropoffType(),
  385. 'ShipTimestamp' => date('c'),
  386. 'PackagingType' => $r->getPackaging(),
  387. 'TotalInsuredValue' => ['Amount' => $r->getValue(), 'Currency' => $this->getCurrencyCode()],
  388. 'Shipper' => [
  389. 'Address' => ['PostalCode' => $r->getOrigPostal(), 'CountryCode' => $r->getOrigCountry()],
  390. ],
  391. 'Recipient' => [
  392. 'Address' => [
  393. 'PostalCode' => $r->getDestPostal(),
  394. 'CountryCode' => $r->getDestCountry(),
  395. 'Residential' => (bool)$this->getConfigData('residence_delivery'),
  396. ],
  397. ],
  398. 'ShippingChargesPayment' => [
  399. 'PaymentType' => 'SENDER',
  400. 'Payor' => ['AccountNumber' => $r->getAccount(), 'CountryCode' => $r->getOrigCountry()],
  401. ],
  402. 'CustomsClearanceDetail' => [
  403. 'CustomsValue' => ['Amount' => $r->getValue(), 'Currency' => $this->getCurrencyCode()],
  404. ],
  405. 'RateRequestTypes' => 'LIST',
  406. 'PackageCount' => '1',
  407. 'PackageDetail' => 'INDIVIDUAL_PACKAGES',
  408. 'RequestedPackageLineItems' => [
  409. '0' => [
  410. 'Weight' => [
  411. 'Value' => (double)$r->getWeight(),
  412. 'Units' => $this->getConfigData('unit_of_measure'),
  413. ],
  414. 'GroupPackageCount' => 1,
  415. ],
  416. ],
  417. ],
  418. ];
  419. if ($r->getDestCity()) {
  420. $ratesRequest['RequestedShipment']['Recipient']['Address']['City'] = $r->getDestCity();
  421. }
  422. if ($purpose == self::RATE_REQUEST_GENERAL) {
  423. $ratesRequest['RequestedShipment']['RequestedPackageLineItems'][0]['InsuredValue'] = [
  424. 'Amount' => $r->getValue(),
  425. 'Currency' => $this->getCurrencyCode(),
  426. ];
  427. } else {
  428. if ($purpose == self::RATE_REQUEST_SMARTPOST) {
  429. $ratesRequest['RequestedShipment']['ServiceType'] = self::RATE_REQUEST_SMARTPOST;
  430. $ratesRequest['RequestedShipment']['SmartPostDetail'] = [
  431. 'Indicia' => (double)$r->getWeight() >= 1 ? 'PARCEL_SELECT' : 'PRESORTED_STANDARD',
  432. 'HubId' => $this->getConfigData('smartpost_hubid'),
  433. ];
  434. }
  435. }
  436. return $ratesRequest;
  437. }
  438. /**
  439. * Makes remote request to the carrier and returns a response
  440. *
  441. * @param string $purpose
  442. * @return mixed
  443. */
  444. protected function _doRatesRequest($purpose)
  445. {
  446. $ratesRequest = $this->_formRateRequest($purpose);
  447. $ratesRequestNoShipTimestamp = $ratesRequest;
  448. unset($ratesRequestNoShipTimestamp['RequestedShipment']['ShipTimestamp']);
  449. $requestString = $this->serializer->serialize($ratesRequestNoShipTimestamp);
  450. $response = $this->_getCachedQuotes($requestString);
  451. $debugData = ['request' => $this->filterDebugData($ratesRequest)];
  452. if ($response === null) {
  453. try {
  454. $client = $this->_createRateSoapClient();
  455. $response = $client->getRates($ratesRequest);
  456. $this->_setCachedQuotes($requestString, $response);
  457. $debugData['result'] = $response;
  458. } catch (\Exception $e) {
  459. $debugData['result'] = ['error' => $e->getMessage(), 'code' => $e->getCode()];
  460. $this->_logger->critical($e);
  461. }
  462. } else {
  463. $debugData['result'] = $response;
  464. }
  465. $this->_debug($debugData);
  466. return $response;
  467. }
  468. /**
  469. * Do remote request for and handle errors
  470. *
  471. * @return Result
  472. */
  473. protected function _getQuotes()
  474. {
  475. $this->_result = $this->_rateFactory->create();
  476. // make separate request for Smart Post method
  477. $allowedMethods = explode(',', $this->getConfigData('allowed_methods'));
  478. if (in_array(self::RATE_REQUEST_SMARTPOST, $allowedMethods)) {
  479. $response = $this->_doRatesRequest(self::RATE_REQUEST_SMARTPOST);
  480. $preparedSmartpost = $this->_prepareRateResponse($response);
  481. if (!$preparedSmartpost->getError()) {
  482. $this->_result->append($preparedSmartpost);
  483. }
  484. }
  485. // make general request for all methods
  486. $response = $this->_doRatesRequest(self::RATE_REQUEST_GENERAL);
  487. $preparedGeneral = $this->_prepareRateResponse($response);
  488. if (!$preparedGeneral->getError()
  489. || $this->_result->getError() && $preparedGeneral->getError()
  490. || empty($this->_result->getAllRates())
  491. ) {
  492. $this->_result->append($preparedGeneral);
  493. }
  494. return $this->_result;
  495. }
  496. /**
  497. * Prepare shipping rate result based on response
  498. *
  499. * @param mixed $response
  500. * @return Result
  501. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  502. */
  503. protected function _prepareRateResponse($response)
  504. {
  505. $costArr = [];
  506. $priceArr = [];
  507. $errorTitle = 'For some reason we can\'t retrieve tracking info right now.';
  508. if (is_object($response)) {
  509. if ($response->HighestSeverity == 'FAILURE' || $response->HighestSeverity == 'ERROR') {
  510. if (is_array($response->Notifications)) {
  511. $notification = array_pop($response->Notifications);
  512. $errorTitle = (string)$notification->Message;
  513. } else {
  514. $errorTitle = (string)$response->Notifications->Message;
  515. }
  516. } elseif (isset($response->RateReplyDetails)) {
  517. $allowedMethods = explode(",", $this->getConfigData('allowed_methods'));
  518. if (is_array($response->RateReplyDetails)) {
  519. foreach ($response->RateReplyDetails as $rate) {
  520. $serviceName = (string)$rate->ServiceType;
  521. if (in_array($serviceName, $allowedMethods)) {
  522. $amount = $this->_getRateAmountOriginBased($rate);
  523. $costArr[$serviceName] = $amount;
  524. $priceArr[$serviceName] = $this->getMethodPrice($amount, $serviceName);
  525. }
  526. }
  527. asort($priceArr);
  528. } else {
  529. $rate = $response->RateReplyDetails;
  530. $serviceName = (string)$rate->ServiceType;
  531. if (in_array($serviceName, $allowedMethods)) {
  532. $amount = $this->_getRateAmountOriginBased($rate);
  533. $costArr[$serviceName] = $amount;
  534. $priceArr[$serviceName] = $this->getMethodPrice($amount, $serviceName);
  535. }
  536. }
  537. }
  538. }
  539. $result = $this->_rateFactory->create();
  540. if (empty($priceArr)) {
  541. $error = $this->_rateErrorFactory->create();
  542. $error->setCarrier($this->_code);
  543. $error->setCarrierTitle($this->getConfigData('title'));
  544. $error->setErrorMessage($errorTitle);
  545. $error->setErrorMessage($this->getConfigData('specificerrmsg'));
  546. $result->append($error);
  547. } else {
  548. foreach ($priceArr as $method => $price) {
  549. $rate = $this->_rateMethodFactory->create();
  550. $rate->setCarrier($this->_code);
  551. $rate->setCarrierTitle($this->getConfigData('title'));
  552. $rate->setMethod($method);
  553. $rate->setMethodTitle($this->getCode('method', $method));
  554. $rate->setCost($costArr[$method]);
  555. $rate->setPrice($price);
  556. $result->append($rate);
  557. }
  558. }
  559. return $result;
  560. }
  561. /**
  562. * Get origin based amount form response of rate estimation
  563. *
  564. * @param \stdClass $rate
  565. * @return null|float
  566. */
  567. protected function _getRateAmountOriginBased($rate)
  568. {
  569. $amount = null;
  570. $rateTypeAmounts = [];
  571. if (is_object($rate)) {
  572. // The "RATED..." rates are expressed in the currency of the origin country
  573. foreach ($rate->RatedShipmentDetails as $ratedShipmentDetail) {
  574. $netAmount = (string)$ratedShipmentDetail->ShipmentRateDetail->TotalNetCharge->Amount;
  575. $rateType = (string)$ratedShipmentDetail->ShipmentRateDetail->RateType;
  576. $rateTypeAmounts[$rateType] = $netAmount;
  577. }
  578. foreach ($this->_ratesOrder as $rateType) {
  579. if (!empty($rateTypeAmounts[$rateType])) {
  580. $amount = $rateTypeAmounts[$rateType];
  581. break;
  582. }
  583. }
  584. if ($amount === null) {
  585. $amount = (string)$rate->RatedShipmentDetails[0]->ShipmentRateDetail->TotalNetCharge->Amount;
  586. }
  587. }
  588. return $amount;
  589. }
  590. /**
  591. * Set free method request
  592. *
  593. * @param string $freeMethod
  594. * @return void
  595. */
  596. protected function _setFreeMethodRequest($freeMethod)
  597. {
  598. $r = $this->_rawRequest;
  599. $weight = $this->getTotalNumOfBoxes($r->getFreeMethodWeight());
  600. $r->setWeight($weight);
  601. $r->setService($freeMethod);
  602. }
  603. /**
  604. * Get xml quotes
  605. *
  606. * @return Result
  607. */
  608. protected function _getXmlQuotes()
  609. {
  610. $r = $this->_rawRequest;
  611. $xml = $this->_xmlElFactory->create(
  612. ['data' => '<?xml version = "1.0" encoding = "UTF-8"?><FDXRateAvailableServicesRequest/>']
  613. );
  614. $xml->addAttribute('xmlns:api', 'http://www.fedex.com/fsmapi');
  615. $xml->addAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
  616. $xml->addAttribute('xsi:noNamespaceSchemaLocation', 'FDXRateAvailableServicesRequest.xsd');
  617. $requestHeader = $xml->addChild('RequestHeader');
  618. $requestHeader->addChild('AccountNumber', $r->getAccount());
  619. $requestHeader->addChild('MeterNumber', '0');
  620. $xml->addChild('ShipDate', date('Y-m-d'));
  621. $xml->addChild('DropoffType', $r->getDropoffType());
  622. if ($r->hasService()) {
  623. $xml->addChild('Service', $r->getService());
  624. }
  625. $xml->addChild('Packaging', $r->getPackaging());
  626. $xml->addChild('WeightUnits', 'LBS');
  627. $xml->addChild('Weight', $r->getWeight());
  628. $originAddress = $xml->addChild('OriginAddress');
  629. $originAddress->addChild('PostalCode', $r->getOrigPostal());
  630. $originAddress->addChild('CountryCode', $r->getOrigCountry());
  631. $destinationAddress = $xml->addChild('DestinationAddress');
  632. $destinationAddress->addChild('PostalCode', $r->getDestPostal());
  633. $destinationAddress->addChild('CountryCode', $r->getDestCountry());
  634. $payment = $xml->addChild('Payment');
  635. $payment->addChild('PayorType', 'SENDER');
  636. $declaredValue = $xml->addChild('DeclaredValue');
  637. $declaredValue->addChild('Value', $r->getValue());
  638. $declaredValue->addChild('CurrencyCode', $this->getCurrencyCode());
  639. if ($this->getConfigData('residence_delivery')) {
  640. $specialServices = $xml->addChild('SpecialServices');
  641. $specialServices->addChild('ResidentialDelivery', 'true');
  642. }
  643. $xml->addChild('PackageCount', '1');
  644. $request = $xml->asXML();
  645. $responseBody = $this->_getCachedQuotes($request);
  646. if ($responseBody === null) {
  647. $debugData = ['request' => $this->filterDebugData($request)];
  648. try {
  649. $url = $this->getConfigData('gateway_url');
  650. if (!$url) {
  651. $url = $this->_defaultGatewayUrl;
  652. }
  653. $ch = curl_init();
  654. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  655. curl_setopt($ch, CURLOPT_URL, $url);
  656. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
  657. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
  658. curl_setopt($ch, CURLOPT_POSTFIELDS, $request);
  659. $responseBody = curl_exec($ch);
  660. curl_close($ch);
  661. $debugData['result'] = $this->filterDebugData($responseBody);
  662. $this->_setCachedQuotes($request, $responseBody);
  663. } catch (\Exception $e) {
  664. $debugData['result'] = ['error' => $e->getMessage(), 'code' => $e->getCode()];
  665. $responseBody = '';
  666. }
  667. $this->_debug($debugData);
  668. }
  669. return $this->_parseXmlResponse($responseBody);
  670. }
  671. /**
  672. * Prepare shipping rate result based on response
  673. *
  674. * @param mixed $response
  675. * @return Result
  676. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  677. */
  678. protected function _parseXmlResponse($response)
  679. {
  680. $costArr = [];
  681. $priceArr = [];
  682. if (strlen(trim($response)) > 0) {
  683. $xml = $this->parseXml($response, \Magento\Shipping\Model\Simplexml\Element::class);
  684. if (is_object($xml)) {
  685. if (is_object($xml->Error) && is_object($xml->Error->Message)) {
  686. $errorTitle = (string)$xml->Error->Message;
  687. } elseif (is_object($xml->SoftError) && is_object($xml->SoftError->Message)) {
  688. $errorTitle = (string)$xml->SoftError->Message;
  689. } else {
  690. $errorTitle = 'Sorry, something went wrong. Please try again or contact us and we\'ll try to help.';
  691. }
  692. $allowedMethods = explode(",", $this->getConfigData('allowed_methods'));
  693. foreach ($xml->Entry as $entry) {
  694. if (in_array((string)$entry->Service, $allowedMethods)) {
  695. $costArr[(string)$entry->Service] = (string)$entry
  696. ->EstimatedCharges
  697. ->DiscountedCharges
  698. ->NetCharge;
  699. $priceArr[(string)$entry->Service] = $this->getMethodPrice(
  700. (string)$entry->EstimatedCharges->DiscountedCharges->NetCharge,
  701. (string)$entry->Service
  702. );
  703. }
  704. }
  705. asort($priceArr);
  706. } else {
  707. $errorTitle = 'Response is in the wrong format.';
  708. }
  709. } else {
  710. $errorTitle = 'For some reason we can\'t retrieve tracking info right now.';
  711. }
  712. $result = $this->_rateFactory->create();
  713. if (empty($priceArr)) {
  714. $error = $this->_rateErrorFactory->create();
  715. $error->setCarrier('fedex');
  716. $error->setCarrierTitle($this->getConfigData('title'));
  717. $error->setErrorMessage($this->getConfigData('specificerrmsg'));
  718. $result->append($error);
  719. } else {
  720. foreach ($priceArr as $method => $price) {
  721. $rate = $this->_rateMethodFactory->create();
  722. $rate->setCarrier('fedex');
  723. $rate->setCarrierTitle($this->getConfigData('title'));
  724. $rate->setMethod($method);
  725. $rate->setMethodTitle($this->getCode('method', $method));
  726. $rate->setCost($costArr[$method]);
  727. $rate->setPrice($price);
  728. $result->append($rate);
  729. }
  730. }
  731. return $result;
  732. }
  733. /**
  734. * Get configuration data of carrier
  735. *
  736. * @param string $type
  737. * @param string $code
  738. * @return array|false
  739. * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
  740. */
  741. public function getCode($type, $code = '')
  742. {
  743. $codes = [
  744. 'method' => [
  745. 'EUROPE_FIRST_INTERNATIONAL_PRIORITY' => __('Europe First Priority'),
  746. 'FEDEX_1_DAY_FREIGHT' => __('1 Day Freight'),
  747. 'FEDEX_2_DAY_FREIGHT' => __('2 Day Freight'),
  748. 'FEDEX_2_DAY' => __('2 Day'),
  749. 'FEDEX_2_DAY_AM' => __('2 Day AM'),
  750. 'FEDEX_3_DAY_FREIGHT' => __('3 Day Freight'),
  751. 'FEDEX_EXPRESS_SAVER' => __('Express Saver'),
  752. 'FEDEX_GROUND' => __('Ground'),
  753. 'FIRST_OVERNIGHT' => __('First Overnight'),
  754. 'GROUND_HOME_DELIVERY' => __('Home Delivery'),
  755. 'INTERNATIONAL_ECONOMY' => __('International Economy'),
  756. 'INTERNATIONAL_ECONOMY_FREIGHT' => __('Intl Economy Freight'),
  757. 'INTERNATIONAL_FIRST' => __('International First'),
  758. 'INTERNATIONAL_GROUND' => __('International Ground'),
  759. 'INTERNATIONAL_PRIORITY' => __('International Priority'),
  760. 'INTERNATIONAL_PRIORITY_FREIGHT' => __('Intl Priority Freight'),
  761. 'PRIORITY_OVERNIGHT' => __('Priority Overnight'),
  762. 'SMART_POST' => __('Smart Post'),
  763. 'STANDARD_OVERNIGHT' => __('Standard Overnight'),
  764. 'FEDEX_FREIGHT' => __('Freight'),
  765. 'FEDEX_NATIONAL_FREIGHT' => __('National Freight'),
  766. ],
  767. 'dropoff' => [
  768. 'REGULAR_PICKUP' => __('Regular Pickup'),
  769. 'REQUEST_COURIER' => __('Request Courier'),
  770. 'DROP_BOX' => __('Drop Box'),
  771. 'BUSINESS_SERVICE_CENTER' => __('Business Service Center'),
  772. 'STATION' => __('Station'),
  773. ],
  774. 'packaging' => [
  775. 'FEDEX_ENVELOPE' => __('FedEx Envelope'),
  776. 'FEDEX_PAK' => __('FedEx Pak'),
  777. 'FEDEX_BOX' => __('FedEx Box'),
  778. 'FEDEX_TUBE' => __('FedEx Tube'),
  779. 'FEDEX_10KG_BOX' => __('FedEx 10kg Box'),
  780. 'FEDEX_25KG_BOX' => __('FedEx 25kg Box'),
  781. 'YOUR_PACKAGING' => __('Your Packaging'),
  782. ],
  783. 'containers_filter' => [
  784. [
  785. 'containers' => ['FEDEX_ENVELOPE', 'FEDEX_PAK'],
  786. 'filters' => [
  787. 'within_us' => [
  788. 'method' => [
  789. 'FEDEX_EXPRESS_SAVER',
  790. 'FEDEX_2_DAY',
  791. 'FEDEX_2_DAY_AM',
  792. 'STANDARD_OVERNIGHT',
  793. 'PRIORITY_OVERNIGHT',
  794. 'FIRST_OVERNIGHT',
  795. ],
  796. ],
  797. 'from_us' => [
  798. 'method' => ['INTERNATIONAL_FIRST', 'INTERNATIONAL_ECONOMY', 'INTERNATIONAL_PRIORITY'],
  799. ],
  800. ],
  801. ],
  802. [
  803. 'containers' => ['FEDEX_BOX', 'FEDEX_TUBE'],
  804. 'filters' => [
  805. 'within_us' => [
  806. 'method' => [
  807. 'FEDEX_2_DAY',
  808. 'FEDEX_2_DAY_AM',
  809. 'STANDARD_OVERNIGHT',
  810. 'PRIORITY_OVERNIGHT',
  811. 'FIRST_OVERNIGHT',
  812. 'FEDEX_FREIGHT',
  813. 'FEDEX_1_DAY_FREIGHT',
  814. 'FEDEX_2_DAY_FREIGHT',
  815. 'FEDEX_3_DAY_FREIGHT',
  816. 'FEDEX_NATIONAL_FREIGHT',
  817. ],
  818. ],
  819. 'from_us' => [
  820. 'method' => ['INTERNATIONAL_FIRST', 'INTERNATIONAL_ECONOMY', 'INTERNATIONAL_PRIORITY'],
  821. ],
  822. ],
  823. ],
  824. [
  825. 'containers' => ['FEDEX_10KG_BOX', 'FEDEX_25KG_BOX'],
  826. 'filters' => [
  827. 'within_us' => [],
  828. 'from_us' => ['method' => ['INTERNATIONAL_PRIORITY']],
  829. ],
  830. ],
  831. [
  832. 'containers' => ['YOUR_PACKAGING'],
  833. 'filters' => [
  834. 'within_us' => [
  835. 'method' => [
  836. 'FEDEX_GROUND',
  837. 'GROUND_HOME_DELIVERY',
  838. 'SMART_POST',
  839. 'FEDEX_EXPRESS_SAVER',
  840. 'FEDEX_2_DAY',
  841. 'FEDEX_2_DAY_AM',
  842. 'STANDARD_OVERNIGHT',
  843. 'PRIORITY_OVERNIGHT',
  844. 'FIRST_OVERNIGHT',
  845. 'FEDEX_FREIGHT',
  846. 'FEDEX_1_DAY_FREIGHT',
  847. 'FEDEX_2_DAY_FREIGHT',
  848. 'FEDEX_3_DAY_FREIGHT',
  849. 'FEDEX_NATIONAL_FREIGHT',
  850. ],
  851. ],
  852. 'from_us' => [
  853. 'method' => [
  854. 'INTERNATIONAL_FIRST',
  855. 'INTERNATIONAL_ECONOMY',
  856. 'INTERNATIONAL_PRIORITY',
  857. 'INTERNATIONAL_GROUND',
  858. 'FEDEX_FREIGHT',
  859. 'FEDEX_1_DAY_FREIGHT',
  860. 'FEDEX_2_DAY_FREIGHT',
  861. 'FEDEX_3_DAY_FREIGHT',
  862. 'FEDEX_NATIONAL_FREIGHT',
  863. 'INTERNATIONAL_ECONOMY_FREIGHT',
  864. 'INTERNATIONAL_PRIORITY_FREIGHT',
  865. ],
  866. ],
  867. ],
  868. ],
  869. ],
  870. 'delivery_confirmation_types' => [
  871. 'NO_SIGNATURE_REQUIRED' => __('Not Required'),
  872. 'ADULT' => __('Adult'),
  873. 'DIRECT' => __('Direct'),
  874. 'INDIRECT' => __('Indirect'),
  875. ],
  876. 'unit_of_measure' => [
  877. 'LB' => __('Pounds'),
  878. 'KG' => __('Kilograms'),
  879. ],
  880. ];
  881. if (!isset($codes[$type])) {
  882. return false;
  883. } elseif ('' === $code) {
  884. return $codes[$type];
  885. }
  886. if (!isset($codes[$type][$code])) {
  887. return false;
  888. } else {
  889. return $codes[$type][$code];
  890. }
  891. }
  892. /**
  893. * Return FeDex currency ISO code by Magento Base Currency Code
  894. *
  895. * @return string 3-digit currency code
  896. * @throws \Magento\Framework\Exception\NoSuchEntityException
  897. */
  898. public function getCurrencyCode()
  899. {
  900. $codes = [
  901. 'DOP' => 'RDD',
  902. 'XCD' => 'ECD',
  903. 'ARS' => 'ARN',
  904. 'SGD' => 'SID',
  905. 'KRW' => 'WON',
  906. 'JMD' => 'JAD',
  907. 'CHF' => 'SFR',
  908. 'JPY' => 'JYE',
  909. 'KWD' => 'KUD',
  910. 'GBP' => 'UKL',
  911. 'AED' => 'DHS',
  912. 'MXN' => 'NMP',
  913. 'UYU' => 'UYP',
  914. 'CLP' => 'CHP',
  915. 'TWD' => 'NTD',
  916. ];
  917. $currencyCode = $this->_storeManager->getStore()->getBaseCurrencyCode();
  918. return $codes[$currencyCode] ?? $currencyCode;
  919. }
  920. /**
  921. * Get tracking
  922. *
  923. * @param string|string[] $trackings
  924. * @return Result|null
  925. */
  926. public function getTracking($trackings)
  927. {
  928. $this->setTrackingReqeust();
  929. if (!is_array($trackings)) {
  930. $trackings = [$trackings];
  931. }
  932. foreach ($trackings as $tracking) {
  933. $this->_getXMLTracking($tracking);
  934. }
  935. return $this->_result;
  936. }
  937. /**
  938. * Set tracking request
  939. *
  940. * @return void
  941. */
  942. protected function setTrackingReqeust()
  943. {
  944. $r = new \Magento\Framework\DataObject();
  945. $account = $this->getConfigData('account');
  946. $r->setAccount($account);
  947. $this->_rawTrackingRequest = $r;
  948. }
  949. /**
  950. * Send request for tracking
  951. *
  952. * @param string[] $tracking
  953. * @return void
  954. */
  955. protected function _getXMLTracking($tracking)
  956. {
  957. $trackRequest = [
  958. 'WebAuthenticationDetail' => [
  959. 'UserCredential' => [
  960. 'Key' => $this->getConfigData('key'),
  961. 'Password' => $this->getConfigData('password'),
  962. ],
  963. ],
  964. 'ClientDetail' => [
  965. 'AccountNumber' => $this->getConfigData('account'),
  966. 'MeterNumber' => $this->getConfigData('meter_number'),
  967. ],
  968. 'Version' => [
  969. 'ServiceId' => 'trck',
  970. 'Major' => self::$trackServiceVersion,
  971. 'Intermediate' => '0',
  972. 'Minor' => '0',
  973. ],
  974. 'SelectionDetails' => [
  975. 'PackageIdentifier' => ['Type' => 'TRACKING_NUMBER_OR_DOORTAG', 'Value' => $tracking],
  976. ],
  977. 'ProcessingOptions' => 'INCLUDE_DETAILED_SCANS'
  978. ];
  979. $requestString = $this->serializer->serialize($trackRequest);
  980. $response = $this->_getCachedQuotes($requestString);
  981. $debugData = ['request' => $this->filterDebugData($trackRequest)];
  982. if ($response === null) {
  983. try {
  984. $client = $this->_createTrackSoapClient();
  985. $response = $client->track($trackRequest);
  986. $this->_setCachedQuotes($requestString, $response);
  987. $debugData['result'] = $response;
  988. } catch (\Exception $e) {
  989. $debugData['result'] = ['error' => $e->getMessage(), 'code' => $e->getCode()];
  990. $this->_logger->critical($e);
  991. }
  992. } else {
  993. $debugData['result'] = $response;
  994. }
  995. $this->_debug($debugData);
  996. $this->_parseTrackingResponse($tracking, $response);
  997. }
  998. /**
  999. * Parse tracking response
  1000. *
  1001. * @param string $trackingValue
  1002. * @param \stdClass $response
  1003. * @return void
  1004. */
  1005. protected function _parseTrackingResponse($trackingValue, $response)
  1006. {
  1007. if (!is_object($response) || empty($response->HighestSeverity)) {
  1008. $this->appendTrackingError($trackingValue, __('Invalid response from carrier'));
  1009. return;
  1010. } elseif (in_array($response->HighestSeverity, self::$trackingErrors)) {
  1011. $this->appendTrackingError($trackingValue, (string) $response->Notifications->Message);
  1012. return;
  1013. } elseif (empty($response->CompletedTrackDetails) || empty($response->CompletedTrackDetails->TrackDetails)) {
  1014. $this->appendTrackingError($trackingValue, __('No available tracking items'));
  1015. return;
  1016. }
  1017. $trackInfo = $response->CompletedTrackDetails->TrackDetails;
  1018. // Fedex can return tracking details as single object instead array
  1019. if (is_object($trackInfo)) {
  1020. $trackInfo = [$trackInfo];
  1021. }
  1022. $result = $this->getResult();
  1023. $carrierTitle = $this->getConfigData('title');
  1024. $counter = 0;
  1025. foreach ($trackInfo as $item) {
  1026. $tracking = $this->_trackStatusFactory->create();
  1027. $tracking->setCarrier(self::CODE);
  1028. $tracking->setCarrierTitle($carrierTitle);
  1029. $tracking->setTracking($trackingValue);
  1030. $tracking->addData($this->processTrackingDetails($item));
  1031. $result->append($tracking);
  1032. $counter ++;
  1033. }
  1034. // no available tracking details
  1035. if (!$counter) {
  1036. $this->appendTrackingError(
  1037. $trackingValue,
  1038. __('For some reason we can\'t retrieve tracking info right now.')
  1039. );
  1040. }
  1041. }
  1042. /**
  1043. * Get tracking response
  1044. *
  1045. * @return string
  1046. */
  1047. public function getResponse()
  1048. {
  1049. $statuses = '';
  1050. if ($this->_result instanceof \Magento\Shipping\Model\Tracking\Result) {
  1051. if ($trackings = $this->_result->getAllTrackings()) {
  1052. foreach ($trackings as $tracking) {
  1053. if ($data = $tracking->getAllData()) {
  1054. if (!empty($data['status'])) {
  1055. $statuses .= __($data['status']) . "\n<br/>";
  1056. } else {
  1057. $statuses .= __('Empty response') . "\n<br/>";
  1058. }
  1059. }
  1060. }
  1061. }
  1062. }
  1063. if (empty($statuses)) {
  1064. $statuses = __('Empty response');
  1065. }
  1066. return $statuses;
  1067. }
  1068. /**
  1069. * Get allowed shipping methods
  1070. *
  1071. * @return array
  1072. */
  1073. public function getAllowedMethods()
  1074. {
  1075. $allowed = explode(',', $this->getConfigData('allowed_methods'));
  1076. $arr = [];
  1077. foreach ($allowed as $k) {
  1078. $arr[$k] = $this->getCode('method', $k);
  1079. }
  1080. return $arr;
  1081. }
  1082. /**
  1083. * Return array of authenticated information
  1084. *
  1085. * @return array
  1086. */
  1087. protected function _getAuthDetails()
  1088. {
  1089. return [
  1090. 'WebAuthenticationDetail' => [
  1091. 'UserCredential' => [
  1092. 'Key' => $this->getConfigData('key'),
  1093. 'Password' => $this->getConfigData('password'),
  1094. ],
  1095. ],
  1096. 'ClientDetail' => [
  1097. 'AccountNumber' => $this->getConfigData('account'),
  1098. 'MeterNumber' => $this->getConfigData('meter_number'),
  1099. ],
  1100. 'TransactionDetail' => [
  1101. 'CustomerTransactionId' => '*** Express Domestic Shipping Request v9 using PHP ***',
  1102. ],
  1103. 'Version' => ['ServiceId' => 'ship', 'Major' => '10', 'Intermediate' => '0', 'Minor' => '0'],
  1104. ];
  1105. }
  1106. /**
  1107. * Form array with appropriate structure for shipment request
  1108. *
  1109. * @param \Magento\Framework\DataObject $request
  1110. * @return array
  1111. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  1112. * @SuppressWarnings(PHPMD.NPathComplexity)
  1113. * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
  1114. */
  1115. protected function _formShipmentRequest(\Magento\Framework\DataObject $request)
  1116. {
  1117. if ($request->getReferenceData()) {
  1118. $referenceData = $request->getReferenceData() . $request->getPackageId();
  1119. } else {
  1120. $referenceData = 'Order #' .
  1121. $request->getOrderShipment()->getOrder()->getIncrementId() .
  1122. ' P' .
  1123. $request->getPackageId();
  1124. }
  1125. $packageParams = $request->getPackageParams();
  1126. $customsValue = $packageParams->getCustomsValue();
  1127. $height = $packageParams->getHeight();
  1128. $width = $packageParams->getWidth();
  1129. $length = $packageParams->getLength();
  1130. $weightUnits = $packageParams->getWeightUnits() == \Zend_Measure_Weight::POUND ? 'LB' : 'KG';
  1131. $unitPrice = 0;
  1132. $itemsQty = 0;
  1133. $itemsDesc = [];
  1134. $countriesOfManufacture = [];
  1135. $productIds = [];
  1136. $packageItems = $request->getPackageItems();
  1137. foreach ($packageItems as $itemShipment) {
  1138. $item = new \Magento\Framework\DataObject();
  1139. $item->setData($itemShipment);
  1140. $unitPrice += $item->getPrice();
  1141. $itemsQty += $item->getQty();
  1142. $itemsDesc[] = $item->getName();
  1143. $productIds[] = $item->getProductId();
  1144. }
  1145. // get countries of manufacture
  1146. $productCollection = $this->_productCollectionFactory->create()->addStoreFilter(
  1147. $request->getStoreId()
  1148. )->addFieldToFilter(
  1149. 'entity_id',
  1150. ['in' => $productIds]
  1151. )->addAttributeToSelect(
  1152. 'country_of_manufacture'
  1153. );
  1154. foreach ($productCollection as $product) {
  1155. $countriesOfManufacture[] = $product->getCountryOfManufacture();
  1156. }
  1157. $paymentType = $this->getPaymentType($request);
  1158. $optionType = $request->getShippingMethod() == self::RATE_REQUEST_SMARTPOST
  1159. ? 'SERVICE_DEFAULT' : $packageParams->getDeliveryConfirmation();
  1160. $requestClient = [
  1161. 'RequestedShipment' => [
  1162. 'ShipTimestamp' => time(),
  1163. 'DropoffType' => $this->getConfigData('dropoff'),
  1164. 'PackagingType' => $request->getPackagingType(),
  1165. 'ServiceType' => $request->getShippingMethod(),
  1166. 'Shipper' => [
  1167. 'Contact' => [
  1168. 'PersonName' => $request->getShipperContactPersonName(),
  1169. 'CompanyName' => $request->getShipperContactCompanyName(),
  1170. 'PhoneNumber' => $request->getShipperContactPhoneNumber(),
  1171. ],
  1172. 'Address' => [
  1173. 'StreetLines' => [$request->getShipperAddressStreet()],
  1174. 'City' => $request->getShipperAddressCity(),
  1175. 'StateOrProvinceCode' => $request->getShipperAddressStateOrProvinceCode(),
  1176. 'PostalCode' => $request->getShipperAddressPostalCode(),
  1177. 'CountryCode' => $request->getShipperAddressCountryCode(),
  1178. ],
  1179. ],
  1180. 'Recipient' => [
  1181. 'Contact' => [
  1182. 'PersonName' => $request->getRecipientContactPersonName(),
  1183. 'CompanyName' => $request->getRecipientContactCompanyName(),
  1184. 'PhoneNumber' => $request->getRecipientContactPhoneNumber(),
  1185. ],
  1186. 'Address' => [
  1187. 'StreetLines' => [$request->getRecipientAddressStreet()],
  1188. 'City' => $request->getRecipientAddressCity(),
  1189. 'StateOrProvinceCode' => $request->getRecipientAddressStateOrProvinceCode(),
  1190. 'PostalCode' => $request->getRecipientAddressPostalCode(),
  1191. 'CountryCode' => $request->getRecipientAddressCountryCode(),
  1192. 'Residential' => (bool)$this->getConfigData('residence_delivery'),
  1193. ],
  1194. ],
  1195. 'ShippingChargesPayment' => [
  1196. 'PaymentType' => $paymentType,
  1197. 'Payor' => [
  1198. 'AccountNumber' => $this->getConfigData('account'),
  1199. 'CountryCode' => $this->_scopeConfig->getValue(
  1200. \Magento\Sales\Model\Order\Shipment::XML_PATH_STORE_COUNTRY_ID,
  1201. \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
  1202. $request->getStoreId()
  1203. ),
  1204. ],
  1205. ],
  1206. 'LabelSpecification' => [
  1207. 'LabelFormatType' => 'COMMON2D',
  1208. 'ImageType' => 'PNG',
  1209. 'LabelStockType' => 'PAPER_8.5X11_TOP_HALF_LABEL',
  1210. ],
  1211. 'RateRequestTypes' => ['ACCOUNT'],
  1212. 'PackageCount' => 1,
  1213. 'RequestedPackageLineItems' => [
  1214. 'SequenceNumber' => '1',
  1215. 'Weight' => ['Units' => $weightUnits, 'Value' => $request->getPackageWeight()],
  1216. 'CustomerReferences' => [
  1217. 'CustomerReferenceType' => 'CUSTOMER_REFERENCE',
  1218. 'Value' => $referenceData,
  1219. ],
  1220. 'SpecialServicesRequested' => [
  1221. 'SpecialServiceTypes' => 'SIGNATURE_OPTION',
  1222. 'SignatureOptionDetail' => ['OptionType' => $optionType],
  1223. ],
  1224. ],
  1225. ],
  1226. ];
  1227. // for international shipping
  1228. if ($request->getShipperAddressCountryCode() != $request->getRecipientAddressCountryCode()) {
  1229. $requestClient['RequestedShipment']['CustomsClearanceDetail'] = [
  1230. 'CustomsValue' => ['Currency' => $request->getBaseCurrencyCode(), 'Amount' => $customsValue],
  1231. 'DutiesPayment' => [
  1232. 'PaymentType' => $paymentType,
  1233. 'Payor' => [
  1234. 'AccountNumber' => $this->getConfigData('account'),
  1235. 'CountryCode' => $this->_scopeConfig->getValue(
  1236. \Magento\Sales\Model\Order\Shipment::XML_PATH_STORE_COUNTRY_ID,
  1237. \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
  1238. $request->getStoreId()
  1239. ),
  1240. ],
  1241. ],
  1242. 'Commodities' => [
  1243. 'Weight' => ['Units' => $weightUnits, 'Value' => $request->getPackageWeight()],
  1244. 'NumberOfPieces' => 1,
  1245. 'CountryOfManufacture' => implode(',', array_unique($countriesOfManufacture)),
  1246. 'Description' => implode(', ', $itemsDesc),
  1247. 'Quantity' => ceil($itemsQty),
  1248. 'QuantityUnits' => 'pcs',
  1249. 'UnitPrice' => ['Currency' => $request->getBaseCurrencyCode(), 'Amount' => $unitPrice],
  1250. 'CustomsValue' => ['Currency' => $request->getBaseCurrencyCode(), 'Amount' => $customsValue],
  1251. ],
  1252. ];
  1253. }
  1254. if ($request->getMasterTrackingId()) {
  1255. $requestClient['RequestedShipment']['MasterTrackingId'] = $request->getMasterTrackingId();
  1256. }
  1257. if ($request->getShippingMethod() == self::RATE_REQUEST_SMARTPOST) {
  1258. $requestClient['RequestedShipment']['SmartPostDetail'] = [
  1259. 'Indicia' => (double)$request->getPackageWeight() >= 1 ? 'PARCEL_SELECT' : 'PRESORTED_STANDARD',
  1260. 'HubId' => $this->getConfigData('smartpost_hubid'),
  1261. ];
  1262. }
  1263. // set dimensions
  1264. if ($length || $width || $height) {
  1265. $requestClient['RequestedShipment']['RequestedPackageLineItems']['Dimensions'] = [
  1266. 'Length' => $length,
  1267. 'Width' => $width,
  1268. 'Height' => $height,
  1269. 'Units' => $packageParams->getDimensionUnits() == \Zend_Measure_Length::INCH ? 'IN' : 'CM',
  1270. ];
  1271. }
  1272. return $this->_getAuthDetails() + $requestClient;
  1273. }
  1274. /**
  1275. * Do shipment request to carrier web service, obtain Print Shipping Labels and process errors in response
  1276. *
  1277. * @param \Magento\Framework\DataObject $request
  1278. * @return \Magento\Framework\DataObject
  1279. */
  1280. protected function _doShipmentRequest(\Magento\Framework\DataObject $request)
  1281. {
  1282. $this->_prepareShipmentRequest($request);
  1283. $result = new \Magento\Framework\DataObject();
  1284. $client = $this->_createShipSoapClient();
  1285. $requestClient = $this->_formShipmentRequest($request);
  1286. $debugData['request'] = $this->filterDebugData($requestClient);
  1287. $response = $client->processShipment($requestClient);
  1288. if ($response->HighestSeverity != 'FAILURE' && $response->HighestSeverity != 'ERROR') {
  1289. $shippingLabelContent = $response->CompletedShipmentDetail->CompletedPackageDetails->Label->Parts->Image;
  1290. $trackingNumber = $this->getTrackingNumber(
  1291. $response->CompletedShipmentDetail->CompletedPackageDetails->TrackingIds
  1292. );
  1293. $result->setShippingLabelContent($shippingLabelContent);
  1294. $result->setTrackingNumber($trackingNumber);
  1295. $debugData['result'] = $client->__getLastResponse();
  1296. $this->_debug($debugData);
  1297. } else {
  1298. $debugData['result'] = ['error' => '', 'code' => '', 'xml' => $client->__getLastResponse()];
  1299. if (is_array($response->Notifications)) {
  1300. foreach ($response->Notifications as $notification) {
  1301. $debugData['result']['code'] .= $notification->Code . '; ';
  1302. $debugData['result']['error'] .= $notification->Message . '; ';
  1303. }
  1304. } else {
  1305. $debugData['result']['code'] = $response->Notifications->Code . ' ';
  1306. $debugData['result']['error'] = $response->Notifications->Message . ' ';
  1307. }
  1308. $this->_debug($debugData);
  1309. $result->setErrors($debugData['result']['error']);
  1310. }
  1311. $result->setGatewayResponse($client->__getLastResponse());
  1312. return $result;
  1313. }
  1314. /**
  1315. * Return Tracking Number
  1316. *
  1317. * @param array|object $trackingIds
  1318. * @return string
  1319. */
  1320. private function getTrackingNumber($trackingIds)
  1321. {
  1322. return is_array($trackingIds) ? array_map(
  1323. function ($val) {
  1324. return $val->TrackingNumber;
  1325. },
  1326. $trackingIds
  1327. ) : $trackingIds->TrackingNumber;
  1328. }
  1329. /**
  1330. * For multi package shipments. Delete requested shipments if the current shipment request is failed
  1331. *
  1332. * @param array $data
  1333. *
  1334. * @return bool
  1335. */
  1336. public function rollBack($data)
  1337. {
  1338. $requestData = $this->_getAuthDetails();
  1339. $requestData['DeletionControl'] = 'DELETE_ONE_PACKAGE';
  1340. foreach ($data as &$item) {
  1341. $requestData['TrackingId'] = $item['tracking_number'];
  1342. $client = $this->_createShipSoapClient();
  1343. $client->deleteShipment($requestData);
  1344. }
  1345. return true;
  1346. }
  1347. /**
  1348. * Return container types of carrier
  1349. *
  1350. * @param \Magento\Framework\DataObject|null $params
  1351. *
  1352. * @return array|bool
  1353. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  1354. */
  1355. public function getContainerTypes(\Magento\Framework\DataObject $params = null)
  1356. {
  1357. if ($params == null) {
  1358. return $this->_getAllowedContainers($params);
  1359. }
  1360. $method = $params->getMethod();
  1361. $countryShipper = $params->getCountryShipper();
  1362. $countryRecipient = $params->getCountryRecipient();
  1363. if (($countryShipper == self::USA_COUNTRY_ID && $countryRecipient == self::CANADA_COUNTRY_ID ||
  1364. $countryShipper == self::CANADA_COUNTRY_ID &&
  1365. $countryRecipient == self::USA_COUNTRY_ID) &&
  1366. $method == 'FEDEX_GROUND'
  1367. ) {
  1368. return ['YOUR_PACKAGING' => __('Your Packaging')];
  1369. } else {
  1370. if ($method == 'INTERNATIONAL_ECONOMY' || $method == 'INTERNATIONAL_FIRST') {
  1371. $allTypes = $this->getContainerTypesAll();
  1372. $exclude = ['FEDEX_10KG_BOX' => '', 'FEDEX_25KG_BOX' => ''];
  1373. return array_diff_key($allTypes, $exclude);
  1374. } else {
  1375. if ($method == 'EUROPE_FIRST_INTERNATIONAL_PRIORITY') {
  1376. $allTypes = $this->getContainerTypesAll();
  1377. $exclude = ['FEDEX_BOX' => '', 'FEDEX_TUBE' => ''];
  1378. return array_diff_key($allTypes, $exclude);
  1379. } else {
  1380. if ($countryShipper == self::CANADA_COUNTRY_ID && $countryRecipient == self::CANADA_COUNTRY_ID) {
  1381. // hack for Canada domestic. Apply the same filter rules as for US domestic
  1382. $params->setCountryShipper(self::USA_COUNTRY_ID);
  1383. $params->setCountryRecipient(self::USA_COUNTRY_ID);
  1384. }
  1385. }
  1386. }
  1387. }
  1388. return $this->_getAllowedContainers($params);
  1389. }
  1390. /**
  1391. * Return all container types of carrier
  1392. *
  1393. * @return array|bool
  1394. */
  1395. public function getContainerTypesAll()
  1396. {
  1397. return $this->getCode('packaging');
  1398. }
  1399. /**
  1400. * Return structured data of containers witch related with shipping methods
  1401. *
  1402. * @return array|bool
  1403. */
  1404. public function getContainerTypesFilter()
  1405. {
  1406. return $this->getCode('containers_filter');
  1407. }
  1408. /**
  1409. * Return delivery confirmation types of carrier
  1410. *
  1411. * @param \Magento\Framework\DataObject|null $params
  1412. *
  1413. * @return array
  1414. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  1415. */
  1416. public function getDeliveryConfirmationTypes(\Magento\Framework\DataObject $params = null)
  1417. {
  1418. return $this->getCode('delivery_confirmation_types');
  1419. }
  1420. /**
  1421. * Recursive replace sensitive fields in debug data by the mask
  1422. *
  1423. * @param string $data
  1424. * @return string
  1425. */
  1426. protected function filterDebugData($data)
  1427. {
  1428. foreach (array_keys($data) as $key) {
  1429. if (is_array($data[$key])) {
  1430. $data[$key] = $this->filterDebugData($data[$key]);
  1431. } elseif (in_array($key, $this->_debugReplacePrivateDataKeys)) {
  1432. $data[$key] = self::DEBUG_KEYS_MASK;
  1433. }
  1434. }
  1435. return $data;
  1436. }
  1437. /**
  1438. * Parse track details response from Fedex
  1439. *
  1440. * @param \stdClass $trackInfo
  1441. * @return array
  1442. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  1443. * @SuppressWarnings(PHPMD.NPathComplexity)
  1444. */
  1445. private function processTrackingDetails(\stdClass $trackInfo)
  1446. {
  1447. $result = [
  1448. 'shippeddate' => null,
  1449. 'deliverydate' => null,
  1450. 'deliverytime' => null,
  1451. 'deliverylocation' => null,
  1452. 'weight' => null,
  1453. 'progressdetail' => [],
  1454. ];
  1455. $datetime = $this->parseDate(!empty($trackInfo->ShipTimestamp) ? $trackInfo->ShipTimestamp : null);
  1456. if ($datetime) {
  1457. $result['shippeddate'] = gmdate('Y-m-d', $datetime->getTimestamp());
  1458. }
  1459. $result['signedby'] = !empty($trackInfo->DeliverySignatureName) ?
  1460. (string) $trackInfo->DeliverySignatureName :
  1461. null;
  1462. $result['status'] = (!empty($trackInfo->StatusDetail) && !empty($trackInfo->StatusDetail->Description)) ?
  1463. (string) $trackInfo->StatusDetail->Description :
  1464. null;
  1465. $result['service'] = (!empty($trackInfo->Service) && !empty($trackInfo->Service->Description)) ?
  1466. (string) $trackInfo->Service->Description :
  1467. null;
  1468. $datetime = $this->getDeliveryDateTime($trackInfo);
  1469. if ($datetime) {
  1470. $result['deliverydate'] = gmdate('Y-m-d', $datetime->getTimestamp());
  1471. $result['deliverytime'] = gmdate('H:i:s', $datetime->getTimestamp());
  1472. }
  1473. $address = null;
  1474. if (!empty($trackInfo->EstimatedDeliveryAddress)) {
  1475. $address = $trackInfo->EstimatedDeliveryAddress;
  1476. } elseif (!empty($trackInfo->ActualDeliveryAddress)) {
  1477. $address = $trackInfo->ActualDeliveryAddress;
  1478. }
  1479. if (!empty($address)) {
  1480. $result['deliverylocation'] = $this->getDeliveryAddress($address);
  1481. }
  1482. if (!empty($trackInfo->PackageWeight)) {
  1483. $result['weight'] = sprintf(
  1484. '%s %s',
  1485. (string) $trackInfo->PackageWeight->Value,
  1486. (string) $trackInfo->PackageWeight->Units
  1487. );
  1488. }
  1489. if (!empty($trackInfo->Events)) {
  1490. $events = $trackInfo->Events;
  1491. if (is_object($events)) {
  1492. $events = [$trackInfo->Events];
  1493. }
  1494. $result['progressdetail'] = $this->processTrackDetailsEvents($events);
  1495. }
  1496. return $result;
  1497. }
  1498. /**
  1499. * Parse delivery datetime from tracking details
  1500. *
  1501. * @param \stdClass $trackInfo
  1502. * @return \Datetime|null
  1503. */
  1504. private function getDeliveryDateTime(\stdClass $trackInfo)
  1505. {
  1506. $timestamp = null;
  1507. if (!empty($trackInfo->EstimatedDeliveryTimestamp)) {
  1508. $timestamp = $trackInfo->EstimatedDeliveryTimestamp;
  1509. } elseif (!empty($trackInfo->ActualDeliveryTimestamp)) {
  1510. $timestamp = $trackInfo->ActualDeliveryTimestamp;
  1511. }
  1512. return $timestamp ? $this->parseDate($timestamp) : null;
  1513. }
  1514. /**
  1515. * Get delivery address details in string representation Return City, State, Country Code
  1516. *
  1517. * @param \stdClass $address
  1518. * @return \Magento\Framework\Phrase|string
  1519. */
  1520. private function getDeliveryAddress(\stdClass $address)
  1521. {
  1522. $details = [];
  1523. if (!empty($address->City)) {
  1524. $details[] = (string) $address->City;
  1525. }
  1526. if (!empty($address->StateOrProvinceCode)) {
  1527. $details[] = (string) $address->StateOrProvinceCode;
  1528. }
  1529. if (!empty($address->CountryCode)) {
  1530. $details[] = (string) $address->CountryCode;
  1531. }
  1532. return implode(', ', $details);
  1533. }
  1534. /**
  1535. * Parse tracking details events from response
  1536. * Return list of items in such format:
  1537. * ['activity', 'deliverydate', 'deliverytime', 'deliverylocation']
  1538. *
  1539. * @param array $events
  1540. * @return array
  1541. */
  1542. private function processTrackDetailsEvents(array $events)
  1543. {
  1544. $result = [];
  1545. /** @var \stdClass $event */
  1546. foreach ($events as $event) {
  1547. $item = [
  1548. 'activity' => (string) $event->EventDescription,
  1549. 'deliverydate' => null,
  1550. 'deliverytime' => null,
  1551. 'deliverylocation' => null
  1552. ];
  1553. $datetime = $this->parseDate(!empty($event->Timestamp) ? $event->Timestamp : null);
  1554. if ($datetime) {
  1555. $item['deliverydate'] = gmdate('Y-m-d', $datetime->getTimestamp());
  1556. $item['deliverytime'] = gmdate('H:i:s', $datetime->getTimestamp());
  1557. }
  1558. if (!empty($event->Address)) {
  1559. $item['deliverylocation'] = $this->getDeliveryAddress($event->Address);
  1560. }
  1561. $result[] = $item;
  1562. }
  1563. return $result;
  1564. }
  1565. /**
  1566. * Append error message to rate result instance
  1567. *
  1568. * @param string $trackingValue
  1569. * @param string $errorMessage
  1570. */
  1571. private function appendTrackingError($trackingValue, $errorMessage)
  1572. {
  1573. $error = $this->_trackErrorFactory->create();
  1574. $error->setCarrier('fedex');
  1575. $error->setCarrierTitle($this->getConfigData('title'));
  1576. $error->setTracking($trackingValue);
  1577. $error->setErrorMessage($errorMessage);
  1578. $result = $this->getResult();
  1579. $result->append($error);
  1580. }
  1581. /**
  1582. * Parses datetime string from FedEx response.
  1583. * According to FedEx API, datetime string should be in \DateTime::ATOM format, but
  1584. * sometimes FedEx returns datetime without timezone and in that case timezone will be set as UTC.
  1585. *
  1586. * @param string $timestamp
  1587. * @return bool|\DateTime
  1588. */
  1589. private function parseDate($timestamp)
  1590. {
  1591. if ($timestamp === null) {
  1592. return false;
  1593. }
  1594. $formats = [\DateTime::ATOM, 'Y-m-d\TH:i:s'];
  1595. foreach ($formats as $format) {
  1596. // set UTC timezone for a case if timestamp does not contain any timezone
  1597. $utcTimezone = new \DateTimeZone('UTC');
  1598. $dateTime = \DateTime::createFromFormat($format, $timestamp, $utcTimezone);
  1599. if ($dateTime !== false) {
  1600. return $dateTime;
  1601. }
  1602. }
  1603. return false;
  1604. }
  1605. /**
  1606. * Defines payment type by request. Two values are available: RECIPIENT or SENDER.
  1607. *
  1608. * @param DataObject $request
  1609. * @return string
  1610. */
  1611. private function getPaymentType(DataObject $request): string
  1612. {
  1613. return $request->getIsReturn() && $request->getShippingMethod() !== self::RATE_REQUEST_SMARTPOST
  1614. ? 'RECIPIENT'
  1615. : 'SENDER';
  1616. }
  1617. }