diff --git a/vendor/magento/module-fedex/Model/Carrier.php b/vendor/magento/module-fedex/Model/Carrier.php index d1f29cf2f8b55..bca48f3c93c4e 100644 --- a/vendor/magento/module-fedex/Model/Carrier.php +++ b/vendor/magento/module-fedex/Model/Carrier.php @@ -9,10 +9,12 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\HTTP\Client\CurlFactory; use Magento\Framework\Measure\Length; use Magento\Framework\Measure\Weight; use Magento\Framework\Module\Dir; use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Url\DecoderInterface; use Magento\Framework\Webapi\Soap\ClientFactory; use Magento\Framework\Xml\Security; use Magento\Quote\Model\Quote\Address\RateRequest; @@ -50,6 +52,41 @@ class Carrier extends AbstractCarrierOnline implements \Magento\Shipping\Model\C */ public const RATE_REQUEST_SMARTPOST = 'SMART_POST'; + /** + * Web Service Type REST + * + * @var string + */ + public const WEB_SERVICE_REST = 'REST'; + + /** + * REST end point for Rate API + * + * @var string + */ + public const RATE_REQUEST_END_POINT = 'rate/v1/rates/quotes'; + + /** + * Authentication grant type for REST endpoints + * + * @var string + */ + public const AUTHENTICATION_GRANT_TYPE = 'client_credentials'; + + /** + * Oauth End point to get Access Token + * + * @var string + */ + public const OAUTH_REQUEST_END_POINT = 'oauth/token'; + + /** + * REST end point of Tracking API + * + * @var string + */ + public const TRACK_REQUEST_END_POINT = 'track/v1/trackingnumbers'; + /** * Code of the carrier * @@ -57,6 +94,20 @@ class Carrier extends AbstractCarrierOnline implements \Magento\Shipping\Model\C */ protected $_code = self::CODE; + /** + * REST end point to Create Shipment + * + * @var string + */ + public const SHIPMENT_REQUEST_END_POINT = '/ship/v1/shipments'; + + /** + * REST end point to cancel Shipment + * + * @var string + */ + public const SHIPMENT_CANCEL_END_POINT = '/ship/v1/shipments/cancel'; + /** * Types of rates, order is important * @@ -164,6 +215,16 @@ class Carrier extends AbstractCarrierOnline implements \Magento\Shipping\Model\C */ private $_rawTrackingRequest; + /** + * @var CurlFactory + */ + private $curlFactory; + + /** + * @var DecoderInterface + */ + private $decoderInterface; + /** * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory @@ -183,6 +244,8 @@ class Carrier extends AbstractCarrierOnline implements \Magento\Shipping\Model\C * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\Module\Dir\Reader $configReader * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory + * @param CurlFactory $curlFactory + * @param DecoderInterface $decoderInterface * @param array $data * @param Json|null $serializer * @param ClientFactory|null $soapClientFactory @@ -207,6 +270,8 @@ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Framework\Module\Dir\Reader $configReader, \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory, + CurlFactory $curlFactory, + DecoderInterface $decoderInterface, array $data = [], Json $serializer = null, ClientFactory $soapClientFactory = null @@ -237,6 +302,8 @@ public function __construct( $this->_trackServiceWsdl = $wsdlBasePath . 'TrackService_v' . self::$trackServiceVersion . '.wsdl'; $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); $this->soapClientFactory = $soapClientFactory ?: ObjectManager::getInstance()->get(ClientFactory::class); + $this->curlFactory = $curlFactory; + $this->decoderInterface = $decoderInterface; } /** @@ -430,6 +497,87 @@ public function getVersionInfo() return ['ServiceId' => 'crs', 'Major' => '10', 'Intermediate' => '0', 'Minor' => '0']; } + /** + * Forming request for rate estimation depending to the purpose for REST API + * + * @param string $purpose + * @return array + */ + protected function _formRestRateRequest($purpose): array + { + $r = $this->_rawRequest; + $ratesRequest = [ + 'accountNumber' => [ + 'value' => $r->getAccount() + ], + 'requestedShipment' => [ + 'pickupType' => $this->getConfigData('pickup_type'), + 'packagingType' => $r->getPackaging(), + 'shipper' => [ + 'address' => ['postalCode' => $r->getOrigPostal(), 'countryCode' => $r->getOrigCountry()], + ], + 'recipient' => [ + 'address' => [ + 'postalCode' => $r->getDestPostal(), + 'countryCode' => $r->getDestCountry(), + 'residential' => (bool)$this->getConfigData('residence_delivery'), + ], + ], + 'customsClearanceDetail' => [ + 'dutiesPayment' => [ + 'payor' => [ + 'responsibleParty' => [ + 'accountNumber' => [ + 'value' => $r->getAccount() + ], + 'address' => [ + 'countryCode' => $r->getOrigCountry() + ] + ] + ], + 'paymentType' => 'SENDER', + ], + 'commodities' => [ + [ + 'customsValue' => ['amount' => $r->getValue(), 'currency' => $this->getCurrencyCode()] + ] + ] + ], + 'rateRequestType' => ['LIST', 'ACCOUNT'] + ] + ]; + + foreach ($r->getPackages() as $packageNum => $package) { + $ratesRequest['requestedShipment']['requestedPackageLineItems'][$packageNum]['subPackagingType'] = + 'PACKAGE'; + $ratesRequest['requestedShipment']['requestedPackageLineItems'][$packageNum]['groupPackageCount'] = 1; + $ratesRequest['requestedShipment']['requestedPackageLineItems'][$packageNum]['weight']['value'] + = (double) $package['weight']; + $ratesRequest['requestedShipment']['requestedPackageLineItems'][$packageNum]['weight']['units'] + = $this->getConfigData('unit_of_measure'); + if (isset($package['price'])) { + $ratesRequest['requestedShipment']['requestedPackageLineItems'][$packageNum]['declaredValue']['amount'] + = (double) $package['price']; + $ratesRequest['requestedShipment']['requestedPackageLineItems'][$packageNum]['declaredValue'] + ['currency'] = $this->getCurrencyCode(); + } + } + + $ratesRequest['requestedShipment']['totalPackageCount'] = count($r->getPackages()); + if ($r->getDestCity()) { + $ratesRequest['requestedShipment']['recipient']['address']['city'] = $r->getDestCity(); + } + + if ($purpose == self::RATE_REQUEST_SMARTPOST) { + $ratesRequest['requestedShipment']['serviceType'] = self::RATE_REQUEST_SMARTPOST; + $ratesRequest['requestedShipment']['smartPostInfoDetail'] = [ + 'indicia' => (double)$r->getWeight() >= 1 ? 'PARCEL_SELECT' : 'PRESORTED_STANDARD', + 'hubId' => $this->getConfigData('smartpost_hubid'), + ]; + } + return $ratesRequest; + } + /** * Forming request for rate estimation depending to the purpose * @@ -501,6 +649,100 @@ protected function _formRateRequest($purpose) return $ratesRequest; } + /** + * Send request for tracking + * + * @param string $tracking + * @return void + */ + protected function _getTrackingInformation($tracking): void + { + if ($this->getConfigData('enabled_tracking_api')) { + $accessToken = $this->getTrackingApiAccessToken(); + } else { + $accessToken = $this->_getAccessToken(); + } + + if (!empty($accessToken)) { + + $trackRequest = [ + 'includeDetailedScans' => true, + 'trackingInfo' => [ + [ + 'trackingNumberInfo' => [ + 'trackingNumber'=> $tracking + ] + ] + ] + ]; + + $requestString = $this->serializer->serialize($trackRequest); + $response = $this->_getCachedQuotes($requestString); + $debugData = ['request' => $trackRequest]; + + if ($response === null) { + $response = $this->sendRequest(self::TRACK_REQUEST_END_POINT, $requestString, $accessToken); + $this->_setCachedQuotes($requestString, $response); + } + $debugData['result'] = $response; + + $this->_debug($debugData); + $this->_parseTrackingResponse($tracking, $response); + } else { + $this->appendTrackingError( + $tracking, + __('Authorization Error. No Access Token found with given credentials.') + ); + return; + } + } + + /** + * Get Access Token for Rest API + * + * @return string|null + */ + protected function _getAccessToken(): string|null + { + $apiKey = $this->getConfigData('api_key') ?? null; + $secretKey = $this->getConfigData('secret_key') ?? null; + + return $this->retrieveAccessToken($apiKey, $secretKey); + } + + /** + * Make the call to get the access token + * + * @param string|null $apiKey + * @param string|null $secretKey + * @return string|null + */ + private function retrieveAccessToken(?string $apiKey, ?string $secretKey): string|null + { + if (!$apiKey || !$secretKey) { + $this->_debug(__('Authentication keys are missing.')); + return null; + } + + $requestArray = [ + 'grant_type' => self::AUTHENTICATION_GRANT_TYPE, + 'client_id' => $apiKey, + 'client_secret' => $secretKey + ]; + + $request = http_build_query($requestArray); + $accessToken = null; + $response = $this->sendRequest(self::OAUTH_REQUEST_END_POINT, $request); + + if (!empty($response['errors'])) { + $debugData = ['request_type' => 'Access Token Request', 'result' => $response]; + $this->_debug($debugData); + } elseif (!empty($response['access_token'])) { + $accessToken = $response['access_token']; + } + + return $accessToken; + } /** * Makes remote request to the carrier and returns a response @@ -511,6 +753,17 @@ protected function _formRateRequest($purpose) protected function _doRatesRequest($purpose) { $ratesRequest = $this->_formRateRequest($purpose); + $accessToken = null; + + if ($this->isRestConfiguration()) { + $response = null; + $accessToken = $this->_getAccessToken(); + if (empty($accessToken)) { + return null; + } + $ratesRequest = $this->_formRestRateRequest($purpose); + } + $ratesRequestNoShipTimestamp = $ratesRequest; unset($ratesRequestNoShipTimestamp['RequestedShipment']['ShipTimestamp']); $requestString = $this->serializer->serialize($ratesRequestNoShipTimestamp); @@ -518,8 +771,14 @@ protected function _doRatesRequest($purpose) $debugData = ['request' => $this->filterDebugData($ratesRequest)]; if ($response === null) { try { - $client = $this->_createRateSoapClient(); - $response = $client->getRates($ratesRequest); + if ($this->isRestConfiguration()) { + $response = $this->sendRequest(self::RATE_REQUEST_END_POINT, $requestString, $accessToken); + $debugData['type'] = 'REST'; + } else { + $client = $this->_createRateSoapClient(); + $response = $client->getRates($ratesRequest); + $debugData['type'] = 'SOAP'; + } $this->_setCachedQuotes($requestString, $response); $debugData['result'] = $response; } catch (\Exception $e) { @@ -534,6 +793,80 @@ protected function _doRatesRequest($purpose) return $response; } + /** + * Get Access Token for Tracking Rest API + * + * @return string|null + */ + private function getTrackingApiAccessToken(): string|null + { + $trackingApiKey = $this->getConfigData('tracking_api_key') ?? null; + $trackingSecretKey = $this->getConfigData('tracking_api_secret_key') ?? null; + + return $this->retrieveAccessToken($trackingApiKey, $trackingSecretKey); + } + + /** + * Send Curl Request + * + * @param string $endpoint + * @param string $request + * @param string|null $accessToken + * @return array|bool + */ + protected function sendRequest($endpoint, $request, $accessToken = null): array|bool + { + if ($accessToken) { + $headers = [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer '.$accessToken, + 'X-locale' => 'en_US', + + ]; + } else { + $headers = ['Content-Type' => 'application/x-www-form-urlencoded']; + } + + $curlClient = $this->curlFactory->create(); + $url = $this->_getUrl($endpoint); + try { + $curlClient->setHeaders($headers); + if ($endpoint == self::SHIPMENT_CANCEL_END_POINT) { + $curlClient->setOptions([CURLOPT_ENCODING => 'gzip,deflate,sdch', CURLOPT_CUSTOMREQUEST => 'PUT']); + } else { + $curlClient->setOptions([CURLOPT_ENCODING => 'gzip,deflate,sdch']); + } + $curlClient->post($url, $request); + $response = $curlClient->getBody(); + $debugData = ['curl_response' => $response]; + $debugData['url'] = $url; + $this->_debug($debugData); + return $this->serializer->unserialize($response); + } catch (\Exception $e) { + $this->_logger->critical($e); + } + return false; + } + + /** + * Get Url for REST API + * + * @param string|null $endpoint + * @return string + */ + protected function _getUrl($endpoint = null): string + { + $url = $this->getConfigFlag('sandbox_mode') ? $this->getConfigData('sandbox_webservices_url') + : $this->getConfigData('production_webservices_url'); + + if ($this->isRestConfiguration()) { + $url = $this->getConfigFlag('sandbox_mode') ? $this->getConfigData('rest_sandbox_webservices_url') + : $this->getConfigData('rest_production_webservices_url'); + } + + return $endpoint ? $url . $endpoint : $url; + } + /** * Do remote request for and handle errors * @@ -571,13 +904,35 @@ protected function _getQuotes() * @return Result * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - protected function _prepareRateResponse($response) + protected function _prepareRateResponse($response): Result { $costArr = []; $priceArr = []; - $errorTitle = 'For some reason we can\'t retrieve tracking info right now.'; + $errorTitle = __('For some reason we can\'t retrieve tracking info right now.'); - if (is_object($response)) { + if ($this->isRestConfiguration() && is_array($response)) { + if (!empty($response['errors'])) { + if (is_array($response['errors'])) { + $notification = reset($response['errors']); + $errorTitle = (string)$notification['message']; + } else { + $errorTitle = (string)$response['errors']['message']; + } + } elseif (isset($response['output']['rateReplyDetails'])) { + $allowedMethods = explode(",", $this->getConfigData('allowed_methods')); + if (is_array($response['output']['rateReplyDetails'])) { + foreach ($response['output']['rateReplyDetails'] as $rate) { + $serviceName = (string)$rate['serviceType']; + if (in_array($serviceName, $allowedMethods)) { + $amount = $this->_getRateAmountOriginBased($rate); + $costArr[$serviceName] = $amount; + $priceArr[$serviceName] = $this->getMethodPrice($amount, $serviceName); + } + } + asort($priceArr); + } + } + } elseif (is_object($response)) { if ($response->HighestSeverity == 'FAILURE' || $response->HighestSeverity == 'ERROR') { if (is_array($response->Notifications)) { $notification = array_pop($response->Notifications); @@ -674,34 +1029,86 @@ protected function _getPerorderPrice($cost, $handlingType, $handlingFee) * @param \stdClass $rate * @return null|float */ - protected function _getRateAmountOriginBased($rate) + protected function _getRateAmountOriginBasedSoap($rate): null|float { $amount = null; $currencyCode = ''; $rateTypeAmounts = []; - if (is_object($rate)) { - // The "RATED..." rates are expressed in the currency of the origin country - foreach ($rate->RatedShipmentDetails as $ratedShipmentDetail) { - $netAmount = (string)$ratedShipmentDetail->ShipmentRateDetail->TotalNetCharge->Amount; - $currencyCode = (string)$ratedShipmentDetail->ShipmentRateDetail->TotalNetCharge->Currency; - $rateType = (string)$ratedShipmentDetail->ShipmentRateDetail->RateType; - $rateTypeAmounts[$rateType] = $netAmount; + + // The "RATED..." rates are expressed in the currency of the origin country + foreach ($rate->RatedShipmentDetails as $ratedShipmentDetail) { + $netAmount = (string)$ratedShipmentDetail->ShipmentRateDetail->TotalNetCharge->Amount; + $currencyCode = (string)$ratedShipmentDetail->ShipmentRateDetail->TotalNetCharge->Currency; + $rateType = (string)$ratedShipmentDetail->ShipmentRateDetail->RateType; + $rateTypeAmounts[$rateType] = $netAmount; + } + + foreach ($this->_ratesOrder as $rateType) { + if (!empty($rateTypeAmounts[$rateType])) { + $amount = $rateTypeAmounts[$rateType]; + break; } + } - foreach ($this->_ratesOrder as $rateType) { - if (!empty($rateTypeAmounts[$rateType])) { - $amount = $rateTypeAmounts[$rateType]; - break; - } + if ($amount === null) { + $amount = (string)$rate->RatedShipmentDetails[0]->ShipmentRateDetail->TotalNetCharge->Amount; + } + + return (float)$amount * $this->getBaseCurrencyRate($currencyCode); + } + + /** + * Get origin based amount form response of rate estimation + * + * @param array $rate + * @return null|float + */ + protected function _getRateAmountOriginBasedRest($rate): null|float + { + $amount = null; + $currencyCode = ''; + $rateTypeAmounts = []; + + // The "RATED..." rates are expressed in the currency of the origin country + foreach ($rate['ratedShipmentDetails'] as $ratedShipmentDetail) { + $netAmount = (string)$ratedShipmentDetail['totalNetCharge']; + $currencyCode = (string)$ratedShipmentDetail['shipmentRateDetail']['currency']; + + if (!empty($ratedShipmentDetail['ratedPackages'])) { + $rateType = (string)reset($ratedShipmentDetail['ratedPackages']) + ['packageRateDetail']['rateType']; + $rateTypeAmounts[$rateType] = $netAmount; } + } - if ($amount === null) { - $amount = (string)$rate->RatedShipmentDetails[0]->ShipmentRateDetail->TotalNetCharge->Amount; + foreach ($this->_ratesOrder as $rateType) { + if (!empty($rateTypeAmounts[$rateType])) { + $amount = $rateTypeAmounts[$rateType]; + break; } + } - $amount = (float)$amount * $this->getBaseCurrencyRate($currencyCode); + if ($amount === null && !empty($rate['ratedShipmentDetails'][0]['totalNetCharge'])) { + $amount = (string)$rate['ratedShipmentDetails'][0]['totalNetCharge']; } + return (float)$amount * $this->getBaseCurrencyRate($currencyCode); + } + + /** + * Get origin based amount form response of rate estimation + * + * @param \stdClass|array $rate + * @return null|float + */ + protected function _getRateAmountOriginBased($rate): null|float + { + $amount = null; + if ($this->isRestConfiguration() && is_array($rate)) { + $amount = $this->_getRateAmountOriginBasedRest($rate); + } elseif (is_object($rate)) { + $amount = $this->_getRateAmountOriginBasedSoap($rate); + } return $amount; } @@ -929,6 +1336,15 @@ public function getCode($type, $code = '') 'BUSINESS_SERVICE_CENTER' => __('Business Service Center'), 'STATION' => __('Station'), ], + 'pickup_type' => [ + 'CONTACT_FEDEX_TO_SCHEDULE' => __('Contact Fedex to Schedule'), + 'DROPOFF_AT_FEDEX_LOCATION' => __('DropOff at Fedex Location'), + 'USE_SCHEDULED_PICKUP' => __('Use Scheduled Pickup'), + 'ON_CALL' => __('On Call'), + 'PACKAGE_RETURN_PROGRAM' => __('Package Return Program'), + 'REGULAR_STOP' => __('Regular Stop'), + 'TAG' => __('Tag'), + ], 'packaging' => [ 'FEDEX_ENVELOPE' => __('FedEx Envelope'), 'FEDEX_PAK' => __('FedEx Pak'), @@ -938,6 +1354,10 @@ public function getCode($type, $code = '') 'FEDEX_25KG_BOX' => __('FedEx 25kg Box'), 'YOUR_PACKAGING' => __('Your Packaging'), ], + 'web_service_type' => [ + 'SOAP' => __('SOAP'), + 'REST' => __('REST') + ], 'containers_filter' => [ [ 'containers' => ['FEDEX_ENVELOPE', 'FEDEX_PAK'], @@ -1088,14 +1508,23 @@ public function getCurrencyCode() */ public function getTracking($trackings) { - $this->setTrackingReqeust(); + if ($this->isRestConfiguration()) { + if (!is_array($trackings)) { + $trackings = [$trackings]; + } + foreach ($trackings as $tracking) { + $this->_getTrackingInformation($tracking); + } + } else { + $this->setTrackingReqeust(); - if (!is_array($trackings)) { - $trackings = [$trackings]; - } + if (!is_array($trackings)) { + $trackings = [$trackings]; + } - foreach ($trackings as $tracking) { - $this->_getXMLTracking($tracking); + foreach ($trackings as $tracking) { + $this->_getXMLTracking($tracking); + } } return $this->_result; @@ -1119,7 +1548,7 @@ protected function setTrackingReqeust() /** * Send request for tracking * - * @param string[] $tracking + * @param string|string[] $tracking * @return void */ protected function _getXMLTracking($tracking) @@ -1173,25 +1602,41 @@ protected function _getXMLTracking($tracking) * @param string $trackingValue * @param \stdClass $response * @return void + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ protected function _parseTrackingResponse($trackingValue, $response) { - if (!is_object($response) || empty($response->HighestSeverity)) { - $this->appendTrackingError($trackingValue, __('Invalid response from carrier')); - return; - } elseif (in_array($response->HighestSeverity, self::$trackingErrors)) { - $this->appendTrackingError($trackingValue, (string) $response->Notifications->Message); - return; - } elseif (empty($response->CompletedTrackDetails) || empty($response->CompletedTrackDetails->TrackDetails)) { - $this->appendTrackingError($trackingValue, __('No available tracking items')); - return; - } + if ($this->isRestConfiguration()) { + if (!is_array($response) || empty($response['output'])) { + $this->_debug($response); + $this->appendTrackingError($trackingValue, __('Invalid response from carrier')); + return; + } elseif (empty(reset($response['output']['completeTrackResults'])['trackResults'])) { + $this->_debug('No available tracking items'); + $this->appendTrackingError($trackingValue, __('No available tracking items')); + return; + } + $trackInfo = reset($response['output']['completeTrackResults'])['trackResults']; + } else { + if (!is_object($response) || empty($response->HighestSeverity)) { + $this->appendTrackingError($trackingValue, __('Invalid response from carrier')); + return; + } elseif (in_array($response->HighestSeverity, self::$trackingErrors)) { + $this->appendTrackingError($trackingValue, (string) $response->Notifications->Message); + return; + } elseif (empty($response->CompletedTrackDetails) + || empty($response->CompletedTrackDetails->TrackDetails)) { + $this->appendTrackingError($trackingValue, __('No available tracking items')); + return; + } - $trackInfo = $response->CompletedTrackDetails->TrackDetails; + $trackInfo = $response->CompletedTrackDetails->TrackDetails; - // Fedex can return tracking details as single object instead array - if (is_object($trackInfo)) { - $trackInfo = [$trackInfo]; + // Fedex can return tracking details as single object instead array + if (is_object($trackInfo)) { + $trackInfo = [$trackInfo]; + } } $result = $this->getResult(); @@ -1202,7 +1647,13 @@ protected function _parseTrackingResponse($trackingValue, $response) $tracking->setCarrier(self::CODE); $tracking->setCarrierTitle($carrierTitle); $tracking->setTracking($trackingValue); - $tracking->addData($this->processTrackingDetails($item)); + + if ($this->isRestConfiguration()) { + $tracking->addData($this->processRestTrackingDetails($item)); + } else { + $tracking->addData($this->processTrackingDetails($item)); + } + $result->append($tracking); $counter ++; } @@ -1344,136 +1795,268 @@ protected function _formShipmentRequest(\Magento\Framework\DataObject $request) $paymentType = $this->getPaymentType($request); $optionType = $request->getShippingMethod() == self::RATE_REQUEST_SMARTPOST ? 'SERVICE_DEFAULT' : $packageParams->getDeliveryConfirmation(); - $requestClient = [ - 'RequestedShipment' => [ - 'ShipTimestamp' => time(), - 'DropoffType' => $this->getConfigData('dropoff'), - 'PackagingType' => $request->getPackagingType(), - 'ServiceType' => $request->getShippingMethod(), - 'Shipper' => [ - 'Contact' => [ - 'PersonName' => $request->getShipperContactPersonName(), - 'CompanyName' => $request->getShipperContactCompanyName(), - 'PhoneNumber' => $request->getShipperContactPhoneNumber(), - ], - 'Address' => [ - 'StreetLines' => [$request->getShipperAddressStreet()], - 'City' => $request->getShipperAddressCity(), - 'StateOrProvinceCode' => $request->getShipperAddressStateOrProvinceCode(), - 'PostalCode' => $request->getShipperAddressPostalCode(), - 'CountryCode' => $request->getShipperAddressCountryCode(), + + if ($this->isRestConfiguration()) { + $requestClient = [ + 'requestedShipment' => [ + 'shipDatestamp' => date('Y-m-d'), + 'pickupType' => $this->getConfigData('pickup_type'), + 'serviceType' => $request->getShippingMethod(), + 'packagingType' => $request->getPackagingType(), + 'shipper' => [ + 'contact' => [ + 'personName' => $request->getShipperContactPersonName(), + 'companyName' => $request->getShipperContactCompanyName(), + 'phoneNumber' => $request->getShipperContactPhoneNumber(), + ], + 'address' => [ + 'streetLines' => [$request->getShipperAddressStreet()], + 'city' => $request->getShipperAddressCity(), + 'stateOrProvinceCode' => $request->getShipperAddressStateOrProvinceCode(), + 'postalCode' => $request->getShipperAddressPostalCode(), + 'countryCode' => $request->getShipperAddressCountryCode(), + ] ], - ], - 'Recipient' => [ - 'Contact' => [ - 'PersonName' => $request->getRecipientContactPersonName(), - 'CompanyName' => $request->getRecipientContactCompanyName(), - 'PhoneNumber' => $request->getRecipientContactPhoneNumber(), + 'recipients' => [ + [ + 'contact' => [ + 'personName' => $request->getRecipientContactPersonName(), + 'companyName' => $request->getRecipientContactCompanyName(), + 'phoneNumber' => $request->getRecipientContactPhoneNumber() + ], + 'address' => [ + 'streetLines' => [$request->getRecipientAddressStreet()], + 'city' => $request->getRecipientAddressCity(), + 'stateOrProvinceCode' => $request->getRecipientAddressStateOrProvinceCode(), + 'postalCode' => $request->getRecipientAddressPostalCode(), + 'countryCode' => $request->getRecipientAddressCountryCode(), + 'residential' => (bool)$this->getConfigData('residence_delivery'), + ] + ], ], - 'Address' => [ - 'StreetLines' => [$request->getRecipientAddressStreet()], - 'City' => $request->getRecipientAddressCity(), - 'StateOrProvinceCode' => $request->getRecipientAddressStateOrProvinceCode(), - 'PostalCode' => $request->getRecipientAddressPostalCode(), - 'CountryCode' => $request->getRecipientAddressCountryCode(), - 'Residential' => (bool)$this->getConfigData('residence_delivery'), + 'shippingChargesPayment' => [ + 'paymentType' => $paymentType, + 'payor' => [ + 'responsibleParty' => [ + 'accountNumber' => ['value' => $this->getConfigData('account')] + ], + ], ], - ], - 'ShippingChargesPayment' => [ - 'PaymentType' => $paymentType, - 'Payor' => [ - 'AccountNumber' => $this->getConfigData('account'), - 'CountryCode' => $this->_scopeConfig->getValue( - \Magento\Sales\Model\Order\Shipment::XML_PATH_STORE_COUNTRY_ID, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $request->getStoreId() - ), + 'labelSpecification' => [ + 'labelFormatType' => 'COMMON2D', + 'imageType' => 'PNG', + 'labelStockType' => 'PAPER_85X11_TOP_HALF_LABEL', ], + 'rateRequestType' => ['ACCOUNT'], + 'totalPackageCount' => 1 ], - 'LabelSpecification' => [ - 'LabelFormatType' => 'COMMON2D', - 'ImageType' => 'PNG', - 'LabelStockType' => 'PAPER_8.5X11_TOP_HALF_LABEL', - ], - 'RateRequestTypes' => ['ACCOUNT'], - 'PackageCount' => 1, - 'RequestedPackageLineItems' => [ - 'SequenceNumber' => '1', - 'Weight' => ['Units' => $weightUnits, 'Value' => $request->getPackageWeight()], - 'CustomerReferences' => [ - 'CustomerReferenceType' => 'CUSTOMER_REFERENCE', - 'Value' => $referenceData, + 'labelResponseOptions' => 'LABEL', + 'accountNumber' => ['value' => $this->getConfigData('account')] + ]; + + // for international shipping + if ($request->getShipperAddressCountryCode() != $request->getRecipientAddressCountryCode()) { + $requestClient['requestedShipment']['customsClearanceDetail'] = [ + 'totalCustomsValue' => ['currency' => $request->getBaseCurrencyCode(), 'amount' => $customsValue], + 'dutiesPayment' => [ + 'paymentType' => $paymentType, + 'payor' => [ + 'responsibleParty' => [ + 'accountNumber' => ['value' => $this->getConfigData('account')], + 'address' => ['countryCode' => $this->_scopeConfig->getValue( + \Magento\Sales\Model\Order\Shipment::XML_PATH_STORE_COUNTRY_ID, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $request->getStoreId() + )], + ], + ], ], - 'SpecialServicesRequested' => [ - 'SpecialServiceTypes' => 'SIGNATURE_OPTION', - 'SignatureOptionDetail' => ['OptionType' => $optionType], + 'commodities' => [ + [ + 'weight' => ['units' => $weightUnits, 'value' => $request->getPackageWeight()], + 'numberOfPieces' => 1, + 'countryOfManufacture' => implode(',', array_unique($countriesOfManufacture)), + 'description' => implode(', ', $itemsDesc), + 'quantity' => ceil($itemsQty), + 'quantityUnits' => 'pcs', + 'unitPrice' => ['currency' => $request->getBaseCurrencyCode(), 'amount' => $unitPrice], + 'customsValue' => ['currency' => $request->getBaseCurrencyCode(), + 'amount' => $customsValue], + ], ], + ]; + } + + if ($request->getMasterTrackingId()) { + $requestClient['requestedShipment']['masterTrackingId']['trackingNumber'] = + $request->getMasterTrackingId(); + } + + if ($request->getShippingMethod() == self::RATE_REQUEST_SMARTPOST) { + $requestClient['requestedShipment']['smartPostInfoDetail'] = [ + 'indicia' => (double)$request->getPackageWeight() >= 1 ? 'PARCEL_SELECT' : 'PRESORTED_STANDARD', + 'hubId' => $this->getConfigData('smartpost_hubid'), + ]; + } + + $requestedPackageLineItems = [ + 'sequenceNumber' => '1', + 'weight' => ['units' => $weightUnits, 'value' => $request->getPackageWeight()], + 'customerReferences' => [ + [ + 'customerReferenceType' => 'CUSTOMER_REFERENCE', + 'value' => $referenceData, + ] ], - ], - ]; + 'packageSpecialServices' => [ + 'specialServiceTypes' => ['SIGNATURE_OPTION'], + 'signatureOptionType' => $optionType + + ] + ]; + + // set dimensions + if ($length || $width || $height) { + $requestedPackageLineItems['dimensions'] = [ + 'length' => $length, + 'width' => $width, + 'height' => $height, + 'units' => $packageParams->getDimensionUnits() == Length::INCH ? 'IN' : 'CM', + ]; + } - // for international shipping - if ($request->getShipperAddressCountryCode() != $request->getRecipientAddressCountryCode()) { - $requestClient['RequestedShipment']['CustomsClearanceDetail'] = [ - 'CustomsValue' => ['Currency' => $request->getBaseCurrencyCode(), 'Amount' => $customsValue], - 'DutiesPayment' => [ - 'PaymentType' => $paymentType, - 'Payor' => [ - 'AccountNumber' => $this->getConfigData('account'), - 'CountryCode' => $this->_scopeConfig->getValue( - \Magento\Sales\Model\Order\Shipment::XML_PATH_STORE_COUNTRY_ID, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $request->getStoreId() - ), + $requestClient['requestedShipment']['requestedPackageLineItems'] = [$requestedPackageLineItems]; + return $requestClient; + } else { + $requestClient = [ + 'RequestedShipment' => [ + 'ShipTimestamp' => time(), + 'DropoffType' => $this->getConfigData('dropoff'), + 'PackagingType' => $request->getPackagingType(), + 'ServiceType' => $request->getShippingMethod(), + 'Shipper' => [ + 'Contact' => [ + 'PersonName' => $request->getShipperContactPersonName(), + 'CompanyName' => $request->getShipperContactCompanyName(), + 'PhoneNumber' => $request->getShipperContactPhoneNumber(), + ], + 'Address' => [ + 'StreetLines' => [$request->getShipperAddressStreet()], + 'City' => $request->getShipperAddressCity(), + 'StateOrProvinceCode' => $request->getShipperAddressStateOrProvinceCode(), + 'PostalCode' => $request->getShipperAddressPostalCode(), + 'CountryCode' => $request->getShipperAddressCountryCode(), + ], + ], + 'Recipient' => [ + 'Contact' => [ + 'PersonName' => $request->getRecipientContactPersonName(), + 'CompanyName' => $request->getRecipientContactCompanyName(), + 'PhoneNumber' => $request->getRecipientContactPhoneNumber(), + ], + 'Address' => [ + 'StreetLines' => [$request->getRecipientAddressStreet()], + 'City' => $request->getRecipientAddressCity(), + 'StateOrProvinceCode' => $request->getRecipientAddressStateOrProvinceCode(), + 'PostalCode' => $request->getRecipientAddressPostalCode(), + 'CountryCode' => $request->getRecipientAddressCountryCode(), + 'Residential' => (bool)$this->getConfigData('residence_delivery'), + ], + ], + 'ShippingChargesPayment' => [ + 'PaymentType' => $paymentType, + 'Payor' => [ + 'AccountNumber' => $this->getConfigData('account'), + 'CountryCode' => $this->_scopeConfig->getValue( + \Magento\Sales\Model\Order\Shipment::XML_PATH_STORE_COUNTRY_ID, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $request->getStoreId() + ), + ], + ], + 'LabelSpecification' => [ + 'LabelFormatType' => 'COMMON2D', + 'ImageType' => 'PNG', + 'LabelStockType' => 'PAPER_8.5X11_TOP_HALF_LABEL', + ], + 'RateRequestTypes' => ['ACCOUNT'], + 'PackageCount' => 1, + 'RequestedPackageLineItems' => [ + 'SequenceNumber' => '1', + 'Weight' => ['Units' => $weightUnits, 'Value' => $request->getPackageWeight()], + 'CustomerReferences' => [ + 'CustomerReferenceType' => 'CUSTOMER_REFERENCE', + 'Value' => $referenceData, + ], + 'SpecialServicesRequested' => [ + 'SpecialServiceTypes' => 'SIGNATURE_OPTION', + 'SignatureOptionDetail' => ['OptionType' => $optionType], + ], ], - ], - 'Commodities' => [ - 'Weight' => ['Units' => $weightUnits, 'Value' => $request->getPackageWeight()], - 'NumberOfPieces' => 1, - 'CountryOfManufacture' => implode(',', array_unique($countriesOfManufacture)), - 'Description' => implode(', ', $itemsDesc), - 'Quantity' => ceil($itemsQty), - 'QuantityUnits' => 'pcs', - 'UnitPrice' => ['Currency' => $request->getBaseCurrencyCode(), 'Amount' => $unitPrice], - 'CustomsValue' => ['Currency' => $request->getBaseCurrencyCode(), 'Amount' => $customsValue], ], ]; - } - if ($request->getMasterTrackingId()) { - $requestClient['RequestedShipment']['MasterTrackingId'] = $request->getMasterTrackingId(); - } + // for international shipping + if ($request->getShipperAddressCountryCode() != $request->getRecipientAddressCountryCode()) { + $requestClient['RequestedShipment']['CustomsClearanceDetail'] = [ + 'CustomsValue' => ['Currency' => $request->getBaseCurrencyCode(), 'Amount' => $customsValue], + 'DutiesPayment' => [ + 'PaymentType' => $paymentType, + 'Payor' => [ + 'AccountNumber' => $this->getConfigData('account'), + 'CountryCode' => $this->_scopeConfig->getValue( + \Magento\Sales\Model\Order\Shipment::XML_PATH_STORE_COUNTRY_ID, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $request->getStoreId() + ), + ], + ], + 'Commodities' => [ + 'Weight' => ['Units' => $weightUnits, 'Value' => $request->getPackageWeight()], + 'NumberOfPieces' => 1, + 'CountryOfManufacture' => implode(',', array_unique($countriesOfManufacture)), + 'Description' => implode(', ', $itemsDesc), + 'Quantity' => ceil($itemsQty), + 'QuantityUnits' => 'pcs', + 'UnitPrice' => ['Currency' => $request->getBaseCurrencyCode(), 'Amount' => $unitPrice], + 'CustomsValue' => ['Currency' => $request->getBaseCurrencyCode(), 'Amount' => $customsValue], + ], + ]; + } - if ($request->getShippingMethod() == self::RATE_REQUEST_SMARTPOST) { - $requestClient['RequestedShipment']['SmartPostDetail'] = [ - 'Indicia' => (double)$request->getPackageWeight() >= 1 ? 'PARCEL_SELECT' : 'PRESORTED_STANDARD', - 'HubId' => $this->getConfigData('smartpost_hubid'), - ]; - } + if ($request->getMasterTrackingId()) { + $requestClient['RequestedShipment']['MasterTrackingId'] = $request->getMasterTrackingId(); + } - // set dimensions - if ($length || $width || $height) { - $requestClient['RequestedShipment']['RequestedPackageLineItems']['Dimensions'] = [ - 'Length' => $length, - 'Width' => $width, - 'Height' => $height, - 'Units' => $packageParams->getDimensionUnits() == Length::INCH ? 'IN' : 'CM', - ]; - } + if ($request->getShippingMethod() == self::RATE_REQUEST_SMARTPOST) { + $requestClient['RequestedShipment']['SmartPostDetail'] = [ + 'Indicia' => (double)$request->getPackageWeight() >= 1 ? 'PARCEL_SELECT' : 'PRESORTED_STANDARD', + 'HubId' => $this->getConfigData('smartpost_hubid'), + ]; + } - return $this->_getAuthDetails() + $requestClient; + // set dimensions + if ($length || $width || $height) { + $requestClient['RequestedShipment']['RequestedPackageLineItems']['Dimensions'] = [ + 'Length' => $length, + 'Width' => $width, + 'Height' => $height, + 'Units' => $packageParams->getDimensionUnits() == Length::INCH ? 'IN' : 'CM', + ]; + } + return $this->_getAuthDetails() + $requestClient; + } } /** - * Do shipment request to carrier web service, obtain Print Shipping Labels and process errors in response + * Do shipment request to carrier's SOAP web service, obtain Print Shipping Labels and process errors in response * * @param \Magento\Framework\DataObject $request + * @param \Magento\Framework\DataObject $result * @return \Magento\Framework\DataObject */ - protected function _doShipmentRequest(\Magento\Framework\DataObject $request) + protected function _doShipmentRequestSoap($request, $result): \Magento\Framework\DataObject { - $this->_prepareShipmentRequest($request); - $result = new \Magento\Framework\DataObject(); $client = $this->_createShipSoapClient(); $requestClient = $this->_formShipmentRequest($request); $debugData['request'] = $this->filterDebugData($requestClient); @@ -1503,7 +2086,77 @@ protected function _doShipmentRequest(\Magento\Framework\DataObject $request) $result->setErrors($debugData['result']['error']); } $result->setGatewayResponse($client->__getLastResponse()); + return $result; + } + + /** + * Do shipment request to carrier's REST web service, obtain Print Shipping Labels and process errors in response + * + * @param \Magento\Framework\DataObject $request + * @param \Magento\Framework\DataObject $result + * @return \Magento\Framework\DataObject + */ + protected function _doShipmentRequestRest($request, $result): \Magento\Framework\DataObject + { + $response = null; + $accessToken = $this->_getAccessToken(); + if (empty($accessToken)) { + return $result->setErrors(__('Authorization Error. No Access Token found with given credentials.')); + } + + $requestClient = $this->_formShipmentRequest($request); + $requestString = $this->serializer->serialize($requestClient); + + $debugData = ['request' => $this->filterDebugData($requestClient)]; + + $response = $this->sendRequest(self::SHIPMENT_REQUEST_END_POINT, $requestString, $accessToken); + + $debugData['result'] = $response; + + if (!empty($response['output']['transactionShipments'])) { + $shippingLabelContent = $this->getPackagingLabel( + reset($response['output']['transactionShipments'])['pieceResponses'] + ); + $trackingNumber = $this->getTrackingNumber( + reset($response['output']['transactionShipments'])['pieceResponses'] + ); + $result->setShippingLabelContent($this->decoderInterface->decode($shippingLabelContent)); + $result->setTrackingNumber($trackingNumber); + } else { + $debugData['result'] = ['error' => '', 'code' => '', 'message' => $response]; + if (is_array($response['errors'])) { + foreach ($response['errors'] as $notification) { + $debugData['result']['code'] .= $notification['code'] . '; '; + $debugData['result']['error'] .= $notification['message'] . '; '; + } + } else { + $debugData['result']['code'] = $response['errors']['code'] . ' '; + $debugData['result']['error'] = $response['errors']['message'] . ' '; + } + + $result->setErrors($debugData['result']['error']); + } + $this->_debug($debugData); + return $result; + } + + /** + * Do shipment request to carrier web service, obtain Print Shipping Labels and process errors in response + * + * @param \Magento\Framework\DataObject $request + * @return \Magento\Framework\DataObject + */ + protected function _doShipmentRequest(\Magento\Framework\DataObject $request): \Magento\Framework\DataObject + { + $this->_prepareShipmentRequest($request); + $result = new \Magento\Framework\DataObject(); + + if ($this->isRestConfiguration()) { + $result = $this->_doShipmentRequestRest($request, $result); + } else { + $result = $this->_doShipmentRequestSoap($request, $result); + } return $result; } @@ -1515,12 +2168,27 @@ protected function _doShipmentRequest(\Magento\Framework\DataObject $request) */ private function getTrackingNumber($trackingIds) { - return is_array($trackingIds) ? array_map( - function ($val) { - return $val->TrackingNumber; - }, - $trackingIds - ) : $trackingIds->TrackingNumber; + if ($this->isRestConfiguration()) { + return reset($trackingIds)['trackingNumber']; + } else { + return is_array($trackingIds) ? array_map( + function ($val) { + return $val->TrackingNumber; + }, + $trackingIds + ) : $trackingIds->TrackingNumber; + } + } + + /** + * Return Packaging Label + * + * @param array|object $pieceResponses + * @return string + */ + private function getPackagingLabel($pieceResponses): string + { + return reset(reset($pieceResponses)['packageDocuments'])['encodedLabel']; } /** @@ -1532,12 +2200,33 @@ function ($val) { */ public function rollBack($data) { - $requestData = $this->_getAuthDetails(); - $requestData['DeletionControl'] = 'DELETE_ONE_PACKAGE'; - foreach ($data as &$item) { - $requestData['TrackingId'] = $item['tracking_number']; - $client = $this->_createShipSoapClient(); - $client->deleteShipment($requestData); + if ($this->isRestConfiguration()) { + $accessToken = $this->_getAccessToken(); + if (empty($accessToken)) { + $this->_debug('Authorization Error. No Access Token found with given credentials.'); + return false; + } + + $requestData['accountNumber'] = ['value' => $this->getConfigData('account')]; + $requestData['deletionControl'] = 'DELETE_ALL_PACKAGES'; + + foreach ($data as &$item) { + $requestData['trackingNumber'] = $item['tracking_number']; + $requestString = $this->serializer->serialize($requestData); + + $debugData = ['request' => $requestData]; + $response = $this->sendRequest(self::SHIPMENT_CANCEL_END_POINT, $requestString, $accessToken); + $debugData['result'] = $response; + $this->_debug($debugData); + } + } else { + $requestData = $this->_getAuthDetails(); + $requestData['DeletionControl'] = 'DELETE_ONE_PACKAGE'; + foreach ($data as &$item) { + $requestData['TrackingId'] = $item['tracking_number']; + $client = $this->_createShipSoapClient(); + $client->deleteShipment($requestData); + } } return true; @@ -1627,7 +2316,7 @@ public function getDeliveryConfirmationTypes(\Magento\Framework\DataObject $para /** * Recursive replace sensitive fields in debug data by the mask * - * @param string $data + * @param string|array $data * @return string */ protected function filterDebugData($data) @@ -1713,13 +2402,145 @@ private function processTrackingDetails(\stdClass $trackInfo) return $result; } + /** + * Parse track details response from Fedex for REST API + * + * @param array $trackInfo + * @return array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + private function processRestTrackingDetails($trackInfo): array + { + $result = [ + 'shippeddate' => null, + 'deliverydate' => null, + 'deliverytime' => null, + 'deliverylocation' => null, + 'weight' => null, + 'progressdetail' => [], + ]; + + if (!empty($trackInfo['dateAndTimes']) && is_array($trackInfo['dateAndTimes'])) { + $datetime = null; + foreach ($trackInfo['dateAndTimes'] as $dateAndTimeInfo) { + if (!empty($dateAndTimeInfo['type']) && $dateAndTimeInfo['type'] == 'SHIP') { + $datetime = $this->parseDate($dateAndTimeInfo['dateTime']); + break; + } + } + + if ($datetime) { + $result['shippeddate'] = gmdate('Y-m-d', $datetime->getTimestamp()); + } + } + + $result['signedby'] = !empty($trackInfo['deliveryDetails']['receivedByName']) ? + (string) $trackInfo['deliveryDetails']['receivedByName'] : + null; + + $result['status'] = (!empty($trackInfo['latestStatusDetail']) && + !empty($trackInfo['latestStatusDetail']['description'])) ? + (string) $trackInfo['latestStatusDetail']['description'] : + null; + $result['service'] = (!empty($trackInfo['serviceDetail']) && + !empty($trackInfo['serviceDetail']['description'])) ? + (string) $trackInfo['serviceDetail']['description'] : + null; + + $datetime = $this->getDeliveryDateTime($trackInfo); + if ($datetime) { + $result['deliverydate'] = gmdate('Y-m-d', $datetime->getTimestamp()); + $result['deliverytime'] = gmdate('H:i:s', $datetime->getTimestamp()); + } + + $address = null; + if (!empty($trackInfo['deliveryDetails']['estimatedDeliveryAddress'])) { + $address = $trackInfo['deliveryDetails']['estimatedDeliveryAddress']; + } elseif (!empty($trackInfo['deliveryDetails']['actualDeliveryAddress'])) { + $address = $trackInfo['deliveryDetails']['actualDeliveryAddress']; + } + + if (!empty($address)) { + $result['deliverylocation'] = $this->getDeliveryAddress($address); + } + + if (!empty($trackInfo['packageDetails']['weightAndDimensions']['weight'])) { + $weightUnit = $this->getConfigData('unit_of_measure') ?? 'LB'; + $weightValue = null; + foreach ($trackInfo['packageDetails']['weightAndDimensions']['weight'] as $weightInfo) { + if ($weightInfo['unit'] == $weightUnit) { + $weightValue = $weightInfo['value']; + break; + } + } + + $result['weight'] = sprintf( + '%s %s', + (string) $weightValue, + (string) $weightUnit + ); + } + + if (!empty($trackInfo['scanEvents'])) { + $events = $trackInfo['scanEvents']; + if (is_object($trackInfo['scanEvents'])) { + $events = [$trackInfo['scanEvents']]; + } + $result['progressdetail'] = $this->processTrackDetailsEvents($events); + } + + return $result; + } + /** * Parse delivery datetime from tracking details * - * @param \stdClass $trackInfo + * @param \stdClass|array $trackInfo * @return \Datetime|null */ - private function getDeliveryDateTime(\stdClass $trackInfo) + private function getDeliveryDateTime($trackInfo) + { + $timestamp = null; + if ($this->isRestConfiguration() && is_array($trackInfo)) { + $timestamp = $this->getDeliveryDateTimeRest($trackInfo); + return $timestamp ?: null; + } else { + $timestamp = $this->getDeliveryDateTimeSoap($trackInfo); + return $timestamp ? $this->parseDate($timestamp) : null; + } + } + + /** + * Parse delivery datetime from tracking details for REST API + * + * @param array $trackInfo + * @return \Datetime|null + */ + private function getDeliveryDateTimeRest($trackInfo): \Datetime|null + { + $timestamp = null; + if (!empty($trackInfo['dateAndTimes']) && is_array($trackInfo['dateAndTimes'])) { + foreach ($trackInfo['dateAndTimes'] as $dateAndTimeInfo) { + if (!empty($dateAndTimeInfo['type']) && + ($dateAndTimeInfo['type'] == 'ESTIMATED_DELIVERY' || $dateAndTimeInfo['type'] == 'ACTUAL_DELIVERY') + && !empty($dateAndTimeInfo['dateTime']) + ) { + $timestamp = $this->parseDate($dateAndTimeInfo['dateTime']); + break; + } + } + } + return $timestamp ?: null; + } + + /** + * Parse delivery datetime from tracking details for SOAP API + * + * @param \stdClass $trackInfo + * @return string|null + */ + private function getDeliveryDateTimeSoap($trackInfo): string|null { $timestamp = null; if (!empty($trackInfo->EstimatedDeliveryTimestamp)) { @@ -1727,30 +2548,42 @@ private function getDeliveryDateTime(\stdClass $trackInfo) } elseif (!empty($trackInfo->ActualDeliveryTimestamp)) { $timestamp = $trackInfo->ActualDeliveryTimestamp; } - - return $timestamp ? $this->parseDate($timestamp) : null; + return $timestamp; } /** * Get delivery address details in string representation Return City, State, Country Code * - * @param \stdClass $address + * @param \stdClass|array $address * @return \Magento\Framework\Phrase|string */ - private function getDeliveryAddress(\stdClass $address) + private function getDeliveryAddress($address): \Magento\Framework\Phrase|string { $details = []; + if ($this->isRestConfiguration() && is_array($address)) { + if (!empty($address['city'])) { + $details[] = (string) $address['city']; + } - if (!empty($address->City)) { - $details[] = (string) $address->City; - } + if (!empty($address['stateOrProvinceCode'])) { + $details[] = (string) $address['stateOrProvinceCode']; + } - if (!empty($address->StateOrProvinceCode)) { - $details[] = (string) $address->StateOrProvinceCode; - } + if (!empty($address['countryCode'])) { + $details[] = (string) $address['countryCode']; + } + } else { + if (!empty($address->City)) { + $details[] = (string) $address->City; + } - if (!empty($address->CountryCode)) { - $details[] = (string) $address->CountryCode; + if (!empty($address->StateOrProvinceCode)) { + $details[] = (string) $address->StateOrProvinceCode; + } + + if (!empty($address->CountryCode)) { + $details[] = (string) $address->CountryCode; + } } return implode(', ', $details); @@ -1764,13 +2597,62 @@ private function getDeliveryAddress(\stdClass $address) * @param array $events * @return array */ - private function processTrackDetailsEvents(array $events) + private function processTrackDetailsEvents(array $events): array + { + if ($this->isRestConfiguration()) { + return $this->processTrackDetailsEventsRest($events); + } else { + return $this->processTrackDetailsEventsSoap($events); + } + } + + /** + * Parse tracking details events from response + * Return list of items in such format: + * ['activity', 'deliverydate', 'deliverytime', 'deliverylocation'] + * + * @param array $events + * @return array + */ + private function processTrackDetailsEventsRest(array $events): array + { + $result = []; + foreach ($events as $event) { + $item = [ + 'activity' => (string) $event['eventDescription'], + 'deliverydate' => null, + 'deliverytime' => null, + 'deliverylocation' => null + ]; + + $datetime = $this->parseDate(!empty($event['date']) ? $event['date'] : null); + if ($datetime) { + $item['deliverydate'] = gmdate('Y-m-d', $datetime->getTimestamp()); + $item['deliverytime'] = gmdate('H:i:s', $datetime->getTimestamp()); + } + + if (!empty($event['scanLocation'])) { + $item['deliverylocation'] = $this->getDeliveryAddress($event['scanLocation']); + } + $result[] = $item; + } + return $result; + } + + /** + * Parse tracking details events from response + * Return list of items in such format: + * ['activity', 'deliverydate', 'deliverytime', 'deliverylocation'] + * + * @param array $events + * @return array + */ + private function processTrackDetailsEventsSoap(array $events): array { $result = []; - /** @var \stdClass $event */ foreach ($events as $event) { $item = [ - 'activity' => (string) $event->EventDescription, + 'activity' => (string)$event->EventDescription, 'deliverydate' => null, 'deliverytime' => null, 'deliverylocation' => null @@ -1788,7 +2670,6 @@ private function processTrackDetailsEvents(array $events) $result[] = $item; } - return $result; } @@ -1848,6 +2729,20 @@ private function getPaymentType(DataObject $request): string : 'SENDER'; } + /** + * Check if Admin configuration is REST or SOAP + * + * @return bool + */ + private function isRestConfiguration(): bool + { + $serviceType = $this->getConfigData('web_service_type'); + if ($serviceType && $serviceType == self::WEB_SERVICE_REST) { + return true; + } + return false; + } + /** * Creates packages for rate request. * diff --git a/vendor/magento/module-fedex/Model/Source/PickupType.php b/vendor/magento/module-fedex/Model/Source/PickupType.php new file mode 100644 index 0000000000000..482534483c68b --- /dev/null +++ b/vendor/magento/module-fedex/Model/Source/PickupType.php @@ -0,0 +1,36 @@ + + + + Magento\Fedex\Model\Source\WebServiceType + Magento\Config\Model\Config\Backend\Encrypted @@ -25,14 +29,58 @@ Magento\Config\Model\Config\Backend\Encrypted + + SOAP + Magento\Config\Model\Config\Backend\Encrypted + + SOAP + Magento\Config\Model\Config\Backend\Encrypted + + SOAP + + + + + Magento\Config\Model\Config\Backend\Encrypted + + REST + + + + + Magento\Config\Model\Config\Backend\Encrypted + + REST + + + + + Magento\Config\Model\Config\Source\Yesno + + + + Magento\Config\Model\Config\Backend\Encrypted + + 1 + + + + + Magento\Config\Model\Config\Backend\Encrypted + + 1 + @@ -42,6 +90,7 @@ Magento\Fedex\Model\Config\Backend\FedexUrl + SOAP 0 @@ -49,6 +98,23 @@ Magento\Fedex\Model\Config\Backend\FedexUrl + SOAP + 1 + + + + + Magento\Fedex\Model\Config\Backend\FedexUrl + + REST + 0 + + + + + Magento\Fedex\Model\Config\Backend\FedexUrl + + REST 1 @@ -63,6 +129,16 @@ Magento\Fedex\Model\Source\Dropoff + + SOAP + + + + + Magento\Fedex\Model\Source\PickupType + + REST + diff --git a/vendor/magento/module-fedex/etc/config.xml b/vendor/magento/module-fedex/etc/config.xml index 1d9defb62efe4..32288c8432015 100644 --- a/vendor/magento/module-fedex/etc/config.xml +++ b/vendor/magento/module-fedex/etc/config.xml @@ -9,19 +9,27 @@ + SOAP + + + + 0 + + 0 0 0 EUROPE_FIRST_INTERNATIONAL_PRIORITY,FEDEX_1_DAY_FREIGHT,FEDEX_2_DAY_FREIGHT,FEDEX_2_DAY,FEDEX_2_DAY_AM,FEDEX_3_DAY_FREIGHT,FEDEX_EXPRESS_SAVER,FEDEX_GROUND,FIRST_OVERNIGHT,GROUND_HOME_DELIVERY,INTERNATIONAL_ECONOMY,INTERNATIONAL_ECONOMY_FREIGHT,INTERNATIONAL_FIRST,INTERNATIONAL_GROUND,INTERNATIONAL_PRIORITY,INTERNATIONAL_PRIORITY_FREIGHT,PRIORITY_OVERNIGHT,SMART_POST,STANDARD_OVERNIGHT,FEDEX_FREIGHT,FEDEX_NATIONAL_FREIGHT REGULAR_PICKUP + DROPOFF_AT_FEDEX_LOCATION FEDEX_GROUND 0 Magento\Fedex\Model\Carrier diff --git a/vendor/magento/module-fedex/etc/di.xml b/vendor/magento/module-fedex/etc/di.xml index c542b1f04d1eb..57fec07b7f87c 100644 --- a/vendor/magento/module-fedex/etc/di.xml +++ b/vendor/magento/module-fedex/etc/di.xml @@ -13,6 +13,8 @@ 1 1 1 + 1 + 1 1 1 1 diff --git a/vendor/magento/module-fedex/i18n/en_US.csv b/vendor/magento/module-fedex/i18n/en_US.csv index 2911ebe793f23..b91043d7cc666 100644 --- a/vendor/magento/module-fedex/i18n/en_US.csv +++ b/vendor/magento/module-fedex/i18n/en_US.csv @@ -79,3 +79,17 @@ Debug,Debug "Sort Order","Sort Order" "Can't convert a shipping cost from ""%1-%2"" for FedEx carrier.","Can't convert a shipping cost from ""%1-%2"" for FedEx carrier." "Fedex API endpoint URL\'s must use fedex.com","Fedex API endpoint URL\'s must use fedex.com" +"Authentication keys are missing.","Authentication keys are missing." +"Authorization Error. No Access Token found with given credentials.","Authorization Error. No Access Token found with given credentials." +"Contact Fedex to Schedule","Contact Fedex to Schedule" +"DropOff at Fedex Location","DropOff at Fedex Location" +"Scheduled Pickup","Scheduled Pickup" +"On Call","On Call" +"Package Return Program","Package Return Program" +"Regular Stop","Regular Stop" +"Tag","Tag" +"Api Key","Api Key" +"Secret Key","Secret Key" +"Enable Tracking API credentials","Enable Tracking API credentials" +"Tracking API Key","Tracking API Key" +"Tracking API Secret Key","Tracking API Secret Key"