fedex 1.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +1 -0
- data/Readme.md +134 -76
- data/fedex.gemspec +6 -2
- data/lib/fedex.rb +35 -34
- data/lib/fedex/credentials.rb +26 -0
- data/lib/fedex/helpers.rb +19 -0
- data/lib/fedex/label.rb +11 -0
- data/lib/fedex/rate.rb +6 -6
- data/lib/fedex/request/base.rb +251 -0
- data/lib/fedex/request/label.rb +94 -0
- data/lib/fedex/request/rate.rb +66 -0
- data/lib/fedex/shipment.rb +19 -265
- data/lib/fedex/version.rb +1 -1
- data/spec/lib/fedex/label_spec.rb +54 -0
- data/spec/lib/fedex/shipment_spec.rb +149 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/support/vcr.rb +15 -0
- metadata +50 -18
- data/spec/fedex_spec.rb +0 -136
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'fedex/request/base'
|
2
|
+
require 'fedex/label'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module Fedex
|
6
|
+
module Request
|
7
|
+
class Label < Base
|
8
|
+
def initialize(credentials, options={})
|
9
|
+
super(credentials, options)
|
10
|
+
requires!(options, :filename)
|
11
|
+
@filename = options[:filename]
|
12
|
+
end
|
13
|
+
|
14
|
+
# Sends post request to Fedex web service and parse the response.
|
15
|
+
# A Fedex::Label object is created if the response is successful and
|
16
|
+
# a PDF file is created with the label at the specified location.
|
17
|
+
def process_request
|
18
|
+
api_response = self.class.post(api_url, :body => build_xml)
|
19
|
+
puts api_response if @debug == true
|
20
|
+
response = parse_response(api_response)
|
21
|
+
if success?(response)
|
22
|
+
label_details = response[:process_shipment_reply][:completed_shipment_detail][:completed_package_details][:label]
|
23
|
+
|
24
|
+
create_pdf(label_details)
|
25
|
+
Fedex::Label.new(label_details)
|
26
|
+
else
|
27
|
+
error_message = if response[:process_shipment_reply]
|
28
|
+
[response[:process_shipment_reply][:notifications]].flatten.first[:message]
|
29
|
+
else
|
30
|
+
api_response["Fault"]["detail"]["fault"]["reason"]
|
31
|
+
end rescue $1
|
32
|
+
raise RateError, error_message
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# Add information for shipments
|
39
|
+
def add_requested_shipment(xml)
|
40
|
+
xml.RequestedShipment{
|
41
|
+
xml.ShipTimestamp Time.now.utc.iso8601(2)
|
42
|
+
xml.DropoffType @shipping_options[:drop_off_type] ||= "REGULAR_PICKUP"
|
43
|
+
xml.ServiceType service_type
|
44
|
+
xml.PackagingType @shipping_options[:packaging_type] ||= "YOUR_PACKAGING"
|
45
|
+
add_shipper(xml)
|
46
|
+
add_recipient(xml)
|
47
|
+
add_shipping_charges_payment(xml)
|
48
|
+
add_customs_clearance(xml) if @customs_clearance
|
49
|
+
xml.LabelSpecification {
|
50
|
+
xml.LabelFormatType "COMMON2D"
|
51
|
+
xml.ImageType "PDF"
|
52
|
+
}
|
53
|
+
xml.RateRequestTypes "ACCOUNT"
|
54
|
+
add_packages(xml)
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
# Build xml Fedex Web Service request
|
59
|
+
def build_xml
|
60
|
+
builder = Nokogiri::XML::Builder.new do |xml|
|
61
|
+
xml.ProcessShipmentRequest(:xmlns => "http://fedex.com/ws/ship/v10"){
|
62
|
+
add_web_authentication_detail(xml)
|
63
|
+
add_client_detail(xml)
|
64
|
+
add_version(xml)
|
65
|
+
add_requested_shipment(xml)
|
66
|
+
}
|
67
|
+
end
|
68
|
+
builder.doc.root.to_xml
|
69
|
+
end
|
70
|
+
|
71
|
+
def create_pdf(label_details)
|
72
|
+
[label_details[:parts]].flatten.each do |part|
|
73
|
+
if image = (Base64.decode64(part[:image]) if part[:image])
|
74
|
+
FileUtils.mkdir_p File.dirname(@filename)
|
75
|
+
File.open(@filename, 'w') do |file|
|
76
|
+
file.write image
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def service_id
|
83
|
+
'ship'
|
84
|
+
end
|
85
|
+
|
86
|
+
# Successful request
|
87
|
+
def success?(response)
|
88
|
+
response[:process_shipment_reply] &&
|
89
|
+
%w{SUCCESS WARNING NOTE}.include?(response[:process_shipment_reply][:highest_severity])
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'fedex/request/base'
|
2
|
+
|
3
|
+
module Fedex
|
4
|
+
module Request
|
5
|
+
class Rate < Base
|
6
|
+
# Sends post request to Fedex web service and parse the response, a Rate object is created if the response is successful
|
7
|
+
def process_request
|
8
|
+
api_response = self.class.post(api_url, :body => build_xml)
|
9
|
+
puts api_response if @debug == true
|
10
|
+
response = parse_response(api_response)
|
11
|
+
if success?(response)
|
12
|
+
rate_details = [response[:rate_reply][:rate_reply_details][:rated_shipment_details]].flatten.first[:shipment_rate_detail]
|
13
|
+
Fedex::Rate.new(rate_details)
|
14
|
+
else
|
15
|
+
error_message = if response[:rate_reply]
|
16
|
+
[response[:rate_reply][:notifications]].flatten.first[:message]
|
17
|
+
else
|
18
|
+
api_response["Fault"]["detail"]["fault"]["reason"]
|
19
|
+
end rescue $1
|
20
|
+
raise RateError, error_message
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Add information for shipments
|
27
|
+
def add_requested_shipment(xml)
|
28
|
+
xml.RequestedShipment{
|
29
|
+
xml.DropoffType @shipping_options[:drop_off_type] ||= "REGULAR_PICKUP"
|
30
|
+
xml.ServiceType service_type
|
31
|
+
xml.PackagingType @shipping_options[:packaging_type] ||= "YOUR_PACKAGING"
|
32
|
+
add_shipper(xml)
|
33
|
+
add_recipient(xml)
|
34
|
+
add_shipping_charges_payment(xml)
|
35
|
+
add_customs_clearance(xml) if @customs_clearance
|
36
|
+
xml.RateRequestTypes "ACCOUNT"
|
37
|
+
add_packages(xml)
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
# Build xml Fedex Web Service request
|
42
|
+
def build_xml
|
43
|
+
builder = Nokogiri::XML::Builder.new do |xml|
|
44
|
+
xml.RateRequest(:xmlns => "http://fedex.com/ws/rate/v10"){
|
45
|
+
add_web_authentication_detail(xml)
|
46
|
+
add_client_detail(xml)
|
47
|
+
add_version(xml)
|
48
|
+
add_requested_shipment(xml)
|
49
|
+
}
|
50
|
+
end
|
51
|
+
builder.doc.root.to_xml
|
52
|
+
end
|
53
|
+
|
54
|
+
def service_id
|
55
|
+
'crs'
|
56
|
+
end
|
57
|
+
|
58
|
+
# Successful request
|
59
|
+
def success?(response)
|
60
|
+
response[:rate_reply] &&
|
61
|
+
%w{SUCCESS WARNING NOTE}.include?(response[:rate_reply][:highest_severity])
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/fedex/shipment.rb
CHANGED
@@ -1,285 +1,39 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
1
|
+
require 'fedex/credentials'
|
2
|
+
require 'fedex/request/label'
|
3
|
+
require 'fedex/request/rate'
|
4
|
+
|
3
5
|
module Fedex
|
4
|
-
#Fedex::Shipment
|
5
6
|
class Shipment
|
6
|
-
|
7
|
-
|
8
|
-
# If true the rate method will return the complete response from the Fedex Web Service
|
9
|
-
attr_accessor :debug
|
10
|
-
# Fedex Text URL
|
11
|
-
TEST_URL = "https://gatewaybeta.fedex.com:443/xml/"
|
12
|
-
|
13
|
-
# Fedex Production URL
|
14
|
-
PRODUCTION_URL = "https://gateway.fedex.com:443/xml/"
|
15
|
-
|
16
|
-
# Fedex Version number for the Fedex service used
|
17
|
-
VERSION = 10
|
18
|
-
|
19
|
-
# List of available Service Types
|
20
|
-
SERVICE_TYPES = %w(EUROPE_FIRST_INTERNATIONAL_PRIORITY FEDEX_1_DAY_FREIGHT FEDEX_2_DAY FEDEX_2_DAY_AM FEDEX_2_DAY_FREIGHT FEDEX_3_DAY_FREIGHT FEDEX_EXPRESS_SAVER FEDEX_FIRST_FREIGHT FEDEX_FREIGHT_ECONOMY FEDEX_FREIGHT_PRIORITY FEDEX_GROUND FIRST_OVERNIGHT GROUND_HOME_DELIVERY INTERNATIONAL_ECONOMY INTERNATIONAL_ECONOMY_FREIGHT INTERNATIONAL_FIRST INTERNATIONAL_PRIORITY INTERNATIONAL_PRIORITY_FREIGHT PRIORITY_OVERNIGHT SMART_POST STANDARD_OVERNIGHT)
|
21
|
-
|
22
|
-
# List of available Packaging Type
|
23
|
-
PACKAGING_TYPES = %w(FEDEX_10KG_BOX FEDEX_25KG_BOX FEDEX_BOX FEDEX_ENVELOPE FEDEX_PAK FEDEX_TUBE YOUR_PACKAGING)
|
24
|
-
|
25
|
-
# List of available DropOffTypes
|
26
|
-
DROP_OFF_TYPES = %w(BUSINESS_SERVICE_CENTER DROP_BOX REGULAR_PICKUP REQUEST_COURIER STATION)
|
27
|
-
|
28
|
-
# Clearance Brokerage Type
|
29
|
-
CLEARANCE_BROKERAGE_TYPE = %w(BROKER_INCLUSIVE BROKER_INCLUSIVE_NON_RESIDENT_IMPORTER BROKER_SELECT BROKER_SELECT_NON_RESIDENT_IMPORTER BROKER_UNASSIGNED)
|
30
|
-
|
31
|
-
# Recipient Custom ID Type
|
32
|
-
RECIPIENT_CUSTOM_ID_TYPE = %w(COMPANY INDIVIDUAL PASSPORT)
|
33
|
-
|
34
|
-
# In order to use Fedex rates API you must first apply for a developer(and later production keys),
|
7
|
+
|
8
|
+
# In order to use Fedex rates API you must first apply for a developer(and later production keys),
|
35
9
|
# Visit {http://www.fedex.com/us/developer/ Fedex Developer Center} for more information about how to obtain your keys.
|
36
10
|
# @param [String] key - Fedex web service key
|
37
11
|
# @param [String] password - Fedex password
|
38
12
|
# @param [String] account_number - Fedex account_number
|
39
|
-
# @param [String] meter - Fedex meter number
|
13
|
+
# @param [String] meter - Fedex meter number
|
40
14
|
# @param [String] mode - [development/production]
|
41
|
-
#
|
15
|
+
#
|
42
16
|
# return a Fedex::Shipment object
|
43
17
|
def initialize(options={})
|
44
|
-
|
45
|
-
@key = options[:key]
|
46
|
-
@password = options[:password]
|
47
|
-
@account_number = options[:account_number]
|
48
|
-
@meter = options[:meter]
|
49
|
-
@mode = options[:mode]
|
18
|
+
@credentials = Credentials.new(options)
|
50
19
|
end
|
51
|
-
|
52
|
-
|
20
|
+
|
53
21
|
# @param [Hash] shipper, A hash containing the shipper information
|
54
22
|
# @param [Hash] recipient, A hash containing the recipient information
|
55
23
|
# @param [Array] packages, An arrary including a hash for each package being shipped
|
56
24
|
# @param [String] service_type, A valid fedex service type, to view a complete list of services Fedex::Shipment::SERVICE_TYPES
|
57
|
-
|
58
|
-
|
59
|
-
@
|
60
|
-
@shipping_options = options[:shipping_options] ||={}
|
61
|
-
process_request
|
62
|
-
end
|
63
|
-
|
64
|
-
# Sends post request to Fedex web service and parse the response, a Rate object is created if the response is successful
|
65
|
-
def process_request
|
66
|
-
api_response = Shipment.post(api_url, :body => build_xml)
|
67
|
-
puts api_response if @debug == true
|
68
|
-
response = parse_response(api_response)
|
69
|
-
if success?(response)
|
70
|
-
rate_details = [response[:rate_reply][:rate_reply_details][:rated_shipment_details]].flatten.first[:shipment_rate_detail]
|
71
|
-
rate = Fedex::Rate.new(rate_details)
|
72
|
-
else
|
73
|
-
error_message = (response[:rate_reply].nil? ? api_response["Fault"]["detail"]["fault"]["reason"] : [response[:rate_reply][:notifications]].flatten.first[:message]) rescue $1
|
74
|
-
raise RateError, error_message
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
# Build xml Fedex Web Service request
|
79
|
-
def build_xml
|
80
|
-
builder = Nokogiri::XML::Builder.new do |xml|
|
81
|
-
xml.RateRequest(:xmlns => "http://fedex.com/ws/rate/v10"){
|
82
|
-
add_web_authentication_detail(xml)
|
83
|
-
add_client_detail(xml)
|
84
|
-
add_version(xml)
|
85
|
-
add_requested_shipment(xml)
|
86
|
-
}
|
87
|
-
end
|
88
|
-
builder.doc.root.to_xml
|
89
|
-
end
|
90
|
-
|
91
|
-
# Fedex Web Service Api
|
92
|
-
def api_url
|
93
|
-
@mode == "production" ? PRODUCTION_URL : TEST_URL
|
94
|
-
end
|
95
|
-
|
96
|
-
private
|
97
|
-
# Helper method to validate required fields
|
98
|
-
def requires!(hash, *params)
|
99
|
-
params.each { |param| raise RateError, "Missing Required Parameter #{param}" if hash[param].nil? }
|
100
|
-
end
|
101
|
-
|
102
|
-
def camelize(str) #:nodoc:
|
103
|
-
str.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
|
104
|
-
end
|
105
|
-
|
106
|
-
# Add web authentication detail information(key and password) to xml request
|
107
|
-
def add_web_authentication_detail(xml)
|
108
|
-
xml.WebAuthenticationDetail{
|
109
|
-
xml.UserCredential{
|
110
|
-
xml.Key @key
|
111
|
-
xml.Password @password
|
112
|
-
}
|
113
|
-
}
|
114
|
-
end
|
115
|
-
|
116
|
-
# Add Client Detail information(account_number and meter_number) to xml request
|
117
|
-
def add_client_detail(xml)
|
118
|
-
xml.ClientDetail{
|
119
|
-
xml.AccountNumber @account_number
|
120
|
-
xml.MeterNumber @meter
|
121
|
-
}
|
122
|
-
end
|
123
|
-
|
124
|
-
# Add Version to xml request, using the latest version V10 Sept/2011
|
125
|
-
def add_version(xml)
|
126
|
-
xml.Version{
|
127
|
-
xml.ServiceId 'crs'
|
128
|
-
xml.Major VERSION
|
129
|
-
xml.Intermediate 0
|
130
|
-
xml.Minor 0
|
131
|
-
}
|
132
|
-
end
|
133
|
-
|
134
|
-
# Add information for shipments
|
135
|
-
def add_requested_shipment(xml)
|
136
|
-
xml.RequestedShipment{
|
137
|
-
xml.DropoffType @shipping_options[:drop_off_type] ||= "REGULAR_PICKUP"
|
138
|
-
xml.ServiceType service_type
|
139
|
-
xml.PackagingType @shipping_options[:packaging_type] ||= "YOUR_PACKAGING"
|
140
|
-
add_shipper(xml)
|
141
|
-
add_recipient(xml)
|
142
|
-
add_shipping_charges_payment(xml)
|
143
|
-
add_customs_clearance(xml) if @customs_clearance
|
144
|
-
xml.RateRequestTypes "ACCOUNT"
|
145
|
-
add_packages(xml)
|
146
|
-
}
|
147
|
-
end
|
148
|
-
|
149
|
-
# Add shipper to xml request
|
150
|
-
def add_shipper(xml)
|
151
|
-
xml.Shipper{
|
152
|
-
xml.Contact{
|
153
|
-
xml.PersonName @shipper[:name]
|
154
|
-
xml.CompanyName @shipper[:company]
|
155
|
-
xml.PhoneNumber @shipper[:phone_number]
|
156
|
-
}
|
157
|
-
xml.Address {
|
158
|
-
xml.StreetLines @shipper[:address]
|
159
|
-
xml.City @shipper[:city]
|
160
|
-
xml.StateOrProvinceCode @shipper[:state]
|
161
|
-
xml.PostalCode @shipper[:postal_code]
|
162
|
-
xml.CountryCode @shipper[:country_code]
|
163
|
-
}
|
164
|
-
}
|
165
|
-
end
|
166
|
-
|
167
|
-
# Add recipient to xml request
|
168
|
-
def add_recipient(xml)
|
169
|
-
xml.Recipient{
|
170
|
-
xml.Contact{
|
171
|
-
xml.PersonName @recipient[:name]
|
172
|
-
xml.CompanyName @recipient[:company]
|
173
|
-
xml.PhoneNumber @recipient[:phone_number]
|
174
|
-
}
|
175
|
-
xml.Address {
|
176
|
-
xml.StreetLines @recipient[:address]
|
177
|
-
xml.City @recipient[:city]
|
178
|
-
xml.StateOrProvinceCode @recipient[:state]
|
179
|
-
xml.PostalCode @recipient[:postal_code]
|
180
|
-
xml.CountryCode @recipient[:country_code]
|
181
|
-
xml.Residential @recipient[:residential]
|
182
|
-
}
|
183
|
-
}
|
184
|
-
end
|
185
|
-
|
186
|
-
# Add shipping charges to xml request
|
187
|
-
def add_shipping_charges_payment(xml)
|
188
|
-
xml.ShippingChargesPayment{
|
189
|
-
xml.PaymentType "SENDER"
|
190
|
-
xml.Payor{
|
191
|
-
xml.AccountNumber @account_number
|
192
|
-
xml.CountryCode @shipper[:country_code]
|
193
|
-
}
|
194
|
-
}
|
195
|
-
end
|
196
|
-
|
197
|
-
# Add packages to xml request
|
198
|
-
def add_packages(xml)
|
199
|
-
package_count = @packages.size
|
200
|
-
xml.PackageCount package_count
|
201
|
-
@packages.each do |package|
|
202
|
-
xml.RequestedPackageLineItems{
|
203
|
-
xml.GroupPackageCount 1
|
204
|
-
xml.Weight{
|
205
|
-
xml.Units package[:weight][:units]
|
206
|
-
xml.Value package[:weight][:value]
|
207
|
-
}
|
208
|
-
xml.Dimensions{
|
209
|
-
xml.Length package[:dimensions][:length]
|
210
|
-
xml.Width package[:dimensions][:width]
|
211
|
-
xml.Height package[:dimensions][:height]
|
212
|
-
xml.Units package[:dimensions][:units]
|
213
|
-
}
|
214
|
-
}
|
215
|
-
end
|
216
|
-
end
|
217
|
-
|
218
|
-
# Add customs clearance(for international shipments)
|
219
|
-
def add_customs_clearance(xml)
|
220
|
-
xml.CustomsClearanceDetail{
|
221
|
-
customs_to_xml(xml, @customs_clearance)
|
222
|
-
}
|
223
|
-
end
|
224
|
-
|
225
|
-
# Build nodes dinamically from the provided customs clearance hash
|
226
|
-
def customs_to_xml(xml, hash)
|
227
|
-
hash.each do |key, value|
|
228
|
-
if value.is_a?(Hash)
|
229
|
-
xml.send "#{camelize(key.to_s)}" do |x|
|
230
|
-
customs_to_xml(x, value)
|
231
|
-
end
|
232
|
-
elsif value.is_a?(Array)
|
233
|
-
node = key
|
234
|
-
value.each do |v|
|
235
|
-
xml.send "#{camelize(node.to_s)}" do |x|
|
236
|
-
customs_to_xml(x, v)
|
237
|
-
end
|
238
|
-
end
|
239
|
-
else
|
240
|
-
xml.send "#{camelize(key.to_s)}", value unless key.is_a?(Hash)
|
241
|
-
end
|
242
|
-
end
|
243
|
-
end
|
244
|
-
|
245
|
-
# Parse response, convert keys to underscore symbols
|
246
|
-
def parse_response(response)
|
247
|
-
response = sanitize_response_keys(response)
|
25
|
+
# @param [String] filename, A location where the label will be saved
|
26
|
+
def label(options = {})
|
27
|
+
Request::Label.new(@credentials, options).process_request
|
248
28
|
end
|
249
29
|
|
250
|
-
#
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
else
|
257
|
-
response
|
258
|
-
end
|
30
|
+
# @param [Hash] shipper, A hash containing the shipper information
|
31
|
+
# @param [Hash] recipient, A hash containing the recipient information
|
32
|
+
# @param [Array] packages, An arrary including a hash for each package being shipped
|
33
|
+
# @param [String] service_type, A valid fedex service type, to view a complete list of services Fedex::Shipment::SERVICE_TYPES
|
34
|
+
def rate(options = {})
|
35
|
+
Request::Rate.new(@credentials, options).process_request
|
259
36
|
end
|
260
37
|
|
261
|
-
def underscorize(key) #:nodoc:
|
262
|
-
key.to_s.sub(/^(v[0-9]+|ns):/, "").gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').downcase
|
263
|
-
end
|
264
|
-
|
265
|
-
# Successful request
|
266
|
-
def success?(response)
|
267
|
-
(!response[:rate_reply].nil? and %w{SUCCESS WARNING NOTE}.include? response[:rate_reply][:highest_severity])
|
268
|
-
end
|
269
|
-
|
270
|
-
# Use GROUND_HOME_DELIVERY for shipments going to a residential address within the US.
|
271
|
-
def service_type
|
272
|
-
if @recipient[:residential].to_s =~ /true/i and @service_type =~ /GROUND/i and @recipient[:country_code] =~ /US/i
|
273
|
-
"GROUND_HOME_DELIVERY"
|
274
|
-
else
|
275
|
-
@service_type
|
276
|
-
end
|
277
|
-
end
|
278
|
-
|
279
|
-
# String to CamelCase
|
280
|
-
def camelize(str)
|
281
|
-
str.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
|
282
|
-
end
|
283
|
-
|
284
38
|
end
|
285
39
|
end
|