diff --git a/env.sample.php b/env.sample.php index ad3b64e..955991f 100644 --- a/env.sample.php +++ b/env.sample.php @@ -50,6 +50,8 @@ $SETTINGS = [ "ups_access_key" => "", "ups_user_account" => "", "ups_password" => "", + "fedex_apikey" => "", + "fedex_secret" => "", "shippo_key" => "", "easypost_key" => "", ]; \ No newline at end of file diff --git a/lib/Tracking.lib.php b/lib/Tracking.lib.php index c84dd6e..4e74ddd 100644 --- a/lib/Tracking.lib.php +++ b/lib/Tracking.lib.php @@ -29,6 +29,8 @@ class Tracking { return Tracking_USPS::track($barcode->getSanitized()); case "ups": return Tracking_UPS::track($barcode->getSanitized()); + case "fedex": + return Tracking_FedEx::track($barcode->getSanitized()); default: return Tracking_EasyPost::track($barcode->getSanitized()); //return Tracking_Shippo::track($barcode->getSanitized()); diff --git a/lib/TrackingStatus.lib.php b/lib/TrackingStatus.lib.php index 203ad15..6d295bf 100644 --- a/lib/TrackingStatus.lib.php +++ b/lib/TrackingStatus.lib.php @@ -255,4 +255,73 @@ class TrackingStatus { } } + public static function FedExEventTypeToStatus(string $eventcode) { + switch ($eventcode) { + case "OC": + case "OX": + case "PD": + case "RP": + case "RG": + case "SH": + return TrackingStatus::TRACKING_STATUS_PRE_TRANSIT; + case "AA": + case "AC": + case "AD": + case "AF": + case "AP": + case "AR": + case "AX": + case "CH": + case "DD": + case "DP": + case "DR": + case "DS": + case "EA": + case "ED": + case "EO": + case "EP": + case "FD": + case "HL": + case "IT": + case "IX": + case "LO": + case "OD": + case "OF": + case "PF": + case "PL": + case "PM": + case "PU": + case "PX": + case "SF": + case "TR": + case "CC": + case "CD": + case "CP": + case "EA": + case "CA": + case "CU": + case "BR": + case "TP": + case "IP": + case "DO": // dropped off + return TrackingStatus::TRACKING_STATUS_TRANSIT; + case "RC": // Recipient + case "DL": // Delivered + case "HP": // Ready for pickup + return TrackingStatus::TRACKING_STATUS_DELIVERED; + case "RS": + return TrackingStatus::TRACKING_STATUS_RETURNED; + case "CA": + case "DE": + case "DY": + case "LP": + case "RD": + case "SE": + return TrackingStatus::TRACKING_STATUS_FAILURE; + case "SP": + default: + return TrackingStatus::TRACKING_STATUS_UNKNOWN; + } + } + } diff --git a/lib/Tracking_FedEx.lib.php b/lib/Tracking_FedEx.lib.php new file mode 100644 index 0000000..efbdde3 --- /dev/null +++ b/lib/Tracking_FedEx.lib.php @@ -0,0 +1,187 @@ + 'application/x-www-form-urlencoded' + ]; + + $postdata = http_build_query([ + "grant_type" => "client_credentials", + "client_id" => env("fedex_apikey", ""), + "client_secret" => env("fedex_secret", "") + ]); + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($ch, CURLOPT_TIMEOUT, 45); + //curl_setopt($ch, CURLOPT_URL, "https://apis-sandbox.fedex.com/oauth/token"); // TEST + curl_setopt($ch, CURLOPT_URL, "https://apis.fedex.com/oauth/token"); // PROD + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $postdata); + $response = curl_exec($ch); + + $data = json_decode($response, true); + + if (isset($data["access_token"])) { + return [$data["access_token"], $data["expires_in"]]; + } + throw new Exception("Couldn't get FedEx access token."); + } + + /** + * + * @param string $code + * @return \TrackingInfo + * @throws TrackingException + */ + public static function track(string $code, string $carrier = ""): TrackingInfo { + global $memcache; + $barcode = new TrackingBarcode($code); + + $fedexauthtoken = $memcache->get("logistics.tracking.fedex_auth_token"); + if ($fedexauthtoken == false) { + $tokendata = Tracking_FedEx::getAuthToken(); + $memcache->set("logistics.tracking.fedex_auth_token", $tokendata[0], $tokendata[1] - 120); + $fedexauthtoken = $tokendata[0]; + } + + try { + $trackrequest = [ + "includeDetailedScans" => true, + "trackingInfo" => [ + [ + //"shipDateBegin" => date("Y-m-d", strtotime("now - 3 months")), + "trackingNumberInfo" => [ + "trackingNumber" => $barcode->getCode() + ] + ] + ] + ]; + $headers = [ + "Authorization: Bearer $fedexauthtoken", + 'X-locale: en_US', + 'Content-Type: application/json' + ]; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($ch, CURLOPT_TIMEOUT, 45); + //curl_setopt($ch, CURLOPT_URL, "https://apis-sandbox.fedex.com/track/v1/trackingnumbers"); // TEST + curl_setopt($ch, CURLOPT_URL, "https://apis.fedex.com/track/v1/trackingnumbers"); // PROD + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($trackrequest)); + curl_setopt($ch, CURLOPT_ENCODING, ""); + $response = curl_exec($ch); + + // CHECK TO SEE IF WE GOT AN ERROR + if ((curl_errno($ch)) && (curl_errno($ch) != 0)) { + throw new TrackingException(curl_error($ch)); + } + + $result = json_decode($response, true); + + if (isset($result["errors"]) || !empty($result["errors"])) { + if (count($result["errors"]) > 0) { + throw new TrackingException($result["errors"][0]["message"]); + } + throw new TrackingException("The FedEx tracking system responded with an error. Try again later."); + } + + if (!empty($result["output"]["completeTrackResults"]) && !empty($result["output"]["completeTrackResults"][0]["trackResults"])) { + $trackinfo = $result["output"]["completeTrackResults"][0]["trackResults"][0]; + } else { + throw new TrackingException("No tracking details found."); + } + if (!empty($result["output"]["completeTrackResults"][0]["trackResults"][0]["error"])) { + throw new TrackingException($result["output"]["completeTrackResults"][0]["trackResults"][0]["error"]["message"]); + } + } catch (TrackingException $ex) { + throw $ex; + } catch (Exception $ex) { + throw new TrackingException("There was a server problem. This code cannot be tracked right now."); + } + + $info = new TrackingInfo(); + + $info->setCode($trackinfo["trackingNumberInfo"]["trackingNumber"]); + + $info->setCarrier("fedex"); + $info->setService(new Service($trackinfo["serviceDetail"]["type"], $trackinfo["serviceDetail"]["description"])); + + // Set carrier info: Ground, Express, Freight, etc + $carrierid = "fedex"; + switch ($trackinfo["trackingNumberInfo"]["carrierCode"]) { + case "FEDEX_GROUND": + case "FEDEX_EXPRESS": + case "FEDEX_FREIGHT": + default: + $carrierid = "fedex"; + break; + } + $info->setCarrierAttributionText(CarrierAssets::getAttribution(Carriers::getCarrierCode($carrierid))); + $info->setCarrierLogo(CarrierAssets::getLogo(Carriers::getCarrierCode($carrierid))); + + if (count($trackinfo["scanEvents"]) > 0) { + $current = $trackinfo["scanEvents"][0]; + $current_status = new TrackingEntry( + TrackingStatus::FedExEventTypeToStatus($current["eventType"]), + ($current["eventDescription"] ?? "Unknown") + . (TrackingStatus::FedExEventTypeToStatus($current["eventType"]) == TrackingStatus::TRACKING_STATUS_UNKNOWN ? " " . $current["eventType"] : ""), + date("c", strtotime($current["date"])) + ); + + $current_location = new Location(); + // FedEx docs say it should be $current["scanLocation"]["locationContactAndAddress"]["address"]["...}] but it's not I guess + $current_location->city = $current["scanLocation"]["city"] ?? ""; + $current_location->state = $current["scanLocation"]["stateOrProvinceCode"] ?? ""; + $current_location->zip = $current["scanLocation"]["postalCode"] ?? ""; + $current_location->country = $current["scanLocation"]["countryCode"] ?? ""; + $current_status->setLocation($current_location); + + $info->setCurrentStatus($current_status); + } + + + $to = new Location(); + $to->city = $trackinfo["recipientInformation"]["address"]["city"] ?? ""; + $to->state = $trackinfo["recipientInformation"]["address"]["stateOrProvinceCode"] ?? ""; + $to->zip = $trackinfo["recipientInformation"]["address"]["postalCode"] ?? ""; + $to->country = $trackinfo["recipientInformation"]["address"]["countryCode"] ?? ""; + $info->setTo($to); + + $from = new Location(); + $from->city = $trackinfo["shipperInformation"]["address"]["city"] ?? ""; + $from->state = $trackinfo["shipperInformation"]["address"]["stateOrProvinceCode"] ?? ""; + $from->zip = $trackinfo["shipperInformation"]["address"]["postalCode"] ?? ""; + $from->country = $trackinfo["shipperInformation"]["address"]["countryCode"] ?? ""; + $info->setFrom($from); + + foreach ($trackinfo["scanEvents"] as $history) { + $location = new Location(); + $location->city = $history["scanLocation"]["city"] ?? ""; + $location->state = $history["scanLocation"]["stateOrProvinceCode"] ?? ""; + $location->zip = $history["scanLocation"]["postalCode"] ?? ""; + $location->country = $history["scanLocation"]["countryCode"] ?? ""; + if (!empty($history["date"])) { + $datetimestring = date("c", strtotime($history["date"])); + } else { + $datetimestring = ""; + } + $info->appendHistoryEntry(new TrackingEntry( + TrackingStatus::FedExEventTypeToStatus($history["eventType"]), + ($history["eventDescription"] ?? "Unknown") + . (TrackingStatus::FedExEventTypeToStatus($history["eventType"]) == TrackingStatus::TRACKING_STATUS_UNKNOWN ? " " . $history["eventType"] : ""), + $datetimestring, + $location)); + } + + return $info; + } + +}