* *@version 0.3 * *@package Quinta * @subpackage Classes */ class FedexRequest extends ShippingRequest { /** * NOTE: LabelStockType must be one of the following: * - PAPER_4X6 * - PAPER_4X8 * - PAPER_4X9 * - PAPER_7X4.75 * - PAPER_8.5X11_BOTTOM_HALF_LABEL * - PAPER_8.5X11_TOP_HALF_LABEL * - STOCK_4X6 * - STOCK_4X6.75_LEADING_DOC_TAB * - STOCK_4X6.75_TRAILING_DOC_TAB * - STOCK_4X8 * - STOCK_4X9_LEADING_DOC_TAB * - STOCK_4X9_TRAILING_DOC_TAB * @var string */ protected $strLabelStockType = 'PAPER_4X6'; /** * NOTE: LabelFormatType must be one of the following: * COMMON2D * LABEL_DATA_ONLY *@var string paper format for label */ protected $strLabelFormatType = 'COMMON2D'; /** * NOTE: ImageType must be one of the following: * DPL * EPL2 * PDF * PNG * ZPLII *@var string image format for label */ protected $strImageType = 'PNG'; /** *@var string Account number provided by Fedex */ protected $strAccountNumber; /** *@var string Meter number provided by Fedex */ protected $strMeterNumber; /** *@var string strPayorType - who pays for the shipping */ protected $strPayorType = 'SENDER'; /** * NOTE: DropoffType must be one of the following: * - BUSINESS_SERVICE_CENTER * - DROP_BOX * - REGULAR_PICKUP * - REQUEST_COURIER * - STATION *@var string Drop off method */ protected $strDropoffType = 'REGULAR_PICKUP'; /** * NOTE: PackagingType must be one of the following: * - FEDEX_10KG_BOX * - FEDEX_25KG_BOX * - FEDEX_BOX * - FEDEX_ENVELOPE * - FEDEX_PAK * - FEDEX_TUBE * - YOUR_PACKAGING *@var string Packaging type */ protected $strPackagingType = 'YOUR_PACKAGING'; /** *@var string Indicates units of weight (LB | KG) */ protected $strWeightUnits = 'LB'; /** *@var string Indicates units of length (IN | CM) */ protected $strLengthUnits = 'IN'; /** * FedEx combines units (eg. pounds and ounces ) into one figure .. *@var float fltWeight - weight of package in designated units .. */ protected $fltWeight; /** * FedexRequest Constructor * * @param ShippingMethod objShippingMethod - the method for which to obtain estimate */ public function __construct(ShippingMethod $objShippingMethod){ parent::__construct($objShippingMethod); //now unused .. $this->strRemoteCgiUrl = '/GatewayDC'; if($objShippingMethod->TestMode){///@todo defined in config - fixme! $this->strRemoteDomainName = 'gatewaybeta.fedex.com'; $this->strRemoteAccountId = FEDEX_TESTKEY; $this->strRemotePassword = FEDEX_TESTPASSWORD; $this->strAccountNumber = FEDEX_TESTACCOUNT_NUMBER; $this->strMeterNumber = FEDEX_TESTMETER_NUMBER; $this->strRemoteDomainName = 'gatewaybeta.fedex.com'; //unused .. }else{ $this->strRemoteDomainName = 'gateway.fedex.com'; $this->strRemoteAccountId = FEDEX_KEY; $this->strRemotePassword = FEDEX_PASSWORD; $this->strAccountNumber = FEDEX_ACCOUNT_NUMBER; $this->strMeterNumber = FEDEX_METER_NUMBER; $this->strRemoteDomainName = 'gateway.fedex.com'; //unused .. } $this->fltWeight = $objShippingMethod->Pounds; $this->fltWeight += $objShippingMethod->Ounces / 16; } //Public interface .. /** * Returns a shipping rate for the order for this method *@return image object containing the image code */ public function GetRate(){ $this->intShippingRequestType = ShippingRequestType::Rate; //Example code does this - not sure why or if it is needed .. ini_set("soap.wsdl_cache_enabled", "0"); if($this->blnTestMode) $this->strWsdlUri = __QUINTA_CORE__ . '/utilities/FDXRateService_v5_test.wsdl'; else $this->strWsdlUri = __QUINTA_CORE__ . '/utilities/FDXRateService_v5.wsdl'; $this->strSoapFunction = 'getRates'; $this->createSoapRateRequest(); try{ $this->submitSoapRequest(); } catch (SoapFault $objFault) { // exit(var_dump($objFault)); throw new Exception($objFault->faultstring); } $this->handleRateResponse(); return $this->Rate; } /** * Utility function to format the mult-dimensional array of RateRequest (labels) request data * to be passed on the the Soap client function. *@return array - suitable for passing to the Soap function for RateRequest (labels) requests */ protected function createSoapRateRequest(){ $arySoapParamsToReturn['WebAuthenticationDetail'] = $this->createWebAuthDetailArray(); $arySoapParamsToReturn['ClientDetail'] = $this->createClientDetailArray(); $arySoapParamsToReturn['TransactionDetail'] = $this->createTransactionDetailArray(); $arySoapParamsToReturn['Version'] = $this->createVersionDetailArray('crs'); $aryRequestedShipment = $this->createRequestedShipmentDetailArray(); $aryRequestedShipment['PackageDetail'] = 'INDIVIDUAL_PACKAGES'; $arySoapParamsToReturn['RequestedShipment'] = $aryRequestedShipment; $this->arySoapRequest = $arySoapParamsToReturn; } /** * Parses the rate request SOAP response from FEDEX server * @todo - handle errors more elegantly, make more robust .. */ protected function handleRateResponse(){ $this->Rate = 0; //first check for Fedex errors if('FAILURE' == $this->mixSoapResponse->HighestSeverity || 'ERROR' == $this->mixSoapResponse->HighestSeverity ) { $this->blnIsAvailable = false; $this->blnHasErrors = true; $mixNotifications = $this->mixSoapResponse->Notifications; if(is_array($mixNotifications)) foreach($mixNotifications as $objNotification) $this->strErrors .= $objNotification->Severity . ':' . $objNotification->Message . "\n"; else $this->strErrors .= $mixNotifications->Severity . ' : ' .$mixNotifications->Message . "\n"; //Service is not allowed to destination - eg. fedex_2_day to Australia .. not really // an error, just not available so reset HasErrors: if( false !== stripos( $this->strErrors, 'Service is not allowed' ) ) $this->blnHasErrors = false; } else { $this->blnIsAvailable = true; $mixDetails = $this->mixSoapResponse->RateReplyDetails->RatedShipmentDetails; if(is_array($mixDetails)) $this->Rate = $mixDetails[0]->ShipmentRateDetail->TotalNetCharge->Amount; else $this->Rate = $mixDetails->ShipmentRateDetail->TotalNetCharge->Amount; } if($this->blnIsAvailable && !$this->Rate){ $this->blnIsAvailable = false; $this->HasErrors = true; $this->strErrors = 'Unknown Fedex Error.'; } return $this->Rate; } /** * Returns a shipping label for this method to the order address * Note: this function uses the SOAP interface provided from Fedex - partially due to time constraints * and partially to remind me to reimplement these service request classes to use SOAP; all of the * web services seem to be providing WSDLs now and it is builtin to PHP so it makes sense. Later. * This quick example implements the "new" style Fedex API. *@return string containing label image or null on failure .. */ public function GetLabel(){ $this->intShippingRequestType = ShippingRequestType::Label; //Example code does this - not sure why or if it is needed .. ini_set("soap.wsdl_cache_enabled", "0"); if($this->blnTestMode) $this->strWsdlUri = __QUINTA_CORE__ . '/utilities/FDXShipService_v5_test.wsdl'; else $this->strWsdlUri = __QUINTA_CORE__ . '/utilities/FDXShipService_v5.wsdl'; $this->strSoapFunction = 'processShipment'; $this->createSoapShipRequest(); try{ $this->submitSoapRequest(); } catch (SoapFault $objFault) { // exit(var_dump($objFault)); throw new Exception($objFault->faultstring . ' - ' . $objFault->detail->fault->details->ValidationFailureDetail->message); } return $this->handleLabelResponse(); } /** * Handles a label response returning the image. On failure the image is null and errors are * stored in strErrors. The image is the string containing the formatted image from Fedex - this * can be written directly to a file as eg. image.png or image.pdf .. * @return string containing the image code returned from Fedex */ protected function handleLabelResponse(){ $this->objShippingLabelImage = null; //first check for Fedex errors if('FAILURE' == $this->mixSoapResponse->HighestSeverity || 'ERROR' == $this->mixSoapResponse->HighestSeverity ) { $this->blnHasErrors = true; //Note: this part is unclear - Notifications can be an array or object?? May not work correctly .. if(is_array($this->mixSoapResponse->Notifications)) foreach($this->mixSoapResponse->Notifications as $objNotification) $this->strErrors .= $objNotification->Severity . ':' . $objNotification->Message . "\n"; else $this->strErrors .= $this->mixSoapResponse->Notifications->Severity . ': ' . $this->mixSoapResponse->Notifications->Message . "\n"; } else { $objPackageDetails = $this->mixSoapResponse->CompletedShipmentDetail->CompletedPackageDetails; $this->objShippingLabelImage = $objPackageDetails->Label->Parts->Image; $strFinalPrice = $this->mixSoapResponse->CompletedShipmentDetail->ShipmentRating; if(!empty($strFinalPrice)) { $this->objShippingMethod->Order->ShippingCost = $strFinalPrice; $this->objShippingMethod->Order->Save(false,true); } if($this->Order->IsInternational){ if(property_exists($objPackageDetails, 'PackageDocuments')){ $aryPackageDocuments = $objPackageDetails->PackageDocuments; foreach($aryPackageDocuments as $objDocument){ $this->aryExtraDocumentImages[] = new ExtraDocumentImage( $objDocument->Parts->Image, $objDocument->Type, $objDocument->CopiesToPrint ); } } } $strTrackingNumber = $objPackageDetails->TrackingId->TrackingNumber; if(!empty($strTrackingNumber)){ if(! TrackingNumber::LoadByOrderIdNumber($this->objShippingMethod->OrderId, $strTrackingNumber )){ $objTrackingNumber = new TrackingNumber(); $objTrackingNumber->OrderId = $this->objShippingMethod->OrderId; $objTrackingNumber->Number = $strTrackingNumber; $objTrackingNumber->Save(); } } } return $this->objShippingLabelImage; } /** * Utility function to format the mult-dimensional array of ShipRequest (labels) request data * to be passed on the the Soap client function. *@return array - suitable for passing to the Soap function for ShipRequest (labels) requests */ protected function createSoapShipRequest(){ $arySoapParamsToReturn['WebAuthenticationDetail'] = $this->createWebAuthDetailArray(); $arySoapParamsToReturn['ClientDetail'] = $this->createClientDetailArray(); $arySoapParamsToReturn['TransactionDetail'] = $this->createTransactionDetailArray(); $arySoapParamsToReturn['Version'] = $this->createVersionDetailArray('ship'); $aryRequestedShipment = $this->createRequestedShipmentDetailArray(); $aryRequestedShipment['LabelSpecification'] = array('LabelFormatType' => $this->strLabelFormatType, 'ImageType' => $this->strImageType, 'LabelStockType' => $this->strLabelStockType, ); $arySoapParamsToReturn['RequestedShipment'] = $aryRequestedShipment; $this->arySoapRequest = $arySoapParamsToReturn; } protected function createWebAuthDetailArray(){ return array('UserCredential' => array('Key' => $this->strRemoteAccountId, 'Password' => $this->strRemotePassword ) ); } protected function createClientDetailArray(){ return array('AccountNumber' => $this->strAccountNumber, 'MeterNumber' => $this->strMeterNumber ); } protected function createTransactionDetailArray(){ return array('CustomerTransactionId' => STORE_NAME . ' Order ' . $this->Order->Id ); } protected function createVersionDetailArray($strService){ return array('ServiceId' => $strService, 'Major' => '5', 'Intermediate' => '0', 'Minor' => '0'); } protected function createShipperDetailArray(){ $strSenderCountryCode = CountryType::ToIsoCode2( $this->OriginCountryId); $arySenderStreets = array(); if(STORE_ADDRESS1) $arySenderStreets[] = STORE_ADDRESS1; if(STORE_ADDRESS2) $arySenderStreets[] = STORE_ADDRESS2; if(! count($arySenderStreets)) throw new Exception('FedexRequest: Shipper address must have at least one street line!'); return array('Contact' => array('PersonName' => STORE_OWNER, 'CompanyName' => STORE_NAME, 'PhoneNumber' => STORE_PHONE, ), 'Address' => array('StreetLines' =>$arySenderStreets, 'City' => STORE_CITY, 'StateOrProvinceCode' => STORE_STATE, 'PostalCode' => STORE_POSTAL_CODE, 'CountryCode' => $strSenderCountryCode, ), ); } protected function createRecipientDetailArray(){ $strRecipientName = $this->Order->FullShippingName; $strRecipientCompany = $this->Order->ShippingCompany; $strRecipientCity = $this->Order->ShippingCity; $strRecipientPhone = $this->Order->Account->Person->PhoneNumber; $aryRecipientStreets = array(); if($this->Order->ShippingStreet1) $aryRecipientStreets[] = $this->Order->ShippingStreet1; if($this->Order->ShippingStreet2) $aryRecipientStreets[] = $this->Order->ShippingStreet2; if(! count($aryRecipientStreets)) throw new Exception('FedexRequest: Recipient address must have at least one street line!'); $aryToReturn = array('Contact' => array('PersonName' => $strRecipientName, 'CompanyName' => $strRecipientCompany, 'PhoneNumber' => $strRecipientPhone, ), 'Address' => array('StreetLines' => $aryRecipientStreets, 'City' => $strRecipientCity, //Fedex barfs if the state or province is over 2 chars .. 'StateOrProvinceCode' => substr($this->DestinationStateCode, 0, 2), 'PostalCode' => $this->DestinationZip, 'CountryCode' => $this->DestinationCountryCode, ), ); return $aryToReturn; } protected function createRequestedShipmentDetailArray(){ $strSenderCountryCode = CountryType::ToIsoCode2( $this->OriginCountryId); $aryToReturn = array( 'ShipTimestamp' => date('c'), 'DropoffType' => $this->strDropoffType, 'ServiceType' => $this->ServiceType, 'PackagingType' => $this->strPackagingType, 'TotalWeight' => array( 'Value' => number_format($this->Weight, 1), 'Units' => $this->strWeightUnits, ), 'Shipper' => $this->createShipperDetailArray(), 'Recipient' => $this->createRecipientDetailArray(), 'ShippingChargesPayment' => array( 'PaymentType' => $this->strPayorType, 'Payor' => array('AccountNumber' => $this->strAccountNumber, 'CountryCode' => $strSenderCountryCode, ), ), 'RateRequestTypes' => array('ACCOUNT'), // valid values ACCOUNT and LIST 'PackageCount' => 1, 'RequestedPackages' => $this->createRequestedPackagesDetailArray(), ); if($this->Order->IsInternational && ShippingRequestType::Label == $this->intShippingRequestType) $aryToReturn['InternationalDetail'] = $this->createInternationalDetailArray(); return $aryToReturn; } protected function createInternationalDetailArray(){ //start with "Contact" and "Address" populated .. $aryToReturn = $this->createShipperDetailArray(); $aryToReturn['CustomsValue'] = array('Amount' => number_format($this->TotalValue, 2), 'Currency' => 'USD', ); $aryToReturn['DocumentContent'] = 'DOCUMENTS_ONLY'; $aryToReturn['DutiesPayment'] = array('PaymentType' => 'SENDER', 'Payor' => array('AccountNumber' => $this->strAccountNumber, 'CountryCode' => $this->DestinationCountryCode, ), ); $aryCommodities = array(); foreach( OrderItem::LoadArrayByOrderId( $this->Order->Id ) as $objOrderItem ){ $objProduct = $objOrderItem->Product; $fltWeight = $objProduct->Weight / 16; if('KG' == $this->strWeightUnits ) $fltWeight = $fltWeight / 2.2; $fltTotalAmount = $objProduct->RetailPrice * $objOrderItem->Quantity; $aryCommodities[] = array('NumberOfPieces' => $objOrderItem->Quantity, 'Description' => $objProduct->ShortDescription, 'CountryOfManufacture' => 'US', 'Weight' => array('Value' => $fltWeight, 'Units' => $this->strWeightUnits, ), 'Quantity' => $objOrderItem->Quantity, 'QuantityUnits' => 'EA', 'UnitPrice' => array('Amount' => $objProduct->RetailPrice, 'Currency' => 'USD', ), 'CustomsValue' => array('Amount' => $fltTotalAmount, 'Currency' => 'USD', ), ); } if(empty($aryCommodities)) throw new Exception('No order items for international Order #' . $this->Order->Id); $aryToReturn['Commodities'] = $aryCommodities; return $aryToReturn; } protected function createRequestedPackagesDetailArray(){ return array('0' => array('SequenceNumber' => '1', 'Weight' => array( 'Value' => number_format($this->Weight, 1), 'Units' => $this->strWeightUnits, ), ), ); } public function __get($strName){ switch ($strName){ case 'Weight': return $this->fltWeight ; case 'WeightUnits': return $this->strWeightUnits ; case 'LengthUnits': return $this->strLengthUnits ; case 'MeterNumber': return $this->strMeterNumber ; default: try { return parent::__get($strName); } catch (QCallerException $objExc) { $objExc->IncrementOffset(); throw $objExc; } } } public function __set($strName, $mixValue){ switch ($strName){ case 'Weight': try { return ($this->fltWeight = QType::Cast($mixValue, QType::Float )); } catch (QInvalidCastException $objExc) { $objExc->IncrementOffset(); throw $objExc; } case 'WeightUnits': try { return ($this->strWeightUnits = QType::Cast($mixValue, QType::String )); } catch (QInvalidCastException $objExc) { $objExc->IncrementOffset(); throw $objExc; } case 'LengthUnits': try { return ($this->strLengthUnits = QType::Cast($mixValue, QType::String )); } catch (QInvalidCastException $objExc) { $objExc->IncrementOffset(); throw $objExc; } case 'MeterNumber': try { return ($this->strMeterNumber = QType::Cast($mixValue, QType::String )); } catch (QInvalidCastException $objExc) { $objExc->IncrementOffset(); throw $objExc; } default: try { return (parent::__set($strName, $mixValue)); } catch (QCallerException $objExc) { $objExc->IncrementOffset(); throw $objExc; } } } /*************************************************************************************/ ///@todo - implement me: /** * Returns an account status report *@return string containing the status report */ public function GetAccountStatus() { throw new QCallerException(sprintf('FEDEXRequest: Shipping request type %s unsupported! ', ShippingRequestType::ToString($this->ShippingRequestType)) ); } /** * Returns whether this method is available for the order address *@return boolean true if method is available */ public function GetAvailability() { throw new QCallerException(sprintf('FEDEXRequest: Shipping request type %s unsupported! ', ShippingRequestType::ToString($this->ShippingRequestType)) ); } /** * Submits an account credit payment *@return boolean true on success */ public function CreditAccount() { throw new QCallerException(sprintf('FEDEXRequest: Shipping request type %s unsupported! ', ShippingRequestType::ToString($this->ShippingRequestType)) ); } //Request string creators /** * Creates a method available request */ protected function createAvailabilityRequest() { throw new QCallerException(sprintf('FEDEXRequest: Shipping request type %s unsupported! ', ShippingRequestType::ToString($this->ShippingRequestType)) ); } /** * Creates a label printing request */ protected function createLabelRequest() { throw new QCallerException(sprintf('FEDEXRequest: Shipping request type %s unsupported! ', ShippingRequestType::ToString($this->ShippingRequestType)) ); } /** * Creates a request submitting an account credit payment */ protected function createCreditAccountRequest() { throw new QCallerException(sprintf('FEDEXRequest: Shipping request type %s unsupported! ', ShippingRequestType::ToString($this->ShippingRequestType)) ); } /** * Creates an account status request */ protected function createAccountStatusRequest() { throw new QCallerException(sprintf('FEDEXRequest: Shipping request type %s unsupported! ', ShippingRequestType::ToString($this->ShippingRequestType)) ); } /** * Handles an account status request */ protected function handleAccountStatusResponse() { throw new QCallerException(sprintf('FEDEXRequest: Shipping request type %s unsupported! ', ShippingRequestType::ToString($this->ShippingRequestType)) ); } /** * Handles a request submitting an account credit payment */ protected function handleCreditAccountResponse() { throw new QCallerException(sprintf('FEDEXRequest: Shipping request type %s unsupported! ', ShippingRequestType::ToString($this->ShippingRequestType)) ); } /** * Handles a method available request */ protected function handleAvailabilityResponse() { throw new QCallerException(sprintf('FEDEXRequest: Shipping request type %s unsupported! ', ShippingRequestType::ToString($this->ShippingRequestType)) ); } /*************************************************************************************/ /*********************** Old XML POST version functions - DEPRECATED ********************************** These are left as examples and in case some would prefer to use them (as PHP SOAP is a bit buggy ..) public function GetRate() { $this->createRequest(ShippingRequestType::Rate, WebRequestType::POST); $this->submitRequest(); return $this->Rate; }*/ /** * Creates the POST string for the rate request - DEPRECATED */ protected function createRateRequest() { $this->strRequest = ''; $blnAddOriginStateAndZip = ( CountryType::UnitedStates == $this->OriginCountryId ||CountryType::Canada == $this->OriginCountryId ); $blnAddDestinationStateAndZip = ( CountryType::UnitedStates == $this->DestinationCountryId ||CountryType::Canada == $this->DestinationCountryId ); $str = $this->createXMLOpenTags('FDXRateRequest'); $str .= ''; // $str .= 'Express Rate'; $str .= '' . $this->strAccountNumber . ''; $str .= '' . $this->strMeterNumber . ''; $str .= '' . $this->Carrier . ''; $str .= ''; $str .= '' . $this->strDropoffType . ''; $str .= '' . $this->ServiceType . ''; $str .= ''.$this->strPackagingType . ''; $str .= ''. $this->strWeightUnits . ''; $str .= '' . number_format($this->Weight, 1) . ''; $str .= ''; if($blnAddOriginStateAndZip) { $str .= '' . $this->OriginStateCode . ''; $str .= '' . $this->OriginZip.''; } $str .= '' . $this->OriginCountryCode . ''; $str .= ''; $str .= ''; if($blnAddDestinationStateAndZip) { $str .= '' . $this->DestinationStateCode . ''; $str .= '' . $this->DestinationZip.''; } $str .= '' . $this->DestinationCountryCode . ''; $str .= ''; $str .= ''; $str .= '' . $this->strPayorType . ''; $str .= ''; $str .= '1'; $str .= ''; $this->strRequest = $str; } /** * Parses the rate request response from FEDEX server - XML POST VERSION: DEPRECATED. * @todo - handle errors more elegantly, make more robust .. */ /* protected function handleRateResponse() { $objDomDoc = $this->getDomDocument('FDXRateReply'); if($objDomDoc) { $strErrorMessage = $this->requestErrors($objDomDoc); if($strErrorMessage) { $this->blnHasErrors = true; $this->strErrors = $strErrorMessage; $this->Rate = 0; } else { $nodeList = $objDomDoc->getElementsByTagName('NetCharge'); if($nodeList->length > 0) $this->Rate = $nodeList->item(0)->nodeValue; else $this->Rate = 0; } } else { $this->HasErrors = true; $this->Errors = 'Unknown FEDEX error ..Request:' . $this->strRequest . ' Response:' . $this->strResponse; $this->Rate = 0; } //debugging: // if(!$this->Rate) // { // $this->Errors = 'Unknown FEDEX error ..Request:' . $this->strRequest . ' Response:' . $this->strResponse; // die($this->Errors); // } }*/ /** * Utility function to check for request errors - returns either a string containing * server error messages or false if there were none. *@param DOMDocument objDomDoc - the server response .. *@return string | boolean error messages or false if request succeeded. */ private function requestErrors($objDomDoc) { $mixToReturn = false; $nodeListErrors = $objDomDoc->getElementsByTagName('Error'); if( $nodeListErrors->length > 0 ) { $this->blnHasErrors = true; $mixToReturn = 'Request: ' . $this->strRequest; $nodeListErrorMessages = $objDomDoc->getElementsByTagName('Message'); if( $nodeListErrorMessages->length) $mixToReturn .= ' Message: ' . $nodeListErrorMessages->item(0)->nodeValue; } return $mixToReturn; } private function createXMLOpenTags($strApi) { $strToReturn = ''; $strToReturn .= sprintf('<%s xmlns:api="http://www.fedex.com/fsmapi" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="%s.xsd">', $strApi, $strApi); return $strToReturn; } }//end class }//end define ?>