From c420e11e738cad4d2fa023a6e90b212f4ebfb84f Mon Sep 17 00:00:00 2001
From: xqiu <1487053+xqiu@users.noreply.github.com>
Date: Wed, 13 Nov 2024 11:32:23 -0800
Subject: [PATCH 1/4] Add Fedex REST API usage for ship rate and tracking
Fedex is currently in transition from WSDL to REST API. This change allows configurable usage of WSDL or REST API.
Note: uses shipstream/fedex-rest-sdk, but currently it's not updated with fedex tracking API schema, so need to use a patch branch for it.
Also composer installing shipstream/fedex-rest-sdk requires PHP 8.1+, so I've to change php version as well for this commit.
---
.../Mage/Usa/Model/Shipping/Carrier/Fedex.php | 391 +++++++++++++++++-
app/code/core/Mage/Usa/etc/config.xml | 2 +
app/code/core/Mage/Usa/etc/system.xml | 32 ++
composer.json | 9 +-
4 files changed, 427 insertions(+), 7 deletions(-)
diff --git a/app/code/core/Mage/Usa/Model/Shipping/Carrier/Fedex.php b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Fedex.php
index d7832a788f7..d157b10539b 100644
--- a/app/code/core/Mage/Usa/Model/Shipping/Carrier/Fedex.php
+++ b/app/code/core/Mage/Usa/Model/Shipping/Carrier/Fedex.php
@@ -13,6 +13,19 @@
* @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
*/
+ use ShipStream\FedEx\FedEx;
+ use ShipStream\FedEx\Api\RatesAndTransitTimesV1\Dto\FullSchemaQuoteRate;
+ use ShipStream\FedEx\Api\RatesAndTransitTimesV1\Dto\AccountNumber;
+ use ShipStream\FedEx\Api\RatesAndTransitTimesV1\Dto\RequestedShipment;
+ use ShipStream\FedEx\Api\RatesAndTransitTimesV1\Dto\RateParty;
+ use ShipStream\FedEx\Api\RatesAndTransitTimesV1\Dto\Address;
+ use ShipStream\FedEx\Api\RatesAndTransitTimesV1\Dto\RequestedPackageLineItem;
+ use ShipStream\FedEx\Api\RatesAndTransitTimesV1\Dto\Weight;
+ use ShipStream\FedEx\Enums\Endpoint;
+ use ShipStream\FedEx\Api\TrackV1\Dto\FullSchemaTrackingNumbers;
+ use ShipStream\FedEx\Api\TrackV1\Dto\TrackingInfo;
+ use ShipStream\FedEx\Api\TrackV1\Dto\TrackingNumberInfo;
+
/**
* Fedex shipping implementation
*
@@ -63,6 +76,13 @@ class Mage_Usa_Model_Shipping_Carrier_Fedex extends Mage_Usa_Model_Shipping_Carr
*/
protected $_rawRequest = null;
+ /**
+ * REST API request data
+ *
+ * @var Varien_Object|null
+ */
+ protected $_fedexRestRequestData = null;
+
/**
* Rate result data
*
@@ -166,9 +186,14 @@ public function collectRates(Mage_Shipping_Model_Rate_Request $request)
if (!$this->getConfigFlag($this->_activeFlag)) {
return false;
}
- $this->setRequest($request);
-
- $this->_getQuotes();
+
+ if ($this->getConfigData('use_rest_api')) {
+ $this->setRestApiRequest($request);
+ $this->_getRestApiQuotes();
+ } else {
+ $this->setRequest($request);
+ $this->_getQuotes();
+ }
$this->_updateFreeMethodQuote($request);
@@ -778,6 +803,8 @@ public function getCode($type, $code = '')
'STANDARD_OVERNIGHT' => Mage::helper('usa')->__('Standard Overnight'),
'FEDEX_FREIGHT' => Mage::helper('usa')->__('Freight'),
'FEDEX_NATIONAL_FREIGHT' => Mage::helper('usa')->__('National Freight'),
+ 'FEDEX_INTERNATIONAL_ECONOMY' => Mage::helper('usa')->__('International Economy'),
+ 'FEDEX_INTERNATIONAL_PRIORITY' => Mage::helper('usa')->__('International Priority'),
],
'dropoff' => [
'REGULAR_PICKUP' => Mage::helper('usa')->__('Regular Pickup'),
@@ -814,6 +841,7 @@ public function getCode($type, $code = '')
'INTERNATIONAL_FIRST',
'INTERNATIONAL_ECONOMY',
'INTERNATIONAL_PRIORITY',
+ 'FEDEX_INTERNATIONAL_PRIORITY',
]
]
]
@@ -840,6 +868,7 @@ public function getCode($type, $code = '')
'INTERNATIONAL_FIRST',
'INTERNATIONAL_ECONOMY',
'INTERNATIONAL_PRIORITY',
+ 'FEDEX_INTERNATIONAL_PRIORITY',
]
]
]
@@ -848,7 +877,12 @@ public function getCode($type, $code = '')
'containers' => ['FEDEX_10KG_BOX', 'FEDEX_25KG_BOX'],
'filters' => [
'within_us' => [],
- 'from_us' => ['method' => ['INTERNATIONAL_PRIORITY']]
+ 'from_us' => [
+ 'method' => [
+ 'INTERNATIONAL_PRIORITY',
+ 'FEDEX_INTERNATIONAL_PRIORITY'
+ ]
+ ]
]
],
[
@@ -885,6 +919,7 @@ public function getCode($type, $code = '')
'FEDEX_NATIONAL_FREIGHT',
'INTERNATIONAL_ECONOMY_FREIGHT',
'INTERNATIONAL_PRIORITY_FREIGHT',
+ 'FEDEX_INTERNATIONAL_PRIORITY'
]
]
]
@@ -949,14 +984,21 @@ public function getCurrencyCode()
*/
public function getTracking($trackings)
{
- $this->setTrackingReqeust();
+ if (!$this->getConfigData('use_rest_api')) {
+ $this->setTrackingReqeust();
+ }
if (!is_array($trackings)) {
$trackings = [$trackings];
}
foreach ($trackings as $tracking) {
- $this->_getXMLTracking($tracking);
+ if ($this->getConfigData('use_rest_api')) {
+ $this->_getRestApiTracking($tracking);
+ }
+ else{
+ $this->_getXMLTracking($tracking);
+ }
}
return $this->_result;
@@ -1604,4 +1646,341 @@ public function getDeliveryConfirmationTypes(?Varien_Object $params = null)
{
return $this->getCode('delivery_confirmation_types');
}
+
+ /**
+ * Prepare and set request to this instance
+ *
+ * @param Mage_Shipping_Model_Rate_Request $request
+ * @return $this
+ */
+ public function setRestApiRequest(Mage_Shipping_Model_Rate_Request $request)
+ {
+ $this->_request = $request;
+
+ // Step 1: Create an AccountNumber object
+ $accountNumber = new AccountNumber(value: $this->getConfigData('account'));
+
+ // Step 2: Create a RequestedShipment object with shipment details
+ $requestedShipment = new RequestedShipment(
+ shipper: new RateParty(
+ address : new Address(
+ countryCode: Mage::getModel('directory/country')->load(
+ Mage::getStoreConfig(
+ Mage_Shipping_Model_Shipping::XML_PATH_STORE_COUNTRY_ID,
+ $request->getStoreId()
+ )
+ )->getIso2Code(),
+ postalCode: Mage::getStoreConfig(
+ Mage_Shipping_Model_Shipping::XML_PATH_STORE_ZIP,
+ $request->getStoreId()
+ )
+ )),
+ recipient: new RateParty(
+ address : new Address(
+ countryCode: Mage::getModel('directory/country')->load($request->getDestCountryId())->getIso2Code(),
+ postalCode: $request->getDestPostcode(),
+ )),
+ pickupType: 'CONTACT_FEDEX_TO_SCHEDULE',
+ requestedPackageLineItems: [
+ new RequestedPackageLineItem(
+ weight: new Weight(
+ units: $this->getConfigData('unit_of_measure'),
+ value: $this->getTotalNumOfBoxes($request->getPackageWeight()),
+ )),
+ ],
+ rateRequestType: ['ACCOUNT'] //['LIST', 'ACCOUNT']
+ );
+
+ // Step 3: Construct the FullSchemaQuoteRate object
+ $rateRequest = new FullSchemaQuoteRate(
+ accountNumber: $accountNumber,
+ requestedShipment: $requestedShipment,
+ );
+
+ $this->_fedexRestRequestData = $rateRequest;
+
+ return $this;
+ }
+
+ /**
+ * Do remote request for and handle errors
+ *
+ * @return Mage_Shipping_Model_Rate_Result
+ */
+ protected function _getRestApiQuotes()
+ {
+ $this->_result = Mage::getModel('shipping/rate_result');
+
+ // Initialize FedEx SDK connector
+ $connector = new FedEx(
+ clientId: $this->getConfigData('key'),
+ clientSecret: $this->getConfigData('password'),
+ endpoint: $this->getConfigFlag('sandbox_mode') ? Endpoint::SANDBOX : Endpoint::PROD
+ );
+
+ try {
+ // Get the Rates and Transit Times API instance
+ $api = $connector->ratesTransitTimesV1();
+
+ $this->_debug($this->_fedexRestRequestData);
+
+ // Perform the API call
+ $response = $api->rateAndTransitTimes($this->_fedexRestRequestData);
+
+ // Process the response and convert it to Magento rate result format
+ $this->_prepareRestApiRateResponse($response);
+ } catch (Exception $e) {
+ $error = Mage::getModel('shipping/rate_result_error');
+ $error->setCarrier($this->_code);
+ $error->setCarrierTitle($this->getConfigData('title'));
+ $error->setErrorMessage($this->getConfigData('specificerrmsg'));
+ $this->_result->append($error);
+ }
+ }
+
+ /**
+ * Return FeDex currency ISO code by Magento Base Currency Code
+ *
+ * @return string 3-digit currency code
+ */
+ public function getOpenmageCurrencyCodeFromFedexCurrencyCode($currencyCode)
+ {
+ $codes = [
+ 'RDD' => 'DOP',
+ 'ECD' => 'XCD',
+ 'ARN' => 'ARS',
+ 'SID' => 'SGD',
+ 'WON' => 'KRW',
+ 'JAD' => 'JMD',
+ 'SFR' => 'CHF',
+ 'JYE' => 'JPY',
+ 'KUD' => 'KWD',
+ 'UKL' => 'GBP',
+ 'DHS' => 'AED',
+ 'NMP' => 'MXN',
+ 'UYP' => 'UYU',
+ 'CHP' => 'CLP',
+ 'NTD' => 'TWD',
+ ];
+ return $codes[$currencyCode] ?? $currencyCode;
+ }
+
+ /**
+ * Prepare shipping rate result based on response
+ *
+ * @param mixed $response
+ * @return Mage_Shipping_Model_Rate_Result
+ */
+ protected function _prepareRestApiRateResponse($response)
+ {
+ $rateResponseDetails = $response->dto()->output->rateReplyDetails ?? [];
+
+ if (empty($rateResponseDetails)) {
+ $error = Mage::getModel('shipping/rate_result_error');
+ $error->setCarrier($this->_code);
+ $error->setCarrierTitle($this->getConfigData('title'));
+ $error->setErrorMessage($this->getConfigData('specificerrmsg'));
+ $this->_result->append($error);
+ return;
+ }
+
+ $this->_debug($rateResponseDetails);
+
+ foreach ($rateResponseDetails as $rateDetail) {
+ $serviceType = $rateDetail->serviceType ?? '';
+ //remove FEDEX_ from serviceType and assign to secondType so that it's compatible with the mangento 1.9x web service allowed methods
+ $secondType = str_replace('FEDEX_', '', $serviceType);
+
+ if (!array_key_exists($serviceType, $this->getAllowedMethods()) && !array_key_exists($secondType, $this->getAllowedMethods())) {
+ continue;
+ }
+
+ $rate = $rateDetail->ratedShipmentDetails[0]->totalNetCharge;
+
+ // if the currency is different with the store currency, calculate using the exchange rate
+ $currencyCode = (string)$rateDetail->ratedShipmentDetails[0]->shipmentRateDetail->currency;
+ $currencyCode = $this->getOpenmageCurrencyCodeFromFedexCurrencyCode($currencyCode);
+ $storeCurrencyCode = Mage::app()->getStore()->getBaseCurrencyCode();
+ if($storeCurrencyCode != $currencyCode){
+ $currencyStore = Mage::getModel('directory/currency')->load($storeCurrencyCode);
+ $currencyCurrent = Mage::getModel('directory/currency')->load($currencyCode);
+ $rate = round($rate / $currencyStore->getRate($currencyCurrent));
+ }
+
+ $method = Mage::getModel('shipping/rate_result_method');
+ $method->setCarrier($this->_code);
+ $method->setCarrierTitle($this->getConfigData('title'));
+ $method->setMethod($serviceType);
+ $method->setMethodTitle($this->getCode('method', $serviceType));
+
+ $method->setCost($rate);
+ $method->setPrice($this->getFinalPriceWithHandlingFee($rate));
+
+ $this->_result->append($method);
+ }
+ }
+
+ /**
+ * Send REST API request for tracking
+ *
+ * @param array $tracking
+ */
+ protected function _getRestApiTracking($tracking)
+ {
+ $this->_result = Mage::getModel('shipping/tracking_result');
+
+ // Initialize FedEx SDK connector
+ $connector = new FedEx(
+ clientId: $this->getConfigData('rest_track_key'),
+ clientSecret: $this->getConfigData('rest_track_secrete'),
+ endpoint: $this->getConfigFlag('sandbox_mode') ? Endpoint::SANDBOX : Endpoint::PROD
+ );
+
+ try {
+ // Create a TrackingRequest instance
+ $trackRequest = new FullSchemaTrackingNumbers(
+ includeDetailedScans: true,
+ trackingInfo: [
+ new TrackingInfo(
+ trackingNumberInfo: new TrackingNumberInfo(trackingNumber: $tracking),
+ )
+ ],
+ );
+
+ $this->_debug($trackRequest);
+
+ // Send tracking request
+ $api = $connector->trackV1();
+ $response = $api->trackByTrackingNumber($trackRequest);
+
+ // Parse the response and convert it to tracking result format
+ $this->_parseRestApiTrackingResponse($tracking, $response);
+ } catch (Exception $e) {
+ $error = Mage::getModel('shipping/tracking_result_error');
+ $error->setCarrier($this->_code);
+ $error->setCarrierTitle($this->getConfigData('title'));
+ $error->setTracking($tracking);
+ $error->setErrorMessage(Mage::helper('usa')->__('Unable to retrieve tracking'));
+ $this->_result->append($error);
+ }
+ }
+
+ /**
+ * Parse tracking response
+ *
+ * @param array $trackingValue
+ * @param stdClass $response
+ */
+ protected function _parseRestApiTrackingResponse($trackingValue, $response)
+ {
+ $this->_debug($response->body());
+
+ $trackInfo = $response->dto()->output->completeTrackResults[0]->trackResults[0] ?? null;
+
+ if (!$trackInfo || !isset($trackInfo->latestStatusDetail)) {
+ $error = Mage::getModel('shipping/tracking_result_error');
+ $error->setCarrier('fedex');
+ $error->setCarrierTitle($this->getConfigData('title'));
+ $error->setTracking($trackingValue);
+ $error->setErrorMessage(Mage::helper('usa')->__('Unable to retrieve tracking'));
+ $this->_result->append($error);
+ return;
+ }
+
+ $resultArray = [];
+ $resultArray['status'] = (string)$trackInfo->latestStatusDetail->description;
+ $resultArray['service'] = (string)$trackInfo->serviceDetail->description;
+
+ // Handle delivery date and time
+ if($trackInfo->dateAndTimes){
+ if($trackInfo->dateAndTimes[0]->type == 'ACTUAL_DELIVERY' || $trackInfo->dateAndTimes[0]->type == 'ACTUAL_PICKUP' ||
+ $trackInfo->dateAndTimes[0]->type == 'ESTIMATED_DELIVERY' || $trackInfo->dateAndTimes[0]->type == 'ESTIMATED_DELIVERY') {
+ $timestamp = strtotime((string) $trackInfo->dateAndTimes[0]->dateTime);
+ $resultArray['deliverydate'] = date('Y-m-d', $timestamp);
+ $resultArray['deliverytime'] = date('H:i:s', $timestamp);
+ }
+
+ if($trackInfo->dateAndTimes[0]->type == 'ACTUAL_DELIVERY' || $trackInfo->dateAndTimes[0]->type == 'ACTUAL_PICKUP') {
+ $timestamp = strtotime((string) $trackInfo->dateAndTimes[0]->dateTime);
+ $resultArray['shippeddate'] = date('Y-m-d', $timestamp);
+ }
+ }
+
+ // Handle delivery location
+ if (isset($trackInfo->lastUpdatedDestinationAddress)) {
+ $deliveryLocation = $trackInfo->lastUpdatedDestinationAddress;
+ $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);
+ }
+ }
+
+ if(isset($trackInfo->deliveryDetails)) {
+ $resultArray['signedby'] = (string)($trackInfo->deliveryDetails->signedByName ?? '');
+ }
+
+ if (isset($trackInfo->packageDetails->weightAndDimensions->weight[0]->value) && isset($trackInfo->packageDetails->weightAndDimensions->weight[0]->unit)) {
+ $resultArray['weight'] = "{$trackInfo->packageDetails->weightAndDimensions->weight[0]->value} {$trackInfo->packageDetails->weightAndDimensions->weight[0]->unit}";
+ }
+
+ // Track package progress
+ $packageProgress = [];
+ foreach ($trackInfo->scanEvents ?? [] as $event) {
+ $tempArray = [];
+ $tempArray['activity'] = (string)$event->derivedStatus;
+ $timestamp = strtotime((string)$event->date);
+ if ($timestamp) {
+ $tempArray['deliverydate'] = date('Y-m-d', $timestamp);
+ $tempArray['deliverytime'] = date('H:i:s', $timestamp);
+ }
+ if (isset($event->scanLocation)) {
+ $addressArray = [];
+ if (isset($event->scanLocation->city)) {
+ $addressArray[] = (string)$event->scanLocation->city;
+ }
+ if (isset($event->scanLocation->stateOrProvinceCode)) {
+ $addressArray[] = (string)$event->scanLocation->stateOrProvinceCode;
+ }
+ if (isset($event->scanLocation->countryCode)) {
+ $addressArray[] = (string)$event->scanLocation->countryCode;
+ }
+ if ($addressArray) {
+ $tempArray['deliverylocation'] = implode(', ', $addressArray);
+ }
+ }
+ $packageProgress[] = $tempArray;
+ }
+ $resultArray['progressdetail'] = $packageProgress;
+
+ // Prepare tracking result
+ if (!$this->_result) {
+ $this->_result = Mage::getModel('shipping/tracking_result');
+ }
+
+ if (isset($resultArray)) {
+ $tracking = Mage::getModel('shipping/tracking_result_status');
+ $tracking->setCarrier('fedex');
+ $tracking->setCarrierTitle($this->getConfigData('title'));
+ $tracking->setTracking($trackingValue);
+ $tracking->addData($resultArray);
+ $this->_result->append($tracking);
+ } else {
+ $error = Mage::getModel('shipping/tracking_result_error');
+ $error->setCarrier('fedex');
+ $error->setCarrierTitle($this->getConfigData('title'));
+ $error->setTracking($trackingValue);
+ $error->setErrorMessage(Mage::helper('usa')->__('Unable to retrieve tracking'));
+ $this->_result->append($error);
+ }
+ }
+
}
diff --git a/app/code/core/Mage/Usa/etc/config.xml b/app/code/core/Mage/Usa/etc/config.xml
index 6925a94e3ab..314ff0a32eb 100644
--- a/app/code/core/Mage/Usa/etc/config.xml
+++ b/app/code/core/Mage/Usa/etc/config.xml
@@ -126,6 +126,8 @@