Another interesting Job!!

The Job:

For many years, I have been downloading FedEx's rate charts into FoxPro tables and using this to compute the freight cost for an invoice in our accounting system.  The accounting system is written in FoxPro 6.0 and includes source code so this makes modifications very easy.  However, there is very little information on how to access FedEx's API from FoxPro.
The project is to send to FedEx everything necessary to compute the rate.  Source location, destination, box count, box weights, box sizes, declared value, COD and any other relevant information and get back either the list rate or my contractual rate, all from inside my FoxPro program in real time.

This blog only covers one of the many API functions FedEx is making available.  Getting the price is the one I wanted,  You might want to get quotes on all available services for the information you pass to it.  You can setup a shipment and print the labels,  Get address validations.  There is a list on page 1033 of the developers guide. 

My research into this started at FedEx's developer site.  www.fedex.com/developer You will need to sign up to download documentation and to get a sandbox account, password, meter for testing.

At the developer site, I found examples for programming languages are C#, Java, VB.net and PHP for the web programming.  It also talks about another technology called WSDL which I don't understand at all.  The best documents I found was a bunch of txt files of requests and responses for many of the possible rate service requests.  This gives a properly formatted xml document.  Rate service request starts on page 446.  It includes most if not all of the xml field names for options to pass to FedEx.

In essence, the process is to send an XML document to FedEx and parse the returning data stream.

This code should answer the questions: How do you do this in FoxPro and what needs to be in the XML request?  How do you parse the results?

Here is my code.  Replace some information as needed. 
SET TALK OFF
SET SAFETY OFF
malias=""
WAIT WINDOW "Contacting FedEx's servers for quote." NOWAIT
  mservice="FEDEX_GROUND"
  raddress1=""
  raddress2=""
  rcity="Chester"
  rstate="AK"
  rzip="99548"
  rcountrycode="US"
  mshipresid=.F.
  rcod=.f.
  mshipmentvalue=500
**Sometimes cod is on shipment, sometimes it is on each package, depending on the service  I am using the codpkg variable to control where the cod information has to be in the xml string
codpkg=.T.

 **    other services and cod method
 * mservice="GROUND_HOME_DELIVERY"
 *mservice="FEDEX_GROUND"
 *mservice="GROUND_HOME_DELIVERY"
 *mservice="PRIORITY_OVERNIGHT"
 * codpkg=.F.
 *mservice="STANDARD_OVERNIGHT"
 * codpkg=.F.
 *mservice="FEDEX_2_DAY"
 * codpkg=.F.
 *mservice="FEDEX_2_DAY_AM"
 * codpkg=.F.
 *mservice="FEDEX_EXPRESS_SAVER"
 * codpkg=.F.
 *mservice="FIRST_OVERNIGHT"
 * codpkg=.F.
 *mservice="INTERNATIONAL_ECONOMY"
 *mservice="INTERNATIONAL_PRIORITY"
 *mservice="INTERNATIONAL_FIRST"
 *mservice="FEDEX_GROUND"
**example shipment has two boxes
**create a box size and weight array
  DIME marray(2,4)
  marray(1,1)=20 &&WEIGHT
  marray(1,2)=18 &&LENGTH
  marray(1,3)=18 &&WIDTH
  marray(1,4)=24 &&DEPTH
  marray(2,1)=20 &&WEIGHT
  marray(2,2)=0 &&LENGTH
  marray(2,3)=0 &&WIDTH
  marray(2,4)=0 &&DEPTH
 mboxvalue=mshipmentvalue/ALEN(marray,1)
 

mtotalcnt=0
mtotalweight=0
FOR a=1 TO ALEN(marray,1)
  mtotalcnt=mtotalcnt+1
  mtotalweight=mtotalweight+marray(a,1)
ENDFOR


**this starts the xml request
strrequest =""
*strRequest =strRequest+'<?xml version="1.0" encoding="UTF-8"?>'
strrequest =strrequest+'<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" '
strrequest =strrequest+'xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
strrequest =strrequest+'xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://fedex.com/ws/rate/v20"> '
strrequest =strrequest+"<SOAP-ENV:Body>"

strrequest =strrequest+"<RateRequest>"
strrequest =strrequest+"<WebAuthenticationDetail>"
strrequest =strrequest+"<UserCredential>"
strrequest =strrequest+"<Key>your key</Key>"
strrequest =strrequest+"<Password>your password</Password>"
strrequest =strrequest+"</UserCredential>"
strrequest =strrequest+"</WebAuthenticationDetail>"
strrequest =strrequest+"<ClientDetail>"
strrequest =strrequest+"<AccountNumber>your account #</AccountNumber>"
strrequest =strrequest+"<MeterNumber>your meter number</MeterNumber>"
strrequest =strrequest+"</ClientDetail>"
strrequest =strrequest+"<TransactionDetail>"
strrequest =strrequest+"<CustomerTransactionId>Whatever description you want</CustomerTransactionId>"
strrequest =strrequest+"</TransactionDetail>"
strrequest =strrequest+"<Version>"
strrequest =strrequest+"<ServiceId>crs</ServiceId>" && this is for rate service
strrequest =strrequest+"<Major>20</Major>"
strrequest =strrequest+"<Intermediate>0</Intermediate>"
strrequest =strrequest+"<Minor>0</Minor>"
strrequest =strrequest+"</Version>"
mdate=STR(YEAR(DATE()),4)+"-"+STRTRAN(STR(MONTH(DATE()),2)," ","0")+"-"+STRTRAN(STR(DAY(DATE()),2)," ","0")

strrequest =strrequest+"<RequestedShipment>"
strrequest =strrequest+"<ShipTimestamp>"+mdate+"T12:34:56-06:00</ShipTimestamp>"
strrequest =strrequest+"<DropoffType>REGULAR_PICKUP</DropoffType>"
strrequest =strrequest+"<ServiceType>"+mservice+"</ServiceType>"
strrequest =strrequest+"<PackagingType>YOUR_PACKAGING</PackagingType>"
strrequest =strrequest+"<TotalWeight>"
strrequest =strrequest+"<Units>LB</Units>"
strrequest =strrequest+"<Value>"+ALLT(STR(mtotalweight,6,1))+"</Value>"
strrequest =strrequest+"</TotalWeight>"

strrequest =strrequest+"<Shipper>"
strrequest =strrequest+"<Contact>"
strrequest =strrequest+"<CompanyName>Input Your Information</CompanyName>"
strrequest =strrequest+"<PhoneNumber>Input Your Information</PhoneNumber>"
strrequest =strrequest+"</Contact>"
strrequest =strrequest+"<Address>"
strrequest =strrequest+"<StreetLines>Input Your Information</StreetLines>"
strrequest =strrequest+"<StreetLines>Input Your Information</StreetLines>"
strrequest =strrequest+"<City>Van Nuys</City>"
strrequest =strrequest+"<StateOrProvinceCode>CA</StateOrProvinceCode>"
strrequest =strrequest+"<PostalCode>91406</PostalCode>"
strrequest =strrequest+"<CountryCode>US</CountryCode>"
strrequest =strrequest+"</Address>"
strrequest =strrequest+"</Shipper>"

strrequest =strrequest+"<Recipient>"
strrequest =strrequest+"<Contact>"
strrequest =strrequest+"<PersonName>Input Your Information</PersonName>"
strrequest =strrequest+"<PhoneNumber>Input Your Information</PhoneNumber>"
strrequest =strrequest+"</Contact>"
strrequest =strrequest+"<Address>"
strrequest =strrequest+"<StreetLines>"+raddress1+"</StreetLines>"
strrequest =strrequest+"<StreetLines>"+raddress2+"</StreetLines>"
strrequest =strrequest+"<City>"+rcity+"</City>"
strrequest =strrequest+"<StateOrProvinceCode>"+rstate+"</StateOrProvinceCode>"
strrequest =strrequest+"<PostalCode>"+rzip+"</PostalCode>"
strrequest =strrequest+"<CountryCode>"+rcountry+"</CountryCode>"
IF mresidential
   strrequest =strrequest+"<Residential>1</Residential>"
ENDIF
strrequest =strrequest+"</Address>"
strrequest =strrequest+"</Recipient>"

strrequest =strrequest+"<ShippingChargesPayment>"
strrequest =strrequest+"<PaymentType>SENDER</PaymentType>"
strrequest =strrequest+"<Payor>"
strrequest =strrequest+"<ResponsibleParty>"
strrequest =strrequest+"<AccountNumber>your account number</AccountNumber>"
strrequest =strrequest+"</ResponsibleParty>"
strrequest =strrequest+"</Payor>"
strrequest =strrequest+"</ShippingChargesPayment>"
**Sometimes cod is on shipment, sometimes it is on each package, depending on the service
IF (rcod AND rcountry<>"US") AND NOT codpkg
   strrequest =strrequest+"<SpecialServicesRequested>"
   strrequest =strrequest+"<SpecialServiceTypes>COD</SpecialServiceTypes>"
   strrequest =strrequest+"<CodDetail>"
   strrequest =strrequest+"<CodCollectionAmount>"
   strrequest =strrequest+"<Currency>USD</Currency>"
   strrequest =strrequest+"<Amount>Enter cod amount</Amount>"
   strrequest =strrequest+"</CodCollectionAmount>"
   strrequest =strrequest+"<CollectionType>GUARANTEED_FUNDS</CollectionType>"
   strrequest =strrequest+"</CodDetail>"
   strrequest =strrequest+"</SpecialServicesRequested>"
ENDIF

**this is where the package level kicks in

strrequest =strrequest+"<RateRequestTypes>LIST</RateRequestTypes>"
strrequest =strrequest+"<PackageCount>"+ALLT(STR(ALEN(marray,1)))+"</PackageCount>"
FOR a=1 TO ALEN(marray,1)
  strrequest =strrequest+"<RequestedPackageLineItems>"
  strrequest =strrequest+"<SequenceNumber>1</SequenceNumber>"
  strrequest =strrequest+"<GroupNumber>1</GroupNumber>"
  strrequest =strrequest+"<GroupPackageCount>1</GroupPackageCount>"
  strrequest =strrequest+"<InsuredValue>"
  strrequest =strrequest+"<Currency>USD</Currency>"
  strrequest =strrequest+"<Amount>"+ALLT(STR(mboxvalue,8))+"</Amount>"
  strrequest =strrequest+"</InsuredValue>"

  strrequest =strrequest+"<Weight>"
  strrequest =strrequest+"<Units>LB</Units>"
  strrequest =strrequest+"<Value>" + ALLTRIM( STR(marray(a,1),5,1) )+"</Value>"
  strrequest =strrequest+"</Weight>"
  strrequest =strrequest+"<Dimensions>"
  strrequest =strrequest+"<Length>" + ALLTRIM(STR(marray(a,2),5,0))+"</Length>"
  strrequest =strrequest+"<Width>" + ALLTRIM(STR(marray(a,3),5,0))+"</Width>"
  strrequest =strrequest+"<Height>" + ALLTRIM(STR(marray(a,4),5,0))+"</Height>"
  strrequest =strrequest+"<Units>IN</Units>"

  strrequest =strrequest+"</Dimensions>"
 
 
  IF (rcod AND rcountry="US") AND codpkg
    strrequest =strrequest+"<SpecialServicesRequested>"
    strrequest =strrequest+"<SpecialServiceTypes>COD</SpecialServiceTypes>"
    strrequest =strrequest+"<CodDetail>"
    strrequest =strrequest+"<CodCollectionAmount>"
    strrequest =strrequest+"<Currency>USD</Currency>"
    strrequest =strrequest+"<Amount>500</Amount>"
    strrequest =strrequest+"</CodCollectionAmount>"
    strrequest =strrequest+"<CollectionType>GUARANTEED_FUNDS</CollectionType>"
    strrequest =strrequest+"</CodDetail>"
    strrequest =strrequest+"</SpecialServicesRequested>"
  ENDIF
  strrequest =strrequest+"</RequestedPackageLineItems>"
NEXT


strrequest =strrequest+"</RequestedShipment>"
strrequest =strrequest+"</RateRequest> "
strrequest =strrequest+"</SOAP-ENV:Body> "
strrequest =strrequest+"</SOAP-ENV:Envelope>"
**here is the sending of the xml to FedEx
 

xmlhttp = CREATEOBJECT("MSXML2.ServerXMLHTTP")
theurl="https://wsbeta.fedex.com:443/web-services"
WITH xmlhttp
  .OPEN("POST",theurl,"false")
  .SEND(strrequest)

***There are two ways to deal with the response,  as text and as XML.  I could not find things easily as XML so I used Text but if you can figure out the xml, un-rem the currresponse line
  * currresponse=.responsexml
  currtxt=.responsetext
ENDWITH
**the next step is to parse out the text stream.  I place the results into a table.

**parse the resulting text file into a dbf

intag=.F.
mvalue=""
SELECT 0
CREATE TABLE result (tag1 c(50), tag2 c(200), tag3 c(30))
mstat="start"
mlevel=0
FOR a=1 TO LEN(currtxt)
    DO CASE
    CASE SUBSTR(currtxt,a,1)="<"
      IF SUBSTR(currtxt,a,2)="</"
         mstat="end"
      ENDIF
      IF NOT EMPTY(mvalue)
         REPLACE tag2 WITH mvalue
      ENDIF
      mvalue=""
      intag=.T.
      mtag=SUBSTR(currtxt,a,1)
   CASE SUBSTR(currtxt,a,1)=">"
      mtag=mtag+SUBSTR(currtxt,a,1)
      intag=.F.
      IF mstat="end"
         REPLACE tag3 WITH mtag
         mlevel=mlevel-2
      ELSE
         APPEND BLANK
         REPLACE tag1 WITH SPACE(mlevel)+mtag
         mlevel=mlevel+2
      ENDIF
      mstat="start"
   CASE NOT intag
      mvalue=mvalue+SUBSTR(currtxt,a,1)
   OTHERWISE
      mtag=mtag+SUBSTR(currtxt,a,1)
   ENDCASE
NEXT
**at this point, all of the xml tags are in field tag1, the data is in tag2, the ending xml is in tag3 (which is not necessary)
**it's now a matter of searching for what you need.  To see the result at this point, pause the program and brows the data file.

**below is what I wanted.

myrate=0
listrate=0
mdimweight=0
LOCATE FOR ALLT(tag1)="<HighestSeverity>"
mresult=ALLT(tag2)
DO CASE
CASE mresult="NOTE"
   LOCATE FOR ALLT(tag1)="<Message>"
   mresult=mresult+": "+TRIM(tag2)
CASE mresult<>"SUCCESS"
   LOCATE FOR ALLT(tag1)="<Message>"
   mresult=mresult+": "+TRIM(tag2)
   WAIT CLEAR
   RETURN mresult+SPACE(12)+STR(myrate,12,2)+STR(listrate,12,2)
ENDCASE

LOCATE FOR ALLT(tag1)="<RatedShipmentDetails>"
DO WHILE NOT EOF()
   IF ALLT(tag1)="<TotalNetCharge>"
      SKIP
      SKIP
      myrate=VAL(tag2)
      EXIT
   ENDIF
   SKIP
ENDDO
LOCATE FOR ALLT(tag2)="PAYOR_LIST_PACKAGE"
SKIP -2
MFEDREC=RECNO()
SET FILTER TO RECNO()=>MFEDREC
LOCATE FOR ALLT(tag1)="<RatedShipmentDetails>"
DO WHILE NOT EOF()
   IF ALLT(tag1)="<TotalNetCharge>"
      SKIP
      SKIP
      listrate=VAL(tag2)
      EXIT
   ENDIF
   SKIP
ENDDO
**now get dim weight
LOCATE FOR ALLT(tag1)="<TotalBillingWeight>"
SKIP
SKIP
mdimweight=VAL(tag2)
**find surcharges
LOCATE FOR ALLT(tag2)="INSURED_VALUE"
IF NOT EOF()
   SKIP 4
   minsur=VAL(tag2)
   SKIP
   minsur=minsur+VAL(tag2)
ENDIF
LOCATE FOR ALLT(tag2)="FUEL"
IF NOT EOF()
   SKIP 4
   mfuel=VAL(tag2)
   SKIP
   mfuel=mfuel+VAL(tag2)
ENDIF
LOCATE FOR ALLT(tag2)="RESIDENTIAL_DELIVERY"
IF NOT EOF()
   SKIP 5
   mresdel=VAL(tag2)
ENDIF
LOCATE FOR ALLT(tag2)="DELIVERY_AREA"
IF NOT EOF()
   SKIP 4
   mdas=VAL(tag2)
   SKIP
   mdas=mdas+VAL(tag2)
ENDIF
LOCATE FOR ALLT(tag2)="OUT_OF_DELIVERY_AREA"
IF NOT EOF()
   SKIP 4
   mdas=mdas+VAL(tag2)
   SKIP
   mdas=mdas+VAL(tag2)
ENDIF
LOCATE FOR ALLT(tag2)="COD"
IF NOT EOF()
   SKIP 4
   mcodamt=VAL(tag2)
   SKIP
   mcodamt=mcodamt+VAL(tag2)
ENDIF
WAIT CLEAR
USE
ERASE result.dbf

RETURN mresult+STR(mdimweight,12,2)+STR(myrate,12,2)+STR(listrate,12,2)
*use the returning value however you want

 

Hit Counter