* @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Carrier extends AbstractCarrierOnline implements \Magento\Shipping\Model\Carrier\CarrierInterface { /** * Code of the carrier * * @var string */ const CODE = 'fedex'; /** * Purpose of rate request * * @var string */ const RATE_REQUEST_GENERAL = 'general'; /** * Purpose of rate request * * @var string */ const RATE_REQUEST_SMARTPOST = 'SMART_POST'; /** * Code of the carrier * * @var string */ protected $_code = self::CODE; /** * Types of rates, order is important * * @var array */ protected $_ratesOrder = [ 'RATED_ACCOUNT_PACKAGE', 'PAYOR_ACCOUNT_PACKAGE', 'RATED_ACCOUNT_SHIPMENT', 'PAYOR_ACCOUNT_SHIPMENT', 'RATED_LIST_PACKAGE', 'PAYOR_LIST_PACKAGE', 'RATED_LIST_SHIPMENT', 'PAYOR_LIST_SHIPMENT', ]; /** * Rate request data * * @var RateRequest|null */ protected $_request = null; /** * Rate result data * * @var Result|null */ protected $_result = null; /** * Path to wsdl file of rate service * * @var string */ protected $_rateServiceWsdl; /** * Path to wsdl file of ship service * * @var string */ protected $_shipServiceWsdl = null; /** * Path to wsdl file of track service * * @var string */ protected $_trackServiceWsdl = null; /** * Container types that could be customized for FedEx carrier * * @var string[] */ protected $_customizableContainerTypes = ['YOUR_PACKAGING']; /** * @var \Magento\Store\Model\StoreManagerInterface */ protected $_storeManager; /** * @var \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory */ protected $_productCollectionFactory; /** * @inheritdoc */ protected $_debugReplacePrivateDataKeys = [ 'Key', 'Password', 'MeterNumber' ]; /** * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory * @param \Psr\Log\LoggerInterface $logger * @param Security $xmlSecurity * @param \Magento\Shipping\Model\Simplexml\ElementFactory $xmlElFactory * @param \Magento\Shipping\Model\Rate\ResultFactory $rateFactory * @param \Magento\Quote\Model\Quote\Address\RateResult\MethodFactory $rateMethodFactory * @param \Magento\Shipping\Model\Tracking\ResultFactory $trackFactory * @param \Magento\Shipping\Model\Tracking\Result\ErrorFactory $trackErrorFactory * @param \Magento\Shipping\Model\Tracking\Result\StatusFactory $trackStatusFactory * @param \Magento\Directory\Model\RegionFactory $regionFactory * @param \Magento\Directory\Model\CountryFactory $countryFactory * @param \Magento\Directory\Model\CurrencyFactory $currencyFactory * @param \Magento\Directory\Helper\Data $directoryData * @param \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\Module\Dir\Reader $configReader * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory * @param array $data * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory, \Psr\Log\LoggerInterface $logger, Security $xmlSecurity, \Magento\Shipping\Model\Simplexml\ElementFactory $xmlElFactory, \Magento\Shipping\Model\Rate\ResultFactory $rateFactory, \Magento\Quote\Model\Quote\Address\RateResult\MethodFactory $rateMethodFactory, \Magento\Shipping\Model\Tracking\ResultFactory $trackFactory, \Magento\Shipping\Model\Tracking\Result\ErrorFactory $trackErrorFactory, \Magento\Shipping\Model\Tracking\Result\StatusFactory $trackStatusFactory, \Magento\Directory\Model\RegionFactory $regionFactory, \Magento\Directory\Model\CountryFactory $countryFactory, \Magento\Directory\Model\CurrencyFactory $currencyFactory, \Magento\Directory\Helper\Data $directoryData, \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Framework\Module\Dir\Reader $configReader, \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory, array $data = [] ) { $this->_storeManager = $storeManager; $this->_productCollectionFactory = $productCollectionFactory; parent::__construct( $scopeConfig, $rateErrorFactory, $logger, $xmlSecurity, $xmlElFactory, $rateFactory, $rateMethodFactory, $trackFactory, $trackErrorFactory, $trackStatusFactory, $regionFactory, $countryFactory, $currencyFactory, $directoryData, $stockRegistry, $data ); $wsdlBasePath = $configReader->getModuleDir(Dir::MODULE_ETC_DIR, 'Magento_Fedex') . '/wsdl/'; $this->_shipServiceWsdl = $wsdlBasePath . 'ShipService_v10.wsdl'; $this->_rateServiceWsdl = $wsdlBasePath . 'RateService_v10.wsdl'; $this->_trackServiceWsdl = $wsdlBasePath . 'TrackService_v5.wsdl'; } /** * Create soap client with selected wsdl * * @param string $wsdl * @param bool|int $trace * @return \SoapClient */ protected function _createSoapClient($wsdl, $trace = false) { $client = new \SoapClient($wsdl, ['trace' => $trace]); $client->__setLocation( $this->getConfigFlag( 'sandbox_mode' ) ? $this->getConfigData('sandbox_webservices_url') : $this->getConfigData('production_webservices_url') ); return $client; } /** * Create rate soap client * * @return \SoapClient */ protected function _createRateSoapClient() { return $this->_createSoapClient($this->_rateServiceWsdl); } /** * Create ship soap client * * @return \SoapClient */ protected function _createShipSoapClient() { return $this->_createSoapClient($this->_shipServiceWsdl, 1); } /** * Create track soap client * * @return \SoapClient */ protected function _createTrackSoapClient() { return $this->_createSoapClient($this->_trackServiceWsdl, 1); } /** * Collect and get rates * * @param RateRequest $request * @return Result|bool|null */ public function collectRates(RateRequest $request) { if (!$this->canCollectRates()) { return $this->getErrorMessage(); } $this->setRequest($request); $this->_getQuotes(); $this->_updateFreeMethodQuote($request); return $this->getResult(); } /** * Prepare and set request to this instance * * @param RateRequest $request * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ public function setRequest(RateRequest $request) { $this->_request = $request; $r = new \Magento\Framework\DataObject(); if ($request->getLimitMethod()) { $r->setService($request->getLimitMethod()); } if ($request->getFedexAccount()) { $account = $request->getFedexAccount(); } else { $account = $this->getConfigData('account'); } $r->setAccount($account); if ($request->getFedexDropoff()) { $dropoff = $request->getFedexDropoff(); } else { $dropoff = $this->getConfigData('dropoff'); } $r->setDropoffType($dropoff); if ($request->getFedexPackaging()) { $packaging = $request->getFedexPackaging(); } else { $packaging = $this->getConfigData('packaging'); } $r->setPackaging($packaging); if ($request->getOrigCountry()) { $origCountry = $request->getOrigCountry(); } else { $origCountry = $this->_scopeConfig->getValue( \Magento\Sales\Model\Order\Shipment::XML_PATH_STORE_COUNTRY_ID, \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $request->getStoreId() ); } $r->setOrigCountry($this->_countryFactory->create()->load($origCountry)->getData('iso2_code')); if ($request->getOrigPostcode()) { $r->setOrigPostal($request->getOrigPostcode()); } else { $r->setOrigPostal( $this->_scopeConfig->getValue( \Magento\Sales\Model\Order\Shipment::XML_PATH_STORE_ZIP, \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $request->getStoreId() ) ); } if ($request->getDestCountryId()) { $destCountry = $request->getDestCountryId(); } else { $destCountry = self::USA_COUNTRY_ID; } $r->setDestCountry($this->_countryFactory->create()->load($destCountry)->getData('iso2_code')); if ($request->getDestPostcode()) { $r->setDestPostal($request->getDestPostcode()); } else { } $weight = $this->getTotalNumOfBoxes($request->getPackageWeight()); $r->setWeight($weight); if ($request->getFreeMethodWeight() != $request->getPackageWeight()) { $r->setFreeMethodWeight($request->getFreeMethodWeight()); } $r->setValue($request->getPackagePhysicalValue()); $r->setValueWithDiscount($request->getPackageValueWithDiscount()); $r->setMeterNumber($this->getConfigData('meter_number')); $r->setKey($this->getConfigData('key')); $r->setPassword($this->getConfigData('password')); $r->setIsReturn($request->getIsReturn()); $r->setBaseSubtotalInclTax($request->getBaseSubtotalInclTax()); $this->setRawRequest($r); return $this; } /** * Get result of request * * @return Result|null */ public function getResult() { return $this->_result; } /** * Get version of rates request * * @return array */ public function getVersionInfo() { return ['ServiceId' => 'crs', 'Major' => '10', 'Intermediate' => '0', 'Minor' => '0']; } /** * Forming request for rate estimation depending to the purpose * * @param string $purpose * @return array */ protected function _formRateRequest($purpose) { $r = $this->_rawRequest; $ratesRequest = [ 'WebAuthenticationDetail' => [ 'UserCredential' => ['Key' => $r->getKey(), 'Password' => $r->getPassword()], ], 'ClientDetail' => ['AccountNumber' => $r->getAccount(), 'MeterNumber' => $r->getMeterNumber()], 'Version' => $this->getVersionInfo(), 'RequestedShipment' => [ 'DropoffType' => $r->getDropoffType(), 'ShipTimestamp' => date('c'), 'PackagingType' => $r->getPackaging(), 'TotalInsuredValue' => ['Amount' => $r->getValue(), 'Currency' => $this->getCurrencyCode()], 'Shipper' => [ 'Address' => ['PostalCode' => $r->getOrigPostal(), 'CountryCode' => $r->getOrigCountry()], ], 'Recipient' => [ 'Address' => [ 'PostalCode' => $r->getDestPostal(), 'CountryCode' => $r->getDestCountry(), 'Residential' => (bool)$this->getConfigData('residence_delivery'), ], ], 'ShippingChargesPayment' => [ 'PaymentType' => 'SENDER', 'Payor' => ['AccountNumber' => $r->getAccount(), 'CountryCode' => $r->getOrigCountry()], ], 'CustomsClearanceDetail' => [ 'CustomsValue' => ['Amount' => $r->getValue(), 'Currency' => $this->getCurrencyCode()], ], 'RateRequestTypes' => 'LIST', 'PackageCount' => '1', 'PackageDetail' => 'INDIVIDUAL_PACKAGES', 'RequestedPackageLineItems' => [ '0' => [ 'Weight' => [ 'Value' => (double)$r->getWeight(), 'Units' => $this->getConfigData('unit_of_measure'), ], 'GroupPackageCount' => 1, ], ], ], ]; if ($purpose == self::RATE_REQUEST_GENERAL) { $ratesRequest['RequestedShipment']['RequestedPackageLineItems'][0]['InsuredValue'] = [ 'Amount' => $r->getValue(), 'Currency' => $this->getCurrencyCode(), ]; } else { if ($purpose == self::RATE_REQUEST_SMARTPOST) { $ratesRequest['RequestedShipment']['ServiceType'] = self::RATE_REQUEST_SMARTPOST; $ratesRequest['RequestedShipment']['SmartPostDetail'] = [ 'Indicia' => (double)$r->getWeight() >= 1 ? 'PARCEL_SELECT' : 'PRESORTED_STANDARD', 'HubId' => $this->getConfigData('smartpost_hubid'), ]; } } return $ratesRequest; } /** * Makes remote request to the carrier and returns a response * * @param string $purpose * @return mixed */ protected function _doRatesRequest($purpose) { $ratesRequest = $this->_formRateRequest($purpose); $requestString = serialize($ratesRequest); $response = $this->_getCachedQuotes($requestString); $debugData = ['request' => $this->filterDebugData($ratesRequest)]; if ($response === null) { try { $client = $this->_createRateSoapClient(); $response = $client->getRates($ratesRequest); $this->_setCachedQuotes($requestString, serialize($response)); $debugData['result'] = $response; } catch (\Exception $e) { $debugData['result'] = ['error' => $e->getMessage(), 'code' => $e->getCode()]; $this->_logger->critical($e); } } else { $response = unserialize($response); $debugData['result'] = $response; } $this->_debug($debugData); return $response; } /** * Do remote request for and handle errors * * @return Result */ protected function _getQuotes() { $this->_result = $this->_rateFactory->create(); // make separate request for Smart Post method $allowedMethods = explode(',', $this->getConfigData('allowed_methods')); if (in_array(self::RATE_REQUEST_SMARTPOST, $allowedMethods)) { $response = $this->_doRatesRequest(self::RATE_REQUEST_SMARTPOST); $preparedSmartpost = $this->_prepareRateResponse($response); if (!$preparedSmartpost->getError()) { $this->_result->append($preparedSmartpost); } } // make general request for all methods $response = $this->_doRatesRequest(self::RATE_REQUEST_GENERAL); $preparedGeneral = $this->_prepareRateResponse($response); if (!$preparedGeneral->getError() || $this->_result->getError() && $preparedGeneral->getError() || empty($this->_result->getAllRates()) ) { $this->_result->append($preparedGeneral); } return $this->_result; } /** * Prepare shipping rate result based on response * * @param mixed $response * @return Result * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function _prepareRateResponse($response) { $costArr = []; $priceArr = []; $errorTitle = 'For some reason we can\'t retrieve tracking info right now.'; if (is_object($response)) { if ($response->HighestSeverity == 'FAILURE' || $response->HighestSeverity == 'ERROR') { if (is_array($response->Notifications)) { $notification = array_pop($response->Notifications); $errorTitle = (string)$notification->Message; } else { $errorTitle = (string)$response->Notifications->Message; } } elseif (isset($response->RateReplyDetails)) { $allowedMethods = explode(",", $this->getConfigData('allowed_methods')); if (is_array($response->RateReplyDetails)) { foreach ($response->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); } else { $rate = $response->RateReplyDetails; $serviceName = (string)$rate->ServiceType; if (in_array($serviceName, $allowedMethods)) { $amount = $this->_getRateAmountOriginBased($rate); $costArr[$serviceName] = $amount; $priceArr[$serviceName] = $this->getMethodPrice($amount, $serviceName); } } } } $result = $this->_rateFactory->create(); if (empty($priceArr)) { $error = $this->_rateErrorFactory->create(); $error->setCarrier($this->_code); $error->setCarrierTitle($this->getConfigData('title')); $error->setErrorMessage($errorTitle); $error->setErrorMessage($this->getConfigData('specificerrmsg')); $result->append($error); } else { foreach ($priceArr as $method => $price) { $rate = $this->_rateMethodFactory->create(); $rate->setCarrier($this->_code); $rate->setCarrierTitle($this->getConfigData('title')); $rate->setMethod($method); $rate->setMethodTitle($this->getCode('method', $method)); $rate->setCost($costArr[$method]); $rate->setPrice($price); $result->append($rate); } } return $result; } /** * Get origin based amount form response of rate estimation * * @param \stdClass $rate * @return null|float */ protected function _getRateAmountOriginBased($rate) { $amount = null; $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; $rateType = (string)$ratedShipmentDetail->ShipmentRateDetail->RateType; $rateTypeAmounts[$rateType] = $netAmount; } foreach ($this->_ratesOrder as $rateType) { if (!empty($rateTypeAmounts[$rateType])) { $amount = $rateTypeAmounts[$rateType]; break; } } if (is_null($amount)) { $amount = (string)$rate->RatedShipmentDetails[0]->ShipmentRateDetail->TotalNetCharge->Amount; } } return $amount; } /** * Set free method request * * @param string $freeMethod * @return void */ protected function _setFreeMethodRequest($freeMethod) { $r = $this->_rawRequest; $weight = $this->getTotalNumOfBoxes($r->getFreeMethodWeight()); $r->setWeight($weight); $r->setService($freeMethod); } /** * Get xml quotes * * @return Result */ protected function _getXmlQuotes() { $r = $this->_rawRequest; $xml = $this->_xmlElFactory->create( ['data' => ''] ); $xml->addAttribute('xmlns:api', 'http://www.fedex.com/fsmapi'); $xml->addAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); $xml->addAttribute('xsi:noNamespaceSchemaLocation', 'FDXRateAvailableServicesRequest.xsd'); $requestHeader = $xml->addChild('RequestHeader'); $requestHeader->addChild('AccountNumber', $r->getAccount()); $requestHeader->addChild('MeterNumber', '0'); $xml->addChild('ShipDate', date('Y-m-d')); $xml->addChild('DropoffType', $r->getDropoffType()); if ($r->hasService()) { $xml->addChild('Service', $r->getService()); } $xml->addChild('Packaging', $r->getPackaging()); $xml->addChild('WeightUnits', 'LBS'); $xml->addChild('Weight', $r->getWeight()); $originAddress = $xml->addChild('OriginAddress'); $originAddress->addChild('PostalCode', $r->getOrigPostal()); $originAddress->addChild('CountryCode', $r->getOrigCountry()); $destinationAddress = $xml->addChild('DestinationAddress'); $destinationAddress->addChild('PostalCode', $r->getDestPostal()); $destinationAddress->addChild('CountryCode', $r->getDestCountry()); $payment = $xml->addChild('Payment'); $payment->addChild('PayorType', 'SENDER'); $declaredValue = $xml->addChild('DeclaredValue'); $declaredValue->addChild('Value', $r->getValue()); $declaredValue->addChild('CurrencyCode', $this->getCurrencyCode()); if ($this->getConfigData('residence_delivery')) { $specialServices = $xml->addChild('SpecialServices'); $specialServices->addChild('ResidentialDelivery', 'true'); } $xml->addChild('PackageCount', '1'); $request = $xml->asXML(); $responseBody = $this->_getCachedQuotes($request); if ($responseBody === null) { $debugData = ['request' => $request]; try { $url = $this->getConfigData('gateway_url'); if (!$url) { $url = $this->_defaultGatewayUrl; } $ch = curl_init(); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); curl_setopt($ch, CURLOPT_POSTFIELDS, $request); $responseBody = curl_exec($ch); curl_close($ch); $debugData['result'] = $responseBody; $this->_setCachedQuotes($request, $responseBody); } catch (\Exception $e) { $debugData['result'] = ['error' => $e->getMessage(), 'code' => $e->getCode()]; $responseBody = ''; } $this->_debug($debugData); } return $this->_parseXmlResponse($responseBody); } /** * Prepare shipping rate result based on response * * @param mixed $response * @return Result * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function _parseXmlResponse($response) { $costArr = []; $priceArr = []; if (strlen(trim($response)) > 0) { $xml = $this->parseXml($response, 'Magento\Shipping\Model\Simplexml\Element'); if (is_object($xml)) { if (is_object($xml->Error) && is_object($xml->Error->Message)) { $errorTitle = (string)$xml->Error->Message; } elseif (is_object($xml->SoftError) && is_object($xml->SoftError->Message)) { $errorTitle = (string)$xml->SoftError->Message; } else { $errorTitle = 'Sorry, something went wrong. Please try again or contact us and we\'ll try to help.'; } $allowedMethods = explode(",", $this->getConfigData('allowed_methods')); foreach ($xml->Entry as $entry) { if (in_array((string)$entry->Service, $allowedMethods)) { $costArr[(string)$entry->Service] = (string)$entry ->EstimatedCharges ->DiscountedCharges ->NetCharge; $priceArr[(string)$entry->Service] = $this->getMethodPrice( (string)$entry->EstimatedCharges->DiscountedCharges->NetCharge, (string)$entry->Service ); } } asort($priceArr); } else { $errorTitle = 'Response is in the wrong format.'; } } else { $errorTitle = 'For some reason we can\'t retrieve tracking info right now.'; } $result = $this->_rateFactory->create(); if (empty($priceArr)) { $error = $this->_rateErrorFactory->create(); $error->setCarrier('fedex'); $error->setCarrierTitle($this->getConfigData('title')); $error->setErrorMessage($this->getConfigData('specificerrmsg')); $result->append($error); } else { foreach ($priceArr as $method => $price) { $rate = $this->_rateMethodFactory->create(); $rate->setCarrier('fedex'); $rate->setCarrierTitle($this->getConfigData('title')); $rate->setMethod($method); $rate->setMethodTitle($this->getCode('method', $method)); $rate->setCost($costArr[$method]); $rate->setPrice($price); $result->append($rate); } } return $result; } /** * Get configuration data of carrier * * @param string $type * @param string $code * @return array|false * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function getCode($type, $code = '') { $codes = [ 'method' => [ 'EUROPE_FIRST_INTERNATIONAL_PRIORITY' => __('Europe First Priority'), 'FEDEX_1_DAY_FREIGHT' => __('1 Day Freight'), 'FEDEX_2_DAY_FREIGHT' => __('2 Day Freight'), 'FEDEX_2_DAY' => __('2 Day'), 'FEDEX_2_DAY_AM' => __('2 Day AM'), 'FEDEX_3_DAY_FREIGHT' => __('3 Day Freight'), 'FEDEX_EXPRESS_SAVER' => __('Express Saver'), 'FEDEX_GROUND' => __('Ground'), 'FIRST_OVERNIGHT' => __('First Overnight'), 'GROUND_HOME_DELIVERY' => __('Home Delivery'), 'INTERNATIONAL_ECONOMY' => __('International Economy'), 'INTERNATIONAL_ECONOMY_FREIGHT' => __('Intl Economy Freight'), 'INTERNATIONAL_FIRST' => __('International First'), 'INTERNATIONAL_GROUND' => __('International Ground'), 'INTERNATIONAL_PRIORITY' => __('International Priority'), 'INTERNATIONAL_PRIORITY_FREIGHT' => __('Intl Priority Freight'), 'PRIORITY_OVERNIGHT' => __('Priority Overnight'), 'SMART_POST' => __('Smart Post'), 'STANDARD_OVERNIGHT' => __('Standard Overnight'), 'FEDEX_FREIGHT' => __('Freight'), 'FEDEX_NATIONAL_FREIGHT' => __('National Freight'), ], 'dropoff' => [ 'REGULAR_PICKUP' => __('Regular Pickup'), 'REQUEST_COURIER' => __('Request Courier'), 'DROP_BOX' => __('Drop Box'), 'BUSINESS_SERVICE_CENTER' => __('Business Service Center'), 'STATION' => __('Station'), ], 'packaging' => [ 'FEDEX_ENVELOPE' => __('FedEx Envelope'), 'FEDEX_PAK' => __('FedEx Pak'), 'FEDEX_BOX' => __('FedEx Box'), 'FEDEX_TUBE' => __('FedEx Tube'), 'FEDEX_10KG_BOX' => __('FedEx 10kg Box'), 'FEDEX_25KG_BOX' => __('FedEx 25kg Box'), 'YOUR_PACKAGING' => __('Your Packaging'), ], 'containers_filter' => [ [ 'containers' => ['FEDEX_ENVELOPE', 'FEDEX_PAK'], 'filters' => [ 'within_us' => [ 'method' => [ 'FEDEX_EXPRESS_SAVER', 'FEDEX_2_DAY', 'FEDEX_2_DAY_AM', 'STANDARD_OVERNIGHT', 'PRIORITY_OVERNIGHT', 'FIRST_OVERNIGHT', ], ], 'from_us' => [ 'method' => ['INTERNATIONAL_FIRST', 'INTERNATIONAL_ECONOMY', 'INTERNATIONAL_PRIORITY'], ], ], ], [ 'containers' => ['FEDEX_BOX', 'FEDEX_TUBE'], 'filters' => [ 'within_us' => [ 'method' => [ 'FEDEX_2_DAY', 'FEDEX_2_DAY_AM', 'STANDARD_OVERNIGHT', 'PRIORITY_OVERNIGHT', 'FIRST_OVERNIGHT', 'FEDEX_FREIGHT', 'FEDEX_1_DAY_FREIGHT', 'FEDEX_2_DAY_FREIGHT', 'FEDEX_3_DAY_FREIGHT', 'FEDEX_NATIONAL_FREIGHT', ], ], 'from_us' => [ 'method' => ['INTERNATIONAL_FIRST', 'INTERNATIONAL_ECONOMY', 'INTERNATIONAL_PRIORITY'], ], ] ], [ 'containers' => ['FEDEX_10KG_BOX', 'FEDEX_25KG_BOX'], 'filters' => [ 'within_us' => [], 'from_us' => ['method' => ['INTERNATIONAL_PRIORITY']], ] ], [ 'containers' => ['YOUR_PACKAGING'], 'filters' => [ 'within_us' => [ 'method' => [ 'FEDEX_GROUND', 'GROUND_HOME_DELIVERY', 'SMART_POST', 'FEDEX_EXPRESS_SAVER', 'FEDEX_2_DAY', 'FEDEX_2_DAY_AM', 'STANDARD_OVERNIGHT', 'PRIORITY_OVERNIGHT', 'FIRST_OVERNIGHT', 'FEDEX_FREIGHT', 'FEDEX_1_DAY_FREIGHT', 'FEDEX_2_DAY_FREIGHT', 'FEDEX_3_DAY_FREIGHT', 'FEDEX_NATIONAL_FREIGHT', ], ], 'from_us' => [ 'method' => [ 'INTERNATIONAL_FIRST', 'INTERNATIONAL_ECONOMY', 'INTERNATIONAL_PRIORITY', 'INTERNATIONAL_GROUND', 'FEDEX_FREIGHT', 'FEDEX_1_DAY_FREIGHT', 'FEDEX_2_DAY_FREIGHT', 'FEDEX_3_DAY_FREIGHT', 'FEDEX_NATIONAL_FREIGHT', 'INTERNATIONAL_ECONOMY_FREIGHT', 'INTERNATIONAL_PRIORITY_FREIGHT', ], ], ] ], ], 'delivery_confirmation_types' => [ 'NO_SIGNATURE_REQUIRED' => __('Not Required'), 'ADULT' => __('Adult'), 'DIRECT' => __('Direct'), 'INDIRECT' => __('Indirect'), ], 'unit_of_measure' => [ 'LB' => __('Pounds'), 'KG' => __('Kilograms'), ], ]; if (!isset($codes[$type])) { return false; } elseif ('' === $code) { return $codes[$type]; } if (!isset($codes[$type][$code])) { return false; } else { return $codes[$type][$code]; } } /** * Return FeDex currency ISO code by Magento Base Currency Code * * @return string 3-digit currency code */ public function getCurrencyCode() { $codes = [ 'DOP' => 'RDD', 'XCD' => 'ECD', 'ARS' => 'ARN', 'SGD' => 'SID', 'KRW' => 'WON', 'JMD' => 'JAD', 'CHF' => 'SFR', 'JPY' => 'JYE', 'KWD' => 'KUD', 'GBP' => 'UKL', 'AED' => 'DHS', 'MXN' => 'NMP', 'UYU' => 'UYP', 'CLP' => 'CHP', 'TWD' => 'NTD', ]; $currencyCode = $this->_storeManager->getStore()->getBaseCurrencyCode(); return isset($codes[$currencyCode]) ? $codes[$currencyCode] : $currencyCode; } /** * Get tracking * * @param string|string[] $trackings * @return Result|null */ public function getTracking($trackings) { $this->setTrackingReqeust(); if (!is_array($trackings)) { $trackings = [$trackings]; } foreach ($trackings as $tracking) { $this->_getXMLTracking($tracking); } return $this->_result; } /** * Set tracking request * * @return void */ protected function setTrackingReqeust() { $r = new \Magento\Framework\DataObject(); $account = $this->getConfigData('account'); $r->setAccount($account); $this->_rawTrackingRequest = $r; } /** * Send request for tracking * * @param string[] $tracking * @return void */ protected function _getXMLTracking($tracking) { $trackRequest = [ 'WebAuthenticationDetail' => [ 'UserCredential' => [ 'Key' => $this->getConfigData('key'), 'Password' => $this->getConfigData('password'), ], ], 'ClientDetail' => [ 'AccountNumber' => $this->getConfigData('account'), 'MeterNumber' => $this->getConfigData('meter_number'), ], 'Version' => ['ServiceId' => 'trck', 'Major' => '5', 'Intermediate' => '0', 'Minor' => '0'], 'PackageIdentifier' => ['Type' => 'TRACKING_NUMBER_OR_DOORTAG', 'Value' => $tracking], 'IncludeDetailedScans' => 1, ]; $requestString = serialize($trackRequest); $response = $this->_getCachedQuotes($requestString); $debugData = ['request' => $trackRequest]; if ($response === null) { try { $client = $this->_createTrackSoapClient(); $response = $client->track($trackRequest); $this->_setCachedQuotes($requestString, serialize($response)); $debugData['result'] = $response; } catch (\Exception $e) { $debugData['result'] = ['error' => $e->getMessage(), 'code' => $e->getCode()]; $this->_logger->critical($e); } } else { $response = unserialize($response); $debugData['result'] = $response; } $this->_debug($debugData); $this->_parseTrackingResponse($tracking, $response); } /** * Parse tracking response * * @param string[] $trackingValue * @param \stdClass $response * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ protected function _parseTrackingResponse($trackingValue, $response) { if (is_object($response)) { if ($response->HighestSeverity == 'FAILURE' || $response->HighestSeverity == 'ERROR') { $errorTitle = (string)$response->Notifications->Message; } elseif (isset($response->TrackDetails)) { $trackInfo = $response->TrackDetails; $resultArray['status'] = (string)$trackInfo->StatusDescription; $resultArray['service'] = (string)$trackInfo->ServiceInfo; $timestamp = isset( $trackInfo->EstimatedDeliveryTimestamp ) ? $trackInfo->EstimatedDeliveryTimestamp : $trackInfo->ActualDeliveryTimestamp; $timestamp = strtotime((string)$timestamp); if ($timestamp) { $resultArray['deliverydate'] = date('Y-m-d', $timestamp); $resultArray['deliverytime'] = date('H:i:s', $timestamp); } $deliveryLocation = isset( $trackInfo->EstimatedDeliveryAddress ) ? $trackInfo->EstimatedDeliveryAddress : $trackInfo->ActualDeliveryAddress; $deliveryLocationArray = []; if (isset($deliveryLocation->City)) { $deliveryLocationArray[] = (string)$deliveryLocation->City; } if (isset($deliveryLocation->StateOrProvinceCode)) { $deliveryLocationArray[] = (string)$deliveryLocation->StateOrProvinceCode; } if (isset($deliveryLocation->CountryCode)) { $deliveryLocationArray[] = (string)$deliveryLocation->CountryCode; } if ($deliveryLocationArray) { $resultArray['deliverylocation'] = implode(', ', $deliveryLocationArray); } $resultArray['signedby'] = (string)$trackInfo->DeliverySignatureName; $resultArray['shippeddate'] = date('Y-m-d', (int)$trackInfo->ShipTimestamp); if (isset($trackInfo->PackageWeight) && isset($trackInfo->Units)) { $weight = (string)$trackInfo->PackageWeight; $unit = (string)$trackInfo->Units; $resultArray['weight'] = "{$weight} {$unit}"; } $packageProgress = []; if (isset($trackInfo->Events)) { $events = $trackInfo->Events; if (isset($events->Address)) { $events = [$events]; } foreach ($events as $event) { $tempArray = []; $tempArray['activity'] = (string)$event->EventDescription; $timestamp = strtotime((string)$event->Timestamp); if ($timestamp) { $tempArray['deliverydate'] = date('Y-m-d', $timestamp); $tempArray['deliverytime'] = date('H:i:s', $timestamp); } if (isset($event->Address)) { $addressArray = []; $address = $event->Address; if (isset($address->City)) { $addressArray[] = (string)$address->City; } if (isset($address->StateOrProvinceCode)) { $addressArray[] = (string)$address->StateOrProvinceCode; } if (isset($address->CountryCode)) { $addressArray[] = (string)$address->CountryCode; } if ($addressArray) { $tempArray['deliverylocation'] = implode(', ', $addressArray); } } $packageProgress[] = $tempArray; } } $resultArray['progressdetail'] = $packageProgress; } } if (!$this->_result) { $this->_result = $this->_trackFactory->create(); } if (isset($resultArray)) { $tracking = $this->_trackStatusFactory->create(); $tracking->setCarrier('fedex'); $tracking->setCarrierTitle($this->getConfigData('title')); $tracking->setTracking($trackingValue); $tracking->addData($resultArray); $this->_result->append($tracking); } else { $error = $this->_trackErrorFactory->create(); $error->setCarrier('fedex'); $error->setCarrierTitle($this->getConfigData('title')); $error->setTracking($trackingValue); $error->setErrorMessage( $errorTitle ? $errorTitle : __('For some reason we can\'t retrieve tracking info right now.') ); $this->_result->append($error); } } /** * Get tracking response * * @return string */ public function getResponse() { $statuses = ''; if ($this->_result instanceof \Magento\Shipping\Model\Tracking\Result) { if ($trackings = $this->_result->getAllTrackings()) { foreach ($trackings as $tracking) { if ($data = $tracking->getAllData()) { if (!empty($data['status'])) { $statuses .= __($data['status']) . "\n
"; } else { $statuses .= __('Empty response') . "\n
"; } } } } } if (empty($statuses)) { $statuses = __('Empty response'); } return $statuses; } /** * Get allowed shipping methods * * @return array */ public function getAllowedMethods() { $allowed = explode(',', $this->getConfigData('allowed_methods')); $arr = []; foreach ($allowed as $k) { $arr[$k] = $this->getCode('method', $k); } return $arr; } /** * Return array of authenticated information * * @return array */ protected function _getAuthDetails() { return [ 'WebAuthenticationDetail' => [ 'UserCredential' => [ 'Key' => $this->getConfigData('key'), 'Password' => $this->getConfigData('password'), ], ], 'ClientDetail' => [ 'AccountNumber' => $this->getConfigData('account'), 'MeterNumber' => $this->getConfigData('meter_number'), ], 'TransactionDetail' => [ 'CustomerTransactionId' => '*** Express Domestic Shipping Request v9 using PHP ***', ], 'Version' => ['ServiceId' => 'ship', 'Major' => '10', 'Intermediate' => '0', 'Minor' => '0'] ]; } /** * Form array with appropriate structure for shipment request * * @param \Magento\Framework\DataObject $request * @return array * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ protected function _formShipmentRequest(\Magento\Framework\DataObject $request) { if ($request->getReferenceData()) { $referenceData = $request->getReferenceData() . $request->getPackageId(); } else { $referenceData = 'Order #' . $request->getOrderShipment()->getOrder()->getIncrementId() . ' P' . $request->getPackageId(); } $packageParams = $request->getPackageParams(); $customsValue = $packageParams->getCustomsValue(); $height = $packageParams->getHeight(); $width = $packageParams->getWidth(); $length = $packageParams->getLength(); $weightUnits = $packageParams->getWeightUnits() == \Zend_Measure_Weight::POUND ? 'LB' : 'KG'; $unitPrice = 0; $itemsQty = 0; $itemsDesc = []; $countriesOfManufacture = []; $productIds = []; $packageItems = $request->getPackageItems(); foreach ($packageItems as $itemShipment) { $item = new \Magento\Framework\DataObject(); $item->setData($itemShipment); $unitPrice += $item->getPrice(); $itemsQty += $item->getQty(); $itemsDesc[] = $item->getName(); $productIds[] = $item->getProductId(); } // get countries of manufacture $productCollection = $this->_productCollectionFactory->create()->addStoreFilter( $request->getStoreId() )->addFieldToFilter( 'entity_id', ['in' => $productIds] )->addAttributeToSelect( 'country_of_manufacture' ); foreach ($productCollection as $product) { $countriesOfManufacture[] = $product->getCountryOfManufacture(); } $paymentType = $request->getIsReturn() ? 'RECIPIENT' : 'SENDER'; $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(), ], ], '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], ], ], ], ]; // 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->getMasterTrackingId()) { $requestClient['RequestedShipment']['MasterTrackingId'] = $request->getMasterTrackingId(); } if ($request->getShippingMethod() == self::RATE_REQUEST_SMARTPOST) { $requestClient['RequestedShipment']['SmartPostDetail'] = [ 'Indicia' => (double)$request->getPackageWeight() >= 1 ? 'PARCEL_SELECT' : 'PRESORTED_STANDARD', 'HubId' => $this->getConfigData('smartpost_hubid'), ]; } // set dimensions if ($length || $width || $height) { $requestClient['RequestedShipment']['RequestedPackageLineItems']['Dimensions'] = [ 'Length' => $length, 'Width' => $width, 'Height' => $height, 'Units' => $packageParams->getDimensionUnits() == \Zend_Measure_Length::INCH ? 'IN' : 'CM' ]; } return $this->_getAuthDetails() + $requestClient; } /** * 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) { $this->_prepareShipmentRequest($request); $result = new \Magento\Framework\DataObject(); $client = $this->_createShipSoapClient(); $requestClient = $this->_formShipmentRequest($request); $response = $client->processShipment($requestClient); if ($response->HighestSeverity != 'FAILURE' && $response->HighestSeverity != 'ERROR') { $shippingLabelContent = $response->CompletedShipmentDetail->CompletedPackageDetails->Label->Parts->Image; $trackingNumber = $this->getTrackingNumber( $response->CompletedShipmentDetail->CompletedPackageDetails->TrackingIds ); $result->setShippingLabelContent($shippingLabelContent); $result->setTrackingNumber($trackingNumber); $debugData = ['request' => $client->__getLastRequest(), 'result' => $client->__getLastResponse()]; $this->_debug($debugData); } else { $debugData = [ 'request' => $client->__getLastRequest(), 'result' => ['error' => '', 'code' => '', 'xml' => $client->__getLastResponse()], ]; if (is_array($response->Notifications)) { foreach ($response->Notifications as $notification) { $debugData['result']['code'] .= $notification->Code . '; '; $debugData['result']['error'] .= $notification->Message . '; '; } } else { $debugData['result']['code'] = $response->Notifications->Code . ' '; $debugData['result']['error'] = $response->Notifications->Message . ' '; } $this->_debug($debugData); $result->setErrors($debugData['result']['error']); } $result->setGatewayResponse($client->__getLastResponse()); return $result; } /** * @param array|object $trackingIds * @return string */ private function getTrackingNumber($trackingIds) { return is_array($trackingIds) ? array_map( function($val) { return $val->TrackingNumber; }, $trackingIds ) : $trackingIds->TrackingNumber; } /** * For multi package shipments. Delete requested shipments if the current shipment * request is failed * * @param array $data * @return bool */ 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); } return true; } /** * Return container types of carrier * * @param \Magento\Framework\DataObject|null $params * @return array|bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function getContainerTypes(\Magento\Framework\DataObject $params = null) { if ($params == null) { return $this->_getAllowedContainers($params); } $method = $params->getMethod(); $countryShipper = $params->getCountryShipper(); $countryRecipient = $params->getCountryRecipient(); if (($countryShipper == self::USA_COUNTRY_ID && $countryRecipient == self::CANADA_COUNTRY_ID || $countryShipper == self::CANADA_COUNTRY_ID && $countryRecipient == self::USA_COUNTRY_ID) && $method == 'FEDEX_GROUND' ) { return ['YOUR_PACKAGING' => __('Your Packaging')]; } else { if ($method == 'INTERNATIONAL_ECONOMY' || $method == 'INTERNATIONAL_FIRST') { $allTypes = $this->getContainerTypesAll(); $exclude = ['FEDEX_10KG_BOX' => '', 'FEDEX_25KG_BOX' => '']; return array_diff_key($allTypes, $exclude); } else { if ($method == 'EUROPE_FIRST_INTERNATIONAL_PRIORITY') { $allTypes = $this->getContainerTypesAll(); $exclude = ['FEDEX_BOX' => '', 'FEDEX_TUBE' => '']; return array_diff_key($allTypes, $exclude); } else { if ($countryShipper == self::CANADA_COUNTRY_ID && $countryRecipient == self::CANADA_COUNTRY_ID) { // hack for Canada domestic. Apply the same filter rules as for US domestic $params->setCountryShipper(self::USA_COUNTRY_ID); $params->setCountryRecipient(self::USA_COUNTRY_ID); } } } } return $this->_getAllowedContainers($params); } /** * Return all container types of carrier * * @return array|bool */ public function getContainerTypesAll() { return $this->getCode('packaging'); } /** * Return structured data of containers witch related with shipping methods * * @return array|bool */ public function getContainerTypesFilter() { return $this->getCode('containers_filter'); } /** * Return delivery confirmation types of carrier * * @param \Magento\Framework\DataObject|null $params * @return array * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function getDeliveryConfirmationTypes(\Magento\Framework\DataObject $params = null) { return $this->getCode('delivery_confirmation_types'); } /** * Recursive replace sensitive fields in debug data by the mask * @param string $data * @return string */ protected function filterDebugData($data) { foreach (array_keys($data) as $key) { if (is_array($data[$key])) { $data[$key] = $this->filterDebugData($data[$key]); } elseif (in_array($key, $this->_debugReplacePrivateDataKeys)) { $data[$key] = self::DEBUG_KEYS_MASK; } } return $data; } }