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