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.
@@ -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
@@ -1,285 +1,39 @@
1
- require 'httparty'
2
- require 'nokogiri'
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
- include HTTParty
7
- format :xml
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
- requires!(options, :key, :password, :account_number, :meter, :mode)
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
- def rate(options = {})
58
- requires!(options, :shipper, :recipient, :packages, :service_type)
59
- @shipper, @recipient, @packages, @service_type, @customs_clearance, @debug = options[:shipper], options[:recipient], options[:packages], options[:service_type], options[:customs_clearance], options[:debug]
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
- # Recursively sanitizes the response object by clenaing up any hash keys.
251
- def sanitize_response_keys(response)
252
- if response.is_a?(Hash)
253
- response.inject({}) { |result, (key, value)| result[underscorize(key).to_sym] = sanitize_response_keys(value); result }
254
- elsif response.is_a?(Array)
255
- response.collect { |result| sanitize_response_keys(result) }
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