fedex 2.0.1 → 2.2.0

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 CHANGED
@@ -8,4 +8,7 @@ doc/*
8
8
  pkg/*
9
9
  .yardopts
10
10
  fedex_credentials.yml
11
- spec/vcr
11
+ spec/vcr
12
+ *.bbprojectd
13
+ .DS_Store
14
+ hacks
data/Readme.md CHANGED
@@ -116,7 +116,7 @@ Fedex provides multiple total values; `total_net_charge` is the final amount you
116
116
  @rate_zone="51">
117
117
  ```
118
118
 
119
- ### ** Generate a shipping label(PDF) **
119
+ ### ** Generate a shipping label (PDF) **
120
120
 
121
121
  To create a label for a shipment:
122
122
 
@@ -129,8 +129,84 @@ label = fedex.label(:filename => "my_dir/example.pdf",
129
129
  :shipping_details => shipping_details)
130
130
  ```
131
131
 
132
- The label will be saved to the file system as the filename you specify and is Adobe PDF format.
133
- Note that you can currently print a label for a single package at a time.
132
+ ### ** Generate a shipping label in any available format **
133
+
134
+ Change the filename extension and pass a label_specification hash. For example:
135
+
136
+ ```ruby
137
+ example_spec = {
138
+ :image_type => "EPL2",
139
+ :label_stock_type => "STOCK_4X6"
140
+ }
141
+
142
+ label = fedex.label(:filename => "my_dir/example_epl2.pcx",
143
+ :shipper=>shipper,
144
+ :recipient => recipient,
145
+ :packages => packages,
146
+ :service_type => "FEDEX_GROUND",
147
+ :shipping_details => shipping_details,
148
+ :label_specification => example_spec)
149
+ ```
150
+
151
+ ### ** Tracking a shipment **
152
+
153
+ To track a shipment:
154
+
155
+ ```ruby
156
+ tracking_info = fedex.track(:tracking_number => "1234567890123")
157
+
158
+ tracking_info.tracking_number
159
+ # => "1234567890123"
160
+
161
+ tracking_info.status
162
+ # => "Delivered"
163
+
164
+ tracking_info.events.first.description
165
+ # => "On FedEx vehicle for delivery"
166
+ ```
167
+
168
+ ### ** Tracking a shipment **
169
+
170
+ To track a shipment:
171
+
172
+ ```ruby
173
+ tracking_info = fedex.track(:tracking_number => "1234567890123")
174
+
175
+ tracking_info.tracking_number
176
+ # => "1234567890123"
177
+
178
+ tracking_info.status
179
+ # => "Delivered"
180
+
181
+ tracking_info.events.first.description
182
+ # => "On FedEx vehicle for delivery"
183
+ ```
184
+
185
+ ### ** Verifying an address **
186
+
187
+ To verify an address is valid and deliverable:
188
+
189
+ ```ruby
190
+
191
+ address = {
192
+ :street => "5 Elm Street",
193
+ :city => "Norwalk",
194
+ :state => "CT",
195
+ :postal_code => "06850",
196
+ :country => "USA"
197
+ }
198
+
199
+ address_result = fedex.validate_address(:address => address)
200
+
201
+ address_result.residential
202
+ # => true
203
+
204
+ address_result.score
205
+ # => 100
206
+
207
+ address_result.postal_code
208
+ # => "06850-3901"
209
+ ```
134
210
 
135
211
  # Services/Options Available
136
212
 
@@ -143,6 +219,8 @@ Fedex::Shipment::DROP_OFF_TYPES
143
219
  # Contributors:
144
220
  - [jazminschroeder](http://github.com/jazminschroeder) (Jazmin Schroeder)
145
221
  - [parndt](https://github.com/parndt) (Philip Arndt)
222
+ - [mmell](https://github.com/mmell) (Michael Mell)
223
+ - [jordanbyron](https://github.com/jordanbyron) (Jordan Byron)
146
224
 
147
225
  # Copyright/License:
148
226
  Copyright 2011 [Jazmin Schroeder](http://jazminschroeder.com)
data/fedex.gemspec CHANGED
@@ -9,8 +9,8 @@ Gem::Specification.new do |s|
9
9
  s.authors = ["Jazmin Schroeder"]
10
10
  s.email = ["jazminschroeder@gmail.com"]
11
11
  s.homepage = "https://github.com/jazminschroeder/fedex"
12
- s.summary = %q{Fedex Rate Webservice}
13
- s.description = %q{Ruby Library to use Fedex Web Services(version 10)}
12
+ s.summary = %q{Fedex Web Services}
13
+ s.description = %q{Provides an interface to Fedex Web Services(version 10) - shipping rates, generate labels and address validation}
14
14
 
15
15
  s.rubyforge_project = "fedex"
16
16
 
@@ -0,0 +1,30 @@
1
+ module Fedex
2
+ class Address
3
+
4
+ attr_reader :changes, :score, :confirmed, :available, :status, :residential,
5
+ :business, :street_lines, :city, :state, :province_code,
6
+ :postal_code, :country_code
7
+
8
+ def initialize(options)
9
+ @changes = options[:changes]
10
+ @score = options[:score].to_i
11
+ @confirmed = options[:delivery_point_validation] == "CONFIRMED"
12
+ @available = options[:delivery_point_validation] != "UNAVAILABLE"
13
+
14
+ @status = options[:residential_status]
15
+ @residential = status == "RESIDENTIAL"
16
+ @business = status == "BUSINESS"
17
+
18
+ address = options[:address]
19
+
20
+ @street_lines = address[:street_lines]
21
+ @city = address[:city]
22
+ @state = address[:state_or_province_code]
23
+ @province_code = address[:state_or_province_code]
24
+ @postal_code = address[:postal_code]
25
+ @country_code = address[:country_code]
26
+
27
+ @options = options
28
+ end
29
+ end
30
+ end
data/lib/fedex/helpers.rb CHANGED
@@ -2,9 +2,10 @@ module Fedex
2
2
  module Helpers
3
3
 
4
4
  private
5
- # String to CamelCase
6
- def camelize(str)
7
- str.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
5
+ # String or :symbol to CamelCase
6
+ def camelize(s)
7
+ # s.to_s.split('_').map { |e| e.capitalize }.join('')
8
+ s.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
8
9
  end
9
10
 
10
11
  # Helper method to validate required fields
@@ -16,4 +17,4 @@ module Fedex
16
17
  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
17
18
  end
18
19
  end
19
- end
20
+ end
data/lib/fedex/label.rb CHANGED
@@ -1,11 +1,52 @@
1
+ require 'base64'
2
+ require 'pathname'
3
+
1
4
  module Fedex
2
5
  class Label
3
- attr_accessor :options
6
+ attr_accessor :options, :image, :response_details
4
7
 
5
8
  # Initialize Fedex::Label Object
6
9
  # @param [Hash] options
7
- def initialize(options = {})
8
- @options = options
10
+ def initialize(label_details = {})
11
+ @response_details = label_details[:process_shipment_reply]
12
+ package_details = label_details[:process_shipment_reply][:completed_shipment_detail][:completed_package_details]
13
+ @options = package_details[:label]
14
+ @options[:format] = label_details[:format]
15
+ @options[:tracking_number] = package_details[:tracking_ids][:tracking_number]
16
+ @options[:file_name] = label_details[:file_name]
17
+
18
+ @image = Base64.decode64(options[:parts][:image]) if has_image?
19
+
20
+ if file_name = @options[:file_name]
21
+ save(file_name, false)
22
+ end
23
+ end
24
+
25
+ def name
26
+ [tracking_number, format].join('.')
27
+ end
28
+
29
+ def format
30
+ options[:format]
31
+ end
32
+
33
+ def tracking_number
34
+ options[:tracking_number]
35
+ end
36
+
37
+ def has_image?
38
+ options[:parts] && options[:parts][:image]
39
+ end
40
+
41
+ def save(path, append_name = true)
42
+ return unless has_image?
43
+
44
+ full_path = Pathname.new(path)
45
+ full_path = full_path.join(name) if append_name
46
+
47
+ File.open(full_path, 'wb') do|f|
48
+ f.write(@image)
49
+ end
9
50
  end
10
51
  end
11
52
  end
@@ -0,0 +1,92 @@
1
+ require 'fedex/request/base'
2
+ require 'fedex/address'
3
+ require 'fileutils'
4
+
5
+ module Fedex
6
+ module Request
7
+ class Address < Base
8
+ def initialize(credentials, options={})
9
+ requires!(options, :address)
10
+ @credentials = credentials
11
+ @address = options[:address]
12
+ end
13
+
14
+ def process_request
15
+ api_response = self.class.post(api_url, :body => build_xml)
16
+ puts api_response if @debug == true
17
+ response = parse_response(api_response)
18
+ if success?(response)
19
+ options = response[:address_validation_reply][:address_results][:proposed_address_details]
20
+
21
+ Fedex::Address.new(options)
22
+ else
23
+ error_message = if response[:address_validation_reply]
24
+ [response[:address_validation_reply][:notifications]].flatten.first[:message]
25
+ else
26
+ api_response["Fault"]["detail"]["fault"]["reason"]
27
+ end rescue $1
28
+ raise RateError, error_message
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ # Build xml Fedex Web Service request
35
+ def build_xml
36
+ builder = Nokogiri::XML::Builder.new do |xml|
37
+ xml.AddressValidationRequest(:xmlns => "http://fedex.com/ws/addressvalidation/v2"){
38
+ add_web_authentication_detail(xml)
39
+ add_client_detail(xml)
40
+ add_version(xml)
41
+ add_request_timestamp(xml)
42
+ add_address_validation_options(xml)
43
+ add_address_to_validate(xml)
44
+ }
45
+ end
46
+ builder.doc.root.to_xml
47
+ end
48
+
49
+ def add_request_timestamp(xml)
50
+ timestamp = Time.now
51
+
52
+ # Calculate current timezone offset manually.
53
+ # Ruby <= 1.9.2 does not support this in Time#strftime
54
+ #
55
+ utc_offest = "#{timestamp.gmt_offset < 0 ? "-" : "+"}%02d:%02d" %
56
+ (timestamp.gmt_offset / 60).abs.divmod(60)
57
+ timestamp = timestamp.strftime("%Y-%m-%dT%H:%M:%S") + utc_offest
58
+
59
+ xml.RequestTimestamp timestamp
60
+ end
61
+
62
+ def add_address_validation_options(xml)
63
+ xml.Options{
64
+ xml.CheckResidentialStatus true
65
+ }
66
+ end
67
+
68
+ def add_address_to_validate(xml)
69
+ xml.AddressesToValidate{
70
+ xml.Address{
71
+ xml.StreetLines @address[:street]
72
+ xml.City @address[:city]
73
+ xml.StateOrProvinceCode @address[:state]
74
+ xml.PostalCode @address[:postal_code]
75
+ xml.CountryCode @address[:country]
76
+ }
77
+ }
78
+ end
79
+
80
+ def service
81
+ { :id => 'aval', :version => 2 }
82
+ end
83
+
84
+ # Successful request
85
+ def success?(response)
86
+ response[:address_validation_reply] &&
87
+ %w{SUCCESS WARNING NOTE}.include?(response[:address_validation_reply][:highest_severity])
88
+ end
89
+
90
+ end
91
+ end
92
+ end
@@ -17,9 +17,6 @@ module Fedex
17
17
  # Fedex Production URL
18
18
  PRODUCTION_URL = "https://gateway.fedex.com:443/xml/"
19
19
 
20
- # Fedex Version number for the Fedex service used
21
- VERSION = 10
22
-
23
20
  # List of available Service Types
24
21
  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)
25
22
 
@@ -48,6 +45,7 @@ module Fedex
48
45
  requires!(options, :shipper, :recipient, :packages, :service_type)
49
46
  @credentials = credentials
50
47
  @shipper, @recipient, @packages, @service_type, @customs_clearance, @debug = options[:shipper], options[:recipient], options[:packages], options[:service_type], options[:customs_clearance], options[:debug]
48
+ @debug = ENV['DEBUG'] == 'true'
51
49
  @shipping_options = options[:shipping_options] ||={}
52
50
  end
53
51
 
@@ -73,14 +71,18 @@ module Fedex
73
71
  xml.ClientDetail{
74
72
  xml.AccountNumber @credentials.account_number
75
73
  xml.MeterNumber @credentials.meter
74
+ xml.Localization{
75
+ xml.LanguageCode 'en' # English
76
+ xml.LocaleCode 'us' # United States
77
+ }
76
78
  }
77
79
  end
78
80
 
79
81
  # Add Version to xml request, using the latest version V10 Sept/2011
80
82
  def add_version(xml)
81
83
  xml.Version{
82
- xml.ServiceId service_id
83
- xml.Major VERSION
84
+ xml.ServiceId service[:id]
85
+ xml.Major service[:version]
84
86
  xml.Intermediate 0
85
87
  xml.Minor 0
86
88
  }
@@ -164,12 +166,22 @@ module Fedex
164
166
  xml.Units package[:weight][:units]
165
167
  xml.Value package[:weight][:value]
166
168
  }
167
- xml.Dimensions{
168
- xml.Length package[:dimensions][:length]
169
- xml.Width package[:dimensions][:width]
170
- xml.Height package[:dimensions][:height]
171
- xml.Units package[:dimensions][:units]
172
- }
169
+ if package[:dimensions]
170
+ xml.Dimensions{
171
+ xml.Length package[:dimensions][:length]
172
+ xml.Width package[:dimensions][:width]
173
+ xml.Height package[:dimensions][:height]
174
+ xml.Units package[:dimensions][:units]
175
+ }
176
+ end
177
+ if package[:customer_refrences]
178
+ xml.CustomerReferences{
179
+ package[:customer_refrences].each do |value|
180
+ xml.CustomerReferenceType 'CUSTOMER_REFERENCE'
181
+ xml.Value value
182
+ end
183
+ }
184
+ end
173
185
  }
174
186
  end
175
187
  end
@@ -177,7 +189,7 @@ module Fedex
177
189
  # Add customs clearance(for international shipments)
178
190
  def add_customs_clearance(xml)
179
191
  xml.CustomsClearanceDetail{
180
- customs_to_xml(xml, @customs_clearance)
192
+ hash_to_xml(xml, @customs_clearance)
181
193
  }
182
194
  end
183
195
 
@@ -192,22 +204,22 @@ module Fedex
192
204
  raise NotImplementedError, "Override build_xml in subclass"
193
205
  end
194
206
 
195
- # Build nodes dinamically from the provided customs clearance hash
196
- def customs_to_xml(xml, hash)
207
+ # Build xml nodes dynamically from the hash keys and values
208
+ def hash_to_xml(xml, hash)
197
209
  hash.each do |key, value|
210
+ element = camelize(key)
198
211
  if value.is_a?(Hash)
199
- xml.send "#{camelize(key.to_s)}" do |x|
200
- customs_to_xml(x, value)
212
+ xml.send element do |x|
213
+ hash_to_xml(x, value)
201
214
  end
202
215
  elsif value.is_a?(Array)
203
- node = key
204
216
  value.each do |v|
205
- xml.send "#{camelize(node.to_s)}" do |x|
206
- customs_to_xml(x, v)
207
- end
208
- end
217
+ xml.send element do |x|
218
+ hash_to_xml(x, v)
219
+ end
220
+ end
209
221
  else
210
- xml.send "#{camelize(key.to_s)}", value unless key.is_a?(Hash)
222
+ xml.send element, value
211
223
  end
212
224
  end
213
225
  end
@@ -217,7 +229,7 @@ module Fedex
217
229
  response = sanitize_response_keys(response)
218
230
  end
219
231
 
220
- # Recursively sanitizes the response object by clenaing up any hash keys.
232
+ # Recursively sanitizes the response object by cleaning up any hash keys.
221
233
  def sanitize_response_keys(response)
222
234
  if response.is_a?(Hash)
223
235
  response.inject({}) { |result, (key, value)| result[underscorize(key).to_sym] = sanitize_response_keys(value); result }
@@ -228,8 +240,9 @@ module Fedex
228
240
  end
229
241
  end
230
242
 
231
- def service_id
232
- 'crs'
243
+ def service
244
+ raise NotImplementedError,
245
+ "Override service in subclass: {:id => 'service', :version => 1}"
233
246
  end
234
247
 
235
248
  # Use GROUND_HOME_DELIVERY for shipments going to a residential address within the US.
@@ -248,4 +261,4 @@ module Fedex
248
261
 
249
262
  end
250
263
  end
251
- end
264
+ end